From 7964aa9f5347baac1084a14e32e0506503fd1f69 Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Mon, 6 Jun 2022 11:20:30 -0500 Subject: [PATCH 1/3] integrated iforce2d.net mixercalc (#447) * integrated iforce2d mixercalc * mixercalc * Codacy and Codacy-related cleanup * mixercalc * Codacy and Codacy-related cleanup * mixercalc * Codacy and Codacy-related cleanup * mixercalc fix hover over commandoutput * mixercalc mouse-over/copybox verbiage * mixercalc prop direction verbiage * mixercalc prop direction verbiage * mixercalc * disable hover capability * minimize copy-verbiage --- locales/en/messages.json | 4 +- resources/mixercalc/cog.png | Bin 0 -> 4841 bytes resources/mixercalc/emu-prop-ccw.png | Bin 0 -> 7446 bytes resources/mixercalc/emu-prop-cw.png | Bin 0 -> 7959 bytes src/css/tabs/mixercalc.css | 80 +++ src/js/gui.js | 3 +- src/js/main.js | 5 + src/js/tabs/mixercalc.js | 989 +++++++++++++++++++++++++++ src/main.html | 14 +- src/tabs/mixercalc.html | 105 +++ 10 files changed, 1190 insertions(+), 10 deletions(-) create mode 100644 resources/mixercalc/cog.png create mode 100644 resources/mixercalc/emu-prop-ccw.png create mode 100644 resources/mixercalc/emu-prop-cw.png create mode 100644 src/css/tabs/mixercalc.css create mode 100644 src/js/tabs/mixercalc.js create mode 100644 src/tabs/mixercalc.html diff --git a/locales/en/messages.json b/locales/en/messages.json index 2d50f3d03..a570678c8 100644 --- a/locales/en/messages.json +++ b/locales/en/messages.json @@ -230,10 +230,12 @@ "configMigrationSuccessful": { "message": "Configuration migration complete, migrations applied: $1" }, - "tabFirmwareFlasher": { "message": "Firmware Flasher" }, + "tabMixerCalc": { + "message": "Motor Mix Calc" + }, "tabLanding": { "message": "Welcome" }, diff --git a/resources/mixercalc/cog.png b/resources/mixercalc/cog.png new file mode 100644 index 0000000000000000000000000000000000000000..9c8ec0a179bd449dff7492598f889574febf8616 GIT binary patch literal 4841 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 literal 0 HcmV?d00001 diff --git a/resources/mixercalc/emu-prop-ccw.png b/resources/mixercalc/emu-prop-ccw.png new file mode 100644 index 0000000000000000000000000000000000000000..48b441a0fc51f8e88e62896a1f4234ec9c6f6bde GIT binary patch literal 7446 zcmcgx7aC89wemFN@)|^)KMsyj6P* z@B%CXp7O+cB>y1}%0KgQc{#wx>z~XlyK{}Ih;An4Ae&HA6 z|1@gmVdI?wmIFB_ShkGGr4y>8!f}*P0x;>2f~TH>A)%(;1{FCl%k4c-uT6@0h-xEg zi8P`AO`BrQ$y+4M))w7qH0rGL`|H${_TE$Ys>iO@NpgANa#Ks!x_;}s&dSQPlZU&f z(39YXF1e0-)7eind5%Ow+(A*Da;*Tt5-UDDv8D*PA!YL~RcZlP2{#S6(b>jm6#=gx zo*Tj>G}8K7&1l$|6CeSFmP(P?dMZ~kO>7X zffB3vq1sMeoVWCd1RSc`f;>ZsYAZgh|EDAhepbU`h53 zE*SNNi&Twveg*RalpLoPdsxx?SMmh)5|oo&*}M2$yDsC%Q}f+}y4{`&{qlM%>E`6M zr>qXrel!@G&A-sY?10{QR4X;`0}DzHfLoKz1bmo^atHc*;t$?IxyUdZ<;RcPy+y9` zPFdRPOVj;Zsd76)T`G*hJ>2hUQI7J??-YA_(pz1wL;7|ti+8IPgtRf=%6()~#C z5bEA8>f)DyiZaQe#`O@ANh$gx0_X=_@IXw$bH4J5jk5<;_2-gA-||HodyNaehY?VB zVI7|ekC(rUA1B>T_sbq^2LPiY<`h5OujC_tMyH{xn?X^NGIob=i=Hl?6bmhAr2r=x z>ZOFudEhu4uRJa}^YuvM0{)k~A##YHgFk|I5^)}3KXNL)sL)>{$$>0XPu`DK6FV#) z_9{xPdPm0}=`fzYP&6>U;Bj1i*er~z-!YBw5*RQ+wMpSifpRIQyHt%VJ#iU~F3*6n zKX&EgkA8qei1VNx`WnOt@}_{|ujXf7S$1jChgm&$ME#e3zlQGQ$M(tTli7m4TeeEMRt}i*(=S7FV&|NHy(sd z{H`2!ZkltH>uk8v%lrFJ%5zF2c*z2M^^6Xd)1#>_*&e|a|x@-IKUHgh*l{ps>|7VRe(83 zXt1qqfSKhdYhrITlYo0=ow?m~TQ>6Var<YHXfKDPeYz|^8YtXV{(`OLznIZm?ma)To2{EhXGpaa$76O7I7AucsQ5@FnCtPz+}kY13tAD}uzIpTp(^4--W8^~E7+y29eS;2 z+s?y6eh?HzNTbQ!>6y)!>}0Njr1i?sh@q93X4SoxHr)tMOS-^tMHk02Z4IFYnszUR z2ds&JOjYrlDk33P)TB#;1lSSu6>YD+*euc=%};YZ`%#!$pU4mR9rg_#=pJgc%Pez+ z;2wOh*s}kNk+Jm7nAIettNVPSX0zZsY&+C5@C|c0IqF*~_ltIgXWTL-I^+-NdF8xX zxEB_Q*7WA5ZVC{&6j2s|hUOw+Z3&@+&=Ohe0N3s%+s!LOWWGHm))4dH(nNi#5E;0Y zV+ukvrmaYMuX5zS{+j;4ZPSbF?!eTZoAagbtImlq&OHmSLMH$3vO_vWvX?F><%{)a zyd$;PgXO-G!bAA-YW;kAf_SwX(a3n6ldLJOaK02l53QE}7HMiPFf1@Qz!{@6ArFG^ z>3M)FTaMzF4Hh?7FtdUCm(dw#Xw1U4;fnywzoDdpmBe>k>onrP@9Marfv3_fpT6jq zJ6vmdPpDXCisja(FBT=BPflojFYsvDvxfjG=`T76VOdGR?LZhC5Ug)6QHYmaHji>#B04wvco4_)t($E1p`hK`%dEF|A(4~M|I>4YFq zOJ(UKUtZdxTh203L;r<-h_H=mrWmisbI1`)hooG1--9{gdvr#nONm8uN{}#Wy505D z2;!!8R`$6CyyjGyRa`J-QfSOm+GS-t1H{K0XpUqdY^6~=*ZB$Oe_{gFtFS1Udjxl+ zM|pRRoh8__gdj9&%9SyPi*FoN+E;L5?yx)Ui~4b1u_*>g>Gy9CV0iH}Yq z%iA&9C}%@rgGvXgtagEIo*l*L!U47%yk9Ze$EEt448_A?=|!)UVy+1|8D%*$bRrAZ zIQO2GFZV_AdD89&G6m`ET$*>dB9ZPO%vIgL%TJbNMEilFHOB0cg>emX{V`04#b`d->%=AgFB-9T^7tGcqmB!L z|8?a?EUxt3GPg`^Eg?)i{VX%Mcvh2r6|DM3yLdxF(>(bkS zJS44We#8Mb^{Gaf^MH0qT~(Sk_wR5tNZ#8@FdR=CI~PG4jKe{7GNQJ!{4$`LUc|*l z2w+rhLFu_xwk$pbeXbB9K{?)+vhUASAE=id>4kh1i{V4tNBsbqLS2;MW10S(m5E{# z;KuG#u`_)qK1%zU6q@UxD9sBtc=>%CF_+#l)q|6G<8*Q97OAAYpB;qwF6nKx%uz=4 z)#k?L2UF_pazxGb&w}dQ6Q0J0X5EfGtc{dBPYJ~oLLuU5vj~lc#E&8UDV}QR^t3ZO znPXF=M0Q@eaMW3&@j%LlmFbF=k5Wf7Vl+Jtcp_LAI_QrvPmwfd5s_DJAT<_&BF`6k z{ki=;60>Dt62!An<-qxx@fL>BAqzvZgvf`c_>+Bx_I|EUpXoO=6_T>z7UCH#kol7C zMiBndkAG~FQ#zF_)*wPcY?Y%8K;53_DkIpoY7))Pes zDmQS7RE{*ps>O}C7$v3qkZMzer#5`pSBx`cH~gsD4o`EsyN@->{6I}D1MdP@n5{X7 zK5I&y4mx?6{w1`%U;x|R*$Tt7`w3WS9)xw~d@g*}_i}qeF4ee92?G(bn3MU6*W3SC z5yhH~Ng}}{+T!{e_kq$8wie5*SkYL_0Z9clLX~|5j#P<#g-pGKO1zCqKJ8*jmvb+9 z`>JUgREhYeR7DbP!i{aabl4Xc^z#p`Vz&G4t}NhryAH^$mTUC4Co>1!A3Ip*A`1EE zZ5pr?uxfUq^{I6+vxv`{JV(kOE@*lK2BP5eGt%7oGTPb=ng^y%DT{tzTH*y4J=|sD z6%KvMqcwoRLX5#EtsY=(sS;NRZnh?}cDjmuNZU`%pC>GP%KYfN=HFNtb9VFK*-ppV zmlOe-=FMXK7&k#lGE#QF<19jRlACj7Hj{Rve)PA!QRO!y`%Y@b&{-L+KyV|mY)bIQ zTfF4npc!IL&F2uC6%JdW5r=t2!xwMgl~aPDvA&~>o`MaZkfc1nRQxsX4NT6De~|yU z&z5~>8AU%eux+y>PKd&%HZS?`GZ~YrP)OILpB*qab0&lO(yNH1=WxyprCgmhez&-N zv$c4V zLy%+CX2qtZR@u~X`4{JLPzQrx&kva6ALg@tAvr8cvLOUj2@(i1wO?3WwoRH+^WU1X zy*E)tE03v#T1cnfa)VkVQhqcihBg+nM4mW{vDCCxD#ar?1nD$UG=dEcB{BZ+amSZD zLy12XB+E#xt)Szx0c*T^tp24G4Jso+y+;v*%V(0f%Y~Ocw~qgXGoH?=*`3Fh9}Vtf zV^KxV_3Knb0zGtB&W9Ie^79kmRDr?@}23||NPXwb_ar<4mU#1?i3df(CeS~DTilvk}vNhPCb#p=`*pKR107MnBP!)&S4 zKB-m${r%Y`K09kTAfSe2HZj$dLt01Sz&?`%#UD@aO=WcS26wv`0J=uYW~7v99PhFI zGKd&kJ;%Rl$5B`%E;$~0#F$fTdnUJAW*2rA1tYnwq8xX4DVn8)}}SmVpWdHT7ZG#6_P&~q2w@lek=Vc+#y1sn+mTBo?LjqkPO z?edlKO#eU;-r!RQqe;4N`CZYq)vxgxunbVFHSbi28JNc~#-wl~+aDA1`uJtEJbh2;z>!eB+@G3f{)t&t zL^*XOs~!YrZ-rM}F=#vRK;D0V1{(tB0{GigGcn2%PClBxk=QlhHq3}=YtNN3``I_2 z4X<1uSQNU5eK4R*Lz2A^>1_DX1k*kU}H zS2iSG)ZcFdely@tH;s`EZ@!0{&DE)Ur@zn1-#_x15J~081hp)Pb0jcCsBC(7<)UKa zQaA2)9Wds8kEK81ro=PfBnvx?DQbBk0`9)y37}Qe@oe_oXr26yF_CX^s7U^=q3rkmcc->`$PyYUVZKX_8SolF?YP@_ANdQN}E15lajQ-Lu6B9=FO z2E6QuYY@@$aTIG9C7xa3#8E#87#3Q=TCdOsena{^tKbxRgn0i$T0@`M*@4Z8$cJs_ z*BvX%j!m!1mZN%DZkRj2d(=~j0OV0N2y=iZi;5v-p6R>YL!C^c z6S8PrT2Z>mu6*`4mlK_FiHlZ!Y;WC>*>V;(kzTVJg1aRE@M83kH$zM z(3dGmKNN8o-k}1$Q%WoMu^)=cVgDy<>9T6SO7n1lICZ*1j#9|Djbj2y=S!t0O`<~# zXl2@DwBG8z&A^$XR*hs8UC#KH0G>6Djsth3S!-}M&Bzl>=OwzszEAT*bg@H<+qY@; z2zG;Wg)wQgk3{X|OLK(($!njuPi@puy-L#kp44zyW%8FH3sDMQMjAgteS{H>#sNCy@?<@pggRr+b zlCTPx5G#IXUNrr1o7*rZ`ar*!j6=14ZxW;x{Rzs%$zWnqYe!xN-5~(rr`wX1i>W#? z;fK^ad1lgX)ie=IcWW$XEBC?81}GvL_1}HbT^RdzN`!;1yQI@2=~(&ij5S(;e#T0= z=YV*+H&OdXN!?=Se+wF?V%tM7axGzV{=Akchv?zD{zU?+*%xJ&0cNi?5f<4 zyrh#IzJa`S!9cFZun3*tn)e~>)NTld)i@FfbIkbL{PsE(&gu18xWh;%?Vq&!9fS;s zYT$;Atif5QVN;zHX08W{S(HOqr`Vx?2TvXDhBA((TQ*m z_lCu-f6^;*gq4%awgO!Bsea~boV?L6gO4sB1O&O-bb{K?kM-HIZLQDXAXVO!P*n?q zy&~Z|1zZJw66-Nw`WSYDDNCa40Wn+<4!VFdiB>6L!!6s6@4{>0fx4B@S|qdAoBFM7 zEw2e-PAh)#YU&;*E!=-#2Vex;Ne}P#M_J5uehaYZM`lm&rFTw~`5B_Mg!48 zg;Owq{=W!~SOk*6yzm4#b#;zkg7~D>i1nsSk4M~#@9WvkXQI%0s}8FjWiH(PS-#{N z*N|a4w5-!IrXWI=K_BQ^C7(wc~Sj6L!0)ram}wnJ9#q9RK^NZVSSl1VyB zw%Sy@4T*K{s$^)snOCFMkL2D|kZ-4Ul+LS@Azm#Rw`J=QQGS2A+`U1OsIt-WZxZ6T z&3y8%j{xOX29AxQ4G`b(hy=>gt->M)zF(1pN5t{o4Ph6-u8Exjq<-9S z05W>i?TMuNIe_?!W{91B4(K%|_7Sp)u*P`O_aTfnEwN}~SkB+w8-d!@JuvvW-}X;L zW$(jnHe)1dK#wU3FeuyHL{HJw5ad4hhYYo4i$RM2A|2XPD2ERFO@O}s>#8%)XVu`k z06bA!{xZl7qijT7K~SD|uVLc~P)t$M$Hbs|p9-$hY6H+|JUZ0;JQIw|lj!Lev+y2! zxD9i3$QbpQ%t(yoA7(!@)n0?-e4jVFW%_SG7XJ7IENEiN%`Q;K3M!sw*=gS10Hc#v zr6O%M#kx8s2OM0S#?2J^fqm(*!NH}D(FiA!Sg(Iqs#nxkYip_IV_X5U=s-NXN4?>R zric*;j_se}^|F!S{F@0JLaF-ByMfn@nn~%J)>v)X2*)a$Y`cj1Mg%ooHI-$cQseuN ze~@R`B^;1|W}(i2n+eJ*RTnRTlKL5j!S30$a9R8fYZ}~NBM%rrBC7R#rVU>Lf2{|+tcqIIs zu@Nc-u9fD^iAgwF+H}ZlW{0#DH@cHICUJPh*xi?M7vXElM1TDRR7)1KGL1=YK!9ngabX84 zU6kUhupdJZ{O=!3zi}5=RE%L(L+YiFfRQ9Q0{?wHVVV_|sH>3$g(iH4m%k18;XA{6 z5r0&icD3gIF&{6aUhIl(nm(zQlr}C4Ye=>M!Up;3H2YV$=aWn_57Lp({;PL6f}{|QNSKuF4q5#5! z=&YeqQ_na9{$k;I@;-(>W*b-Ph4}_7NZzd81SW&6N}ba6;#?b1Xk^z>iTpOM+6;r{ zHi`AaD)Gw8G4oX}xh}Du#P32M7rzGc4F;^0*u`2#0R<%#WfI9SGQ;oLo&cHnF5jCB5Q5&piO=>$MT( z@!(0}9oygJdjw744@UT$~E$>+1UTV)+Hmf1bIJ^^EvF zEL>-LpUL#%$C!dXu!NE?->7Y1cFpMQA9fLtk;K3ar&I^W>U~he9vu#ZN*<>`hpP8z zu>c__8UZ`n)KHv6CBrEVSguR5;ezO$irBwqRyP$rw$; z^1JuHxG&C}XU;P(X3qJ{_dL%`oUXPyF#(VO000m}Ua06l`pp0A3GU-4gW{;J9`#7WBpSseia5?gyPY&0dV7(mMJ<34;>sz*kPszhs*ojoRl~0Zx(uX~my9~M1<+-NL zqyYw>BZfhdBS~}i-qj_kT9dG2!fz!}5jzxh9_4vQZRS4h zwiav%sLEnqyV1$3)6`X-w4c`Be>@_rt}c8jWtBTc!8_E#u7i zGif!pmn^Th@$^@~;aXWQ>_$OCww0z2MaViRW55qZe4~HR_;|cItrD9DU$bm%C)M~f zg!Z7au>ZCiK4~Zw@W4AvPsSrkTT8WKmCKY}y)@vGv%=1P{jK-WCU58Or&8h*jl&Kq zQ%p#@cWYcn@9#{y6H3{O#WgrLM+(yW&k@3_Q(S4$?|*?nx9&2 zJ5wf0fV*yLhoYI9@=QJQ*}^3)$S|ps#Wg}v) zV8(HO!qSit;l{#kwvqDQ?Ve&Ns~G&8PrqwdcAIZM%J=z>%dvER{vaso+wY9T_=$%a z71ToS#PiT)VyPZ|U2`?$Bh#7{tCuR`h!z3cYp`y-C?CG2Zb4)s=a=HnIcbIfq)6 zmcdP;9+K~Hy_thY4$I3MYFtP=z>Oj1rB_UWWXRq>cCVvz^Wf*^l;Du6-ZdN0zWI4} z?wo$x&gX4`>eOMZcD!OHJm!q}TMvj=J@H2zQ~L{3JI0 z20cNGL*-Cr?-4@5Gm4&~)$T3_;iIh%=GQ?`RCt3t53xW{57U8N&^vwe@{LOa2Rci{e*xpX@|9sA-RgGGDERn4#@YUsY17 z1IhE95KYe$Upu_bOX$3Iu)q+N04p{S`K|Ncf80=#IcS*?512pZja{4(kvDpQ(J32x z263w7_=i0j%1Z2dl77+lL-=_sM}%4H;7ERcVJlL2_L`UeDyAqqW?K93H`ls&26JiW9JxI@dd)m0+2U&sEvB_q<| zTorsUVcSE~_ZLYTON5eow5;d~m$_-pls8j_-f3tm4yvhuTu#z*Gd*5ROhL@PgtX*p z2i{SH4ICPh2V^DwUSm~8S|gD8ZA#qmB`bE1Wf-^~thkv5%Kc$ZP)~zH_ayE8y}%dl z+x9Mg-)G51F;W#lh}T8BG)HUKenBLTr+s@DS^Nhr;AaksrOi}@8W(UqGGsv57qXf*%T6vqTBXD#edt{6yFE^b!nLsoMLz41*l1j%AIj8AuNzL=qrxX=ILQl_1 zqv9{jf%Qkv^cWdcp2}me*bo=@5S28p2q70^yob_pvZwX^CdUMfYH%XLNonNTbc&}c zsvUnxA8LTAHdy4^9)MSl@4993Q+^|zD{J25zY3_WW^UX{ExV@G2))A+7Cu{;vmv)7yLtlqHLFxM-*hP^9hZL^{&}Tpy zCpb~R@84gw+~et+opcA|(D+7~Wrf4y9Z7*Bgqj+ERr`+qMwNElR!`UeX09}!scrcf zp2|}1zeVpvlqkV85PLVV^+v3x)1&`)PS==4m{}ojpF;gjkGyubVB1uv#b7d$x9Hj& zkwJUZ%09PopWp9kotz%IEEJL%vGE31MQVaqlIq7m1zPtHH<^+@nKPJyw>02g?wzZ{ ztuV|YitGpmViX4~vsJqOi<}je53}Nm&E>uQrc;0Bafz4dtHtR0_MiGw?O2mPgUO)9^i6wKkGbmC&!%2gP6Ka1V3k$@XvhqSW5AFcD;< z8Zb)IS&e?xP_4QWVEqYJ@4~V^V7~I94P_g72H+Vv>XAax)uiIbgrp8X(a-W@3-aW? z*z{9gkESYE8rS;QW2@X^rNYKh(!9-ycCqfR^9^iS@UIHvWra?HMV#>xG)2$m83|*jn8&^d)XhB_(jk?I_ajp&)i9 zy}Mxplf@3C>L)i;@i7ku?QxueJ4#54$#K{Kc3+v&f@L4JM-33%wUB}%N68Y(^A_|& z#J$tmcB*hmGg?F+@qi<1A2~y72CmrCY(SxcBs4WkXMX78TDuq>%G$R6VeL(xp}VHj&i5FXUlEW|>#nBBV3|@_6>p7L&yI82J zeP)T=3Z9}=&jnn$(ycNV7(XZh%W5HlrWPOs>Ol=R@SpEgnMZDBgrB~w9QsA^4S7G= zF;R4W`t27M#kW%ilVdgtUs9^aSA<^Dg4d0?Zo~4btF-Ok|5LG0V`21^(Mv6po49u^ zAQpD@z{$eTAaR*}dZZ&aP2!g=ISrl;h-AsIqzZL}7*R1HU z%dhXm+Z>%f!1I}70>Hg>AtJ!Q6E6|M#yr>>i*ri_frQZU-yV@@#!u2;W!+VKwIgP#_$$6X{elae5~#0C!@g-73bl!?IN* z;t`tVn<|nE?VH!!IH{{zqmNVlUHofN0+l0d&6|=Te~qz!x*T<5aoB+xYeB3XRWh@P zQYk+XB0}|_Z#qKGe);E}@FybvQJtw9E2*KjZ9z@=E)6R=%ZdUWE^`Uf9p^~>4YXXM zuPh<+ENv4_??4!smQZx`*LJyvTc1)>uaIbdOj5kw!}>$O$VVV^tN3i&bynEUCntph znXA96GWc(~)_}tz30cqtVCg)e^>R>44Cx2lv^AdUrZ4 z=1;rE(0J#C95D7xzzNo*c7a;%*{{tv+YOFEco$Kt!1wTu7YAX!CXsKAwsebKj9d8Q1=>0_Y43@Xr|K zLfyfNDQzxAV<3ej!&QX4IPD5a);{?2igtYgnPumoXgM#NY&g*Q!1VB-RUr( z5T3k(rYt(aN zMNnjT=DR? z0>Yvq7@rx#7MpV{FLgD&79%Uin-m8QZS&%gU$MJKZe3akcN?w>oUG`dZ!WSi`S<~C z&uyTm&4jOG*OCtZ<#(L@2r>KmT>(3|Q`$;sqs}d9he`xBj>dp)hfoVaf~ym{<4^%_&y04m*=bV ze!;MNHpbUZ74e9Reu?Ree_cN715ynk?%@t%`0Zy`Q8zrx-RBIP!hU$d+yWnGps;$J z!b(H;G*B1;?>w>VX>>ux@K^fdE=*;K4QzYYM(i^^A8jVMqH3X`H=9D@(=KfcYJjd_ z07iC4i>$L*vSIP-)*3ehN%KBv6n$}ak0WW}joe%K17W>Bpq3tna=T`v78L1@0B6Wx zSGZ>`?;*GW~cwJW*0GF#Nr_eElUd-WftC2Y#?&>$2xI{s(EhsMFp3;Q0JAE0I zv45~HhJ&(8`m7#hA}dlPdB3{4i(jldUrol1gW=uK&Z~#YKJ_0!u9qY z)_{ADDUhBp2u4llq{ZXCA3xuM-DvIHxNA68+5@Ko#^%iK+bq1&XiBs&fnaG8BC*J= z9Xut{9u8r_=h9%JM73V>8{-Xi9Z!wE?;$FR!Jm3Y=2qkA7z2HtNWI5J6yzNxv3hN7 ziUwvUwv=zH#u^!Ig|AwCp}G<$&TRTC)t?r-P#xJmI6o%Ig7EnA0AXJUaXSbO$TN1Nr-=6v9>v_Y;Ltu@? zcbzm8x7}X3IU7EoQ?8{1DO7C%p8aBUicS4l3O$_db{xa8tTkK5fUm4kNFZh?6*5+c zK>J&kK@<=>*u4YX<_6X2f1g6f&y`O+7GRZzPxWmH{Mp}@14Isi;BpHKIz4R;nFB3H zmv{~S-N{WTOTxP#G6U0j{9hDvoO~F>nUv8gCFCD+4O@Gx8kY$fGU7yS>?TsM7ESDK zbhB!(MNPHoipk4*73|F9XucO&LdB0W!V_fs=B5LN%nTIkT$fTaU^HODf#4WbI&)ZGMH)R=nyDT0zFR)g~9exemU; zVWndW=f|^be0|t9Gc059sm=TIC>^5hj_B8LOT>=c=n!=A2u1Hqi~H$5=-~@LJ-$=@5l6*hMmr6R;M*xx63+X|FGqLF zf$>A(VID27N+-V7r^^!%XIkdU$!s2-yXk$XQPI=1;T>|5mi}}$C5WQSYJ6qvV+jPm zyev%((#Ipy3tVFKAC3EZDvv`O=c0Pj-qaFd`3T1cD_M2LUu(0vnUJ62&(HimmjFb+ zq`!w%b3K_-{oleWeupEHj4OH~P$W~Ltx)oDiI49uWq#eX;f^kKYuM%F@(`v~BJ)Br zecc8*yw9GaetL{kQVq7@x~fTk(3#<@U}dJ}F#?UP&Y<(kzSMtDnOjsxIA}l~nHiPC z{}3^XJvwbQgIU^wsN6rQmpXh_@B2u1kJMC+7+&n!guY|S_V!DX&lx$Sy=LY;wXXG! zqO#rkZi-kAA}@|@V(9Vg#Y?1~l{)ahTq+L}e`Fx(GnB1x8&50^xV)w0G^QFnHVMn{ z4>S7J`Qc&~F4e;>7^(ReO7akY=ZMk7RMA3@5oRPQQZ2Q!Yq|j6p(5+^lpUp{-m345S=G7i2Wfs*TS!X39O-K zy@iqIaPey(Z)|RMJ_#uwggN=C!78K6^Up~Vp1#jG=NE^qci}Ve1)XDe_g6LTi z((0vL`4;~>`M&clI7u}&TWjRiJV2DKk%rk-B5wX+{5hfLf86-=_E;5NG(4eV`SNv) zl}$v^KzA!NkR$(6cCVPDu*W;1u=tSYXv^z>-7&*^>tO~Bb`hsBs6Mxr%dAUC4Z|J9 zk0^hVb}zCTwhD3)kR#VB9D76WI96C$naB0sfMvh9C$~p|aYmuc;I0v_$8dhl85C2r z^U{GE{^?Si()rdxG;W#7QMF1e^CN$!z_K82{jhG`zQfk2(A+2ZEUZkbxvvz;T}ZJ& z2DPa}wkiFye%y4h=HXe?Ptb>Sxy%{HX)1m5Edyt;(l3%ei|y~(ZMv92whFZE*pijc z65=5k;9Jv?0ExgD(SU>_FF&Ugq{^x#?%0OV+9-Ocohm`H~$=lUICNkO@6yfD68brG5 zFw&D7+O;YCrwF(Veq@;tu=7sG4`dDqP?h(_z}3uAPwL{TVQfHfUBL+FVO~HpZyDu% zki~xa>~hwWH$s(#FeHn1o98U98rd*Ash)@;gxz>n>T9JhM5c1Fagd-{7?HAWT4{qPh5T2I z457_U)ZEkGZ##4$hOS_4)1#$;4$6_3?V}K%b>yVBT!-!)L!?L=lJW6wiZ-DX`@A1hcgJsLBdI7r$s|hz^j+7Y{1xBNjJvVHw4z5z`Z-GU^i% zeI0A*Mbbin#>Vn2w-_)j>Qa4~-$@%><8w=g27j)7tWK`m;f}w?x@9x%?z2>(H$oY& zj1mjrPdqawH$dSB`m)LyLN4%)HHsLWGEHO5$JE;tdKfXr+I34+;ud8TWMEWNee5Qq zO5YxyHnrs_{Ji}0M)9Q<4zl*KfumLbg-Gt83`qqeR%L@WiKi$yOij^OQ2Lea*HOGa zqzRhsCD+9kV$?_C#KyH{5=4^5>qr-}=RjkYO-Muv4XwOMwniExXFqeBOg6;Q?Xjoq z^!8ZWcF!W;r6AZEAUCJ@)ZEvY6#$xB^PK&p`c!S-?kRP+=Hm?#HT~ub;+Ew(pa@jiLU+U0QrXvM(335r=hb z8T=zPVo(?pKTg38$wM0KRSP8eEp0Mz-vwIyAiu-#@@=BPX6lVSLOQ3_`+Kg~0(~dk zT-{{BWYcZGCYmu~7QRC^lJ{Xz^QR=+rxQx$*zC-SZ-GT2m6r|LUw4p0E}IHs1=~}) w-$Am+cHcq3`+-LudSHoHR}D=Xn-3VvfyjeADbnYU|J4A9s*** This has not been tested!! ***\n\n'; + */ + if (motors.length > 1) { + const cg = { + 'x': 0, + 'y': 0 + }; + const cg2 = { + 'x': 0, + 'y': 0 + }; + for (let i = 1; i < motors.length; i++) { + const motor = getMotorByNumber(i); // Motors[parseInt(i)]; + cg.x += motor.mixvalue.x; + cg.y += motor.mixvalue.y; + cg2.x += motor.position.x; + cg2.y += motor.position.y; + } + cg.x /= motors.length - 1; + cg.y /= motors.length - 1; + cg2.x /= motors.length - 1; + cg2.y /= motors.length - 1; + let maxdist = -100000; + let maxdistyaw = -100000; + let maxdistkk = -100000; + for (let i = 1; i < motors.length; i++) { + const motor = getMotorByNumber(i); // Motors[parseInt(i)]; + maxdist = Math.max(Math.abs(motor.mixvalue.x - cg.x), maxdist); + maxdist = Math.max(Math.abs(motor.mixvalue.y - cg.y), maxdist); + let d = dist(motor.mixvalue, cg); + maxdistyaw = Math.max(d, maxdistyaw); + d = dist(motor.position, cg2); + maxdistkk = Math.max(d, maxdistkk); + } + for (let i = 1; i < motors.length; i++) { + const motor = getMotorByNumber(i); // Motors[parseInt(i)]; + const d = dist(motor.mixvalue, cg); + const x = parseFloat(Number(-(motor.mixvalue.x - cg.x) / maxdist).toFixed(3)); + const y = parseFloat(Number(-(motor.mixvalue.y - cg.y) / maxdist).toFixed(3)); + let z = parseFloat(Number(((motor.direction === 0 ? -1 : 1) * d) / maxdistyaw).toFixed(3)); + z = motor.direction === 0 ? -1 : 1; // Hmm... yaw values are always the same magnitude no matter their location + // Cmix += "cmix " + i + " 1 " + x + " " + y + " " + z + "\n"; + mmix += `mmix ${i - 1} 1 ${x} ${y} ${z}\n`; + + /* + * Mwii += "motor[" + (i - 1) + "] = PIDMIX(" + x + "," + y + "," + z + ");\n" + * arducoptermix += "add_motor_raw(AP_MOTORS_MOT_" + i + ", " + x + ", " + (-y) + ", AP_MOTORS_MATRIX_YAW_FACTOR_" + (z > 0 ? "CCW" : "CW") + ", " + i + ");\n" + * var a = (0.5 * Math.PI) + Math.atan2(y, -x); + * if (a < 0) a += 2 * Math.PI; + * var deg = a * 180 / Math.PI; + * let d = dist(motor.position, cg2); + * var f = d / maxdistkk; + * var p = Math.cos(a) * f * 100; + * var r = Math.sin(a) * f * 100; + * p = p.toFixed(0); + * r = r.toFixed(0); + * if (p === -0) p = 0; + * if (r === -0) r = 0; + * kk2mix += "Motor " + i + ": P= " + p.toString().paddingLeft(" ") + " R= " + r.toString().paddingLeft(" ") + "\n"; + */ + } + // Cmix += "save\n"; + mmix += 'set yaw_motors_reversed = OFF\n'; + mmix += 'save\n'; + } + + /* + * $("#mmixcommands").text(cmix); + * $("#mwiicommands").text(mwii); + */ + switch (commandType) { + case 'cf1.10': + $('#commands').text(mmix); + break; + default: + break; + } + + /* + * Disable legacy mix types + * if (commandType === "multiwii") { + * $("#commands").text(mwii); + * } else if (commandType === "cf1.9") { + * $("#commands").text(cmix); + * } else if (commandType === "cf1.10") { + * $("#commands").text(mmix); + * } else if (commandType === "kk2") { + * $("#commands").html(kk2mix); + * } else if (commandType === "arducopter") { + * $("#commands").html(arducoptermix); + * } else { + * $("#commands").text("Select a command type"); + * } + */ + } + + function updateMotorConstraintsSatisfied() { + for (let i = 0; i < motors.length; i++) { + motors[parseInt(i)].constraints = 0; + } + for (let i = 0; i < relations.length; i++) { + const relation = relations[parseInt(i)]; + if (!relation.distance) { + continue; + } + motors[getMotorIndexByNumber(relation.a)].constraints += 1; + motors[getMotorIndexByNumber(relation.b)].constraints += 1; + } + //for (let i = 0; i < motors.length; i++) { + //console.log(`debug: mixercalc: Motor [${i}](#${motors[parseInt(i)].number})` + ` has ${motors[parseInt(i)].constraints} constraints.`); + //} + } + + function draw() { + ctx.fillStyle = 'rgb(62,62,62)'; // 'rgb(245,245,245)'; //canvas color + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.textAlign = 'center'; + ctx.strokeStyle = 'rgba(0, 0, 0, 0.125)'; + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.save(); + ctx.translate(0, canvas.height); + ctx.save(); + ctx.scale(1, -1); + ctx.translate(300, 300); + // CG + if (motors.length > 1) { + const cg = { + 'x': 0, + 'y': 0 + }; + for (let i = 1; i < motors.length; i++) { + cg.x += motors[parseInt(i)].mixvalue.x; + cg.y += motors[parseInt(i)].mixvalue.y; + } + cg.x /= motors.length - 1; + cg.y /= motors.length - 1; + + /* + *Ctx.fillStyle = "rgba(255, 0, 255, 0.5)"; + *ctx.beginPath(); + *ctx.arc(cg.x * limit, cg.y * limit, 9, 0, 2 * Math.PI, false); + *ctx.fill(); + */ + ctx.save(); + ctx.translate(cg.x * limit, cg.y * limit); + ctx.scale(0.33, 0.33); + ctx.translate(-cogImage.width / 2, -cogImage.height / 2); + ctx.drawImage(cogImage, 0, 0); + ctx.restore(); + } + // Direction relation lines + for (let i = 0; i < relations.length; i++) { + const relation = relations[parseInt(i)]; + if (!relation.h && !relation.v) { continue; } + const ma = getMotorByNumber(relation.a); // Motors[relation.a]; + const mb = getMotorByNumber(relation.b); // Motors[relation.b]; + /* + * If (relation.h) ctx.strokeStyle = "rgba(0, 255, 0, 1)"; //green horizontal + * else ctx.strokeStyle = "rgba(0, 0, 255, 1)"; //blue vertical + */ + ctx.strokeStyle = 'rgba(0, 163, 224, 1)'; // EmuBlue for both horizontal and vertical + ctx.beginPath(); + ctx.moveTo(ma.mixvalue.x * limit, ma.mixvalue.y * limit); + ctx.lineTo(mb.mixvalue.x * limit, mb.mixvalue.y * limit); + ctx.stroke(); + } + ctx.strokeStyle = 'rgba(255, 255, 255, 0.50)'; // "rgba(0, 0, 0, 0.0625)"; + // Distance relation lines + for (let i = 0; i < relations.length; i++) { + const relation = relations[parseInt(i)]; + if (!relation.distance) { continue; } + const ma = getMotorByNumber(relation.a); // Motors[relation.a]; + const mb = getMotorByNumber(relation.b); // Motors[relation.b]; + ctx.beginPath(); + ctx.moveTo(ma.mixvalue.x * limit, ma.mixvalue.y * limit); + ctx.lineTo(mb.mixvalue.x * limit, mb.mixvalue.y * limit); + ctx.stroke(); + } + ctx.font = '18px Arial'; + // Motors + for (let i = 1; i < motors.length; i++) { + if (i === highlightedMotor) { + ctx.shadowBlur = 20; + ctx.shadowColor = 'rgb(0,162,244)'; // "rgb(255,128,0)"; //orange + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + } else { + ctx.shadowBlur = 0; + } + // Motor images + ctx.save(); + const motor = motors[parseInt(i)]; + const imageObj = motor.image; + if (imageObj) { + ctx.translate(motor.mixvalue.x * limit, motor.mixvalue.y * limit); + ctx.rotate(imageObj.angle); + const s = i === 0 ? 0.5 : 0.75; + ctx.scale(s, s); + ctx.translate(-imageObj.width / 2, -imageObj.height / 2); + ctx.drawImage(imageObj, 0, 0); + } + ctx.restore(); + // Motor inner circles + if (motor.constraints > 2) { + ctx.fillStyle = 'rgba(0, 255, 0, 0.5)'; // Green + //console.log("debug: mixercalc: motor ["+i+"](#"+motor.number+")"+" has "+motor.constraints+" so coloring it green."); + } else { + ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; // Red + //console.log("debug: mixercalc: motor ["+i+"](#"+motor.number+")"+" has "+motor.constraints+" so coloring it red."); + } + ctx.beginPath(); + ctx.arc(motor.mixvalue.x * limit, motor.mixvalue.y * limit, 12, 0, 2 * Math.PI, false); + ctx.fill(); + ctx.shadowBlur = 0; + // Don't show number on FC + if (i === 0) { continue; } + ctx.fillStyle = 'black'; + // Motor numbers + ctx.save(); + ctx.translate(motor.mixvalue.x * limit, motor.mixvalue.y * limit - 6); + ctx.scale(1, -1); + ctx.fillText(motor.number, 0, 0); + ctx.restore(); + } + ctx.font = '14px Arial'; + // Distance relation text + for (let i = 0; i < relations.length; i++) { + const relation = relations[parseInt(i)]; + if (!relation.distance) { continue; } + const ma = getMotorByNumber(relation.a); // Motors[relation.a]; + const mb = getMotorByNumber(relation.b); // Motors[relation.b]; + ctx.save(); + ctx.translate(0.5 * (ma.mixvalue.x + mb.mixvalue.x) * limit, 0.5 * (ma.mixvalue.y + mb.mixvalue.y) * limit - 6); + ctx.scale(1, -1); + ctx.strokeStyle = 'rgba(0,0,0,1)'; + ctx.strokeText(relation.distance, 0, 0); + ctx.fillStyle = 'rgba(255,255,255,1.0)';// 'rgba(0,0,0,0.75)'; + ctx.fillText(relation.distance, 0, 0); + ctx.restore(); + } + ctx.restore(); + ctx.restore(); + } + + function initMotorImages() { + for (let i = 0; i < motors.length; i++) { + const motor = motors[parseInt(i)]; + if (motor.image) { + continue; + } + const imageObj = new Image(); + imageObj.width = 150; + imageObj.height = 150; + if (motor.number === 0) { + imageObj.src = 'resources/mixercalc/cog.png'; + } else if (motor.direction === 1) { + imageObj.src = 'resources/mixercalc/emu-prop-cw.png'; + } else { + imageObj.src = 'resources/mixercalc/emu-prop-ccw.png'; + } + imageObj.angle = motor.imageAngle ? motor.imageAngle : 0; + motor.image = imageObj; + } + } + + function doReparse() { + const input = $('#inputs').val(); + const lines = input.split('\n'); + let error = false; + const constraints = []; + for (let i = 0; i < lines.length && !error; i++) { + let line = lines[parseInt(i)].trim(); + line = line.replace(/\s+/g, ' '); + if (line === ' ' || line === '') { + continue; + } + const parts = line.split(' '); + if (parts.length === 3) { + const ma = parseInt(parts[0].trim()); + const mb = parseInt(parts[1].trim()); + const val = parts[2].trim().toLowerCase(); + if (isNaN(ma) || isNaN(mb) || (ma >= motors.length) || (mb >= motors.length) || !(val === 'h' || val === 'v' || !isNaN(parseInt(val)))) { + error = true; + } else { + constraints.push([ + ma, + mb, + val + ]); + } + } else if (parts.length !== 0) { + error = true; + } + } + if (!error) { + relations = []; + for (let i = 0; i < constraints.length; i++) { + const ma = constraints[parseInt(i)][0]; + const mb = constraints[parseInt(i)][1]; + const val = constraints[parseInt(i)][2].toLowerCase(); + if (val === 'h') { + relations.push({ + 'a': ma, + 'b': mb, + 'h': true + }); + } else if (val === 'v') { + relations.push({ + 'a': ma, + 'b': mb, + 'v': true + }); + } else { + relations.push({ + 'a': ma, + 'b': mb, + 'distance': parseInt(val) + }); + } + } + updateMotorConstraintsSatisfied(); + } + //console.log('debug: mixercalc: doReparse'); + } + + function startReparseTimer() { + if (reparseTimer) { clearTimeout(reparseTimer); } + reparseTimer = setTimeout(function() { + doReparse(); + }, 2000); + } + + function addMotorsRadial(howMany, layout) { + while (motors.length > 1) // HowMany+1) + { + motors.pop(); + } + const radius = 200; + const offset = howMany === 4 ? -0.5 : howMany === 6 ? 0 : 0.5; + for (let i = 0; motors.length < (howMany + 1); i++) { + const x = Math.cos((i + offset) / howMany * 2 * Math.PI); + const y = Math.sin((i + offset) / howMany * 2 * Math.PI); + motors.push({ + 'number': i + 1, + 'direction': i % 2, + 'position': { + 'x': 300 + (radius * x), + 'y': 300 + (radius * y) + } + }); + } + switch (howMany) { + case 4: + if (layout === 'cf') { + motors[3].number = 4; + motors[4].number = 3; + } + break; + case 6: + if (layout === 'cf') { + motors[1].number = 5; + motors[2].number = 2; + motors[3].number = 4; + motors[4].number = 6; + motors[5].number = 3; + motors[6].number = 1; + } + break; + case 8: + if (layout === 'cf') { + motors[1].number = 6; + motors[2].number = 2; + motors[3].number = 5; + motors[4].number = 1; + motors[5].number = 8; + motors[6].number = 4; + motors[7].number = 7; + motors[8].number = 3; + } + break; + default: + break; + } + for (let i = 1; i < motors.length; i++) { motors[parseInt(i)].image = null; } + initMotorImages(); + } + + function doPreset(which) { + switch (which) { + case 'quadx': + addMotorsRadial(4, 'cf'); + $('#inputs').val(''); + break; + case 'apex5': + addMotorsRadial(4, 'cf'); + $('#inputs').val('1 2 208 \n' + + '3 4 208 \n' + + '1 3 218 \n' + + '2 4 218\n' + + '1 4 256\n' + + '2 3 256\n' + + '2 4 h\n' + + '1 3 h\n' + + '1 2 v\n' + + '3 4 v\n'); + break; + case 'clone6': + addMotorsRadial(4, 'cf'); + $('#inputs').val('1 2 158 \n' + + '3 4 158 \n' + + '1 3 212 \n' + + '2 4 212\n' + + '1 4 304.24\n' + + '2 3 304.24\n' + + '2 4 h\n' + + '1 3 h\n' + + '1 2 v\n' + + '3 4 v\n'); + break; + case 'quad+': + addMotorsRadial(4, 'cf'); + $('#inputs').val('1 2 150\n' + + '1 3 150\n' + + '2 4 150\n' + + '3 4 150\n' + + '1 4 212.132\n' + + '2 3 212.132\n' + + '1 4 v\n' + + '2 3 h'); + break; + case 'zmr': + addMotorsRadial(4, 'cf'); + $('#inputs').val('1 2 156\n' + + '3 4 156\n' + + '1 3 202\n' + + '2 4 202\n' + + '1 4 256\n' + + '2 3 256\n' + + '2 4 h\n' + + '1 3 h\n' + + '1 2 v'); + break; + case 'deadcat7': + addMotorsRadial(4, 'cf'); + $('#inputs').val('1 2 182\n' + + '2 4 287\n' + + '3 4 182\n' + + '1 3 269\n' + + '1 4 332\n' + + '2 3 332\n' + + '2 4 h\n' + + '1 3 h'); + break; + case 'gh160': + addMotorsRadial(4, 'cf'); + $('#inputs').val('1 2 107\n' + + '3 4 107\n' + + '1 3 110\n' + + '2 4 135\n' + + '1 4 162\n' + + '2 3 162\n' + + '2 4 h\n' + + '1 3 h'); + break; + case 'hexx': + addMotorsRadial(6, 'cf'); + $('#inputs').val(''); + break; + case 'octx': + addMotorsRadial(8, 'cf'); + $('#inputs').val(''); + break; + case 'jure': + addMotorsRadial(8, 'cf'); + $('#inputs').val('2 7 998\n' + + '4 5 998\n' + + '2 5 765\n' + + '4 7 765\n' + + '2 5 h\n' + + '2 7 v\n' + + '3 6 344\n' + + '1 8 344\n' + + '3 8 530\n' + + '1 6 530\n' + + '1 6 h\n' + + '1 8 v\n' + + '3 6 v\n' + + '3 8 h'); + break; + default: + break; + } + doReparse(); + } + + function onMouseMove(canvas, evt) { + const rect = canvas.getBoundingClientRect(); + mousePos = { + 'x': evt.clientX - rect.left, + 'y': canvas.height - (evt.clientY - rect.top) + }; + + /* + * Console.log("debug: mixercalc: "+mixerPosToMotorPos(canvasPosToMixerPos(mousePos))); + * console.log("debug: mixercalc: "+ motors[1].position ); + */ + if (!draggingMotor) { + // Check which motor the mouse is over + highlightedMotor = null; + let bestDist = 100000; + for (let i = 1; i < motors.length; i++) { + const motor = motors[parseInt(i)]; + + /* + *If (motor.constraints > 0) { + *continue; + *} + */ + const motorPos = mixerPosToCanvasPos(motor.mixvalue); + const d = dist(motorPos, mousePos); + if (d < 50 && d < bestDist) { + bestDist = d; + highlightedMotor = i; + } + } + } else { + const motor = motors[parseInt(highlightedMotor)]; + // Motor.mixvalue = canvasPosToMixerPos(mousePos); + motor.position = mixerPosToMotorPos(canvasPosToMixerPos(mousePos)); + } + } + + //remove - never used + //function onMouseOut(canvas, evt) { + // highlightedMotor = null; + //} + + function onMouseDown(canvas, evt) { + if (!highlightedMotor || draggingMotor) { return; } + draggingMotor = true; + } + + function onMouseUp(canvas, evt) { + draggingMotor = false; + mousePos = null; + } + + function onKeyUp(canvas, evt) { + const motorPos = mixerPosToMotorPos(canvasPosToMixerPos(mousePos)); + const nextMotorNumber = motors.length; + switch (evt.keyCode) { + case 37: // Left + motors.push({ + 'number': nextMotorNumber, + 'direction': 1, + 'position': motorPos + }); + initMotorImages(); + updateMotorConstraintsSatisfied(); + break; + case 39: // Right + motors.push({ + 'number': nextMotorNumber, + 'direction': 0, + 'position': motorPos + }); + initMotorImages(); + updateMotorConstraintsSatisfied(); + break; + case 46: // Delete + case 68: // D + if (!draggingMotor && highlightedMotor) { + motors.splice(highlightedMotor, 1); + for (let i = 0; i < motors.length; i++) { + motors[parseInt(i)].number = i; + } + for (let i = relations.length - 1; i >= 0; i--) { + if (relations[parseInt(i)].a >= motors.length || relations[parseInt(i)].b >= motors.length) { + relations.splice(i, 1); + } + } + updateMotorConstraintsSatisfied(); + doReparse(); + } + break; + case 38: // 38 = up + case 82: // 82 = r + if (!draggingMotor && highlightedMotor) { + const motor = motors[highlightedMotor]; + motor.direction = 1 - motor.direction; + if (motor.direction === 1) { + motor.image.src = 'resources/mixercalc/emu-prop-cw.png'; + } else { + motor.image.src = 'resources/mixercalc/emu-prop-ccw.png'; + } + } + break; + default: + break; + } + //console.log("debug: mixercalc: keyup: ${evt.keyCode}"); + } + + function canvasPosToMixerPos(canvasPos) { + const cx = (canvasPos.x - canvas.width / 2) / limit; + const cy = (canvasPos.y - canvas.width / 2) / limit; + return { + 'x': cx, + 'y': cy + }; + } + + function calculateMixerValues() { + const fcpos = motors[0].position; + // Bounding box of all motors + let maxx = -100000; + let maxy = -100000; + for (let i = 1; i < motors.length; i++) { + const motor = motors[parseInt(i)]; + maxx = Math.max(Math.abs(motor.position.x - fcpos.x), maxx); + maxy = Math.max(Math.abs(motor.position.y - fcpos.y), maxy); + } + scale = 1; + if (maxx > maxy) { scale = 1 / maxx; } + else { scale = 1 / maxy; } + /* + * Set mixpos in each motor as percentage of furthest distance + * var mixervals = ""; + */ + for (let i = 0; i < motors.length; i++) { + const motor = motors[parseInt(i)]; + /* + * Var vx = (motor.position.x - fcpos.x) * scale; + * var vy = (motor.position.y - fcpos.y) * scale; + * motor.mixvalue = { x:vx, y:vy }; + */ + motor.mixvalue = motorPosToMixerPos(motor.position); + // Mixervals += "Motor "+i+": "+Number(motor.mixvalue.x).toFixed(4)+", "+Number(motor.mixvalue.y).toFixed(4)+"
"; + } + /* + * Mixervals += "Scale: "+scale+"
"; + * $("#mixervals").html(mixervals); + */ + } + + function step() { + if (!draggingMotor) { + // Rotate motor images + for (let i = 1; i < motors.length; i++) { + const motor = motors[parseInt(i)]; + if (motor.image) { + if (motor.image.angle < -360 || motor.image.angle > 360) { + motor.image.angle = 0; // Reset + } + motor.image.angle += (motor.direction === 1 ? 1 : -1) * 0.05; // 0.015; //animation speed (increment/decrement angle) + motor.imageAngle = motor.image.angle; + } + } + relaxConnections(); + } + calculateMixerValues(); + updateMmixCommands(); + draw(); + } + + function animate() { + if (run) { window.requestAnimFrame(animate); } + step(); + } + + /* + * No longer used: + * function onCommandTypeChanged() { + * CommandType = $("#commandtype").val(); + * //console.log("debug: mixercalc: "+commandType); + * UpdateMmixCommands(); + * If (commandType === "cf1.9" || commandType === "cf1.10") $("#cfextra").show(); //"remember save" + * Else $("#cfextra").hide(); + * If (commandType === "arducopter") $("#arducopterextra").show(); + * Else $("#arducopterextra").hide(); + * } + */ + + $(document).ready(function() { + /* + * $(".toggleDivLink").click(function(e) { + * E.preventDefault(); + * Var divId = $(this).attr("divid"); + * $("#" + divId).slideToggle("slow"); + * }); + * $(".toggleDivLinkAll").click(function(e) { + * E.preventDefault(); + * Var divId = $(this).attr("divid"); + * $("." + divId).slideToggle("slow"); + * }); + * //setTimeout(function(){listLessons();}, 200); + * $("#animtoggle").click(function() { + * Run = !run; + * If (run) animate(); + * }); + */ + $('#reverseAllMotorsButton').click(function(event) { + event.preventDefault(); + reverseAllMotors(); + }); + //$('.outputpanel').mouseenter(function() { + // run = false; + // //animate(); //dont animate or else commandbox is wrong + //}); + //$('.outputpanel').mouseleave(function() { + // run = true; + // animate(); + //}); + $('#inputs').keyup(function() { + startReparseTimer(); + }); + $('#inputs').change(function() { + startReparseTimer(); + }); + $('.preset').click(function(event) { + event.preventDefault(); + doPreset(event.target.id); + }); + canvas = document.getElementById('mixerviz'); + ctx = canvas.getContext('2d'); + canvas.addEventListener('mousemove', function(evt) { + onMouseMove(canvas, evt); + }, false); + canvas.addEventListener('mousedown', function(evt) { + onMouseDown(canvas, evt); + }, false); + canvas.addEventListener('mouseup', function(evt) { + onMouseUp(canvas, evt); + }, false); + canvas.addEventListener('keyup', function(evt) { + onKeyUp(canvas, evt); + }, false); + const fc = { + 'number': 0, + 'position': { + 'x': 300, + 'y': 300 + } + }; + /* + * Fc.image = new Image(); + * fc.image.src = "resources/mixercalc/fc.png"; //not rendered, but fc still used for calculations + */ + motors.push(fc); + cogImage = new Image(); + cogImage.src = 'resources/mixercalc/cog.png'; + addMotorsRadial(4); + initMotorImages(); + updateMotorConstraintsSatisfied(); + animate(); + canvas.focus(); // Fair spot here (problem is cannot 100% steal focus, else input caret lost) + //console.log('debug: mixercalc: canvas.focus'); + }); //document.ready +} // mixerCalcMain + +/* //moved this to a listener + * Function reverseAllMotorsButton(){ + * reverseAllMotors(); + * } + */ + +function mixCommandCopyClipboardFunc() { + // $("#commands").select(); //jquery //works for text area only, so need more logic for pre/div + const node = document.getElementById('commands'); + // Chrome only solution + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(node); + selection.removeAllRanges(); + selection.addRange(range); + document.execCommand('copy'); // Copy to clipboard + document.getElementById('mixCommandCopyButton').textContent = '------- Copied -------'; +} + +function mixCommandCopyClipboardOutFunc() { + document.getElementById('mixCommandCopyButton').textContent = 'Copy to Clipboard'; +} + +mixerCalcMain(); diff --git a/src/main.html b/src/main.html index db2986d8f..b1001140f 100644 --- a/src/main.html +++ b/src/main.html @@ -287,14 +287,12 @@
    -
  • -
  • -
  • -
  • -
  • -
  • +
  • +
  • +
  • +
  • +
  • +
  • diff --git a/src/tabs/mixercalc.html b/src/tabs/mixercalc.html new file mode 100644 index 000000000..445cd8929 --- /dev/null +++ b/src/tabs/mixercalc.html @@ -0,0 +1,105 @@ + + + + Multi-rotor motor mixing calculator + + + + + + + + +

    Motor Mix Calculator

    + Original code by iforce2d.net. Code used and modified with permission.
    +
    +
    This tool lets you calculate mixer settings by simply measuring the distances between motors. It is intended for "flat" motor layouts. i.e. this will not work for X8, Y6, Z, Lynchpin, etc. True-X do not need calculation. Stretch-X, Squish-X, DeadCat, H, etc. should benefit from calculations.
    +
    For keyboard input, first click the graphical canvas to give it focus. Then you can use these keys:
      +
    • left arrow -- add a CCW motor at the mouse position
    • +
    • right arrow -- add a CW motor at the mouse position
    • +
    • r or up-arrow -- reverse the rotation of the highlighted motor
    • +
    • d or delete -- delete the highlighted motor
    • +
    After adding the necessary motors on the canvas, enter constraints for pairs of motors, and let the simulation run for a few seconds until the output values at the bottom of the page have settled.
    +
    Ideally each motor should have three constraints. When three or more constraints are present the inner circle for that motor will turn green. Make sure to have at least one horizontal or vertical constraint to define the overall direction of the layout.
    +
    For a demonstration of usage, see: this YouTube video.
    +
    +
    + + + + + + + +
    +
    + + +
    +
    +
    Constraints
    +
    + +
    Examples
    4 motors:
    + 6 motors:
    + 8 motors:
    + +
    +
    + + Enter lines in the format:
          motor motor constraint
    +
    A constraint can be one of:
          number - distance between locations (gray)
          h - align motors horizontally (blue)
          v - align motors vertically (blue)
    +
    Examples:
    Motors 1 and 2 are 195mm apart
    + 1 2 195
    Motors 3 and 4 are 220mm apart
    + 4 3 220
    Motors 2 and 4 are aligned horizontally
    + 2 4 h
    Motors 1 and 2 are aligned vertically
    + 2 1 v
    +
    Constraints will be automatically applied two seconds after the last change has been made. If the motors get into any strange positions you can move them manually by dragging with the mouse. +
    +
    +
    +
    +

    Mixer commands:

    + + WARNINGS:
    + 1) MMIX commands are potentially unsafe until all motors constrained to green status in graphical canvas above. Pilot is responsible for correct motor constraints.
    + 2) MMIX commands are unsafe until all propeller directions are accurate. This tool does not alter propeller directions physically. Do NOT enable the reversed-propellers toggle in Configuration tab when using these MMIX results. Instead, Yaw-direction is set by the last value of each MMIX line below (1|-1). Pilot is responsible for correct propeller directions.
    +
    +
    +
    + + + + + + +
    + + + + +
    Be sure to allow the calculations to complete. +
    + + + + +
    +
    +
    + + + From 953a89725461478451966190cc8d170c06fc2d78 Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Mon, 6 Jun 2022 11:33:38 -0500 Subject: [PATCH 2/3] nwjs 0.50.3 (#453) * all nwjs 0.50.3, except win32 --- gulpfile.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 6c4f0edfb..4573c248d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -39,16 +39,16 @@ const LINUX_INSTALL_DIR = '/opt/emuflight'; var gitChangeSetId; // FIXME: hardcoded version number +// 0.45.6 Win7 connects; 0.42.3 fixed OSX Flashing; 0.46.X broke Win7 connect. maybe serial/usb needs updating +// reverted to 0.42.6 due to Windows increased CLI-tab buffer/autocomplete issues. +// 0.50.3 is last version to open Links properly. also works on Win11. var NWversion; -if ((os.platform() === 'win32') || (os.platform() === 'win64')) { +if (os.platform() === 'win32') { NWversion ='0.42.6' } else { NWversion ='0.50.3' } -// 0.45.6 Win7 connects; 0.42.3 fixed OSX Flashing; 0.46.X breaks Win7 connect -// maybe serial/usb needs updating -// revert to 0.42.6 due to Windows® users increased CLI-tab buffer/autocomplete issues. var nwBuilderOptions = { version: NWversion, files: './dist/**/*', From 85efd27f2ced7bebf77eee2b96b36264c59b1d6b Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Mon, 6 Jun 2022 11:47:41 -0500 Subject: [PATCH 3/3] add ImpulseRC Driver Fixer Link (#454) --- locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en/messages.json b/locales/en/messages.json index a570678c8..ce028db46 100644 --- a/locales/en/messages.json +++ b/locales/en/messages.json @@ -582,7 +582,7 @@ "message": "NEWS" }, "defaultWelcomeText": { - "message": "EmuFlight development is in heavy flux. Some UI features may be unfinished or some firmware features only accessible via CLI.

    EmuFlight source code and releases reside on GitHub.

    Helio/Strix IMUF can be flashed via the IMUF Flasher Tool or Downloaded from the IMUF releases repository.

    EmuFlight BlackBox Logs can be read with a fork of BBE available from this EmuFlight-BlackBox-Explorer repository.

    Latest CP210x Drivers.
    Latest STM USB VCP Drivers.
    Latest Zadig.
    " + "message": "EmuFlight development is in heavy flux. Some UI features may be unfinished or some firmware features only accessible via CLI.

    EmuFlight source code and releases reside on GitHub.

    Helio/Strix IMUF can be flashed via the IMUF Flasher Tool or Downloaded from the IMUF releases repository.

    EmuFlight BlackBox Logs can be read with a fork of BBE available from this EmuFlight-BlackBox-Explorer repository.

    CP210x Drivers.
    STM USB VCP Drivers.
    Zadig.
    ImpulseRC Driver Fixer.
    " }, "defaultContributingHead": { "message": "Contributing"