From 6efbb5bb1c71238527c3ab2756d9123e71bff0f9 Mon Sep 17 00:00:00 2001 From: Martin Enzinger Date: Thu, 15 Aug 2024 16:36:05 +0200 Subject: [PATCH] Add magic wand selection tool --- package-lock.json | 70 +++--- package.json | 1 + public/ico/magic-wand-add.png | Bin 0 -> 3019 bytes public/ico/magic-wand-remove.png | Bin 0 -> 2659 bytes public/ico/magic-wand.png | Bin 0 -> 2710 bytes src/data/enums/CustomCursorStyle.ts | 5 +- src/logic/actions/EditorActions.ts | 2 +- src/logic/context/ContextManager.ts | 5 +- src/logic/render/PolygonRenderEngine.ts | 55 +++- src/store/Actions.ts | 8 +- src/store/index.ts | 6 +- src/store/magic-wand/actionCreators.ts | 39 +++ src/store/magic-wand/reducer.ts | 43 ++++ src/store/magic-wand/types.ts | 44 ++++ src/store/selectors/MagicWandSelector.ts | 19 ++ src/utils/EditorUtil.ts | 11 +- src/utils/MagicWandUtil.ts | 236 ++++++++++++++++++ src/views/Common/Slider/Slider.scss | 31 +++ src/views/Common/Slider/Slider.tsx | 46 ++++ src/views/EditorView/Editor/Editor.scss | 2 +- .../EditorTopNavigationBar.tsx | 52 +++- 21 files changed, 634 insertions(+), 41 deletions(-) create mode 100644 public/ico/magic-wand-add.png create mode 100644 public/ico/magic-wand-remove.png create mode 100644 public/ico/magic-wand.png create mode 100644 src/store/magic-wand/actionCreators.ts create mode 100644 src/store/magic-wand/reducer.ts create mode 100644 src/store/magic-wand/types.ts create mode 100644 src/store/selectors/MagicWandSelector.ts create mode 100644 src/utils/MagicWandUtil.ts create mode 100644 src/views/Common/Slider/Slider.scss create mode 100644 src/views/Common/Slider/Slider.tsx diff --git a/package-lock.json b/package-lock.json index 449fbc12..75d3a2c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@emotion/styled": "^11.9.3", "@mui/material": "^5.9.1", "@mui/system": "^5.9.1", + "@techstark/opencv-js": "^4.10.0-beta.1", "@tensorflow-models/coco-ssd": "^2.2.2", "@tensorflow-models/posenet": "^2.2.2", "@tensorflow/tfjs": "^3.19.0", @@ -2147,6 +2148,11 @@ "@swc/core": "*" } }, + "node_modules/@techstark/opencv-js": { + "version": "4.10.0-beta.1", + "resolved": "https://registry.npmjs.org/@techstark/opencv-js/-/opencv-js-4.10.0-beta.1.tgz", + "integrity": "sha512-4ARkSxFCBvEg7qv7LHpbNW+XePua8Wdm6wUf3snz/i4nDFAgY6LPOLoWme2AOkZBbKMUir1O5kswJI5D+vXJjw==" + }, "node_modules/@tensorflow-models/coco-ssd": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@tensorflow-models/coco-ssd/-/coco-ssd-2.2.2.tgz", @@ -2661,8 +2667,7 @@ "node_modules/@types/node": { "version": "18.0.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.6.tgz", - "integrity": "sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==", - "license": "MIT" + "integrity": "sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==" }, "node_modules/@types/node-fetch": { "version": "2.6.2", @@ -3465,6 +3470,17 @@ "node": ">= 10.0.0" } }, + "node_modules/aws-sdk/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "node_modules/aws-sdk/node_modules/uuid": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", @@ -3707,17 +3723,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dev": true, - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4958,9 +4963,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -11085,6 +11090,11 @@ "@jest/create-cache-key-function": "^27.4.2" } }, + "@techstark/opencv-js": { + "version": "4.10.0-beta.1", + "resolved": "https://registry.npmjs.org/@techstark/opencv-js/-/opencv-js-4.10.0-beta.1.tgz", + "integrity": "sha512-4ARkSxFCBvEg7qv7LHpbNW+XePua8Wdm6wUf3snz/i4nDFAgY6LPOLoWme2AOkZBbKMUir1O5kswJI5D+vXJjw==" + }, "@tensorflow-models/coco-ssd": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@tensorflow-models/coco-ssd/-/coco-ssd-2.2.2.tgz", @@ -12061,6 +12071,17 @@ "xml2js": "0.4.19" }, "dependencies": { + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "uuid": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", @@ -12235,17 +12256,6 @@ "node-int64": "^0.4.0" } }, - "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -13143,9 +13153,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "for-each": { "version": "0.3.3", diff --git a/package.json b/package.json index e8e42880..eefe90d7 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@emotion/styled": "^11.9.3", "@mui/material": "^5.9.1", "@mui/system": "^5.9.1", + "@techstark/opencv-js": "^4.10.0-beta.1", "@tensorflow-models/coco-ssd": "^2.2.2", "@tensorflow-models/posenet": "^2.2.2", "@tensorflow/tfjs": "^3.19.0", diff --git a/public/ico/magic-wand-add.png b/public/ico/magic-wand-add.png new file mode 100644 index 0000000000000000000000000000000000000000..3fc9a3915bc764f5d4b829643eecb46a0dcffafd GIT binary patch literal 3019 zcmZ`*2|Uz!8~=|njz|bQskg?+T{A;irZE~!sAL?8YAwcK)?AEXqBP|UgxlKun-0svGe%Po1sz&nC$=j;Fg;o1Peiw1y25aLY%00#pA zFT4N%n*{)0vH0~)IM9$makuw#Z~*i{T?T+blmRHHLBJOPQ3u4)Kn*wm+4@O$h8O}8 zAf^PKV{ueK@}mz8l;Xz=JYvq83=0cGQ5k`3vNwx@Vg~zzI5ZFPIY*wvC%$hU^s^57 z0hq}FuzEn2W#_>L0EH&;futKKYz1RTQC;0b+#L>K$xJ%R+n4D>L2>CUF$=(Pv7kz) zgm@#kbQ*(=6feZ)Te{2hE5G}T#4N(~MKhZ*{e*Z!fTmF>> z&gfHH|CfyG&H6xE8^(tkapDTFBr2Ce^RS`PDGWB)58e=i!L8NyH_PYl)-m1xmATIH zCDWQoV+OmhyvY>2;m2g_pmpBjX0h(h6gHE_5l_E#8!MoAY5U*HCaT@f$$q zUptQ9uoX84{yIosu(u(&av`rz9E|@DQRQ({eEg0j;`!J|@cqq~bjdPS&ne28*JZL| zBz1mFi9tz7J$TrsuS|^hBg@>%ArQ4Hc%wSAT8X!w1h6Qhn}|_9#y?1&Z~BdU@y#Ek zw4vq8mn|zj8zY{Px{u6<9{H(rsQa7cEys%7qR*LkL_!sWr^uyVdnqm-+nWrO~_8$rLcukx(E5t7udDW#fY6LcSo}SVjKCM!v*~RLNNiFWKSlyFj6G z)sRk=--zVv>SS$qwPHG(iu)Yoe(sBNC= z_UDD9O*;zQghqne3HLr}QM}Re4x>xRM>=k2`ol7aft#nY?W@L<^Z8IqN4hKvl7h8Q zE}+T8_bZ1SUNius)P+6SJALI+Zb#XB>4}Ph;mhyZwd`KeeLSkVtEGo844;OGg8RBX zm+uY0)TtVwg9*uzq)C34tiYc)NTd5+Nwj|$TD`09g&D;n zVR&e||K6nVOuGLe&A|$_@$jm*zEUo@+Mgip#a$z@HCZ~UF7<;uh`;>i@ZOc9Oxb$j zkCAv>rC>$k(6^pJ@7k3$i`))A$?5lh?x9wE)uwVqvglr;MkBN}=k{$m4s**&QiST- z#OfTKGl-85D)Roml3 z=V}sMGVTx@Yfg2>*hcBahWM&vYYJXFD;^&(Cg`@r0W|pHHoaZ^;@*pe7`oKMAA#JmuAJRa( zV}R;3D-pfDp(Zpq(9m;O*3RHyP@tbuv)RTI2O!Dv?6|R4v(Hp4TXH42*;m|nkR66a z**K@e0(+uT39eJou;@I_sUf=4D5$IJ!daw`ot%CG#tAdgtx`JM*^=}%iP5-+-#d7t z43|@lYWU$4#j~pCvE#i=tyodE&cm_MqUqHwx^G&mdULfllz!Xg3@>w*H@)9E_#^MM z`Zzx`r+Hs4++Wz1sha?A7WR=7?@%JWiu^DsIW#6PR2c^@qhuY=Q6e^r`nnV1pCJs)lrWEF@k_4e@6vda~O86Lgd;1qcw)#jJ9 z>C}85cX^xIGTYdM*%5d_sa~LM85!tMx3rRGHB{g2je1xUu#(i!|zUbZX$3O)iVUxPJ&Vt9|iL}`)i@qi%`QnM&MjlYV$;Anc~yyPuU{b(-wLa zL)RvvLVUA)MTJjyX*`A&YC+%eaSq)@iQhUR{(a3hnN z5tV$zD{bC4tC~IwK_o5Hc<3#~2RD(9P}6587|?A~9TL<(_d0pjZ=6dd7==Mi`i4agY_mkZmY^0ULr;qn%ag>xziq7EKgNGxE$`$ zJk@&1wAbk9n`@%I0}+Zs_RF(3jR`N7ygZv(eXz=?j-$NXl5MS*+bA&g#S$y0cCR5a zhknRqp!@*1XoBs|b-McdA%;6^cR9Jrd)G$SmVtL!WM74hDzUjLdsk5ZQP9t3?z-EA zenLWOvlnb)$i~S~A94G4eOqBlsBK62`@4?!>rIBc!rw=2{^>P8=8e1{ZgJ@*Hbyve qyA)^u@=D}&p$~aRd7v;c5}{K}w3R}~4_J!-klGUu+g!8qI`=;}_thZ) literal 0 HcmV?d00001 diff --git a/public/ico/magic-wand-remove.png b/public/ico/magic-wand-remove.png new file mode 100644 index 0000000000000000000000000000000000000000..a92e45092762d8b06f83f9f9b52521996d9732b2 GIT binary patch literal 2659 zcmZ`*3p|tSAAh$oJgF$x*sUQq+lj3bnuf@6;*(&gp$W&-?tI-|zeVU7q*%yq`DA*V|o1@n=N< z08~6ZNdDj)EgQ?=;FI^XH~^gFVq7RL0B|N>NlKFk*GPtkKLr4Gm;iuiF93W1RiYOF zkcb6dKLavvpalE^(fum> zLvR2T)Tx6bG=l}meZjE7S$0K%L&jO0larGzS)4clgT`Z8a``c!9##eU7NZLKl^k*-`m6KQLSwZx)`iby1qz>nU7 z_a~9R(7}}xDmEd3hsR)KCM|Bp=Nt*L4Dh}zA(I_KV#PB#0T<+8c_O*|TdNyL2#wnViAEh`pJ^=AsW>_pja6CG>`n13k0CxrYPVX5+af)Ag? z1mE;x1Tdk+084dC@Zb*PUHME}0+%1a<+6$2p9KQud&Czq0VC^_C7tuREf=+5UlJ8T z=HER|RMaKB1-}k(V^5MxfDrOJ+^c4_jrQgGVpLFVO{VIl^ULm3pv_P8?F)`mIpk!35 zMSf!{l!w>bis_i{@Et$AE4i9*TOsUPB0}L-!S2MORZSm5`x5D~F*<0+lHr#(^n`*x zIqUdVjYoeQnT@D=^J194{H^to6cdrt0n-$ilstLROAR4(2XZd?T(psJ&skF7_ic^X zt|KW>bD`ocJ6Jhutg%=xPwqnDSa^%cJDpq6qEz&henFTVEF8&3!FT3EwaDjAplp=G z)1{-%g-COc$1vXez@mZDQm~4TzLh@j$2}hWbxvUVv^=c>5+2##@*gpxI`edH+RffM zCG5f7E>c_-``vr^v&$26O~W&ywd$}8UsX>h6`t`dAK7KE`%yb8M7-eroMo^u*#9c& zZXathPV!E@erh3BL6Q;9buS85&wyA`iAuvSk6$G_{6V)%4ra@nHGh~geP_n+Q+XAr z##k?j!MuUw!*n4-dfHD~1j>*A40)t_9CMXT*$z`2Axwq2_H%T(GGN|Sw0(J4sC26&xV`O&;!fA`B7N73?; z{o&UO(MW@R`!Z>-?`Bt@k6+9)%+8cpg*^P1kFG~1KVPFp%v z19A08tWMC)lbgv87aThSZ${+yogLD8?Acyb*Z4N5WaP%l3zVx^;wjShu5!(V%Cw^3 z{gEl9Xzttr{AOmA7w%7Jo5LeQ>#RwxK0JCgERZgRN4Ar0ccbBLj*-f%XJCeTZ-R+#KO=f4J5!D?&^?BL`$2@ZT=mA+M+;byxAsWHxr?(vARTk|Low#@u&hx1; z*%VgNYqIU&=4TDH^=ru4kV}~_3f4vFAUYXU?isCCM%@>2q=!YbuDv)>NuK^0n0=)F z{fYnT$FE3fvVK(1R=cLkxvpZm-6cYkDYlKqc?`ov`YL(!m`g`bz{K$A>s?t>w&AtX)8^K%F$9T<6D$=J7J;o&i;&`1W_xumX7*uW*L!Q%J%PIR1 zHZ34f$ZVm^15&}7(Dt5(Vh#O<7=8tBXx`f z`cY77??7#{wP4FoQg>OacA1NiyC_Jf^`XMRa`^LB>tDO5)E77Vg$PFCwM$x#qucEE zFq&@Mg*S%NUMVfNfB39BIepXm9;%k$@JBy%3BYmOi7?#X+<$$lQ>&y#Z(A1bP*ZIE z7(2#aD_ClN@C|Hkc*olyBaeR7=f-4-56>{Q<&V(8h~m~-0M|DbN@}0*)w~=Rx6y7{ z8^VpgPOT%EVv*f7rE|dE+c>)`6Z!jTk1#(YvQCr59fn(>wuMK7`TYRREWe)Ebg3_B zfSz?dRioiX-)mKRi>onV)>=J&$0;(izzr+0`9yV0OCP?F&fVLffR>vLeGHkB2TH$w fqYDbtJ^^K8CT)MkZHxl{lK@XQZ<2UJRQmq_rgsJs literal 0 HcmV?d00001 diff --git a/public/ico/magic-wand.png b/public/ico/magic-wand.png new file mode 100644 index 0000000000000000000000000000000000000000..a5690e4d1dfa140f8e4549409cb41abde208a932 GIT binary patch literal 2710 zcmZ`*3p|ti8-HhV9T~=i%r$W`8>Q{I9E{N)jZ`?oY{M8^+t@^XC{8ZbbRnW#(nS~d zT%r?F{v1gqgcN_5oa7b}(s`#&`FBq5`}ys8-sgF~&+~kL@AKQ|O(eMxWu!Jp0RSN5 z=s<7-B~`d2#K2$HV5&PPL<6mztpT7!AiYdp2R@@H4sOl>a6k_LV&VW`5j4fT27qv5 z0Ql1n0PyJmpv=m;?P?A>(r6xz0nW~V0jNs=>mc#~6x1N#4L~*k!fc=h?0~3#)!iT% zKm@eG!R2+74v2nE9}aalX(338IA_G{(pn{ckWXJ>Um0q34G% zaF4#y`oCg)GV3#Fbu&KqXfEskPonc_3{L_*lorMT`B-9%jm=kk`-kTnyERCUA0pRy zzDL?J8BF#r7MVh`#C$2X#GTH7-Cd2akvtW+?KH>}594(xbHIwssELW}ITv|$j z$iHXYQcBf)2K;vr^Kc|syYnG$y`8Tr?MAMYtIB&K5oEhA9SI)XgFxcnq)BU5R-CM` z+0v8P*|29vCL!pVZeHE)b}fTeJJHBzwD-OqIbDz=?UE6HUyvEECR1Yiq`Q z{OHdnqs)Q*6nlMSc#v2>d~^YE>h09z zsq#;9pZKn~t*ojSFwaF?ihv4a;YZ%$#e08d3=I+o+6MsZ?&ya_Puh!)doQQ=$@rXw zbnVlmi!WZP4-Y(@tP#I!$wIU`#=O#(X#FX|Ywn1ETQKe@fKFQsV+7Ir0-75H>VBki z`PZftXD-*0Y#JXntUuv+UQ#aZ@Ey4Zw>%+HnB1Nhh=@hcl;{wxyo2lS#kwq+UwDN8%X$o7>=~Txb0FnK{ZJ5$~a?G@@RPE#2 z!4OT^nqbd`Y0J9R~G%3bupLjL`Li7|=r z_&q0+HSRZU!Ye!-33iV_x2TDv#h_#M#>&JLO>28;EKgay^ckhx<;eP3Q3qAWA3=j9 zRbDxrE#+w>y;GR-Yj~$JmwT--rDm8|Tu$$B(q;AUg&}lfk>e%{gPWID_EX-v6SMZg_# zGDtE;AUola=+b+_gUnwf$cSy#>d|1JPF&6 z=qyhqFO(8{Ntn2uXZ6vJdNNEZYUDh*%SW$;7-9dymz?8_;frqLL`Rywbgnw6Mk+(H z5~r1C*fZ7&7mb2#VM-uJwLftLPuwFL(?>zBWk=WSh)ws7<_u?O%U2IgcA` zt8}FCruf2@+}xsLMBk)2rF~`Xf1FE4PGgpiy_m9V8`G)~ug3PbPr`gi{>xG7fw>9# zF!5Vh9~b`wed#omR@>@nqxY_!Hk(7wyrEow+vsyMy1^{NmSKKix_@YW8{(J3=V|Z5&PwC6QSaxj7xHmW&)aldSO*)RNZ(QE~{y&8yJ7Cx|wwDCLwZ6SMiH|J(CG zFxF<=yny_9_hI9YDrKbJUJ8P8^a@gONU@HW-Whtsf0k`mU6NFPJ$n7Cr|ub%ZF#Uy zY8^?(OQ6bHIKxtg_XP4u_v37FCk!a_c@=6o~5mI9=q?Z_Pu6~T3ttjzx4C$X2L z-ncl_joF*ENuKzqPAP2j+-Q^QE|T6fb{ZMZ54qWzC@boUxy)|c5K`T9uA|IwMd9%y zk>xOp1DU!GEmp19ksHsx5*r%W5fZS}|Da;qu8sZmv1xHKUifY0Xy-yGwDCLq7eoms7ytkO literal 0 HcmV?d00001 diff --git a/src/data/enums/CustomCursorStyle.ts b/src/data/enums/CustomCursorStyle.ts index 4cc591fa..ae741dfe 100644 --- a/src/data/enums/CustomCursorStyle.ts +++ b/src/data/enums/CustomCursorStyle.ts @@ -6,5 +6,8 @@ export enum CustomCursorStyle { CANCEL = "CANCEL", CLOSE = "CLOSE", GRAB = "GRAB", - GRABBING = "GRABBING" + GRABBING = "GRABBING", + MAGIC_WAND = "MAGIC_WAND", + MAGIC_WAND_ADD = "MAGIC_WAND_ADD", + MAGIC_WAND_REMOVE = "MAGIC_WAND_REMOVE", } \ No newline at end of file diff --git a/src/logic/actions/EditorActions.ts b/src/logic/actions/EditorActions.ts index ab71e34e..c2235baa 100644 --- a/src/logic/actions/EditorActions.ts +++ b/src/logic/actions/EditorActions.ts @@ -124,7 +124,7 @@ export class EditorActions { EditorModel.cursor.style.top = mousePositionOverViewPort.y + "px"; EditorModel.cursor.style.display = "block"; - if (isMouseOverImage && ![CustomCursorStyle.GRAB, CustomCursorStyle.GRABBING].includes(GeneralSelector.getCustomCursorStyle())) { + if (isMouseOverImage && ![CustomCursorStyle.GRAB, CustomCursorStyle.GRABBING, CustomCursorStyle.MAGIC_WAND].includes(GeneralSelector.getCustomCursorStyle())) { const imageSize: ISize = ImageUtil.getSize(EditorModel.image); const scale: number = imageSize.width / viewPortContentImageRect.width; const mousePositionOverImage: IPoint = PointUtil.multiply( diff --git a/src/logic/context/ContextManager.ts b/src/logic/context/ContextManager.ts index 2a10e99e..b46de8d2 100644 --- a/src/logic/context/ContextManager.ts +++ b/src/logic/context/ContextManager.ts @@ -1,12 +1,13 @@ import {ContextType} from "../../data/enums/ContextType"; import {HotKeyAction} from "../../data/HotKeyAction"; import {store} from "../../index"; -import {updateActiveContext} from "../../store/general/actionCreators"; +import {updateActiveContext, updateCustomCursorStyle} from "../../store/general/actionCreators"; import {xor, isEmpty} from "lodash"; import {EditorContext} from "./EditorContext"; import {PopupContext} from "./PopupContext"; import {GeneralSelector} from "../../store/selectors/GeneralSelector"; import {EventType} from "../../data/enums/EventType"; +import {CustomCursorStyle} from "../../data/enums/CustomCursorStyle"; export class ContextManager { private static activeCombo: string[] = []; @@ -56,11 +57,13 @@ export class ContextManager { ContextManager.addToCombo(keyCode); } ContextManager.execute(event); + store.dispatch(updateCustomCursorStyle(CustomCursorStyle.MAGIC_WAND_ADD)); } private static onUp(event: KeyboardEvent): void { const keyCode: string = ContextManager.getKeyCodeFromEvent(event); ContextManager.removeFromCombo(keyCode); + store.dispatch(updateCustomCursorStyle(CustomCursorStyle.MAGIC_WAND)); } public static onFocus() { diff --git a/src/logic/render/PolygonRenderEngine.ts b/src/logic/render/PolygonRenderEngine.ts index 467bb446..8261116c 100644 --- a/src/logic/render/PolygonRenderEngine.ts +++ b/src/logic/render/PolygonRenderEngine.ts @@ -27,6 +27,11 @@ import {GeneralSelector} from '../../store/selectors/GeneralSelector'; import {Settings} from '../../settings/Settings'; import {LabelUtil} from '../../utils/LabelUtil'; import {PolygonUtil} from '../../utils/PolygonUtil'; +import {MagicWandUtil} from '../../utils/MagicWandUtil'; +import {EditorModel} from '../../staticModels/EditorModel'; +import {PlatformUtil} from '../../utils/PlatformUtil'; +import {MagicWandSelector} from '../../store/selectors/MagicWandSelector'; +import {updateMagicWandMask,updateMagicWandRect} from '../../store/magic-wand/actionCreators'; export class PolygonRenderEngine extends BaseRenderEngine { @@ -69,7 +74,9 @@ export class PolygonRenderEngine extends BaseRenderEngine { public mouseDownHandler(data: EditorData): void { const isMouseOverCanvas: boolean = RenderEngineUtil.isMouseOverCanvas(data); if (isMouseOverCanvas) { - if (this.isCreationInProgress()) { + if (MagicWandSelector.getMagicWandActiveStatus()) { + this.magicWandSelection(data); + } else if (this.isCreationInProgress()) { const isMouseOverStartAnchor: boolean = this.isMouseOverAnchor( data.mousePositionOnViewPortContent, this.activePath[0]); if (isMouseOverStartAnchor) { @@ -165,7 +172,16 @@ export class PolygonRenderEngine extends BaseRenderEngine { if (!!this.canvas && !!data.mousePositionOnViewPortContent && !GeneralSelector.getImageDragModeStatus()) { const isMouseOverCanvas: boolean = RenderEngineUtil.isMouseOverCanvas(data); if (isMouseOverCanvas) { - if (this.isCreationInProgress()) { + if (MagicWandSelector.getMagicWandActiveStatus()) { + if (data.activeKeyCombo.includes("Shift")) { + store.dispatch(updateCustomCursorStyle(CustomCursorStyle.MAGIC_WAND_ADD)); + } else if (data.activeKeyCombo.includes(PlatformUtil.isMac(window.navigator.userAgent) ? "Alt" : "Control")) { + store.dispatch(updateCustomCursorStyle(CustomCursorStyle.MAGIC_WAND_REMOVE)); + + } else { + store.dispatch(updateCustomCursorStyle(CustomCursorStyle.MAGIC_WAND)); + } + } else if (this.isCreationInProgress()) { const isMouseOverStartAnchor: boolean = this.isMouseOverAnchor(data.mousePositionOnViewPortContent, this.activePath[0]); if (isMouseOverStartAnchor && this.activePath.length > 2) store.dispatch(updateCustomCursorStyle(CustomCursorStyle.CLOSE)); @@ -297,6 +313,12 @@ export class PolygonRenderEngine extends BaseRenderEngine { } } + private removePolygonLabel(labelId: string) { + const imageData: ImageData = LabelsSelector.getActiveImageData(); + imageData.labelPolygons = imageData.labelPolygons.filter((polygon: LabelPolygon) => polygon.id !== labelId); + store.dispatch(updateImageDataById(imageData.id, imageData)); + }; + private addPolygonLabel(polygon: IPoint[]) { const activeLabelId = LabelsSelector.getActiveLabelNameId(); const imageData: ImageData = LabelsSelector.getActiveImageData(); @@ -447,4 +469,33 @@ export class PolygonRenderEngine extends BaseRenderEngine { } return null; } + + private magicWandSelection(data: EditorData): void { + const mousePositionSnapped: IPoint = RectUtil.snapPointToRect(data.mousePositionOnViewPortContent, data.viewPortContentImageRect); + const actualPosition = RenderEngineUtil.transferPointFromViewPortContentToImage(mousePositionSnapped, data); + this.activePath.push(mousePositionSnapped); + const shiftKey = data.activeKeyCombo.includes("Shift"); + const altKey = data.activeKeyCombo.includes(PlatformUtil.isMac(window.navigator.userAgent) ? "Alt" : "Control"); + const modifier = shiftKey ? "add" : altKey ? "remove" : undefined; + if (modifier && MagicWandSelector.getMagicWandMask()) { + const { mask: updatedMask, rect: updatedRect, path } = MagicWandUtil.editSelection(EditorModel.image, actualPosition, MagicWandSelector.getMagicWandMask(), MagicWandSelector.getMagicWandRect(), MagicWandSelector.getMagicWandTolerance(), modifier); + if (path.length === 0) return; + store.dispatch(updateMagicWandMask(updatedMask)); + store.dispatch(updateMagicWandRect(updatedRect)); + this.activePath = path.map((point: IPoint) => RenderEngineUtil.transferPointFromImageToViewPortContent(point, data)); + const polygonOnImage: IPoint[] = RenderEngineUtil.transferPolygonFromViewPortContentToImage(this.activePath, data); + this.removePolygonLabel(LabelsSelector.getActiveLabelId()); + this.addPolygonLabel(polygonOnImage); + this.finishLabelCreation(); + } else { + const { mask: updatedMask, rect: updatedRect, path } = MagicWandUtil.getSelection(EditorModel.image, actualPosition, MagicWandUtil.initMatrix(), MagicWandUtil.initRectangle(), MagicWandSelector.getMagicWandTolerance()); + if (path.length === 0) return; + store.dispatch(updateMagicWandMask(updatedMask)); + store.dispatch(updateMagicWandRect(updatedRect)); + this.activePath = path.map((point: IPoint) => RenderEngineUtil.transferPointFromImageToViewPortContent(point, data)); + const polygonOnImage: IPoint[] = RenderEngineUtil.transferPolygonFromViewPortContentToImage(this.activePath, data); + this.addPolygonLabel(polygonOnImage); + this.finishLabelCreation(); + } + } } diff --git a/src/store/Actions.ts b/src/store/Actions.ts index b6d3921e..51495631 100644 --- a/src/store/Actions.ts +++ b/src/store/Actions.ts @@ -31,8 +31,14 @@ export enum Action { UPDATE_HIGHLIGHTED_LABEL_ID = '@@UPDATE_HIGHLIGHTED_LABEL_ID', UPDATE_LABEL_NAMES = '@@UPDATE_LABEL_NAMES', UPDATE_FIRST_LABEL_CREATED_FLAG = '@@UPDATE_FIRST_LABEL_CREATED_FLAG', + + // MAGIC WAND + UPDATE_MAGIC_WAND_ACTIVE_STATUS = '@@UPDATE_MAGIC_WAND_ACTIVE_STATUS', + UPDATE_MAGIC_WAND_TOLERANCE = '@@UPDATE_MAGIC_WAND_TOLERANCE', + UPDATE_MAGIC_WAND_MASK = '@@UPDATE_MAGIC_WAND_MASK', + UPDATE_MAGIC_WAND_RECT = '@@UPDATE_MAGIC_WAND_RECT', // NOTIFICATIONS SUBMIT_NEW_NOTIFICATION = '@@SUBMIT_NEW_NOTIFICATION', - DELETE_NOTIFICATION_BY_ID = '@@DELETE_NOTIFICATION_BY_ID' + DELETE_NOTIFICATION_BY_ID = '@@DELETE_NOTIFICATION_BY_ID', } diff --git a/src/store/index.ts b/src/store/index.ts index 458650b7..e23f468c 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,14 +1,16 @@ -import { combineReducers } from 'redux'; +import {combineReducers} from 'redux'; import {labelsReducer} from './labels/reducer'; import {generalReducer} from './general/reducer'; import {aiReducer} from './ai/reducer'; +import {magicWandReducer} from './magic-wand/reducer'; import {notificationsReducer} from './notifications/reducer'; export const rootReducer = combineReducers({ general: generalReducer, labels: labelsReducer, ai: aiReducer, - notifications: notificationsReducer + magicWand: magicWandReducer, + notifications: notificationsReducer, }); export type AppState = ReturnType; diff --git a/src/store/magic-wand/actionCreators.ts b/src/store/magic-wand/actionCreators.ts new file mode 100644 index 00000000..d4612cc9 --- /dev/null +++ b/src/store/magic-wand/actionCreators.ts @@ -0,0 +1,39 @@ +import {MagicWandActionTypes} from './types'; +import {Action} from '../Actions'; +import {Matrix,Rectangle} from '../../utils/MagicWandUtil'; + +export function updateMagicWandActiveStatus(magicWandActive: boolean): MagicWandActionTypes { + return { + type: Action.UPDATE_MAGIC_WAND_ACTIVE_STATUS, + payload: { + magicWandActive, + }, + }; +} + +export function updateMagicWandTolerance(magicWandTolerance: number): MagicWandActionTypes { + return { + type: Action.UPDATE_MAGIC_WAND_TOLERANCE, + payload: { + magicWandTolerance, + }, + }; +} + +export function updateMagicWandMask(magicWandMask: Matrix): MagicWandActionTypes { + return { + type: Action.UPDATE_MAGIC_WAND_MASK, + payload: { + magicWandMask, + }, + }; +} + +export function updateMagicWandRect(magicWandRect: Rectangle): MagicWandActionTypes { + return { + type: Action.UPDATE_MAGIC_WAND_RECT, + payload: { + magicWandRect, + }, + }; +} diff --git a/src/store/magic-wand/reducer.ts b/src/store/magic-wand/reducer.ts new file mode 100644 index 00000000..baf30891 --- /dev/null +++ b/src/store/magic-wand/reducer.ts @@ -0,0 +1,43 @@ +import {MagicWandActionTypes, MagicWandState} from './types'; +import {Action} from '../Actions'; + +const initialState: MagicWandState = { + magicWandActive: false, + magicWandTolerance: 30, + magicWandMask: undefined, + magicWandRect: undefined, +}; + +export function magicWandReducer( + state = initialState, + action: MagicWandActionTypes +): MagicWandState { + switch (action.type) { + case Action.UPDATE_MAGIC_WAND_ACTIVE_STATUS: { + return { + ...state, + magicWandActive: action.payload.magicWandActive + } + } + case Action.UPDATE_MAGIC_WAND_TOLERANCE: { + return { + ...state, + magicWandTolerance: action.payload.magicWandTolerance + } + } + case Action.UPDATE_MAGIC_WAND_MASK: { + return { + ...state, + magicWandMask: action.payload.magicWandMask + } + } + case Action.UPDATE_MAGIC_WAND_RECT: { + return { + ...state, + magicWandRect: action.payload.magicWandRect + } + } + default: + return state; + } +} diff --git a/src/store/magic-wand/types.ts b/src/store/magic-wand/types.ts new file mode 100644 index 00000000..c7b2d3a4 --- /dev/null +++ b/src/store/magic-wand/types.ts @@ -0,0 +1,44 @@ +import {Matrix,Rectangle} from '../../utils/MagicWandUtil'; +import {Action} from '../Actions'; + +export type MagicWandState = { + magicWandActive: boolean; + magicWandTolerance: number; + magicWandMask: Matrix; + magicWandRect: Rectangle; +} + +interface updateMagicWandActiveStatus { + type: typeof Action.UPDATE_MAGIC_WAND_ACTIVE_STATUS; + payload: { + magicWandActive: boolean; + } +} + +interface updateMagicWandTolerance { + type: typeof Action.UPDATE_MAGIC_WAND_TOLERANCE; + payload: { + magicWandTolerance: number; + } +} + +interface updateMagicWandMask { + type: typeof Action.UPDATE_MAGIC_WAND_MASK; + payload: { + magicWandMask: Matrix; + } +} + +interface updateMagicWandRect { + type: typeof Action.UPDATE_MAGIC_WAND_RECT; + payload: { + magicWandRect: Rectangle; + } +} + +export type MagicWandActionTypes = updateMagicWandActiveStatus + | updateMagicWandTolerance + | updateMagicWandMask + | updateMagicWandRect; + + diff --git a/src/store/selectors/MagicWandSelector.ts b/src/store/selectors/MagicWandSelector.ts new file mode 100644 index 00000000..76c9d14b --- /dev/null +++ b/src/store/selectors/MagicWandSelector.ts @@ -0,0 +1,19 @@ +import {store} from '../..'; + +export class MagicWandSelector { + public static getMagicWandActiveStatus(): boolean { + return store.getState().magicWand.magicWandActive; + } + + public static getMagicWandTolerance(): number{ + return store.getState().magicWand.magicWandTolerance; + } + + public static getMagicWandMask(): any{ + return store.getState().magicWand.magicWandMask; + } + + public static getMagicWandRect(): any{ + return store.getState().magicWand.magicWandRect; + } +} diff --git a/src/utils/EditorUtil.ts b/src/utils/EditorUtil.ts index d03dc41d..51db23bc 100644 --- a/src/utils/EditorUtil.ts +++ b/src/utils/EditorUtil.ts @@ -18,6 +18,12 @@ export class EditorUtil { return "ico/hand-fill.png"; case CustomCursorStyle.GRABBING: return "ico/hand-fill-grab.png"; + case CustomCursorStyle.MAGIC_WAND: + return "ico/magic-wand.png"; + case CustomCursorStyle.MAGIC_WAND_ADD: + return "ico/magic-wand-add.png"; + case CustomCursorStyle.MAGIC_WAND_REMOVE: + return "ico/magic-wand-remove.png"; default: return null; } @@ -32,7 +38,10 @@ export class EditorUtil { "close": cursorStyle === CustomCursorStyle.CLOSE, "cancel": cursorStyle === CustomCursorStyle.CANCEL, "grab": cursorStyle === CustomCursorStyle.GRAB, - "grabbing": cursorStyle === CustomCursorStyle.GRABBING + "grabbing": cursorStyle === CustomCursorStyle.GRABBING, + "magic-wand": cursorStyle === CustomCursorStyle.MAGIC_WAND, + "magic-wand-add": cursorStyle === CustomCursorStyle.MAGIC_WAND_ADD, + "magic-wand-remove": cursorStyle === CustomCursorStyle.MAGIC_WAND_REMOVE, } ); }; diff --git a/src/utils/MagicWandUtil.ts b/src/utils/MagicWandUtil.ts new file mode 100644 index 00000000..e3664a72 --- /dev/null +++ b/src/utils/MagicWandUtil.ts @@ -0,0 +1,236 @@ +import cv, { Mat, Rect } from "@techstark/opencv-js"; +import { IPoint } from "src/interfaces/IPoint"; + +export type Matrix = Mat; +export type Rectangle = Rect; + +export class MagicWandUtil { + public static initMatrix() { + return new cv.Mat(); + } + + public static initRectangle() { + return new cv.Rect(); + } + + private static isPointInsideRect( + rect: cv.Rect, + x: number, + y: number + ): boolean { + return ( + x >= rect.x && + x <= rect.x + rect.width && + y >= rect.y && + y <= rect.y + rect.height + ); + } + + private static getContours(mask: cv.Mat): cv.MatVector { + let contours = new cv.MatVector(); + let hierarchy = new cv.Mat(); + + const contourInput = mask.clone(); + + cv.findContours( + contourInput, + contours, + hierarchy, + cv.RETR_CCOMP, + cv.CHAIN_APPROX_TC89_L1 + ); + + let poly = new cv.MatVector(); + for (let i = 0; i < contours.size(); ++i) { + let tmp = new cv.Mat(); + let cnt = contours.get(i); + cv.approxPolyDP(cnt, tmp, 3, true); + poly.push_back(tmp); + cnt.delete(); + tmp.delete(); + } + + return poly; + } + + private static removeOutliers( + rect: cv.Rect, + matVector: cv.MatVector + ): cv.MatVector { + const poly = matVector.clone(); + + for (let i = 0; i < poly.size(); i++) { + let c = poly.get(i); + for (let j = 0; j < c.data32S.length; j += 2) { + if ( + c.data32S[j] < rect.x || + c.data32S[j] > rect.x + rect.width || + c.data32S[j + 1] < rect.y || + c.data32S[j + 1] > rect.y + rect.height + ) { + c.data32S[j] = 0; + c.data32S[j + 1] = 0; + } + } + } + + return poly; + } + + private static getOutline( + mask: cv.Mat, + rect: cv.Rect, + poly: cv.MatVector + ): IPoint[] { + let mask1 = new cv.Mat(mask.rows, mask.cols, cv.CV_8U, new cv.Scalar(0)); + + let contours = new cv.MatVector(); + let hierarchy = new cv.Mat(); + + cv.fillPoly(mask1, poly, new cv.Scalar(255)); + cv.findContours( + mask1, + contours, + hierarchy, + cv.RETR_EXTERNAL, + cv.CHAIN_APPROX_SIMPLE + ); + + let approxPoly: IPoint[] = []; + for (let i = 0; i < contours.size(); i++) { + let c = contours.get(i); + for (let j = 0; j < c.data32S.length; j += 2) { + const x = c.data32S[j]; + const y = c.data32S[j + 1]; + if (this.isPointInsideRect(rect, x, y)) { + approxPoly.push({ x, y }); + } + } + } + + return approxPoly; + } + + private static removeDuplicates(rect: cv.Rect, points: IPoint[]): IPoint[] { + const center = { + x: rect.x + rect.width / 2, + y: rect.y + rect.height / 2, + } as IPoint; + const result = []; + const groups = []; + + for (let i = 0; i < points.length; i++) { + let found = false; + for (let j = 0; j < groups.length; j++) { + if ( + Math.abs(points[i].x - groups[j][0].x) < 10 && + Math.abs(points[i].y - groups[j][0].y) < 10 + ) { + groups[j].push(points[i]); + found = true; + break; + } + } + if (!found) { + groups.push([points[i]]); + } + } + + for (let i = 0; i < groups.length; i++) { + let minDist = 1000000; + let minIndex = -1; + for (let j = 0; j < groups[i].length; j++) { + const dist = Math.sqrt( + Math.pow(center.x - groups[i][j].x, 2) + + Math.pow(center.y - groups[i][j].y, 2) + ); + if (dist < minDist) { + minDist = dist; + minIndex = j; + } + } + result.push(groups[i][minIndex]); + } + + return result; + } + + private static findExteriorContour(mask: cv.Mat, rect: cv.Rect): IPoint[] { + const contourPoly = this.getContours(mask); + const updatedPoly = this.removeOutliers(rect, contourPoly); + const points = this.getOutline(mask, rect, updatedPoly); + const deduplicatedPoints = this.removeDuplicates(rect, points); + + contourPoly.delete(); + updatedPoly.delete(); + return deduplicatedPoints; + } + + public static editSelection( + imageElement: HTMLImageElement, + startPoint: IPoint, + mask: cv.Mat, + rect: cv.Rect, + tolerance: number, + modifier?: "add" | "remove" + ): { mask: cv.Mat; rect: cv.Rect; path: IPoint[] } { + const { x, y } = startPoint; + + const img = cv.imread(imageElement); + const floodMask = cv.Mat.zeros(img.rows + 2, img.cols + 2, cv.CV_8U); + const imageMatrix = new cv.Mat(img.cols, img.rows, cv.CV_8U); + + cv.cvtColor(img, imageMatrix, cv.COLOR_RGBA2RGB); + + let newRect = new cv.Rect(); + + cv.floodFill( + imageMatrix, + floodMask, + new cv.Point(x, y), + cv.Scalar.all(255), + newRect, + cv.Scalar.all(tolerance), + cv.Scalar.all(tolerance), + 4 | cv.FLOODFILL_FIXED_RANGE | cv.FLOODFILL_MASK_ONLY | (255 << 8) + ); + + if (modifier) { + rect = new cv.Rect( + Math.min(rect.x, newRect.x), + Math.min(rect.y, newRect.y), + Math.max(rect.x + rect.width, newRect.x + newRect.width) - + Math.min(rect.x, newRect.x), + Math.max(rect.y + rect.height, newRect.y + newRect.height) - + Math.min(rect.y, newRect.y) + ); + } else { + rect = newRect; + } + + const maskClone = mask.clone(); + const floodMaskClone = floodMask.clone(); + + if (modifier === "add") { + cv.bitwise_or(maskClone, floodMaskClone, mask); + } else if (modifier === "remove") { + const invertedFloodMask = new cv.Mat(); + cv.bitwise_not(floodMaskClone, invertedFloodMask); + cv.bitwise_and(maskClone, invertedFloodMask, mask); + } else { + mask = floodMaskClone; + } + + return { mask, rect, path: this.findExteriorContour(mask, rect) }; + } + + public static getSelection( + imageElement: HTMLImageElement, + startPoint: IPoint, + mask: cv.Mat, + rect: cv.Rect, + tolerance: number + ): { mask: cv.Mat; rect: cv.Rect; path: IPoint[] } { + return this.editSelection(imageElement, startPoint, mask, rect, tolerance); + } +} diff --git a/src/views/Common/Slider/Slider.scss b/src/views/Common/Slider/Slider.scss new file mode 100644 index 00000000..73ddc73f --- /dev/null +++ b/src/views/Common/Slider/Slider.scss @@ -0,0 +1,31 @@ +@import '../../../settings/Settings'; + +* { + box-sizing: border-box; +} + +.SliderWrapper { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; + align-content: flex-start; + + padding-right: 5px; + margin-right: 5px; + background-color: none; + + .Slider { + width: 100px; + margin: 0 5px; + color: $darkThemeThirdColor !important; + background-color: $darkThemeSecondColor !important; + + .MuiSlider-thumb { + width: 8px; + height: 8px; + background-color: $darkThemeThirdColor; + } + } +} \ No newline at end of file diff --git a/src/views/Common/Slider/Slider.tsx b/src/views/Common/Slider/Slider.tsx new file mode 100644 index 00000000..02a76f0b --- /dev/null +++ b/src/views/Common/Slider/Slider.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { Slider as MuiSlider } from '@mui/material'; +import './Slider.scss'; + +interface IProps { + label?: string; + onChange?: (value: number) => any; + onFocus?: (event: React.FocusEvent) => any; + style?: React.CSSProperties; + minValue?: number; + maxValue?: number; + step?: number; + value?: number; +} + +export const Slider = (props: IProps) => { + + const { + label, + onChange, + onFocus, + style, + minValue, + maxValue, + step, + value, + } = props; + + return ( +
+ onChange(value as number)} + /> +
+ ); +}; diff --git a/src/views/EditorView/Editor/Editor.scss b/src/views/EditorView/Editor/Editor.scss index a0c97f76..f93d9b93 100644 --- a/src/views/EditorView/Editor/Editor.scss +++ b/src/views/EditorView/Editor/Editor.scss @@ -77,7 +77,7 @@ border: 2px solid transparent; } - &.move, &.add, &.resize, &.close, &.cancel, &.grab, &.grabbing { + &.move, &.add, &.resize, &.close, &.cancel, &.grab, &.grabbing, &.magic-wand, &.magic-wand-add, &.magic-wand-remove { > img { display: block; } diff --git a/src/views/EditorView/EditorTopNavigationBar/EditorTopNavigationBar.tsx b/src/views/EditorView/EditorTopNavigationBar/EditorTopNavigationBar.tsx index dec53fc8..703e278c 100644 --- a/src/views/EditorView/EditorTopNavigationBar/EditorTopNavigationBar.tsx +++ b/src/views/EditorView/EditorTopNavigationBar/EditorTopNavigationBar.tsx @@ -5,9 +5,11 @@ import classNames from 'classnames'; import { AppState } from '../../../store'; import { connect } from 'react-redux'; import { updateCrossHairVisibleStatus, updateImageDragModeStatus } from '../../../store/general/actionCreators'; +import { updateMagicWandActiveStatus, updateMagicWandTolerance } from '../../../store/magic-wand/actionCreators'; import { GeneralSelector } from '../../../store/selectors/GeneralSelector'; import { ViewPointSettings } from '../../../settings/ViewPointSettings'; import { ImageButton } from '../../Common/ImageButton/ImageButton'; +import { Slider } from '../../Common/Slider/Slider'; import { ViewPortActions } from '../../../logic/actions/ViewPortActions'; import { LabelsSelector } from '../../../store/selectors/LabelsSelector'; import { LabelType } from '../../../data/enums/LabelType'; @@ -15,6 +17,7 @@ import { AISelector } from '../../../store/selectors/AISelector'; import { ISize } from '../../../interfaces/ISize'; import { AIActions } from '../../../logic/actions/AIActions'; import { Fade, styled, Tooltip, tooltipClasses, TooltipProps } from '@mui/material'; + const BUTTON_SIZE: ISize = { width: 30, height: 30 }; const BUTTON_PADDING: number = 10; @@ -66,8 +69,11 @@ interface IProps { activeContext: ContextType; updateImageDragModeStatusAction: (imageDragMode: boolean) => any; updateCrossHairVisibleStatusAction: (crossHairVisible: boolean) => any; + updateMagicWandActiveStatusAction: (magicWandActive: boolean) => any; + updateMagicWandToleranceAction: (magicWandTolerance: number) => any; imageDragMode: boolean; crossHairVisible: boolean; + magicWandActive: boolean; activeLabelType: LabelType; } @@ -76,8 +82,11 @@ const EditorTopNavigationBar: React.FC = ( activeContext, updateImageDragModeStatusAction, updateCrossHairVisibleStatusAction, + updateMagicWandActiveStatusAction, + updateMagicWandToleranceAction, imageDragMode, crossHairVisible, + magicWandActive, activeLabelType }) => { const getClassName = () => { @@ -102,6 +111,17 @@ const EditorTopNavigationBar: React.FC = ( updateCrossHairVisibleStatusAction(!crossHairVisible); }; + const magicWandOnClick = () => { + if (magicWandActive) { + updateMagicWandToleranceAction(30); + } + updateMagicWandActiveStatusAction(!magicWandActive); + }; + + const magicWandToleranceChange = (sliderValue: number) => { + updateMagicWandToleranceAction(sliderValue); + }; + const withAI = ( (activeLabelType === LabelType.RECT && AISelector.isAISSDObjectDetectorModelLoaded()) || (activeLabelType === LabelType.RECT && AISelector.isAIYOLOObjectDetectorModelLoaded()) || @@ -169,6 +189,10 @@ const EditorTopNavigationBar: React.FC = ( imageDragOnClick ) } + + + +
{ getButtonWithTooltip( 'cursor-cross-hair', @@ -180,6 +204,28 @@ const EditorTopNavigationBar: React.FC = ( crossHairOnClick ) } + { + activeLabelType === LabelType.POLYGON && getButtonWithTooltip( + 'magic-wand', + magicWandActive ? 'turn-off cursor magic-wand' : 'turn-on cursor magic-wand', + 'ico/magic-wand.png', + 'magic-wand', + magicWandActive, + undefined, + magicWandOnClick + ) + } + { + activeLabelType === LabelType.POLYGON && magicWandActive && + + }
{withAI &&
{ @@ -211,13 +257,17 @@ const EditorTopNavigationBar: React.FC = ( const mapDispatchToProps = { updateImageDragModeStatusAction: updateImageDragModeStatus, - updateCrossHairVisibleStatusAction: updateCrossHairVisibleStatus + updateCrossHairVisibleStatusAction: updateCrossHairVisibleStatus, + updateMagicWandActiveStatusAction: updateMagicWandActiveStatus, + updateMagicWandToleranceAction: updateMagicWandTolerance, }; const mapStateToProps = (state: AppState) => ({ activeContext: state.general.activeContext, imageDragMode: state.general.imageDragMode, crossHairVisible: state.general.crossHairVisible, + magicWandActive: state.magicWand.magicWandActive, + magicWandTolerance: state.magicWand.magicWandTolerance, activeLabelType: state.labels.activeLabelType });