From 36486a7232b8ff2bda16a16793db8b2d5d9a907c Mon Sep 17 00:00:00 2001 From: ienaga Date: Fri, 13 Mar 2026 14:58:22 +0900 Subject: [PATCH 01/26] =?UTF-8?q?#263=20=E3=83=9E=E3=82=B9=E3=82=AF?= =?UTF-8?q?=E5=87=A6=E7=90=86=E5=BE=8C=E3=81=AE=E3=83=95=E3=82=A3=E3=83=AB?= =?UTF-8?q?=E3=82=BF=E3=83=BC=E9=81=A9=E7=94=A8=E3=81=AE=E3=83=90=E3=82=B0?= =?UTF-8?q?=E3=82=92=E6=94=B9=E4=BF=AE=E3=80=81=E4=B8=8D=E8=A6=81=E3=81=AA?= =?UTF-8?q?=E3=82=B3=E3=83=BC=E3=83=89=E3=82=84=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- e2e/pages/mask/sprite-mask.html | 76 +- .../mask-sprite-webgl-darwin.png | Bin 28244 -> 61024 bytes .../filter-quality-webgpu-darwin.png | Bin 30990 -> 32124 bytes .../mask-sprite-webgpu-darwin.png | Bin 50541 -> 92602 bytes package-lock.json | 1641 +++++++++-------- package.json | 24 +- packages/display/src/DisplayObject.ts | 10 + .../Compute/ComputePipelineManager.test.ts | 159 -- .../src/Compute/ComputePipelineManager.ts | 322 ---- .../service/ComputeExecuteBlurService.test.ts | 318 ---- .../service/ComputeExecuteBlurService.ts | 106 -- packages/webgpu/src/Context.ts | 5 - .../usecase/ContextApplyFilterUseCase.ts | 85 +- .../ContextContainerEndLayerUseCase.ts | 16 +- .../FilterApplyBevelFilterUseCase.ts | 20 +- .../FilterApplyBlurFilterUseCase.ts | 45 +- .../BlurFilterComputeShaderService.test.ts | 289 --- .../service/BlurFilterComputeShaderService.ts | 84 - .../FilterApplyBlurComputeUseCase.test.ts | 437 ----- .../usecase/FilterApplyBlurComputeUseCase.ts | 292 --- .../FilterApplyConvolutionFilterUseCase.ts | 13 +- ...FilterApplyDisplacementMapFilterUseCase.ts | 13 +- .../FilterApplyDropShadowFilterUseCase.ts | 18 +- packages/webgpu/src/Filter/FilterUtil.ts | 35 + .../FilterApplyGlowFilterUseCase.ts | 14 +- .../FilterApplyGradientBevelFilterUseCase.ts | 6 +- .../FilterApplyGradientGlowFilterUseCase.ts | 6 +- packages/webgpu/src/Shader/PipelineManager.ts | 30 + .../webgpu/src/interface/ICachedBindGroup.ts | 8 - .../webgpu/src/interface/IFilterConfig.ts | 2 - .../src/interface/ILocalFilterConfig.ts | 2 - .../webgpu/src/interface/IPooledBuffer.ts | 8 - src/index.ts | 2 +- 33 files changed, 1128 insertions(+), 2958 deletions(-) delete mode 100644 packages/webgpu/src/Compute/ComputePipelineManager.test.ts delete mode 100644 packages/webgpu/src/Compute/ComputePipelineManager.ts delete mode 100644 packages/webgpu/src/Compute/service/ComputeExecuteBlurService.test.ts delete mode 100644 packages/webgpu/src/Compute/service/ComputeExecuteBlurService.ts delete mode 100644 packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.test.ts delete mode 100644 packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.ts delete mode 100644 packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.test.ts delete mode 100644 packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.ts create mode 100644 packages/webgpu/src/Filter/FilterUtil.ts delete mode 100644 packages/webgpu/src/interface/ICachedBindGroup.ts delete mode 100644 packages/webgpu/src/interface/IPooledBuffer.ts diff --git a/e2e/pages/mask/sprite-mask.html b/e2e/pages/mask/sprite-mask.html index 1a970242..b51e38b1 100644 --- a/e2e/pages/mask/sprite-mask.html +++ b/e2e/pages/mask/sprite-mask.html @@ -592,32 +592,66 @@ container15.addChild(triangleMask); videoWrapper3.mask = triangleMask; - // 全ビデオの読み込みと再生開始を待機 + // 全ビデオのcompleteイベントを待機 const allVideos = [video1, video2, video3]; - const waitForVideos = () => { + await Promise.all(allVideos.map(video => { return new Promise((resolve) => { - const checkLoaded = () => { - // loadedかつ再生中(paused=false)を確認 - if (allVideos.every(v => v.loaded && !v.paused)) { - // フレーム描画を待つために複数フレーム待機 - let frameCount = 0; - const waitFrames = () => { - frameCount++; - if (frameCount >= 10) { - setTimeout(resolve, 1000); - } else { - requestAnimationFrame(waitFrames); - } - }; - requestAnimationFrame(waitFrames); - } else { - requestAnimationFrame(checkLoaded); - } + if (video.loaded && !video.paused) { + resolve(); + } else { + video.addEventListener("complete", () => resolve()); + } + }); + })); + + // 固定フレームのためにシークして一時停止 + const SEEK_TIME = 1.0; + await Promise.all(allVideos.map(video => { + return new Promise((resolve) => { + if (!video.$videoElement) { + resolve(); + return; + } + + const onSeeked = () => { + video.$videoElement.removeEventListener("seeked", onSeeked); + video.pause(); + resolve(); }; - checkLoaded(); + + video.$videoElement.addEventListener("seeked", onSeeked); + video.seek(SEEK_TIME); }); + })); + + // changedフラグを更新してレンダリングを強制 + const applyChanges = (displayObject) => { + displayObject.changed = true; + let parent = displayObject.parent; + while (parent && !parent.changed) { + parent.changed = true; + parent = parent.parent; + } }; - await waitForVideos(); + + // レンダリング安定化のため数フレーム待機 + await new Promise((resolve) => { + let frameCount = 0; + const waitFrames = () => { + allVideos.forEach((video) => { + if (video.$videoElement && video.$videoElement.readyState >= 2) { + applyChanges(video); + } + }); + frameCount++; + if (frameCount >= 10) { + resolve(); + } else { + requestAnimationFrame(waitFrames); + } + }; + requestAnimationFrame(waitFrames); + }); window.__E2E_RENDER_COMPLETE__ = true; }); diff --git a/e2e/snapshots/webgl/mask.spec.ts-snapshots/mask-sprite-webgl-darwin.png b/e2e/snapshots/webgl/mask.spec.ts-snapshots/mask-sprite-webgl-darwin.png index 0f6267b8f71e788e6b0fc4ea54cd57c1b0b39c04..b6be7e67d7d19d03c3f4b8f633cf092d95d5b779 100644 GIT binary patch literal 61024 zcmce-c|4T=+c!K2S&EV+YlZq|E&GzCgd|jUgQ4u(WM5}c$rmAnELoDoVC-WbWXZno z%h(2EU&c1f%zgS@*Y$e-dH%Ye=f3;roTkql=dmB}* zIywQ>%W@*jewRhQe0f?f(oZryTA=XZBQO{lAbrRS`N3`an(Y- z?4NI&EP=nzAwCMLJbChAU&2GmM?V_%HAb0DC5K|jct>b3)1#7Sqs4Mm`3+blL;f4Z z*K+DFk66hQ<8smzKg?7NblK?`!)l@T&s$%CgL%0cU`XfgJbBQ|7|`d|{JrFAjP7GO zi|yNesORsbWU?8=IaQJ=B>vqIntjwH_l?4fRwwI!gw-fBaCtrIpffR3x(sGjnTqvH z+pimGG~Fux5Ose*Pbty9aUVoLcn|#WKr32UyAk|0%^Nj_ z%XvtOj&4@s?AB{ez9Tf384c{9TD4%f5^<`_8|*A7tOLz!x2)VN$#3|>-2<9> zPygV0!E$Sb^F55Ww$mUu(ubcYlb4;Te$MCK?|teZ;$LkI5bXA{@N4CyIvuMv&Ot5c znLjAiD}nGd{fDBRR*6%_ ?zXavvF@#%5dHo)lGpmyUZ+kWTn(Bq| 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 literal 28244 zcmd42c{r5s`#*dmlq|g~QAi>xS<04OlqE8iEn7$tV(hya-i0Dd3E3+l#**xVnTb$j z-?EIcjh$f(vzX<%`+UCN<98g-pU?3e&++`@o}0_HoY%R&&ey!UYi`8HbA|^106r7r zoA&{L6a2_NaF`u@=!xHE1At?I$;}&*ybH@vk zA32^#yf|jqVyqwi=a!-F?JKH(l7sKw&UsT!x9t5s`1{! zA34MhOB|61GSL^hZ=#fKY89~e)a_ewb4hb@Qu)vBzHX&!O6RL6k@=zDBx{r;B_#{5 zE##nCEb*F-M_0j}VLs>Zfsa%Uc17^fbPnJEUylAidr=RfkyerRr-X#onTT*xfyB+V z>N;5qGDek^zGsJbSU7uMMLuV6=DM-B&i!jq@`OR*)1{E~$&jYe1U~1ANkTP)&>!sf zVK6Y7?m>HkTN*gzxravN&Ir9fET((EWuaCr`=HZTcl|z9h5N%Bz-+6QDt)%j;8=W* z!D1*PxwV>`MT-!==qFbwX zE{v$ad|Cn;2%&9q!=BL9U={-Sz?(Pdo%Cp zebqZy5fOp!LSD*I#sY*;=p_6w?~+Agr4(HXf~rMk z#98C(b3*cky2*&RS`&2Z>RPKY^OOKKn%s#j$r)*_t~OJPc7LXn3e7PJiVzn+bE$it z;kk!dggPMNjyP&dNjfs3|Lph#yZuyR6MpMC$}hL~$UNal*{FXcne1Bay|6e?f9Kf& zLR%Mlfd3;>Go}n5tEX-(i}mXZG`|o?GE>J43B<2~KniG$j_7kwHrwkx_yC<~?Np)4qHW97@;+HrOB5I5uU-&lb8q^%K z|D?vk?mHDYioC_u1Ybzos6L{*rExpNy|Nh&_?wA4`&aZszjxkH$=;7zquNf2x|4z} zEgP()tfV~Ig`FvAe@LzDYUt1mZc~axBRTX@r9hfyViZ}jn`E!FUiJ7w-iyvh3%vf& zU*Xh+X0?6Ro3~>%A$x(^k<-*4D;5PJz7Jw582t~t*J@qv8qPI~2urdnhK-QyzxKO! z6*pW_&3#tduQ2OlKSx6mY~SX6?zNw0Vxjb^fwV=rByO}?oVl!`iN=O*nyxt$ z?T~%>R3aM?{m+S8PEBf(_l}=>E>UpE8HgS{PvbW{`CMqGUNlxECEQ>EAK_gHp7n#A6^Z4nSgE) zjut%@?J|P(|9mobCPyCEGWVb~`+BzILkjF_obG5|M$e`BwfqD}ZMh^yXN#KvYJSeY5! zhM%e1F_|>_?&N!UfY^aa+)}9FKN+`p_?wfj3gS&s(`F|Cr`c zyo#&Fdf90Kz{ez>QJ^idyg_Gw8P1!DiNCNH0 zw4#tCi_EGgCmk#tjFmTaY64e8x(gvr^nw@1e!lrax7P}{I6O6KpTUExzE9iL<03IR zg@xIKxh^N5a8aQPr8Py>#^|~RvsRa&W!x3BD3eRV{w}L!nJ*?PIMg=x4sk_StBOV6 z2*YpCAmWZ9T_h1FEUl?d2JRlT3{7WOTusQtJ@8>UD9PnT`T@~bWq|f`kpvR0O3X3I z6K%^qzA*$1VoocR=N;ez17c0ATQ-W3B>P_&n;y)n68kC4xg(g=ri+ZRTezJ&=$$~O zdXWLP%-ej-}J;OIwEiu;)K6^1o5Y!U9%yq$DS z(d@&cIhwTKY%8yW&lx_z)f9*;zqv|wcN@?5|Dyb6RW1K4ZQLsa9kE9b=x}~7+Tp6E z?9CE&pPk>K0{@XZ0z(7R-oh9Z9+>biGX=;ZtOmB7LMp>bJ*#&sC(5=c_dJsqrV3Oyq0AXzMs)f z5`l!!59)e_eqt17GJI&o(t{219GVh}|5;iYugd6bh@|p)t(&g12|EpIa?fvl-|yc^ zGHuXO12Cb-qa{?-A?Tgjof87(=&cygyUnMUB-j*`du>Oa9{8?O-8mF}GV*B~ORM09NE_BWYhE0{YYA{i=NQ(*(8wp7xEcHKnI_dsngkV)g!|43@V z5Qsa=SUA0^Ht4F#+=?Blsb^PgODL(-xXbj$9YRj>&?X>!fTHyw=amk!d6h2yt_Scv zXc|0d&!mFzKW7YWESYlo$WKoOyvsSp^N&$%{~q%u5Lc<~T1LXgt8pE3kdb6`4$}l2 zcrnIy;e8cipP)5L5>`G1Ge-bL9dP1FSpgHYqr#H~^pq_N#@(%$kg(e|H4ySLTCt?mVSHP=%*5Y{?kSq>e>&|%9Fr1Zox#B~CEC+|8DmbIL z9830>{D=EX>e2xjpn|zq~at(79Zy4 z#Xi9VHBm36zJO-w0TqkdTmV?m1LEXIRACvgh~_7IHYuFWfl}s8-gY-LBmUvQTt5~q zQOer+R%iCy>+BFT(sb1W%(&Q06|_braAS3FA5kHVvplkP{J?7tFcT}wr`XUo*E6uC zbr&iwxKO~wWM=|xqds_B9rD3xXmn$6SHL51k-DDWWDZu>y*}=?#-`K}`x))uY;lmt z758lQ);K7J5g)1pYVe8Htum#g;F8OkbrzBCGKaXFMX&S5-kc{^k#fvw*lb6Zxps`p z&jdAbkKu}juoEj|!$XM>EvLqvg~SZf99t9RP= zP(wFpXc}z78V8j(tW8;VMJZE-rn2d$O%{#~Py8AuEL-qT!JsM)7;uA^ zV}dR%!9AU%?4JNnJ6{oh;t94>dZ1$JmQl}@lrm00^7pw11K&-LimXci9^r6HIBEjN z+kM6nGq73m*g)I4nE*E7@}kV#i<$6+(%@s|q2~~jgI_&63?9b%I#Up9=N?2Tb=W80 z%tVlL1pvjj;4RK(NEY2oEIlo!%1HN0hb;gtD$P6dUOX)73?Oy4Wu7)!ot-EHdDvZf zaHRfv$gTIUmKJ6DCORF!JvsT8fE<3Cpy}E5Y|8S}d!o}NT zDZRr0;-UUyWMseSie28B=U&4s&ldL}Pc!r#5q&i|_67|OUvHo?A~X+q;U>hU;mc!- z9ioV=%XOOK1f-*5wWLd_XBjRD=y|a#)}~qipCh}{Dvn@86k1T*PK3XUH9I{-ZR3l! z6N)A>gOVffoPZ+UO_BDq%x}#Wvo(60Sm^{>lpMvQMZ^WO_r!gEV=9U%mo)Ck3eD8` z&p|kV+6$mq3r|=m**&AlW=b)}4$Te)*>L?V^0&*GvDMgTtsaXyk!iR-49!uT7JhEe zIXfvmFxo>=Xtk@ydQ1?hm?!jt4HNIc@o!6T&($h*S)D*wUHA?n zQGh!42B*iRe*{Z)RRA$x6(Jt-|= z_;S)wz5AEJUGrsT#-07IK*p_TSaD9|H7oA-u`PD66**HI*ujtrF1wbJ_4Ug168em! zmn-Gs;jtN7^$5NKuQ1x0cwt*ATnSf@_TH_#l03X z`o5~pL?3)L`eMit)RsN}V*lcxTG-{SKc|pBoR2o`FR9(u`8{erui9U?N?sR#7)B}t zXOaR8Oxu(GYl!O-jI+F$qBI$fyZk}owo$~bqVfI|;d`(4 z@Ky8%0BH4}KGM6%uTbn(`v)}9Wzv+d>*^$e{VSB3%tC(iLs~Iv9IbP2Tc7Y5K8EyD zF7KSHTH``_4l^28Ey&q0n=zXrCx8YXH6WnNbcoFQ>A=G)$w$rpXf=lKD{dHGN6Y8<>Iq_ zRi&__812s}!Nu9%wmE_s__4M?R)8Ulw4L{6nujff43-jS}!lm_(5L<99t1jG(u{>NCZ-9dxF=^O25 zdk>sCII5JQ{3=-@Ff&O+X*;mS@8kLqw2Q4$f9l6nV1->vDn5mY@3uh+1LwT?H9o$& zzHWlHEyBg!_zEVIkM@3N;RWE&@JB;NSVsRE+v+Nfmi`J9jb0#435OdPOf3yunhtg- z(jG+E)1icfhaNyQ?{BUT-%DjpIbQx(jZL)~oqi2lhUc=<(M^)wpG^Ck)dyQ6dOGW# z%!TUEL{}bQH`hvYY^l>WHmL1U0LZ!*6eGQMd^(wwgOx2$`k{ACwt%%pJYZ^&N~1(8 zjw18Ec@1^c*Apk2KaC3l=S=xE*qrI*I+T@pIAwc{krKZn)S7@NNBZDvLb!pS?*ZOB zTH6mKhBdtZ<4d@#nO1CZ`B%^fyRrpzyHCxA@V*MSHJ{hZ2sm>}DLspV&eqYQo=|x? z2B$hYJ&2mE27rlS0)KXo{C{;`ewZY3##(sgS9b}@dh0k{Zalq83eq&1g`|ciFr^kU ze*2B%kbr~8TanCxUY_#Ed1gAq=MW%#k~?qU$Mcj^Z(lUN{x;Y5qXcLD=I=CXokV!E z>z)_nU6qtclf+2L1d`NTV?#+QS%K8q9jZ+H9@OJO3#@=TP$?XP*kXt0BHszFi)v=fry8zT|Era{q_ssZU`6U6Ta!JJg2YGNz3;Y4dt4 zO{<0Y#^|#X5O)iRTN+A88-m8$@B3VNw*B1nz$eCp6(3yrs%WV?neE@RWlMR=nm9 zcxLkKnAigw9pWAUw1a6ufcx1?pU*0Sm82))+VRzp@U55>?sK<(*=vrQ2i4(jP|tsaG4qost>`{B44p=N-C8+q+z)1c!S^T? zd3JSSKBMQ6wA9X9Dq0a?6@c>>OYm$);#a1XTL&S>r{BP)rjN@9)yQjApN+N@gkC=t zTt6iU^MZt&Y>SzM*0E2M_?s=bbu{KzMD){0_8~pVPR)R@j?=9Hhm+9AxiHOBFt5ahO$5rm#>A-IQ)Tp23>rTP3a5~rSB_6x#N3}&F@fqTXjB7M zZD|?n-EapJ9(3K>WlT`WRE2uZpt(Ve+6#^*q&#EHku!Sk9$Q?-7Ij^HRju8o5xzh# z6zA7V2rkh~*>}kE*yx=?Elw5FLG+?ia8@Xv2T;@klN3@Nq*p*zWJ!jx)vA9^rK3rI zho+x~Zp?EV3@I7@V&iwbvUTHqyk=dZe4jX&SW0E<8E6{(ic2eiHMk)}8hp(U+>Pxw zCsOoR28dzdCBG(8*7{;B9iJg8J0z4LlcS0b0m~OZRaHX4^cr-hlc#U1=F&;_0gaLL zX>B%8{6~Gt>916{X6@?K>$77(KZel31hF-SC(I&AQ-PCtmmk8P92Q{%;%)$O!CR~! z(<(Tm1f!V6V}SP!Ly?x*j3GNnSy>sq$t<6TlnSx9vek(cPjNvfYw;oB=!KqvyKFvU z&l5{3dBNR><8EjQFh*NQIlpA5b9k1wj_QSXPkh@VRCq%z;VT*L3yc1fxkOt2FO14?HKw)yDp$G}P}m zD`A_C1YQrl_IZ-Wcj3cu#iYLG>EnRGCw5-3Tuf4?NT9hr2h&CO6lTXX z9#{7!NYzZvB^gG;YC7ac%rijwsWS{?YDzmt>_npMlUUP%r1E{X#%jU9L}Ag23V;_g z3r0}e)&AuaRHWqav-fswqKTwtCC5KVrEK=p{37(;Ya-6ZG1z;DpR2`X12DeZopeW@(x1}FEp;-4qU3yh5d#SHg0VuuvFse5u)mBY9Ag2$R z-s}0Q}3(2(tLY=4=LrJ2Asbt$HaXO z{)~Hf>RJf1mCe{{r_772V=-AK!iHGgwmG=-!G=kkOHa|hAGJb+)9iLSd!Pp&a}?yH zMwBD?xpVYnJPtcjj~$ARA!0d9(qZ-Qef}d*D_T-=)UpCf20T*!bt`cN%~NjnX0qbi zH54eSkystSp8seMRWrB=R%p6+mCa|4g_odQ_J=GA#K2Act-ANcI9D6`N%mR?5cbr1 z5ObNlh5K>3(SgFCJonCjN<~MiWFOb==nL%8a@af)IgbPY4lv;os)u>spl!-9)@Vm< zFP7jYSM13p<5vZ{2FVKoSC~sBHs(0o&Z_menh4hKB_mF;RAz3MXAiy;^M;aq|R=c@#gR$ye{>Ta!@6Q8sFkUgHYA` zqky6wn31^CQkIKwGed;wju7s1M>Q?>ni{SiX*k>ohT+M10v+F7LO6=GZ)NRtG(-jl zxz-p|H&}6A=fQL>a{#=l0Iy>Xvrv7tVTxq`2jvPoaAY&KuU53t3O0!PSZBpsX^MtN z^>@1y_!+h^f|CN8tklc>V{+9pc_!;&Hv9$H;8qSP+DC-st9L!kTZRW3dX{=QvEq(O*rVQ}s{&q$^Cnc|@zQV-9p>>XKf`pV99U zoV8E@eal65MbgS5PWCq1WC+@{v)iZ1k(xCBL{0TqD_CVEqnzyQBfBFf&0Fw4&urjS){@ ztDm~EbGGFPzOuh^dN)gvw+GIc!S}Z`gc+=4_b1J}un9-Af#Kf?5@CCf=A8{&4q-_F zS(nZ>6@{*hsU{WJfN1@^6Jx(Ss(`=!FC3649m9dIs!UGekMs%~AXUE4cYjCq0*#OvD3 zXgj=#xZTzE#SV2KkU^j_miRuh{by+LSG$X-?VS6@?}royO#JpsgW#{gTI=ynpVPyz z#X^_biu`6ZyZ+G{^5`vcQ4dNM(CY-VY1^F({TZ;BayhZ6j+i#W-|~@e;1YR7cUwGD zoG!o}_k4^`IC=wwol*0q1-6uttE)mk2ZDp>c0`NP+i0*rwOD-V{KA)ScX3keWjP{T*NLs^z$~=z)KhI$NnFO(C@5hap`}t0Eolt5$6Xy z>63rsfIE=BqwDXvDhkGgCYm?uGLa``NsS==elkk4iD zfjzb8YOoJu@O9hEUgB8=`ccRN-pELo(U%7&&^yS7fN%F2L+S!8t^%prY{GGFOo+nG zVOoe|{mxL^(_&mx03ipxUBVW9MGS2Za0wp+$2?;)mU%*+K_j~ai*izm1sx8Yeojx2 z`yoH5x456xSw47Ze;B~NXZ`H%G|ga_JXhAq?fmHoMlmwBS^nf!`1R~p*S0$oAsIf` zwaQ7F4OI8tMt_*Pz*IXOG&$&@yEk z;Rm$8x6c+WaqrMs&U@5x+fwIwaPkBma-Q5Yc{CKp$ngQ&Y28CXGDCOJu>s`XD24(L zWJU}SW=p^rG;G)70W)OjqrneH70-5`zK(2JTI=s=sPlT@Qf* z&ZKhY^eQIxZx@d$L=BP^(CxjEw^YW>J2F@b_W80v+~I;TRA52Q`!#iYtqrM!XwH6=I14> zg9F$ZjQRZ%l?nkl7MPa_kG4VK-Bwe#Gfi7L8YT_WFJ2=r9?4X=FsF09SK%1gcm&&| zk5s{*di;km;A8P$fcL+QhW}r_n4xlYqBQbt9UPv8M^qpWYMu1QCUv0#UZa$^h?LD` zmHquclj$D2W2E}t2z&iu$4}OFntEL_YZQk~C+7l@uM6#I=!L9G_sga$y${fp`{GfT zVAFLI{e`hItV3tLT#<;{aKg7g5IgN4tohrm@gHANV=*rOM7vzsqsC%M(b7o2Z)m2` z;%L&|cWXmg-SUlWq#5faR~oGSWzU&R}#bsvEvO}81%@>V!Zf;#Ra0y^z++~w?%C4{TVp&b@ zB&xbJm#3CjIH5mi-9NedTj=uN!ir^)*;LDRQOFI~~^0`=ly$F1!k zmVLAI5nN)ixX&GO|Ngoz$~OFFaC-4k6BI&fqWP^t6C2pJP$+k?#LoIAE2!>>o2{0kjn@$A`n8TAV+#8e z>*kQ(9LE%SQr9&{Xlg20_`Q#ljqnh^gO$)7gnmi72g{}a+b!T_P~@`_oGt|!U-GJ6 zU{9M?`nVtAX0xjHdXt~4_B@d0WI(;WzG)J{q^i#;Y`L&7YLA<)iP@AcM@TyvkD=EK zwk}0l6C4nXw+Hafdp>U`mpX!TmR%+9V^&={k|YC*+wZ%w0;{)oIO%@ zJ}Pl><|>U}cGSA~82UisCCRlg50mnjO~1bX~8aygRsf4I2|N6CqM? zm6RDXrJ!DM_#PHdV>zF`QP_g0wM#om$enjhQz$85adu{dRH*dTexx@{b&JxHIFa(S z?)smZ7HqeVm>quT?Ai2fW>Qhk075FlQXszR7rCMMRmgVN%U@2;cJ`>k?<1x9^96Qq z*S%e(o7x}Yw6(kvbMHXKjTV{_B!yqHYu@n=&os|CEo^QU6fF~coL_@I0f}S52k1Jv zZaanzi{@a7NC+`5-id9PJZZ8umR>m&aiI34-+hBRy4S*a9F@UFk9%9$`C@p7P`M!Y zd}NeMoX$V}5aD=nPj@*`mqD{llQ^^PShVW-871ve5pWQ+65Ny0F~cdPW_9Hh$j<}J zG@}0BV9Z>rMWJe|t53c^*&o(N7T0Mx8@(g-e6pc1OR+20?&pLEr3xyb;S!^fl9oc%ZBXNdS`RR0h+!rJemEDoCmX9vD^mCMz^x;S%h z$Sn&ki;%j=_zFhzk#^wqaggmJJxGF0Cj}5b66r-fNQ6@JD6d1*Gc1w+WCVHMCkz?7 z{cX#$_FV<`w$!QTV0)BZ5%c)cZ*BbPei{{zcDiz^&G6^mup#G~*3c~KoMcWZ8ddoAnQCL&- zR9C$$Sof;ew+6Jljh8(_brzrHz~jcEq7~}?ogv`u`5MxBwofPx=iqzt0>Dctj0g)Q zpg-OBRcDyT6FEw{1yWA^ZLuqYI`}7aGllhoHWy3IN4dI(s8I`Y%LkXhLEFgl}yl9nlnQevNqdhMjQDfN%zHPD`cI7XiE0s7Io~R z9kY#eO^fVT?7m?dsx`Ym%mY>A4`*7?CWsm~*LF#PP5h#~$tq`&lxdIe5HZZa5e(q^ z;lUMoIk^r4dq&TA)UrW}&Q4R64y7+83a_9O5OSbM|KyINOqb-BJ#>UOlKN*Ts8meS z_`{ue);wW+=9*x!n|pDJXudk!WgUIs#Io4iS8JK8hrs-Uo;}F->+8d5S-0wdq|VBDxN5;xp%2C_eEi-mZo;V z(_#mn{0Ui3$Yz&w??YuxOD*A7N-xfSndd$l9FO`#~`h2)H)|5 zX^2pQj%2Olh~FwbRdJgpx8bM*%9qmj z`-u{M@Ab9qxh$+qaG~Z8h0mhT72dh}=EBndH8MgDHjENGoSiYn9Dt-(rEr2N+@CCn zCPs+M<+Tn_Soefs-`l3e8Hn+lnMZ`E2-ZS~lOOd>Y7Cmyu6sP6x^~wXDNUNaPfLP4 zJ&B+Bf^5zVYA}E1?buou)H~LZCZzB%VyA6S=<+Z9=Y4!!^_3xARpWM}zl>$gVHx?jT6GWFC$^)*h zZbKDDM#T0O=WOGmh)}42WfbID$8g#>>w$27W5wQF{ER4ms*znQB2bkFSrhMGXmeFb z(*l({p#Z8w=D$g=RixVQeB^1vID^U{dFgV|*(!x1*ydapoFTwk8*g{IwX0TuB$l99 zIZi0b2nEgGYtH&Z)?%?JDe%F@&*>rMmD4C%3}j|coHZ_=Ut4NT3e=%z~>TG##mCIRRu6^hQE$X*c_BdKLCal3C`{ z=9=A@OmlRt+Dn^vF+E+95$+Q5WbPRm4uPSUBhDSp=e)A@I9_K!Be!-9{7O|B=8!wR zP8RR;HGIuN`#Dk=HEJjVI&uYC)-V!8PHH_^aC!yOzjliv#9=)VI*zRjBlH2aKVouE zLUWL2RHYxV@@}LAcb#_j8P&mu9=XZNi~aS#SU|_o@pKW9g62IVLn(&!t-6)lX&dX^ z7SooR{A+$j_|Vj~wZo&6`U1ZX*X!+fa!i19R72zH2lPA~v*O}brwyDe1lx9HF=cd^ zaRu}RR%@67jzvNBTP{~=`jzkS-!rf5GrE9l77ta}^^PJBm3(le zCB0pY^^*TFsFoc!wb7JxF)cHKvp-Xl!^q2i3laIPSjo3e_bacKo@8@W>yDlw7>@2$ z*VYD3c{kir(%oa=L~+-Yw6!%Ys2`Z$(G0#MfiUoTY?j3@(4xBC(R7Kb`ANb=2}58tfBs^SrV;GheV%0Y?K+{J*mVTC_=Nqa zl31s+%hR+?nllbklt28~1!|_ztl!{*#xHK)<0M83MuW6zft)Qd;JznPR1p)~o^3 zg1t2%g=yB$rNW&5+|cBGjc?5&GZY!b4*g9w+b0C)V%gsahbG_Z|LYB3S20PrusI`L zIAOBYeI7BI+<8BfIihWI+KBjk@H`UQ6O92jEdlz{f?4T2DZf4C}cc)C~W~OFyvo4y5TKN_p@h%D4%KWig z0E+_sH+{zlO~D_WeYLEh>Hj7Cpt^@&MQ2WzK_-B4M>ap@(V91lG+HNPRhFIYsZ)1V zTSX9(6q0BklIDPqM8|h?NUP-&8?s@A=hyxe;mQIaj5pUYT@@ch)-gL-F)A<22Pvm4 z)BlaJr0B2-*T>p)u~5qkA(qgHwP~xtlf99P0pu&J?o9z5+D(K}K@@R*9lrdWMXV4n zo4E$>-C3hg|I%MDkaw77qM5@+2%Dps;;?nF@zlzFNc_axLdQ7+3P;DZB^acW3(GSAN2eyuaJ)XoTL> zWE;@E^MNgHEvu@s4@RjW(4x~>vw*apdwjh<}?AeU{QH61_tccBIYxZYu%FZ zZsHW$5>`Nq))8@*S(p;(;l7MG6djZ{0 z%0QMu4QKE;qN^N=nOa#r@_Gr}y&Qvw4caW8;~ss&UhFmp0Fm_Fl~Y zsZ2f}QO2bH7TkRgYcG<>^`S3#uS3ql`B?{-S91BU-?21=RhppvsA}>F8SQhw5?upPI3LFrS{I^( zQy#tey#0pJl+B&;E*w z?BkQ`40|=Yl?uafxUqn!_OZgX3UMk%RtBEXe*~OK>Far4c6Af9zRrN0(>bsg${4$U zs4^Qh!gjq9=mIyqpj+_8OhQdQH`3c%c7>?u?8jw&s5r)vZ6yIL4)i)vo{yEwwSz!5 zmNthkHCON6nHFM@1Y{l=LMJZpI%tc(J@0FDO+rr=r44WyQycE(L%6L31>iwn?z2^X z#<@=U9Z}BqAEGo_pQOlc79c7VI9>Lb+5o|+3Tbg>&Lz1(emoby_S|H^L)zxx{;dfaCy*(aWo-CG6%W^$)>4mUgc-PaFrZ7HnKU-}7UB;R%n8LB{Dl zWR6_HWQpnmr{@pdW0r9|nhvFa7dt5TNGO0&w7TYNfH^wyM`It^J@J+--}7X*k$poi zjti#EKDph>mU=+gpBnG^GD@zoN?%6CL_rC}{H#ztLneHjW zeGLfgxHtePS{{5P>s1yGJ`C8o-5Xi<7N!CU4`ey(W$?1yi5Q-{pMm%lq?~}jr@tvE zc@B~=1wbfYIQ#mbilJuqpQ$V91DdVyL~l+#wZFeF?kp>AbF&!bvtmWC0aBpL!fcz3 zpHy1!&KXe`j$+&g{`>!BtI~t=0B{=ADCaN1-NPeWmdaLOqd1ifL{sA26?UxM+F&H? z5#_;cr5Gn3U?l@=7PMR53G{c2*aI0WOW;^zLx2l(+Rq)LKAl+0_Pp|T${Gm#rI!ZS zPEH>lFwBKDkRNUDTL$GDCh4BIn9eaE2Yy#exU7O`X^vL0_;AnF!0)thv^x9~JvOHTxpd?)XrJ6@wC%_tXzN$oHXox6oT z2S2{08h_q5cWZtn0wwyquxCPM=HwqCmd0I1%m*!R8zdXIGL6$1WTgX&%A*12 z(ls0N3o46olxtrJn2e^otGwl4#|KNq^Q3BHqtkvaTTf1Co*3%5U9;?u(TrHV_H}tc z676U&l2S|@y~rWU8p7zX)h_o4o{K=T$?OzO8OU4pb2!`y8Rab(47@UV28@#O-dU~Z zeTxXSye<84fU*8xH{pY+tv)d?J&=BW`TM2Cha_3d?-NS+mF6b#=zd67$?Jsm<-2$r z;DaQt9N_pU7#KmF(b+j2HGkgMa_tJ_;ElrK!`gp7Rou(puQ#i;ouB)#z~c)<`%v6C zg%4MJn6lRuBbH6S43T-q{T- zX}dTMx+uF8C~V#r)&t%*@wofRvKY;D9^X0v-=f*mcZ!Hlepas)e*wK_GyIxVE1YfR z)nDhne$DqN7|*RYtfFOIZ*<{tLuNtU0sn+9RWmk^H5|d|T38kxspXxG`6@LdNC52{JPsqD|P)kIwE&QeK3C9v)$^c$}w&Qivm(0 zw#l1%-dD2ydF*Z3mD}TEG9~jL^FOtyHLHtsp$$?Ns}DQ8LLCUsc4wvbK|sEX_7vv_ zJ~mXo0ZWDu*tX;IAP;1X= z3K$I2dA^=FcOf|H}z81v45O8s`Uh5Z1!ndII75=pAD?dVc#MWVTyxU=;NH*S4R8w5piZw!6=W zjv{*$f~!bkML1&c%XqZ^exdvwx*M46XoC(ZPGY&x#oI?%puDv4u!DKHpTWv=Fe$kf z`FMY2eCD|H$g|Zucp^a(Z*zvsOlpH`(XDb}J*{v)_-N9RodHaJn~(>T)-wX0(B0def`09@f4Kn4 z+{8yj->7Qxk4=S@`C9)aU-0>pXMW9P88nlKPaaNNCrG^OY|HRiIBK;x{Sc&IR z#N5SuOFfp~X7`A=i;#d0Dw_0tE+z()fC(S&Qh!f+!(lqv4)GfmZVjAuAz6+`WBZ4RWpmZh!VSSQ#>Va((4GSJ&y# z-&6XT!-R+9IR8!Zjz0nCPr{1On}&!~6Gym((w4}eIY9brNo--paYIm>eU0YMyqJZ- zub16#X@uI%Ueq`X+uo)mzX~F4HCS?^kRy8yl{>rjm1vJMINnPP_R;}vVQ1y?syOPS z_m*L2iAbB#?B&23Cq&QlTLg%Utf(z3Pi7f@<#l?`R`~i{ya0HM_Ub!?!ieX(1Ql3i z23!T!*LHoMyPg~H_zPgPcY6MD5I9=JpC)q~9T&A_lEdquF6QHKaVGKQJ(WGWobH*< z?;nle%?>Eo?A=JWg+?4S7lv5YF3Y9ZEG$2JeUEE@pJNqNe?!A}#|z7^yPST|v&^jh zjo}`KS{_Q4|6A4C+dv()b{1S-h{0BR;|aQ=1^uOV6uA)S3}_0QYCe?B7Ne|$kn(?0 zz7Hv+g5P6ues}Zq_1mvIiWE+ zU}Q+R6odcI%Xj~_uIr8c1#VP|+aFz`yA`W)5Vp3ghY9=p!IsPlQ#5{9IxnYbqp!io zsVQo1(f5(>$*96edwF`NtHWkpa3yrt!F%69X)T?kd9q!OGomNvJLm-rA532eb;vf5 zrMp$Bo;D|#*>t{~6k+pn_mc5p7lYU@H*}X0H%R*@rtrcJe`a**TcfCu7aM!r+qr6p3|Qs3B4#YF(>1hyBbeSiIB!uOrps9Ie1HYp8S zuH)FW*qjMfNDm{s`qmW^ioRE#gmvB6Y#xFO@1Nx`0Hek0lzWNaC$$9by%Q!6-Fzp~ zQ#4y~SNrr(Uu@pfvd_5ID`z-ovs{(%(gXijd*2xpRrmCH0RfTp2qGCl5D*E20nwD{q} z^vaa6Iaj)ywN_**W)3j(Xu#YB1Al3Xyp!Kyesem7UC_PFPcd7 zy#go7{**dDA@$3GMQc=x|#^508QDB#U zYdr(YRqD9w?o*_WMAM+kjsi84Ayc7M!*bW@8bkC@Nm3vY(+x1>{b&*w@51-#Qq-h# z8gy@4T_h;R&HCA#NzHPqNZhYDVG#|nDakeUm3Lgfv% zv`%fggqZ#@QH{^F*`C2Z^3qF&NM`KE4s$S1vdraJvJwi#YyQk zL%UbcAeBrIc+?uCM}8b;fCmD*@E?8$PUUga-M323{N9P!t+{c2Iz=|8Beh2E8sg5uri=xDQ3x2wH{jb5DrRWF@K3Y?G&!I{1=Iz))t3EhMn;+|vq z4!gM#ws@KUX+L~sqb~W+fy9gbE8O)YOZQeo^c9dL-FJl3SehHZEIoX4MrbA<&tIv_#Y#(W&%@xIeWs5Y!f`YGjAHR`0 zDvdA*HX}}T9wFMv?IXGe&ciU48*K}O_Sc#_nAqccC8u8VFQfH`yjrh96CmP-AWlZm z2v&&Le_DJH?1G3GyE9OY_dy0Nb$1+#ukV?RXNXwl)JQ&@-a|UN#Vn<}evvdAUEP>* zl=H28H<_YcM3~%uGSRT&Lw!@u^~-^5Rim^1oP%j1=3scWH62qhz{uq9A3ME)dKye) zj3s+`B!%8XDm~nE?wq$fMt-b29^J^(&Jv&FduD%-lDb8IJ7>CPa^OIc_OO#w3)247 zT{)qg1v6IvSEI|KU1!t5KcLv8?rE{&=2Y0L6~sme<6-#KNgkZzVby0kjU&&5DVaGh z3D?+FA?Ecgmls`o1MDGiYT@*TaZz!jCd@N2DAKSmrn=b8%@W3v*$LIBi3eUM zvTbVd?w!WY3W2UlH=8;PZ^<7azh*e0O{;|aD{)iH+Sry6$C}CKl_05__crUm<^9-M zsDg7|#aY`)5E1=DD1xNVibTjjN@t+ct`+^flAae8pZD2jcH*$&R6+kqW5qM<>!g1u zp8T%6{4%9;F-*8y48znRcv!%4&^R%*3U`{DCY)LWt=xmYDYqq!moPw#+y8?#Xe7PpszJ z=SFj*hi1q3OO&kF*Q2K^)H!|z3~jr^k|kD(1g|DIl>az0s&^ z-P7J2EA#O#X?LBV&2p7{q*R@iN0GuNEAhcK2OAkUkU;3;CDb4=cOb0`1^51ql2nDd zDNDNJ-s6!bJI9AIgPI8hg^#O0KPz1O!#nh|Tx#xeU_hZDA5JHA3n{D7B%wF;i0fAm z&q-+=Lt-L%>GrgGP4yn(U8<{q6OvFJfjyrF?#3@29{hS%?H*!|vN^~9=@F>w zv)aXLE)BTE*fV{Px=T&$73wtNv#fS3~TU z3pl5|9hj$OaIJDp*@8SL9q;@?ySbr65woC5KBN~dk%;A2GcEZZ`_5yd%}3*x%CP!U zin>PW*;q@%sGD`gkw)f5Psszdr3_S5e-zqJ^i=c(C%02lG5=YhQaPqAlMK+@xq~~% z(o}!{Pu=Lc@Hjd}ZFx_9@+^~GNHVE#k`T@tn0Gc0rw}_w62*T&$UdRIo3Pw#!&(_c zH*8Bw{)rl-%HR}Sr^JJqm;N$>eIow9gPX=g*6MAeovI&~x?VASI8f3iMg-9TM*uG~ z=x@#Q`thWEQA6F6H_NR?1t4Jt$YWDLA3#&2iKJ{XEBW{sq@GUqiKcc0SQHeiTTqw zVX5f!JgY+N$0ftx*}W}}pZzIT1Tdxo7;gt7LCp6j0hK1jhwl%E(e-iWX~@oCx%a=N zBE@Xf&6vh4nJlK5FHm|4^fXc^yRlOCQr@G>r`4SjGRGLKiY*^lEVeG1d*#l1lvVi4 z#?Y}x5OpL$R5-dtOl)3kyrg>St9jzhS*$CaF3zp8OL*q>=?u(IX55OQx%?lHC%x z7~L$xSncx6zq^Ae9Ah}&^9?3{A4>;!Iv+Rff<>y0?%sNNirc^?Bz`r#x`cd(PvAhM zR!Zx-x*xv^fS4))fi1%>iEHJ@;PJdh0t`k*1V2|-+u-0E@g%w%)B!hUV6_L&%y6WR zoZ3b#1291fl5+qNz5VO(8Ch0V&GhtkelkY3b!`NNFs@3kq0oEMNyBQ(LeNz(Tz!tJ z6;dvs5Z6lj%or?!%NvgzC+x0PhKJiS`?a)TWtN2Sl9eY1JN(ND?kXzl;1LH^SS&0I z;Fg{-I&;-S6B91Cx7uE3Z@QE+?{zum2x3@tCHxk-cS8+gr+*qX7FL~W{Jbe4^`D%K zU~65nx5VyO!bq%dMKvXge)GDTEkv(P)|35EmWsqi1By_G;a8*#1sJe;?iN!q({BPJ zZ*cHyN=OSOjuO?F_^?Ne$CV-&4Z|(riYp{k)^7JlHk%bum7}4ZIy8~B8tprij7k3< zCq>erriWEnK3x+4Y%?8s2PT^Aelf3`PHWaBoK&JVRCU0@EiQ}o2Ku4{Q23ZwP<`)4 zS2&-02l^hHamJCBa9Ltl zOkU+AZ?0^0S(yR9{Y3>O*W)<}Wd%Dv0a8{@S*{f+25`qmfP`jSYy(uZk~P}*_w#VL zWYopM6<=t6wT^1)9Sv$_*pi8p7ivBF0Ph&6%)b5&lJ}N9rO`_YjjnbUM!ImJV58xd zF1ewm0IvN*OAEX7ROJ0KUVYt$g0 zof|%5PIV@R!IZ9qlDE#ba`OvxIbCEQx#0gg*Zgni0>_TnN%Ah2ZC|`5x2EzknC&Fq zgNP{=gex(j6pF{X+(gn&?Iw~WIU1LZkC*128WEB4euSuEu9kcfV_3XpZ6163fkgzs z*Zv)jEaNPtg6Y_CZ?Pe^j(1R*=al#*C)E0-*AkDuB+K1{wB{Oc8O@uS^Cafs1jPr7 zg^MU=-7|K|muK&k$St7nKv7?MyP4t4(Bh1TWv(lkTh6y43h{Vy@DQFcVGsHj%MI@s z2{?k{%h+=g*hCEcfk1{ixxF&E1t%Yfj7{0&3_MqbXC{fCJB(Qiu!~?->cGHQsU=D=;`K$k$cJ1}^>p@mx{9GPg?^Zyo!&gj@ zJW34|%l?XC2bg4CBV(Zp%ac8-ze((F z_NPcSJn4=KAQzjS>M@0jF9;aMRwmOP7u(&GDB|*XMw&i%!dFP!JVpueatjVm8M2 zjqXfdCC(8tTF+t@W`~EefgMN1W{&ou_vCVGHD7_`O(*V<$nZd|OsVGGy5oJ};fr_Q zUk!N@Ju;MB9Xv5{hxXkb3*&kP%pyT{QK}CTXM)_IQf7> zXrwTn`g)ex_m2<-{4=1R%McWN=xpk7kYnj#m^;_A?)J}G=jp737QjZ;-hf(DKXhfH z9FeUzNN67z9QWkIr`E>Mw}63a+rqL$VMFXp3n*dJ~08ObU$+lDM8t6*x&-%HSqI zdYkuDvWT*Ecc|jYl?Wm=uvxHt95nv7Z@Yi~wmOB(vBtSjYR*(O?Iox?S;6#jXWq`M z40(jT;wR~BwWVlv82ZiDB?BzMaGUt6v7?uxZA&V3z$}MSExS&a@D~BUcsB}|0;Bf! z`PG%}GX|uwDOF4+I^YsONeSbhG^-cjKexI+^@hkib)X2*s&5V7UQE^)u8sz)ngXLI zm6G(Y{^zZ9L>=sFthZ4`$bbFJ&{!yD1#ax+bxS8m91na%L8seTkxOnEECaqr3HGob zYbQ}z_MX#hu_&Fb1-JG7bKE-RP~64Pdqb8rIr~^*SiCP-1C84~sNqfNDAf)Di~(I3 zz1|VKFyCWGTQgFoF3{t}+gxK~vEm!}Ao+)Vl+s5>_kirpFCKb%Pl2j{b>zyXG6fUa zNj0XV?8Tqz!1<@&G;6x5BC}Q(vX`-it5E~L7Vb=49lNLDAqsu#d|oPU#7T`>g5i(Z-RJovu*(rXds!bxj$5|G zgAV2pdM^?h5HXmfGE9I;$(F?dCUfRp_i>wrG4DS2V{tCDT>;c7MCI=eX9zHxOM35ck-$}x;#F1TYEdR=yK_%Pn`Bo74i0eFwaR7 zzM3Sk=o*!bPE;p3`B<+%cc`1_Hu=UkzE{cjWb$qCXia@urBm@9l@Z7-(y1Ysboq_XSzsw>X*pcdY>?9Wop)+{$92G z(2OCaiaT#5gJq)6+^h|v-5dZ9Y~Qx35}K4(X2LauK=jpx5Yd!?R><{l`-K)jPa{`a z<%El{Vh6R_dcS1Z4jQu7t znHFj4NmSD#a>A`0RAxochcA`M$zlL4EtYQZzO*8iYiWmO?4bU9wF&LbmX&K zK1Gf*ucH*42p!=Uz6Vp};NILTMy@T?`5O@A9CvK1%$=v*!Yc{3o5c8BVn7t_mN&JE zyn6V`hUHr73gZEseij;aDMLukLXv*ffQO@Z#cq-H+RtF!*mlgpDPFcIietirhBKHp zm>e;JCRf^V`MI0by%1!jmD?dzF&WqFN+^D~vSH*hb_d!62jeIn^I`?I8DH-@m12{L zI}I!A6mj3=o;u65xLAi_UKJ}4(KY?RR2}?7yQ3E~!*1Jn7%?Pmx*<^gDu#NEJ`Ax} z&a8j3yE3-&oIPDXP%nTn{t{8DDeKIaF}wOPOwq4X6M6nzp=jGo*Reji90fa++=5jY z$nyQF0E@Ig1iQjA@{o%6Zl{@5*+{(6y4wW+{m|mrXG?tQQ}TzeUxKU!bKPo(%nf~F zcxYbo4A2{gMUN>oRHHYVQJHzNGbS?=!8|fH&iMWp^%-v;UM9Of-6&-4`sP6e-p~$aen-@;l)h>r?&q-o0XfGgA5U zPm+#V$zAK`OnuhT9W2gFHJQF}#jG#r^aR&2?eDzW1mCi_nIK-zO&8P1w|Dd^#@7o5 zttNk$%HZ0%!Vp!UF^}6}A2<2Lt=D-(5s9eR9>|i1?vbWW_3W*c_t7rpY*kLT#^H0^ zQ`2n{^D!3nryhGolG@lcDPBzi#}-zxK9qs*Q2?OQU(X@eFEy^6dST~q52-q-6Ip6@P5 z<-lJp-Q_gOlpRk&yw2B8H!`bF2}tjl<`Dx|H`r~A3=v-wnYsp9Q~)^#p0-Bc~93aKc=083n0XlUv!hU}GeNmpmB@fYj~BM~vWC zz#=6Iph6M1OYl{6?OnKJkylOBwu_I$%C}nf*P32p@oya@OCZyawILhywEZFSplchr z#M@;}dtbk7u|^bR>1`R8%2z(C2=1$PHcf%`<#WKXA4-K!Qp&@RiJ=(yUy|j!McET8 zAB<*LA_Sg=a#$^ZOCC!l3kNJt@6NdYzQ1RExPck<;KeDdJ#WFh9xORK>=7GKtlggH zCvjtc32yob2<0ZLM(y7TS2t^q@h#!4 zW_iD20Px+b{ezo)-OJwg);KVZAUF{K9F_yiM~Jx|79z+aocDIWhd!Vi|7QGa-%Q2J zm$VL&BM@8Pk7v>zCqKLf7&ekaiI}buF>y+)X%4XXW9L+K*(G>hzq4-KX=@5P?Qxr_ zajYH}PX{nZJ8%s5-W}^p%sS`P8XpIva#Kq4jfq( z0e|TJ^0>ggss04kHaZ~vxtn&13qtq@$3zeqcg+a3I+k#M>r0GhGXfGRI_WPflizP)MuwIO0qTOrSTImNJ) z7W-$la-742&<{9Z02pb{DJCirSoSi00vW^X)!@MOBG}nmx4@;(!}`RK4LAZVkH7Z) zRDw2EWzXqZWpY_+J~Ap!+|5C~sT}VH0UsO$Xy(|*hL0~G_7k0qBDy`>Cmxdha4bxuYI)-4iZ(SQsy70Xukc~2!SFdzOm*2-u-@aYv?;~!4 zJt%NI1W&Y}!^Ztszq9|=7_BP9Y&=QixcOy>lLQE4qwVzl`8U39Y&1A`8!UrZUI8hj zmJZ@_>)C>FVak`D-s=S0qP$1?EflA_B>1uvOlc{A39nMsu;*D{JpkiEE&HoDtghr0 zYE9lj`p${bUg4lL^RL*N%?Uqz_C(faq(jQ~=|7oI2$}0RU-0l@5Y=Ex%c?T=XVwd? zhr}7J?v7~_8JyZ7+5#BR!zZv!EM@gXK zf|dah&99@;si2_znWBIt0-loc34?>3WRnzhR|8~2ntqf9a>aZWWha$&W0>7E8(4M1 zf20gaG{na$JgXtNUBxhe7i|u;D}*!T?+5h>OpQA9}TN~zA|hm;2h{N z_fU!KN+^*UNSh9dlzbvrK3+1IYWnv6!8sb=&t(pf!sKY2FW1zne>p^SaYlKKA#34% ze}>PO2CG+r@UzPwMnkHL#J@Q7MQ&Bp%D-~Km*3-169d>&A%~#hQRfz#Dd`|K86PHo)SaMU<(1a*OAeY^~F4R@=e-D<=zph^Ur zIgE>>`Z7A0IxxA=l62M@q>bVr8TrZ)ZN9P?0Z>daG*Q=8& zmDB*@3N`K$sHrxvv0Vv4r(jZK8f1vut7y)kU3?+TcqJu-62x1l=eRy?GMZsG8dKzU zOS~{hb2b4tp&Y+UC3wpF&h^$ZqiaT*2soLym?l$m))1kik<&fd7aJ;`Qa$niJy8roP!J|t9A(=Wo1UDi_eDA)6*L=J-Typx8ld`>Y97+O zt%rZ`+l(Dfq;?Jq72v&nLnVABqhheHgO ze~b*6GMXtq@{`}2%imuL!U7UnNW@x*o{3Z$* zKY7tLyesK?7Fj$KT;y%LjOkk&IITiot98KQxHRR4MW7X+c9qf8=d%=Qn>oMDyiB;P zMN9%Pz9k9-)^Rs&KZ40PavSF)Qg4f`<+9erOAud$Zh}`;lq;|kL#M{({$m}(<*liy zCb*N69!6q^#q$1kTyw)x&-jd2yRhtqy9u+}+* zJ}Zgr59RG4bP%TkXb`x3b*a0o3nhE{d}-Q?(Y(xqhpGo01rfp@r+(R|>5{8r?I1l+ zs6l;+zPCoV&BA0X*vRa3%`>8b#n@M zC>LVTKmFSC_m>BK_Celw_L##T<_$UV>#qm77c+K5{zBV3y47OKzW&;UpB{&Mab4>d zJ^|$QRA4n@?UuW}z!A3JNBl~N?4eDn_hh(Sb!7+{C%}KBjGr2xStKd7!lcKC{%Q0* z>z1RWSL7V~-NFi7JaDB1+wp$L}@gu1aPq5 zF+cH7y&osiAPU`m4^~1MW6HnE@2>=__OUW@&?YYanL4h%dA{9hI;L8(n9fNzrf3tUMDyf`mIm6h)~~rVtn?Ao;~&}?T@T!YJHJu z{#T;aGKY3{O9a4Id|&`4;?;OcI_W1fy!raL9LIO&?Ap-T7B##{4^~GQFiBdU`0EeE zEFSL}W6fftY5U!pZ0F`+nHpZ#rZNa1opTR71+V@_FY1% z`SBdTq*_z_Zf?r_W;f8XMI;DOCct*_~lBG>w$lddUM$xP6s11aDB8zR+~`QsxU z0j5d5@L5CfezYWFnqoxvPmFNl4<4l5wAP-t`bL%E3Esh*{*TIY+| zZ+zV5OvM$}e!S8B0^JcmEIZD`ffkAgZBi!AIE{8E>irCQsrSN!(8>auVq&19&TDCe zV&FtUo(kUWtGx14%I$}lxd4?Q1R{iBi)S;QkV>yJ1o2LfUwRl=vXzV*-mcY~BW0C( z_2cV6$@@#J>EVDfGDj6$-(W1_tTM8C0$vM~bAA2RcqY5vj^Eswh+xAl2xUKVfd%qh zH(2Rea-FK9&qoNEB{wYY=7dLb$edYt?_+RCn!oQ8fIeZs_G-oa?B~V0&eA&q~P`xBJy? zSPRa*`;`y6p-h-he|j~XwIRfnPYIQNi2EURB$+Gbr-uMhuE+_L8EfGVep|0MS@LR^4T%wlx+`;cQ=OsaPj%PXt()lyIr)#zgZ}M3H3w}=715B&)$Qs@1 zGnMbT@+D|%RN(>Y@_aL8(Y$082b_H z_L0cEZ*^MRaE|OP(4ce(QqEn=O}kqg8WN>lw;^UKr9NezJO^L;MZA|3-Zv>nN{EiU zlr@`_ciq4B^);-(X~Eolxw8da_bu{uD!w(gz-J8_4L5YP+M5CH#)6d=y)Y#B9jfn) z5g#7}%6)e0i{15&_)CVJTLVV`2DUXgL)V5*mzn@E!;qU zoIbSxXA7!%WU{L!Lsh3Hw|dRDVrK#j9nb~DC)u|mK&4PC;>;3K`TBPo0fE@VyxNer znTsh#`&ot*xmX<~4OWMpK=%@XVHstgO2En=6=RWEzS?2>&aR*>pGrcZQ`R^bll0SK7-CDdJZy{Kfa>nkFK zM=28^TKL3&t>{UxQ&WUuQV#F7>eVOTrjO>A;td@o!k^dUZ%hsN&AMIdI4!zKf$!1# z2C|h^#m4HJZRi<>(pL{Q*`r^EOVH4mf+XR0)3*#7fz>8bXxhUgGS+3@oIzvnB@h&eYV>XV)S<{%1QrJT#9BLDQG zMdMC%&uke9gr{N=!?Uf#V4dlqRz>|c18rrTC|R=ONua_nH0vk$G_YTHyHw=ct8q)Y z=jEfM7zOBxR!Y~M!8lT9;$nhSX>=*YRB3%n`?oE+bJgBLi}hGXc{E&UZfSS{1T-^%BP5*h<%g3(SjL{b0BtN=@Vac%aXKhUDbPo; z$y}5_;9NTOT3xGIeQsj!JM7NH?N@; zzFb&jq>a|((>(HDy~6;Y_5gRJ?lbkMcV_$O+_aRrLHnj!IY)t}D~!)KdmEF8ZT*Pi zlR_)*Ty4l6PZ9h7KSXL>Zkc#fM#Cf5<$L@k0cu;~%Ycgj_`<8W&S&=C$Gq zrt?h~$}WU`V!7`>qw22AT1dT4h>SSFo-9nMPR(o*wy(J3_o-*t{cd8%VQiyX-$let zh@K1mXfpnGLAMLMrvPW0Wkrp{WpN5&Kr3_L?n#Hz;vFx&W{uzpDdw30#ES1C7#n@q zDjwDJr}_6WmO(+vnmx3+{Pq0jSV~q&;aZF_;dtT8SELSz7p@b=ZRf{_e-oc%j++ zw}aK^NZsFdVQKSs`Y{pD0`#XA?qMr$#kVUP$62 z?-5eq*MJa1TXc0QwqV&4)TIs&K69)zIK$M^I_`wS&Wcr%#IrQV9{;tAO-^vV1N=3) zl^pUWFm?K3e#ku z{Dz1!WDPy9_0ZcN`~$CGK8^<24b$E$Ccmh64ItDn~nZy zb~qkDDC99%Af~Eu|FLO$H0`m3kZKy=+C$j=2+(5KU3S~qFYKf(D5r8sEuzu3VBpi= z-no0Egwm+_FKSfHqCEp5F;9aY(qnIq+pdBg_1QjXN*fI*4shom9^Nw@Y}-0;?VR4M zQBN{oc+zi$g&)*6)8h0BRn6n1|GCG<%gv~^>5FjPMi!z`%!Xj23`J55i^QpBVm3x$ zFAA|KvCyUWugz?JN!8@I?QIp{#fKY#e6$td8no%M?>iAmglvs+VXY4cNi~ooldm8n z>|^z~^7!}go-QNE7w8+=bzaPD_v#N%%Nn@BZeu@+m;j2HV_21@MDi~ zYp3DX^>_=G<~Os)$mYLKMPDCP{3+V8Fj9JwMa)VnQl!M(Ig6nc&r&IN_->rnxAHl2 zxTJLQFWhkJ;b|uze-j_~JrqnV(}FQwNBCNr7~e$Nl87^SC2t73b{O{a$5whmauf}U zdjm%aCf4=7soI!ykl~|iXJa|j`^{mFni7bcODMFrNc7qAnN-Q-hT>ija6kR)s-8Fo z=i_zabk8O9%cgU>Z{gJ)K`V^q6|>DWDda{^!)tgHY!%dCRW-4P_PUjiRA!XIxw~NQ z)HiLr52V_xwYW5d*fx1qSuWQ9+aF1AO6CBzmT9UGI=08h-u)@;FaIaWU{1H=^m(uW zpGaFdt2sPrV*?4J?}IID3R6Iw`sxQr^v+I?&>t(VvmDO5SgiQ16d%CK2$<_rv*JJa z%U(;o_x$sn_j@3ZJ-wohm5Mst1uOcNus4brg>&D3lM?yr`a3DYUvJ@k{y$xLiWK1W z%H*=q;yu$nq>ppR&Y4OTR{JxFWS@f0)tW}H{*_hKz?$J@eCKK3kvYH@g-6si!yI(S zlb1^PJg0%pKL13N5NR7RgO1qFN#(?ich>&3Z3a+g`?!j~|Cyc@DleG020i20qB)Lg z7aqXo)E0qq6^=unwp&RI=2%}Zo146n45p8_)9jn6)6wAT>wFN0jv8UE0)GfYj1N`l zn?WO}&d3COpH34e_ILvijxQ0anvh)0x4mq#V34#zelVv4US&)dXcZ49xh@-reO4)* z8bDkW<&!zd_u9)NU$IETpAfzN*M!oxd@xv$FgJz9wQtNV4XvX+R@Xo|;v*Gv%@bYa zC=a!%va?ebwlu?mc(yGKil>1l`OIedIKH376PxaCi)r%tGp$3NL}dNOBbUjgJH?>Q z^Gcykg9+!*zMOdV(5>3TiLD!6wWnGl;VgOlwZe)puMY_i*^A@oB2TeHg#F3aF+)Sg zsQMHTbgdDH!KMNV(=jr|s@lDP>F2|=i%B%<6|PUnit%92KvO7j?`?TyjDd@`ms8Js zNj<+s9wy>4E- zgsRkq4MQuJ%X-9E{1eGyEgpeF%AkL{iZ2+@zb-NtPxjG6Mq| z)z+Yqlrc?1Q_*@nD^{sR>DOO>%bRe>*cG$F)Cb7j%fD;($bTa9Uh1jw)k$*|wTZiw z<~-Q0SXHnGlp;`ft(6}k#gND;zC^8Gt!kvS>F&J(_nYuC>&Cn6Hi>I^_GZ+~_^>4nviz)IsFwJ>+_Q*+yOz}>!tD%v*K*;Nq zTVDlX-bJuch$U|CP$h&i_7P4Uyo3J=%Nb9i@HBC5#UcBuTsOj;0KY(pZb1vfI*!s! z4^O+wOH{i_U_VVVdAvLXSuIfe-&g=(wtv}*VUM;Vwy~R9evJ{5_z$khoN4?^TC9pr zMrw7us8f6nV2E@tuHxW}(yN`fZrv9%x{5#MeX&^_(n{}B5i%2VdFZ1uk%@Zm(;I;m z_$e1ZDqcdhJx3x}3&Vc;QVXkf)3 zD^tL+9S1uDuG^_|s(vCF@9;D-uXX^K>rEsCeht4b0UXiW9vfqzW*Am7(c*k*c{gP?;57maNSN}=_5$qpeWKSr48?{KZux@v=FUWN2^D{_=NlemliZAE{l zLEl6^g-n*l901Sc@W)*6ylefXh>LHGuLSZpksiVfLRbS*1chY$#;qO1&{4zl4ceR)KZog8h>wdr8au%%&tv!a`C z!hqu!pD*E6&DrNt+W7N?o7}j&%+4EAdRPJbP}4MpY(PkN1}y*S0!)kY&qcn>t=eXk z2DJpNkA?ix^6U-mUYBRKrDDiUoAEA#7MVxmRF z(aj`*qDpp?=hf-F2X^8t_E?@Z0<$R?GM&408J-_h?%A5Px!s${pO8-xXtY`r=S zr#c(yyW>lh-@SHUcGRxytb4q%+w&pFEsfL=C2gB_6e>d}R!zenPGqJfxmrG78@DCo zHocCUz>H*35={s0DgAQCvzC6PrQ;Mex0N)bp-!pt;xKYFU|NkUqqqGDB@um&S(_IH zPW^S0<)!6ueTTf**SnHLsc@>>i>N$G-iwzfKT}Mcz!gMFCmb(8>W4-o1-53`Cs)B(y^v@- z)+3ML4wP5*g8wvG0sr#4$lfQ4sY=RJQKoK3Uk<_xbWgRQhfsVt(MaO2N`~FN_i9oE z-)&{XX28ftaPt7($lY7|3kDsMOFxER zjJ{Y=%1~M#_do9yQkj=D`FQEk$l*32DMFJ@EX?&ruh75^cT);<=iZVDW}F^7Brfx1 z=1o*spDVo8e^?=p?ua5H-ZZBBx7sz-NXE`i=_#+*iOC=U3a1+3$GF(d%1o@iUfJJd za+N6itBRX0B=+Qs+3(#~2sE7Hf9hYWB}io8^<3?;$rnABSBeIdxBb^pwV8xTn>y|6 z)P`+Ypn81E{o9qFg8@_jX zd8lAC9Bp$D?0TU1Zco}nq?TqJZBf3L%i!<~W&m0(>nwkHUwevw1#^Dq(@fPawp;M2 zO?`Z1A;LyvpjH$kADl);l5R z3(|P*(#U{7D}^-T~gpg+5_ZpF%V(#^xte`r*{4Gs~)CU z{mCc9>Xg^Wr(RJB#~T0&v+&emi<#8|LHHS(rs~|{j$jj%i@3Qj-!~c9jL~>KfapA_ zR@zzHf>0)$U)(5hSl%hf7QA{gG%;w@hH1*uaP*6a1$W17LUXB?UYzX=iof>bnK0y4 zd$;O%t*uoWnJtWAt;E1Yt@|<7^Tfp1tZwuxEsVKDa!SCoc ztzxlynICJn(0Nzz{%pVF7np{fW69*4Q7!~CZO#RS^_oA(Yb-vNS5R+N$W7CVyQRY# z?A-dC7X5j@aYy6O=u5eLnO`}yg&`MrUC80iff4s))lz)y;ldtxmE4rV)JZ&%u4PDD z_vq}(zWU&R-DlNH?fx=2FoLf-CJF&^vCzr^t4%LDto8W?vHlu~zH|K6gBE7fxmy`p z`18fkZ$~j3^G{XvO}}+&QWF<~Yir|zO2W^E-`GLb8P>i|HPk7VAt(u9FnPgmgPWIJ z`cg-nAL_-l#I_$njC>X~Ym17xJI!1{021}KvORH=3zPk`CBLkWhrjq*^1{zU@##%x ziUI$>K(1$3|A{!ek8e675F8c0a{XBk^RRHuVZ#jgo&3v9KQ7GzTy-JGp_jG(L|Tgo z+k&(k;lUliSZM%pRF$U^;e0AQ@~jaU^_eilt;V4@e9_l%?oHR1>70&ar*J=O`}uc;j`VX2Ydq|FL{%r z-t&qAfR)BdcMHl$4jYK@gT504Nm-YXb`NB99V0mEt`L241}u z0r>pOprl?D0gx>Jr*K36XGp8$o{#p^nUpW>{Fuop6#&8P!hnijJpyN6ckm@Pz?5+} zfw^+u0PZw(3KTUj=zs@e9^K**NUpp4aMI%LgW|?e-1bb2dc%@w0VF_=y^v985Ec8l zbE~A-kUcRX1{loduBBN`*I!$a>bj4sXxMLcy8iOQU0dE5A8j~fzO(({nnTf3Cq#Gv zT;YG~Cg@lOeRq>h?nPpqE4J=8hteMH$WZX_9FwVr-G06l|21g4!O>G;Etj~237?J% zAB6KS{Uw5!zVNGYuISkFHXXXdSKA9k{rXa_THT7AEhCttUo)-VF5d_@!-LG3)|D8b9s`>+!Bwk zz~RYU%x967wn^NuJq(cSq5|Lx_{aS2@I887n=@lIQGCjTu@YLpAiq^xDBW(VmH5*B zuGiElAjz`HJd&e%xt^Fgb7Ntqeyf3#M=Tm>IvyeyIDdHyYM8JHO~-bHOeiPheRDqs z0A28?4ZWzSXYKj$%g2 zx{DyNvkw7E*<5m>Z1S0f&b-pcf`wOoRhJ(|`rf}@rmmls(#`$T?fm*wXY~+?RF^sq zLR_vg-1n!Iq5q7VklT(6tuI1g5J~}kMb`=icrVS@+c>!?F81xggoc zvV@ziYQIw3e)LB1Ks>mft?;!CJRIb#uuKnfg-t#&%Pwc2yk^nz?JmOJpMe0BEFjh@ zE#$~Q?x>+el>GRBs$#JYUCDm%g4~l2Wq-}7u-@J&vLfbBi)&?9oaExj=ZB27!|BRr zqzFGg+&H9JkDtkgRquAn&%&1t#T#?}Ji2;%el9$u(?bmNjvFHM#xlbduA|~4veCGlf$3yq6*-WR7;=oFMuCn~C zkpzv5j*Dv=sqG*Qk+O3V8s?U=1!+q|V#9-@;SN7%G@8%-Hx}^hbLVrM5ub+`vO1N= zBb9z9&h+?vL?YBAM9k>sp(6GeU|NSK_K9>BpS?A>_{A-|QN_XO=one-fzbY}mB3)s z5OY}Sd;aoQAm8|~Vb3!Q*+u-MsLR8+`)NnyN5W}M{X6e}ES!|;7wOOu`fWr4_PD;$ z!VMkVhz!qvV3f1!qBK_l%U14AG}-jF@66>gN{sV>_!AEKOhk*wg;^s4kOcC&h4EI8 zeU;)7r7I19+HhL3^UWNsMbVCYEW4zuUZ$3>sW9x4CCsS~xlp>K8QFIkw z)j!tua%pd@CwTx8VCM2bkI9ZaAswzJK#&#CWMmEJG+XPx{poFRZ8T=+_Tk_)c5u>u$Hnmf^LNoQtNh@b^SHY@%~O{-M;WsgcZF z1jhd`CMPaC++bYigfh%@?@3td4(ZPJ$B)LL5(3@I2#Lw!ThWOzVK2U`kD2b`y2|=% zrt5%h3!+@!0eH>=ajU*%xLr|@Fu@_6hSYpkW ztJfE+N5k_*xp>wIv?f!07%DH`bT(_{jxwJC3%CjVe!aD9BIu?noeU^k_^u+5<)9%C zvym77{?AbR55rlO3njDrK9iU#>MEU71&kq9v=>A-Rgu_KeMW^lDYYY95;Whu`mAjt z3*n!11{HyblAA+FAuF8e@2U?rJ8Xu@9DtlGp^D2y=+SzbG|YM?S1LX>uY%B<;jUn) zK|w=xY90&!r~w}1PEoE-&5HBeJi9n&CGiO*W76g9*_8kHB%rzA%VtOqPpod-%>G&< z;m+ypz0fJljvSxQF+&&f}p?6#W++( z8B%u>7r`Hk9&c0L%3;N7@)*5=pP&!Bi^Fh>kKT7OI4ar*djWSl^wzV#wzp^9-9<`R z7^~6in`37kFTgALln90Yk|}}DxM4VbUG;C7nN2nKL$6RT z1GJ3+=+&Zh_JSXE*!)t#K*i3@t}xd#fo1r)DESzC#y{;zqMDsBQ*jJqaAl_IwX^1~ zTys)ZG=gfAPeXsPIs0_usNXfTg7u?VJ&Z6WURnsvZK>vIXsY zTG^^ujk|M?Wmw?fc;6BWZx9LPq-n11T&Zajkk0tJx@dw~gB;mwrGcs<-BW`hqMeM) z)dTk1ZqvCQju>E?pIfd^?FjsI4T&3Tm}#vOY13j1;Rc>lR^IKssd8&qx>}WddZ`*U z4vu!`wiBceEYr4kKA_5N4;zbugFDvtWqLndd3q@ARCT1%nTQHBW$h#Y%GaLGQ(%um zg(-$IU}+n&LuE^?=0-sFobEMu-f%5DJg^|4N9Vo9o}EA(jTV!5>`y80-Oh^liVnWl zYWpXwM>`vyYH$AAN&vM01{p|4l94U4T<~}4W!0}SbjNzuUP4EyF0y+cr@Oo`f~}at z&&LV0+okoL%E+`r-eG$%@O+pr|8k5)59K-g12%;CF#S?I>sWcA#BaxBH@;=qCS7gm z3NisLyN+Q5Lq5IgGTDYLczl^U_1W{LaJqGkQg`C70frSjD4#M|H3O;;CyZQME`sVCC70FoGia?5uo(SwcF zjb&hJJVs*hZSU3ipSGYfTHGv$#1H9!DGX}zCpx`IFL!_PHZ4do_N$yx7W0&{`s2A0 zjtyX1fUBHeKLUTpO$_MXiZ`|(sc%ytmV)h9Z09WxPe5fYJK~t(zK0i8y z*pYIvY_UFzCyAb9JPUvuSMW7@EskfWs7>vx#JEb~(OBR+k%D$6PP71aO9->{joiNE zRe)03tvIE@`f(0$KgGgpw^ZS6!bpb!fi?oBCDupJcf$IChV!{H5j;e0m2Sbvck&QJ zDTt${)KV(@_-JZYw>70aZy`}{S9DNbpvCl)eCRXhhY-TskT<_|mH)A?da+gEwQ$qP zRP%|VCjF)Xi((49g=bDvhN(le!bUcwfVaU9)EY&Y{cZ+moSVSqrbN-(HW`YVeSfHP zL+x%)+eJ1kq%>|Ua_a0?;{(%7wl%NfTayg4m^bzZnj3q?ICFkqx!7ByA3ot4I&NR> z^f+%UH)8xS;_HbdF2zoG48Bdc{-!DVl1IilCPLKgo|w(&LHN<-b}r}T!C6H7I35kY z`>;+8P>l`yX>0svIW${~+DkQ|8yEwB$gPJ#k5T{y)am-9xwh&@ZD*{#_6^*lm){QF1uDC?d(FOBzFvlbVA@xz3BqrnqIc|C~yU;=Kx*$~J` zZz7qO;{V>Omt^Nc==V%}G32`UR)RkkUvzT&G14T9fiisZ?w6O0R`r<~c1&7z(pEKm zLH^l=gK_w-nq;9l*Zy`vg=rO+mQ9=-&`Eo3>^uwLW_dYkX0+-?A=GkNEPJ}m%{T{@ z)lQ2V>@3NjI5<){vt#agAuuZhkXcsqHzz=o^O5!0F>Zs@RkLp>Ku97-i2~gZhkHdP z{ocvDSSu5+ERZVe8pE4oPjc*hjx81cV?hVp(2P+GMhE@0EST#jtESDikCq!VhyA~H z`k~j_qRsan!S(bxIkRiGGvVoLpf>P}eNSf|N60IGo2C_3A6zh}nEi$o zl<3o}5fXA({zY_M)q;NZ00p}rcq$Vi{tN91)OONioz5WYb#&sFHP zIN$@mB{k9?*;%(um0?zU_9s~%=Q3Dl7sqV2s5d~H<0gHz z*wcLAd?YtwaEp-I69R-F6Sv?*SvToe=||OXV|bOlYkn|PBU5&;NgFP3A{3?;q4e)& z7e-fRau>hs=s6FiQ^bYfo^pcwUOnECCFCSCcWl~sHY!J4ieSV*x~ZJPH|4+M4Hef{ z=M7K2xxKw*Kpen1-Zm(3W&h$i&=nx)o3nor;Ly-qRE~DdgoydNh@!*5JKyAg24U&< z&36WG(~(wM(V5?K>Zmk{Bxt8pRt%R)uVFv~VY4spqen<$U2mGYuC9J1*2y>2+Uagv z?qx_3Cbhs)l{_=g;j9XeEei<@P9k|cU>rUR{qg2qwvOoHIhCb>>?F}yZs17rfS$*? ziA$E)Y~61)+>D$SzZc3MTa6qwRJRxQZeHPJ^X$HCE*5Vnc!Rz!Aj?m9tq($@f;?7@ zei6HMVhz#~b zDRYEd|CAm!>&28EjsJacLwDN<&@kOEc_IWk==V!j7p)Reek4drID?H9BmO@YCml0!#fNQe+1$4b;` zf=}+=XFoz?)R{Z>!mr4ph2oAvna$8wFvPjGfy7=+4yy(G5&rtuorkvc z^W)|%g}0M)G&wGV_P$}S&D3P9uBUw=rMC?sR9X*~61Q{E&~^b80lsMyuAf$oD*2k7 zDE0ADt;o0EFvB_%S?C9(wmtBA+)QGiV+0L4t9UQ?#b=MnXL3!7x)5ORYRD{C75{dfSd-s9DRD)If{$>*4LVJh{ag20!9Oue@y6VGhy?!%C*$En zxULrKM}_h}eDWmqIMYyHZYAh#3Ql_vx-d$R#xrp{1my3xK(j0VRpAvg!Qq)aEZ1_o z)ax><{)vh|hxt#EN!O;|beaX3bs@2ctJKnU2R@UtF=>Ok+y0;S8sZv#Jr2!^)z6R6 z^4{LND8|O=3iGLN3T93c9$2;MKl~_ZL(n;E&-Od{RS|CZ!~waLokFgKESaO%tYOYO zldlIkv8T$ut|djjj^4F23S&nrxa`P@?K_LQGtEFWvjGEcpKUpp6?!&P9OqDXZlQXd%dOpJnc5Ul9V2uQ{#vhOFyiIGO^ZlnK4jex<=k%KOEyzeQUUwnuPPLO5p{ncNpW!#` zGA%^jjcUc4-|J5T^{rHXDh`IiFKyZoLoJEtiyLi9y1~^CTdQq6{u>K`>Z_-p7hlee zReLiRCw5*8e%3sgcJ%&RqL!Qy^a40Rx#L~{ZEj|y1?$Vqk-{#`kSHDf8DPvkX6wXm zwts~gS@r$Q;+PJ*U?omI8Fd0seY5a?)F=ipW?A|D8xV~t|2>-a^kRdm=rCXp*6+`!y=Bh=pPV@V!B#WR{NVCyT z4;%?{mQ{3s<7qPI5-V4iZBCRGkAdt~sa>-tEki7eL-JvrV~_X=I9)MJ(Cd`ZrVkgY zBi>cTkLi&>?^2?OGZ|!v+09fd5=Syf8r@I_}{{y&JRzH8p(gK?jeUnC73cI z?_G5OOrPWO9a^Ee`Cti4`2fmEAyTTRc1C;9)MC&G+-0guD<_|Wi!P0SE1q$7%s+YD zm+%cg_f_3($V+ZDII;hZP2nUPFRsY8@= zk&^`Ek=>ky!XxF3t6UuX^__BDK1S^R|EPInmqkV{+q+4`aKRi~@fT4}wp#!1j&X>fFm zAe+0gZ*vE48rgcR(BXGN0KCDRZ3}3+#jCv?n0M=|`)Kg?OFMaZh>ls5%#{2zT!XF} zZVv!n=T42J>1SCDjIhXccn7BC4%J~s1%Rf|uX<}^9&u-8Jo%R|%&}P26{1lanf~LW z0lPf^5=sCgt&*t0tZDc`%Bh*b1|EE|PgY#K7+47NNRoR~*HstzDO@rs-ASb8FD~a& zSS_D=E}#gc0kpu}3y}cSMDkAX?GpGZpZ?1$2zbi^THpS^GR*UThOqhnJD5ax8Hh88 zAmx9w0aQNbF5SkdjiM}C)JX}x!t>l2sJm7~w9%8p; zf!Jk05Hq73j82G%c#zm%UM3sD{8#wc8ONdKy!9_@QIYoJD_E-=z{8FUoK+k6^f>hRjdA43&OEqVnLjocGec0 zZHlLLVJ#T7j@2!D8_HvXRCgR~i;U$Qlq1=MAhES z>q+#93k;d<`7LlAZPL0catI~G0`-47n+>cJi$k-Sd9lxK|mPq4j4Hk}KwWX)` zdHJnO2psh08XoW{JqNVjl}Vzrl$n%pI12%8aUSM2C>bQZ`C6dF>hH^O>slU1 zTCm!-DC-A_~&h)D~=SeZAbhjXu}c zryOE{Z*GbmhHuobIf|ZHUTdsi#jH`WOoAZj&zJq^@Ld*8WW^I#%}8v*v3@TtIl8y4 z3oskO{scP#0V8hL%snno`d|0(-ND!W-up~Amy_dmn;}nXqs|QM|Ja{6YOlL(&)iFw zaocLNMRTok@7lZKu&2;-5pK6Pq!-w;2(_6Oa65cwN}HRZ(JnCp+No_-6r6dfyeSNO z6NcpM^!1TK8sX&0IxT-0D-&kvOCZFcQnh*v+a9|EPNYmSVn9WWVPSiETJFpRBpfz| z%c$3TmdC7#E~d(W7@Jv%>^R2*`@-6+7*w@6d{&G>GeRcjIZ&>R`@?d{a0*wU*u>r5 ztn26iQd`m082flOVRA!1>a$uWYH zz#u3@Jh~r5VbVaXHWKTgFQ)@TW7JkuAjNnN_MT^nCiahmXr=NLaAjoGcV!rDx2>;l zw4^NwVQa`8IV8!+zJmW4WYB3-Jd?p}E>K&Nr=>mkZvT36 z4#+hY5iAlO*kAqK+R+g-l9u9M`GOp%yNp{0{ycw<*G;M|LAH^1Spk*#a zeLn3P)YtY|J>HOkI!>1^0qEuqBRsc0_Y&WuCX$cPQfr;#Gdso;`^A+6z0xwdwY21? z$$8s*LzA;CkI=j!S@MkdvUy_b-(RO-!)n%ev(3KeV8hLY<42-sUod&tjaQ~fTMzKF z_APFkuCx2z-v|8vj&e??X^Pr4q?Dbc`+vM-rb;{VK5oz6_MC;Y9P|2j^}@G5?kxfe z=`USln(Xc-W@f|zhzNivRMW9DQ!(*Xz!lwX*NxcHv9nSHoAFNOL4^@vLxFSkU?E#D zCMaRB8M;&2NXykag$MBfs22gLrZRjGQxqj!{dg{YJ8)4Y`)#nR-=`exj?3rx-RaZcBS5yB{3))D4KRC%GT~ zcNjI*bevLtF}xACY;*xRYG$-?Q}^Q0l5jizpjP53fts6sZ`3kMNVUVWCKP2t%3-cJ zky{n6au-OF1$J4la(jGqs_72D(4m3U4DKu+kZ+dfPlY1YOu1x2WSijXcZ6uwu3~5E zDz7@54FhlIRyq<7o9p}rzhK4}QJaq32>Zd$`{zT_yXeUN9y|ijWXn}XGwpXkbNdBjFFODaeEazd#du_io!;-tr=P}6s04NJeZ9FQrdz{GluBUCLyVW$pn7S=`-%yMB zalvXvgokj6JE=U~UT_<82%wbFwOWwbdgNZxMF7wmAV3)Gnj$T-f4oTZD_b6hvfaR% zm`;ggo+cN=-nN@O_n{$(^ja~EAnrJ&w8e>0f1(@~-R4(YUt1V#U=fTchDwhmW_s zHpY0Am)pi*bd)cw{mxezZ{V zTp#rjY56lj>zu1Cg6mXGO^eAe=rgGU_fc2HwKxkmu}o#y2(hOIq9%#zM+ z$6ZI~$2xKdYopgnP4h(5F8>zFfx9F&u7XfvRGhu3ms?eCxk_;O0l@>h|qwMa+ zDxM)av(90vwh8v{a2KF@MZ+s;=EiJFf+YsqbyML{LqS5&WM^i z%l|ru^P0^)h`gS|VlLMY8#=tXxxWnT==-?VanW$oV=qAC0iO6*iuv~yf^Hf9em>_@ z5c@h3#-YbBx?(A4V~Nx(Kf-N$fxt{;>P$l@_0P^$dV0847%xGc-R$xF(1yOWbM>HApAE4n7$C`%GrFD~ampfc zc75M3C%AJJywOH&r!Dq+(6>9YG_{b?eC==~e!xOGLCV*ipQI_?SMiYs8%>;rwjtBfZKL~JQ%n8?d<((wYz7)@l|hK-Q6A9HAYK(k4K}gf8AQPl97?YdG7kX zR0BUd;!&qNp0#@dYdFs$-_CCGK|JO5f4w=X#8C7O33P`B4wdLTmlR%}?DA3;q&?#S z#bK#uV`a#PaQzy0lVnIimY$htZ6WS(FPJ}eUz#J9Ir_f!i@rZ#^gZLKnvEaDL*G`S zXUQuu1OvV+w(RV^dp^vzF1jBa_4ROJ=J=0RVO8$BD{Z;WpZADqR3kBiWH|#(84nvr z6@)NBPV=q`l%Cl|c{kv{F~YskU!it6ip89%Xt&qTP`jvV1r5->6PyZYHTLEfdLNDR zQq(3rcb{t?guCf&_!gc;Dm-D1diRCRhnYbbysO-HP>k`?&4YJ2aF+xE$xaV9X{t;f5X)@V{ZGd4 zJ@9HEgb~;;)a$#dWWRhhUC<&t(?jS{^YXR1(`-Arm4P8zH9y_q{BnxTh0S{Y+I;dw zJq7rfs55l73z2$KQF-oF!e}M4Vq-2-e?e)vSG!2xGCZ@<3JjA6P`QmijR(Ns7d(jF zaJ^Z+Lc3I=livN5Or8U$J^N)VpK>y0{wLVV;i(|+=@r0Ct$&c$hdiktziPNxVK3>V zBL0*~HN8EE#vM#JuOR;^LKZAQS^n7t8-2O;BOQJe0KEioZ}X9BlDq!~uH514{ms~0 zNAw6+R{IH}3$cH=QD_OT0>IZU|1@?Q=)DOs`6}47EMpnv`@h;d^Khu!zwZxv-WXZH%ggz}gbsKHiW|ipEQpOm};jZV|u#y{v zaekhbpL`W1gT-tNB|b~hc_ppNzxEke*wS7Rx;6;f9dA54JvBij1pKW<;kB=Ss4)2j z8RlFOFA9;S^3r^M`WM0#H)+oV=5KR=RyiQ7dxig*c6nzz8`nkvXB!1(zVg7ycyw$; ze+a&l;P~^jzR5;m$iXg``w7WSvk~KtH>t&h%UY+sEg`o-H#2fRjJ{< z*+^=QkpPk%R$iO}yb?`mr!R@#`~bPgtg&%dm21RO-*Mhy(GHt+=ngw;YKO`hy;V#g zLf=37o~O^Q2bIvN$I7)Mly5Q~DDx$C%EcD2k=VF0P@3&O)+cemgZQNa6qau^wl^)| zQlhkeN6$I;Nz*`Ou_F&9x{s|r%>(F+Rk1kL7QKM4S57#j#31-D8&RMt zsOZXq_IXNcvPPT;*h`OY#8=JN_8f1btIbfNDh>0|IY*8Q&Gz3ypvi?8IK{5HCgY-$M`G5fcPNHW6>&&!oB7){bVrU0FVA^xbpUBK7t#VNUXQZe4S3jcv~exB!?W#g zo{}3E0Sp#xim+Xukqua?fp*7Kx`Ob?famrR?>gx9$Obb>wwJiDWU-y#SCxq{V6@9c z2FYEQ5jTWv#S~dA)*iG(ut$Nw22eELPNQ#}cU0rl1dWv1Rtp^1)}ZnP+F5mdE>DcP ziplF5>A!UI^wij|)(2-)!C_26t3B{8!e`egfhNz!f-eyKs=d~kmS|X6pVuFLm3B@r zUMYT%78+M?B!$s!AWc6ZH}*mLZpBCdyFo9%2b(Bn#o)YBN^MS+Oht6^Rnxe}T-iUh z1V=?IbHc^WU!XG6{_X|d1RxrwSW)-lv&iG$+;Kt9G|fZb@%i{n!mF}J^?K3#e6D&? zGFrCq2Q%qo-$M~vGQgl0L|A*J^);&fxV2Hc2bA_y<&iYvcC^6H$9+Gm$Qhf6C71PeG^D~IokfNX{<+S+iM!3*MNVS&{=W! zcbH18&_BsiU@DIJzv)YJgU_1vN_3e5D{*CqhT#Y$!_YG(hv#gX^ynK|-|vj;5glIwwzK51p zRpM8-D?&23TwbvqS^8~$>dsbIYib)dbhyO%@ z^99q#yCh-)qy8KmhBNGKMXgVSS3;|vMWBPGE5CcpL(fwJJSIAm54di(60d*H8GN~G z_smY>Z6b7PEV|PL?0%JU93KQH*>kBLZA%x#DTJrpmD_8s-IQ|v_F z(64`6E_;?$1B%|z1JmtR>uQW&af*uaj!c*e%1T#bD2YOzrQKT~>gXA8CEql$qCWDj z>7B0crbKMezR~O3A~5(m$g7hHA}5Ku(Yw5&}wK)b2Ri9zNxCc z-2g1$d5lhFz^HXUa`$E2xfK%&OcP#r`kAI_ZL!Hwf7Nl$3*Vq`CCeM+m?g~*Hec}1 zd!HE;H5Z}WR%^sEP-U${2739lH}v`x4`M*A{|xmk!82Puar*ctcINJ>HxQXs0LdFX z=pt};>3TVtKw+fBpLd&`i;9pJ3$Q;BSwkA}>(ym^%6Q0p0sl%caO#Dc|006GK!jT7 z&rb?L(mS3-2oFup4hfsinUJ4bg(u521F%D0b{y_Fm}o(w*7H(&>3d?z5N@usiTt&w z8ij&H5to!=#C6!d{-a;JreQkM%0K-SM~o(C-(X{rsE5f4+2F&OgMYoAf25OYOEf-d z*qDTEzXvgg@(n4vpb#+=Ft=hZ$xcVKB?LPol>w@X0@P?5PlFb{(-{gjvv5(O%k!@v zoe#?$y~UO9nQpJQpoo!OP8CYFG|kiq?Q=tZdK@> zZ6^xEpw(b9>ElQ;}9;93B)H2TzYFYjtj{tk!bljSQQ<$(RLmLGXG!r;Tahkbq z!g~DI4(v5Zed~+2aoPU{NKwMq?!zn={HNo(iyZEn z4^K~?&e}gCw=zfNm)XB1p54?I!%Kojmlcs*1w8Mr;Ngl#ugW8XshkJ91nKD~AElbT z23+NLV*EdYuTl8r6n`k+zTBr2JG@A$-e=lQl+2$p*Ry^Y6ijpyMjYbXilY@5I8XOP z9^Soo(-GwFEFZ3B00Tqw)az2Zl>wwHJz8;{|AJKiR|Z~LJobd!?x)J<1^0Gohy*|Z zxTDp6-Eml3KU-0(%~GI;*0OzfK=Be@XkW0 zn67`^tNE~Bbq_wfQBdnk0aS)mwBg^zK*o@N7RqAp((xtcWzvc9nRZ$bbSbQQcYh+& z!n=s;XCwxup2f|q{!Fx!3(Nr^!ft=${;*_J12uFtb!?X)0@8S=VdahjuXfG&gx=0N zZ|7~hxg+-8tGZ(5I-Ao{WVG*DKeW)H5jci(q@``*RX%_jc)B{umJs32V$HAB!cd*J zKX5Y$$Jm&wd%~VRa9WkhJJ~dIWMy?GFPi3lI&7-xC7)k?suKT37z|~$e*HK+YnJrD zaBRo>P32f5^X!fOh|Nl=jYWWOirxMpLbnpcvvsEi&7%`Ku(zA?mp>xd-F+x-3py$3 zG~1+xr0bTd^`nwwIER|gk3TVf#V65^A1`*wfIfXKtEyrsp10cUdh^iiGOVG@&)VVK zK{xrqqFe5$v1=D|1k{R2`|pBvYg4})o2`hYYQ{ugQ8Hx&-vS$W7`t~&${Nf;Xr@1$ zag8wn_*wY8q;?SBe>ZC^id&Tq(;bspB#N%My!+Ew(|Q>bf{*6%IBL`}K@i*7nbK?a zc+|{D8n|ZlZ`3Uk@w-EvO*EhHQaA{<7wXZ`>r2Of;z#&^&Ea{7TeJUjM+xQ7bGE~0 zstYDget6nR&0dbsFXm2z$h+1SStFRk?UyObq3PzgrgcXIvaZ7Khd!CD%9+9zOrMqz zlI+VaZfa|pk8W{Us7m-jwAxns$G%7a-#*5Q8)_vnwUlk(LBDd$zyjKO(>eX(jZfsY z*XhEMPLAj75<#L9t1kaR1zt$Nk6pd>ULHI*Ko~9-Z%q!O>NOa$h^1ck$}Hj8RoVQ z-TkI&`iYfNRpmQlk;Uak0X6)$lRtM!jHp8$^Z(4ovgZLIX%RNI1YzinGv8Ek+e>E? z+ct$GjK0KmJ}H-y9lU{2j;&?c_aJ029fZk5Soh|~^;#<3`MvoLoin%<*{X@~M9}LC9gwgtLkLT1K({(my zZAVd$Zvar^jWfOw@wOkG)jX$ziksQy2Pcfg3G6Wqg=r~M({aBl!}3+Sg4T}@ib5H% zU|yPjgpU$%fD)^Zd_+pV!m5jAv7{J(amIMxD<}$Tv3KJH`;krx+Wz^EnR`W7Hnjl` zzbyacndyoE(>fZB_x7ua5sm1F&J846R_)R6JQ>KH2L9ivfgfMc24d$v1pkiBU z<;8hxHE>R*_nmIDz`i)Hh$ribeA?-AE;=AA;SmMn_~u)86DU0vJZIP(uV`=&`De|FWuiV*Px zGm_LC>$t6x@DHs{TU9*P>~+L%#Iw>^+8c^**)m4?S;unqdX$oQO$fGDA{=L;_#~Yf zz?GAo0hI5ks{*B)QInx;i!0E43h#>O%JDz!AK^W_>qd33Pt7yRl!YF}4-eSLM_Cxh zn~e1a#FdfP$`|p}~FZgGJ%_T zHeDLqn!04UVcNvRCZ?deZbGvtn|~vDM+SNWU(CU! zYy2bTJWkhVLNBHI5IkcnlfYSexjY+&o|tFX2fuThIYEhEr(x-}Bf01o>9^!$YFy~^#G?LvqjXAm{#NV+*yid-Ui^7R8`#QU`B@G+V|yBnIwdHpa* zGT3Qs1vc!=Z`395IB4ceu%P}7`h3l)Dl`wxXrk54wh#mh2>t+!$1ihc#e*ZVy#`9c zU7dZ~{4uB%AXmf~DrCEKagXyf!bCtBamx+>-O*SWJ#XzWO1?K2T`%BuTMm?Ty3m&W zgDL!c!EZJe)eg(@S{R})p#-|-LawkUsye+iaa(#o0&VR|jnV%6`^gI}0jlp>HLGHO zBB0~Ucl7&&(RXxn)C5H^*&^sEQm6R%d6>dG?nZ9%*DVE)9Vq-Mc>Tsbd%b}ACwA3u za3WhFUQ*RJYBXg(MQm%!P3#fag>tjA!YskDu>AU1qct(}lrE?5f>{Qo4~1TZg>zmC zdr397v4*hGcfvVbDT?Lbie$r0BNz;|B~|}PYWA^=w*DAiNe`2Z%}QxIF%KdiaC+3Q zF8}L?YS>g;N0V!C8IEQ|tG)8bY zaAksN;cAlUZ2Q@<=@?qzH3N*Q1VHBj57J4;YUN8q!2Ud&8KGLg*&%zE|IxYbYhz4X zC4)zuU`|{n3u^LWw}ws;mj6gJHhR7+uq21ZO%^c5&N^E>$wa>{V|dYYy<3Yx4lCc zQHS77NY8-6tN~L)g-BJ#^K>>S41(U!jm~n6bKAgWQLQ(gq&XhN+_O#5l&KH@))OHA z>143ktM4KNc9cUprj4+;5-VtEXS+J@vOvn}_Su*I&am50@+@9Uk#a8WcPf2yHI#rU z0{+~U%Lpq&ty3c3I6h|AQv@tG)D<20DO&<#^LNSN<3yk$G~g9ze9xA^w;dl1ICtjy zxHWpJ!nxAGp`p-*Afw7pogM62E2nQCl_jB(xbB@b(&^OYq>huDvpRD-@u%6ca~E?b z2cAihXBDQHB=({?sfPW6dH~szpv!v?GVc#T(xnc6H6XEIhQcF(zOi*edo z!I?rnUyTWptkhC2vED&EAw7NLO6F$4SpD7_tV$}7IyTiisuMfukH7ft5J7p$?{#b_ zMUit+oj0W<^t-UZ=_&JD;w$R~cr)c)M4~8yavvAQy>m6{C;j>HDljltF4)KCDQHU_ z-Mq98!W<4@zi~{-Kn10?pBt|MocKYdak-Vcq8N_*kmjiBLq+jF=lf?SFN=(XJ$x3* z=GRjKb^g4Lg^;ukyDk~W>%=6toqA}k6do;79{%Jl3$He+KSFR@2J1b?qp4HE#(wsF zp$n)=a?E}}9cq)JGiSGrW-eW%m+|T?^*r|-=4jU9rNfs?LAtXK$&!8qa*c089xY%S zOM{y-lUTKHb>kH#%Feb4Nam^D?X3Lyig7gCtaoI@e@2I8k_*t9-gB zv4r`o#ysi970pATkhp7c^H6lBOF+m0gZ9*4isH?EK*oZ7kanQ4E8GZAhK-KuB#p}nzAY{ z(3{FcaI43o6XX=KgT$}v-@sJ7K4JRxZf}&y*sk|*`HzXXQ!)6hhm!Y) zqnP-PrZ0`djeYzy6Kqa}ip~M?h87L!cN(Sbmqko6Hj{R@E5<^!27V#QYOU(7d%ePA zA=>O@sSL+NXOXd`vPFa8p(`k2NFs>#UY@;qsZA|~esG5I!b|0wMT@Vd^16?N^h)9H z!g|$BWt|u4%4IhuwJF}Qd~8zZpoOEcqD;9>aeG&(Ufp<5?E>Nk%P4fuNg$2P_h#3FWtf?=71C0uJ1MooAq5F4>4az!@=NcrId#JIkm{~o`oOx68!9Jiu?!W#wa zu=RFfmY(u-Y|9<#B(;+6xKr=_BjL9x^hxf!<5B?wzYjlT>J@BD?f5wKWw%}bUDs_z zL!P1gqQj#V+2#J}4Slat;V2!gGxJvvA|OMEKzn=V)q8I;ofQWaAFP?iy%e(BCw4Pd zFED;tT+e429{Y}KY%;fw0>L1DTRXJ0sx~$jHeN&-5s1vc zR>}ePIUMxmsA0jj=5WQbw1$HnTErxa@!~}hvbr!slsNRhaq5U#N$+%z|I|)2M+C^! z=K>5n@y0vFRp*2l&pKPGlTtT`4VOF#GX3N@9>4#rDy8Lz{f>a^TW@`fX^mbxKXBT; zj%6g40p3!sJ8L}+yQRXkpc0B#=M5txT~gu!G!H~4~->EJefK2Jyh&p8$?)}+EvxWbsgE1odLE4 z*X>%T+^RL;iz~TOQUT};(Onq@jBSi^xSiDagI_5`)2x=L81S_pC?d4smu%8?HGTpVw|Xz8{=);@mt>mOV(j^x!iPC_nEY$vQZLE4y6@ zWtjOji%JlYfW)t_x1(mhYj@hk8^wRx95JC#ZA2a;((;?y{-2D4m+gD#Z~ipO`}9uF zOZq5wOtTPW+he$Zcu!TuSpHl_$|Z!97in4DLJlwPk5}^#SROFk{j}@8m+-F3hvc2s zCtolMgm!NNuBCbMSlSSpOFwr@)um+ss&1T}oMJAe%A$h%u&8K}<)glG@Y^9!WBW(j zo8-j^PYW1Y-+ZG`6LLaq5$TvSq(k#`-Wj^{BoVpg@Yr)RlNqnJl%{ZvxCs!;Wu!e5 zX}lxNh4k7}n^$~1@nrkCXG$qMnGGQk5p}d)aMW#Yo0|NP@UOTC&Jp=s%LiMuWx3Px({NJONyHQaHmH zCkW;srMxZ|^pSoQSh|r|%WW7$d>tf4Rsh^-?#H`iTc-4f4q}23v}oE#gcV`bQaZ+A zWO^#Y0dxZdqYk9CJ4s`)XgDn0LQtiQ$b7NO^2O))KS0v%Kh8@x=&nuKw?|@L%?G zPfPDL6Gm*J1Rh^4qN`TR(QAd(S4CV?Xq;tAv7Ho=ssE)Xyj$I%S&|6C0fZNp3_n$@ zL2q~8ob1<{9SeMqHGax`t=2)Z(G-o9sy%Va8MDGt3axB5c6_QLZ2UsvFy_s}ssZ=6 ztA8nsaIyHpZeqhbO4u2=@!z4bLD@mjC+H3lS}L%6KEV#P|7e=Bq;~!%aUt2xQQ+)( zuHa&B|CjJpzqm_AQo#^iS2?Cu61F}Sv)`tM$C#yZ5P z0~VYpz?KE?w2sjG)1l^8Po7SslFZVz>?@oL;`*Wsho&!$Xa8-mEV`_42Fhu;^T6fV z*jn&0sY{*DBQF8Zqc6tE+{tffVD&8xf6 z$-gSi8xEaLFBR=aPS2&oq>FmY?y8Dm+)*9bI<7lH$2iW)Mb3g{<+}S(O%8>=V+rYo zYUzh!eHs*XcJiMyPY;Tk-QU1gZc!Sv80tRoCpmya)5v|HGxA!`)k*5$8@qL(!+quDy|;AKUaJU2CgQ z{@ZwNs{SB=MlN3sq8H%;N?GgYjNB|LD36sz`Bz+gtgn|8c!E|dD$cCa^L;i z-IyP!opK{*T9mRQh;A+UsP263U}(DpK|qNRa^R6NPO%hTJ{Gz9<2#GdoJpKH{lSXK z^w_V9OzxSGzHFgn=s9jP#~}zdmWI|Lf`XTg2WvM!aQLv*JA^>Sec_YT)YKl4y=9Lx z<8?)S>%_)W-M*3fv7H%wSLgY$jkg`3Gpf1$q;?J+&4 z0eo1HKUYY@=;)Miihtz8>HzP=P-rEJ0Gk z1MLW`AhQjEaAujpFRQ~EM!9g6|hPDI&2tfj~da^TEa+V=6%tyqAgL?e?K z0o7SVFfHSKOGz*e<4@yIf+60X;-2j;`GKMVZoB;3b~ti8G=#IPD1LW5&rJYulS3rA zbt=t4Py|6ywcv#C>g6JKdh`dzwBcN!^6Hc;C~XPIKo+ePYP5@d*=~&EQ=u<(#L#aH zMjxG>tgYL;#~eX#4$;vwR{m}z%iFsEu-oXby4qzejwt!@e<=&R2W4Nd%H-$Qe zN+NaQd^*C{z=n7ID>^*Q!EG#BxF}0UvlsaE;Q2!jo(YE0^eKYyB^@0YVfKgXqEOAa z-Rm`O1Cgp366X27`hBfSdUtQmYfE2_;`u0j2#-mcfrr$b5K5qVla|6+KzQ=@_>Iji z^6G$X$dKw8lnpzaEykM7sjDj!N3^|n94)6y2w4?_Kv1tP0$^y#2Y?YVh87m4i-&aM zB#Fr5zw|%xPk!Ozm{8#iyM7htTF1thz7NO~z=fR-HFN2U>l_u_+U%{a+(-TguLgmX z&C}kZ?9)7B{EY__Ig6ahm%4ta6-(WEKFQP@UBwxPmVVcjrhX{;EnIcmm~#+XLr{y< zEM$e&JQTc!l%6{ux55h@-CiLVy*{I(j$TF!o8Wu}&-2ttJbLM4lxt}a{WU5GiZDE^ zdTm5>R7jv52wul>UEcVW#s-6}dv|ecpaTQ}b|b&ds@nl_l7za)ormH1%P=O(>OR)g zTQ{q6#0Fn?PxreE@KU0=HFPhxNGkaWQgA+h_A~SH&bMlYoZHl zM-sybaPSX1z@H)sXdpoOitm)J9{|%Do<*CtpFBx2#FIym(Tt{$&CG4Lh0gATGo0Kbg62aG&FiCk#`PomIUwf?_ z&->^aIW;Q#uy-x}{IL;(2!u7K!Vfws=@I^XIQqd-d%Hg|47?OFw|pMO-WqxA>ZLdH z=G1f8iX8LnaYlK^8k0}R>j#+rDs(QY&bW#d?&##+XbU0CVhG9xe1k*ci66`CjuVx( z@)#t>1(0!?TmNFzYVl{D2M?%@!fIS5mw#SMs3&Eu*<|y{7q%}cPZ!v|2d9`*?RLE* z+uKT%aP=;5Xy2LE$aaIdwdYTwi_z@B$mXj+|!FN5~%l`SR?tEB^?exlg!1z@m&#>`6yx- ztai~oK==!1#CLUZ0dq}fxnUbNLg~Tj@51`H&DW?{X5}!$D)3UgZtF;`x46!t2}e5Y z!EekgRd0!TgFW@QtSygXS+4>DBBX(&u?Xanm(daLUZDqhUHE|(xVh{JL)L}$&Ww@w zVGzltudAZI9CVvKUA=D)3`5`T^wX}xrAt*j`EUTV2`iC3T57$$dV3`|8W7di?nA`f zTcNRI6?46#$_alK@>5efos6-vAL(~Ij(fyT->_8y`UN#D zrCCPFGh2QJIPM>wc4(Wq`vS<3=aU$!mzQA@pfCWC#tLq}@q532Eu{en2;;X8cRZ=l zNQs)*BrRXwGs13~IsUrxG$pr3ROzMIRK?)WXbp!L2>;w=*$%u#tPy3+=yZ~lv*!yW z2iNhKfDKC+&SEa1P0U4#d}A(&u8bU2P_b4C{v2mBl0CKNYrWkUXk)b1-^2+3kwS%e zZ&9(?^$=#mOZVOZR9Tb&FHw!wr(8OZlIK-Z4iUDgamPHk7N#j_8VPmpP(fDT6>DX= zLf|A={banusU`4kXKL(nfE^6EVZxOE7hu)rDGbftKMm~$vd9zg+{34jahfaMfuaX4 z#`)UGXgz(T7{w(#{|XD{RgvpzZ`ZH?8*}Wd3I^J z!G++aFLv_QNr#lq3s{0dp6M8$`wp8$UZx~6`DP6?_Ep<6*mzE@$VT~G5`_sr$)kdt zOY<{RH-&tQ<0qV=jb1T%!Hu8{1Pd|D4BJpdLK%b9)$eYdk?(@94}4{b0eZ>!nr6sb z|7ZL1(zykscFf(~m95a4d0QP`>Z14vC}KdZB%86+){pcpebL|i+iVLN!@Fn%DF({MaVS2 zyUQAu6N0r1A%SnYvX<&c&QWf!sq3;8ucKELP*sI1(UdZa@`M+NpV>1L<=nbjZxZEq z`#at4ut2mcTa&(?``*2l3%I1KEVib=nltRgdr+kO>?Kb7{3~=1?>fzjTsPQfY*pqWH7My`4VTsMp(>IMZG)VMz zwN^#&D&Jxv?5go@brkQ@IWw1L3h9cc#`qHUmig%~ZoM{NLFY}78uG9A#_YWM>-BjF3;Dlc6?evm7P1nhzoGcQ{pYiyZ z7-IJ6!Dh)!$H)A6W}FTHx` zm(zIdsNAMzY|{s)Wp@pt%a-P7w@O^O8&@C8eE7_|=~G41iF^uqn!No{KsQEhJz^&j zr@DUZ5-qM|SCaLwOBWk8o3K_XOM-Y?ukp7}Vy``G*dRS7SQC(v!Mhu5u&8uSo}2uJ zO$XThoz#ZjEBlKrdMkEVp-)fQC;?Cv0dlNeCnLecT$jD)v32pX7O{Fg9dDK%P}l1j zTGtB%^N#3g!k-?0ebG}ZHg%U-573e=_sTM^5VSIE956Ov_Z>979$VYkD=e_sRO*kh zv-){gUPI_I)${>KUTmo~0OcPN%MU)<-^`tAB{571q0ysdg=v5POVMUzhF|XrS6`_= zXqs^wk9eZfH@>Q`=}fQO@dg0swU?Iy3%qNc^hP=oLY;~?ZBg)Rws*!)?n5zm1aN-H z->;o%u3uL$;LkF!7iIo2uqL=lSt_*?8TjRmi?^nFyDdxhBt3C9Ryxm>jFNqc6bYrm`+&6{LTSvv0PtbOC1X)`jB{MgEp)f1LJAox+3 z@mb7^@fRzQv=X2c%1NzQsqHh$wR-Rlx6itQ={k3EcU{tMVMJ3^NS#}Z1b~KX1$

    CB9YTPe}Cnc8&^AJBCxD0DlGNcX1CJM{E>V~ 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
  1. nBlZtB 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 diff --git a/e2e/snapshots/webgpu/mask.spec.ts-snapshots/mask-sprite-webgpu-darwin.png b/e2e/snapshots/webgpu/mask.spec.ts-snapshots/mask-sprite-webgpu-darwin.png index 5c8548106e5c1d4405e9aa533f283349d3bc8229..5c11d5dc394dcdf8a5a8b2103512f131742eb22f 100644 GIT binary patch literal 92602 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𝔜F3a6=Q* z-x?r2DDRL}VpkCPk&`@9Cp8ZKfVqn`D%Nibh3Fz}27a`k`4efI=_d*zoVrE&UsJk4 zqB!Crf~wW5XdGJ3mSf?->c@Qi{~K@T{2k{Pw)-?To1}4Lqe)}iPGdH02X+dfae?|ILkaDL2MlQnzx?78=i>-szw^j~={YKFCdkn)}IaTwomT*q|{bDaQR zWlqbqSYM9@#~H{Wl{i33oPpr}ou!fDp&s-{$$vCsX$_9WQmwd9ok!icOG0RNwl>rI zY{e(&j(rq~g!UkYU^S1=TXv9Dt`bMS;M**S%c31`st7;oW0&w-10IlVsI5S^K}iNi z#$h<|IhY09#$AAAGHN>gf}$8@OZMozJAfRyx`CXB)-JFod4ha`Hm91=iNApCOt+-} zj~w;r`1tY(D5@kGXuo6SY~~hZ+V#P01(lFcP8z5k^3=*2Dj9P2^Wq-iNXmQeySqqLY6{fpi+r2oA$xntfcM?2vQ7UrL=MC z@N9(e`NvcmG%tO!`ci_M$WSaa};Gj zO$*Gdc1eLmqx;`fb_`{SlvrLKc;+EmI13%lPa`c7Oz4j-R5zTTeg%NA|&z^f?Tf^;A?6IX7bwF`&$0Ht0I=jVb8-3P;3bShfHQ^r0kJzL{XNK;CAv z-*tFhR7`$ZL>ZwRcEle51mnRR``3?!vGXCo1@#~=E0OZfvlj|G$7}mVphSN}%eE5} zL|gTVf9BlhzFiPCpP8Ra<>pekuka=4Q+Fmd00{zmBgprO$mqqr(nOP!o(kX(-o4sF zjd1kl3xH;GtCRogH)-lC_8%|5 zm9zq+Krq!cqDC6`=EPW%PxK2MlpKbgXXL|~AfUte4dR14^+8<9OTwO#2?VKgjd6eY+8ZiF9eeg| zb?qW!E}+3U0^FYvVRgj`A!7v3_vM8ykmr~Zo-hF|J&Gyrm3OU><~CKh;z@W&2(*fs zf@er4sRiYicojbqp-Nn5J})#_{*)ke^V0F)9yLElSdHMlMOYpSm1TXpG5D685|i92 zYD4GUgQ!*7P9JV<4@vqe8OAzYc32ovmK3Ihq>zjFhD)Zw6%_1_oN_G+DxH$>zNZ#I z50)tTGe}sJS3cwmT={`6-0_rdue)#*2KF(~X-e*w#^Z%T`fg`c@O%a^x!?qy=QT{B z2)Y4O%zKDK2sPlE7YP&3h)}0HZ6NZeL22A~5vZ>5a z_x?bI_IVk_;5m1;M<-Tch24f9xjNuT20>{M663@iy)m3w<+@<#s*VQt)lpaz*f zrOkdRI@q9I#oO|)f*>nd*U_$3^#HFO(o{tiAW-BJdZ3fTEU?#0vUBt`6bo~Bi_Pmm zy7%=*r9Zf&T5fZa8)&O(+e3+PMB{f=1+Bo&$e=P*F*DLzdM`;=e^dh{A>MwHd^1Z+ zaKmscfaR+d(xMMzQd^cr-AzdHh_n^@PfcS-xAUvbF&-OC+vku z!f6#GM_T+|aI0C70(6;#9Xx+$^BSvRk!}3w77&%9AyG&X{DWP_%%So^ed*ll4qWg_ zob>UznyFP|W_0q$p*R&TK$$W6=&$A%;cQZu$5hhH%Ene^q^XbNOnUMOfQfZq0`1LL zfc=J}zST5RQUpS``t+wt$u+)lEHf=}*Bl!D&VyEHG7#jgDS8=SbMA zvas;n-!DrZFlIDdCv0-{)k&9wB&&A5BK}B?D|E0@l|iSrijCWjfb=aR7XHlmnXZAW z0fXjjs~pi5o(o0~br38Wk$>u|$tNWCmZy(KRt3wr^!+&ioUyBcXre%?S)@o)0WW1k z?6C`M=$SqP0Cpv&#jS~`G$oC+EJokxIPkSl5mz_BGa7Gx2Evdj7O|2&u&}^jrhT!) z=pXU_^@UHA1aoGzI8q}xp02l<28TQFoJ!PY5q_q}hK@8=QN=nv^7Vgv0V;JQL^8^) zoP@u4jdu`8r;5IQ!n8q-XXV0>Tq2+Ps_j8_bb%z&VA-;#7%)!G@bzBz$*q_?DAEF; zpYTA%$g6xU@PQu0^ofwxL9KR+l}^N!QN*z&g(XjN{sSK!dLW9e16d70gUgDUqB8S- z0bn{rpwQM6jW#)eJoPeh5G!Yi83o`uWdTNSq&xL30D09{{-8!P>&n1E1HhO(RRZ-_ z@{R(BY&8h)UJKrEb0!^pBuazSn^ZIY|Z!mwFFn{=J}&g z1$w!-Fr^-(f8~F_o&r4pBTgojw;JT}RAqTyOz7&!j-=aVV@&g{v3pC1y8vKrnMm~* zu&A?`JzOf>h(ZGe1viN|TIp1kpku^!$Kh7WzJr&W6^g;I&W)^;D5lxaadY1f|L~Xt zD;PW)2U8BkA}T6szT*=tzX^b=KnyxG!xVeOFIBV3x#oqG*ZX|;oUM?H$%st_GOh#3 zx*n1;(uooC^YfedqrXQ=T0czu!QX|6Dbq`q&9a)&YU;scK-T#i{>0iNpf!X+e+Yv_<3%uLg+mdm~o|*zP z&_9Flp$>pO0KmTeMZ0k||=A6eB_4-R&^eTOILpzc> zQhdS7Sr7Xq4I}MWrv))YlQdqHQB)P&qb zD^>rz<&$q7cw%GJ*Xu=Qc_H}?f85O|r3S%w#Z1!EGM_>Y3*9vGS>e}Jl&$y0@@q&c$Yo7;js3hq}jbEWc(;D%i?=$QSK&j&-$vZ>n z3}8So)Km$OZVEJbK5a&stu`tIrPtGqU#cV8F1w!8z6Xp^{L&%d$s)ene2o`Da)ppIN&PP~26P9iHDnWX#O|;` z*|S36x#TARG_eF~J3K5Rna=-aaJ0lwlEO44yIidr)WdH)(~s}_a_M%~b+>F;QWunP zmQxk7Pn-2rLd^%{S6gxl<^WL{k!SfOsn~+-1~5g`*%pnhSnK*5_LM_>Lziy)Df&7lZk4pV{zpP`I!Is#4IO0$FL5i>YIK9JG=*wjXhjSmObqOA_53dX znuYg%tU}}ai9l?ZZz0fNh|?H<^*Xil$W8sms90oRhi*q>7ZTWp-Nj8u+L3nzmqaB; z%}}^H63D2orS%JhZH1G>8D{6@oe?xNG*qE;oZfG=-H9}va={&KC< zWFK$9yd#v~k|)#(455OmIo1acg)n&@o<1p+)hbB+vcjR%qsdQAhFj^atyHfCr^M<; z{(J?X%VOL#Dvrn@evVx>}5?-%(vT* zdzQSAKKJ}|)_C(#Dbdr@drPhaX8w#(ip(`s)DK8k|2Kuh{=FhHP@G#XjqJBhY(PEa zT70TKQ_KEly?L;P@n=rXjtX`xPL?h1EeXT#SeUDS?b4Apcx(6~q-w~n<&th|smiG_ zg78<*6eJW>`cSwtAiXT{=^2if8!c-sc?uNh3Po}`B&&UvGy#B)Z;v-73|2$y<^Zt2 z%2aG_Z$*#Iyr-h;6CNMeHqL(<#gAb*K;Z>UVjshM;E(^$)ZYKb;)ukuvbPfxw-J$; zcZ)my!NIb?59Y%b3aseri&nQAD4A~q0%UsVyZXoLN*-9H(Jmmj_*gm2T4PmK?S#xg zg8O(-a(V2Rc}+Eu>U`6iY&hRsI=Fc_TBDDb7XZQ&;N_9#@k**UmDO$u7&PE-UV>)1 znJT}D!KceHZH1QLwB8TZdw1V)=7&D_1Me*M1yG^@q7`+r!@Z*gGl?54GQEjiu{Hg! zbQ)wRQOniD4Ln$VS{tZ@_hQ3fJ>G@Cv|1m&Y#}jt;2$HU?v^am#xt&4U#(Q^b<|qd z`Pyad35KsblCi4#vP(Rdt1Mb}F!xG_nir_zlVvsx&LSGMZnRH%G_Ha%?l|A;i9nCQ z!4Ad6et(^2p9N9OSva&ce5YkDgM02YZ^L$I{)433Ny57`<+{*QDsaFwVrDws><`vt zXi_wm4X3M2f+NMTWsYQ3C{+5PH?;mRNahE6-)BWHI8Jip*i>sS$m*}(^6a&Nj?F)v zxQ*KLQu?dyxqD7!h6RI;#qiII5MorqMe_2;e(mT~Z@o3^EIjw;OJm}ctIVn-CBJNr z+Ahq^ZB=iqJ#R<3cA<8-Zxti+od&i;`7$i4+}saEm34^8OI5$W*dEdE%)S0&XRlma z_1WKetN3juSWxYB5JjWjeg&UPo2O=}gVs6dAqBTdvH_2b@UOz#@Ablj^6)Zbw`$GY z5^2@^*!_{krd7v=J^#zol44>nu+L4zANy>U1E8O-1v)$k6DwVec@`R-qg1AHERI?| zhGbh}yHNXM2i%5d?y6^eg;4TA=IggNyP5V6bPJb!-aQQ>JeS3k$?tm_$h%BpeygW6 zX&p6|(Esk2EkPxEn;fM!8Q5M{?=?@sm(K|+*+=Gq!`{0I*)3NtKTL;g%q>(Vq9P)V zP7;stL|9=W)Vi_Sdv_B#y1*FZID6iT10MFRy#4x-?P#tvI`kB~O(L8`W^U{@QN~QE z13%0}v>zg~aI-bfp-<0e#!id_RXaH*zuc#0C3LWp6pY&yx{!_Ny>!fUD)MCw)T)b3 zzxqYdR}|WkY>}a7%@4t6``tME9sB%zPm7t89W3+p?0P(BtWYxgTsPJA>|_7t71z%H zcC3u>Uax=z)#&V@BnH>a_D#ma$iNlF4LQBjJg<3n5YXALK{80(BFXIqtMcA`l5_M^ z)@)vb-E0ao(|=T^VEp&U%(3l@#k^Qo>JO|Mk@jQP=_Yxv{hnBmQS}ynQHp>;oZbs{ z_pSfnYQmA$rqZo+@LsdT!Kr9atbnS~J#e!NsSHXF+wvVA^o3)`iWzL%Rv(15c#V+t z_R^gg-k)24%Ww&Nza&Yzpvuh?o15g`LnuD4KR>yvdiXbAWi~Kb`_xK#>N#ZZ3FEu| zxU03W_7rr)s0mG(#KgO0Ef@Qb27S~FbEdOvp8kHWTKV4tc*`v2`NBd^E{}RBFY6Cc zJIl+e785<=LYLbE1KAoYZ2TFk*790gxVv!8?1(zfZ{Xg}@i%$$ycx zOsu4ARTc$%W?n4$Y5kZI5ppcfEevs-V>WdQRfCRnLhj_(kY6l9hb2GIcz=xhYw_F4 z{Vr*i^C(|JZGOxN=V4^Jp0W`c#3A>-QEKP*{rlkTF(sepc-FOi7WRufuqoQ!V}=%% zsPZYyZg}zEeUu+F5R`qY8J)%&50HukAc|FKolrboj`kK#Au2rw0@Cn3xwjQx&oI}p z88|u~YlYLQ+b+oUUPmo0mB5;DXg80r%Tns-q!FmaVMLT6V(jd4 z;iKj;54Fl??@Tz;2BP6B=PPE>zsFHGE{-#@Avo*d7q%rv0K%`OAwaxXcUrpQxGc-h zDLO^&q}6(g>&uqAlfgTD%lk!brF-a^m93h2Ju+FTyXg=m5@%Mrp=*xTLSFq?=k#@( zpLeS|X!3JqWZX&16}2&<0mem`^ZPwZyZ+m%)`p5vP%VJ>k$dYYCiK|iUATUFD~%!Q zI$q%O5xCeFW~A%de)m`y_ZBj;GYll@kF1<=;C4;uift#4RyJ_56?G^YhpX5W#w*?= zedqiq(&#LJX*i=>i`K8EcQdo|Ts=?Iyq4S*hsBbLgRfPfK6OlWTvF=iL}Lw=9zEdU z2pVCQ6+?HK*`#NSxZe>w{%Ha;$Db3Lt#WiO#cMTSHy&n?*8hJa>`HtauZ8b*U`!9h z<#y4!X+sHfnTqSBv$hWH%}elwk1_K$<I*!-F|hS*k`e_X>^R6mr9 z&ULq(=}sDl7)srnEqkoV@OIi?h9Z-#;y(PNI5=4#ON7)#(V&0@^zggT z7Y+nuM+l5viyXdhUZNpTzA2~55t9Gg3&{Jr*~(<}2%@0Z%lE`cg3|-TL-P79oq*zL zGm5#fqK5Q|B;?!m8ZGW=%WEfs?sQwTko3j@sH7=V%WH~L=}WIcKT!w@ez-`E{~-K) zX8f?qMY*5JTTp5uTE9Y{uZl(7w;+s$U^wD_1mKohftJ{K zlAkJH=@fIlK|)kda1~3eETjz}HZT;*NwW8Ic53hvqI944|6^jjx8Dozai7N5Q$uc^x>&+!Rue* zP5{64SQ>K|{$IuCGe7BvEAH~Dmws?SJCTi4uBX6VyX~h_%zr%uKZggU=-E#FLt`Ui z@NXs_)%8bP7KXod%eHwf@Asc+-jk*CVN0Pjh!}Q784_E>o;uz5sDLY^Ke!T5lOsEw%c^CoH;)0mL%j7RnQ)+qwRb{B@Z+1;zZMBVU*IXa3gLQqZ0Hzh~6` z=gHF6YtQTqKnfh){Vx#NNMdc8i_lxH`kjM1Bg9=n}-p zM+_Ga7%Cew6STBEy-g85Ur16S#ylZ84mLi!$nsw2ZJcA_%UC%eh9;+^gvG?{_%%vl zx9a8<+c8_H%7+lG3~)~UI3g)LQ$1oA&iCohDclt*d@GyvP2>_S9X%Uq08+g$z&-Ta zPT6;vFQuXA_61*!B68_w6!Uq(sQD`KXdY+MtPAEE$w7uPLT_CFJer4BK!rK@>WIKbDM~>hZ3XPUvQtK+-P&(1I$ooA*MvvpNNN ze?6uu4`tqOy+)_*tEnR7-KA2Vc%NnyPOU2SuSFrbvcByP7W%f`7=p}IQ+ciXlI54) zli$7MZpW`!)4L!3w$Js%k`_Y~(gBPE=8^jI9MvytTBWl3wE>Kew}kQO7C{#m;gnwY zS3{OvS7nGDyU&YIZx@4c5(DqiAW4Hir7}F#>~r3uFU)*`Llu$jYQBe6-M1u^f)sG)GUNzjQQ#{eHLQ%L9^{&zxUVYk6;V&Gq?q zeRt*T>$bV4|A;eu+PM%d>`ra};wZ0b#PmDQXN;L7yIgkndUx^DFG?plZbOk$y-&JM zRx=vd8y%ij7Kou`!j0uST|bAhze(}2iXwz7g^c}p9XAGHdG$lX=K-)Vk%-7PY^*3}Sf9ZUj|3U&7@F5lF&Y4}pWw;>o@>JgQ-Z{LEpo zpS#0|=yLnYdZ9qPRPfbxZ;D^e+wr>=;90w?LrDg5rn_-fOzsp8Z{q)|f3VrzE3v2~+>I^SNW_y6I-cKG=C?vyC zZvAn{maxoQhedF^t(Ie;=BV$p@>&u-g)|I{CmV%$%$a4o$IRBm_G{mRbAwvNf>Y#c zdFZ38nIf$PR*@}TQE^pETvPn+QXG$Z{IgVhF9nt6m43L;{93fEypi{xCFps39UT9W zWdpBEY!rRXie8wb`=WH4zHxV=b$O786*ums?JN!u2^|r6kGJcEJZr1}E;oyjSfJY7y+UxM6y;X+u-h!Gh{Vs6Raw3- z_V!;<0ZY{6NLCCArzJA4`MAk%kHd4g z8m@<|{kYUqSB~1L>)j^YQqW=!ku6CC=IIMk1gc6@v{OhR4F{fUeg((B^%D_@&=aDL zE8IJUgIwFyEllvO&zlJ6uV-1we7+~^VWprVQsztLe zcZB?hDvDHsPWoSAR-aF}iMaHOAzU&Ue0>h{pZRZ}u0+Qc#Qk!n5J8_tiOp#8yIGv2)* z=udJI47AqcLpdG>cSooPEkRs6@`md)J8x2m&JGj+48kjr9c{N4-fp#r-9`AYmI;NZ z#mDy!h7eIkZ>e}gosf=WpX~JCmXqG>Ma^-cresc(schx4=iO}06V$uUQKhS@RqSJY zo=SIXT|AxlLD%Im43{BXYN$--As%HY%n$gQiW6mItFd@$$3`H_6O7}_$It`5!mcNJ zd$Ko9#WTvTtGV;M=`x?-G2fc0!R`%Swr@q<(U<-Z80x-zJ&;8x9dnB&UisNEY`3%$ zCUgL-FbEOeD+QHVh3ER!FiB2k@AqdT+j+QhvMY^!-@7`Q=oM@DTckU$i6e?@p7c}U zn=~&qlhu5N-})QRtcp_}l|cog4ijM#>T5E1wZs}v7q%9~x zFxRYen5jhJgPBCHd!?IG5i@;S419xLHA1%uU0#q-CjFpj+qH+U*FyK0T2M!7+u8MG zU1e-X_lb%Xz^AX4w%7~@uGM(dF?!e6AobwfAax^wuqS}*RCs**Y!+hSxKGn!8yo@5m5H$8QdRn5-eCL;e@LP|BgysG*$1> zoS+#~>wZvvJgXAnJ5DiGi)k<)f^_NJ_3O_nj_*fIU!&&#$HjraH!S2?b3a~=Nf!kD zwuy}SuCto=8>~dqIof->(Rg9wQs#m(v2wiQX0i;$`+2I}mjAJ5t?2xFrN_TSdr_$A zIXtZmz0Si*dAp#XKEfRWkW-TUZfXh{GETF0_LEgj$f(UsBxF@!)?OFmPuUNt%t43rH>6*m(487Y;D#}gaa^XZyTp^ z9y5}}3cF0d*W@?hc%EEt_4QEzhQmyN_+b>wdA*5aO8C8B(xnEGX^lj-7~(aCoVuoN zeuwNZ@wkv#r?lW(c|6*0sTo%8qUtJiqzV@ORDb_QNvk%n<5|^ZR{KFnwsZ$?qJ@o) z?E6tYy7p^RaG1pjLYEY*D7Swd;c9#hN^xDviU~1C*yEh3xGmx*Hiy+pI=-~tICOXC zui1R6QJjQN2bZGdHci`_+eZt#y8{H5LARCz^1no%ByyFoBZ+UzBT>AXl+I}yKW+&D zK>2UPwB+b9XnI#2KK!ytXMN-0(@^8$9c65>Lqtkoy{BF#PD>iw`<6X2FqUN>NDk;4 z?tfp(>JP(##%j7udi3SR*sdtmeEEQ&>l!uL*oFj_q13m8LMy0AQg)Q^-H97mK{eYpNquCw|pZn^iuZIF8(~GY4BQDYj zg?5JroP1yIZD%5G3rbGBYnhUsieXejqGs;wLJ|P7L!GhxXhO=|l>{p%T#%FowdMH) z)|g^xE&M-TK%sHI%WbhKRPMKL3w;AoZ!# zS3}qgA|zgP;3dt9f@NFt6zBVj`&$B7bgcB%uVg9<%nLH;K}?d1hAHq89>BkYi974MGD&~&~-X8PjsI;mt+ z7ZLP^P+IWAB^{|yM20Mq3^10IaYV+k;wx`P zW!bs5IJF3>!s<4GB|C*a&U}KnPrD_MU+D%BvJ4=#y;4Bclrnp4jZbQZt>~gY(N)=% z1Xd?!AuXPduO={rKzHr!Aw}%^<$r=3hb5pee?KDj1+F+Uzf61N*>mIWRq4G?DA`Uz z(`D(RV3siIx>1NBug60=*5I^hJ*@(_XMVNiK2T)FtW!9c=M%Mm$7a}j%e>Q!Hc4QRwID2YZy;<%*d?Fh}n6={1 zM{h%shs2FKHfD9>wdNVGE>(K!j}RP!qLbgyy4jvuJ1Jf+G1Gc@dp7r(9OYD_kRl2S zyJ=(!yJs$^Oh$=r5u)8Hp!IN$*k{nD!CRDR4VwQB8euQ#YH#eDf!_}-BKyeg^hbL^o9Eun`yK~|Hw8WPFdxqQ%UOdTXZ0Yn+F@N0KXA%aDmhN54}S-C-upjp>n0?uenPes zf+m&bFcM^?wdEQ$a)lwCzP(?Rm2)J7RZxDWlOHcm<$`kpUk$8*;kIVm-|7*YB&M#K z^vEnMv~p$LQ~|TYSIbqMS92Du_$0QBYQ?gD^`&v3mpuxzWht%J6>MZtYZ!5xE~|kP z9oxrxPq~M`<=_2Ta$eKnLPg@o9cMl*x^*wV$qw;bJ~#8&11+jDo}DmpOJsNVE9LqH z(Pa|3^gy)+gcbsZRei>P1~cqLMf{_i5RYgt_|Oy=BqseNK{q%624jGqyfj66kbU)vq#gIZ@w`7PaR|KC`0C zlSn_|YSgGrD*C0I=peSvemLo6%AU6_4o(O3qMl_mGE+`|t^c?pMfNqm(e1A8CNW{! zPx*oPkHd@V@{{U4f8BliU%0ADfdo$$+C#8V;9G_$3Y@g_BZtJ5dZ;_To_)h)c-~qc z-gwF>(aT7XFdwj-GaZRu{0Ux5ay05@}*Q>wbglj{7r!vE|XckDSM*_lFk7mXP28_dtZv# z|Fjlm*X&D`(M~n%8*^7p??vz5k~ceYpgC!38#Mc8dVFb7@5&*YjlToKAB!A4xT3!? zlOXzSh{5su{lpcF!FkPrH~fhEq_iU%Wr5cpkCLC)v(s0zDMdLkGGM-$M$<>~PWebZ zsrwHkMbHfz5EYu>fgDsiY%T1KbEzw*u_7qJ?8{zSzJ zi@{vi!2J#TJ#rcS+c8L0^z%-(Cu#Pu*clLck!wI+g=c68SDk@3#{BEgL0W`TnF&AD z#3Il!79BWnYo@*znv}l;Q!o)1#C&it?2t1waM>Y{mOyxls(p?!+1lNXtf(U-6fV zoDLcz!5hiRG(pGF{Y`7hsAj}Gn(>sqD@}XS8?ozh2JXG==mzJhg3+%hdZXOdHjk&a z`<-yw-12d{sh$p5VuIq05GP`5J)e+^Xsagw-l&-e;XygcZ*GTWvqY}>CG1*1l`}E< zHv@o(8HA7QB4e_OLpL8y>;7I8N!7gm$1j7!Gt0wiztb?BXZAz5P=1>_E%)4$F?6+h%ZJpc|tJT&;_NFyA6WYB;o zBCgRj>+28PU@6RVL)h@^z*tqKWsM>RlvU`9E~>pUpS6?#Zlt&c zWX;~OShzBBJc}y=4_j5WixKZg_MYv4Z_oC!FHLVdUGa-lEVC?~HXCD|fj6KiC7@YTv0abHK@iavBUubr?DUNy^ zBS8Z1LTEw~F&EQRHz(K)$k*kjNI2m>nzdjp;qi(zWFp_)8f=TQTPcz<{O8TxyaqZGzCDwD=C;- ziH+%wC|HvhdRQAbP-i;|I)W8+C4>eNFBRe02{J=2%aB|ndj|MJ@QKrvz(sD%YW#$C ziszYn$A|8wY-&aSpYsT| z;?U38$vpZU>e|Uk2Nmm%fLfvcmMn1us2=q_Fu zKXje>icbgb7%kFzx=UBQqTWGhj-h@uO?P64SeMok^N{qB{Y!kLKjdvX#YN6p$~)(H z?v$LnNAay7gnw2F46S6=zAtiM`)RZo1PenJ>I}xmX^aipc^YNPt>+Y{EB_O1);u?V zHP76J#$eCn5PMi&&RT{QB{m^N=co>`B39Ja7Gv9%zx8NI>+7eZhvF$2it{{Ixn8x? zqT6g|_Sb(QLFYcWz@I=fzCy2vq=mPezy27S=+9d-UYOlf6t`TTqlp!}BkGa@Z^=Xaw(x&~M|A3xG zK{;KI*}fABd_Sr-d1Cc6@uqVk(_Los)v~zDSfaPCwC#KaVK)xTf2p3ONXoW@)Q`vG z{sn>P4AZY1#{?R|qR4CR;WuolS6NgY2wRv!Bp)rb=_vfkpA28UHd3JuP*Z7&kXJ7dN*(c+sp(YVF$ioaMaL zYR5NCRC*d}Ts_^~E0BW=*cs(xGR)W%a5gUN_)fmDWjYD(_ez!NbuT#C4P9}g%sT?so`|CH zt*PhenRV8H!$lx-Zpwb6ITy}Zf&pltPf#DLC62Sp;OZwcy49S2Mfz_Cx_^0EFTh>;kElu@IJL@>&0LVOi#0POhGgD>vSuA@(RQ8`{zy z*trC;(kuLCp~NbG2{QZ|mru zbuW#a`L~fZG}d6eTcGgsNQFqB_aM&KO1;-UL*hF;HPNr(rWJ$vhE$gMO1*aXF%ZB+ z$;<1jt5fQr$PghRJdSV{f)EB9-hl%)RLb@6SeiS&cS~Nz+hfv$my&eiQMj2}Xhwep zh^;Umu@w@4^d=cZ$sp5=Ikhz2fQgQNWl)+pF(FotZP+=20w9An?Ry_=A6`0;e4>PGS1rB(+uZeW4?ylE_PIc@NqvA2t6 zSPhhz_!$$EGpo6^anK(9(hYG2Tk6Z1{rHFO6cO6xC`pxbL@LVOkMlDVG1-Uc?It}U zEZr_M%(`9j{D%^g7RV^Hu8+db%??=_>q!_wdfS9(f#qKfj^6^6fmU%-{QZk=Gqa_} z8fH!0l&(WK;;v=%F!RqPrLyNwZl%If)f^tVBGV>_hK7VN*ITSf5rge6OV$gc_DQzg z*Dm(va5VYBqQNQt=PQ`U%%mjF`V$i0!Gp6UIHJcqXn;ALopLoOxjUce1J3m!RlP~a z8g_~AzbGqsx^2JGp!k-QEi=l|-$rV~#1pT3@Q{y%)6UVLp>Z|>BBdCtf1^ybT*kbf zlXpqFGI_O{l4__7vs;Ou^I|1DA*qQHuBMI;so&doZ&FsR^Wq?k5l4VuUS1iNuSjFD zoNT%LL7S$^RrPmz>6`skfn)-u?Q#R!QFCEoVSavROA91k&@0|AX9LS{5ddshw_QX* zz?Zi}`*|cl{6FZm<$u4K6!iT3X_{4zCWw;D;6dwa>CCs@8H!RT5|6UgL8Xrv z+>Y&n8}^S|!<47fS^@a%IFK{!I?xF+%yNnR@)EAP3P%QjCUNP%ZT-z5ki{sw1!?JS z@pMViSj+z z10Zlfnf)51u?>df7Ych+5oO47wm{=7b%f4|e0fQ#@XwL^ zm64QWHKA*cOgqt5Fggk&J}~W9vEJ)>C!WVN}JteI{t0es1nZ`+2Rjpns={ zb^Xpz&ZOyhiY{+0uBPkXAtW6g9l#>Cdpk`CND$x8BRF5OZp}+kNN!qs69m(R@5?MZe_uw-c?b#qAu!>*|`JE^TQkZmOlIG0o6EQCWh&&|z^Kkwn|p_Oq~% zJg2ZwRLRcVc4+Yaj*E+Hd1WQn2fLb4Dd;AcUX~^sm6C<$(PL4WEc477l3JLfJ3r?# zP@`g24j>jvWPiw`1pc&2=&;+#B*D9P6ouAY`gC4=W;58E%EDio)$NG1F3@P)uEQne zaMOD+U<~a&Hd)KJd@≺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 literal 50541 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�>t#2h5hN1_xz7G?fls* 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 diff --git a/package-lock.json b/package-lock.json index 58a56364..374a66d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@next2d/player", - "version": "3.0.5", + "version": "3.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@next2d/player", - "version": "3.0.5", + "version": "3.1.0", "license": "MIT", "workspaces": [ "packages/*" @@ -16,27 +16,27 @@ "htmlparser2": "^10.1.0" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.4", + "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", "@playwright/test": "^1.58.2", - "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-commonjs": "^29.0.2", "@rollup/plugin-node-resolve": "^16.0.3", - "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", - "@types/node": "^25.3.3", - "@typescript-eslint/eslint-plugin": "^8.56.1", - "@typescript-eslint/parser": "^8.56.1", - "@vitest/web-worker": "^4.0.18", + "@types/node": "^25.5.0", + "@typescript-eslint/eslint-plugin": "^8.57.0", + "@typescript-eslint/parser": "^8.57.0", + "@vitest/web-worker": "^4.1.0", "@webgpu/types": "^0.1.69", - "eslint": "^10.0.2", + "eslint": "^10.0.3", "eslint-plugin-unused-imports": "^4.4.1", - "globals": "^17.3.0", + "globals": "^17.4.0", "jsdom": "^28.1.0", "rollup": "^4.59.0", "tslib": "^2.8.1", "typescript": "^5.9.3", - "vite": "^7.3.1", - "vitest": "^4.0.18", + "vite": "^8.0.0", + "vitest": "^4.1.0", "vitest-webgl-canvas-mock": "^1.1.0", "xml2js": "^0.6.2" }, @@ -216,9 +216,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.28.tgz", - "integrity": "sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.0.tgz", + "integrity": "sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA==", "dev": true, "funding": [ { @@ -239,459 +239,51 @@ "dev": true, "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], - "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { @@ -724,15 +316,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", - "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.2", + "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", - "minimatch": "^10.2.1" + "minimatch": "^10.2.4" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -778,22 +370,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", - "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.0" + "@eslint/core": "^1.1.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", - "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -804,9 +396,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", - "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { @@ -817,7 +409,7 @@ "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.3", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -862,9 +454,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", - "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -872,13 +464,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", - "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.0", + "@eslint/core": "^1.1.1", "levn": "^0.4.1" }, "engines": { @@ -886,9 +478,9 @@ } }, "node_modules/@exodus/bytes": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", - "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", "dev": true, "license": "MIT", "engines": { @@ -1005,6 +597,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@next2d/cache": { "resolved": "packages/cache", "link": true @@ -1065,6 +674,26 @@ "resolved": "packages/webgpu", "link": true }, + "node_modules/@oxc-project/runtime": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", + "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@playwright/test": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", @@ -1078,13 +707,275 @@ "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=18" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", + "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", + "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", + "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-commonjs": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.0.tgz", - "integrity": "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==", + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.2.tgz", + "integrity": "sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==", "dev": true, "license": "MIT", "dependencies": { @@ -1134,18 +1025,18 @@ } }, "node_modules/@rollup/plugin-terser": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", + "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", "dev": true, "license": "MIT", "dependencies": { - "serialize-javascript": "^6.0.1", + "serialize-javascript": "^7.0.3", "smob": "^1.0.0", "terser": "^5.17.4" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { "rollup": "^2.0.0||^3.0.0||^4.0.0" @@ -1563,6 +1454,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1603,9 +1505,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.3.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", - "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", "dependencies": { @@ -1620,17 +1522,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", - "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/type-utils": "8.56.1", - "@typescript-eslint/utils": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -1643,7 +1545,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.1", + "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1659,16 +1561,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", - "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "engines": { @@ -1684,14 +1586,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", - "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.1", - "@typescript-eslint/types": "^8.56.1", + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "engines": { @@ -1706,14 +1608,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", - "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1" + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1724,9 +1626,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", - "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", "dev": true, "license": "MIT", "engines": { @@ -1741,15 +1643,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", - "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -1766,9 +1668,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", - "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", "dev": true, "license": "MIT", "engines": { @@ -1780,16 +1682,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", - "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.1", - "@typescript-eslint/tsconfig-utils": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1847,16 +1749,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", - "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1" + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1871,13 +1773,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", - "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1902,17 +1804,17 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", "tinyrainbow": "^3.0.3" }, "funding": { @@ -1920,13 +1822,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.18", + "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1935,7 +1837,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -1957,9 +1859,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", "dev": true, "license": "MIT", "dependencies": { @@ -1970,13 +1872,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.18", + "@vitest/utils": "4.1.0", "pathe": "^2.0.3" }, "funding": { @@ -1984,13 +1886,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1999,9 +1902,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", "dev": true, "license": "MIT", "funding": { @@ -2009,13 +1912,14 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" }, "funding": { @@ -2023,9 +1927,9 @@ } }, "node_modules/@vitest/web-worker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/web-worker/-/web-worker-4.0.18.tgz", - "integrity": "sha512-h9MiAI3nQNVeEH8Tn1p9CwJGmXPJPUTGhzcuQalIk+6fqIazqUDVzDi+NUKrpK6sQKgSa4MonhyDThhtZqH+cA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/web-worker/-/web-worker-4.1.0.tgz", + "integrity": "sha512-K0K2DXrgmvHdwAXHQx1j4qOqgid2Fhr2F2yiFVGbAf10vsoUQYQA1Twe/L8wRAv+DzAWe0OO4ikjz5ulMc1/CA==", "dev": true, "license": "MIT", "dependencies": { @@ -2035,7 +1939,7 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.0.18" + "vitest": "4.1.0" } }, "node_modules/@webgpu/types": { @@ -2194,6 +2098,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2210,14 +2121,14 @@ } }, "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" @@ -2231,13 +2142,13 @@ "license": "MIT" }, "node_modules/cssstyle": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.1.0.tgz", - "integrity": "sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^5.0.0", + "@asamuzakjp/css-color": "^5.0.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.28", "css-tree": "^3.1.0", "lru-cache": "^11.2.6" @@ -2302,6 +2213,16 @@ "node": ">=0.10.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -2382,54 +2303,12 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2444,18 +2323,18 @@ } }, "node_modules/eslint": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz", - "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", + "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.2", + "@eslint/config-array": "^0.23.3", "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.0", - "@eslint/plugin-kit": "^0.6.0", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -2464,7 +2343,7 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.1", + "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.1.1", "esquery": "^1.7.0", @@ -2477,7 +2356,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.1", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -2516,9 +2395,9 @@ } }, "node_modules/eslint-scope": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", - "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2584,9 +2463,9 @@ } }, "node_modules/eslint/node_modules/espree": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", - "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2801,9 +2680,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, @@ -2846,9 +2725,9 @@ } }, "node_modules/globals": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", - "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", "dev": true, "license": "MIT", "engines": { @@ -3137,6 +3016,267 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3174,9 +3314,9 @@ } }, "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true, "license": "CC0-1.0" }, @@ -3423,9 +3563,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -3471,16 +3611,6 @@ "node": ">=6" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3522,6 +3652,40 @@ "node": ">=4" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", + "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.115.0", + "@rolldown/pluginutils": "1.0.0-rc.9" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + } + }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", @@ -3567,31 +3731,10 @@ "fsevents": "~2.3.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3625,13 +3768,13 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", + "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/shebang-command": { @@ -3713,9 +3856,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", "dev": true, "license": "MIT" }, @@ -3806,9 +3949,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -3816,29 +3959,29 @@ } }, "node_modules/tldts": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", - "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "version": "7.0.25", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz", + "integrity": "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.23" + "tldts-core": "^7.0.25" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", - "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "version": "7.0.25", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.25.tgz", + "integrity": "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==", "dev": true, "license": "MIT" }, "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3909,9 +4052,9 @@ } }, "node_modules/undici": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", - "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.0.tgz", + "integrity": "sha512-jxytwMHhsbdpBXxLAcuu0fzlQeXCNnWdDyRHpvWsUl8vd98UwYdl9YTyn8/HcpcJPC3pwUveefsa3zTxyD/ERg==", "dev": true, "license": "MIT", "engines": { @@ -3936,17 +4079,17 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", + "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", + "@oxc-project/runtime": "0.115.0", + "lightningcss": "^1.32.0", "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.9", "tinyglobby": "^0.2.15" }, "bin": { @@ -3963,9 +4106,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.0.0-alpha.31", + "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -3978,13 +4122,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -4026,31 +4173,31 @@ } }, "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -4066,12 +4213,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -4100,6 +4248,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, diff --git a/package.json b/package.json index 25e85e3f..f43e191f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@next2d/player", - "version": "3.0.5", + "version": "3.1.0", "description": "Experience the fast and beautiful anti-aliased rendering of WebGL. You can create rich, interactive graphics, cross-platform applications and games without worrying about browser or device compatibility.", "author": "Toshiyuki Ienaga (https://github.com/ienaga/)", "license": "MIT", @@ -52,27 +52,27 @@ "htmlparser2": "^10.1.0" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.4", + "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", "@playwright/test": "^1.58.2", - "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-commonjs": "^29.0.2", "@rollup/plugin-node-resolve": "^16.0.3", - "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", - "@types/node": "^25.3.3", - "@typescript-eslint/eslint-plugin": "^8.56.1", - "@typescript-eslint/parser": "^8.56.1", - "@vitest/web-worker": "^4.0.18", + "@types/node": "^25.5.0", + "@typescript-eslint/eslint-plugin": "^8.57.0", + "@typescript-eslint/parser": "^8.57.0", + "@vitest/web-worker": "^4.1.0", "@webgpu/types": "^0.1.69", - "eslint": "^10.0.2", + "eslint": "^10.0.3", "eslint-plugin-unused-imports": "^4.4.1", - "globals": "^17.3.0", + "globals": "^17.4.0", "jsdom": "^28.1.0", "rollup": "^4.59.0", "tslib": "^2.8.1", "typescript": "^5.9.3", - "vite": "^7.3.1", - "vitest": "^4.0.18", + "vite": "^8.0.0", + "vitest": "^4.1.0", "vitest-webgl-canvas-mock": "^1.1.0", "xml2js": "^0.6.2" }, diff --git a/packages/display/src/DisplayObject.ts b/packages/display/src/DisplayObject.ts index 12a9aea2..ad2eb86f 100644 --- a/packages/display/src/DisplayObject.ts +++ b/packages/display/src/DisplayObject.ts @@ -428,6 +428,16 @@ export class DisplayObject extends EventDispatcher */ public parent: ISprite | null; + /** + * @description キャッシュする際の指定Matrix、nullの場合は通常のMatrixで描画 + * Specified Matrix for caching, if null, draw with the normal Matrix + * + * @member {Matrix | null} + * @default null + * @public + */ + public cacheTransform: Matrix | null = null; + /** * @constructor * @public diff --git a/packages/webgpu/src/Compute/ComputePipelineManager.test.ts b/packages/webgpu/src/Compute/ComputePipelineManager.test.ts deleted file mode 100644 index 62ba9cda..00000000 --- a/packages/webgpu/src/Compute/ComputePipelineManager.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -// Mock GPUShaderStage -const GPUShaderStage = { - COMPUTE: 0x04 -}; -(globalThis as any).GPUShaderStage = GPUShaderStage; - -describe("ComputePipelineManager", () => -{ - // Create a mock implementation for testing without the actual class - class MockComputePipelineManager - { - private pipelines: Map; - private bindGroupLayouts: Map; - - constructor (_device: GPUDevice) - { - this.pipelines = new Map(); - this.bindGroupLayouts = new Map(); - - // Initialize mock pipelines - this.pipelines.set("blur_compute_horizontal", { "label": "blur_compute_horizontal" }); - this.pipelines.set("blur_compute_vertical", { "label": "blur_compute_vertical" }); - - // Initialize mock bind group layouts - this.bindGroupLayouts.set("blur_compute", { "label": "blur_compute_bind_group_layout" }); - } - - getPipeline (name: string): any - { - return this.pipelines.get(name); - } - - getBindGroupLayout (name: string): any - { - return this.bindGroupLayouts.get(name); - } - - destroy (): void - { - this.pipelines.clear(); - this.bindGroupLayouts.clear(); - } - } - - const createMockDevice = (): GPUDevice => - { - return { - "createShaderModule": vi.fn(() => ({ "label": "mockShaderModule" })), - "createBindGroupLayout": vi.fn(() => ({ "label": "mockBindGroupLayout" })), - "createPipelineLayout": vi.fn(() => ({ "label": "mockPipelineLayout" })), - "createComputePipeline": vi.fn(() => ({ "label": "mockComputePipeline" })) - } as unknown as GPUDevice; - }; - - beforeEach(() => - { - vi.clearAllMocks(); - }); - - describe("constructor", () => - { - it("should create instance with device", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - expect(manager).toBeDefined(); - }); - - it("should initialize blur pipelines", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - expect(manager.getPipeline("blur_compute_horizontal")).toBeDefined(); - expect(manager.getPipeline("blur_compute_vertical")).toBeDefined(); - }); - }); - - describe("getPipeline", () => - { - it("should return horizontal blur pipeline", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - const pipeline = manager.getPipeline("blur_compute_horizontal"); - - expect(pipeline).toBeDefined(); - expect(pipeline.label).toBe("blur_compute_horizontal"); - }); - - it("should return vertical blur pipeline", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - const pipeline = manager.getPipeline("blur_compute_vertical"); - - expect(pipeline).toBeDefined(); - expect(pipeline.label).toBe("blur_compute_vertical"); - }); - - it("should return undefined for non-existent pipeline", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - expect(manager.getPipeline("nonexistent")).toBeUndefined(); - }); - }); - - describe("getBindGroupLayout", () => - { - it("should return blur compute bind group layout", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - const layout = manager.getBindGroupLayout("blur_compute"); - - expect(layout).toBeDefined(); - }); - - it("should return undefined for non-existent layout", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - expect(manager.getBindGroupLayout("nonexistent")).toBeUndefined(); - }); - }); - - describe("destroy", () => - { - it("should clear pipelines", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - manager.destroy(); - - expect(manager.getPipeline("blur_compute_horizontal")).toBeUndefined(); - expect(manager.getPipeline("blur_compute_vertical")).toBeUndefined(); - }); - - it("should clear bind group layouts", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - manager.destroy(); - - expect(manager.getBindGroupLayout("blur_compute")).toBeUndefined(); - }); - }); -}); diff --git a/packages/webgpu/src/Compute/ComputePipelineManager.ts b/packages/webgpu/src/Compute/ComputePipelineManager.ts deleted file mode 100644 index 46a873d9..00000000 --- a/packages/webgpu/src/Compute/ComputePipelineManager.ts +++ /dev/null @@ -1,322 +0,0 @@ -/** - * @description Compute Pipeline Manager - * Compute Shaderパイプラインの管理 - * - * Compute Shaderは並列処理に最適で、フィルター処理を高速化。 - * 特に大きなブラー半径(64+)の場合、20-35%の高速化が期待できる。 - * - * @class - */ -export class ComputePipelineManager -{ - private device: GPUDevice; - private pipelines: Map; - private bindGroupLayouts: Map; - - /** - * @constructor - * @param {GPUDevice} device - WebGPU device - */ - constructor (device: GPUDevice) - { - this.device = device; - this.pipelines = new Map(); - this.bindGroupLayouts = new Map(); - - this.initializeBlurPipelines(); - } - - /** - * @description ブラー用Compute Pipelineを初期化 - * @private - */ - private initializeBlurPipelines (): void - { - // ブラーCompute Shader用のBindGroupLayoutを作成 - const blurBindGroupLayout = this.device.createBindGroupLayout({ - "label": "blur_compute_bind_group_layout", - "entries": [ - { - // 入力テクスチャ - "binding": 0, - "visibility": GPUShaderStage.COMPUTE, - "texture": { - "sampleType": "float" - } - }, - { - // 出力テクスチャ(Storage Texture) - "binding": 1, - "visibility": GPUShaderStage.COMPUTE, - "storageTexture": { - "access": "write-only", - "format": "rgba8unorm" - } - }, - { - // パラメータ(方向、ブラー半径など) - "binding": 2, - "visibility": GPUShaderStage.COMPUTE, - "buffer": { - "type": "uniform" - } - } - ] - }); - - this.bindGroupLayouts.set("blur_compute", blurBindGroupLayout); - - // 水平/垂直ブラーパイプラインを作成 - // 同じシェーダーを使用し、方向はuniformで制御 - this.createBlurComputePipeline("blur_compute_horizontal"); - this.createBlurComputePipeline("blur_compute_vertical"); - - // 共有メモリ版(大半径用) - this.createBlurComputePipeline("blur_compute_shared_horizontal", true); - this.createBlurComputePipeline("blur_compute_shared_vertical", true); - } - - /** - * @description ブラーCompute Pipelineを作成 - * @param {string} name - パイプライン名 - * @private - */ - private createBlurComputePipeline (name: string, useSharedMemory: boolean = false): void - { - const shaderModule = this.device.createShaderModule({ - "label": `${name}_shader`, - "code": useSharedMemory ? this.getSharedBlurComputeShaderCode() : this.getBlurComputeShaderCode() - }); - - const pipelineLayout = this.device.createPipelineLayout({ - "label": `${name}_layout`, - "bindGroupLayouts": [this.bindGroupLayouts.get("blur_compute")!] - }); - - const pipeline = this.device.createComputePipeline({ - "label": name, - "layout": pipelineLayout, - "compute": { - "module": shaderModule, - "entryPoint": "main" - } - }); - - this.pipelines.set(name, pipeline); - } - - /** - * @description ブラーCompute Shaderコードを生成 - * ボックスブラー(均一加重平均)を使用。Fragment Shaderと同一出力。 - * @return {string} WGSLシェーダーコード - * @private - */ - private getBlurComputeShaderCode (): string - { - return ` -struct BlurParams { - direction: vec2, // (1,0) or (0,1) - radius: f32, // ブラー半径 - fraction: f32, // 端ピクセルのブレンド割合 - texSize: vec2, // テクスチャサイズ - samples: f32, // サンプル数 - padding: f32, // パディング(16バイトアライメント) -} - -@group(0) @binding(0) var inputTexture: texture_2d; -@group(0) @binding(1) var outputTexture: texture_storage_2d; -@group(0) @binding(2) var params: BlurParams; - -const WORKGROUP_SIZE: u32 = 16u; - -@compute @workgroup_size(16, 16, 1) -fn main( - @builtin(global_invocation_id) globalId: vec3 -) { - let texSize = vec2(u32(params.texSize.x), u32(params.texSize.y)); - let radius = i32(params.radius); - - let outCoord = globalId.xy; - - if (outCoord.x >= texSize.x || outCoord.y >= texSize.y) { - return; - } - - let direction = vec2(i32(params.direction.x), i32(params.direction.y)); - let samples = params.samples; - let fraction = params.fraction; - - var color = vec4(0.0); - - for (var i = -radius; i <= radius; i = i + 1) { - var sampleCoord = vec2(outCoord) + direction * i; - - sampleCoord.x = clamp(sampleCoord.x, 0, i32(texSize.x) - 1); - sampleCoord.y = clamp(sampleCoord.y, 0, i32(texSize.y) - 1); - - let sample = textureLoad(inputTexture, vec2(sampleCoord), 0); - - // 端ピクセルにfraction重みを適用(Fragment Shaderと同じロジック) - if (i == -radius || i == radius) { - color = color + sample * fraction; - } else { - color = color + sample; - } - } - - color = color / samples; - - textureStore(outputTexture, outCoord, color); -} -`; - } - - /** - * @description 共有メモリ版ブラーCompute Shaderコードを生成 - * ワークグループ共有メモリでテクスチャ読み込みの重複を排除。 - * radius >= 8 で通常版より高速。 - * @return {string} WGSLシェーダーコード - * @private - */ - private getSharedBlurComputeShaderCode (): string - { - return ` -struct BlurParams { - direction: vec2, - radius: f32, - fraction: f32, - texSize: vec2, - samples: f32, - padding: f32, -} - -@group(0) @binding(0) var inputTexture: texture_2d; -@group(0) @binding(1) var outputTexture: texture_storage_2d; -@group(0) @binding(2) var params: BlurParams; - -const TILE: u32 = 16u; -const MAX_APRON: u32 = 24u; -const SHARED_W: u32 = TILE + 2u * MAX_APRON; - -var tile: array, ${(16 + 2 * 24) * 16}>; - -@compute @workgroup_size(16, 16, 1) -fn main( - @builtin(global_invocation_id) globalId: vec3, - @builtin(local_invocation_id) localId: vec3, - @builtin(workgroup_id) workgroupId: vec3 -) { - let texSize = vec2(u32(params.texSize.x), u32(params.texSize.y)); - let radius = u32(params.radius); - let apron = min(radius, MAX_APRON); - let isHorizontal = params.direction.x > 0.5; - let fraction = params.fraction; - let samples = params.samples; - - let threadIdx = localId.x + localId.y * TILE; - let totalThreads = TILE * TILE; - - if (isHorizontal) { - let sharedWidth = TILE + 2u * apron; - let baseX = workgroupId.x * TILE; - let y = globalId.y; - let clampedY = clamp(y, 0u, max(texSize.y, 1u) - 1u); - - // 全スレッドが協調ロード(範囲外スレッドもclampされた座標でロード) - var idx = threadIdx; - loop { - if (idx >= sharedWidth) { break; } - let gx = i32(baseX) + i32(idx) - i32(apron); - let cx = u32(clamp(gx, 0, i32(max(texSize.x, 1u)) - 1)); - tile[localId.y * SHARED_W + idx] = textureLoad(inputTexture, vec2(cx, clampedY), 0); - idx += totalThreads; - } - - // 全スレッドがバリアに到達(早期returnなし) - workgroupBarrier(); - - // 範囲内のスレッドのみ出力 - let outX = globalId.x; - if (outX < texSize.x && y < texSize.y) { - let iRadius = i32(radius); - var color = vec4(0.0); - for (var i = -iRadius; i <= iRadius; i = i + 1) { - let tileIdx = i32(localId.x) + i32(apron) + i; - let s = tile[localId.y * SHARED_W + u32(clamp(tileIdx, 0, i32(sharedWidth) - 1))]; - if (i == -iRadius || i == iRadius) { - color += s * fraction; - } else { - color += s; - } - } - textureStore(outputTexture, vec2(outX, y), color / samples); - } - } else { - let sharedHeight = TILE + 2u * apron; - let baseY = workgroupId.y * TILE; - let x = globalId.x; - let clampedX = clamp(x, 0u, max(texSize.x, 1u) - 1u); - - // 全スレッドが協調ロード(範囲外スレッドもclampされた座標でロード) - var idx = threadIdx; - loop { - if (idx >= sharedHeight) { break; } - let gy = i32(baseY) + i32(idx) - i32(apron); - let cy = u32(clamp(gy, 0, i32(max(texSize.y, 1u)) - 1)); - tile[idx * TILE + localId.x] = textureLoad(inputTexture, vec2(clampedX, cy), 0); - idx += totalThreads; - } - - // 全スレッドがバリアに到達(早期returnなし) - workgroupBarrier(); - - // 範囲内のスレッドのみ出力 - let outY = globalId.y; - if (x < texSize.x && outY < texSize.y) { - let iRadius = i32(radius); - var color = vec4(0.0); - for (var i = -iRadius; i <= iRadius; i = i + 1) { - let tileIdx = i32(localId.y) + i32(apron) + i; - let s = tile[u32(clamp(tileIdx, 0, i32(sharedHeight) - 1)) * TILE + localId.x]; - if (i == -iRadius || i == iRadius) { - color += s * fraction; - } else { - color += s; - } - } - textureStore(outputTexture, vec2(x, outY), color / samples); - } - } -} -`; - } - - /** - * @description パイプラインを取得 - * @param {string} name - パイプライン名 - * @return {GPUComputePipeline | undefined} - */ - getPipeline (name: string): GPUComputePipeline | undefined - { - return this.pipelines.get(name); - } - - /** - * @description BindGroupLayoutを取得 - * @param {string} name - レイアウト名 - * @return {GPUBindGroupLayout | undefined} - */ - getBindGroupLayout (name: string): GPUBindGroupLayout | undefined - { - return this.bindGroupLayouts.get(name); - } - - /** - * @description リソースを破棄 - */ - destroy (): void - { - this.pipelines.clear(); - this.bindGroupLayouts.clear(); - } -} diff --git a/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.test.ts b/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.test.ts deleted file mode 100644 index dd859f05..00000000 --- a/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.test.ts +++ /dev/null @@ -1,318 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { IAttachmentObject } from "../../interface/IAttachmentObject"; -import type { ComputePipelineManager } from "../ComputePipelineManager"; -import { execute } from "./ComputeExecuteBlurService"; - -// Mock GPUBufferUsage -const GPUBufferUsage = { - UNIFORM: 0x0040, - COPY_DST: 0x0008 -}; -(globalThis as any).GPUBufferUsage = GPUBufferUsage; - -describe("ComputeExecuteBlurService", () => -{ - const createMockDevice = () => - { - const mockBuffer = { "label": "mockBuffer" }; - return { - "createBuffer": vi.fn(() => mockBuffer), - "createBindGroup": vi.fn(() => ({ "label": "mockBindGroup" })), - "queue": { - "writeBuffer": vi.fn() - } - } as unknown as GPUDevice; - }; - - const createMockCommandEncoder = () => - { - const mockComputePass = { - "setPipeline": vi.fn(), - "setBindGroup": vi.fn(), - "dispatchWorkgroups": vi.fn(), - "end": vi.fn() - }; - return { - "beginComputePass": vi.fn(() => mockComputePass), - "_mockComputePass": mockComputePass - } as unknown as GPUCommandEncoder & { _mockComputePass: any }; - }; - - const createMockComputePipelineManager = (hasPipeline: boolean = true) => - { - return { - "getPipeline": vi.fn(() => hasPipeline ? { "label": "mockPipeline" } : null), - "getBindGroupLayout": vi.fn(() => hasPipeline ? { "label": "mockLayout" } : null) - } as unknown as ComputePipelineManager; - }; - - const createMockAttachment = (width: number, height: number): IAttachmentObject => - { - return { - "id": 1, - "width": width, - "height": height, - "clipLevel": 0, - "msaa": false, - "mask": false, - "color": null, - "texture": { - "id": 1, - "width": width, - "height": height, - "area": width * height, - "smooth": true, - "resource": {} as GPUTexture, - "view": { "label": "mockView" } as unknown as GPUTextureView - }, - "stencil": null, - "msaaTexture": null, - "msaaStencil": null - }; - }; - - beforeEach(() => - { - vi.spyOn(console, "error").mockImplementation(() => {}); - }); - - describe("pipeline selection", () => - { - it("should use horizontal pipeline when isHorizontal is true", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(pipelineManager.getPipeline).toHaveBeenCalledWith("blur_compute_horizontal"); - }); - - it("should use vertical pipeline when isHorizontal is false", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, false, 8); - - expect(pipelineManager.getPipeline).toHaveBeenCalledWith("blur_compute_vertical"); - }); - - it("should return early when pipeline not found", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(false); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(commandEncoder.beginComputePass).not.toHaveBeenCalled(); - }); - }); - - describe("parameter buffer", () => - { - it("should create uniform buffer with correct usage", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(device.createBuffer).toHaveBeenCalledWith( - expect.objectContaining({ - "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST - }) - ); - }); - - it("should write parameter data to buffer", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(device.queue.writeBuffer).toHaveBeenCalled(); - }); - - it("should set horizontal direction vector when isHorizontal is true", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - // Check the params passed to writeBuffer - const writeBufferCall = (device.queue.writeBuffer as ReturnType).mock.calls[0]; - const params = writeBufferCall[2] as Float32Array; - expect(params[0]).toBe(1.0); // direction.x - expect(params[1]).toBe(0.0); // direction.y - }); - - it("should set vertical direction vector when isHorizontal is false", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, false, 8); - - const writeBufferCall = (device.queue.writeBuffer as ReturnType).mock.calls[0]; - const params = writeBufferCall[2] as Float32Array; - expect(params[0]).toBe(0.0); // direction.x - expect(params[1]).toBe(1.0); // direction.y - }); - }); - - describe("bind group", () => - { - it("should create bind group with correct layout", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(pipelineManager.getBindGroupLayout).toHaveBeenCalledWith("blur_compute"); - }); - - it("should create bind group with source and dest textures", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(device.createBindGroup).toHaveBeenCalled(); - }); - }); - - describe("compute pass", () => - { - it("should begin compute pass with correct label for horizontal", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(commandEncoder.beginComputePass).toHaveBeenCalledWith({ - "label": "blur_compute_pass_h" - }); - }); - - it("should begin compute pass with correct label for vertical", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, false, 8); - - expect(commandEncoder.beginComputePass).toHaveBeenCalledWith({ - "label": "blur_compute_pass_v" - }); - }); - - it("should set pipeline", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(commandEncoder._mockComputePass.setPipeline).toHaveBeenCalled(); - }); - - it("should set bind group", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(commandEncoder._mockComputePass.setBindGroup).toHaveBeenCalledWith(0, expect.anything()); - }); - - it("should end compute pass", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(commandEncoder._mockComputePass.end).toHaveBeenCalled(); - }); - }); - - describe("workgroup dispatch", () => - { - it("should calculate correct workgroup count for 256x256", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - // 256 / 16 = 16 workgroups in each dimension - expect(commandEncoder._mockComputePass.dispatchWorkgroups).toHaveBeenCalledWith(16, 16, 1); - }); - - it("should round up workgroup count for non-aligned size", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(100, 100); - const dest = createMockAttachment(100, 100); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - // ceil(100 / 16) = 7 workgroups in each dimension - expect(commandEncoder._mockComputePass.dispatchWorkgroups).toHaveBeenCalledWith(7, 7, 1); - }); - }); -}); diff --git a/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.ts b/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.ts deleted file mode 100644 index 1026baa3..00000000 --- a/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { IAttachmentObject } from "../../interface/IAttachmentObject"; -import type { ComputePipelineManager } from "../ComputePipelineManager"; - -/** - * @description プリアロケートされたFloat32Array (サイズ8) - */ -const $params8 = new Float32Array(8); - -/** - * @description プリアロケートされたBindGroupEntry配列 (バインディング3つ) - */ -const $computeEntries3: GPUBindGroupEntry[] = [ - { "binding": 0, "resource": null as unknown as GPUTextureView }, - { "binding": 1, "resource": null as unknown as GPUTextureView }, - { "binding": 2, "resource": { "buffer": null as unknown as GPUBuffer } } -]; - -/** - * @description プリアロケートされたComputePassDescriptor - */ -const $labelH: GPUComputePassDescriptor = { "label": "blur_compute_pass_h" }; -const $labelV: GPUComputePassDescriptor = { "label": "blur_compute_pass_v" }; - -/** - * @description Compute Shaderでブラーを実行(ボックスブラー) - * Execute box blur using Compute Shader - * - * Fragment Shaderと同一のボックスブラーアルゴリズムを使用。 - * - * @param {GPUDevice} device - WebGPU device - * @param {GPUCommandEncoder} commandEncoder - コマンドエンコーダー - * @param {ComputePipelineManager} computePipelineManager - Compute Pipeline Manager - * @param {IAttachmentObject} source - 入力アタッチメント - * @param {IAttachmentObject} dest - 出力アタッチメント - * @param {boolean} isHorizontal - 水平ブラーかどうか - * @param {number} blur - ブラー量(bufferBlurX/Y相当) - * @param {object} [bufferManager] - バッファマネージャー(プール化用) - * @return {void} - */ -export const execute = ( - device: GPUDevice, - commandEncoder: GPUCommandEncoder, - computePipelineManager: ComputePipelineManager, - source: IAttachmentObject, - dest: IAttachmentObject, - isHorizontal: boolean, - blur: number, - bufferManager?: { acquireAndWriteUniformBuffer(data: Float32Array, byteLength?: number): GPUBuffer } -): void => { - - // radius 8~24 の場合は共有メモリ版を使用(MAX_APRON=24の制限) - const halfBlur = Math.ceil(blur * 0.5); - const useShared = halfBlur >= 8 && halfBlur <= 24; - const pipelineName = useShared - ? isHorizontal ? "blur_compute_shared_horizontal" : "blur_compute_shared_vertical" - : isHorizontal ? "blur_compute_horizontal" : "blur_compute_vertical"; - const pipeline = computePipelineManager.getPipeline(pipelineName); - const bindGroupLayout = computePipelineManager.getBindGroupLayout("blur_compute"); - - if (!pipeline || !bindGroupLayout) { - return; - } - - // ボックスブラーパラメータ(Fragment ShaderのcalculateDirectionalBlurParamsと同一ロジック) - const fraction = 1 - (halfBlur - blur * 0.5); - const samples = 1 + blur; - - $params8[0] = isHorizontal ? 1.0 : 0.0; // direction.x - $params8[1] = isHorizontal ? 0.0 : 1.0; // direction.y - $params8[2] = halfBlur; // radius (halfBlur) - $params8[3] = fraction; // fraction - $params8[4] = source.width; // texSize.x - $params8[5] = source.height; // texSize.y - $params8[6] = samples; // samples - $params8[7] = 0.0; // padding - - const paramsBuffer = bufferManager - ? bufferManager.acquireAndWriteUniformBuffer($params8) - : (() => { - const buf = device.createBuffer({ - "size": $params8.byteLength, - "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST - }); - device.queue.writeBuffer(buf, 0, $params8); - return buf; - })(); - - $computeEntries3[0].resource = source.texture!.view; - $computeEntries3[1].resource = dest.texture!.view; - ($computeEntries3[2].resource as GPUBufferBinding).buffer = paramsBuffer; - const bindGroup = device.createBindGroup({ - "layout": bindGroupLayout, - "entries": $computeEntries3 - }); - - const computePass = commandEncoder.beginComputePass(isHorizontal ? $labelH : $labelV); - - computePass.setPipeline(pipeline); - computePass.setBindGroup(0, bindGroup); - - const workgroupsX = Math.ceil(dest.width / 16); - const workgroupsY = Math.ceil(dest.height / 16); - - computePass.dispatchWorkgroups(workgroupsX, workgroupsY, 1); - computePass.end(); -}; diff --git a/packages/webgpu/src/Context.ts b/packages/webgpu/src/Context.ts index 3e80c193..3efdded1 100644 --- a/packages/webgpu/src/Context.ts +++ b/packages/webgpu/src/Context.ts @@ -11,7 +11,6 @@ import { TextureManager } from "./TextureManager"; import { FrameBufferManager } from "./FrameBufferManager"; import { AttachmentManager } from "./AttachmentManager"; import { PipelineManager } from "./Shader/PipelineManager"; -import { ComputePipelineManager } from "./Compute/ComputePipelineManager"; import { $rootNodes, $resetAtlas, @@ -238,7 +237,6 @@ export class Context private textureManager: TextureManager; private frameBufferManager: FrameBufferManager; private pipelineManager: PipelineManager; - private computePipelineManager: ComputePipelineManager; private attachmentManager: AttachmentManager; public newDrawState: boolean = false; @@ -286,7 +284,6 @@ export class Context pipelineManager: PipelineManager; textureManager: TextureManager; mainAttachment?: IAttachmentObject; - computePipelineManager: ComputePipelineManager; frameTextures: GPUTexture[]; }; @@ -356,7 +353,6 @@ export class Context this.pipelineManager = new PipelineManager(device, preferred_format); // 遅延パイプライン群を即座に先行作成(初回アクセス時のレイテンシ解消) this.pipelineManager.preloadLazyGroups(); - this.computePipelineManager = new ComputePipelineManager(device); this.attachmentManager = new AttachmentManager(device); // グラデーションLUT共有アタッチメントにGPUDeviceを設定 @@ -383,7 +379,6 @@ export class Context "frameBufferManager": this.frameBufferManager, "pipelineManager": this.pipelineManager, "textureManager": this.textureManager, - "computePipelineManager": this.computePipelineManager, "frameTextures": this.frameTextures }; diff --git a/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts b/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts index 37b789c3..007c6c0a 100644 --- a/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts @@ -17,6 +17,10 @@ import { execute as filterApplyGradientBevelFilterUseCase } from "../../Filter/G import { execute as filterApplyGradientGlowFilterUseCase } from "../../Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase"; import { execute as filterApplyDisplacementMapFilterUseCase } from "../../Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase"; import { execute as blendApplyComplexBlendUseCase } from "../../Blend/usecase/BlendApplyComplexBlendUseCase"; +import { + $isMaskTestEnabled, + $getMaskStencilReference +} from "../../Mask"; const $uniform4 = new Float32Array(4); const $uniform6a = new Float32Array(6); @@ -304,28 +308,38 @@ const drawFilterToMain = ( // MSAA有効時はMSAA版パイプラインを使用してmsaaTextureに描画→texture.viewにresolve const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; + // マスクが有効かチェック + const isMasked = $isMaskTestEnabled(); + const useStencil = isMasked + && (mainAttachment.msaaStencil?.view || mainAttachment.stencil?.view); + // ブレンドモードに応じたパイプラインを選択 let pipelineName: string; - switch (blend_mode) { - case "add": - pipelineName = useMsaa ? "filter_output_add_msaa" : "filter_output_add"; - break; - case "screen": - pipelineName = useMsaa ? "filter_output_screen_msaa" : "filter_output_screen"; - break; - case "alpha": - pipelineName = useMsaa ? "filter_output_alpha_msaa" : "filter_output_alpha"; - break; - case "erase": - pipelineName = useMsaa ? "filter_output_erase_msaa" : "filter_output_erase"; - break; - case "copy": - pipelineName = useMsaa ? "texture_copy_bgra_msaa" : "texture_copy_bgra"; - break; - default: - // normal, layer - pipelineName = useMsaa ? "filter_output_msaa" : "filter_output"; - break; + if (useStencil) { + // マスク有効時はステンシルテスト付きパイプラインを使用 + pipelineName = useMsaa ? "filter_output_masked_msaa" : "filter_output_masked"; + } else { + switch (blend_mode) { + case "add": + pipelineName = useMsaa ? "filter_output_add_msaa" : "filter_output_add"; + break; + case "screen": + pipelineName = useMsaa ? "filter_output_screen_msaa" : "filter_output_screen"; + break; + case "alpha": + pipelineName = useMsaa ? "filter_output_alpha_msaa" : "filter_output_alpha"; + break; + case "erase": + pipelineName = useMsaa ? "filter_output_erase_msaa" : "filter_output_erase"; + break; + case "copy": + pipelineName = useMsaa ? "texture_copy_bgra_msaa" : "texture_copy_bgra"; + break; + default: + // normal, layer + pipelineName = useMsaa ? "filter_output_msaa" : "filter_output"; + break; + } } let pipeline = config.pipelineManager.getPipeline(pipelineName); @@ -365,12 +379,27 @@ const drawFilterToMain = ( // MSAA有効時はmsaaTextureに描画してtexture.viewにresolve const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture.view; const resolveTarget = useMsaa ? mainAttachment.texture.view : null; - const renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor( - colorView, - 0, 0, 0, 0, - "load", - resolveTarget - ); + + let renderPassDescriptor: GPURenderPassDescriptor; + if (useStencil) { + // マスク有効時はステンシル付きレンダーパスを使用 + const stencilView = useMsaa && mainAttachment.msaaStencil?.view + ? mainAttachment.msaaStencil.view : mainAttachment.stencil!.view; + renderPassDescriptor = config.frameBufferManager.createStencilRenderPassDescriptor( + colorView, + stencilView, + "load", + "load", + resolveTarget + ); + } else { + renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor( + colorView, + 0, 0, 0, 0, + "load", + resolveTarget + ); + } // Viewportはfloat値でサブピクセル精度を維持(WebGLのsetTransform相当) // ScissorはGPUIntegerCoordinate必須のため整数化し、viewport領域を包含する @@ -391,6 +420,10 @@ const drawFilterToMain = ( const passEncoder = config.commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); + // マスク有効時はステンシル参照値を設定 + if (useStencil) { + passEncoder.setStencilReference($getMaskStencilReference()); + } passEncoder.setViewport(vpX, vpY, vpW, vpH, 0, 1); passEncoder.setScissorRect(scissorX, scissorY, scissorW, scissorH); passEncoder.draw(6, 1, 0, 0); diff --git a/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts index e33c8475..08f4a3c3 100644 --- a/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts @@ -101,21 +101,20 @@ const copyRegionToFilterAttachment = ( const dstAttachment = config.frameBufferManager.createTemporaryAttachment(width, height); - const pipeline = config.pipelineManager.getPipeline("complex_blend_copy"); + // texture_copy_rgba8 (BlurFilterVertex, yFlipTexCoord=true) を使用 + const pipeline = config.pipelineManager.getPipeline("texture_copy_rgba8"); const bindGroupLayout = config.pipelineManager.getBindGroupLayout("texture_copy"); if (!pipeline || !bindGroupLayout || !srcAttachment.texture || !dstAttachment.texture) { return dstAttachment; } + // BlurFilterVertex (yFlipTexCoord=true): + // texCoord.y=0(fb上端) → uv.y=y/H, texCoord.y=1(fb下端) → uv.y=(y+h)/H const scaleX = width / srcAttachment.width; + const scaleY = height / srcAttachment.height; const offsetX = x / srcAttachment.width; - - // ComplexBlendCopyVertexはOpenGL座標系のtexCoord(Y軸反転)を使用するため、 - // UV uniformでY反転を補正して正しい向きの出力を得る - // texCoord.y=1(fb上端) → uv.y=y/H(ソース上端), texCoord.y=0(fb下端) → uv.y=(y+h)/H(ソース下端) - const scaleY = -(height / srcAttachment.height); - const offsetY = (y + height) / srcAttachment.height; + const offsetY = y / srcAttachment.height; $uniform4[0] = scaleX; $uniform4[1] = scaleY; @@ -492,8 +491,7 @@ const applyFilterChain = ( const newAtt = filterApplyGlowFilterUseCase( filterAttachment, matrix, params[idx++], params[idx++], params[idx++], params[idx++], - params[idx++], params[idx++], - Boolean(params[idx++]), Boolean(params[idx++]), + params[idx++], params[idx++], Boolean(params[idx++]), Boolean(params[idx++]), devicePixelRatio, config ); if (filterAttachment !== newAtt) { diff --git a/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts b/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts index 845fc0b6..944a5c50 100644 --- a/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts +++ b/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts @@ -1,13 +1,9 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; import type { IFilterConfig } from "../../interface/IFilterConfig"; import { $offset } from "../FilterOffset"; +import { DEG_TO_RAD, intToPremultipliedRGBA } from "../FilterUtil"; import { execute as filterApplyBlurFilterUseCase } from "../BlurFilter/FilterApplyBlurFilterUseCase"; -/** - * @description 度からラジアンへの変換係数 - */ -const DEG_TO_RAD: number = Math.PI / 180; - /** * @description プリアロケートされたFloat32Array */ @@ -33,16 +29,6 @@ const $entries4: GPUBindGroupEntry[] = [ { "binding": 3, "resource": null as unknown as GPUTextureView } ]; -/** - * @description 32bit整数からRGB値を抽出(プリマルチプライドアルファ対応) - */ -const intToRGBA = (color: number, alpha: number): [number, number, number, number] => { - const r = (color >> 16 & 0xFF) / 255 * alpha; - const g = (color >> 8 & 0xFF) / 255 * alpha; - const b = (color & 0xFF) / 255 * alpha; - return [r, g, b, alpha]; -}; - /** * @description ベベルフィルターを適用 * WebGL版と同様に、erase前処理で差分テクスチャを作成してからブラーを適用 @@ -220,8 +206,8 @@ export const execute = ( // baseScale, baseOffset (16 bytes) // blurScale, blurOffset (16 bytes) // Total: 80 bytes → 16 floats + 4 floats = 20 floats (80 bytes) - const [hr, hg, hb, ha] = intToRGBA(highlightColor, highlightAlpha); - const [sr, sg, sb, sa] = intToRGBA(shadowColor, shadowAlpha); + const [hr, hg, hb, ha] = intToPremultipliedRGBA(highlightColor, highlightAlpha); + const [sr, sg, sb, sa] = intToPremultipliedRGBA(shadowColor, shadowAlpha); $uniform20[0] = hr; $uniform20[1] = hg; diff --git a/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts b/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts index 84f7b231..c9cce695 100644 --- a/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts +++ b/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts @@ -2,8 +2,6 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; import type { IFilterConfig } from "../../interface/IFilterConfig"; import { $offset } from "../FilterOffset"; import { calculateBlurParams, calculateDirectionalBlurParams } from "../BlurFilterUseCase"; -import { shouldUseComputeShader } from "./service/BlurFilterComputeShaderService"; -import { execute as executeBlurCompute } from "../../Compute/service/ComputeExecuteBlurService"; /** * @description プリアロケートされたFloat32Array (サイズ4) @@ -78,10 +76,6 @@ export const execute = ( const bufferBlurX = baseBlurX * bufferScaleX; const bufferBlurY = baseBlurY * bufferScaleY; - // Compute Shaderを使用すべきか判定 - const useCompute = config.computePipelineManager - && shouldUseComputeShader(baseBlurX, baseBlurY, bufferWidth, bufferHeight); - // ブラーパスを実行 const attachments = [attachment0, attachment1]; let attachmentIndex = 0; @@ -92,19 +86,11 @@ export const execute = ( const srcIndex = attachmentIndex; attachmentIndex = (attachmentIndex + 1) % 2; - if (useCompute) { - executeBlurCompute( - device, commandEncoder, config.computePipelineManager!, - attachments[srcIndex], attachments[attachmentIndex], - true, bufferBlurX, config.bufferManager - ); - } else { - applyDirectionalBlur( - device, commandEncoder, frameBufferManager, pipelineManager, - attachments[srcIndex], attachments[attachmentIndex], sampler, - true, bufferBlurX, config.bufferManager - ); - } + applyDirectionalBlur( + device, commandEncoder, frameBufferManager, pipelineManager, + attachments[srcIndex], attachments[attachmentIndex], sampler, + true, bufferBlurX, config.bufferManager + ); } // 垂直ブラー @@ -112,19 +98,11 @@ export const execute = ( const srcIndex = attachmentIndex; attachmentIndex = (attachmentIndex + 1) % 2; - if (useCompute) { - executeBlurCompute( - device, commandEncoder, config.computePipelineManager!, - attachments[srcIndex], attachments[attachmentIndex], - false, bufferBlurY, config.bufferManager - ); - } else { - applyDirectionalBlur( - device, commandEncoder, frameBufferManager, pipelineManager, - attachments[srcIndex], attachments[attachmentIndex], sampler, - false, bufferBlurY, config.bufferManager - ); - } + applyDirectionalBlur( + device, commandEncoder, frameBufferManager, pipelineManager, + attachments[srcIndex], attachments[attachmentIndex], sampler, + false, bufferBlurY, config.bufferManager + ); } } @@ -138,7 +116,6 @@ export const execute = ( upscaleTexture( device, commandEncoder, frameBufferManager, pipelineManager, resultAttachment, finalAttachment, sampler, - 1 / bufferScaleX, 1 / bufferScaleY, config.bufferManager ); @@ -325,8 +302,6 @@ const upscaleTexture = ( source: IAttachmentObject, dest: IAttachmentObject, sampler: GPUSampler, - _scaleX: number, - _scaleY: number, bufferManager?: IFilterConfig["bufferManager"] ): void => { // temp_アタッチメントはrgba8unormフォーマットなので、texture_copy_rgba8パイプラインを使用 diff --git a/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.test.ts b/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.test.ts deleted file mode 100644 index 073925d7..00000000 --- a/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.test.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { IAttachmentObject } from "../../../interface/IAttachmentObject"; -import type { IFilterConfig } from "../../../interface/IFilterConfig"; -import type { ComputePipelineManager } from "../../../Compute/ComputePipelineManager"; -import { execute, shouldUseComputeShader } from "./BlurFilterComputeShaderService"; - -// Mock the compute blur service -vi.mock("../../../Compute/service/ComputeExecuteBlurService", () => ({ - "execute": vi.fn() -})); - -import { execute as mockExecuteBlurCompute } from "../../../Compute/service/ComputeExecuteBlurService"; - -describe("BlurFilterComputeShaderService", () => -{ - const createMockAttachment = (width: number = 256, height: number = 256): IAttachmentObject => - { - return { - "id": 1, - "width": width, - "height": height, - "clipLevel": 0, - "msaa": false, - "mask": false, - "color": null, - "texture": { - "id": 1, - "width": width, - "height": height, - "area": width * height, - "smooth": true, - "resource": {} as GPUTexture, - "view": {} as GPUTextureView - }, - "stencil": null, - "msaaTexture": null, - "msaaStencil": null - }; - }; - - const createMockDevice = () => - { - return {} as GPUDevice; - }; - - const createMockCommandEncoder = () => - { - return {} as GPUCommandEncoder; - }; - - const createMockComputePipelineManager = () => - { - return {} as ComputePipelineManager; - }; - - const createMockConfig = () => - { - return {} as IFilterConfig; - }; - - beforeEach(() => - { - vi.clearAllMocks(); - }); - - describe("execute", () => - { - it("should call executeBlurCompute with correct parameters", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const computePipelineManager = createMockComputePipelineManager(); - const config = createMockConfig(); - const source = createMockAttachment(); - const dest = createMockAttachment(); - - execute(device, commandEncoder, computePipelineManager, config, source, dest, true, 16); - - expect(mockExecuteBlurCompute).toHaveBeenCalledWith( - device, - commandEncoder, - computePipelineManager, - source, - dest, - true, - 8 // radius = ceil(16 / 2) = 8 - ); - }); - - it("should calculate radius as ceil of blur / 2", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const computePipelineManager = createMockComputePipelineManager(); - const config = createMockConfig(); - const source = createMockAttachment(); - const dest = createMockAttachment(); - - execute(device, commandEncoder, computePipelineManager, config, source, dest, false, 15); - - expect(mockExecuteBlurCompute).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - false, - 8 // radius = ceil(15 / 2) = 8 - ); - }); - - it("should pass isHorizontal = true for horizontal blur", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const computePipelineManager = createMockComputePipelineManager(); - const config = createMockConfig(); - const source = createMockAttachment(); - const dest = createMockAttachment(); - - execute(device, commandEncoder, computePipelineManager, config, source, dest, true, 10); - - expect(mockExecuteBlurCompute).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - true, - expect.any(Number) - ); - }); - - it("should pass isHorizontal = false for vertical blur", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const computePipelineManager = createMockComputePipelineManager(); - const config = createMockConfig(); - const source = createMockAttachment(); - const dest = createMockAttachment(); - - execute(device, commandEncoder, computePipelineManager, config, source, dest, false, 10); - - expect(mockExecuteBlurCompute).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - false, - expect.any(Number) - ); - }); - }); - - describe("shouldUseComputeShader", () => - { - describe("blur threshold", () => - { - it("should return true when blur >= 4 and size >= 128", () => - { - const result = shouldUseComputeShader(4, 4, 128, 128); - - expect(result).toBe(true); - }); - - it("should return true when blurX >= 4 (using max)", () => - { - const result = shouldUseComputeShader(5, 2, 128, 128); - - expect(result).toBe(true); - }); - - it("should return true when blurY >= 4 (using max)", () => - { - const result = shouldUseComputeShader(2, 5, 128, 128); - - expect(result).toBe(true); - }); - - it("should return false when both blurs < 4", () => - { - const result = shouldUseComputeShader(3, 3, 128, 128); - - expect(result).toBe(false); - }); - - it("should return false when max blur < 4", () => - { - const result = shouldUseComputeShader(2, 3, 512, 512); - - expect(result).toBe(false); - }); - }); - - describe("size threshold", () => - { - it("should return true when min size >= 128", () => - { - const result = shouldUseComputeShader(10, 10, 128, 128); - - expect(result).toBe(true); - }); - - it("should return true when width >= 128 and height > 128", () => - { - const result = shouldUseComputeShader(10, 10, 128, 512); - - expect(result).toBe(true); - }); - - it("should return true when height >= 128 and width > 128", () => - { - const result = shouldUseComputeShader(10, 10, 512, 128); - - expect(result).toBe(true); - }); - - it("should return false when width < 128", () => - { - const result = shouldUseComputeShader(10, 10, 100, 512); - - expect(result).toBe(false); - }); - - it("should return false when height < 128", () => - { - const result = shouldUseComputeShader(10, 10, 512, 100); - - expect(result).toBe(false); - }); - - it("should return false when both dimensions < 128", () => - { - const result = shouldUseComputeShader(20, 20, 100, 100); - - expect(result).toBe(false); - }); - }); - - describe("edge cases", () => - { - it("should return true at exact thresholds", () => - { - const result = shouldUseComputeShader(4, 0, 128, 128); - - expect(result).toBe(true); - }); - - it("should return false just below blur threshold", () => - { - const result = shouldUseComputeShader(3.9, 3.9, 128, 128); - - expect(result).toBe(false); - }); - - it("should return false just below size threshold", () => - { - const result = shouldUseComputeShader(10, 10, 127, 127); - - expect(result).toBe(false); - }); - - it("should return false when only one condition is met", () => - { - // Large blur but small size - expect(shouldUseComputeShader(20, 20, 100, 100)).toBe(false); - - // Large size but small blur - expect(shouldUseComputeShader(3, 3, 1024, 1024)).toBe(false); - }); - - it("should handle zero blur values", () => - { - const result = shouldUseComputeShader(0, 0, 512, 512); - - expect(result).toBe(false); - }); - - it("should handle large values", () => - { - const result = shouldUseComputeShader(100, 100, 4096, 4096); - - expect(result).toBe(true); - }); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.ts b/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.ts deleted file mode 100644 index 7cfbace5..00000000 --- a/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { IAttachmentObject } from "../../../interface/IAttachmentObject"; -import type { IFilterConfig } from "../../../interface/IFilterConfig"; -import type { ComputePipelineManager } from "../../../Compute/ComputePipelineManager"; -import { execute as executeBlurCompute } from "../../../Compute/service/ComputeExecuteBlurService"; - -/** - * @description Compute Shaderでブラーパスを実行 - * Apply blur pass using Compute Shader - * - * Fragment Shaderベースの従来実装と比較して: - * - 並列処理による高速化(大きな半径で20-35%) - * - 共有メモリを活用したメモリアクセス最適化 - * - ワークグループ内でのデータ共有 - * - * @param {GPUDevice} device - WebGPU device - * @param {GPUCommandEncoder} commandEncoder - コマンドエンコーダー - * @param {ComputePipelineManager} computePipelineManager - Compute Pipeline Manager - * @param {IFilterConfig} config - フィルター設定 - * @param {IAttachmentObject} source - 入力アタッチメント - * @param {IAttachmentObject} dest - 出力アタッチメント - * @param {boolean} isHorizontal - 水平ブラーかどうか - * @param {number} blur - ブラー量 - * @return {void} - */ -export const execute = ( - device: GPUDevice, - commandEncoder: GPUCommandEncoder, - computePipelineManager: ComputePipelineManager, - _config: IFilterConfig, - source: IAttachmentObject, - dest: IAttachmentObject, - isHorizontal: boolean, - blur: number -): void => { - - // ブラー半径を計算(ブラー量の半分) - const radius = Math.ceil(blur / 2); - - // Compute Shaderでブラーを実行 - executeBlurCompute( - device, - commandEncoder, - computePipelineManager, - source, - dest, - isHorizontal, - radius - ); -}; - -/** - * @description Compute Shaderを使用すべきかどうか判定 - * Determine whether to use Compute Shader - * - * 以下の条件でCompute Shaderを使用: - * - ブラー半径が大きい(8以上) - * - テクスチャサイズが十分大きい(256x256以上) - * - * 小さなブラー半径では Fragment Shader の方が効率的な場合がある。 - * - * @param {number} blurX - X方向のブラー量 - * @param {number} blurY - Y方向のブラー量 - * @param {number} width - テクスチャ幅 - * @param {number} height - テクスチャ高さ - * @return {boolean} Compute Shaderを使用すべきかどうか - */ -export const shouldUseComputeShader = ( - blurX: number, - blurY: number, - width: number, - height: number -): boolean => { - - // ブラー半径のしきい値 - const BLUR_THRESHOLD = 4; - - // テクスチャサイズのしきい値 - const SIZE_THRESHOLD = 128; - - const maxBlur = Math.max(blurX, blurY); - const minSize = Math.min(width, height); - - return maxBlur >= BLUR_THRESHOLD && minSize >= SIZE_THRESHOLD; -}; diff --git a/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.test.ts b/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.test.ts deleted file mode 100644 index 9d8d9053..00000000 --- a/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.test.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { IAttachmentObject } from "../../../interface/IAttachmentObject"; -import type { IFilterConfig } from "../../../interface/IFilterConfig"; -import type { ComputePipelineManager } from "../../../Compute/ComputePipelineManager"; -import { execute } from "./FilterApplyBlurComputeUseCase"; - -// Mock GPUBufferUsage -const GPUBufferUsage = { - UNIFORM: 0x40, - COPY_DST: 0x08 -}; -(globalThis as any).GPUBufferUsage = GPUBufferUsage; - -// Mock offset - use object that will be imported -vi.mock("../../FilterOffset", () => ({ - "$offset": { "x": 0, "y": 0 } -})); - -import { $offset } from "../../FilterOffset"; - -// Mock calculateBlurParams -const mockCalculateBlurParams = vi.fn(); -vi.mock("../../BlurFilterUseCase", () => ({ - "calculateBlurParams": (...args: any[]) => mockCalculateBlurParams(...args) -})); - -// Mock BlurFilterComputeShaderService -const mockBlurComputeService = vi.fn(); -const mockShouldUseComputeShader = vi.fn(); -vi.mock("../service/BlurFilterComputeShaderService", () => ({ - "execute": (...args: any[]) => mockBlurComputeService(...args), - "shouldUseComputeShader": (...args: any[]) => mockShouldUseComputeShader(...args) -})); - -// Mock FilterApplyBlurFilterUseCase (fragment fallback) -const mockExecuteFragmentBlur = vi.fn(); -vi.mock("../FilterApplyBlurFilterUseCase", () => ({ - "execute": (...args: any[]) => mockExecuteFragmentBlur(...args) -})); - -describe("FilterApplyBlurComputeUseCase", () => -{ - const createMockAttachment = (width: number = 100, height: number = 100): IAttachmentObject => - { - return { - "id": 1, - "width": width, - "height": height, - "clipLevel": 0, - "texture": { - "resource": { "label": "mockTexture" } as unknown as GPUTexture, - "view": { "label": "mockTextureView" } as unknown as GPUTextureView - } - } as IAttachmentObject; - }; - - const createMockConfig = (): IFilterConfig => - { - const mockPassEncoder = { - "setPipeline": vi.fn(), - "setBindGroup": vi.fn(), - "setViewport": vi.fn(), - "setScissorRect": vi.fn(), - "draw": vi.fn(), - "end": vi.fn() - }; - - return { - "device": { - "createBuffer": vi.fn(() => ({ "label": "mockBuffer" })), - "queue": { "writeBuffer": vi.fn() }, - "createBindGroup": vi.fn(() => ({ "label": "mockBindGroup" })) - } as unknown as GPUDevice, - "commandEncoder": { - "beginRenderPass": vi.fn(() => mockPassEncoder) - } as unknown as GPUCommandEncoder, - "frameBufferManager": { - "createTemporaryAttachment": vi.fn((w: number, h: number) => createMockAttachment(w, h)), - "releaseTemporaryAttachment": vi.fn(), - "createRenderPassDescriptor": vi.fn(() => ({ - "colorAttachments": [{ "view": {}, "loadOp": "clear", "storeOp": "store" }] - })) - }, - "pipelineManager": { - "getPipeline": vi.fn(() => ({ "label": "mockPipeline" })), - "getBindGroupLayout": vi.fn(() => ({ "label": "mockLayout" })) - }, - "textureManager": { - "createSampler": vi.fn(() => ({ "label": "mockSampler" })) - } - } as unknown as IFilterConfig; - }; - - const createMockComputePipelineManager = (): ComputePipelineManager => - { - return { - "getPipeline": vi.fn(() => ({ "label": "mockComputePipeline" })), - "getBindGroupLayout": vi.fn(() => ({ "label": "mockComputeLayout" })) - } as unknown as ComputePipelineManager; - }; - - beforeEach(() => - { - vi.clearAllMocks(); - $offset.x = 0; - $offset.y = 0; - vi.spyOn(console, "error").mockImplementation(() => {}); - - // Default mock implementations - mockCalculateBlurParams.mockReturnValue({ - "baseBlurX": 16, - "baseBlurY": 16, - "offsetX": 32, - "offsetY": 32, - "bufferScaleX": 1, - "bufferScaleY": 1 - }); - }); - - describe("compute shader decision", () => - { - it("should use fragment shader when compute is not appropriate", () => - { - mockShouldUseComputeShader.mockReturnValue(false); - const expectedResult = createMockAttachment(200, 200); - mockExecuteFragmentBlur.mockReturnValue(expectedResult); - - const sourceAttachment = createMockAttachment(); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - const result = execute( - sourceAttachment, - matrix, - 4, // small blurX - 4, // small blurY - 1, - 1, - config, - computePipelineManager - ); - - expect(mockShouldUseComputeShader).toHaveBeenCalled(); - expect(mockExecuteFragmentBlur).toHaveBeenCalled(); - expect(result).toBe(expectedResult); - }); - - it("should use compute shader when appropriate", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - mockCalculateBlurParams.mockReturnValue({ - "baseBlurX": 32, - "baseBlurY": 32, - "offsetX": 64, - "offsetY": 64, - "bufferScaleX": 1, - "bufferScaleY": 1 - }); - - const sourceAttachment = createMockAttachment(); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 32, - 32, - 1, - 1, - config, - computePipelineManager - ); - - expect(mockShouldUseComputeShader).toHaveBeenCalled(); - expect(mockExecuteFragmentBlur).not.toHaveBeenCalled(); - expect(mockBlurComputeService).toHaveBeenCalled(); - }); - }); - - describe("blur parameters", () => - { - it("should calculate blur parameters correctly", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - - const sourceAttachment = createMockAttachment(100, 100); - const matrix = new Float32Array([2, 0, 0, 0, 2, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 16, - 16, - 3, - 2, - config, - computePipelineManager - ); - - expect(mockCalculateBlurParams).toHaveBeenCalledWith( - matrix, - 16, - 16, - 3, - 2 - ); - }); - - it("should update offset based on blur parameters", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - mockCalculateBlurParams.mockReturnValue({ - "baseBlurX": 16, - "baseBlurY": 16, - "offsetX": 20, - "offsetY": 25, - "bufferScaleX": 1, - "bufferScaleY": 1 - }); - - const sourceAttachment = createMockAttachment(); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 16, - 16, - 1, - 1, - config, - computePipelineManager - ); - - expect($offset.x).toBe(20); - expect($offset.y).toBe(25); - }); - }); - - describe("multi-pass blur", () => - { - it("should perform horizontal and vertical passes for each quality level", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - - const sourceAttachment = createMockAttachment(); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 16, - 16, - 3, // quality = 3 - 1, - config, - computePipelineManager - ); - - // 3 quality passes * 2 directions (horizontal + vertical) = 6 calls - expect(mockBlurComputeService).toHaveBeenCalledTimes(6); - }); - - it("should skip horizontal pass when blurX is 0", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - mockCalculateBlurParams.mockReturnValue({ - "baseBlurX": 0, - "baseBlurY": 16, - "offsetX": 0, - "offsetY": 32, - "bufferScaleX": 1, - "bufferScaleY": 1 - }); - - const sourceAttachment = createMockAttachment(); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 0, - 16, - 1, - 1, - config, - computePipelineManager - ); - - // Only vertical passes - expect(mockBlurComputeService).toHaveBeenCalledTimes(1); - // Should be called with horizontal=false - expect(mockBlurComputeService).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - false, // horizontal = false (vertical pass) - expect.any(Number) - ); - }); - - it("should skip vertical pass when blurY is 0", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - mockCalculateBlurParams.mockReturnValue({ - "baseBlurX": 16, - "baseBlurY": 0, - "offsetX": 32, - "offsetY": 0, - "bufferScaleX": 1, - "bufferScaleY": 1 - }); - - const sourceAttachment = createMockAttachment(); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 16, - 0, - 1, - 1, - config, - computePipelineManager - ); - - // Only horizontal passes - expect(mockBlurComputeService).toHaveBeenCalledTimes(1); - // Should be called with horizontal=true - expect(mockBlurComputeService).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - true, // horizontal = true - expect.any(Number) - ); - }); - }); - - describe("buffer management", () => - { - it("should create temporary attachments for ping-pong buffer", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - - const sourceAttachment = createMockAttachment(100, 100); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 16, - 16, - 1, - 1, - config, - computePipelineManager - ); - - // Should create 2 temporary attachments for ping-pong - expect(config.frameBufferManager.createTemporaryAttachment).toHaveBeenCalledTimes(2); - }); - - it("should release unused buffer after processing", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - - const sourceAttachment = createMockAttachment(100, 100); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 16, - 16, - 1, - 1, - config, - computePipelineManager - ); - - // Should release the unused ping-pong buffer - expect(config.frameBufferManager.releaseTemporaryAttachment).toHaveBeenCalled(); - }); - }); - - describe("return value", () => - { - it("should return result attachment after compute blur", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - - const sourceAttachment = createMockAttachment(100, 100); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - const result = execute( - sourceAttachment, - matrix, - 16, - 16, - 1, - 1, - config, - computePipelineManager - ); - - expect(result).toBeDefined(); - expect(result.texture).toBeDefined(); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.ts b/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.ts deleted file mode 100644 index 9639a3a8..00000000 --- a/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.ts +++ /dev/null @@ -1,292 +0,0 @@ -import type { IAttachmentObject } from "../../../interface/IAttachmentObject"; -import type { IFilterConfig } from "../../../interface/IFilterConfig"; -import type { ComputePipelineManager } from "../../../Compute/ComputePipelineManager"; -import { $offset } from "../../FilterOffset"; -import { calculateBlurParams } from "../../BlurFilterUseCase"; -import { - execute as blurComputeService, - shouldUseComputeShader -} from "../service/BlurFilterComputeShaderService"; -import { execute as executeFragmentBlur } from "../FilterApplyBlurFilterUseCase"; - -/** - * @description プリアロケートされたFloat32Array (サイズ4) - */ -const $uniform4 = new Float32Array(4); - -/** - * @description プリアロケートされたBindGroupEntry配列 (バインディング3つ) - */ -const $entries3: GPUBindGroupEntry[] = [ - { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, - { "binding": 1, "resource": null as unknown as GPUSampler }, - { "binding": 2, "resource": null as unknown as GPUTextureView } -]; - -/** - * @description Compute Shaderを使用したブラーフィルター - * Apply blur filter using Compute Shader - * - * Fragment Shaderベースの従来実装と比較して: - * - 大きなブラー半径で20-35%高速化 - * - 並列処理による効率的なテクスチャサンプリング - * - 共有メモリを活用したメモリアクセス最適化 - * - * 小さなブラー半径(8未満)では従来のFragment Shaderを使用。 - * - * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ - * @param {Float32Array} matrix - 変換行列 - * @param {number} blurX - X方向のブラー量 - * @param {number} blurY - Y方向のブラー量 - * @param {number} quality - クオリティ (1-15) - * @param {number} devicePixelRatio - デバイスピクセル比 - * @param {IFilterConfig} config - WebGPUリソース設定 - * @param {ComputePipelineManager} computePipelineManager - Compute Pipeline Manager - * @return {IAttachmentObject} - フィルター適用後のアタッチメント - */ -export const execute = ( - sourceAttachment: IAttachmentObject, - matrix: Float32Array, - blurX: number, - blurY: number, - quality: number, - devicePixelRatio: number, - config: IFilterConfig, - computePipelineManager: ComputePipelineManager -): IAttachmentObject => { - - const { device, commandEncoder, frameBufferManager, pipelineManager, textureManager } = config; - - // ブラーパラメータを計算 - const blurParams = calculateBlurParams(matrix, blurX, blurY, quality, devicePixelRatio); - const { baseBlurX, baseBlurY, offsetX, offsetY, bufferScaleX, bufferScaleY } = blurParams; - - // オフセットを更新 - $offset.x += offsetX; - $offset.y += offsetY; - - // ブラー用バッファサイズを計算 - const width = sourceAttachment.width + offsetX * 2; - const height = sourceAttachment.height + offsetY * 2; - const bufferWidth = Math.ceil(width * bufferScaleX); - const bufferHeight = Math.ceil(height * bufferScaleY); - - // Compute Shaderを使用すべきか判定 - const useCompute = shouldUseComputeShader(baseBlurX, baseBlurY, bufferWidth, bufferHeight); - - if (!useCompute) { - // 小さなブラーは従来のFragment Shaderを使用 - // FilterApplyBlurFilterUseCaseにフォールバック - return executeFragmentBlur( - sourceAttachment, - matrix, - blurX, - blurY, - quality, - devicePixelRatio, - config - ); - } - - // ピンポンバッファ用の一時アタッチメントを作成 - const attachment0 = frameBufferManager.createTemporaryAttachment(bufferWidth, bufferHeight); - const attachment1 = frameBufferManager.createTemporaryAttachment(bufferWidth, bufferHeight); - - // サンプラーを作成(線形補間) - const sampler = textureManager.createSampler("blur_compute_sampler", true); - - // ソーステクスチャをattachment0にコピー - copyTextureToAttachment( - device, commandEncoder, frameBufferManager, pipelineManager, - sourceAttachment, attachment0, sampler, - bufferScaleX, bufferScaleY, - offsetX * bufferScaleX, offsetY * bufferScaleY, - config.bufferManager - ); - - // バッファスケールを考慮したブラー値 - const bufferBlurX = baseBlurX * bufferScaleX; - const bufferBlurY = baseBlurY * bufferScaleY; - - // Compute Shaderでブラーパスを実行 - const attachments = [attachment0, attachment1]; - let attachmentIndex = 0; - - for (let q = 0; q < quality; ++q) { - // 水平ブラー - if (blurX > 0) { - const srcIndex = attachmentIndex; - attachmentIndex = (attachmentIndex + 1) % 2; - - blurComputeService( - device, commandEncoder, computePipelineManager, config, - attachments[srcIndex], attachments[attachmentIndex], - true, bufferBlurX - ); - } - - // 垂直ブラー - if (blurY > 0) { - const srcIndex = attachmentIndex; - attachmentIndex = (attachmentIndex + 1) % 2; - - blurComputeService( - device, commandEncoder, computePipelineManager, config, - attachments[srcIndex], attachments[attachmentIndex], - false, bufferBlurY - ); - } - } - - // 結果のアタッチメント - let resultAttachment = attachments[attachmentIndex]; - - // バッファスケールが1でない場合は元のサイズにアップスケール - if (bufferScaleX !== 1 || bufferScaleY !== 1) { - const finalAttachment = frameBufferManager.createTemporaryAttachment(width, height); - - upscaleTexture( - device, commandEncoder, frameBufferManager, pipelineManager, - resultAttachment, finalAttachment, sampler, - config.bufferManager - ); - - // ピンポンバッファを解放 - frameBufferManager.releaseTemporaryAttachment(attachment0); - frameBufferManager.releaseTemporaryAttachment(attachment1); - - resultAttachment = finalAttachment; - } else { - // 使わなかったバッファを解放 - const unusedIndex = (attachmentIndex + 1) % 2; - frameBufferManager.releaseTemporaryAttachment(attachments[unusedIndex]); - } - - return resultAttachment; -}; - -/** - * @description テクスチャをアタッチメントにコピー - */ -const copyTextureToAttachment = ( - device: GPUDevice, - commandEncoder: GPUCommandEncoder, - frameBufferManager: IFilterConfig["frameBufferManager"], - pipelineManager: IFilterConfig["pipelineManager"], - source: IAttachmentObject, - dest: IAttachmentObject, - sampler: GPUSampler, - bufferScaleX: number, - bufferScaleY: number, - pixelOffsetX: number, - pixelOffsetY: number, - bufferManager?: IFilterConfig["bufferManager"] -): void => { - const pipeline = pipelineManager.getPipeline("texture_copy_rgba8"); - const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy"); - - if (!pipeline || !bindGroupLayout) { - console.error("[WebGPU BlurCompute] texture_copy_rgba8 pipeline not found"); - return; - } - - const scaledSourceWidth = source.width * bufferScaleX; - const scaledSourceHeight = source.height * bufferScaleY; - - $uniform4[0] = 1; - $uniform4[1] = 1; - $uniform4[2] = 0; - $uniform4[3] = 0; - const uniformBuffer = bufferManager - ? bufferManager.acquireAndWriteUniformBuffer($uniform4) - : device.createBuffer({ - "size": $uniform4.byteLength, - "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST - }); - if (!bufferManager) { - device.queue.writeBuffer(uniformBuffer, 0, $uniform4); - } - - ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; - $entries3[1].resource = sampler; - $entries3[2].resource = source.texture!.view; - const bindGroup = device.createBindGroup({ - "layout": bindGroupLayout, - "entries": $entries3 - }); - - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( - dest.texture!.view, 0, 0, 0, 0, "clear" - ); - - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); - passEncoder.setPipeline(pipeline); - passEncoder.setBindGroup(0, bindGroup); - - passEncoder.setViewport( - pixelOffsetX, pixelOffsetY, - scaledSourceWidth, scaledSourceHeight, - 0, 1 - ); - passEncoder.setScissorRect( - Math.floor(pixelOffsetX), Math.floor(pixelOffsetY), - Math.ceil(scaledSourceWidth), Math.ceil(scaledSourceHeight) - ); - - passEncoder.draw(6, 1, 0, 0); - passEncoder.end(); -}; - -/** - * @description テクスチャをアップスケール - */ -const upscaleTexture = ( - device: GPUDevice, - commandEncoder: GPUCommandEncoder, - frameBufferManager: IFilterConfig["frameBufferManager"], - pipelineManager: IFilterConfig["pipelineManager"], - source: IAttachmentObject, - dest: IAttachmentObject, - sampler: GPUSampler, - bufferManager?: IFilterConfig["bufferManager"] -): void => { - const pipeline = pipelineManager.getPipeline("texture_copy_rgba8"); - const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy"); - - if (!pipeline || !bindGroupLayout) { - console.error("[WebGPU BlurCompute] texture_copy_rgba8 pipeline not found"); - return; - } - - $uniform4[0] = 1; - $uniform4[1] = 1; - $uniform4[2] = 0; - $uniform4[3] = 0; - const uniformBuffer = bufferManager - ? bufferManager.acquireAndWriteUniformBuffer($uniform4) - : device.createBuffer({ - "size": $uniform4.byteLength, - "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST - }); - if (!bufferManager) { - device.queue.writeBuffer(uniformBuffer, 0, $uniform4); - } - - ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; - $entries3[1].resource = sampler; - $entries3[2].resource = source.texture!.view; - const bindGroup = device.createBindGroup({ - "layout": bindGroupLayout, - "entries": $entries3 - }); - - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( - dest.texture!.view, 0, 0, 0, 0, "clear" - ); - - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); - passEncoder.setPipeline(pipeline); - passEncoder.setBindGroup(0, bindGroup); - passEncoder.draw(6, 1, 0, 0); - passEncoder.end(); -}; diff --git a/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts b/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts index 57bee8ff..16b951d5 100644 --- a/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts +++ b/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts @@ -1,6 +1,7 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; import type { IFilterConfig } from "../../interface/IFilterConfig"; import { ShaderSource } from "../../Shader/ShaderSource"; +import { intToStraightRGBA } from "../FilterUtil"; /** * @description プリアロケートされたBindGroupEntry配列 (バインディング3つ) @@ -11,16 +12,6 @@ const $entries3: GPUBindGroupEntry[] = [ { "binding": 2, "resource": null as unknown as GPUTextureView } ]; -/** - * @description 32bit整数からRGB値を抽出 - */ -const intToRGBA = (color: number, alpha: number): [number, number, number, number] => { - const r = (color >> 16 & 0xFF) / 255; - const g = (color >> 8 & 0xFF) / 255; - const b = (color & 0xFF) / 255; - return [r, g, b, alpha]; -}; - /** * @description パイプラインキャッシュ(キー: matrixX,matrixY,preserveAlpha,clamp) */ @@ -130,7 +121,7 @@ export const execute = ( // ユニフォームバッファを作成 const matrixSize = matrixX * matrixY; const matrixArraySize = Math.ceil(matrixSize / 4); - const [r, g, b, a] = intToRGBA(color, alpha); + const [r, g, b, a] = intToStraightRGBA(color, alpha); // マトリクスを4要素ごとにまとめる const paddedMatrix = new Float32Array(matrixArraySize * 4); diff --git a/packages/webgpu/src/Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase.ts b/packages/webgpu/src/Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase.ts index 2f44d640..30493f6c 100644 --- a/packages/webgpu/src/Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase.ts +++ b/packages/webgpu/src/Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase.ts @@ -1,6 +1,7 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; import type { IFilterConfig } from "../../interface/IFilterConfig"; import { ShaderSource } from "../../Shader/ShaderSource"; +import { intToPremultipliedRGBA } from "../FilterUtil"; /** * @description プリアロケートされたFloat32Array (サイズ12: 最大48バイト) @@ -17,16 +18,6 @@ const $entries4: GPUBindGroupEntry[] = [ { "binding": 3, "resource": null as unknown as GPUTextureView } ]; -/** - * @description 32bit整数からRGB値を抽出(プリマルチプライドアルファ) - */ -const intToRGBA = (color: number, alpha: number): [number, number, number, number] => { - const r = (color >> 16 & 0xFF) / 255 * alpha; - const g = (color >> 8 & 0xFF) / 255 * alpha; - const b = (color & 0xFF) / 255 * alpha; - return [r, g, b, alpha]; -}; - /** * @description パイプラインキャッシュ(キー: componentX,componentY,mode) */ @@ -188,7 +179,7 @@ export const execute = ( // substituteColor (mode === 1 の場合) if (needsSubstituteColor) { - const [r, g, b, a] = intToRGBA(color, alpha); + const [r, g, b, a] = intToPremultipliedRGBA(color, alpha); $uniform12[8] = r; $uniform12[9] = g; $uniform12[10] = b; diff --git a/packages/webgpu/src/Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase.ts b/packages/webgpu/src/Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase.ts index b6a73dab..8c769005 100644 --- a/packages/webgpu/src/Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase.ts +++ b/packages/webgpu/src/Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase.ts @@ -1,13 +1,9 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; import type { IFilterConfig } from "../../interface/IFilterConfig"; import { $offset } from "../FilterOffset"; +import { DEG_TO_RAD, intToPremultipliedRGBA } from "../FilterUtil"; import { execute as filterApplyBlurFilterUseCase } from "../BlurFilter/FilterApplyBlurFilterUseCase"; -/** - * @description 度からラジアンへの変換係数 - */ -const DEG_TO_RAD: number = Math.PI / 180; - /** * @description プリアロケートされたFloat32Array (サイズ16) */ @@ -23,16 +19,6 @@ const $entries4: GPUBindGroupEntry[] = [ { "binding": 3, "resource": null as unknown as GPUTextureView } ]; -/** - * @description 32bit整数からRGB値を抽出(プリマルチプライドアルファ対応) - */ -const intToRGBA = (color: number, alpha: number): [number, number, number, number] => { - const r = (color >> 16 & 0xFF) / 255 * alpha; - const g = (color >> 8 & 0xFF) / 255 * alpha; - const b = (color & 0xFF) / 255 * alpha; - return [r, g, b, alpha]; -}; - /** * @description ドロップシャドウフィルターを適用 * Apply drop shadow filter @@ -161,7 +147,7 @@ export const execute = ( // knockout: f32 (4 bytes) // hideObject: f32 (4 bytes) // Total: 64 bytes - const [r, g, b, a] = intToRGBA(color, alpha); + const [r, g, b, a] = intToPremultipliedRGBA(color, alpha); // WebGL版と同じUV変換方式: // uv = texCoord * scale - offset diff --git a/packages/webgpu/src/Filter/FilterUtil.ts b/packages/webgpu/src/Filter/FilterUtil.ts new file mode 100644 index 00000000..7dc5995d --- /dev/null +++ b/packages/webgpu/src/Filter/FilterUtil.ts @@ -0,0 +1,35 @@ +/** + * @description 度数法からラジアンへの変換係数 + * Conversion factor from degrees to radians + */ +export const DEG_TO_RAD: number = Math.PI / 180; + +/** + * @description 32bit整数カラーからプリマルチプライドアルファRGBA値を抽出 + * Extract premultiplied alpha RGBA values from 32bit integer color + * + * @param {number} color - 32bit整数カラー値 + * @param {number} alpha - アルファ値 (0-1) + * @return {[number, number, number, number]} - [r, g, b, a] (プリマルチプライドアルファ) + */ +export const intToPremultipliedRGBA = (color: number, alpha: number): [number, number, number, number] => { + const r = (color >> 16 & 0xFF) / 255 * alpha; + const g = (color >> 8 & 0xFF) / 255 * alpha; + const b = (color & 0xFF) / 255 * alpha; + return [r, g, b, alpha]; +}; + +/** + * @description 32bit整数カラーからストレートRGBA値を抽出 + * Extract straight (non-premultiplied) RGBA values from 32bit integer color + * + * @param {number} color - 32bit整数カラー値 + * @param {number} alpha - アルファ値 (0-1) + * @return {[number, number, number, number]} - [r, g, b, a] (ストレート) + */ +export const intToStraightRGBA = (color: number, alpha: number): [number, number, number, number] => { + const r = (color >> 16 & 0xFF) / 255; + const g = (color >> 8 & 0xFF) / 255; + const b = (color & 0xFF) / 255; + return [r, g, b, alpha]; +}; diff --git a/packages/webgpu/src/Filter/GlowFilter/FilterApplyGlowFilterUseCase.ts b/packages/webgpu/src/Filter/GlowFilter/FilterApplyGlowFilterUseCase.ts index fb27e5f6..c4b06255 100644 --- a/packages/webgpu/src/Filter/GlowFilter/FilterApplyGlowFilterUseCase.ts +++ b/packages/webgpu/src/Filter/GlowFilter/FilterApplyGlowFilterUseCase.ts @@ -1,6 +1,7 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; import type { IFilterConfig } from "../../interface/IFilterConfig"; import { $offset } from "../FilterOffset"; +import { intToPremultipliedRGBA } from "../FilterUtil"; import { execute as filterApplyBlurFilterUseCase } from "../BlurFilter/FilterApplyBlurFilterUseCase"; /** @@ -18,16 +19,6 @@ const $entries4: GPUBindGroupEntry[] = [ { "binding": 3, "resource": null as unknown as GPUTextureView } ]; -/** - * @description 32bit整数からRGB値を抽出(プリマルチプライドアルファ対応) - */ -const intToRGBA = (color: number, alpha: number): [number, number, number, number] => { - const r = (color >> 16 & 0xFF) / 255 * alpha; - const g = (color >> 8 & 0xFF) / 255 * alpha; - const b = (color & 0xFF) / 255 * alpha; - return [r, g, b, alpha]; -}; - /** * @description グローフィルターを適用 * Apply glow filter @@ -72,7 +63,6 @@ export const execute = ( const baseWidth = sourceAttachment.width; const baseHeight = sourceAttachment.height; - // ブラーフィルターを適用(元テクスチャを保持) const blurAttachment = filterApplyBlurFilterUseCase( sourceAttachment, matrix, blurX, blurY, quality, @@ -132,7 +122,7 @@ export const execute = ( // blurScale: vec2, blurOffset: vec2 (16 bytes) // strength: f32, inner: f32, knockout: f32, _padding: f32 (16 bytes) // Total: 64 bytes - const [r, g, b, a] = intToRGBA(color, alpha); + const [r, g, b, a] = intToPremultipliedRGBA(color, alpha); $uniform16[0] = r; $uniform16[1] = g; $uniform16[2] = b; diff --git a/packages/webgpu/src/Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase.ts b/packages/webgpu/src/Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase.ts index 4096a0b5..1cd58a7f 100644 --- a/packages/webgpu/src/Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase.ts +++ b/packages/webgpu/src/Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase.ts @@ -3,11 +3,7 @@ import type { IFilterConfig } from "../../interface/IFilterConfig"; import { $offset } from "../FilterOffset"; import { execute as filterApplyBlurFilterUseCase } from "../BlurFilter/FilterApplyBlurFilterUseCase"; import { generateFilterGradientLUT } from "../../Gradient/GradientLUTGenerator"; - -/** - * @description 度からラジアンへの変換係数 - */ -const DEG_TO_RAD: number = Math.PI / 180; +import { DEG_TO_RAD } from "../FilterUtil"; /** * @description プリアロケートされたFloat32Array diff --git a/packages/webgpu/src/Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase.ts b/packages/webgpu/src/Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase.ts index 58512293..cf943840 100644 --- a/packages/webgpu/src/Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase.ts +++ b/packages/webgpu/src/Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase.ts @@ -3,11 +3,7 @@ import type { IFilterConfig } from "../../interface/IFilterConfig"; import { $offset } from "../FilterOffset"; import { execute as filterApplyBlurFilterUseCase } from "../BlurFilter/FilterApplyBlurFilterUseCase"; import { generateFilterGradientLUT } from "../../Gradient/GradientLUTGenerator"; - -/** - * @description 度からラジアンへの変換係数 - */ -const DEG_TO_RAD: number = Math.PI / 180; +import { DEG_TO_RAD } from "../FilterUtil"; /** * @description プリアロケートされたFloat32Array (サイズ12) diff --git a/packages/webgpu/src/Shader/PipelineManager.ts b/packages/webgpu/src/Shader/PipelineManager.ts index fdfcdb78..d18dc0b7 100644 --- a/packages/webgpu/src/Shader/PipelineManager.ts +++ b/packages/webgpu/src/Shader/PipelineManager.ts @@ -82,6 +82,7 @@ export class PipelineManager ["filter_output_msaa", "texture_copy"], ["filter_output_add_msaa", "texture_copy"], ["filter_output_screen_msaa", "texture_copy"], ["filter_output_alpha_msaa", "texture_copy"], ["filter_output_erase_msaa", "texture_copy"], + ["filter_output_masked", "texture_copy"], ["filter_output_masked_msaa", "texture_copy"], ["positioned_texture", "texture_copy"], ["positioned_texture_rgba", "texture_copy"], ["bitmap_render_msaa", "texture_copy"], ["bitmap_render", "texture_copy"], ["texture_scale", "texture_copy"], ["texture_scale_blend", "texture_copy"], @@ -2158,6 +2159,35 @@ export class PipelineManager pipelineLayout, vertexShaderModule, filterOutputShaderModule, this.format, blend )); } + + // マスク付きフィルター出力パイプライン(ステンシルテスト付き) + const filterMaskedStencil: GPUDepthStencilState = { + "format": "stencil8", + "stencilFront": { + "compare": "equal", + "failOp": "keep", + "depthFailOp": "keep", + "passOp": "keep" + }, + "stencilBack": { + "compare": "equal", + "failOp": "keep", + "depthFailOp": "keep", + "passOp": "keep" + }, + "stencilReadMask": 0xFF, + "stencilWriteMask": 0x00 + }; + this.pipelines.set("filter_output_masked", this.createFullscreenQuadPipeline( + pipelineLayout, vertexShaderModule, filterOutputShaderModule, this.format, BLEND_ALPHA, undefined, filterMaskedStencil + )); + if (this.sampleCount > 1) { + const normalBlendForMask: GPUBlendState = { "color": { "srcFactor": "one", "dstFactor": "one-minus-src-alpha", "operation": "add" }, "alpha": { "srcFactor": "one", "dstFactor": "one-minus-src-alpha", "operation": "add" } }; + this.pipelines.set("filter_output_masked_msaa", this.createFullscreenQuadPipeline( + pipelineLayout, vertexShaderModule, filterOutputShaderModule, this.format, normalBlendForMask, this.sampleCount, filterMaskedStencil + )); + } + if (this.sampleCount > 1) { const copyBlend: GPUBlendState = { "color": { "srcFactor": "one", "dstFactor": "zero", "operation": "add" }, "alpha": { "srcFactor": "one", "dstFactor": "zero", "operation": "add" } }; this.pipelines.set("texture_copy_bgra_msaa", this.createFullscreenQuadPipeline( diff --git a/packages/webgpu/src/interface/ICachedBindGroup.ts b/packages/webgpu/src/interface/ICachedBindGroup.ts deleted file mode 100644 index 4910cbf3..00000000 --- a/packages/webgpu/src/interface/ICachedBindGroup.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @description キャッシュされたBindGroup - * Cached bind group interface - */ -export interface ICachedBindGroup { - bindGroup: GPUBindGroup; - lastUsedFrame: number; -} diff --git a/packages/webgpu/src/interface/IFilterConfig.ts b/packages/webgpu/src/interface/IFilterConfig.ts index f4381878..d069b384 100644 --- a/packages/webgpu/src/interface/IFilterConfig.ts +++ b/packages/webgpu/src/interface/IFilterConfig.ts @@ -1,5 +1,4 @@ import type { IAttachmentObject } from "./IAttachmentObject"; -import type { ComputePipelineManager } from "../Compute/ComputePipelineManager"; /** * @description フィルター処理の共通設定 @@ -29,6 +28,5 @@ export interface IFilterConfig { textureManager: { createSampler(name: string, smooth: boolean): GPUSampler; }; - computePipelineManager?: ComputePipelineManager; frameTextures: GPUTexture[]; } diff --git a/packages/webgpu/src/interface/ILocalFilterConfig.ts b/packages/webgpu/src/interface/ILocalFilterConfig.ts index 986d9b1d..df101f2f 100644 --- a/packages/webgpu/src/interface/ILocalFilterConfig.ts +++ b/packages/webgpu/src/interface/ILocalFilterConfig.ts @@ -3,7 +3,6 @@ import type { BufferManager } from "../BufferManager"; import type { FrameBufferManager } from "../FrameBufferManager"; import type { PipelineManager } from "../Shader/PipelineManager"; import type { TextureManager } from "../TextureManager"; -import type { ComputePipelineManager } from "../Compute/ComputePipelineManager"; /** * @description フィルター適用時のローカル設定(ContextApplyFilterUseCase用) @@ -17,6 +16,5 @@ export interface ILocalFilterConfig { pipelineManager: PipelineManager; textureManager: TextureManager; mainAttachment?: IAttachmentObject; - computePipelineManager?: ComputePipelineManager; frameTextures: GPUTexture[]; } diff --git a/packages/webgpu/src/interface/IPooledBuffer.ts b/packages/webgpu/src/interface/IPooledBuffer.ts deleted file mode 100644 index b73254af..00000000 --- a/packages/webgpu/src/interface/IPooledBuffer.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @description プールされたバッファのエントリ - * Pooled buffer entry - */ -export interface IPooledBuffer { - buffer: GPUBuffer; - size: number; -} diff --git a/src/index.ts b/src/index.ts index 593287b6..dec5f198 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import { Next2D } from "@next2d/core"; if (!("next2d" in window)) { - console.log("%c Next2D Player %c 3.0.5 %c https://next2d.app", + console.log("%c Next2D Player %c 3.1.0 %c https://next2d.app", "color: #fff; background: #5f5f5f", "color: #fff; background: #4bc729", ""); From f83c2ebcbab22619731ff5f2cb9c8c1bbf7b43da Mon Sep 17 00:00:00 2001 From: ienaga Date: Fri, 13 Mar 2026 23:18:27 +0900 Subject: [PATCH 02/26] =?UTF-8?q?#263=20=E4=B8=8D=E8=A6=81=E3=81=AA?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/webgpu/src/AttachmentManager.test.ts | 122 +------- packages/webgpu/src/AttachmentManager.ts | 30 -- ...rCreateRenderPassDescriptorService.test.ts | 174 ----------- ...anagerCreateRenderPassDescriptorService.ts | 51 ---- .../ContextDrawArraysInstancedUseCase.ts | 42 +-- .../usecase/MeshStrokeGenerateUseCase.test.ts | 69 +---- .../Mesh/usecase/MeshStrokeGenerateUseCase.ts | 56 ---- packages/webgpu/src/SamplerCache.test.ts | 273 ------------------ packages/webgpu/src/SamplerCache.ts | 90 ------ ...erCacheCreateCommonSamplersService.test.ts | 134 --------- ...SamplerCacheCreateCommonSamplersService.ts | 50 ---- .../SamplerCacheGenerateKeyService.test.ts | 56 ---- .../service/SamplerCacheGenerateKeyService.ts | 20 -- .../SamplerCacheGetOrCreateService.test.ts | 110 ------- .../service/SamplerCacheGetOrCreateService.ts | 41 --- .../webgpu/src/Shader/ShaderSource.test.ts | 119 -------- packages/webgpu/src/Shader/ShaderSource.ts | 30 -- .../src/Shader/wgsl/common/SharedWgsl.ts | 10 - 18 files changed, 26 insertions(+), 1451 deletions(-) delete mode 100644 packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.test.ts delete mode 100644 packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.ts delete mode 100644 packages/webgpu/src/SamplerCache.test.ts delete mode 100644 packages/webgpu/src/SamplerCache.ts delete mode 100644 packages/webgpu/src/SamplerCache/service/SamplerCacheCreateCommonSamplersService.test.ts delete mode 100644 packages/webgpu/src/SamplerCache/service/SamplerCacheCreateCommonSamplersService.ts delete mode 100644 packages/webgpu/src/SamplerCache/service/SamplerCacheGenerateKeyService.test.ts delete mode 100644 packages/webgpu/src/SamplerCache/service/SamplerCacheGenerateKeyService.ts delete mode 100644 packages/webgpu/src/SamplerCache/service/SamplerCacheGetOrCreateService.test.ts delete mode 100644 packages/webgpu/src/SamplerCache/service/SamplerCacheGetOrCreateService.ts diff --git a/packages/webgpu/src/AttachmentManager.test.ts b/packages/webgpu/src/AttachmentManager.test.ts index 2db615c6..19a459b2 100644 --- a/packages/webgpu/src/AttachmentManager.test.ts +++ b/packages/webgpu/src/AttachmentManager.test.ts @@ -27,16 +27,6 @@ vi.mock("./AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase", "execute": vi.fn() })); -vi.mock("./AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService", () => ({ - "execute": vi.fn((attachment, r, g, b, a, loadOp) => ({ - "colorAttachments": [{ - "view": attachment.texture?.view, - "clearValue": { r, g, b, a }, - "loadOp": loadOp, - "storeOp": "store" - }] - })) -})); describe("AttachmentManager", () => { @@ -65,13 +55,6 @@ describe("AttachmentManager", () => expect(manager).toBeDefined(); }); - it("should initialize with null current attachment", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - - expect(manager.getCurrentAttachment()).toBeNull(); - }); }); describe("getAttachmentObject", () => @@ -119,68 +102,16 @@ describe("AttachmentManager", () => }); }); - describe("bindAttachment", () => + describe("bindAttachment / unbindAttachment", () => { - it("should set current attachment", () => + it("should bind and unbind attachment without error", () => { const device = createMockDevice(); const manager = new AttachmentManager(device); const attachment = manager.getAttachmentObject(100, 100); - manager.bindAttachment(attachment); - - expect(manager.getCurrentAttachment()).toBe(attachment); - }); - }); - - describe("getCurrentAttachment", () => - { - it("should return null before binding", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - - expect(manager.getCurrentAttachment()).toBeNull(); - }); - - it("should return bound attachment", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - const attachment = manager.getAttachmentObject(100, 100); - - manager.bindAttachment(attachment); - - expect(manager.getCurrentAttachment()).toBe(attachment); - }); - }); - - describe("currentAttachmentObject", () => - { - it("should return same as getCurrentAttachment", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - const attachment = manager.getAttachmentObject(100, 100); - - manager.bindAttachment(attachment); - - expect(manager.currentAttachmentObject).toBe(manager.getCurrentAttachment()); - }); - }); - - describe("unbindAttachment", () => - { - it("should set current attachment to null", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - const attachment = manager.getAttachmentObject(100, 100); - - manager.bindAttachment(attachment); - manager.unbindAttachment(); - - expect(manager.getCurrentAttachment()).toBeNull(); + expect(() => manager.bindAttachment(attachment)).not.toThrow(); + expect(() => manager.unbindAttachment()).not.toThrow(); }); }); @@ -196,51 +127,6 @@ describe("AttachmentManager", () => }); }); - describe("createRenderPassDescriptor", () => - { - it("should create descriptor with clear color", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - const attachment = manager.getAttachmentObject(100, 100); - - const descriptor = manager.createRenderPassDescriptor( - attachment, 0.5, 0.5, 0.5, 1.0, "clear" - ); - - expect(descriptor.colorAttachments).toBeDefined(); - expect((descriptor.colorAttachments as any)[0].clearValue).toEqual({ - "r": 0.5, "g": 0.5, "b": 0.5, "a": 1.0 - }); - }); - - it("should use clear as default loadOp", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - const attachment = manager.getAttachmentObject(100, 100); - - const descriptor = manager.createRenderPassDescriptor( - attachment, 0, 0, 0, 0 - ); - - expect((descriptor.colorAttachments as any)[0].loadOp).toBe("clear"); - }); - - it("should accept load as loadOp", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - const attachment = manager.getAttachmentObject(100, 100); - - const descriptor = manager.createRenderPassDescriptor( - attachment, 0, 0, 0, 0, "load" - ); - - expect((descriptor.colorAttachments as any)[0].loadOp).toBe("load"); - }); - }); - describe("dispose", () => { it("should not throw when disposing empty manager", () => diff --git a/packages/webgpu/src/AttachmentManager.ts b/packages/webgpu/src/AttachmentManager.ts index 107ab1ba..b931875b 100644 --- a/packages/webgpu/src/AttachmentManager.ts +++ b/packages/webgpu/src/AttachmentManager.ts @@ -4,7 +4,6 @@ import type { IColorBufferObject } from "./interface/IColorBufferObject"; import type { IStencilBufferObject } from "./interface/IStencilBufferObject"; import { execute as attachmentManagerGetAttachmentObjectUseCase } from "./AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase"; import { execute as attachmentManagerReleaseAttachmentUseCase } from "./AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase"; -import { execute as attachmentManagerCreateRenderPassDescriptorService } from "./AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService"; export class AttachmentManager { @@ -51,16 +50,6 @@ export class AttachmentManager this.currentAttachment = attachment; } - getCurrentAttachment(): IAttachmentObject | null - { - return this.currentAttachment; - } - - get currentAttachmentObject(): IAttachmentObject | null - { - return this.currentAttachment; - } - unbindAttachment(): void { this.currentAttachment = null; @@ -77,25 +66,6 @@ export class AttachmentManager ); } - createRenderPassDescriptor( - attachment: IAttachmentObject, - r: number, - g: number, - b: number, - a: number, - loadOp: GPULoadOp = "clear" - ): GPURenderPassDescriptor - { - return attachmentManagerCreateRenderPassDescriptorService( - attachment, - r, - g, - b, - a, - loadOp - ); - } - dispose(): void { for (const pool of this.texturePool.values()) { diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.test.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.test.ts deleted file mode 100644 index 7cc3038c..00000000 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { describe, it, expect } from "vitest"; -import type { IAttachmentObject } from "../../interface/IAttachmentObject"; -import { execute } from "./AttachmentManagerCreateRenderPassDescriptorService"; - -describe("AttachmentManagerCreateRenderPassDescriptorService", () => -{ - const createMockAttachment = ( - hasColorView: boolean = true, - hasTextureView: boolean = false, - hasStencil: boolean = false - ): IAttachmentObject => ({ - "id": 1, - "width": 256, - "height": 256, - "color": hasColorView ? { "view": { "label": "colorView" } } : null, - "texture": hasTextureView ? { "view": { "label": "textureView" } } : null, - "stencil": hasStencil ? { "view": { "label": "stencilView" } } : null - } as unknown as IAttachmentObject); - - describe("color attachments", () => - { - it("should use color.view when available", () => - { - const attachment = createMockAttachment(true, false, false); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.colorAttachments[0].view).toEqual({ "label": "colorView" }); - }); - - it("should fallback to texture.view when color.view is not available", () => - { - const attachment = createMockAttachment(false, true, false); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.colorAttachments[0].view).toEqual({ "label": "textureView" }); - }); - - it("should throw error when no color view available", () => - { - const attachment = createMockAttachment(false, false, false); - - expect(() => execute(attachment, 0, 0, 0, 1, "clear")).toThrow( - "No color view available for render pass" - ); - }); - - it("should set clearValue with provided RGBA values", () => - { - const attachment = createMockAttachment(true); - - const result = execute(attachment, 0.5, 0.6, 0.7, 0.8, "clear"); - - expect(result.colorAttachments[0].clearValue).toEqual({ - "r": 0.5, - "g": 0.6, - "b": 0.7, - "a": 0.8 - }); - }); - - it("should set loadOp to provided value", () => - { - const attachment = createMockAttachment(true); - - const result = execute(attachment, 0, 0, 0, 1, "load"); - - expect(result.colorAttachments[0].loadOp).toBe("load"); - }); - - it("should default loadOp to clear", () => - { - const attachment = createMockAttachment(true); - - const result = execute(attachment, 0, 0, 0, 1); - - expect(result.colorAttachments[0].loadOp).toBe("clear"); - }); - - it("should set storeOp to store", () => - { - const attachment = createMockAttachment(true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.colorAttachments[0].storeOp).toBe("store"); - }); - }); - - describe("depth stencil attachment", () => - { - it("should include depthStencilAttachment when stencil is available", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment).toBeDefined(); - }); - - it("should not include depthStencilAttachment when stencil is not available", () => - { - const attachment = createMockAttachment(true, false, false); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment).toBeUndefined(); - }); - - it("should set stencil view correctly", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment?.view).toEqual({ "label": "stencilView" }); - }); - - it("should set depth clear value to 1.0", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment?.depthClearValue).toBe(1.0); - }); - - it("should set stencil clear value to 0", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment?.stencilClearValue).toBe(0); - }); - - it("should set depthLoadOp to clear", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment?.depthLoadOp).toBe("clear"); - }); - - it("should set depthStoreOp to store", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment?.depthStoreOp).toBe("store"); - }); - - it("should set stencilLoadOp to clear", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment?.stencilLoadOp).toBe("clear"); - }); - - it("should set stencilStoreOp to store", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment?.stencilStoreOp).toBe("store"); - }); - }); -}); diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.ts deleted file mode 100644 index 77628233..00000000 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { IAttachmentObject } from "../../interface/IAttachmentObject"; - -const $clearValue: GPUColorDict = { "r": 0, "g": 0, "b": 0, "a": 0 }; -const $colorAttachment: GPURenderPassColorAttachment = { - "view": null as unknown as GPUTextureView, - "loadOp": "clear", - "storeOp": "store", - "clearValue": $clearValue -}; -const $depthStencilAttachment: GPURenderPassDepthStencilAttachment = { - "view": null as unknown as GPUTextureView, - "depthLoadOp": "clear", - "depthStoreOp": "store", - "depthClearValue": 1.0, - "stencilLoadOp": "clear", - "stencilStoreOp": "store", - "stencilClearValue": 0 -}; -const $descriptor: GPURenderPassDescriptor = { - "colorAttachments": [$colorAttachment] -}; - -/** - * @description レンダーパスディスクリプタを作成(プリアロケート再利用) - */ -export const execute = ( - attachment: IAttachmentObject, - r: number, - g: number, - b: number, - a: number, - loadOp: GPULoadOp = "clear" -): GPURenderPassDescriptor => { - const colorView = attachment.color?.view ?? attachment.texture?.view; - if (!colorView) { - throw new Error("No color view available for render pass"); - } - $colorAttachment.view = colorView; - $colorAttachment.loadOp = loadOp; - $clearValue.r = r; - $clearValue.g = g; - $clearValue.b = b; - $clearValue.a = a; - if (attachment.stencil?.view) { - $depthStencilAttachment.view = attachment.stencil.view; - $descriptor.depthStencilAttachment = $depthStencilAttachment; - } else { - $descriptor.depthStencilAttachment = undefined; - } - return $descriptor; -}; diff --git a/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts b/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts index 74b4980c..1c66d814 100644 --- a/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts @@ -16,6 +16,26 @@ import { $getAtlasAttachmentObject } from "../../AtlasManager"; let $cachedBindGroup: GPUBindGroup | null = null; let $cachedAtlasView: GPUTextureView | null = null; +/** + * @description ブレンドモードに応じたインスタンスパイプライン名を返す + */ +const $getPipelineName = (mode: IBlendMode): string => { + switch (mode) { + case "add": + return "instanced_add"; + case "screen": + return "instanced_screen"; + case "alpha": + return "instanced_alpha"; + case "erase": + return "instanced_erase"; + case "copy": + return "instanced_copy"; + default: + return "instanced"; + } +}; + export const execute = ( device: GPUDevice, command_encoder: GPUCommandEncoder, @@ -44,27 +64,7 @@ export const execute = ( // 現在のブレンドモードを取得 const blendMode: IBlendMode = $currentBlendMode; - // ブレンドモードに応じたパイプライン名を生成 - // simpleBlendModes: normal, layer, add, screen, alpha, erase, copy - const getPipelineName = (mode: IBlendMode): string => { - switch (mode) { - case "add": - return "instanced_add"; - case "screen": - return "instanced_screen"; - case "alpha": - return "instanced_alpha"; - case "erase": - return "instanced_erase"; - case "copy": - return "instanced_copy"; - default: - // normal, layer - return "instanced"; - } - }; - - const pipelineName = getPipelineName(blendMode); + const pipelineName = $getPipelineName(blendMode); const normalPipeline = pipeline_manager.getPipeline(pipelineName); const maskedPipeline = pipeline_manager.getPipeline("instanced_masked"); diff --git a/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.test.ts b/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.test.ts index d1c60792..35f52f10 100644 --- a/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.test.ts +++ b/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.test.ts @@ -1,10 +1,8 @@ import { describe, it, expect, vi } from "vitest"; import type { IPath } from "../../interface/IPath"; -import type { IPoint } from "../../interface/IPoint"; import { generateStrokeOutline, - generateStrokeMesh, - generateStrokeMeshFromPoints + generateStrokeMesh } from "./MeshStrokeGenerateUseCase"; // Mock $context @@ -182,69 +180,4 @@ describe("MeshStrokeGenerateUseCase", () => }); }); - describe("generateStrokeMeshFromPoints", () => - { - it("should return Float32Array", () => - { - const paths: IPoint[][] = [[ - { "x": 0, "y": 0 }, - { "x": 100, "y": 0 } - ]]; - - const result = generateStrokeMeshFromPoints(paths, 10); - - expect(result).toBeInstanceOf(Float32Array); - }); - - it("should generate triangles for line segment", () => - { - const paths: IPoint[][] = [[ - { "x": 0, "y": 0 }, - { "x": 100, "y": 0 } - ]]; - - const result = generateStrokeMeshFromPoints(paths, 10); - - // 2 triangles * 3 vertices * 4 floats = 24 - expect(result.length).toBe(24); - }); - - it("should skip paths with single point", () => - { - const paths: IPoint[][] = [[ - { "x": 0, "y": 0 } - ]]; - - const result = generateStrokeMeshFromPoints(paths, 10); - - expect(result.length).toBe(0); - }); - - it("should handle multi-segment path", () => - { - const paths: IPoint[][] = [[ - { "x": 0, "y": 0 }, - { "x": 100, "y": 0 }, - { "x": 100, "y": 100 } - ]]; - - const result = generateStrokeMeshFromPoints(paths, 10); - - // 2 line segments * 24 floats each = 48 - expect(result.length).toBe(48); - }); - - it("should handle multiple separate paths", () => - { - const paths: IPoint[][] = [ - [{ "x": 0, "y": 0 }, { "x": 100, "y": 0 }], - [{ "x": 200, "y": 200 }, { "x": 300, "y": 200 }] - ]; - - const result = generateStrokeMeshFromPoints(paths, 10); - - // 2 paths * 1 line segment each * 24 floats = 48 - expect(result.length).toBe(48); - }); - }); }); diff --git a/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.ts b/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.ts index 04ed9b4b..c828f442 100644 --- a/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.ts +++ b/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.ts @@ -793,59 +793,3 @@ export const generateStrokeMesh = (vertices: IPath[], thickness: number): IPath[ return fillVertices; }; -/** - * @description IPoint[][]形式からストロークメッシュを生成(後方互換用) - * @param {IPoint[][]} paths - パス配列 - * @param {number} thickness - 線の太さ - * @return {Float32Array} - */ -export const generateStrokeMeshFromPoints = (paths: IPoint[][], thickness: number): Float32Array => -{ - const triangles: number[] = []; - - // WebGL版と同じ: 内部で半分にする - const halfThickness = thickness / 2; - - for (const path of paths) { - if (path.length < 2) { continue } - - // 各線分に対して矩形を生成 - for (let i = 0; i < path.length - 1; i++) { - const startPoint = path[i]; - const endPoint = path[i + 1]; - - const vector: IPoint = { - "x": endPoint.x - startPoint.x, - "y": endPoint.y - startPoint.y - }; - - const normal = calculateNormalVector(vector.x, vector.y, halfThickness); - - // 矩形の4頂点 - const p0x = startPoint.x + normal.x; - const p0y = startPoint.y + normal.y; - const p1x = endPoint.x + normal.x; - const p1y = endPoint.y + normal.y; - const p2x = endPoint.x - normal.x; - const p2y = endPoint.y - normal.y; - const p3x = startPoint.x - normal.x; - const p3y = startPoint.y - normal.y; - - // Triangle 1: p0, p1, p2 - triangles.push( - p0x, p0y, 0, 0, - p1x, p1y, 0, 0, - p2x, p2y, 0, 0 - ); - - // Triangle 2: p0, p2, p3 - triangles.push( - p0x, p0y, 0, 0, - p2x, p2y, 0, 0, - p3x, p3y, 0, 0 - ); - } - } - - return new Float32Array(triangles); -}; diff --git a/packages/webgpu/src/SamplerCache.test.ts b/packages/webgpu/src/SamplerCache.test.ts deleted file mode 100644 index ff829ec7..00000000 --- a/packages/webgpu/src/SamplerCache.test.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { - SamplerCache, - initSamplerCache, - getSamplerCache, - clearSamplerCache -} from "./SamplerCache"; - -// Mock the service modules -vi.mock("./SamplerCache/service/SamplerCacheGetOrCreateService", () => ({ - "execute": vi.fn((device, cache, minFilter, magFilter, addressModeU, addressModeV) => { - const key = `${minFilter}_${magFilter}_${addressModeU}_${addressModeV}`; - if (!cache.has(key)) { - const sampler = { "label": key } as unknown as GPUSampler; - cache.set(key, sampler); - } - return cache.get(key); - }) -})); - -vi.mock("./SamplerCache/service/SamplerCacheCreateCommonSamplersService", () => ({ - "execute": vi.fn((device, cache) => { - // Pre-create common samplers - cache.set("linear_linear_clamp-to-edge_clamp-to-edge", { "label": "linearClamp" }); - cache.set("nearest_nearest_clamp-to-edge_clamp-to-edge", { "label": "nearestClamp" }); - cache.set("linear_linear_repeat_repeat", { "label": "linearRepeat" }); - cache.set("nearest_nearest_repeat_repeat", { "label": "nearestRepeat" }); - }) -})); - -describe("SamplerCache", () => -{ - const createMockDevice = (): GPUDevice => - { - return { - "createSampler": vi.fn(() => ({ "label": "mockSampler" })) - } as unknown as GPUDevice; - }; - - beforeEach(() => - { - vi.clearAllMocks(); - clearSamplerCache(); - }); - - describe("SamplerCache class", () => - { - it("should create instance with device", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - expect(cache).toBeDefined(); - }); - - it("should pre-create common samplers on initialization", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const stats = cache.getStats(); - expect(stats.size).toBe(4); - }); - - describe("getOrCreate", () => - { - it("should get existing sampler from cache", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler1 = cache.getOrCreate("linear", "linear", "clamp-to-edge", "clamp-to-edge"); - const sampler2 = cache.getOrCreate("linear", "linear", "clamp-to-edge", "clamp-to-edge"); - - expect(sampler1).toBe(sampler2); - }); - - it("should create new sampler for new combination", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const initialSize = cache.getStats().size; - - cache.getOrCreate("linear", "nearest", "mirror-repeat", "clamp-to-edge"); - - expect(cache.getStats().size).toBe(initialSize + 1); - }); - }); - - describe("getLinearClamp", () => - { - it("should return linear clamp sampler", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getLinearClamp(); - - expect(sampler).toBeDefined(); - }); - - it("should return same instance on multiple calls", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler1 = cache.getLinearClamp(); - const sampler2 = cache.getLinearClamp(); - - expect(sampler1).toBe(sampler2); - }); - }); - - describe("getNearestClamp", () => - { - it("should return nearest clamp sampler", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getNearestClamp(); - - expect(sampler).toBeDefined(); - }); - }); - - describe("getLinearRepeat", () => - { - it("should return linear repeat sampler", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getLinearRepeat(); - - expect(sampler).toBeDefined(); - }); - }); - - describe("getNearestRepeat", () => - { - it("should return nearest repeat sampler", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getNearestRepeat(); - - expect(sampler).toBeDefined(); - }); - }); - - describe("getBySmoothRepeat", () => - { - it("should return linear clamp for smooth=true, repeat=false", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getBySmoothRepeat(true, false); - const linearClamp = cache.getLinearClamp(); - - expect(sampler).toBe(linearClamp); - }); - - it("should return nearest clamp for smooth=false, repeat=false", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getBySmoothRepeat(false, false); - const nearestClamp = cache.getNearestClamp(); - - expect(sampler).toBe(nearestClamp); - }); - - it("should return linear repeat for smooth=true, repeat=true", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getBySmoothRepeat(true, true); - const linearRepeat = cache.getLinearRepeat(); - - expect(sampler).toBe(linearRepeat); - }); - - it("should return nearest repeat for smooth=false, repeat=true", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getBySmoothRepeat(false, true); - const nearestRepeat = cache.getNearestRepeat(); - - expect(sampler).toBe(nearestRepeat); - }); - }); - - describe("getStats", () => - { - it("should return cache size", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const stats = cache.getStats(); - - expect(stats).toHaveProperty("size"); - expect(typeof stats.size).toBe("number"); - }); - }); - - describe("dispose", () => - { - it("should clear cache", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - cache.dispose(); - - expect(cache.getStats().size).toBe(0); - }); - }); - }); - - describe("global functions", () => - { - describe("initSamplerCache", () => - { - it("should initialize global cache", () => - { - const device = createMockDevice(); - - initSamplerCache(device); - - expect(getSamplerCache()).not.toBeNull(); - }); - }); - - describe("getSamplerCache", () => - { - it("should return cache after initialization", () => - { - const device = createMockDevice(); - initSamplerCache(device); - - expect(getSamplerCache()).toBeInstanceOf(SamplerCache); - }); - }); - - describe("clearSamplerCache", () => - { - it("should dispose cache", () => - { - const device = createMockDevice(); - initSamplerCache(device); - const cache = getSamplerCache(); - - clearSamplerCache(); - - expect(cache!.getStats().size).toBe(0); - }); - - it("should not throw when cache is null", () => - { - expect(() => clearSamplerCache()).not.toThrow(); - }); - }); - }); -}); diff --git a/packages/webgpu/src/SamplerCache.ts b/packages/webgpu/src/SamplerCache.ts deleted file mode 100644 index c67cd168..00000000 --- a/packages/webgpu/src/SamplerCache.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { execute as samplerCacheGetOrCreateService } from "./SamplerCache/service/SamplerCacheGetOrCreateService"; -import { execute as samplerCacheCreateCommonSamplersService } from "./SamplerCache/service/SamplerCacheCreateCommonSamplersService"; - -export class SamplerCache -{ - private device: GPUDevice; - private cache: Map; - - constructor(device: GPUDevice) - { - this.device = device; - this.cache = new Map(); - - samplerCacheCreateCommonSamplersService(device, this.cache); - } - - getOrCreate( - minFilter: GPUFilterMode, - magFilter: GPUFilterMode, - addressModeU: GPUAddressMode, - addressModeV: GPUAddressMode - ): GPUSampler { - return samplerCacheGetOrCreateService( - this.device, - this.cache, - minFilter, - magFilter, - addressModeU, - addressModeV - ); - } - - getLinearClamp(): GPUSampler - { - return this.getOrCreate("linear", "linear", "clamp-to-edge", "clamp-to-edge"); - } - - getNearestClamp(): GPUSampler - { - return this.getOrCreate("nearest", "nearest", "clamp-to-edge", "clamp-to-edge"); - } - - getLinearRepeat(): GPUSampler - { - return this.getOrCreate("linear", "linear", "repeat", "repeat"); - } - - getNearestRepeat(): GPUSampler - { - return this.getOrCreate("nearest", "nearest", "repeat", "repeat"); - } - - getBySmoothRepeat(smooth: boolean, repeat: boolean): GPUSampler - { - const filter: GPUFilterMode = smooth ? "linear" : "nearest"; - const addressMode: GPUAddressMode = repeat ? "repeat" : "clamp-to-edge"; - return this.getOrCreate(filter, filter, addressMode, addressMode); - } - - getStats(): { size: number } - { - return { - "size": this.cache.size - }; - } - - dispose(): void - { - this.cache.clear(); - } -} - -let $samplerCache: SamplerCache | null = null; - -export const initSamplerCache = (device: GPUDevice): void => -{ - $samplerCache = new SamplerCache(device); -}; - -export const getSamplerCache = (): SamplerCache | null => -{ - return $samplerCache; -}; - -export const clearSamplerCache = (): void => -{ - if ($samplerCache) { - $samplerCache.dispose(); - } -}; diff --git a/packages/webgpu/src/SamplerCache/service/SamplerCacheCreateCommonSamplersService.test.ts b/packages/webgpu/src/SamplerCache/service/SamplerCacheCreateCommonSamplersService.test.ts deleted file mode 100644 index 3f74a905..00000000 --- a/packages/webgpu/src/SamplerCache/service/SamplerCacheCreateCommonSamplersService.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { execute } from "./SamplerCacheCreateCommonSamplersService"; - -describe("SamplerCacheCreateCommonSamplersService", () => -{ - const createMockDevice = () => - { - let samplerId = 0; - return { - "createSampler": vi.fn(() => ({ "id": ++samplerId })) - } as unknown as GPUDevice; - }; - - it("should create 5 common samplers", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - expect(cache.size).toBe(5); - }); - - it("should create linear clamp sampler", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - expect(cache.has("linear_linear_clamp-to-edge_clamp-to-edge")).toBe(true); - }); - - it("should create nearest clamp sampler", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - expect(cache.has("nearest_nearest_clamp-to-edge_clamp-to-edge")).toBe(true); - }); - - it("should create linear repeat sampler", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - expect(cache.has("linear_linear_repeat_repeat")).toBe(true); - }); - - it("should create nearest repeat sampler", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - expect(cache.has("nearest_nearest_repeat_repeat")).toBe(true); - }); - - it("should create linear mirror repeat sampler", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - expect(cache.has("linear_linear_mirror-repeat_mirror-repeat")).toBe(true); - }); - - it("should not overwrite existing samplers", () => - { - const device = createMockDevice(); - const cache = new Map(); - const existingSampler = { "id": "existing" } as unknown as GPUSampler; - cache.set("linear_linear_clamp-to-edge_clamp-to-edge", existingSampler); - - execute(device, cache); - - expect(cache.get("linear_linear_clamp-to-edge_clamp-to-edge")).toBe(existingSampler); - }); - - it("should call createSampler with correct parameters", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - // Verify first call (linear clamp) - expect(device.createSampler).toHaveBeenCalledWith({ - "minFilter": "linear", - "magFilter": "linear", - "addressModeU": "clamp-to-edge", - "addressModeV": "clamp-to-edge" - }); - - // Verify nearest clamp was called - expect(device.createSampler).toHaveBeenCalledWith({ - "minFilter": "nearest", - "magFilter": "nearest", - "addressModeU": "clamp-to-edge", - "addressModeV": "clamp-to-edge" - }); - }); - - it("should only call createSampler 5 times for empty cache", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - expect(device.createSampler).toHaveBeenCalledTimes(5); - }); - - it("should be idempotent when called multiple times", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - const sizeAfterFirst = cache.size; - const callsAfterFirst = (device.createSampler as any).mock.calls.length; - - execute(device, cache); - - expect(cache.size).toBe(sizeAfterFirst); - expect(device.createSampler).toHaveBeenCalledTimes(callsAfterFirst); - }); -}); diff --git a/packages/webgpu/src/SamplerCache/service/SamplerCacheCreateCommonSamplersService.ts b/packages/webgpu/src/SamplerCache/service/SamplerCacheCreateCommonSamplersService.ts deleted file mode 100644 index c7b27a3b..00000000 --- a/packages/webgpu/src/SamplerCache/service/SamplerCacheCreateCommonSamplersService.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { execute as samplerCacheGenerateKeyService } from "./SamplerCacheGenerateKeyService"; - -/** - * @description 頻繁に使用されるサンプラーを事前に作成 - * Pre-create commonly used samplers - * - * @param {GPUDevice} device - * @param {Map} cache - * @return {void} - * @method - * @protected - */ -export const execute = ( - device: GPUDevice, - cache: Map -): void => { - const createAndCache = ( - minFilter: GPUFilterMode, - magFilter: GPUFilterMode, - addressModeU: GPUAddressMode, - addressModeV: GPUAddressMode - ): void => { - const key = samplerCacheGenerateKeyService(minFilter, magFilter, addressModeU, addressModeV); - - if (!cache.has(key)) { - const sampler = device.createSampler({ - minFilter, - magFilter, - addressModeU, - addressModeV - }); - cache.set(key, sampler); - } - }; - - // リニアクランプ(最も一般的) - createAndCache("linear", "linear", "clamp-to-edge", "clamp-to-edge"); - - // ニアレストクランプ - createAndCache("nearest", "nearest", "clamp-to-edge", "clamp-to-edge"); - - // リニアリピート - createAndCache("linear", "linear", "repeat", "repeat"); - - // ニアレストリピート - createAndCache("nearest", "nearest", "repeat", "repeat"); - - // リニアミラーリピート - createAndCache("linear", "linear", "mirror-repeat", "mirror-repeat"); -}; diff --git a/packages/webgpu/src/SamplerCache/service/SamplerCacheGenerateKeyService.test.ts b/packages/webgpu/src/SamplerCache/service/SamplerCacheGenerateKeyService.test.ts deleted file mode 100644 index f36950b6..00000000 --- a/packages/webgpu/src/SamplerCache/service/SamplerCacheGenerateKeyService.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { execute } from "./SamplerCacheGenerateKeyService"; - -describe("SamplerCacheGenerateKeyService", () => -{ - it("should generate key with all linear filters", () => - { - const result = execute("linear", "linear", "clamp-to-edge", "clamp-to-edge"); - expect(result).toBe("linear_linear_clamp-to-edge_clamp-to-edge"); - }); - - it("should generate key with all nearest filters", () => - { - const result = execute("nearest", "nearest", "repeat", "repeat"); - expect(result).toBe("nearest_nearest_repeat_repeat"); - }); - - it("should generate key with mixed filters", () => - { - const result = execute("linear", "nearest", "clamp-to-edge", "repeat"); - expect(result).toBe("linear_nearest_clamp-to-edge_repeat"); - }); - - it("should generate unique keys for different configurations", () => - { - const key1 = execute("linear", "linear", "clamp-to-edge", "clamp-to-edge"); - const key2 = execute("nearest", "nearest", "clamp-to-edge", "clamp-to-edge"); - const key3 = execute("linear", "linear", "repeat", "repeat"); - - expect(key1).not.toBe(key2); - expect(key1).not.toBe(key3); - expect(key2).not.toBe(key3); - }); - - it("should generate same key for same configuration", () => - { - const key1 = execute("linear", "nearest", "repeat", "mirror-repeat"); - const key2 = execute("linear", "nearest", "repeat", "mirror-repeat"); - - expect(key1).toBe(key2); - }); - - it("should handle mirror-repeat address mode", () => - { - const result = execute("linear", "linear", "mirror-repeat", "mirror-repeat"); - expect(result).toBe("linear_linear_mirror-repeat_mirror-repeat"); - }); - - it("should differentiate address modes U and V", () => - { - const key1 = execute("linear", "linear", "repeat", "clamp-to-edge"); - const key2 = execute("linear", "linear", "clamp-to-edge", "repeat"); - - expect(key1).not.toBe(key2); - }); -}); diff --git a/packages/webgpu/src/SamplerCache/service/SamplerCacheGenerateKeyService.ts b/packages/webgpu/src/SamplerCache/service/SamplerCacheGenerateKeyService.ts deleted file mode 100644 index 06c33e10..00000000 --- a/packages/webgpu/src/SamplerCache/service/SamplerCacheGenerateKeyService.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @description サンプラーのキーを生成 - * Generate sampler cache key - * - * @param {GPUFilterMode} minFilter - * @param {GPUFilterMode} magFilter - * @param {GPUAddressMode} addressModeU - * @param {GPUAddressMode} addressModeV - * @return {string} - * @method - * @protected - */ -export const execute = ( - minFilter: GPUFilterMode, - magFilter: GPUFilterMode, - addressModeU: GPUAddressMode, - addressModeV: GPUAddressMode -): string => { - return `${minFilter}_${magFilter}_${addressModeU}_${addressModeV}`; -}; diff --git a/packages/webgpu/src/SamplerCache/service/SamplerCacheGetOrCreateService.test.ts b/packages/webgpu/src/SamplerCache/service/SamplerCacheGetOrCreateService.test.ts deleted file mode 100644 index 1f77798e..00000000 --- a/packages/webgpu/src/SamplerCache/service/SamplerCacheGetOrCreateService.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { execute } from "./SamplerCacheGetOrCreateService"; - -describe("SamplerCacheGetOrCreateService", () => -{ - const createMockDevice = () => - { - return { - "createSampler": vi.fn((descriptor) => ({ ...descriptor, "label": "mock-sampler" })) - } as unknown as GPUDevice; - }; - - it("should return cached sampler if exists", () => - { - const device = createMockDevice(); - const cache = new Map(); - const existingSampler = { "label": "existing" } as unknown as GPUSampler; - - // Pre-populate cache with correct key format (underscore separated) - cache.set("linear_linear_clamp-to-edge_clamp-to-edge", existingSampler); - - const result = execute(device, cache, "linear", "linear", "clamp-to-edge", "clamp-to-edge"); - - expect(result).toBe(existingSampler); - expect(device.createSampler).not.toHaveBeenCalled(); - }); - - it("should create new sampler if not cached", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache, "linear", "linear", "clamp-to-edge", "clamp-to-edge"); - - expect(device.createSampler).toHaveBeenCalledWith({ - "minFilter": "linear", - "magFilter": "linear", - "addressModeU": "clamp-to-edge", - "addressModeV": "clamp-to-edge" - }); - }); - - it("should cache newly created sampler", () => - { - const device = createMockDevice(); - const cache = new Map(); - - const result = execute(device, cache, "nearest", "nearest", "repeat", "repeat"); - - expect(cache.has("nearest_nearest_repeat_repeat")).toBe(true); - expect(cache.get("nearest_nearest_repeat_repeat")).toBe(result); - }); - - it("should generate correct cache key", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache, "linear", "nearest", "repeat", "mirror-repeat"); - - expect(cache.has("linear_nearest_repeat_mirror-repeat")).toBe(true); - }); - - it("should return same sampler for same parameters", () => - { - const device = createMockDevice(); - const cache = new Map(); - - const result1 = execute(device, cache, "linear", "linear", "clamp-to-edge", "clamp-to-edge"); - const result2 = execute(device, cache, "linear", "linear", "clamp-to-edge", "clamp-to-edge"); - - expect(result1).toBe(result2); - expect(device.createSampler).toHaveBeenCalledTimes(1); - }); - - it("should create different samplers for different parameters", () => - { - const device = createMockDevice(); - const cache = new Map(); - - const result1 = execute(device, cache, "linear", "linear", "clamp-to-edge", "clamp-to-edge"); - const result2 = execute(device, cache, "nearest", "nearest", "repeat", "repeat"); - - expect(result1).not.toBe(result2); - expect(device.createSampler).toHaveBeenCalledTimes(2); - }); - - it("should handle all filter modes", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache, "linear", "linear", "clamp-to-edge", "clamp-to-edge"); - execute(device, cache, "nearest", "nearest", "clamp-to-edge", "clamp-to-edge"); - - expect(cache.size).toBe(2); - }); - - it("should handle all address modes", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache, "linear", "linear", "clamp-to-edge", "clamp-to-edge"); - execute(device, cache, "linear", "linear", "repeat", "repeat"); - execute(device, cache, "linear", "linear", "mirror-repeat", "mirror-repeat"); - - expect(cache.size).toBe(3); - }); -}); diff --git a/packages/webgpu/src/SamplerCache/service/SamplerCacheGetOrCreateService.ts b/packages/webgpu/src/SamplerCache/service/SamplerCacheGetOrCreateService.ts deleted file mode 100644 index a0d54691..00000000 --- a/packages/webgpu/src/SamplerCache/service/SamplerCacheGetOrCreateService.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { execute as samplerCacheGenerateKeyService } from "./SamplerCacheGenerateKeyService"; - -/** - * @description サンプラーを取得または作成 - * Get or create sampler - * - * @param {GPUDevice} device - * @param {Map} cache - * @param {GPUFilterMode} minFilter - * @param {GPUFilterMode} magFilter - * @param {GPUAddressMode} addressModeU - * @param {GPUAddressMode} addressModeV - * @return {GPUSampler} - * @method - * @protected - */ -export const execute = ( - device: GPUDevice, - cache: Map, - minFilter: GPUFilterMode, - magFilter: GPUFilterMode, - addressModeU: GPUAddressMode, - addressModeV: GPUAddressMode -): GPUSampler => { - const key = samplerCacheGenerateKeyService(minFilter, magFilter, addressModeU, addressModeV); - - const cached = cache.get(key); - if (cached) { - return cached; - } - - const sampler = device.createSampler({ - minFilter, - magFilter, - addressModeU, - addressModeV - }); - - cache.set(key, sampler); - return sampler; -}; diff --git a/packages/webgpu/src/Shader/ShaderSource.test.ts b/packages/webgpu/src/Shader/ShaderSource.test.ts index 2959403f..a6ef2a89 100644 --- a/packages/webgpu/src/Shader/ShaderSource.test.ts +++ b/packages/webgpu/src/Shader/ShaderSource.test.ts @@ -46,32 +46,6 @@ describe("ShaderSource", () => }); }); - describe("getFillMainVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = ShaderSource.getFillMainVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = ShaderSource.getFillMainVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should return same shader as non-Main variant (uses @override yFlipSign)", () => - { - const mainShader = ShaderSource.getFillMainVertexShader(); - const atlasShader = ShaderSource.getFillVertexShader(); - - expect(mainShader).toBe(atlasShader); - }); - }); - describe("getFillFragmentShader", () => { it("should return a valid WGSL fragment shader string", () => @@ -122,32 +96,6 @@ describe("ShaderSource", () => }); }); - describe("getStencilWriteMainVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = ShaderSource.getStencilWriteMainVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = ShaderSource.getStencilWriteMainVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should return same shader as non-Main variant (uses @override yFlipSign)", () => - { - const mainShader = ShaderSource.getStencilWriteMainVertexShader(); - const atlasShader = ShaderSource.getStencilWriteVertexShader(); - - expect(mainShader).toBe(atlasShader); - }); - }); - describe("getStencilWriteFragmentShader", () => { it("should return a valid WGSL fragment shader string", () => @@ -264,32 +212,6 @@ describe("ShaderSource", () => }); }); - describe("getBasicMainVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = ShaderSource.getBasicMainVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = ShaderSource.getBasicMainVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should return same shader as non-Main variant (uses @override yFlipSign)", () => - { - const mainShader = ShaderSource.getBasicMainVertexShader(); - const atlasShader = ShaderSource.getBasicVertexShader(); - - expect(mainShader).toBe(atlasShader); - }); - }); - describe("getBasicFragmentShader", () => { it("should return a valid WGSL fragment shader string", () => @@ -402,24 +324,6 @@ describe("ShaderSource", () => }); }); - describe("getGradientFillMainVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = ShaderSource.getGradientFillMainVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = ShaderSource.getGradientFillMainVertexShader(); - - expect(shader).toContain("@vertex"); - }); - }); - describe("getGradientFillFragmentShader", () => { it("should return a valid WGSL fragment shader string", () => @@ -481,24 +385,6 @@ describe("ShaderSource", () => }); }); - describe("getBitmapFillMainVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = ShaderSource.getBitmapFillMainVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = ShaderSource.getBitmapFillMainVertexShader(); - - expect(shader).toContain("@vertex"); - }); - }); - describe("getBitmapFillFragmentShader", () => { it("should return a valid WGSL fragment shader string", () => @@ -1024,18 +910,13 @@ describe("ShaderSource", () => { const vertexShaders = [ { name: "getFillVertexShader", fn: () => ShaderSource.getFillVertexShader() }, - { name: "getFillMainVertexShader", fn: () => ShaderSource.getFillMainVertexShader() }, { name: "getStencilWriteVertexShader", fn: () => ShaderSource.getStencilWriteVertexShader() }, - { name: "getStencilWriteMainVertexShader", fn: () => ShaderSource.getStencilWriteMainVertexShader() }, { name: "getStencilFillVertexShader", fn: () => ShaderSource.getStencilFillVertexShader() }, { name: "getMaskVertexShader", fn: () => ShaderSource.getMaskVertexShader() }, { name: "getBasicVertexShader", fn: () => ShaderSource.getBasicVertexShader() }, - { name: "getBasicMainVertexShader", fn: () => ShaderSource.getBasicMainVertexShader() }, { name: "getInstancedVertexShader", fn: () => ShaderSource.getInstancedVertexShader() }, { name: "getGradientFillVertexShader", fn: () => ShaderSource.getGradientFillVertexShader() }, - { name: "getGradientFillMainVertexShader", fn: () => ShaderSource.getGradientFillMainVertexShader() }, { name: "getBitmapFillVertexShader", fn: () => ShaderSource.getBitmapFillVertexShader() }, - { name: "getBitmapFillMainVertexShader", fn: () => ShaderSource.getBitmapFillMainVertexShader() }, { name: "getBlurFilterVertexShader", fn: () => ShaderSource.getBlurFilterVertexShader() }, { name: "getNodeClearVertexShader", fn: () => ShaderSource.getNodeClearVertexShader() }, { name: "getPositionedTextureVertexShader", fn: () => ShaderSource.getPositionedTextureVertexShader() } diff --git a/packages/webgpu/src/Shader/ShaderSource.ts b/packages/webgpu/src/Shader/ShaderSource.ts index 824c5f7f..f2563f62 100644 --- a/packages/webgpu/src/Shader/ShaderSource.ts +++ b/packages/webgpu/src/Shader/ShaderSource.ts @@ -43,11 +43,6 @@ export class ShaderSource return FillVertex; } - static getFillMainVertexShader (): string - { - return FillVertex; - } - static getFillFragmentShader (): string { return FillFragment; @@ -58,11 +53,6 @@ export class ShaderSource return StencilWriteVertex; } - static getStencilWriteMainVertexShader (): string - { - return StencilWriteVertex; - } - static getStencilWriteFragmentShader (): string { return StencilWriteFragment; @@ -73,11 +63,6 @@ export class ShaderSource return StencilFillVertex; } - static getStencilFillMainVertexShader (): string - { - return StencilFillVertex; - } - static getStencilFillFragmentShader (): string { return StencilFillFragment; @@ -98,11 +83,6 @@ export class ShaderSource return BasicVertex; } - static getBasicMainVertexShader (): string - { - return BasicVertex; - } - static getBasicFragmentShader (): string { return BasicFragment; @@ -128,11 +108,6 @@ export class ShaderSource return GradientFillVertex; } - static getGradientFillMainVertexShader (): string - { - return GradientFillVertex; - } - static getGradientFillFragmentShader (): string { return GradientFillFragment; @@ -153,11 +128,6 @@ export class ShaderSource return BitmapFillVertex; } - static getBitmapFillMainVertexShader (): string - { - return BitmapFillVertex; - } - static getBitmapFillFragmentShader (): string { return BitmapFillFragment; diff --git a/packages/webgpu/src/Shader/wgsl/common/SharedWgsl.ts b/packages/webgpu/src/Shader/wgsl/common/SharedWgsl.ts index 3e81858f..06380866 100644 --- a/packages/webgpu/src/Shader/wgsl/common/SharedWgsl.ts +++ b/packages/webgpu/src/Shader/wgsl/common/SharedWgsl.ts @@ -29,13 +29,3 @@ struct VertexOutput { @builtin(position) position: vec4, @location(0) texCoord: vec2, }`; - -export const WgslFullscreenTexCoords = ` - const texCoords = array, 6>( - vec2(0.0, 1.0), - vec2(1.0, 1.0), - vec2(0.0, 0.0), - vec2(0.0, 0.0), - vec2(1.0, 1.0), - vec2(1.0, 0.0) - );`; From a1f37332c841a5335ec0162869b19d621edb6528 Mon Sep 17 00:00:00 2001 From: ienaga Date: Sat, 14 Mar 2026 00:21:58 +0900 Subject: [PATCH 03/26] =?UTF-8?q?#260=20webgpu=E7=89=88=E3=81=AE=E3=83=AA?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/webgpu/src/AtlasManager.test.ts | 68 +--- packages/webgpu/src/AtlasManager.ts | 38 -- .../src/BezierConverter/BezierConverter.ts | 6 +- .../src/Blend/service/BlendAddService.test.ts | 67 ---- .../src/Blend/service/BlendAddService.ts | 13 - .../Blend/service/BlendAlphaService.test.ts | 57 --- .../src/Blend/service/BlendAlphaService.ts | 13 - .../Blend/service/BlendEraseService.test.ts | 57 --- .../src/Blend/service/BlendEraseService.ts | 13 - .../service/BlendGetStateService.test.ts | 99 ----- .../src/Blend/service/BlendGetStateService.ts | 17 - .../Blend/service/BlendOneZeroService.test.ts | 57 --- .../src/Blend/service/BlendOneZeroService.ts | 13 - .../Blend/service/BlendResetService.test.ts | 77 ---- .../src/Blend/service/BlendResetService.ts | 13 - .../Blend/service/BlendScreenService.test.ts | 57 --- .../src/Blend/service/BlendScreenService.ts | 13 - .../Blend/service/BlendSetModeService.test.ts | 65 ---- .../src/Blend/service/BlendSetModeService.ts | 7 - .../usecase/BlendOperationUseCase.test.ts | 77 ---- .../Blend/usecase/BlendOperationUseCase.ts | 41 -- packages/webgpu/src/BufferManager.test.ts | 64 +-- packages/webgpu/src/BufferManager.ts | 24 -- .../src/Filter/BevelFilterShader.test.ts | 122 ------ .../webgpu/src/Filter/BevelFilterShader.ts | 118 ------ .../src/Filter/BitmapFilterShader.test.ts | 146 ------- .../webgpu/src/Filter/BitmapFilterShader.ts | 231 ----------- .../src/Filter/BlurFilterShader.test.ts | 138 ------- .../webgpu/src/Filter/BlurFilterShader.ts | 115 ------ .../Filter/ColorMatrixFilterShader.test.ts | 97 ----- .../src/Filter/ColorMatrixFilterShader.ts | 55 --- .../Filter/ConvolutionFilterShader.test.ts | 232 ----------- .../src/Filter/ConvolutionFilterShader.ts | 130 ------- .../DisplacementMapFilterShader.test.ts | 218 ----------- .../src/Filter/DisplacementMapFilterShader.ts | 130 ------- .../src/Filter/DropShadowFilterShader.test.ts | 95 ----- .../src/Filter/DropShadowFilterShader.ts | 97 ----- .../src/Filter/GlowFilterShader.test.ts | 93 ----- .../webgpu/src/Filter/GlowFilterShader.ts | 70 ---- ...ManagerFlushPendingReleasesService.test.ts | 98 ----- ...ufferManagerFlushPendingReleasesService.ts | 21 - .../src/Mask/usecase/MaskBindUseCase.test.ts | 91 ----- .../src/Mask/usecase/MaskBindUseCase.ts | 24 -- .../src/Mesh/service/MeshLerpService.test.ts | 49 --- .../src/Mesh/service/MeshLerpService.ts | 23 -- .../MeshSplitQuadraticBezierUseCase.test.ts | 247 ------------ .../MeshSplitQuadraticBezierUseCase.ts | 37 -- .../MeshStrokeFillGenerateUseCase.test.ts | 125 ------ .../usecase/MeshStrokeFillGenerateUseCase.ts | 70 ---- packages/webgpu/src/PathCommand.test.ts | 131 +++---- packages/webgpu/src/PathCommand.ts | 94 +---- .../webgpu/src/Shader/BlendModeShader.test.ts | 365 ------------------ packages/webgpu/src/Shader/BlendModeShader.ts | 99 ----- .../GradientLUTGenerateDataUseCase.test.ts | 139 ------- .../usecase/GradientLUTGenerateDataUseCase.ts | 43 --- .../src/Shader/wgsl/fragment/BlendFragment.ts | 87 ----- .../src/Shader/wgsl/vertex/FilterVertex.ts | 20 - .../TexturePoolEvictOldestService.test.ts | 114 ------ .../service/TexturePoolEvictOldestService.ts | 28 -- packages/webgpu/src/WebGPUUtil.test.ts | 14 +- packages/webgpu/src/WebGPUUtil.ts | 16 +- .../webgpu/src/interface/IRectangleInfo.ts | 14 - 62 files changed, 54 insertions(+), 4938 deletions(-) delete mode 100644 packages/webgpu/src/Blend/service/BlendAddService.test.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendAddService.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendAlphaService.test.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendAlphaService.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendEraseService.test.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendEraseService.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendGetStateService.test.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendGetStateService.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendOneZeroService.test.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendOneZeroService.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendResetService.test.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendResetService.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendScreenService.test.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendScreenService.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendSetModeService.test.ts delete mode 100644 packages/webgpu/src/Blend/service/BlendSetModeService.ts delete mode 100644 packages/webgpu/src/Blend/usecase/BlendOperationUseCase.test.ts delete mode 100644 packages/webgpu/src/Blend/usecase/BlendOperationUseCase.ts delete mode 100644 packages/webgpu/src/Filter/BevelFilterShader.test.ts delete mode 100644 packages/webgpu/src/Filter/BevelFilterShader.ts delete mode 100644 packages/webgpu/src/Filter/BitmapFilterShader.test.ts delete mode 100644 packages/webgpu/src/Filter/BitmapFilterShader.ts delete mode 100644 packages/webgpu/src/Filter/BlurFilterShader.test.ts delete mode 100644 packages/webgpu/src/Filter/BlurFilterShader.ts delete mode 100644 packages/webgpu/src/Filter/ColorMatrixFilterShader.test.ts delete mode 100644 packages/webgpu/src/Filter/ColorMatrixFilterShader.ts delete mode 100644 packages/webgpu/src/Filter/ConvolutionFilterShader.test.ts delete mode 100644 packages/webgpu/src/Filter/ConvolutionFilterShader.ts delete mode 100644 packages/webgpu/src/Filter/DisplacementMapFilterShader.test.ts delete mode 100644 packages/webgpu/src/Filter/DisplacementMapFilterShader.ts delete mode 100644 packages/webgpu/src/Filter/DropShadowFilterShader.test.ts delete mode 100644 packages/webgpu/src/Filter/DropShadowFilterShader.ts delete mode 100644 packages/webgpu/src/Filter/GlowFilterShader.test.ts delete mode 100644 packages/webgpu/src/Filter/GlowFilterShader.ts delete mode 100644 packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerFlushPendingReleasesService.test.ts delete mode 100644 packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerFlushPendingReleasesService.ts delete mode 100644 packages/webgpu/src/Mask/usecase/MaskBindUseCase.test.ts delete mode 100644 packages/webgpu/src/Mask/usecase/MaskBindUseCase.ts delete mode 100644 packages/webgpu/src/Mesh/service/MeshLerpService.test.ts delete mode 100644 packages/webgpu/src/Mesh/service/MeshLerpService.ts delete mode 100644 packages/webgpu/src/Mesh/usecase/MeshSplitQuadraticBezierUseCase.test.ts delete mode 100644 packages/webgpu/src/Mesh/usecase/MeshSplitQuadraticBezierUseCase.ts delete mode 100644 packages/webgpu/src/Mesh/usecase/MeshStrokeFillGenerateUseCase.test.ts delete mode 100644 packages/webgpu/src/Mesh/usecase/MeshStrokeFillGenerateUseCase.ts delete mode 100644 packages/webgpu/src/Shader/BlendModeShader.test.ts delete mode 100644 packages/webgpu/src/Shader/BlendModeShader.ts delete mode 100644 packages/webgpu/src/Shader/GradientLUTGenerator/usecase/GradientLUTGenerateDataUseCase.test.ts delete mode 100644 packages/webgpu/src/Shader/GradientLUTGenerator/usecase/GradientLUTGenerateDataUseCase.ts delete mode 100644 packages/webgpu/src/Shader/wgsl/fragment/BlendFragment.ts delete mode 100644 packages/webgpu/src/TexturePool/service/TexturePoolEvictOldestService.test.ts delete mode 100644 packages/webgpu/src/TexturePool/service/TexturePoolEvictOldestService.ts delete mode 100644 packages/webgpu/src/interface/IRectangleInfo.ts diff --git a/packages/webgpu/src/AtlasManager.test.ts b/packages/webgpu/src/AtlasManager.test.ts index daa5c803..f12688a6 100644 --- a/packages/webgpu/src/AtlasManager.test.ts +++ b/packages/webgpu/src/AtlasManager.test.ts @@ -7,10 +7,7 @@ import { $getAtlasAttachmentObject, $hasAtlasAttachmentObject, $rootNodes, - $setAtlasTexture, - $getAtlasTexture, $getActiveTransferBounds, - $getActiveAllTransferBounds, $clearTransferBounds, $setCurrentAtlasIndex, $getCurrentAtlasIndex, @@ -133,44 +130,6 @@ describe("AtlasManager", () => }); }); - describe("$setAtlasTexture / $getAtlasTexture", () => - { - it("should set and get atlas texture", () => - { - const mockTexture = { - "id": 1, - "width": 4096, - "height": 4096, - "area": 4096 * 4096, - "smooth": true, - "resource": {}, - "view": {} - } as any; - - $setAtlasTexture(mockTexture); - - expect($getAtlasTexture()).toBe(mockTexture); - }); - - it("should allow setting to null", () => - { - const mockTexture = { - "id": 1, - "width": 4096, - "height": 4096, - "area": 4096 * 4096, - "smooth": true, - "resource": {}, - "view": {} - } as any; - - $setAtlasTexture(mockTexture); - $setAtlasTexture(null); - - expect($getAtlasTexture()).toBeNull(); - }); - }); - describe("$getActiveTransferBounds", () => { it("should create transfer bounds at index", () => @@ -209,28 +168,6 @@ describe("AtlasManager", () => }); }); - describe("$getActiveAllTransferBounds", () => - { - it("should create all transfer bounds at index", () => - { - const bounds = $getActiveAllTransferBounds(0); - - expect(bounds).toBeInstanceOf(Float32Array); - expect(bounds.length).toBe(4); - }); - - it("should initialize with max/min values", () => - { - const bounds = $getActiveAllTransferBounds(0); - - // Float32Array stores Number.MAX_VALUE as Infinity - expect(bounds[0]).toBe(Infinity); - expect(bounds[1]).toBe(Infinity); - expect(bounds[2]).toBe(-Infinity); - expect(bounds[3]).toBe(-Infinity); - }); - }); - describe("$clearTransferBounds", () => { it("should reset transfer bounds to initial values", () => @@ -250,22 +187,19 @@ describe("AtlasManager", () => expect(bounds[3]).toBe(-Infinity); }); - it("should reset all transfer bounds arrays", () => + it("should reset multiple transfer bounds arrays", () => { const bounds0 = $getActiveTransferBounds(0); const bounds1 = $getActiveTransferBounds(1); - const allBounds0 = $getActiveAllTransferBounds(0); bounds0[0] = 50; bounds1[0] = 60; - allBounds0[0] = 70; $clearTransferBounds(); // Float32Array stores Number.MAX_VALUE as Infinity expect(bounds0[0]).toBe(Infinity); expect(bounds1[0]).toBe(Infinity); - expect(allBounds0[0]).toBe(Infinity); }); }); diff --git a/packages/webgpu/src/AtlasManager.ts b/packages/webgpu/src/AtlasManager.ts index 9bfd2cb1..51203474 100644 --- a/packages/webgpu/src/AtlasManager.ts +++ b/packages/webgpu/src/AtlasManager.ts @@ -1,5 +1,4 @@ import type { IAttachmentObject } from "./interface/IAttachmentObject"; -import type { ITextureObject } from "./interface/ITextureObject"; import type { TexturePacker } from "@next2d/texture-packer"; const $MAX_VALUE: number = Number.MAX_VALUE; @@ -66,18 +65,6 @@ export const $hasAtlasAttachmentObject = (): boolean => export const $rootNodes: TexturePacker[] = []; -export let $atlasTexture: ITextureObject | null = null; - -export const $setAtlasTexture = (texture_object: ITextureObject | null): void => -{ - $atlasTexture = texture_object; -}; - -export const $getAtlasTexture = (): ITextureObject | null => -{ - return $atlasTexture; -}; - const $transferBounds: Float32Array[] = []; export const $getActiveTransferBounds = (index: number): Float32Array => @@ -93,21 +80,6 @@ export const $getActiveTransferBounds = (index: number): Float32Array => return $transferBounds[index]; }; -const $allTransferBounds: Float32Array[] = []; - -export const $getActiveAllTransferBounds = (index: number): Float32Array => -{ - if (!(index in $allTransferBounds)) { - $allTransferBounds[index] = new Float32Array([ - $MAX_VALUE, - $MAX_VALUE, - $MIN_VALUE, - $MIN_VALUE - ]); - } - return $allTransferBounds[index]; -}; - export const $clearTransferBounds = (): void => { for (let idx = 0; idx < $transferBounds.length; ++idx) { @@ -119,16 +91,6 @@ export const $clearTransferBounds = (): void => bounds[0] = bounds[1] = $MAX_VALUE; bounds[2] = bounds[3] = $MIN_VALUE; } - - for (let idx = 0; idx < $allTransferBounds.length; ++idx) { - const bounds = $allTransferBounds[idx]; - if (!bounds) { - continue; - } - - bounds[0] = bounds[1] = $MAX_VALUE; - bounds[2] = bounds[3] = $MIN_VALUE; - } }; let $currentAtlasIndex: number = 0; diff --git a/packages/webgpu/src/BezierConverter/BezierConverter.ts b/packages/webgpu/src/BezierConverter/BezierConverter.ts index 80c670a4..e2563c7d 100644 --- a/packages/webgpu/src/BezierConverter/BezierConverter.ts +++ b/packages/webgpu/src/BezierConverter/BezierConverter.ts @@ -11,10 +11,6 @@ * これにより品質を維持しながら不要な計算を削減。 */ export { - execute as adaptiveCubicToQuad, - calculateAdaptiveThreshold + execute as adaptiveCubicToQuad } from "./usecase/BezierConverterAdaptiveCubicToQuadUseCase"; export type { IQuadraticSegment } from "../interface/IQuadraticSegment"; - -export { execute as calculateFlatness } from "./service/BezierConverterCalculateFlatnessService"; -export { execute as splitCubic } from "./service/BezierConverterSplitCubicService"; diff --git a/packages/webgpu/src/Blend/service/BlendAddService.test.ts b/packages/webgpu/src/Blend/service/BlendAddService.test.ts deleted file mode 100644 index 02261765..00000000 --- a/packages/webgpu/src/Blend/service/BlendAddService.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { execute } from "./BlendAddService"; -import { $setFuncCode, $funcCode } from "../../Blend"; - -describe("BlendAddService", () => -{ - beforeEach(() => - { - // Reset to non-add state - $setFuncCode(0); - }); - - it("should return true when func code is different", () => - { - $setFuncCode(0); - - const result = execute(); - - expect(result).toBe(true); - }); - - it("should set func code to 101 (add)", () => - { - $setFuncCode(0); - - execute(); - - expect($funcCode).toBe(101); - }); - - it("should return false when already set to add (101)", () => - { - $setFuncCode(101); - - const result = execute(); - - expect(result).toBe(false); - }); - - it("should not change func code when already 101", () => - { - $setFuncCode(101); - - execute(); - - expect($funcCode).toBe(101); - }); - - it("should return true when changing from another mode", () => - { - $setFuncCode(301); // screen mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(101); - }); - - it("should return true when changing from normal mode", () => - { - $setFuncCode(613); // normal mode - - const result = execute(); - - expect(result).toBe(true); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendAddService.ts b/packages/webgpu/src/Blend/service/BlendAddService.ts deleted file mode 100644 index 07affca6..00000000 --- a/packages/webgpu/src/Blend/service/BlendAddService.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - $setFuncCode, - $funcCode -} from "../../Blend"; - -export const execute = (): boolean => -{ - if ($funcCode !== 101) { - $setFuncCode(101); - return true; - } - return false; -}; diff --git a/packages/webgpu/src/Blend/service/BlendAlphaService.test.ts b/packages/webgpu/src/Blend/service/BlendAlphaService.test.ts deleted file mode 100644 index 56edb947..00000000 --- a/packages/webgpu/src/Blend/service/BlendAlphaService.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { execute } from "./BlendAlphaService"; -import { $setFuncCode, $funcCode } from "../../Blend"; - -describe("BlendAlphaService", () => -{ - beforeEach(() => - { - $setFuncCode(0); - }); - - it("should return true when func code is different", () => - { - $setFuncCode(0); - - const result = execute(); - - expect(result).toBe(true); - }); - - it("should set func code to 401 (alpha)", () => - { - $setFuncCode(0); - - execute(); - - expect($funcCode).toBe(401); - }); - - it("should return false when already set to alpha (401)", () => - { - $setFuncCode(401); - - const result = execute(); - - expect(result).toBe(false); - }); - - it("should not change func code when already 401", () => - { - $setFuncCode(401); - - execute(); - - expect($funcCode).toBe(401); - }); - - it("should return true when changing from normal mode", () => - { - $setFuncCode(613); // normal mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(401); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendAlphaService.ts b/packages/webgpu/src/Blend/service/BlendAlphaService.ts deleted file mode 100644 index 7f5617aa..00000000 --- a/packages/webgpu/src/Blend/service/BlendAlphaService.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - $setFuncCode, - $funcCode -} from "../../Blend"; - -export const execute = (): boolean => -{ - if ($funcCode !== 401) { - $setFuncCode(401); - return true; - } - return false; -}; diff --git a/packages/webgpu/src/Blend/service/BlendEraseService.test.ts b/packages/webgpu/src/Blend/service/BlendEraseService.test.ts deleted file mode 100644 index 196dd414..00000000 --- a/packages/webgpu/src/Blend/service/BlendEraseService.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { execute } from "./BlendEraseService"; -import { $setFuncCode, $funcCode } from "../../Blend"; - -describe("BlendEraseService", () => -{ - beforeEach(() => - { - $setFuncCode(0); - }); - - it("should return true when func code is different", () => - { - $setFuncCode(0); - - const result = execute(); - - expect(result).toBe(true); - }); - - it("should set func code to 501 (erase)", () => - { - $setFuncCode(0); - - execute(); - - expect($funcCode).toBe(501); - }); - - it("should return false when already set to erase (501)", () => - { - $setFuncCode(501); - - const result = execute(); - - expect(result).toBe(false); - }); - - it("should not change func code when already 501", () => - { - $setFuncCode(501); - - execute(); - - expect($funcCode).toBe(501); - }); - - it("should return true when changing from screen mode", () => - { - $setFuncCode(301); // screen mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(501); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendEraseService.ts b/packages/webgpu/src/Blend/service/BlendEraseService.ts deleted file mode 100644 index 86ee87fd..00000000 --- a/packages/webgpu/src/Blend/service/BlendEraseService.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - $setFuncCode, - $funcCode -} from "../../Blend"; - -export const execute = (): boolean => -{ - if ($funcCode !== 501) { - $setFuncCode(501); - return true; - } - return false; -}; diff --git a/packages/webgpu/src/Blend/service/BlendGetStateService.test.ts b/packages/webgpu/src/Blend/service/BlendGetStateService.test.ts deleted file mode 100644 index dde24480..00000000 --- a/packages/webgpu/src/Blend/service/BlendGetStateService.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { execute } from "./BlendGetStateService"; - -describe("BlendGetStateService", () => -{ - describe("normal blend mode", () => - { - it("should return correct blend state for normal mode", () => - { - const result = execute("normal"); - - expect(result.color.srcFactor).toBe("one"); - expect(result.color.dstFactor).toBe("one-minus-src-alpha"); - expect(result.color.operation).toBe("add"); - expect(result.alpha.srcFactor).toBe("one"); - expect(result.alpha.dstFactor).toBe("one-minus-src-alpha"); - expect(result.alpha.operation).toBe("add"); - }); - }); - - describe("add blend mode", () => - { - it("should return correct blend state for add mode", () => - { - const result = execute("add"); - - expect(result.color.srcFactor).toBe("one"); - expect(result.color.dstFactor).toBe("one"); - expect(result.color.operation).toBe("add"); - expect(result.alpha.srcFactor).toBe("one"); - expect(result.alpha.dstFactor).toBe("one-minus-src-alpha"); - }); - }); - - describe("screen blend mode", () => - { - it("should return correct blend state for screen mode", () => - { - const result = execute("screen"); - - expect(result.color.srcFactor).toBe("one-minus-dst"); - expect(result.color.dstFactor).toBe("one"); - expect(result.color.operation).toBe("add"); - }); - }); - - describe("alpha blend mode", () => - { - it("should return correct blend state for alpha mode", () => - { - const result = execute("alpha"); - - expect(result.color.srcFactor).toBe("zero"); - expect(result.color.dstFactor).toBe("src-alpha"); - expect(result.color.operation).toBe("add"); - expect(result.alpha.srcFactor).toBe("zero"); - expect(result.alpha.dstFactor).toBe("src-alpha"); - }); - }); - - describe("erase blend mode", () => - { - it("should return correct blend state for erase mode", () => - { - const result = execute("erase"); - - expect(result.color.srcFactor).toBe("zero"); - expect(result.color.dstFactor).toBe("one-minus-src-alpha"); - expect(result.color.operation).toBe("add"); - expect(result.alpha.srcFactor).toBe("zero"); - expect(result.alpha.dstFactor).toBe("one-minus-src-alpha"); - }); - }); - - describe("copy blend mode", () => - { - it("should return correct blend state for copy mode", () => - { - const result = execute("copy"); - - expect(result.color.srcFactor).toBe("one"); - expect(result.color.dstFactor).toBe("zero"); - expect(result.color.operation).toBe("add"); - expect(result.alpha.srcFactor).toBe("one"); - expect(result.alpha.dstFactor).toBe("zero"); - }); - }); - - describe("default behavior", () => - { - it("should return normal state for unknown mode", () => - { - const result = execute("unknown" as any); - - expect(result.color.srcFactor).toBe("one"); - expect(result.color.dstFactor).toBe("one-minus-src-alpha"); - }); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendGetStateService.ts b/packages/webgpu/src/Blend/service/BlendGetStateService.ts deleted file mode 100644 index 5f8535eb..00000000 --- a/packages/webgpu/src/Blend/service/BlendGetStateService.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { IBlendMode } from "../../interface/IBlendMode"; -import type { IBlendState } from "../../Blend"; -import { $getBlendState } from "../../Blend"; - -/** - * @description ブレンドモードからWebGPUブレンドステートを取得するサービス - * Service to get WebGPU blend state from blend mode - * - * @param {IBlendMode} mode - * @return {IBlendState} - * @method - * @protected - */ -export const execute = (mode: IBlendMode): IBlendState => -{ - return $getBlendState(mode); -}; diff --git a/packages/webgpu/src/Blend/service/BlendOneZeroService.test.ts b/packages/webgpu/src/Blend/service/BlendOneZeroService.test.ts deleted file mode 100644 index 4b4083fe..00000000 --- a/packages/webgpu/src/Blend/service/BlendOneZeroService.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { execute } from "./BlendOneZeroService"; -import { $setFuncCode, $funcCode } from "../../Blend"; - -describe("BlendOneZeroService", () => -{ - beforeEach(() => - { - $setFuncCode(0); - }); - - it("should return true when func code is different", () => - { - $setFuncCode(100); - - const result = execute(); - - expect(result).toBe(true); - }); - - it("should set func code to 10 (copy/one-zero)", () => - { - $setFuncCode(100); - - execute(); - - expect($funcCode).toBe(10); - }); - - it("should return false when already set to one-zero (10)", () => - { - $setFuncCode(10); - - const result = execute(); - - expect(result).toBe(false); - }); - - it("should not change func code when already 10", () => - { - $setFuncCode(10); - - execute(); - - expect($funcCode).toBe(10); - }); - - it("should return true when changing from normal mode", () => - { - $setFuncCode(613); // normal mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(10); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendOneZeroService.ts b/packages/webgpu/src/Blend/service/BlendOneZeroService.ts deleted file mode 100644 index 108cac13..00000000 --- a/packages/webgpu/src/Blend/service/BlendOneZeroService.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - $setFuncCode, - $funcCode -} from "../../Blend"; - -export const execute = (): boolean => -{ - if ($funcCode !== 10) { - $setFuncCode(10); - return true; - } - return false; -}; diff --git a/packages/webgpu/src/Blend/service/BlendResetService.test.ts b/packages/webgpu/src/Blend/service/BlendResetService.test.ts deleted file mode 100644 index c9ccf537..00000000 --- a/packages/webgpu/src/Blend/service/BlendResetService.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { execute } from "./BlendResetService"; -import { $setFuncCode, $funcCode } from "../../Blend"; - -describe("BlendResetService", () => -{ - beforeEach(() => - { - $setFuncCode(0); - }); - - it("should return true when func code is not 613 (normal)", () => - { - $setFuncCode(101); // add mode - - const result = execute(); - - expect(result).toBe(true); - }); - - it("should set func code to 613 (normal)", () => - { - $setFuncCode(101); // add mode - - execute(); - - expect($funcCode).toBe(613); - }); - - it("should return false when already set to normal (613)", () => - { - $setFuncCode(613); - - const result = execute(); - - expect(result).toBe(false); - }); - - it("should not change func code when already 613", () => - { - $setFuncCode(613); - - execute(); - - expect($funcCode).toBe(613); - }); - - it("should return true when resetting from screen mode", () => - { - $setFuncCode(301); // screen mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(613); - }); - - it("should return true when resetting from erase mode", () => - { - $setFuncCode(501); // erase mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(613); - }); - - it("should return true when resetting from one-zero mode", () => - { - $setFuncCode(10); // one-zero (copy) mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(613); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendResetService.ts b/packages/webgpu/src/Blend/service/BlendResetService.ts deleted file mode 100644 index aff3f081..00000000 --- a/packages/webgpu/src/Blend/service/BlendResetService.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - $setFuncCode, - $funcCode -} from "../../Blend"; - -export const execute = (): boolean => -{ - if ($funcCode !== 613) { - $setFuncCode(613); - return true; - } - return false; -}; diff --git a/packages/webgpu/src/Blend/service/BlendScreenService.test.ts b/packages/webgpu/src/Blend/service/BlendScreenService.test.ts deleted file mode 100644 index ad266f30..00000000 --- a/packages/webgpu/src/Blend/service/BlendScreenService.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { execute } from "./BlendScreenService"; -import { $setFuncCode, $funcCode } from "../../Blend"; - -describe("BlendScreenService", () => -{ - beforeEach(() => - { - $setFuncCode(0); - }); - - it("should return true when func code is different", () => - { - $setFuncCode(0); - - const result = execute(); - - expect(result).toBe(true); - }); - - it("should set func code to 301 (screen)", () => - { - $setFuncCode(0); - - execute(); - - expect($funcCode).toBe(301); - }); - - it("should return false when already set to screen (301)", () => - { - $setFuncCode(301); - - const result = execute(); - - expect(result).toBe(false); - }); - - it("should not change func code when already 301", () => - { - $setFuncCode(301); - - execute(); - - expect($funcCode).toBe(301); - }); - - it("should return true when changing from add mode", () => - { - $setFuncCode(101); // add mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(301); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendScreenService.ts b/packages/webgpu/src/Blend/service/BlendScreenService.ts deleted file mode 100644 index 53acf82c..00000000 --- a/packages/webgpu/src/Blend/service/BlendScreenService.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - $setFuncCode, - $funcCode -} from "../../Blend"; - -export const execute = (): boolean => -{ - if ($funcCode !== 301) { - $setFuncCode(301); - return true; - } - return false; -}; diff --git a/packages/webgpu/src/Blend/service/BlendSetModeService.test.ts b/packages/webgpu/src/Blend/service/BlendSetModeService.test.ts deleted file mode 100644 index b8b0d3f5..00000000 --- a/packages/webgpu/src/Blend/service/BlendSetModeService.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { execute } from "./BlendSetModeService"; -import { $currentBlendMode, $setCurrentBlendMode } from "../../Blend"; - -describe("BlendSetModeService", () => -{ - beforeEach(() => - { - $setCurrentBlendMode("normal"); - }); - - it("should set blend mode to normal", () => - { - execute("normal"); - - expect($currentBlendMode).toBe("normal"); - }); - - it("should set blend mode to add", () => - { - execute("add"); - - expect($currentBlendMode).toBe("add"); - }); - - it("should set blend mode to screen", () => - { - execute("screen"); - - expect($currentBlendMode).toBe("screen"); - }); - - it("should set blend mode to alpha", () => - { - execute("alpha"); - - expect($currentBlendMode).toBe("alpha"); - }); - - it("should set blend mode to erase", () => - { - execute("erase"); - - expect($currentBlendMode).toBe("erase"); - }); - - it("should set blend mode to copy", () => - { - execute("copy"); - - expect($currentBlendMode).toBe("copy"); - }); - - it("should change mode from one to another", () => - { - execute("add"); - expect($currentBlendMode).toBe("add"); - - execute("screen"); - expect($currentBlendMode).toBe("screen"); - - execute("normal"); - expect($currentBlendMode).toBe("normal"); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendSetModeService.ts b/packages/webgpu/src/Blend/service/BlendSetModeService.ts deleted file mode 100644 index 3162d9f5..00000000 --- a/packages/webgpu/src/Blend/service/BlendSetModeService.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { IBlendMode } from "../../interface/IBlendMode"; -import { $setCurrentBlendMode } from "../../Blend"; - -export const execute = (mode: IBlendMode): void => -{ - $setCurrentBlendMode(mode); -}; diff --git a/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.test.ts b/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.test.ts deleted file mode 100644 index 3d74804c..00000000 --- a/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { execute } from "./BlendOperationUseCase"; -import { describe, expect, it, beforeEach } from "vitest"; -import { $setFuncCode } from "../../Blend"; - -describe("BlendOperationUseCase.ts method test", () => -{ - beforeEach(() => - { - // Reset func code before each test - $setFuncCode(0); - }); - - it("test case - add blend mode", () => - { - const changed = execute("add"); - expect(changed).toBe(true); - - // Second call should return false (no change) - const changed2 = execute("add"); - expect(changed2).toBe(false); - }); - - it("test case - screen blend mode", () => - { - const changed = execute("screen"); - expect(changed).toBe(true); - - const changed2 = execute("screen"); - expect(changed2).toBe(false); - }); - - it("test case - alpha blend mode", () => - { - const changed = execute("alpha"); - expect(changed).toBe(true); - - const changed2 = execute("alpha"); - expect(changed2).toBe(false); - }); - - it("test case - erase blend mode", () => - { - const changed = execute("erase"); - expect(changed).toBe(true); - - const changed2 = execute("erase"); - expect(changed2).toBe(false); - }); - - it("test case - copy blend mode", () => - { - const changed = execute("copy"); - expect(changed).toBe(true); - - const changed2 = execute("copy"); - expect(changed2).toBe(false); - }); - - it("test case - normal blend mode (default)", () => - { - const changed = execute("normal"); - expect(changed).toBe(true); - - const changed2 = execute("normal"); - expect(changed2).toBe(false); - }); - - it("test case - switching between modes", () => - { - execute("add"); - const changed = execute("screen"); - expect(changed).toBe(true); - - const changed2 = execute("normal"); - expect(changed2).toBe(true); - }); -}); diff --git a/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.ts b/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.ts deleted file mode 100644 index d7101eb0..00000000 --- a/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { IBlendMode } from "../../interface/IBlendMode"; -import { execute as blendAddService } from "../service/BlendAddService"; -import { execute as blendResetService } from "../service/BlendResetService"; -import { execute as blendScreenService } from "../service/BlendScreenService"; -import { execute as blendAlphaService } from "../service/BlendAlphaService"; -import { execute as blendEraseService } from "../service/BlendEraseService"; -import { execute as blendOneZeroService } from "../service/BlendOneZeroService"; - -/** - * @description 設定されたブレンドモードへ切り替える - * Switch to the set blend mode - * - * @param {IBlendMode} operation - * @return {boolean} ブレンドモードが変更されたかどうか - * @method - * @protected - */ -export const execute = (operation: IBlendMode): boolean => -{ - switch (operation) { - - case "add": - return blendAddService(); - - case "screen": - return blendScreenService(); - - case "alpha": - return blendAlphaService(); - - case "erase": - return blendEraseService(); - - case "copy": - return blendOneZeroService(); - - default: - return blendResetService(); - - } -}; diff --git a/packages/webgpu/src/BufferManager.test.ts b/packages/webgpu/src/BufferManager.test.ts index a557244a..ed36bf52 100644 --- a/packages/webgpu/src/BufferManager.test.ts +++ b/packages/webgpu/src/BufferManager.test.ts @@ -118,15 +118,6 @@ describe("BufferManager", () => expect(manager.getFrameNumber()).toBe(0); }); - it("should initialize with empty pool stats", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - const stats = manager.getPoolStats(); - expect(stats.vertexPoolSize).toBe(0); - expect(stats.uniformPoolSize).toBe(0); - }); }); describe("createVertexBuffer", () => @@ -253,16 +244,6 @@ describe("BufferManager", () => expect(buffer).toBeDefined(); }); - it("should update pool stats", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.acquireVertexBuffer(256); - - const stats = manager.getPoolStats(); - expect(stats.vertexPoolSize).toBe(1); - }); }); describe("releaseVertexBuffer", () => @@ -289,16 +270,6 @@ describe("BufferManager", () => expect(buffer).toBeDefined(); }); - it("should update pool stats", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.acquireUniformBuffer(64); - - const stats = manager.getPoolStats(); - expect(stats.uniformPoolSize).toBe(1); - }); }); describe("releaseUniformBuffer", () => @@ -354,20 +325,6 @@ describe("BufferManager", () => expect(manager.getUniformBuffer("u1")).toBeUndefined(); }); - it("should reset pool stats", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.acquireVertexBuffer(256); - manager.acquireUniformBuffer(64); - - manager.dispose(); - - const stats = manager.getPoolStats(); - expect(stats.vertexPoolSize).toBe(0); - expect(stats.uniformPoolSize).toBe(0); - }); }); describe("acquireStorageBuffer", () => @@ -382,17 +339,6 @@ describe("BufferManager", () => expect(buffer).toBeDefined(); }); - it("should update storage pool stats", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.acquireStorageBuffer(1024); - - const stats = manager.getStoragePoolStats(); - expect(stats.storagePoolSize).toBe(1); - expect(stats.storagePoolInUse).toBe(1); - }); }); describe("releaseStorageBuffer", () => @@ -432,10 +378,7 @@ describe("BufferManager", () => manager.acquireStorageBuffer(256); manager.acquireStorageBuffer(512); - manager.releaseAllStorageBuffers(); - - const stats = manager.getStoragePoolStats(); - expect(stats.storagePoolInUse).toBe(0); + expect(() => manager.releaseAllStorageBuffers()).not.toThrow(); }); }); @@ -469,10 +412,7 @@ describe("BufferManager", () => const manager = new BufferManager(device); manager.acquireStorageBuffer(256); - manager.clearFrameBuffers(); - - const stats = manager.getStoragePoolStats(); - expect(stats.storagePoolInUse).toBe(0); + expect(() => manager.clearFrameBuffers()).not.toThrow(); }); }); diff --git a/packages/webgpu/src/BufferManager.ts b/packages/webgpu/src/BufferManager.ts index 4b78913d..89b93aa8 100644 --- a/packages/webgpu/src/BufferManager.ts +++ b/packages/webgpu/src/BufferManager.ts @@ -347,22 +347,6 @@ export class BufferManager this.dynamicUniform.dispose(); } - getPoolStats (): { vertexPoolSize: number; uniformPoolSize: number } - { - let vertexCount = 0; - for (const bucket of this.vertexBufferBuckets.values()) { - vertexCount += bucket.length; - } - let uniformCount = 0; - for (const bucket of this.uniformBufferBuckets.values()) { - uniformCount += bucket.length; - } - return { - "vertexPoolSize": vertexCount, - "uniformPoolSize": uniformCount - }; - } - clearFrameBuffers (): void { for (const buffer of this.vertexBuffers.values()) { @@ -506,12 +490,4 @@ export class BufferManager return this.frameNumber; } - getStoragePoolStats (): { storagePoolSize: number; storagePoolInUse: number } - { - const inUse = this.storageBufferPool.filter((e) => e.inUse).length; - return { - "storagePoolSize": this.storageBufferPool.length, - "storagePoolInUse": inUse - }; - } } diff --git a/packages/webgpu/src/Filter/BevelFilterShader.test.ts b/packages/webgpu/src/Filter/BevelFilterShader.test.ts deleted file mode 100644 index ec27301e..00000000 --- a/packages/webgpu/src/Filter/BevelFilterShader.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { getBevelFilterFragmentShader, getBevelFilterShaderKey } from "./BevelFilterShader"; - -describe("BevelFilterShader", () => -{ - describe("getBevelFilterFragmentShader", () => - { - it("should return a valid WGSL shader string for full type", () => - { - const shader = getBevelFilterFragmentShader("full", false, false); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should return a valid shader for inner type", () => - { - const shader = getBevelFilterFragmentShader("inner", false, false); - - expect(shader).toContain("filterColor * baseAlpha"); - }); - - it("should return a valid shader for outer type", () => - { - const shader = getBevelFilterFragmentShader("outer", false, false); - - expect(shader).toContain("filterColor * (1.0 - baseAlpha)"); - }); - - it("should contain @vertex attribute", () => - { - const shader = getBevelFilterFragmentShader("full", false, false); - - expect(shader).toContain("@vertex"); - }); - - it("should contain @fragment attribute", () => - { - const shader = getBevelFilterFragmentShader("full", false, false); - - expect(shader).toContain("@fragment"); - }); - - it("should define BevelUniforms struct", () => - { - const shader = getBevelFilterFragmentShader("full", false, false); - - expect(shader).toContain("struct BevelUniforms"); - }); - - it("should include highlight and shadow colors", () => - { - const shader = getBevelFilterFragmentShader("full", false, false); - - expect(shader).toContain("highlightColor"); - expect(shader).toContain("shadowColor"); - }); - - it("should handle knockout mode", () => - { - const shader = getBevelFilterFragmentShader("full", true, false); - - expect(shader).toContain("finalColor"); - }); - - it("should include gradient texture binding when isGradient is true", () => - { - const shader = getBevelFilterFragmentShader("full", false, true); - - expect(shader).toContain("gradientTexture"); - }); - - it("should not include gradient texture binding when isGradient is false", () => - { - const shader = getBevelFilterFragmentShader("full", false, false); - - expect(shader).not.toContain("gradientTexture"); - }); - - it("should use gradient LUT when isGradient is true", () => - { - const shader = getBevelFilterFragmentShader("full", false, true); - - expect(shader).toContain("gradientCoord"); - }); - }); - - describe("getBevelFilterShaderKey", () => - { - it("should generate unique key for full type", () => - { - const key = getBevelFilterShaderKey("full", false, false); - - expect(key).toBe("bevel_full_nko_ng"); - }); - - it("should generate unique key for inner type with knockout", () => - { - const key = getBevelFilterShaderKey("inner", true, false); - - expect(key).toBe("bevel_inner_ko_ng"); - }); - - it("should generate unique key for outer type with gradient", () => - { - const key = getBevelFilterShaderKey("outer", false, true); - - expect(key).toBe("bevel_outer_nko_g"); - }); - - it("should generate different keys for different configurations", () => - { - const key1 = getBevelFilterShaderKey("full", false, false); - const key2 = getBevelFilterShaderKey("full", true, false); - const key3 = getBevelFilterShaderKey("full", false, true); - - expect(key1).not.toBe(key2); - expect(key1).not.toBe(key3); - expect(key2).not.toBe(key3); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/BevelFilterShader.ts b/packages/webgpu/src/Filter/BevelFilterShader.ts deleted file mode 100644 index 5c390985..00000000 --- a/packages/webgpu/src/Filter/BevelFilterShader.ts +++ /dev/null @@ -1,118 +0,0 @@ -export const getBevelFilterFragmentShader = ( - type: string, - knockout: boolean, - isGradient: boolean -): string => { - const isInner = type === "inner"; - const isOuter = type === "outer"; - - const gradientBinding = isGradient ? ` -@group(0) @binding(4) var gradientTexture: texture_2d;` : ""; - - const colorCalculation = isGradient ? ` - let gradientCoord = vec2(blurAlpha, 0.5); - var filterColor = textureSample(gradientTexture, sourceSampler, gradientCoord); -` : ` - let highlightWeight = clamp(blurAlpha * 2.0, 0.0, 1.0); - let shadowWeight = clamp((1.0 - blurAlpha) * 2.0, 0.0, 1.0); - var filterColor = uniforms.highlightColor * highlightWeight + uniforms.shadowColor * shadowWeight; -`; - - let typeProcessing = ""; - if (isInner) { - typeProcessing = ` - let baseAlpha = textureSample(baseTexture, sourceSampler, baseTexCoord).a; - filterColor = filterColor * baseAlpha; - ${knockout ? "let finalColor = filterColor;" : "let finalColor = mix(baseColor, filterColor, filterColor.a);"} -`; - } else if (isOuter) { - typeProcessing = ` - let baseAlpha = textureSample(baseTexture, sourceSampler, baseTexCoord).a; - filterColor = filterColor * (1.0 - baseAlpha); - ${knockout ? "let finalColor = filterColor;" : "let finalColor = filterColor + baseColor * (1.0 - filterColor.a);"} -`; - } else { - typeProcessing = knockout ? ` - let finalColor = filterColor; -` : ` - let finalColor = filterColor + baseColor * (1.0 - filterColor.a); -`; - } - - return ` -struct BevelUniforms { - blurTexCoordScale: vec2, - blurTexCoordOffset: vec2, - baseTexCoordScale: vec2, - baseTexCoordOffset: vec2, - strength: f32, - _pad1: f32, - _pad2: f32, - _pad3: f32, - highlightColor: vec4, - shadowColor: vec4, -} - -@group(0) @binding(0) var uniforms: BevelUniforms; -@group(0) @binding(1) var sourceSampler: sampler; -@group(0) @binding(2) var blurTexture: texture_2d; -@group(0) @binding(3) var baseTexture: texture_2d; -${gradientBinding} - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, -} - -@vertex -fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { - var positions = array, 6>( - vec2(-1.0, -1.0), - vec2(1.0, -1.0), - vec2(-1.0, 1.0), - vec2(-1.0, 1.0), - vec2(1.0, -1.0), - vec2(1.0, 1.0) - ); - - var texCoords = array, 6>( - vec2(0.0, 1.0), - vec2(1.0, 1.0), - vec2(0.0, 0.0), - vec2(0.0, 0.0), - vec2(1.0, 1.0), - vec2(1.0, 0.0) - ); - - var output: VertexOutput; - output.position = vec4(positions[vertexIndex], 0.0, 1.0); - output.texCoord = texCoords[vertexIndex]; - return output; -} - -@fragment -fn fs_main(input: VertexOutput) -> @location(0) vec4 { - let blurTexCoord = input.texCoord * uniforms.blurTexCoordScale + uniforms.blurTexCoordOffset; - let baseTexCoord = input.texCoord * uniforms.baseTexCoordScale + uniforms.baseTexCoordOffset; - - let blurColor = textureSample(blurTexture, sourceSampler, blurTexCoord); - var blurAlpha = blurColor.a * uniforms.strength; - blurAlpha = clamp(blurAlpha, 0.0, 1.0); - - let baseColor = textureSample(baseTexture, sourceSampler, baseTexCoord); - - ${colorCalculation} - ${typeProcessing} - - return finalColor; -} -`; -}; - -export const getBevelFilterShaderKey = ( - type: string, - knockout: boolean, - isGradient: boolean -): string => { - return `bevel_${type}_${knockout ? "ko" : "nko"}_${isGradient ? "g" : "ng"}`; -}; diff --git a/packages/webgpu/src/Filter/BitmapFilterShader.test.ts b/packages/webgpu/src/Filter/BitmapFilterShader.test.ts deleted file mode 100644 index 332c8f13..00000000 --- a/packages/webgpu/src/Filter/BitmapFilterShader.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { getBitmapFilterFragmentShader, getBitmapFilterShaderKey } from "./BitmapFilterShader"; - -describe("BitmapFilterShader", () => -{ - describe("getBitmapFilterFragmentShader", () => - { - it("should return a valid WGSL shader string", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false); - - expect(shader).toContain("@vertex"); - }); - - it("should contain @fragment attribute", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false); - - expect(shader).toContain("@fragment"); - }); - - it("should define BitmapFilterUniforms struct", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false); - - expect(shader).toContain("struct BitmapFilterUniforms"); - }); - - it("should include base scale/offset when transformsBase is true", () => - { - const shader = getBitmapFilterFragmentShader(true, false, true, "full", false, false, false); - - expect(shader).toContain("baseScale"); - expect(shader).toContain("baseOffset"); - }); - - it("should include blur scale/offset when transformsBlur is true", () => - { - const shader = getBitmapFilterFragmentShader(false, true, true, "full", false, false, false); - - expect(shader).toContain("blurScale"); - expect(shader).toContain("blurOffset"); - }); - - it("should include strength when appliesStrength is true", () => - { - const shader = getBitmapFilterFragmentShader(false, false, true, "full", false, true, false); - - expect(shader).toContain("strength"); - }); - - it("should include color for glow mode", () => - { - const shader = getBitmapFilterFragmentShader(false, false, true, "full", false, false, false); - - expect(shader).toContain("color"); - }); - - it("should include highlight/shadow colors for bevel mode", () => - { - const shader = getBitmapFilterFragmentShader(false, false, false, "full", false, false, false); - - expect(shader).toContain("highlightColor"); - expect(shader).toContain("shadowColor"); - }); - - it("should include gradient texture when isGradient is true", () => - { - const shader = getBitmapFilterFragmentShader(false, false, true, "full", false, false, true); - - expect(shader).toContain("gradientTexture"); - }); - - it("should handle inner type", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "inner", false, true, false); - - expect(shader).toContain("blur"); - }); - - it("should handle outer type", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "outer", false, true, false); - - expect(shader).toContain("blur"); - }); - - it("should handle knockout mode", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "full", true, true, false); - - expect(shader).toBeDefined(); - }); - - it("should include isInside helper function", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false); - - expect(shader).toContain("fn isInside"); - }); - }); - - describe("getBitmapFilterShaderKey", () => - { - it("should generate unique key for glow configuration", () => - { - const key = getBitmapFilterShaderKey(true, true, true, "full", false, true, false); - - expect(key).toBe("bitmap_yygfullnsso"); - }); - - it("should generate unique key for bevel configuration", () => - { - const key = getBitmapFilterShaderKey(true, true, false, "full", false, true, false); - - expect(key).toBe("bitmap_yybfullnsso"); - }); - - it("should generate different keys for different configurations", () => - { - const key1 = getBitmapFilterShaderKey(true, true, true, "full", false, true, false); - const key2 = getBitmapFilterShaderKey(true, true, true, "inner", false, true, false); - const key3 = getBitmapFilterShaderKey(true, true, true, "full", true, true, false); - - expect(key1).not.toBe(key2); - expect(key1).not.toBe(key3); - }); - - it("should include gradient flag in key", () => - { - const keyNonGradient = getBitmapFilterShaderKey(true, true, true, "full", false, true, false); - const keyGradient = getBitmapFilterShaderKey(true, true, true, "full", false, true, true); - - expect(keyNonGradient).toContain("so"); - expect(keyGradient).toContain("gr"); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/BitmapFilterShader.ts b/packages/webgpu/src/Filter/BitmapFilterShader.ts deleted file mode 100644 index 2e6fe223..00000000 --- a/packages/webgpu/src/Filter/BitmapFilterShader.ts +++ /dev/null @@ -1,231 +0,0 @@ -export const getBitmapFilterFragmentShader = ( - transformsBase: boolean, - transformsBlur: boolean, - isGlow: boolean, - type: string, - knockout: boolean, - appliesStrength: boolean, - isGradient: boolean -): string => { - const isInner = type === "inner"; - - let textureBindingIndex = 2; - const blurTextureBinding = textureBindingIndex++; - const baseTextureBinding = transformsBase ? textureBindingIndex++ : -1; - const gradientTextureBinding = isGradient ? textureBindingIndex++ : -1; - - let uniformsStruct = `struct BitmapFilterUniforms { -`; - if (transformsBase) { - uniformsStruct += ` baseScale: vec2, - baseOffset: vec2, -`; - } - if (transformsBlur) { - uniformsStruct += ` blurScale: vec2, - blurOffset: vec2, -`; - } - if (appliesStrength) { - uniformsStruct += ` strength: f32, - _padStrength: vec3, -`; - } - if (!isGradient) { - if (isGlow) { - uniformsStruct += ` color: vec4, -`; - } else { - uniformsStruct += ` highlightColor: vec4, - shadowColor: vec4, -`; - } - } - uniformsStruct += "}"; - - let textureBindings = ` -@group(0) @binding(0) var uniforms: BitmapFilterUniforms; -@group(0) @binding(1) var sourceSampler: sampler; -@group(0) @binding(${blurTextureBinding}) var blurTexture: texture_2d;`; - - if (transformsBase) { - textureBindings += ` -@group(0) @binding(${baseTextureBinding}) var baseTexture: texture_2d;`; - } - if (isGradient) { - textureBindings += ` -@group(0) @binding(${gradientTextureBinding}) var gradientTexture: texture_2d;`; - } - - let baseStatement = ""; - if (transformsBase) { - baseStatement = ` - let baseScale = uniforms.baseScale; - let baseOffset = uniforms.baseOffset; - let uv = input.texCoord * baseScale - baseOffset; - let base = mix(vec4(0.0), textureSample(baseTexture, sourceSampler, uv), isInside(uv));`; - } - - let blurStatement = ""; - if (transformsBlur) { - blurStatement = ` - let blurScale = uniforms.blurScale; - let blurOffset = uniforms.blurOffset; - let st = input.texCoord * blurScale - blurOffset; - var blur = mix(vec4(0.0), textureSample(blurTexture, sourceSampler, st), isInside(st));`; - } else { - blurStatement = ` - var blur = textureSample(blurTexture, sourceSampler, input.texCoord);`; - } - - let colorStatement = ""; - if (isGlow) { - if (isInner) { - colorStatement += ` - blur.a = 1.0 - blur.a;`; - } - if (appliesStrength) { - colorStatement += ` - let strength = uniforms.strength; - blur.a = clamp(blur.a * strength, 0.0, 1.0);`; - } - if (isGradient) { - colorStatement += ` - blur = textureSample(gradientTexture, sourceSampler, vec2(blur.a, 0.5));`; - } else { - colorStatement += ` - let color = uniforms.color; - blur = color * blur.a;`; - } - } else { - if (transformsBlur) { - colorStatement += ` - let pq = (vec2(1.0) - input.texCoord) * blurScale - blurOffset; - let blur2 = mix(vec4(0.0), textureSample(blurTexture, sourceSampler, pq), isInside(pq));`; - } else { - colorStatement += ` - let blur2 = textureSample(blurTexture, sourceSampler, vec2(1.0) - input.texCoord);`; - } - colorStatement += ` - var highlightAlpha = blur.a - blur2.a; - var shadowAlpha = blur2.a - blur.a;`; - - if (appliesStrength) { - colorStatement += ` - let strength = uniforms.strength; - highlightAlpha = highlightAlpha * strength; - shadowAlpha = shadowAlpha * strength;`; - } - - colorStatement += ` - highlightAlpha = clamp(highlightAlpha, 0.0, 1.0); - shadowAlpha = clamp(shadowAlpha, 0.0, 1.0);`; - - if (isGradient) { - colorStatement += ` - blur = textureSample(gradientTexture, sourceSampler, vec2( - 0.5019607843137255 - 0.5019607843137255 * shadowAlpha + 0.4980392156862745 * highlightAlpha, - 0.5 - ));`; - } else { - colorStatement += ` - let highlightColor = uniforms.highlightColor; - let shadowColor = uniforms.shadowColor; - blur = highlightColor * highlightAlpha + shadowColor * shadowAlpha;`; - } - } - - let modeExpression = ""; - switch (type) { - case "outer": - modeExpression = knockout - ? "blur - blur * base.a" - : "base + blur - blur * base.a"; - break; - case "full": - modeExpression = knockout - ? "blur" - : "base - base * blur.a + blur"; - break; - case "inner": - default: - modeExpression = "blur"; - break; - } - - const needsBase = transformsBase || (type === "outer" || type === "full" && !knockout); - let baseDecl = ""; - if (needsBase && !transformsBase) { - baseDecl = ` - let base = vec4(0.0);`; - } - - return ` -${uniformsStruct} -${textureBindings} - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, -} - -fn isInside(uv: vec2) -> f32 { - let inside = step(vec2(0.0), uv) * step(uv, vec2(1.0)); - return inside.x * inside.y; -} - -@vertex -fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { - var positions = array, 6>( - vec2(-1.0, -1.0), - vec2(1.0, -1.0), - vec2(-1.0, 1.0), - vec2(-1.0, 1.0), - vec2(1.0, -1.0), - vec2(1.0, 1.0) - ); - - var texCoords = array, 6>( - vec2(0.0, 1.0), - vec2(1.0, 1.0), - vec2(0.0, 0.0), - vec2(0.0, 0.0), - vec2(1.0, 1.0), - vec2(1.0, 0.0) - ); - - var output: VertexOutput; - output.position = vec4(positions[vertexIndex], 0.0, 1.0); - output.texCoord = texCoords[vertexIndex]; - return output; -} - -@fragment -fn fs_main(input: VertexOutput) -> @location(0) vec4 { - ${baseDecl} - ${baseStatement} - ${blurStatement} - ${colorStatement} - - return ${modeExpression}; -} -`; -}; - -export const getBitmapFilterShaderKey = ( - transformsBase: boolean, - transformsBlur: boolean, - isGlow: boolean, - type: string, - knockout: boolean, - appliesStrength: boolean, - isGradient: boolean -): string => { - const key1 = transformsBase ? "y" : "n"; - const key2 = transformsBlur ? "y" : "n"; - const key3 = isGlow ? "g" : "b"; - const key4 = knockout ? "k" : "n"; - const key5 = appliesStrength ? "s" : "n"; - const key6 = isGradient ? "gr" : "so"; - return `bitmap_${key1}${key2}${key3}${type}${key4}${key5}${key6}`; -}; diff --git a/packages/webgpu/src/Filter/BlurFilterShader.test.ts b/packages/webgpu/src/Filter/BlurFilterShader.test.ts deleted file mode 100644 index 09e1a83c..00000000 --- a/packages/webgpu/src/Filter/BlurFilterShader.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { BlurFilterShader } from "./BlurFilterShader"; - -describe("BlurFilterShader", () => -{ - describe("getVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = BlurFilterShader.getVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = BlurFilterShader.getVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should contain main function", () => - { - const shader = BlurFilterShader.getVertexShader(); - - expect(shader).toContain("fn main"); - }); - - it("should define VertexInput struct", () => - { - const shader = BlurFilterShader.getVertexShader(); - - expect(shader).toContain("struct VertexInput"); - }); - - it("should define VertexOutput struct", () => - { - const shader = BlurFilterShader.getVertexShader(); - - expect(shader).toContain("struct VertexOutput"); - }); - - it("should include position and texCoord attributes", () => - { - const shader = BlurFilterShader.getVertexShader(); - - expect(shader).toContain("@location(0) position"); - expect(shader).toContain("@location(1) texCoord"); - }); - }); - - describe("getHorizontalBlurShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlurFilterShader.getHorizontalBlurShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlurFilterShader.getHorizontalBlurShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should define BlurUniforms struct", () => - { - const shader = BlurFilterShader.getHorizontalBlurShader(); - - expect(shader).toContain("struct BlurUniforms"); - }); - - it("should include blur parameters", () => - { - const shader = BlurFilterShader.getHorizontalBlurShader(); - - expect(shader).toContain("blurSize"); - expect(shader).toContain("textureWidth"); - }); - - it("should use textureWidth for horizontal sampling", () => - { - const shader = BlurFilterShader.getHorizontalBlurShader(); - - expect(shader).toContain("1.0 / uniforms.textureWidth"); - }); - - it("should include texture sampling", () => - { - const shader = BlurFilterShader.getHorizontalBlurShader(); - - expect(shader).toContain("textureSample"); - }); - }); - - describe("getVerticalBlurShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlurFilterShader.getVerticalBlurShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlurFilterShader.getVerticalBlurShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should define BlurUniforms struct", () => - { - const shader = BlurFilterShader.getVerticalBlurShader(); - - expect(shader).toContain("struct BlurUniforms"); - }); - - it("should use textureHeight for vertical sampling", () => - { - const shader = BlurFilterShader.getVerticalBlurShader(); - - expect(shader).toContain("1.0 / uniforms.textureHeight"); - }); - - it("should include y-axis offset calculation", () => - { - const shader = BlurFilterShader.getVerticalBlurShader(); - - expect(shader).toContain("input.texCoord.y + offset"); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/BlurFilterShader.ts b/packages/webgpu/src/Filter/BlurFilterShader.ts deleted file mode 100644 index c61ba3aa..00000000 --- a/packages/webgpu/src/Filter/BlurFilterShader.ts +++ /dev/null @@ -1,115 +0,0 @@ -export class BlurFilterShader -{ - static getVertexShader(): string - { - return /* wgsl */` - struct VertexInput { - @location(0) position: vec2, - @location(1) texCoord: vec2, - } - - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - @vertex - fn main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - output.position = vec4(input.position, 0.0, 1.0); - output.texCoord = input.texCoord; - return output; - } - `; - } - - static getHorizontalBlurShader(): string - { - return /* wgsl */` - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - struct BlurUniforms { - blurSize: f32, - textureWidth: f32, - textureHeight: f32, - _padding: f32, - } - - @group(0) @binding(0) var uniforms: BlurUniforms; - @group(0) @binding(1) var textureSampler: sampler; - @group(0) @binding(2) var textureData: texture_2d; - - @fragment - fn main(input: VertexOutput) -> @location(0) vec4 { - let texelSize = 1.0 / uniforms.textureWidth; - var color = vec4(0.0); - let blurRadius = i32(uniforms.blurSize); - - var totalWeight = 0.0; - - for (var i = -blurRadius; i <= blurRadius; i++) { - let offset = f32(i) * texelSize; - let weight = 1.0 - abs(f32(i)) / f32(blurRadius + 1); - - let sampleCoord = vec2( - input.texCoord.x + offset, - input.texCoord.y - ); - - color += textureSample(textureData, textureSampler, sampleCoord) * weight; - totalWeight += weight; - } - - return color / totalWeight; - } - `; - } - - static getVerticalBlurShader(): string - { - return /* wgsl */` - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - struct BlurUniforms { - blurSize: f32, - textureWidth: f32, - textureHeight: f32, - _padding: f32, - } - - @group(0) @binding(0) var uniforms: BlurUniforms; - @group(0) @binding(1) var textureSampler: sampler; - @group(0) @binding(2) var textureData: texture_2d; - - @fragment - fn main(input: VertexOutput) -> @location(0) vec4 { - let texelSize = 1.0 / uniforms.textureHeight; - var color = vec4(0.0); - let blurRadius = i32(uniforms.blurSize); - - var totalWeight = 0.0; - - for (var i = -blurRadius; i <= blurRadius; i++) { - let offset = f32(i) * texelSize; - let weight = 1.0 - abs(f32(i)) / f32(blurRadius + 1); - - let sampleCoord = vec2( - input.texCoord.x, - input.texCoord.y + offset - ); - - color += textureSample(textureData, textureSampler, sampleCoord) * weight; - totalWeight += weight; - } - - return color / totalWeight; - } - `; - } -} diff --git a/packages/webgpu/src/Filter/ColorMatrixFilterShader.test.ts b/packages/webgpu/src/Filter/ColorMatrixFilterShader.test.ts deleted file mode 100644 index 672e57af..00000000 --- a/packages/webgpu/src/Filter/ColorMatrixFilterShader.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { ColorMatrixFilterShader } from "./ColorMatrixFilterShader"; - -describe("ColorMatrixFilterShader", () => -{ - describe("getVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = ColorMatrixFilterShader.getVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = ColorMatrixFilterShader.getVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should define VertexInput struct", () => - { - const shader = ColorMatrixFilterShader.getVertexShader(); - - expect(shader).toContain("struct VertexInput"); - }); - - it("should define VertexOutput struct", () => - { - const shader = ColorMatrixFilterShader.getVertexShader(); - - expect(shader).toContain("struct VertexOutput"); - }); - }); - - describe("getFragmentShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should define ColorMatrixUniforms struct", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(shader).toContain("struct ColorMatrixUniforms"); - }); - - it("should include matrix uniform", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(shader).toContain("matrix: mat4x4"); - }); - - it("should include offset uniform", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(shader).toContain("offset: vec4"); - }); - - it("should apply matrix transformation", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(shader).toContain("uniforms.matrix * color"); - }); - - it("should apply offset", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(shader).toContain("+ uniforms.offset"); - }); - - it("should clamp result to 0-1 range", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(shader).toContain("clamp"); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/ColorMatrixFilterShader.ts b/packages/webgpu/src/Filter/ColorMatrixFilterShader.ts deleted file mode 100644 index ed0108c2..00000000 --- a/packages/webgpu/src/Filter/ColorMatrixFilterShader.ts +++ /dev/null @@ -1,55 +0,0 @@ -export class ColorMatrixFilterShader -{ - static getFragmentShader(): string - { - return /* wgsl */` - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - struct ColorMatrixUniforms { - matrix: mat4x4, - offset: vec4, - } - - @group(0) @binding(0) var uniforms: ColorMatrixUniforms; - @group(0) @binding(1) var textureSampler: sampler; - @group(0) @binding(2) var textureData: texture_2d; - - @fragment - fn main(input: VertexOutput) -> @location(0) vec4 { - var color = textureSample(textureData, textureSampler, input.texCoord); - - var result = uniforms.matrix * color + uniforms.offset; - - result = clamp(result, vec4(0.0), vec4(1.0)); - - return result; - } - `; - } - - static getVertexShader(): string - { - return /* wgsl */` - struct VertexInput { - @location(0) position: vec2, - @location(1) texCoord: vec2, - } - - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - @vertex - fn main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - output.position = vec4(input.position, 0.0, 1.0); - output.texCoord = input.texCoord; - return output; - } - `; - } -} diff --git a/packages/webgpu/src/Filter/ConvolutionFilterShader.test.ts b/packages/webgpu/src/Filter/ConvolutionFilterShader.test.ts deleted file mode 100644 index 68d86687..00000000 --- a/packages/webgpu/src/Filter/ConvolutionFilterShader.test.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { getConvolutionFilterFragmentShader, getConvolutionFilterShaderKey } from "./ConvolutionFilterShader"; - -describe("ConvolutionFilterShader", () => -{ - describe("getConvolutionFilterFragmentShader", () => - { - it("should return a valid WGSL shader string", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("@vertex"); - }); - - it("should contain @fragment attribute", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("@fragment"); - }); - - it("should define ConvolutionUniforms struct", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("struct ConvolutionUniforms"); - }); - - it("should include rcpSize uniform", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("rcpSize: vec2"); - }); - - it("should include rcpDivisor uniform", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("rcpDivisor: f32"); - }); - - it("should include bias uniform", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("bias: f32"); - }); - - it("should include substituteColor uniform", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("substituteColor: vec4"); - }); - - it("should include matrix array", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("matrix: array"); - }); - - it("should generate correct matrix size for 3x3", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - // 3x3 = 9 elements, ceil(9/4) = 3 - expect(shader).toContain("array, 3>"); - }); - - it("should generate correct matrix size for 5x5", () => - { - const shader = getConvolutionFilterFragmentShader(5, 5, true, true); - // 5x5 = 25 elements, ceil(25/4) = 7 - expect(shader).toContain("array, 7>"); - }); - - it("should include isInside helper function", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("fn isInside"); - }); - - it("should include getMatrixWeight helper function", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("fn getMatrixWeight"); - }); - - it("should include getWeightedColor helper function", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("fn getWeightedColor"); - }); - - it("should preserve alpha when preserveAlpha is true", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("result.a = textureSample(sourceTexture, sourceSampler, input.texCoord).a"); - }); - - it("should not preserve alpha when preserveAlpha is false", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, false, true); - - expect(shader).not.toContain("result.a = textureSample(sourceTexture, sourceSampler, input.texCoord).a"); - }); - - it("should include substituteColor handling when clamp is false", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, false); - - expect(shader).toContain("substituteColor"); - expect(shader).toContain("mix(substituteColor, color, isInside(uv))"); - }); - - it("should not include substituteColor handling when clamp is true", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - // Should still have substituteColor in uniforms but not the mix statement - expect(shader).not.toContain("mix(substituteColor, color, isInside(uv))"); - }); - - it("should clamp result values", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("clamp(result * rcpDivisor + bias"); - }); - - it("should premultiply result", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("result.rgb * result.a"); - }); - - it("should unpremultiply color for processing", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("color.rgb / max(0.0001, color.a)"); - }); - - it("should generate 9 getWeightedColor calls for 3x3 matrix", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - let count = 0; - for (let i = 0; i < 9; i++) { - if (shader.includes(`getWeightedColor(${i}`)) { - count++; - } - } - - expect(count).toBe(9); - }); - - it("should handle asymmetric matrix sizes", () => - { - const shader = getConvolutionFilterFragmentShader(5, 3, true, true); - // 5x3 = 15 elements, ceil(15/4) = 4 - expect(shader).toContain("array, 4>"); - }); - }); - - describe("getConvolutionFilterShaderKey", () => - { - it("should generate unique key for 3x3 with preserveAlpha and clamp", () => - { - const key = getConvolutionFilterShaderKey(3, 3, true, true); - - expect(key).toBe("convolution_3x3_pa_c"); - }); - - it("should generate unique key for 5x5 without preserveAlpha and without clamp", () => - { - const key = getConvolutionFilterShaderKey(5, 5, false, false); - - expect(key).toBe("convolution_5x5_npa_nc"); - }); - - it("should include matrix dimensions in key", () => - { - const key = getConvolutionFilterShaderKey(7, 3, true, true); - - expect(key).toContain("7x3"); - }); - - it("should include preserveAlpha flag in key", () => - { - const keyWithPA = getConvolutionFilterShaderKey(3, 3, true, true); - const keyWithoutPA = getConvolutionFilterShaderKey(3, 3, false, true); - - expect(keyWithPA).toContain("_pa_"); - expect(keyWithoutPA).toContain("_npa_"); - }); - - it("should include clamp flag in key", () => - { - const keyWithClamp = getConvolutionFilterShaderKey(3, 3, true, true); - const keyWithoutClamp = getConvolutionFilterShaderKey(3, 3, true, false); - - expect(keyWithClamp).toContain("_c"); - expect(keyWithoutClamp).toContain("_nc"); - }); - - it("should generate different keys for different configurations", () => - { - const key1 = getConvolutionFilterShaderKey(3, 3, true, true); - const key2 = getConvolutionFilterShaderKey(3, 3, false, true); - const key3 = getConvolutionFilterShaderKey(3, 3, true, false); - const key4 = getConvolutionFilterShaderKey(5, 5, true, true); - - expect(key1).not.toBe(key2); - expect(key1).not.toBe(key3); - expect(key1).not.toBe(key4); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/ConvolutionFilterShader.ts b/packages/webgpu/src/Filter/ConvolutionFilterShader.ts deleted file mode 100644 index e112bb40..00000000 --- a/packages/webgpu/src/Filter/ConvolutionFilterShader.ts +++ /dev/null @@ -1,130 +0,0 @@ -export const getConvolutionFilterFragmentShader = ( - matrixX: number, - matrixY: number, - preserveAlpha: boolean, - clamp: boolean -): string => { - const halfX = Math.floor(matrixX * 0.5); - const halfY = Math.floor(matrixY * 0.5); - const size = matrixX * matrixY; - - let matrixStatement = ""; - for (let idx = 0; idx < size; idx++) { - matrixStatement += ` - result = result + getWeightedColor(${idx}, getMatrixWeight(${idx}));`; - } - - const preserveAlphaStatement = preserveAlpha - ? "result.a = textureSample(sourceTexture, sourceSampler, input.texCoord).a;" - : ""; - - const clampStatement = clamp - ? "" - : ` - let substituteColor = uniforms.substituteColor; - color = mix(substituteColor, color, isInside(uv));`; - - return ` -struct ConvolutionUniforms { - rcpSize: vec2, - rcpDivisor: f32, - bias: f32, - substituteColor: vec4, - matrix: array, ${Math.ceil(size / 4)}>, -} - -@group(0) @binding(0) var uniforms: ConvolutionUniforms; -@group(0) @binding(1) var sourceSampler: sampler; -@group(0) @binding(2) var sourceTexture: texture_2d; - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, -} - -fn isInside(uv: vec2) -> f32 { - let inside = step(vec2(0.0), uv) * step(uv, vec2(1.0)); - return inside.x * inside.y; -} - -fn getMatrixWeight(index: i32) -> f32 { - let vecIndex = index / 4; - let component = index % 4; - let vec = uniforms.matrix[vecIndex]; - - if (component == 0) { return vec.x; } - else if (component == 1) { return vec.y; } - else if (component == 2) { return vec.z; } - else { return vec.w; } -} - -fn getWeightedColor(i: i32, weight: f32) -> vec4 { - let rcpSize = uniforms.rcpSize; - - let iDivX = i / ${matrixX}; - let iModX = i - ${matrixX} * iDivX; - let offset = vec2(f32(iModX - ${halfX}), f32(${halfY} - iDivX)); - var uv = input.texCoord + offset * rcpSize; - - var color = textureSample(sourceTexture, sourceSampler, uv); - color = vec4(color.rgb / max(0.0001, color.a), color.a); - ${clampStatement} - - return color * weight; -} - -var input: VertexOutput; - -@vertex -fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { - var positions = array, 6>( - vec2(-1.0, -1.0), - vec2(1.0, -1.0), - vec2(-1.0, 1.0), - vec2(-1.0, 1.0), - vec2(1.0, -1.0), - vec2(1.0, 1.0) - ); - - var texCoords = array, 6>( - vec2(0.0, 1.0), - vec2(1.0, 1.0), - vec2(0.0, 0.0), - vec2(0.0, 0.0), - vec2(1.0, 1.0), - vec2(1.0, 0.0) - ); - - var output: VertexOutput; - output.position = vec4(positions[vertexIndex], 0.0, 1.0); - output.texCoord = texCoords[vertexIndex]; - return output; -} - -@fragment -fn fs_main(fragInput: VertexOutput) -> @location(0) vec4 { - input = fragInput; - - let rcpDivisor = uniforms.rcpDivisor; - let bias = uniforms.bias; - - var result = vec4(0.0); - ${matrixStatement} - - result = clamp(result * rcpDivisor + bias, vec4(0.0), vec4(1.0)); - ${preserveAlphaStatement} - - result = vec4(result.rgb * result.a, result.a); - return result; -} -`; -}; - -export const getConvolutionFilterShaderKey = ( - matrixX: number, - matrixY: number, - preserveAlpha: boolean, - clamp: boolean -): string => { - return `convolution_${matrixX}x${matrixY}_${preserveAlpha ? "pa" : "npa"}_${clamp ? "c" : "nc"}`; -}; diff --git a/packages/webgpu/src/Filter/DisplacementMapFilterShader.test.ts b/packages/webgpu/src/Filter/DisplacementMapFilterShader.test.ts deleted file mode 100644 index 0400ad10..00000000 --- a/packages/webgpu/src/Filter/DisplacementMapFilterShader.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { getDisplacementMapFilterFragmentShader, getDisplacementMapFilterShaderKey } from "./DisplacementMapFilterShader"; - -describe("DisplacementMapFilterShader", () => -{ - describe("getDisplacementMapFilterFragmentShader", () => - { - it("should return a valid WGSL shader string", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("@vertex"); - }); - - it("should contain @fragment attribute", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("@fragment"); - }); - - it("should define DisplacementMapUniforms struct", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("struct DisplacementMapUniforms"); - }); - - it("should include uvToStScale uniform", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("uvToStScale: vec2"); - }); - - it("should include uvToStOffset uniform", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("uvToStOffset: vec2"); - }); - - it("should include scale uniform", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("scale: vec2"); - }); - - it("should include mapTexture binding", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("var mapTexture: texture_2d"); - }); - - it("should include sourceTexture binding", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("var sourceTexture: texture_2d"); - }); - - it("should include isInside helper function", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("fn isInside"); - }); - - // Component channel tests - it("should use mapColor.r for componentX = 1 (RED)", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("mapColor.r"); - }); - - it("should use mapColor.g for componentX = 2 (GREEN)", () => - { - const shader = getDisplacementMapFilterFragmentShader(2, 1, 0); - - expect(shader).toContain("vec2(mapColor.g, mapColor.r)"); - }); - - it("should use mapColor.b for componentX = 4 (BLUE)", () => - { - const shader = getDisplacementMapFilterFragmentShader(4, 1, 0); - - expect(shader).toContain("vec2(mapColor.b, mapColor.r)"); - }); - - it("should use mapColor.a for componentX = 8 (ALPHA)", () => - { - const shader = getDisplacementMapFilterFragmentShader(8, 1, 0); - - expect(shader).toContain("vec2(mapColor.a, mapColor.r)"); - }); - - it("should use 0.5 for unknown component value", () => - { - const shader = getDisplacementMapFilterFragmentShader(99, 99, 0); - - expect(shader).toContain("vec2(0.5, 0.5)"); - }); - - // Mode tests - it("should handle mode 0 (direct sampling)", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("let sourceColor = textureSample(sourceTexture, sourceSampler, uv)"); - expect(shader).not.toContain("substituteColor"); - }); - - it("should include substituteColor for mode 1", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 1); - - expect(shader).toContain("substituteColor: vec4"); - expect(shader).toContain("mix(substituteColor"); - }); - - it("should handle mode 2 (wrap/repeat)", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 2); - - expect(shader).toContain("fract(uv)"); - }); - - it("should handle mode 3 (axis fallback)", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 3); - - expect(shader).toContain("fallbackUv"); - }); - - it("should calculate offset from map color", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("let offset = vec2"); - expect(shader).toContain("- 0.5"); - }); - - it("should calculate displaced UV", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("let uv = input.texCoord + offset * scale"); - }); - - it("should mix original and displaced color based on map bounds", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("mix(originalColor, sourceColor, isInside(st))"); - }); - }); - - describe("getDisplacementMapFilterShaderKey", () => - { - it("should generate unique key for component combination", () => - { - const key = getDisplacementMapFilterShaderKey(1, 2, 0); - - expect(key).toBe("displacement_1_2_0"); - }); - - it("should include all component and mode values", () => - { - const key = getDisplacementMapFilterShaderKey(4, 8, 2); - - expect(key).toBe("displacement_4_8_2"); - }); - - it("should generate different keys for different configurations", () => - { - const key1 = getDisplacementMapFilterShaderKey(1, 2, 0); - const key2 = getDisplacementMapFilterShaderKey(2, 1, 0); - const key3 = getDisplacementMapFilterShaderKey(1, 2, 1); - const key4 = getDisplacementMapFilterShaderKey(4, 8, 3); - - expect(key1).not.toBe(key2); - expect(key1).not.toBe(key3); - expect(key1).not.toBe(key4); - }); - - it("should include componentX in key", () => - { - const key = getDisplacementMapFilterShaderKey(4, 2, 0); - - expect(key).toContain("_4_"); - }); - - it("should include componentY in key", () => - { - const key = getDisplacementMapFilterShaderKey(1, 8, 0); - - expect(key).toContain("_8_"); - }); - - it("should include mode in key", () => - { - const key = getDisplacementMapFilterShaderKey(1, 2, 3); - - expect(key).toContain("_3"); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/DisplacementMapFilterShader.ts b/packages/webgpu/src/Filter/DisplacementMapFilterShader.ts deleted file mode 100644 index 3a3f51d3..00000000 --- a/packages/webgpu/src/Filter/DisplacementMapFilterShader.ts +++ /dev/null @@ -1,130 +0,0 @@ -const getComponentExpression = (component: number): string => { - switch (component) { - case 1: - return "mapColor.r"; - case 2: - return "mapColor.g"; - case 4: - return "mapColor.b"; - case 8: - return "mapColor.a"; - default: - return "0.5"; - } -}; - -const getModeStatement = (mode: number): string => { - switch (mode) { - case 0: - return ` - let sourceColor = textureSample(sourceTexture, sourceSampler, uv);`; - - case 1: - return ` - let substituteColor = uniforms.substituteColor; - let sourceColor = mix(substituteColor, textureSample(sourceTexture, sourceSampler, uv), isInside(uv));`; - - case 3: - return ` - let fallbackUv = mix(input.texCoord, uv, step(abs(uv - vec2(0.5)), vec2(0.5))); - let sourceColor = textureSample(sourceTexture, sourceSampler, fallbackUv);`; - - case 2: - default: - return ` - let sourceColor = textureSample(sourceTexture, sourceSampler, fract(uv));`; - } -}; - -export const getDisplacementMapFilterFragmentShader = ( - componentX: number, - componentY: number, - mode: number -): string => { - const cx = getComponentExpression(componentX); - const cy = getComponentExpression(componentY); - const modeStatement = getModeStatement(mode); - - const hasSubstituteColor = mode === 1; - - return ` -struct DisplacementMapUniforms { - uvToStScale: vec2, - uvToStOffset: vec2, - scale: vec2, - _pad: vec2, -${hasSubstituteColor ? " substituteColor: vec4," : ""} -} - -@group(0) @binding(0) var uniforms: DisplacementMapUniforms; -@group(0) @binding(1) var sourceSampler: sampler; -@group(0) @binding(2) var sourceTexture: texture_2d; -@group(0) @binding(3) var mapTexture: texture_2d; - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, -} - -fn isInside(uv: vec2) -> f32 { - let inside = step(vec2(0.0), uv) * step(uv, vec2(1.0)); - return inside.x * inside.y; -} - -var input: VertexOutput; - -@vertex -fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { - var positions = array, 6>( - vec2(-1.0, -1.0), - vec2(1.0, -1.0), - vec2(-1.0, 1.0), - vec2(-1.0, 1.0), - vec2(1.0, -1.0), - vec2(1.0, 1.0) - ); - - var texCoords = array, 6>( - vec2(0.0, 1.0), - vec2(1.0, 1.0), - vec2(0.0, 0.0), - vec2(0.0, 0.0), - vec2(1.0, 1.0), - vec2(1.0, 0.0) - ); - - var output: VertexOutput; - output.position = vec4(positions[vertexIndex], 0.0, 1.0); - output.texCoord = texCoords[vertexIndex]; - return output; -} - -@fragment -fn fs_main(fragInput: VertexOutput) -> @location(0) vec4 { - input = fragInput; - - let uvToStScale = uniforms.uvToStScale; - let uvToStOffset = uniforms.uvToStOffset; - let scale = uniforms.scale; - - let st = input.texCoord * uvToStScale - uvToStOffset; - let mapColor = textureSample(mapTexture, sourceSampler, st); - - let offset = vec2(${cx}, ${cy}) - 0.5; - let uv = input.texCoord + offset * scale; - - ${modeStatement} - - let originalColor = textureSample(sourceTexture, sourceSampler, input.texCoord); - return mix(originalColor, sourceColor, isInside(st)); -} -`; -}; - -export const getDisplacementMapFilterShaderKey = ( - componentX: number, - componentY: number, - mode: number -): string => { - return `displacement_${componentX}_${componentY}_${mode}`; -}; diff --git a/packages/webgpu/src/Filter/DropShadowFilterShader.test.ts b/packages/webgpu/src/Filter/DropShadowFilterShader.test.ts deleted file mode 100644 index 9604fb9b..00000000 --- a/packages/webgpu/src/Filter/DropShadowFilterShader.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { DropShadowFilterShader } from "./DropShadowFilterShader"; - -describe("DropShadowFilterShader", () => -{ - describe("getVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = DropShadowFilterShader.getVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = DropShadowFilterShader.getVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should define VertexInput and VertexOutput structs", () => - { - const shader = DropShadowFilterShader.getVertexShader(); - - expect(shader).toContain("struct VertexInput"); - expect(shader).toContain("struct VertexOutput"); - }); - }); - - describe("getFragmentShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should define DropShadowUniforms struct", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(shader).toContain("struct DropShadowUniforms"); - }); - - it("should include shadow parameters", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(shader).toContain("shadowColor"); - expect(shader).toContain("distance"); - expect(shader).toContain("angle"); - expect(shader).toContain("strength"); - }); - - it("should include inner shadow option", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(shader).toContain("inner"); - }); - - it("should include knockout option", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(shader).toContain("knockout"); - }); - - it("should include hideObject option", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(shader).toContain("hideObject"); - }); - - it("should calculate shadow offset using angle", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(shader).toContain("cos(radian)"); - expect(shader).toContain("sin(radian)"); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/DropShadowFilterShader.ts b/packages/webgpu/src/Filter/DropShadowFilterShader.ts deleted file mode 100644 index 14160fa7..00000000 --- a/packages/webgpu/src/Filter/DropShadowFilterShader.ts +++ /dev/null @@ -1,97 +0,0 @@ -export class DropShadowFilterShader -{ - static getFragmentShader(): string - { - return /* wgsl */` - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - struct DropShadowUniforms { - shadowColor: vec4, - offset: vec2, - distance: f32, - angle: f32, - strength: f32, - inner: f32, - knockout: f32, - hideObject: f32, - } - - @group(0) @binding(0) var uniforms: DropShadowUniforms; - @group(0) @binding(1) var textureSampler: sampler; - @group(0) @binding(2) var textureData: texture_2d; - - @fragment - fn main(input: VertexOutput) -> @location(0) vec4 { - var originalColor = textureSample(textureData, textureSampler, input.texCoord); - - let radian = uniforms.angle * 3.14159265 / 180.0; - let offsetX = cos(radian) * uniforms.distance / 100.0; - let offsetY = sin(radian) * uniforms.distance / 100.0; - - let shadowCoord = vec2( - input.texCoord.x + offsetX, - input.texCoord.y + offsetY - ); - - var shadowAlpha = textureSample(textureData, textureSampler, shadowCoord).a; - - var shadowColor = vec4( - uniforms.shadowColor.rgb, - shadowAlpha * uniforms.shadowColor.a * uniforms.strength - ); - - if (uniforms.inner > 0.5) { - let alpha = originalColor.a; - shadowColor.a *= alpha; - - if (uniforms.knockout > 0.5) { - return shadowColor; - } else { - return mix(shadowColor, originalColor, alpha); - } - } else { - if (uniforms.hideObject > 0.5) { - return shadowColor * (1.0 - originalColor.a); - } else if (uniforms.knockout > 0.5) { - return shadowColor; - } else { - let combinedAlpha = originalColor.a + shadowColor.a * (1.0 - originalColor.a); - if (combinedAlpha > 0.0) { - let rgb = (originalColor.rgb * originalColor.a + - shadowColor.rgb * shadowColor.a * (1.0 - originalColor.a)) / combinedAlpha; - return vec4(rgb, combinedAlpha); - } else { - return vec4(0.0); - } - } - } - } - `; - } - - static getVertexShader(): string - { - return /* wgsl */` - struct VertexInput { - @location(0) position: vec2, - @location(1) texCoord: vec2, - } - - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - @vertex - fn main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - output.position = vec4(input.position, 0.0, 1.0); - output.texCoord = input.texCoord; - return output; - } - `; - } -} diff --git a/packages/webgpu/src/Filter/GlowFilterShader.test.ts b/packages/webgpu/src/Filter/GlowFilterShader.test.ts deleted file mode 100644 index 92067859..00000000 --- a/packages/webgpu/src/Filter/GlowFilterShader.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { GlowFilterShader } from "./GlowFilterShader"; - -describe("GlowFilterShader", () => -{ - describe("getVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = GlowFilterShader.getVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = GlowFilterShader.getVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should define VertexInput struct", () => - { - const shader = GlowFilterShader.getVertexShader(); - - expect(shader).toContain("struct VertexInput"); - }); - - it("should define VertexOutput struct", () => - { - const shader = GlowFilterShader.getVertexShader(); - - expect(shader).toContain("struct VertexOutput"); - }); - }); - - describe("getFragmentShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = GlowFilterShader.getFragmentShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = GlowFilterShader.getFragmentShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should define GlowUniforms struct", () => - { - const shader = GlowFilterShader.getFragmentShader(); - - expect(shader).toContain("struct GlowUniforms"); - }); - - it("should include glow parameters", () => - { - const shader = GlowFilterShader.getFragmentShader(); - - expect(shader).toContain("glowColor"); - expect(shader).toContain("strength"); - expect(shader).toContain("inner"); - expect(shader).toContain("knockout"); - }); - - it("should handle inner glow mode", () => - { - const shader = GlowFilterShader.getFragmentShader(); - - expect(shader).toContain("uniforms.inner"); - }); - - it("should handle knockout mode", () => - { - const shader = GlowFilterShader.getFragmentShader(); - - expect(shader).toContain("uniforms.knockout"); - }); - - it("should include texture sampling", () => - { - const shader = GlowFilterShader.getFragmentShader(); - - expect(shader).toContain("textureSample"); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/GlowFilterShader.ts b/packages/webgpu/src/Filter/GlowFilterShader.ts deleted file mode 100644 index 6a19f62a..00000000 --- a/packages/webgpu/src/Filter/GlowFilterShader.ts +++ /dev/null @@ -1,70 +0,0 @@ -export class GlowFilterShader -{ - static getFragmentShader(): string - { - return /* wgsl */` - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - struct GlowUniforms { - glowColor: vec4, - strength: f32, - inner: f32, - knockout: f32, - _padding: f32, - } - - @group(0) @binding(0) var uniforms: GlowUniforms; - @group(0) @binding(1) var textureSampler: sampler; - @group(0) @binding(2) var textureData: texture_2d; - - @fragment - fn main(input: VertexOutput) -> @location(0) vec4 { - var originalColor = textureSample(textureData, textureSampler, input.texCoord); - - let alpha = originalColor.a; - - var glowColor = uniforms.glowColor * uniforms.strength * alpha; - - if (uniforms.inner > 0.5) { - if (uniforms.knockout > 0.5) { - return glowColor; - } else { - return mix(originalColor, glowColor, alpha); - } - } else { - if (uniforms.knockout > 0.5) { - return vec4(glowColor.rgb, glowColor.a * (1.0 - alpha)); - } else { - return originalColor + glowColor; - } - } - } - `; - } - - static getVertexShader(): string - { - return /* wgsl */` - struct VertexInput { - @location(0) position: vec2, - @location(1) texCoord: vec2, - } - - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - @vertex - fn main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - output.position = vec4(input.position, 0.0, 1.0); - output.texCoord = input.texCoord; - return output; - } - `; - } -} diff --git a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerFlushPendingReleasesService.test.ts b/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerFlushPendingReleasesService.test.ts deleted file mode 100644 index f5abde1a..00000000 --- a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerFlushPendingReleasesService.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import type { IAttachmentObject } from "../../interface/IAttachmentObject"; -import { execute } from "./FrameBufferManagerFlushPendingReleasesService"; - -describe("FrameBufferManagerFlushPendingReleasesService", () => -{ - const createMockAttachment = (hasTexture: boolean, hasStencil: boolean): IAttachmentObject => - { - return { - "texture": hasTexture ? { - "resource": { "destroy": vi.fn() } - } : null, - "stencil": hasStencil ? { - "resource": { "destroy": vi.fn() } - } : null - } as unknown as IAttachmentObject; - }; - - it("should destroy texture resources", () => - { - const attachment = createMockAttachment(true, false); - const pendingReleases = [attachment]; - - execute(pendingReleases); - - expect(attachment.texture!.resource.destroy).toHaveBeenCalled(); - }); - - it("should destroy stencil resources", () => - { - const attachment = createMockAttachment(false, true); - const pendingReleases = [attachment]; - - execute(pendingReleases); - - expect(attachment.stencil!.resource.destroy).toHaveBeenCalled(); - }); - - it("should destroy both texture and stencil resources", () => - { - const attachment = createMockAttachment(true, true); - const pendingReleases = [attachment]; - - execute(pendingReleases); - - expect(attachment.texture!.resource.destroy).toHaveBeenCalled(); - expect(attachment.stencil!.resource.destroy).toHaveBeenCalled(); - }); - - it("should handle attachment without texture", () => - { - const attachment = createMockAttachment(false, true); - const pendingReleases = [attachment]; - - expect(() => execute(pendingReleases)).not.toThrow(); - }); - - it("should handle attachment without stencil", () => - { - const attachment = createMockAttachment(true, false); - const pendingReleases = [attachment]; - - expect(() => execute(pendingReleases)).not.toThrow(); - }); - - it("should handle empty pending releases array", () => - { - const pendingReleases: IAttachmentObject[] = []; - - expect(() => execute(pendingReleases)).not.toThrow(); - }); - - it("should process multiple attachments", () => - { - const attachment1 = createMockAttachment(true, true); - const attachment2 = createMockAttachment(true, false); - const attachment3 = createMockAttachment(false, true); - const pendingReleases = [attachment1, attachment2, attachment3]; - - execute(pendingReleases); - - expect(attachment1.texture!.resource.destroy).toHaveBeenCalled(); - expect(attachment1.stencil!.resource.destroy).toHaveBeenCalled(); - expect(attachment2.texture!.resource.destroy).toHaveBeenCalled(); - expect(attachment3.stencil!.resource.destroy).toHaveBeenCalled(); - }); - - it("should call destroy exactly once per resource", () => - { - const attachment = createMockAttachment(true, true); - const pendingReleases = [attachment]; - - execute(pendingReleases); - - expect(attachment.texture!.resource.destroy).toHaveBeenCalledTimes(1); - expect(attachment.stencil!.resource.destroy).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerFlushPendingReleasesService.ts b/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerFlushPendingReleasesService.ts deleted file mode 100644 index 847a77a5..00000000 --- a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerFlushPendingReleasesService.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { IAttachmentObject } from "../../interface/IAttachmentObject"; - -/** - * @description フレーム終了時に保留中のテクスチャを解放 - * Release pending textures at end of frame (after submit) - * - * @param {IAttachmentObject[]} pendingReleases - * @return {void} - * @method - * @protected - */ -export const execute = (pendingReleases: IAttachmentObject[]): void => { - for (const att of pendingReleases) { - if (att.texture) { - att.texture.resource.destroy(); - } - if (att.stencil) { - att.stencil.resource.destroy(); - } - } -}; diff --git a/packages/webgpu/src/Mask/usecase/MaskBindUseCase.test.ts b/packages/webgpu/src/Mask/usecase/MaskBindUseCase.test.ts deleted file mode 100644 index 64ed13e1..00000000 --- a/packages/webgpu/src/Mask/usecase/MaskBindUseCase.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { execute } from "./MaskBindUseCase"; - -// Mock Mask module -const mockIsMaskDrawing = vi.fn(() => false); -const mockSetMaskDrawing = vi.fn(); - -vi.mock("../../Mask", () => ({ - "$isMaskDrawing": () => mockIsMaskDrawing(), - "$setMaskDrawing": (value: boolean) => mockSetMaskDrawing(value) -})); - -// Mock MaskEndMaskService -const mockMaskEndMaskService = vi.fn(); -vi.mock("../service/MaskEndMaskService", () => ({ - "execute": () => mockMaskEndMaskService() -})); - -describe("MaskBindUseCase", () => -{ - beforeEach(() => - { - vi.clearAllMocks(); - mockIsMaskDrawing.mockReturnValue(false); - }); - - describe("mask binding", () => - { - it("should set mask drawing to true when mask=true and not already drawing", () => - { - mockIsMaskDrawing.mockReturnValue(false); - - execute(true); - - expect(mockSetMaskDrawing).toHaveBeenCalledWith(true); - expect(mockMaskEndMaskService).toHaveBeenCalled(); - }); - - it("should not change state when mask=true and already drawing", () => - { - mockIsMaskDrawing.mockReturnValue(true); - - execute(true); - - expect(mockSetMaskDrawing).not.toHaveBeenCalled(); - expect(mockMaskEndMaskService).not.toHaveBeenCalled(); - }); - }); - - describe("mask unbinding", () => - { - it("should set mask drawing to false when mask=false and currently drawing", () => - { - mockIsMaskDrawing.mockReturnValue(true); - - execute(false); - - expect(mockSetMaskDrawing).toHaveBeenCalledWith(false); - }); - - it("should not change state when mask=false and not drawing", () => - { - mockIsMaskDrawing.mockReturnValue(false); - - execute(false); - - expect(mockSetMaskDrawing).not.toHaveBeenCalled(); - }); - }); - - describe("mask end service", () => - { - it("should call mask end service when transitioning from not drawing to drawing", () => - { - mockIsMaskDrawing.mockReturnValue(false); - - execute(true); - - expect(mockMaskEndMaskService).toHaveBeenCalled(); - }); - - it("should not call mask end service when mask=false", () => - { - mockIsMaskDrawing.mockReturnValue(true); - - execute(false); - - expect(mockMaskEndMaskService).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/webgpu/src/Mask/usecase/MaskBindUseCase.ts b/packages/webgpu/src/Mask/usecase/MaskBindUseCase.ts deleted file mode 100644 index f652fb4f..00000000 --- a/packages/webgpu/src/Mask/usecase/MaskBindUseCase.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { execute as maskEndMaskService } from "../service/MaskEndMaskService"; -import { - $isMaskDrawing, - $setMaskDrawing -} from "../../Mask"; - -/** - * @description マスクOn/Offに合わせたバインド処理 - * Binding process according to mask On/Off - * - * @param {boolean} mask - * @return {void} - * @method - * @protected - */ -export const execute = (mask: boolean): void => -{ - if (!mask && $isMaskDrawing()) { - $setMaskDrawing(false); - } else if (mask && !$isMaskDrawing()) { - $setMaskDrawing(true); - maskEndMaskService(); - } -}; diff --git a/packages/webgpu/src/Mesh/service/MeshLerpService.test.ts b/packages/webgpu/src/Mesh/service/MeshLerpService.test.ts deleted file mode 100644 index 3cf53c46..00000000 --- a/packages/webgpu/src/Mesh/service/MeshLerpService.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { execute } from "./MeshLerpService"; -import { describe, expect, it } from "vitest"; - -describe("MeshLerpService.ts method test", () => -{ - it("test case - lerp at t=0 returns first point", () => - { - const pointA = { x: 0, y: 0 }; - const pointB = { x: 10, y: 10 }; - - const result = execute(pointA, pointB, 0); - - expect(result.x).toBe(0); - expect(result.y).toBe(0); - }); - - it("test case - lerp at t=1 returns second point", () => - { - const pointA = { x: 0, y: 0 }; - const pointB = { x: 10, y: 10 }; - - const result = execute(pointA, pointB, 1); - - expect(result.x).toBe(10); - expect(result.y).toBe(10); - }); - - it("test case - lerp at t=0.5 returns midpoint", () => - { - const pointA = { x: 0, y: 0 }; - const pointB = { x: 10, y: 20 }; - - const result = execute(pointA, pointB, 0.5); - - expect(result.x).toBe(5); - expect(result.y).toBe(10); - }); - - it("test case - lerp at t=0.25", () => - { - const pointA = { x: 0, y: 0 }; - const pointB = { x: 100, y: 200 }; - - const result = execute(pointA, pointB, 0.25); - - expect(result.x).toBe(25); - expect(result.y).toBe(50); - }); -}); diff --git a/packages/webgpu/src/Mesh/service/MeshLerpService.ts b/packages/webgpu/src/Mesh/service/MeshLerpService.ts deleted file mode 100644 index eb5164aa..00000000 --- a/packages/webgpu/src/Mesh/service/MeshLerpService.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { IPoint } from "../../interface/IPoint"; - -/** - * @description 線形補間 - * Linear interpolation - * - * @param {IPoint} pointA - * @param {IPoint} pointB - * @param {number} t - * @return {IPoint} - * @method - * @protected - */ -export const execute = ( - pointA: IPoint, - pointB: IPoint, - t: number -): IPoint => { - return { - "x": pointA.x + (pointB.x - pointA.x) * t, - "y": pointA.y + (pointB.y - pointA.y) * t - }; -}; diff --git a/packages/webgpu/src/Mesh/usecase/MeshSplitQuadraticBezierUseCase.test.ts b/packages/webgpu/src/Mesh/usecase/MeshSplitQuadraticBezierUseCase.test.ts deleted file mode 100644 index 66a2443d..00000000 --- a/packages/webgpu/src/Mesh/usecase/MeshSplitQuadraticBezierUseCase.test.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { execute } from "./MeshSplitQuadraticBezierUseCase"; - -describe("MeshSplitQuadraticBezierUseCase", () => -{ - describe("basic splitting", () => - { - it("should split a simple quadratic bezier at t=0.5", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 50, y: 100 }; - const end = { x: 100, y: 0 }; - - const result = execute(start, control, end, 0.5); - - expect(result).toHaveLength(2); - expect(result[0]).toHaveLength(3); - expect(result[1]).toHaveLength(3); - }); - - it("should use default t=0.5 when not specified", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 50, y: 100 }; - const end = { x: 100, y: 0 }; - - const result = execute(start, control, end); - - expect(result).toHaveLength(2); - }); - - it("should preserve start point in left sub-curve", () => - { - const start = { x: 10, y: 20 }; - const control = { x: 50, y: 100 }; - const end = { x: 100, y: 0 }; - - const result = execute(start, control, end, 0.5); - - expect(result[0][0]).toEqual(start); - }); - - it("should preserve end point in right sub-curve", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 50, y: 100 }; - const end = { x: 90, y: 30 }; - - const result = execute(start, control, end, 0.5); - - expect(result[1][2]).toEqual(end); - }); - - it("should share the split point between curves", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 50, y: 100 }; - const end = { x: 100, y: 0 }; - - const result = execute(start, control, end, 0.5); - - // M01 (split point) is last of left and first of right - expect(result[0][2]).toEqual(result[1][0]); - }); - }); - - describe("split at t=0.5", () => - { - it("should compute correct intermediate points", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 100, y: 200 }; - const end = { x: 200, y: 0 }; - - const result = execute(start, control, end, 0.5); - - // M0 = lerp(P0, P1, 0.5) = (50, 100) - // M1 = lerp(P1, P2, 0.5) = (150, 100) - // M01 = lerp(M0, M1, 0.5) = (100, 100) - - // Left curve: [P0, M0, M01] = [(0,0), (50,100), (100,100)] - expect(result[0][0]).toEqual({ x: 0, y: 0 }); - expect(result[0][1]).toEqual({ x: 50, y: 100 }); - expect(result[0][2]).toEqual({ x: 100, y: 100 }); - - // Right curve: [M01, M1, P2] = [(100,100), (150,100), (200,0)] - expect(result[1][0]).toEqual({ x: 100, y: 100 }); - expect(result[1][1]).toEqual({ x: 150, y: 100 }); - expect(result[1][2]).toEqual({ x: 200, y: 0 }); - }); - }); - - describe("split at different t values", () => - { - it("should correctly split at t=0.25", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 100, y: 0 }; - const end = { x: 100, y: 100 }; - - const result = execute(start, control, end, 0.25); - - // M0 = lerp(P0, P1, 0.25) = (25, 0) - // M1 = lerp(P1, P2, 0.25) = (100, 25) - // M01 = lerp(M0, M1, 0.25) = (25 + (100-25)*0.25, 0 + (25-0)*0.25) = (43.75, 6.25) - - expect(result[0][1].x).toBeCloseTo(25, 5); - expect(result[0][1].y).toBeCloseTo(0, 5); - - expect(result[1][1].x).toBeCloseTo(100, 5); - expect(result[1][1].y).toBeCloseTo(25, 5); - }); - - it("should correctly split at t=0.75", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 100, y: 0 }; - const end = { x: 100, y: 100 }; - - const result = execute(start, control, end, 0.75); - - // M0 = lerp(P0, P1, 0.75) = (75, 0) - // M1 = lerp(P1, P2, 0.75) = (100, 75) - - expect(result[0][1].x).toBeCloseTo(75, 5); - expect(result[0][1].y).toBeCloseTo(0, 5); - - expect(result[1][1].x).toBeCloseTo(100, 5); - expect(result[1][1].y).toBeCloseTo(75, 5); - }); - - it("should handle t=0 (split at start)", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 50, y: 100 }; - const end = { x: 100, y: 0 }; - - const result = execute(start, control, end, 0); - - // At t=0, split point is at start - expect(result[0][2]).toEqual(start); - expect(result[1][0]).toEqual(start); - }); - - it("should handle t=1 (split at end)", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 50, y: 100 }; - const end = { x: 100, y: 0 }; - - const result = execute(start, control, end, 1); - - // At t=1, split point is at end - expect(result[0][2]).toEqual(end); - expect(result[1][0]).toEqual(end); - }); - }); - - describe("edge cases", () => - { - it("should handle horizontal line (control on line)", () => - { - const start = { x: 0, y: 50 }; - const control = { x: 50, y: 50 }; - const end = { x: 100, y: 50 }; - - const result = execute(start, control, end, 0.5); - - // All y values should remain 50 - expect(result[0][0].y).toBe(50); - expect(result[0][1].y).toBe(50); - expect(result[0][2].y).toBe(50); - expect(result[1][1].y).toBe(50); - expect(result[1][2].y).toBe(50); - }); - - it("should handle vertical line", () => - { - const start = { x: 50, y: 0 }; - const control = { x: 50, y: 50 }; - const end = { x: 50, y: 100 }; - - const result = execute(start, control, end, 0.5); - - // All x values should remain 50 - expect(result[0][0].x).toBe(50); - expect(result[0][1].x).toBe(50); - expect(result[0][2].x).toBe(50); - expect(result[1][1].x).toBe(50); - expect(result[1][2].x).toBe(50); - }); - - it("should handle single point curve", () => - { - const point = { x: 50, y: 50 }; - - const result = execute(point, point, point, 0.5); - - // All points should be the same - expect(result[0][0]).toEqual(point); - expect(result[0][1]).toEqual(point); - expect(result[0][2]).toEqual(point); - expect(result[1][0]).toEqual(point); - expect(result[1][1]).toEqual(point); - expect(result[1][2]).toEqual(point); - }); - - it("should handle negative coordinates", () => - { - const start = { x: -100, y: -100 }; - const control = { x: 0, y: 100 }; - const end = { x: 100, y: -100 }; - - const result = execute(start, control, end, 0.5); - - expect(result).toHaveLength(2); - expect(result[0][0]).toEqual(start); - expect(result[1][2]).toEqual(end); - }); - - it("should handle very small t value", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 50, y: 100 }; - const end = { x: 100, y: 0 }; - - const result = execute(start, control, end, 0.001); - - // Split point should be much closer to start than to end - // At t=0.001, we expect very small values - expect(result[0][2].x).toBeLessThan(1); - expect(result[0][2].y).toBeLessThan(1); - }); - - it("should handle very large coordinates", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 100000, y: 200000 }; - const end = { x: 200000, y: 0 }; - - const result = execute(start, control, end, 0.5); - - expect(result[0][1].x).toBeCloseTo(50000, 0); - expect(result[0][1].y).toBeCloseTo(100000, 0); - }); - }); -}); diff --git a/packages/webgpu/src/Mesh/usecase/MeshSplitQuadraticBezierUseCase.ts b/packages/webgpu/src/Mesh/usecase/MeshSplitQuadraticBezierUseCase.ts deleted file mode 100644 index dba9c744..00000000 --- a/packages/webgpu/src/Mesh/usecase/MeshSplitQuadraticBezierUseCase.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { IPoint } from "../../interface/IPoint"; -import { execute as meshLerpService } from "../service/MeshLerpService"; - -/** - * @description 二次ベジェ曲線を分割する - * Split a quadratic Bezier curve - * - * @param {IPoint} start_point - * @param {IPoint} control_point - * @param {IPoint} end_point - * @param {number} [t = 0.5] - * @return {Array} - * @method - * @protected - */ -export const execute = ( - start_point: IPoint, - control_point: IPoint, - end_point: IPoint, - t: number = 0.5 -): Array => { - - // 二次ベジエ曲線の分割 - // M0 = lerp(P0, P1, t) - // M1 = lerp(P1, P2, t) - // M01 = lerp(M0, M1, t) - const M0 = meshLerpService(start_point, control_point, t); - const M1 = meshLerpService(control_point, end_point, t); - const M01 = meshLerpService(M0, M1, t); - - // 左サブ (0...t): [P0, M0, M01] - // 右サブ (t...1): [M01, M1, P2] - return [ - [start_point, M0, M01], - [M01, M1, end_point] - ]; -}; diff --git a/packages/webgpu/src/Mesh/usecase/MeshStrokeFillGenerateUseCase.test.ts b/packages/webgpu/src/Mesh/usecase/MeshStrokeFillGenerateUseCase.test.ts deleted file mode 100644 index c83389b8..00000000 --- a/packages/webgpu/src/Mesh/usecase/MeshStrokeFillGenerateUseCase.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { describe, it, expect } from "vitest"; -import type { IPath } from "../../interface/IPath"; -import { execute } from "./MeshStrokeFillGenerateUseCase"; - -describe("MeshStrokeFillGenerateUseCase", () => -{ - describe("basic mesh generation", () => - { - it("should return IMeshResult with buffer and indexCount", () => - { - const vertices: IPath[] = [[ - 0, 0, false, - 100, 0, false, - 100, 100, false, - 0, 0, false - ]]; - - const result = execute(vertices); - - expect(result).toHaveProperty("buffer"); - expect(result).toHaveProperty("indexCount"); - expect(result.buffer).toBeInstanceOf(Float32Array); - }); - - it("should generate 4 floats per vertex", () => - { - const vertices: IPath[] = [[ - 0, 0, false, - 100, 0, false, - 100, 100, false, - 0, 0, false - ]]; - - const result = execute(vertices); - - // 2 triangles * 3 vertices = 6 vertices - // 6 vertices * 4 floats = 24 - expect(result.buffer.length).toBe(6 * 4); - expect(result.indexCount).toBe(6); - }); - }); - - describe("bezier coordinates", () => - { - it("should always set bezier to (0.5, 0.5) for stroke fill", () => - { - const vertices: IPath[] = [[ - 0, 0, false, - 100, 0, false, - 100, 100, false, - 0, 0, false - ]]; - - const result = execute(vertices); - - // Check bezier coordinates for all vertices - for (let i = 0; i < result.indexCount; i++) { - const offset = i * 4; - expect(result.buffer[offset + 2]).toBe(0.5); // bezier.u - expect(result.buffer[offset + 3]).toBe(0.5); // bezier.v - } - }); - - it("should set bezier to (0.5, 0.5) even for curve paths", () => - { - const vertices: IPath[] = [[ - 0, 0, false, - 50, 50, true, // control point - 100, 0, false, - 0, 0, false - ]]; - - const result = execute(vertices); - - // All bezier coordinates should be (0.5, 0.5) - for (let i = 0; i < result.indexCount; i++) { - const offset = i * 4; - expect(result.buffer[offset + 2]).toBe(0.5); - expect(result.buffer[offset + 3]).toBe(0.5); - } - }); - }); - - describe("multiple paths", () => - { - it("should handle multiple paths", () => - { - const vertices: IPath[] = [ - [0, 0, false, 100, 0, false, 100, 100, false, 0, 0, false], - [200, 200, false, 300, 200, false, 300, 300, false, 200, 200, false] - ]; - - const result = execute(vertices); - - // 2 paths * 2 triangles * 3 vertices = 12 vertices - expect(result.indexCount).toBe(12); - }); - }); - - describe("empty input", () => - { - it("should handle empty vertices array", () => - { - const vertices: IPath[] = []; - - const result = execute(vertices); - - expect(result.buffer.length).toBe(0); - expect(result.indexCount).toBe(0); - }); - - it("should handle path with insufficient points", () => - { - const vertices: IPath[] = [[ - 0, 0, false, - 100, 0, false - ]]; - - const result = execute(vertices); - - expect(result.buffer.length).toBe(0); - expect(result.indexCount).toBe(0); - }); - }); -}); diff --git a/packages/webgpu/src/Mesh/usecase/MeshStrokeFillGenerateUseCase.ts b/packages/webgpu/src/Mesh/usecase/MeshStrokeFillGenerateUseCase.ts deleted file mode 100644 index 9b1784e1..00000000 --- a/packages/webgpu/src/Mesh/usecase/MeshStrokeFillGenerateUseCase.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { IPath } from "../../interface/IPath"; -import type { IMeshResult } from "../../interface/IMeshResult"; -import { execute as meshStrokeFillGenerateService } from "../service/MeshStrokeFillGenerateService"; - -/** - * @description メッシュ生成用の再利用可能な一時バッファ(GC回避) - */ -let $meshTempBuffer: Float32Array = new Float32Array(32); - -const $upperPowerOfTwo = (v: number): number => -{ - v--; - v |= v >> 1; - v |= v >> 2; - v |= v >> 4; - v |= v >> 8; - v |= v >> 16; - v++; - return v; -}; - -/** - * @description ストローク塗りつぶし用のメッシュを生成する - * Generate a stroke fill mesh - * - * 頂点フォーマット(4 floats per vertex): - * - position: x, y (2 floats) - * - bezier: u, v (2 floats) - 常に (0.5, 0.5) - * - * color/matrixはuniform bufferで供給される - * - * @param {IPath[]} vertices - * @return {IMeshResult} - * @method - * @protected - */ -export const execute = ( - vertices: IPath[] -): IMeshResult => { - - // 頂点数を計算(各パスの三角形数 × 3) - let totalVertices = 0; - for (const vertex of vertices) { - const length = vertex.length - 5; - for (let idx = 3; idx < length; idx += 3) { - totalVertices += 3; - } - } - - // バッファを確保(4 floats per vertex、再利用可能バッファ) - const requiredSize = totalVertices * 4; - if ($meshTempBuffer.length < requiredSize) { - $meshTempBuffer = new Float32Array($upperPowerOfTwo(requiredSize)); - } - const buffer = $meshTempBuffer; - - let index = 0; - for (const vertex of vertices) { - index = meshStrokeFillGenerateService( - vertex, - buffer, - index - ); - } - - return { - "buffer": buffer.subarray(0, index * 4), - "indexCount": index - }; -}; diff --git a/packages/webgpu/src/PathCommand.test.ts b/packages/webgpu/src/PathCommand.test.ts index fdba0c1b..95ba80b8 100644 --- a/packages/webgpu/src/PathCommand.test.ts +++ b/packages/webgpu/src/PathCommand.test.ts @@ -18,8 +18,8 @@ describe("PathCommand", () => pathCommand.lineTo(30, 40); pathCommand.beginPath(); - const paths = pathCommand.getAllPaths(); - expect(paths.length).toBe(0); + const vertices = pathCommand.getVerticesForStroke(); + expect(vertices.length).toBe(0); }); }); @@ -28,11 +28,12 @@ describe("PathCommand", () => it("should start a new path", () => { pathCommand.moveTo(10, 20); - const path = pathCommand.getCurrentPath(); + pathCommand.lineTo(30, 40); + const vertices = pathCommand.getVerticesForStroke(); - expect(path.length).toBe(1); - expect(path[0].x).toBe(10); - expect(path[0].y).toBe(20); + expect(vertices.length).toBe(1); + expect(vertices[0][0]).toBe(10); + expect(vertices[0][1]).toBe(20); }); it("should save previous path if long enough", () => @@ -42,9 +43,10 @@ describe("PathCommand", () => pathCommand.lineTo(10, 10); pathCommand.lineTo(0, 10); pathCommand.moveTo(100, 100); + pathCommand.lineTo(110, 100); - const paths = pathCommand.getAllPaths(); - expect(paths.length).toBe(2); + const vertices = pathCommand.getVerticesForStroke(); + expect(vertices.length).toBe(2); }); }); @@ -55,19 +57,23 @@ describe("PathCommand", () => pathCommand.moveTo(0, 0); pathCommand.lineTo(10, 20); - const path = pathCommand.getCurrentPath(); - expect(path.length).toBe(2); - expect(path[1].x).toBe(10); - expect(path[1].y).toBe(20); + const vertices = pathCommand.getVerticesForStroke(); + expect(vertices.length).toBe(1); + // Path: [0, 0, false, 10, 20, false] = 6 elements + expect(vertices[0].length).toBe(6); + expect(vertices[0][3]).toBe(10); + expect(vertices[0][4]).toBe(20); }); it("should ignore same point", () => { pathCommand.moveTo(10, 20); pathCommand.lineTo(10, 20); + pathCommand.lineTo(30, 40); - const path = pathCommand.getCurrentPath(); - expect(path.length).toBe(1); + const vertices = pathCommand.getVerticesForStroke(); + // moveTo + lineTo(same) + lineTo = 2 unique points (6 elements) + expect(vertices[0].length).toBe(6); }); it("should add multiple lines", () => @@ -77,8 +83,9 @@ describe("PathCommand", () => pathCommand.lineTo(10, 10); pathCommand.lineTo(0, 10); - const path = pathCommand.getCurrentPath(); - expect(path.length).toBe(4); + const vertices = pathCommand.getVerticesForStroke(); + // 4 points * 3 elements = 12 + expect(vertices[0].length).toBe(12); }); }); @@ -91,7 +98,6 @@ describe("PathCommand", () => const vertices = pathCommand.getVerticesForStroke(); expect(vertices.length).toBe(1); - // Path format: [x, y, isCurve, ...] // Should have: start(0,0,false), control(50,100,true), end(100,0,false) expect(vertices[0].length).toBe(9); }); @@ -104,9 +110,9 @@ describe("PathCommand", () => pathCommand.moveTo(0, 0); pathCommand.bezierCurveTo(10, 50, 90, 50, 100, 0); - const path = pathCommand.getCurrentPath(); + const vertices = pathCommand.getVerticesForStroke(); // Should have at least start point and some segments - expect(path.length).toBeGreaterThan(1); + expect(vertices[0].length).toBeGreaterThan(3); }); }); @@ -117,10 +123,9 @@ describe("PathCommand", () => pathCommand.moveTo(150, 100); pathCommand.arc(100, 100, 50); - const path = pathCommand.getCurrentPath(); + const vertices = pathCommand.getVerticesForStroke(); // Adaptive tessellation produces variable segment count - // 4 cubic bezier curves, each converted to quadratic segments - expect(path.length).toBeGreaterThan(1); + expect(vertices[0].length).toBeGreaterThan(3); }); it("should be centered at specified position", () => @@ -128,10 +133,10 @@ describe("PathCommand", () => pathCommand.moveTo(150, 100); pathCommand.arc(100, 100, 50); - const path = pathCommand.getCurrentPath(); - // First point should be at (100+50, 100) = (150, 100) from moveTo - expect(path[0].x).toBeCloseTo(150, 1); - expect(path[0].y).toBeCloseTo(100, 1); + const vertices = pathCommand.getVerticesForStroke(); + // First point should be at (150, 100) from moveTo + expect(vertices[0][0]).toBeCloseTo(150, 1); + expect(vertices[0][1]).toBeCloseTo(100, 1); }); }); @@ -144,10 +149,11 @@ describe("PathCommand", () => pathCommand.lineTo(100, 100); pathCommand.closePath(); - const path = pathCommand.getCurrentPath(); - const lastPoint = path[path.length - 1]; - expect(lastPoint.x).toBe(0); - expect(lastPoint.y).toBe(0); + const vertices = pathCommand.getVerticesForStroke(); + const path = vertices[0]; + // Last point should be (0, 0) + expect(path[path.length - 3]).toBe(0); + expect(path[path.length - 2]).toBe(0); }); it("should not add duplicate point if already at start", () => @@ -155,43 +161,15 @@ describe("PathCommand", () => pathCommand.moveTo(0, 0); pathCommand.lineTo(100, 0); pathCommand.lineTo(0, 0); - const lengthBefore = pathCommand.getCurrentPath().length; + const lengthBefore = pathCommand.getVerticesForStroke()[0].length; pathCommand.closePath(); - const lengthAfter = pathCommand.getCurrentPath().length; + const lengthAfter = pathCommand.getVerticesForStroke()[0].length; expect(lengthAfter).toBe(lengthBefore); }); }); - describe("generateVertices", () => - { - it("should generate triangle vertices for simple triangle", () => - { - pathCommand.moveTo(0, 0); - pathCommand.lineTo(100, 0); - pathCommand.lineTo(50, 100); - pathCommand.closePath(); - - const vertices = pathCommand.generateVertices(); - // 1 triangle = 6 values (3 points * 2 coords) - expect(vertices.length).toBeGreaterThanOrEqual(6); - }); - - it("should generate triangles using fan triangulation", () => - { - pathCommand.moveTo(0, 0); - pathCommand.lineTo(100, 0); - pathCommand.lineTo(100, 100); - pathCommand.lineTo(0, 100); - pathCommand.closePath(); - - const vertices = pathCommand.generateVertices(); - // Square with 5 points (including close) should generate multiple triangles - expect(vertices.length).toBeGreaterThan(6); - }); - }); - - describe("getAllPaths", () => + describe("getVerticesForStroke", () => { it("should return all paths", () => { @@ -205,25 +183,11 @@ describe("PathCommand", () => pathCommand.lineTo(110, 110); pathCommand.lineTo(100, 110); - const paths = pathCommand.getAllPaths(); - expect(paths.length).toBe(2); - expect(paths[0].length).toBe(4); - expect(paths[1].length).toBe(4); - }); - }); - - describe("setScale", () => - { - it("should adjust flatness threshold based on scale", () => - { - pathCommand.setScale(2.0); - // We can't directly access the private threshold, - // but we can verify the bezierCurveTo still works - pathCommand.moveTo(0, 0); - pathCommand.bezierCurveTo(10, 50, 90, 50, 100, 0); - - const path = pathCommand.getCurrentPath(); - expect(path.length).toBeGreaterThan(1); + const vertices = pathCommand.getVerticesForStroke(); + expect(vertices.length).toBe(2); + // Each path has 4 points * 3 elements = 12 + expect(vertices[0].length).toBe(12); + expect(vertices[1].length).toBe(12); }); }); @@ -235,11 +199,8 @@ describe("PathCommand", () => pathCommand.lineTo(30, 40); pathCommand.reset(); - const paths = pathCommand.getAllPaths(); - expect(paths.length).toBe(0); - - const currentPath = pathCommand.getCurrentPath(); - expect(currentPath.length).toBe(0); + const vertices = pathCommand.getVerticesForStroke(); + expect(vertices.length).toBe(0); }); }); }); diff --git a/packages/webgpu/src/PathCommand.ts b/packages/webgpu/src/PathCommand.ts index 52da4079..e50b8744 100644 --- a/packages/webgpu/src/PathCommand.ts +++ b/packages/webgpu/src/PathCommand.ts @@ -1,9 +1,6 @@ import type { IPoint } from "./interface/IPoint"; import type { IPath } from "./interface/IPath"; -import { - adaptiveCubicToQuad, - calculateAdaptiveThreshold -} from "./BezierConverter/BezierConverter"; +import { adaptiveCubicToQuad } from "./BezierConverter/BezierConverter"; /** * @description WebGPU用パスコマンド(WebGL互換形式) @@ -120,11 +117,6 @@ export class PathCommand * @param {number} scale - 現在のスケール(行列のスケール成分) * @return {void} */ - setScale(scale: number): void - { - this.$flatnessThreshold = calculateAdaptiveThreshold(scale); - } - /** * @description 三次ベジェ曲線を二次ベジェ曲線に適応的に近似 * Adaptively approximate cubic bezier with quadratic beziers @@ -250,90 +242,6 @@ export class PathCommand return vertices; } - /** - * @description パスから頂点配列を生成(従来互換用・単純なfan triangulation) - * @return {Float32Array} - */ - generateVertices(): Float32Array - { - const vertices = this.$getVertices; - const triangles: number[] = []; - - for (const path of vertices) { - if (path.length < 9) { continue } // 最低3点(9要素)必要 - - // 点を抽出 - const points: IPoint[] = []; - for (let i = 0; i < path.length; i += 3) { - points.push({ "x": path[i] as number, "y": path[i + 1] as number }); - } - - // Fan triangulation - for (let i = 1; i < points.length - 1; i++) { - triangles.push( - points[0].x, points[0].y, - points[i].x, points[i].y, - points[i + 1].x, points[i + 1].y - ); - } - } - - return new Float32Array(triangles); - } - - /** - * @description 現在のパスを取得(ストローク用) - * @return {IPoint[]} - */ - getCurrentPath(): IPoint[] - { - const points: IPoint[] = []; - for (let i = 0; i < this.$currentPath.length; i += 3) { - points.push({ - "x": this.$currentPath[i] as number, - "y": this.$currentPath[i + 1] as number - }); - } - return points; - } - - /** - * @description すべてのパスを取得(ストローク用) - * @return {IPoint[][]} - */ - getAllPaths(): IPoint[][] - { - const allPaths: IPoint[][] = []; - - for (const path of this.$vertices) { - const points: IPoint[] = []; - for (let i = 0; i < path.length; i += 3) { - points.push({ - "x": path[i] as number, - "y": path[i + 1] as number - }); - } - if (points.length > 0) { - allPaths.push(points); - } - } - - if (this.$currentPath.length >= 3) { - const points: IPoint[] = []; - for (let i = 0; i < this.$currentPath.length; i += 3) { - points.push({ - "x": this.$currentPath[i] as number, - "y": this.$currentPath[i + 1] as number - }); - } - if (points.length > 0) { - allPaths.push(points); - } - } - - return allPaths; - } - /** * @description WebGL互換形式でパスを取得(ストローク用) * [x, y, isCurve, x, y, isCurve, ...] 形式 diff --git a/packages/webgpu/src/Shader/BlendModeShader.test.ts b/packages/webgpu/src/Shader/BlendModeShader.test.ts deleted file mode 100644 index 6e8664c7..00000000 --- a/packages/webgpu/src/Shader/BlendModeShader.test.ts +++ /dev/null @@ -1,365 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { BlendModeShader } from "./BlendModeShader"; - -describe("BlendModeShader", () => -{ - describe("getVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = BlendModeShader.getVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = BlendModeShader.getVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should define VertexInput struct", () => - { - const shader = BlendModeShader.getVertexShader(); - - expect(shader).toContain("struct VertexInput"); - }); - - it("should define VertexOutput struct", () => - { - const shader = BlendModeShader.getVertexShader(); - - expect(shader).toContain("struct VertexOutput"); - }); - - it("should include position in VertexInput", () => - { - const shader = BlendModeShader.getVertexShader(); - - expect(shader).toContain("@location(0) position: vec2"); - }); - - it("should include texCoord in VertexInput", () => - { - const shader = BlendModeShader.getVertexShader(); - - expect(shader).toContain("@location(1) texCoord: vec2"); - }); - - it("should output position with @builtin(position)", () => - { - const shader = BlendModeShader.getVertexShader(); - - expect(shader).toContain("@builtin(position) position: vec4"); - }); - }); - - describe("getMultiplyShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getMultiplyShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getMultiplyShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should define BlendUniforms struct", () => - { - const shader = BlendModeShader.getMultiplyShader(); - - expect(shader).toContain("struct BlendUniforms"); - }); - - it("should include colorTransform uniform", () => - { - const shader = BlendModeShader.getMultiplyShader(); - - expect(shader).toContain("colorTransform: vec4"); - }); - - it("should include addColor uniform", () => - { - const shader = BlendModeShader.getMultiplyShader(); - - expect(shader).toContain("addColor: vec4"); - }); - - it("should have two texture bindings for dst and src", () => - { - const shader = BlendModeShader.getMultiplyShader(); - - expect(shader).toContain("var texture0: texture_2d"); - expect(shader).toContain("var texture1: texture_2d"); - }); - - it("should implement multiply blend formula", () => - { - const shader = BlendModeShader.getMultiplyShader(); - - expect(shader).toContain("src * dst"); - }); - }); - - describe("getScreenShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getScreenShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getScreenShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should define BlendUniforms struct", () => - { - const shader = BlendModeShader.getScreenShader(); - - expect(shader).toContain("struct BlendUniforms"); - }); - - it("should implement screen blend formula", () => - { - const shader = BlendModeShader.getScreenShader(); - expect(shader).toContain("srcRgb + dstRgb - srcRgb * dstRgb"); - }); - }); - - describe("getLightenShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getLightenShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getLightenShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should implement lighten blend using max", () => - { - const shader = BlendModeShader.getLightenShader(); - - expect(shader).toContain("max(srcRgb, dstRgb)"); - }); - - it("should handle zero alpha cases", () => - { - const shader = BlendModeShader.getLightenShader(); - - expect(shader).toContain("if (src.a == 0.0) { return dst; }"); - expect(shader).toContain("if (dst.a == 0.0) { return src; }"); - }); - - it("should unpremultiply colors for blending", () => - { - const shader = BlendModeShader.getLightenShader(); - - expect(shader).toContain("srcRgb = src.rgb / src.a"); - expect(shader).toContain("dstRgb = dst.rgb / dst.a"); - }); - }); - - describe("getDarkenShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getDarkenShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getDarkenShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should implement darken blend using min", () => - { - const shader = BlendModeShader.getDarkenShader(); - - expect(shader).toContain("min(srcRgb, dstRgb)"); - }); - - it("should handle zero alpha cases", () => - { - const shader = BlendModeShader.getDarkenShader(); - - expect(shader).toContain("if (src.a == 0.0) { return dst; }"); - expect(shader).toContain("if (dst.a == 0.0) { return src; }"); - }); - }); - - describe("getOverlayShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getOverlayShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getOverlayShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should implement branchless overlay blend with step on dst", () => - { - const shader = BlendModeShader.getOverlayShader(); - - expect(shader).toContain("step(vec3(0.5), dstRgb)"); - expect(shader).toContain("mix(lo, hi, s)"); - }); - }); - - describe("getHardLightShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getHardLightShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getHardLightShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should implement branchless hard light blend with step on src", () => - { - const shader = BlendModeShader.getHardLightShader(); - - expect(shader).toContain("step(vec3(0.5), srcRgb)"); - expect(shader).toContain("mix(lo, hi, s)"); - }); - }); - - describe("getDifferenceShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getDifferenceShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getDifferenceShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should implement difference blend using abs", () => - { - const shader = BlendModeShader.getDifferenceShader(); - - expect(shader).toContain("abs(srcRgb - dstRgb)"); - }); - }); - - describe("getSubtractShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getSubtractShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getSubtractShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should implement subtract blend (dst - src)", () => - { - const shader = BlendModeShader.getSubtractShader(); - - expect(shader).toContain("dstRgb - srcRgb"); - }); - - it("should handle zero alpha cases", () => - { - const shader = BlendModeShader.getSubtractShader(); - - expect(shader).toContain("if (src.a == 0.0) { return dst; }"); - expect(shader).toContain("if (dst.a == 0.0) { return src; }"); - }); - }); - - describe("common shader properties", () => - { - const shaders = [ - { name: "Multiply", fn: BlendModeShader.getMultiplyShader }, - { name: "Screen", fn: BlendModeShader.getScreenShader }, - { name: "Lighten", fn: BlendModeShader.getLightenShader }, - { name: "Darken", fn: BlendModeShader.getDarkenShader }, - { name: "Overlay", fn: BlendModeShader.getOverlayShader }, - { name: "HardLight", fn: BlendModeShader.getHardLightShader }, - { name: "Difference", fn: BlendModeShader.getDifferenceShader }, - { name: "Subtract", fn: BlendModeShader.getSubtractShader } - ]; - - shaders.forEach(({ name, fn }) => - { - it(`${name} shader should include textureSample calls`, () => - { - const shader = fn(); - - expect(shader).toContain("textureSample"); - }); - - it(`${name} shader should apply color transform`, () => - { - const shader = fn(); - - expect(shader).toContain("uniforms.colorTransform"); - }); - - it(`${name} shader should have sampler binding`, () => - { - const shader = fn(); - - expect(shader).toContain("var sampler0: sampler"); - }); - }); - }); -}); diff --git a/packages/webgpu/src/Shader/BlendModeShader.ts b/packages/webgpu/src/Shader/BlendModeShader.ts deleted file mode 100644 index f709fdb6..00000000 --- a/packages/webgpu/src/Shader/BlendModeShader.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { BlendModeVertex } from "./wgsl/vertex/FilterVertex"; -import { - MultiplyBlendFragment, - ScreenBlendFragment, - LightenBlendFragment, - DarkenBlendFragment, - OverlayBlendFragment, - HardLightBlendFragment, - DifferenceBlendFragment, - SubtractBlendFragment -} from "./wgsl/fragment/BlendFragment"; - -/** - * @description WebGPU用ブレンドモードシェーダー - * Blend mode shaders for WebGPU - */ -export class BlendModeShader -{ - /** - * @description ブレンドモード用の頂点シェーダー - * @return {string} - */ - static getVertexShader(): string - { - return BlendModeVertex; - } - - /** - * @description Multiplyブレンド用のフラグメントシェーダー - * @return {string} - */ - static getMultiplyShader(): string - { - return MultiplyBlendFragment; - } - - /** - * @description Screenブレンド用のフラグメントシェーダー - * @return {string} - */ - static getScreenShader(): string - { - return ScreenBlendFragment; - } - - /** - * @description Lightenブレンド用のフラグメントシェーダー - * @return {string} - */ - static getLightenShader(): string - { - return LightenBlendFragment; - } - - /** - * @description Darkenブレンド用のフラグメントシェーダー - * @return {string} - */ - static getDarkenShader(): string - { - return DarkenBlendFragment; - } - - /** - * @description Overlayブレンド用のフラグメントシェーダー - * @return {string} - */ - static getOverlayShader(): string - { - return OverlayBlendFragment; - } - - /** - * @description Hard Lightブレンド用のフラグメントシェーダー - * @return {string} - */ - static getHardLightShader(): string - { - return HardLightBlendFragment; - } - - /** - * @description Differenceブレンド用のフラグメントシェーダー - * @return {string} - */ - static getDifferenceShader(): string - { - return DifferenceBlendFragment; - } - - /** - * @description Subtractブレンド用のフラグメントシェーダー - * @return {string} - */ - static getSubtractShader(): string - { - return SubtractBlendFragment; - } -} diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/usecase/GradientLUTGenerateDataUseCase.test.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/usecase/GradientLUTGenerateDataUseCase.test.ts deleted file mode 100644 index 6d0c7884..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/usecase/GradientLUTGenerateDataUseCase.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { execute } from "./GradientLUTGenerateDataUseCase"; -import { describe, expect, it } from "vitest"; - -describe("GradientLUTGenerateDataUseCase.ts method test", () => -{ - it("test case - generates LUT data for simple gradient", () => - { - const stops = [ - 0, 1, 0, 0, 1, // ratio=0, red - 1, 0, 0, 1, 1 // ratio=1, blue - ]; - - const result = execute(stops, 0); - - expect(result.pixels).toBeInstanceOf(Uint8Array); - expect(result.resolution).toBe(64); // 2 stops = 64px - expect(result.pixels.length).toBe(64 * 4); - }); - - it("test case - resolution adapts to stop count", () => - { - // 5ストップのグラデーション - const stops = [ - 0, 1, 0, 0, 1, - 0.25, 1, 1, 0, 1, - 0.5, 0, 1, 0, 1, - 0.75, 0, 1, 1, 1, - 1, 0, 0, 1, 1 - ]; - - const result = execute(stops, 0); - - expect(result.resolution).toBe(256); // 5 stops = 256px - }); - - it("test case - first pixel is start color", () => - { - const stops = [ - 0, 1, 0.5, 0.25, 1, // ratio=0 - 1, 0, 0, 1, 1 // ratio=1 - ]; - - const result = execute(stops, 0); - - expect(result.pixels[0]).toBe(255); // r - expect(result.pixels[1]).toBe(128); // g (0.5 * 255) - expect(result.pixels[2]).toBe(64); // b (0.25 * 255) - expect(result.pixels[3]).toBe(255); // a - }); - - it("test case - last pixel is end color", () => - { - const stops = [ - 0, 1, 0, 0, 1, - 1, 0.5, 0.25, 1, 0.5 // ratio=1 - ]; - - const result = execute(stops, 0); - - const lastIndex = (result.resolution - 1) * 4; - expect(result.pixels[lastIndex]).toBe(128); // r (0.5 * 255) - expect(result.pixels[lastIndex + 1]).toBe(64); // g (0.25 * 255) - expect(result.pixels[lastIndex + 2]).toBe(255); // b - expect(result.pixels[lastIndex + 3]).toBe(128); // a (0.5 * 255) - }); - - it("test case - respects custom resolution limits", () => - { - const stops = [ - 0, 1, 0, 0, 1, - 1, 0, 0, 1, 1 - ]; - - const result = execute(stops, 0, 128, 128); - - expect(result.resolution).toBe(128); - }); - - it("test case - handles unsorted stops", () => - { - const stops = [ - 1, 0, 0, 1, 1, // ratio=1 (end) - 0, 1, 0, 0, 1 // ratio=0 (start) - ]; - - const result = execute(stops, 0); - - // 内部でソートされるので、最初のピクセルは赤になるはず - expect(result.pixels[0]).toBe(255); // r - expect(result.pixels[1]).toBe(0); // g - expect(result.pixels[2]).toBe(0); // b - }); - - it("test case - white gradient with varying alpha (0xffffff alpha 1.0 to 0.6)", () => - { - // Issue: 0xffffff (white) colors and transparency/alpha gradients were not displaying - const stops = [ - 0, 1, 1, 1, 1, // ratio=0, white with alpha=1.0 - 1, 1, 1, 1, 0.6 // ratio=1, white with alpha=0.6 - ]; - - const result = execute(stops, 0); - - // First pixel: white with full alpha - expect(result.pixels[0]).toBe(255); // r - expect(result.pixels[1]).toBe(255); // g - expect(result.pixels[2]).toBe(255); // b - expect(result.pixels[3]).toBe(255); // a (1.0) - - // Last pixel: white with 60% alpha - const lastIndex = (result.resolution - 1) * 4; - expect(result.pixels[lastIndex]).toBe(255); // r - expect(result.pixels[lastIndex + 1]).toBe(255); // g - expect(result.pixels[lastIndex + 2]).toBe(255); // b - expect(result.pixels[lastIndex + 3]).toBe(153); // a (0.6 * 255 = 153) - }); - - it("test case - alpha-only gradient (same color, different alphas)", () => - { - const stops = [ - 0, 0.8, 0.4, 0.2, 1, // ratio=0, color with alpha=1.0 - 1, 0.8, 0.4, 0.2, 0 // ratio=1, same color with alpha=0.0 - ]; - - const result = execute(stops, 0); - - // First pixel: full alpha - expect(result.pixels[3]).toBe(255); // a (1.0) - - // Last pixel: zero alpha (fully transparent) - const lastIndex = (result.resolution - 1) * 4; - expect(result.pixels[lastIndex + 3]).toBe(0); // a (0.0) - - // Middle pixel should have ~50% alpha - const midIndex = Math.floor(result.resolution / 2) * 4; - expect(result.pixels[midIndex + 3]).toBeGreaterThan(100); - expect(result.pixels[midIndex + 3]).toBeLessThan(156); - }); -}); diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/usecase/GradientLUTGenerateDataUseCase.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/usecase/GradientLUTGenerateDataUseCase.ts deleted file mode 100644 index f9659e38..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/usecase/GradientLUTGenerateDataUseCase.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { IGradientLUTData } from "../../../interface/IGradientLUTData"; -import { execute as gradientLUTParseStopsService } from "../service/GradientLUTParseStopsService"; -import { execute as gradientLUTCalculateResolutionService } from "../service/GradientLUTCalculateResolutionService"; -import { execute as gradientLUTGeneratePixelsService } from "../service/GradientLUTGeneratePixelsService"; - -/** - * @description グラデーションLUTのピクセルデータを生成 - * Generate gradient LUT pixel data - * - * @param {number[]} stops - [ratio, r, g, b, a, ratio, r, g, b, a, ...] - * @param {number} interpolation - 0: RGB, 1: Linear RGB - * @param {number} [minResolution=64] - 最小解像度 - * @param {number} [maxResolution=512] - 最大解像度 - * @return {IGradientLUTData} - * @method - * @protected - */ -export const execute = ( - stops: number[], - interpolation: number, - minResolution: number = 64, - maxResolution: number = 512 -): IGradientLUTData => { - - // ストップ配列をパースしてソート - const parsedStops = gradientLUTParseStopsService(stops); - - // ストップ数に応じた解像度を計算 - const resolution = gradientLUTCalculateResolutionService( - parsedStops.length, - minResolution, - maxResolution - ); - - // ピクセルデータを生成 - const pixels = gradientLUTGeneratePixelsService( - parsedStops, - resolution, - interpolation - ); - - return { pixels, resolution }; -}; diff --git a/packages/webgpu/src/Shader/wgsl/fragment/BlendFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/BlendFragment.ts deleted file mode 100644 index b3cf048d..00000000 --- a/packages/webgpu/src/Shader/wgsl/fragment/BlendFragment.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @description ブレンドシェーダー共通ヘッダー - */ -const BLEND_HEADER = /* wgsl */`struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, -} - -struct BlendUniforms { - colorTransform: vec4, - addColor: vec4, -} - -@group(0) @binding(0) var uniforms: BlendUniforms; -@group(0) @binding(1) var sampler0: sampler; -@group(0) @binding(2) var texture0: texture_2d; -@group(0) @binding(3) var texture1: texture_2d; -`; - -/** - * @description alpha guard 付きブレンドシェーダーを生成 - */ -const createBlendFragment = (blendLogic: string): string => - BLEND_HEADER + /* wgsl */` -@fragment -fn main(input: VertexOutput) -> @location(0) vec4 { - var src = textureSampleLevel(texture1, sampler0, input.texCoord, 0); - var dst = textureSampleLevel(texture0, sampler0, input.texCoord, 0); - if (src.a == 0.0) { return dst; } - if (dst.a == 0.0) { return src; } - src = src * uniforms.colorTransform + vec4(uniforms.addColor.rgb, 0.0); - let a = src - src * dst.a; - let b = dst - dst * src.a; - var srcRgb = src.rgb / src.a; - var dstRgb = dst.rgb / dst.a; -${blendLogic} - c = vec4(c.rgb * c.a, c.a); - return a + b + c; -} -`; - -export const MultiplyBlendFragment = BLEND_HEADER + /* wgsl */` -@fragment -fn main(input: VertexOutput) -> @location(0) vec4 { - var src = textureSampleLevel(texture1, sampler0, input.texCoord, 0); - var dst = textureSampleLevel(texture0, sampler0, input.texCoord, 0); - src = src * uniforms.colorTransform + vec4(uniforms.addColor.rgb, 0.0); - let a = src - src * dst.a; - let b = dst - dst * src.a; - let c = src * dst; - return a + b + c; -} -`; - -export const ScreenBlendFragment = createBlendFragment( - " var c = vec4(srcRgb + dstRgb - srcRgb * dstRgb, src.a * dst.a);" -); - -export const LightenBlendFragment = createBlendFragment( - " var c = vec4(max(srcRgb, dstRgb), src.a * dst.a);" -); - -export const DarkenBlendFragment = createBlendFragment( - " var c = vec4(min(srcRgb, dstRgb), src.a * dst.a);" -); - -export const OverlayBlendFragment = createBlendFragment( - ` let s = step(vec3(0.5), dstRgb); - let lo = 2.0 * srcRgb * dstRgb; - let hi = 1.0 - 2.0 * (1.0 - srcRgb) * (1.0 - dstRgb); - var c = vec4(mix(lo, hi, s), src.a * dst.a);` -); - -export const HardLightBlendFragment = createBlendFragment( - ` let s = step(vec3(0.5), srcRgb); - let lo = 2.0 * srcRgb * dstRgb; - let hi = 1.0 - 2.0 * (1.0 - srcRgb) * (1.0 - dstRgb); - var c = vec4(mix(lo, hi, s), src.a * dst.a);` -); - -export const DifferenceBlendFragment = createBlendFragment( - " var c = vec4(abs(srcRgb - dstRgb), src.a * dst.a);" -); - -export const SubtractBlendFragment = createBlendFragment( - " var c = vec4(max(dstRgb - srcRgb, vec3(0.0)), src.a * dst.a);" -); diff --git a/packages/webgpu/src/Shader/wgsl/vertex/FilterVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/FilterVertex.ts index 204d76ad..2d8338f0 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/FilterVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/FilterVertex.ts @@ -102,26 +102,6 @@ ${WgslUnitQuadVertices} } `; -export const BlendModeVertex = /* wgsl */` -struct VertexInput { - @location(0) position: vec2, - @location(1) texCoord: vec2, -} - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, -} - -@vertex -fn main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - output.position = vec4(input.position, 0.0, 1.0); - output.texCoord = input.texCoord; - return output; -} -`; - const ScaleUniformsAndStruct = ` struct ScaleUniforms { matrix: vec4, diff --git a/packages/webgpu/src/TexturePool/service/TexturePoolEvictOldestService.test.ts b/packages/webgpu/src/TexturePool/service/TexturePoolEvictOldestService.test.ts deleted file mode 100644 index 25cc4071..00000000 --- a/packages/webgpu/src/TexturePool/service/TexturePoolEvictOldestService.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; -import type { IPooledTexture } from "../../interface/IPooledTexture"; -import { execute } from "./TexturePoolEvictOldestService"; - -describe("TexturePoolEvictOldestService", () => -{ - let pool: IPooledTexture[]; - - const createMockEntry = ( - lastUsedFrame: number, - inUse: boolean = false - ): IPooledTexture => ({ - "texture": { - "destroy": vi.fn() - } as unknown as GPUTexture, - "width": 256, - "height": 256, - "format": "rgba8unorm" as GPUTextureFormat, - inUse, - lastUsedFrame - }); - - beforeEach(() => - { - pool = []; - }); - - it("should evict the oldest unused entry", () => - { - const oldest = createMockEntry(10, false); - const middle = createMockEntry(50, false); - const recent = createMockEntry(100, false); - pool.push(oldest, middle, recent); - - execute(pool); - - expect(pool.length).toBe(2); - expect(pool).not.toContain(oldest); - expect(oldest.texture.destroy).toHaveBeenCalled(); - }); - - it("should skip entries that are in use", () => - { - const oldestInUse = createMockEntry(10, true); - const oldestNotInUse = createMockEntry(50, false); - pool.push(oldestInUse, oldestNotInUse); - - execute(pool); - - expect(pool.length).toBe(1); - expect(pool[0]).toBe(oldestInUse); - expect(oldestInUse.texture.destroy).not.toHaveBeenCalled(); - expect(oldestNotInUse.texture.destroy).toHaveBeenCalled(); - }); - - it("should handle empty pool", () => - { - expect(() => execute(pool)).not.toThrow(); - expect(pool.length).toBe(0); - }); - - it("should not evict anything if all entries are in use", () => - { - pool.push( - createMockEntry(10, true), - createMockEntry(50, true), - createMockEntry(100, true) - ); - - execute(pool); - - expect(pool.length).toBe(3); - }); - - it("should only evict one entry per call", () => - { - pool.push( - createMockEntry(10, false), - createMockEntry(20, false), - createMockEntry(30, false) - ); - - execute(pool); - expect(pool.length).toBe(2); - - execute(pool); - expect(pool.length).toBe(1); - }); - - it("should call destroy on evicted texture", () => - { - const entry = createMockEntry(10, false); - pool.push(entry); - - execute(pool); - - expect(entry.texture.destroy).toHaveBeenCalledTimes(1); - }); - - it("should correctly identify oldest among multiple unused", () => - { - const recent = createMockEntry(100, false); - const oldest = createMockEntry(5, false); - const middle = createMockEntry(50, false); - pool.push(recent, oldest, middle); - - execute(pool); - - expect(pool.length).toBe(2); - expect(pool).not.toContain(oldest); - expect(pool).toContain(recent); - expect(pool).toContain(middle); - }); -}); diff --git a/packages/webgpu/src/TexturePool/service/TexturePoolEvictOldestService.ts b/packages/webgpu/src/TexturePool/service/TexturePoolEvictOldestService.ts deleted file mode 100644 index 34184e61..00000000 --- a/packages/webgpu/src/TexturePool/service/TexturePoolEvictOldestService.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { IPooledTexture } from "../../interface/IPooledTexture"; - -/** - * @description 最も古い未使用エントリを削除 - * Evict the oldest unused pool entry - * - * @param {IPooledTexture[]} pool - * @return {void} - * @method - * @protected - */ -export const execute = (pool: IPooledTexture[]): void => { - let oldestIndex = -1; - let oldestFrame = Infinity; - - for (let i = 0; i < pool.length; i++) { - const entry = pool[i]; - if (!entry.inUse && entry.lastUsedFrame < oldestFrame) { - oldestFrame = entry.lastUsedFrame; - oldestIndex = i; - } - } - - if (oldestIndex >= 0) { - pool[oldestIndex].texture.destroy(); - pool.splice(oldestIndex, 1); - } -}; diff --git a/packages/webgpu/src/WebGPUUtil.test.ts b/packages/webgpu/src/WebGPUUtil.test.ts index c81c8672..95ac398a 100644 --- a/packages/webgpu/src/WebGPUUtil.test.ts +++ b/packages/webgpu/src/WebGPUUtil.test.ts @@ -1,7 +1,6 @@ import { describe, it, expect, beforeEach } from "vitest"; import { $samples, - $setSamples, WebGPUUtil, $context, $setContext, @@ -13,18 +12,9 @@ describe("WebGPUUtil", () => { describe("$samples", () => { - it("should default to 1", () => + it("should have default value", () => { - $setSamples(1); - expect($samples).toBe(1); - }); - - it("should set samples", () => - { - $setSamples(4); - expect($samples).toBe(4); - - $setSamples(1); // Reset + expect(typeof $samples).toBe("number"); }); }); diff --git a/packages/webgpu/src/WebGPUUtil.ts b/packages/webgpu/src/WebGPUUtil.ts index d56298c4..6c5be6ac 100644 --- a/packages/webgpu/src/WebGPUUtil.ts +++ b/packages/webgpu/src/WebGPUUtil.ts @@ -9,21 +9,7 @@ * @note WebGL版と同じくMSAA 4xをデフォルトで有効化 * 曲線のアンチエイリアス品質向上のため */ -export let $samples: number = 4; - -/** - * @description 描画のサンプリング数を変更 - * Change the number of samples for drawing - * - * @param {number} samples - * @return {void} - * @method - * @protected - */ -export const $setSamples = (samples: number): void => -{ - $samples = samples; -}; +export const $samples: number = 4; export class WebGPUUtil { diff --git a/packages/webgpu/src/interface/IRectangleInfo.ts b/packages/webgpu/src/interface/IRectangleInfo.ts deleted file mode 100644 index 60a983d5..00000000 --- a/packages/webgpu/src/interface/IRectangleInfo.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IPoint } from "./IPoint"; -import type { IPath } from "./IPath"; - -/** - * @description 矩形の情報を保持する型 - * Rectangle info type for stroke generation - */ -export interface IRectangleInfo { - path: IPath; - startUp: IPoint; - startDown: IPoint; - endUp: IPoint; - endDown: IPoint; -} From 8c2980909a0daf03b62b27947bdbca174a7f3d1a Mon Sep 17 00:00:00 2001 From: ienaga Date: Sat, 14 Mar 2026 00:31:52 +0900 Subject: [PATCH 04/26] =?UTF-8?q?#260=20webgpu=E7=89=88=E3=81=AE=E3=83=AA?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/webgpu/src/Blend.test.ts | 21 ------------------- packages/webgpu/src/Blend.ts | 7 ------- .../webgpu/src/interface/IGradientLUTData.ts | 8 ------- 3 files changed, 36 deletions(-) delete mode 100644 packages/webgpu/src/interface/IGradientLUTData.ts diff --git a/packages/webgpu/src/Blend.test.ts b/packages/webgpu/src/Blend.test.ts index 53359038..3d80fa6a 100644 --- a/packages/webgpu/src/Blend.test.ts +++ b/packages/webgpu/src/Blend.test.ts @@ -2,8 +2,6 @@ import { describe, it, expect, beforeEach } from "vitest"; import { $setCurrentBlendMode, $currentBlendMode, - $setFuncCode, - $funcCode, $getBlendState } from "./Blend"; @@ -12,7 +10,6 @@ describe("Blend", () => beforeEach(() => { $setCurrentBlendMode("normal"); - $setFuncCode(0); }); describe("blend mode", () => @@ -42,24 +39,6 @@ describe("Blend", () => }); }); - describe("func code", () => - { - it("should default to 0", () => - { - $setFuncCode(0); - expect($funcCode).toBe(0); - }); - - it("should set and get func code", () => - { - $setFuncCode(123); - expect($funcCode).toBe(123); - - $setFuncCode(456); - expect($funcCode).toBe(456); - }); - }); - describe("$getBlendState", () => { it("should return normal blend state", () => diff --git a/packages/webgpu/src/Blend.ts b/packages/webgpu/src/Blend.ts index 2dfe18c8..0ddf00c2 100644 --- a/packages/webgpu/src/Blend.ts +++ b/packages/webgpu/src/Blend.ts @@ -5,18 +5,11 @@ export type { IBlendState }; export let $currentBlendMode: IBlendMode = "normal"; -export let $funcCode: number = 0; - export const $setCurrentBlendMode = (blend_mode: IBlendMode): void => { $currentBlendMode = blend_mode; }; -export const $setFuncCode = (code: number): void => -{ - $funcCode = code; -}; - export const $getBlendState = (mode: IBlendMode): IBlendState => { switch (mode) { diff --git a/packages/webgpu/src/interface/IGradientLUTData.ts b/packages/webgpu/src/interface/IGradientLUTData.ts deleted file mode 100644 index 2213b222..00000000 --- a/packages/webgpu/src/interface/IGradientLUTData.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @description グラデーションLUTデータを生成する結果型 - * Result type for generated gradient LUT data - */ -export interface IGradientLUTData { - pixels: Uint8Array; - resolution: number; -} From 9252019243a9af8720b3de78bfca9515336cb830 Mon Sep 17 00:00:00 2001 From: ienaga Date: Sat, 14 Mar 2026 08:34:20 +0900 Subject: [PATCH 05/26] =?UTF-8?q?#260=20=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=B1=E3=83=BC=E3=82=B9=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sound/service/SoundDecodeService.test.ts | 26 ++ .../VideoBuildFromCharacterUseCase.test.ts | 77 ++++++ .../service/CommandRemoveCacheService.test.ts | 70 ++++++ ...ayObjectContainerClipRenderUseCase.test.ts | 77 ++++++ ...isplayObjectContainerRenderUseCase.test.ts | 137 +++++++++++ .../Shape/service/ShapeCommandService.test.ts | 192 +++++++++++++++ .../usecase/ShapeClipRenderUseCase.test.ts | 86 +++++++ .../Shape/usecase/ShapeRenderUseCase.test.ts | 127 ++++++++++ .../TextFiledGetAlignOffsetService.test.ts | 205 ++++++++++++++++ ...extFieldDrawOffscreenCanvasUseCase.test.ts | 199 +++++++++++++++ .../usecase/TextFieldRenderUseCase.test.ts | 154 ++++++++++++ .../Video/usecase/VideoRenderUseCase.test.ts | 187 ++++++++++++++ .../ContextContainerBeginLayerUseCase.test.ts | 75 ++++++ ...xtContainerDrawCachedFilterUseCase.test.ts | 93 +++++++ .../ContextContainerEndLayerUseCase.test.ts | 122 +++++++++ ...ManagerUpdateIndirectBufferService.test.ts | 74 ++++++ ...anagerCleanupStorageBuffersUseCase.test.ts | 63 +++++ ...ManagerReleaseStorageBufferUseCase.test.ts | 55 +++++ .../ContextFillWithStencilMainService.test.ts | 103 ++++++++ .../ContextContainerEndLayerUseCase.test.ts | 231 ++++++++++++++++++ 20 files changed, 2353 insertions(+) create mode 100644 packages/media/src/Sound/service/SoundDecodeService.test.ts create mode 100644 packages/media/src/Video/usecase/VideoBuildFromCharacterUseCase.test.ts create mode 100644 packages/renderer/src/Command/service/CommandRemoveCacheService.test.ts create mode 100644 packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerClipRenderUseCase.test.ts create mode 100644 packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.test.ts create mode 100644 packages/renderer/src/Shape/service/ShapeCommandService.test.ts create mode 100644 packages/renderer/src/Shape/usecase/ShapeClipRenderUseCase.test.ts create mode 100644 packages/renderer/src/Shape/usecase/ShapeRenderUseCase.test.ts create mode 100644 packages/renderer/src/TextField/service/TextFiledGetAlignOffsetService.test.ts create mode 100644 packages/renderer/src/TextField/usecase/TextFieldDrawOffscreenCanvasUseCase.test.ts create mode 100644 packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.test.ts create mode 100644 packages/renderer/src/Video/usecase/VideoRenderUseCase.test.ts create mode 100644 packages/webgl/src/Context/usecase/ContextContainerBeginLayerUseCase.test.ts create mode 100644 packages/webgl/src/Context/usecase/ContextContainerDrawCachedFilterUseCase.test.ts create mode 100644 packages/webgl/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts create mode 100644 packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.test.ts create mode 100644 packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.test.ts create mode 100644 packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.test.ts create mode 100644 packages/webgpu/src/Context/service/ContextFillWithStencilMainService.test.ts create mode 100644 packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts diff --git a/packages/media/src/Sound/service/SoundDecodeService.test.ts b/packages/media/src/Sound/service/SoundDecodeService.test.ts new file mode 100644 index 00000000..251ad72d --- /dev/null +++ b/packages/media/src/Sound/service/SoundDecodeService.test.ts @@ -0,0 +1,26 @@ +import { execute } from "./SoundDecodeService"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../../MediaUtil", () => ({ + "$getAudioContext": vi.fn(() => ({ + "decodeAudioData": vi.fn(() => Promise.resolve({ "length": 100 })) + })) +})); + +describe("SoundDecodeService.js test", () => { + + it("execute test case1 - empty buffer returns undefined", async () => + { + const emptyBuffer = new ArrayBuffer(0); + const result = await execute(emptyBuffer); + expect(result).toBeUndefined(); + }); + + it("execute test case2 - successful decode", async () => + { + const buffer = new ArrayBuffer(10); + new Uint8Array(buffer).set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + const result = await execute(buffer); + expect(result).toEqual({ "length": 100 }); + }); +}); diff --git a/packages/media/src/Video/usecase/VideoBuildFromCharacterUseCase.test.ts b/packages/media/src/Video/usecase/VideoBuildFromCharacterUseCase.test.ts new file mode 100644 index 00000000..a472cded --- /dev/null +++ b/packages/media/src/Video/usecase/VideoBuildFromCharacterUseCase.test.ts @@ -0,0 +1,77 @@ +import { execute } from "./VideoBuildFromCharacterUseCase"; +import { describe, expect, it, vi } from "vitest"; + +describe("VideoBuildFromCharacterUseCase.js test", () => { + + it("execute test case1 - build video from character with buffer", () => + { + const mockVideo = { + "loop": false, + "autoPlay": false, + "videoWidth": 0, + "videoHeight": 0, + "volume": 0, + "src": "" + } as any; + + const character = { + "loop": true, + "autoPlay": true, + "bounds": { "xMax": 320, "yMax": 240 }, + "volume": 0.8, + "buffer": [0, 1, 2, 3], + "videoData": null + } as any; + + // Mock URL.createObjectURL + const originalCreateObjectURL = URL.createObjectURL; + URL.createObjectURL = vi.fn(() => "blob:mock-url"); + + execute(mockVideo, character); + + expect(mockVideo.loop).toBe(true); + expect(mockVideo.autoPlay).toBe(true); + expect(mockVideo.videoWidth).toBe(320); + expect(mockVideo.videoHeight).toBe(240); + expect(mockVideo.volume).toBe(0.8); + expect(mockVideo.src).toBe("blob:mock-url"); + expect(character.videoData).toBeInstanceOf(Uint8Array); + expect(character.buffer).toBeNull(); + + URL.createObjectURL = originalCreateObjectURL; + }); + + it("execute test case2 - reuse existing videoData", () => + { + const mockVideo = { + "loop": false, + "autoPlay": false, + "videoWidth": 0, + "videoHeight": 0, + "volume": 0, + "src": "" + } as any; + + const existingVideoData = new Uint8Array([10, 20, 30]); + const character = { + "loop": false, + "autoPlay": false, + "bounds": { "xMax": 640, "yMax": 480 }, + "volume": 1.0, + "buffer": [5, 6, 7], + "videoData": existingVideoData + } as any; + + const originalCreateObjectURL = URL.createObjectURL; + URL.createObjectURL = vi.fn(() => "blob:mock-url-2"); + + execute(mockVideo, character); + + expect(character.videoData).toBe(existingVideoData); + expect(character.buffer).not.toBeNull(); + expect(mockVideo.videoWidth).toBe(640); + expect(mockVideo.videoHeight).toBe(480); + + URL.createObjectURL = originalCreateObjectURL; + }); +}); diff --git a/packages/renderer/src/Command/service/CommandRemoveCacheService.test.ts b/packages/renderer/src/Command/service/CommandRemoveCacheService.test.ts new file mode 100644 index 00000000..877b6efd --- /dev/null +++ b/packages/renderer/src/Command/service/CommandRemoveCacheService.test.ts @@ -0,0 +1,70 @@ +import { execute } from "./CommandRemoveCacheService"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@next2d/cache", () => { + const store = new Map>(); + return { + "$cacheStore": { + "has": vi.fn((key: string) => store.has(key)), + "getById": vi.fn((key: string) => store.get(key)), + "removeById": vi.fn((key: string) => store.delete(key)), + "_store": store + } + }; +}); + +vi.mock("../../RendererUtil", () => ({ + "$context": { + "removeNode": vi.fn() + } +})); + +describe("CommandRemoveCacheService.js test", () => { + + it("execute test case1 - remove existing cache entries", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + const { $context } = await import("../../RendererUtil"); + + const node1 = { "id": 1 }; + const node2 = { "id": 2 }; + const cache = new Map(); + cache.set("a", node1); + cache.set("b", node2); + + ($cacheStore as any)._store.set("1", cache); + + const keys = new Float32Array([1]); + execute(keys); + + expect($cacheStore.has).toHaveBeenCalledWith("1"); + expect($cacheStore.getById).toHaveBeenCalledWith("1"); + expect($context.removeNode).toHaveBeenCalledTimes(2); + expect($cacheStore.removeById).toHaveBeenCalledWith("1"); + }); + + it("execute test case2 - skip non-existing cache keys", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + + vi.mocked($cacheStore.has).mockReturnValue(false as any); + + const keys = new Float32Array([999]); + execute(keys); + + expect($cacheStore.has).toHaveBeenCalledWith("999"); + expect($cacheStore.getById).not.toHaveBeenCalledWith("999"); + }); + + it("execute test case3 - empty keys array", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + + vi.mocked($cacheStore.has).mockClear(); + + const keys = new Float32Array([]); + execute(keys); + + expect($cacheStore.has).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerClipRenderUseCase.test.ts b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerClipRenderUseCase.test.ts new file mode 100644 index 00000000..ca8f7aa8 --- /dev/null +++ b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerClipRenderUseCase.test.ts @@ -0,0 +1,77 @@ +import { execute } from "./DisplayObjectContainerClipRenderUseCase"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../../Shape/usecase/ShapeClipRenderUseCase", () => ({ + "execute": vi.fn((render_queue: Float32Array, index: number) => { + // Skip matrix(6) + isGridEnabled(1) + length(1) + commands(n) + index += 6; // matrix + const isGrid = Boolean(render_queue[index++]); + if (isGrid) { index += 24; } + const len = render_queue[index++]; + index += len; + return index; + }) +})); + +describe("DisplayObjectContainerClipRenderUseCase.js test", () => { + + it("execute test case1 - empty container", () => + { + const renderQueue = new Float32Array([0]); // length = 0 + const result = execute(renderQueue, 0); + expect(result).toBe(1); + }); + + it("execute test case2 - shape clip child", async () => + { + const shapeClipMod = await import("../../Shape/usecase/ShapeClipRenderUseCase"); + vi.mocked(shapeClipMod.execute).mockClear(); + + // length(1) + type(1) + shape data + const data: number[] = []; + data.push(1); // 1 child + data.push(0x01); // type = shape + // matrix (6) + data.push(1, 0, 0, 1, 0, 0); + // isGridEnabled + data.push(0); + // command length + data.push(2); + // commands + data.push(9, 12); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0); + + expect(shapeClipMod.execute).toHaveBeenCalledTimes(1); + expect(result).toBe(data.length); + }); + + it("execute test case3 - nested container clip", () => + { + // outer: length=1, type=container(0x00) + // inner: length=0 + const data: number[] = []; + data.push(1); // 1 child + data.push(0x00); // type = container + data.push(0); // inner length = 0 + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0); + + expect(result).toBe(data.length); + }); + + it("execute test case4 - unknown type is skipped", () => + { + const data: number[] = []; + data.push(1); // 1 child + data.push(0x02); // type = text (not handled in clip) + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0); + + // Should consume length + type but nothing else + expect(result).toBe(2); + }); +}); diff --git a/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.test.ts b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.test.ts new file mode 100644 index 00000000..c379b666 --- /dev/null +++ b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.test.ts @@ -0,0 +1,137 @@ +import { execute } from "./DisplayObjectContainerRenderUseCase"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../../RendererUtil", () => ({ + "$context": { + "drawArraysInstanced": vi.fn(), + "save": vi.fn(), + "restore": vi.fn(), + "beginMask": vi.fn(), + "endMask": vi.fn(), + "leaveMask": vi.fn(), + "setMaskBounds": vi.fn(), + "containerBeginLayer": vi.fn(), + "containerEndLayer": vi.fn(), + "containerDrawCachedFilter": vi.fn(), + "globalAlpha": 1, + "imageSmoothingEnabled": true, + "globalCompositeOperation": "normal" + } +})); + +vi.mock("../../Shape/usecase/ShapeRenderUseCase", () => ({ + "execute": vi.fn((_rq: Float32Array, index: number) => index) +})); + +vi.mock("../../Shape/usecase/ShapeClipRenderUseCase", () => ({ + "execute": vi.fn((_rq: Float32Array, index: number) => index) +})); + +vi.mock("../../TextField/usecase/TextFieldRenderUseCase", () => ({ + "execute": vi.fn((_rq: Float32Array, index: number) => index) +})); + +vi.mock("../../Video/usecase/VideoRenderUseCase", () => ({ + "execute": vi.fn((_rq: Float32Array, index: number) => index) +})); + +vi.mock("./DisplayObjectContainerClipRenderUseCase", () => ({ + "execute": vi.fn((_rq: Float32Array, index: number) => index) +})); + +vi.mock("../../DisplayObject/service/DisplayObjectGetBlendModeService", () => ({ + "execute": vi.fn(() => "normal") +})); + +describe("DisplayObjectContainerRenderUseCase.js test", () => { + + it("execute test case1 - simple container no layer, no mask, no children", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.containerBeginLayer).mockClear(); + vi.mocked($context.containerEndLayer).mockClear(); + + const data: number[] = []; + // blendMode + data.push(0); + // useLayer = false + data.push(0); + // useMaskDisplayObject = false + data.push(0); + // children length = 0 + data.push(0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + expect($context.containerBeginLayer).not.toHaveBeenCalled(); + expect($context.containerEndLayer).not.toHaveBeenCalled(); + expect(result).toBe(data.length); + }); + + it("execute test case2 - container with useLayer and blend only", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.containerBeginLayer).mockClear(); + vi.mocked($context.containerEndLayer).mockClear(); + + const data: number[] = []; + // blendMode + data.push(0); + // useLayer = true + data.push(1); + // layerWidth, layerHeight + data.push(800, 600); + // useFilter = false + data.push(0); + // matrix (6) + data.push(1, 0, 0, 1, 0, 0); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // useMaskDisplayObject = false + data.push(0); + // children length = 0 + data.push(0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + expect($context.containerBeginLayer).toHaveBeenCalledWith(800, 600); + expect($context.containerEndLayer).toHaveBeenCalledTimes(1); + expect(result).toBe(data.length); + }); + + it("execute test case3 - container with filter cache hit returns early", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.containerDrawCachedFilter).mockClear(); + + const data: number[] = []; + // blendMode + data.push(0); + // useLayer = true + data.push(1); + // layerWidth, layerHeight + data.push(800, 600); + // useFilter = true + data.push(1); + // filterCache = true + data.push(1); + // uniqueKey + data.push(42); + // filterKey + data.push(99); + // filterBounds (4) + data.push(-10, -10, 110, 110); + // matrix (6) + data.push(1, 0, 0, 1, 0, 0); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + expect($context.containerDrawCachedFilter).toHaveBeenCalledTimes(1); + expect(result).toBe(data.length); + }); +}); diff --git a/packages/renderer/src/Shape/service/ShapeCommandService.test.ts b/packages/renderer/src/Shape/service/ShapeCommandService.test.ts new file mode 100644 index 00000000..4cab1305 --- /dev/null +++ b/packages/renderer/src/Shape/service/ShapeCommandService.test.ts @@ -0,0 +1,192 @@ +import { execute } from "./ShapeCommandService"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../../RendererUtil", () => ({ + "$context": { + "beginPath": vi.fn(), + "moveTo": vi.fn(), + "lineTo": vi.fn(), + "quadraticCurveTo": vi.fn(), + "bezierCurveTo": vi.fn(), + "arc": vi.fn(), + "closePath": vi.fn(), + "fillStyle": vi.fn(), + "strokeStyle": vi.fn(), + "fill": vi.fn(), + "stroke": vi.fn(), + "gradientFill": vi.fn(), + "gradientStroke": vi.fn(), + "bitmapFill": vi.fn(), + "bitmapStroke": vi.fn(), + "thickness": 0, + "caps": 0, + "joints": 0, + "miterLimit": 0 + }, + "$getArray": vi.fn(() => []) +})); + +describe("ShapeCommandService.js test", () => { + + it("execute test case1 - BEGIN_PATH command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.beginPath).mockClear(); + + const commands = new Float32Array([9]); // BEGIN_PATH = 9 + execute(commands); + + expect($context.beginPath).toHaveBeenCalledTimes(1); + }); + + it("execute test case2 - MOVE_TO command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.moveTo).mockClear(); + + const commands = new Float32Array([0, 10, 20]); // MOVE_TO = 0 + execute(commands); + + expect($context.moveTo).toHaveBeenCalledWith(10, 20); + }); + + it("execute test case3 - LINE_TO command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.lineTo).mockClear(); + + const commands = new Float32Array([2, 30, 40]); // LINE_TO = 2 + execute(commands); + + expect($context.lineTo).toHaveBeenCalledWith(30, 40); + }); + + it("execute test case4 - CURVE_TO command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.quadraticCurveTo).mockClear(); + + const commands = new Float32Array([1, 10, 20, 30, 40]); // CURVE_TO = 1 + execute(commands); + + expect($context.quadraticCurveTo).toHaveBeenCalledWith(10, 20, 30, 40); + }); + + it("execute test case5 - CUBIC command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.bezierCurveTo).mockClear(); + + const commands = new Float32Array([3, 1, 2, 3, 4, 5, 6]); // CUBIC = 3 + execute(commands); + + expect($context.bezierCurveTo).toHaveBeenCalledWith(1, 2, 3, 4, 5, 6); + }); + + it("execute test case6 - ARC command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.arc).mockClear(); + + const commands = new Float32Array([4, 50, 60, 25]); // ARC = 4 + execute(commands); + + expect($context.arc).toHaveBeenCalledWith(50, 60, 25); + }); + + it("execute test case7 - FILL_STYLE and END_FILL commands", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.fillStyle).mockClear(); + vi.mocked($context.fill).mockClear(); + + // FILL_STYLE(5) r g b a, END_FILL(7) + const commands = new Float32Array([5, 255, 128, 0, 255, 7]); + execute(commands); + + expect($context.fillStyle).toHaveBeenCalledWith(1, 128 / 255, 0, 1); + expect($context.fill).toHaveBeenCalledTimes(1); + }); + + it("execute test case8 - FILL_STYLE skipped in clip mode", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.fillStyle).mockClear(); + + const commands = new Float32Array([5, 255, 128, 0, 255]); + execute(commands, true); + + expect($context.fillStyle).not.toHaveBeenCalled(); + }); + + it("execute test case9 - STROKE_STYLE command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.strokeStyle).mockClear(); + + // STROKE_STYLE(6) thickness caps joints miterLimit r g b a + const commands = new Float32Array([6, 2, 1, 0, 10, 255, 0, 0, 255]); + execute(commands); + + expect($context.thickness).toBe(2); + expect($context.caps).toBe(1); + expect($context.joints).toBe(0); + expect($context.miterLimit).toBe(10); + expect($context.strokeStyle).toHaveBeenCalledWith(1, 0, 0, 1); + }); + + it("execute test case10 - STROKE_STYLE skipped in clip mode", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.strokeStyle).mockClear(); + + const commands = new Float32Array([6, 2, 1, 0, 10, 255, 0, 0, 255]); + execute(commands, true); + + expect($context.strokeStyle).not.toHaveBeenCalled(); + }); + + it("execute test case11 - END_STROKE command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.stroke).mockClear(); + + const commands = new Float32Array([8]); // END_STROKE = 8 + execute(commands); + + expect($context.stroke).toHaveBeenCalledTimes(1); + }); + + it("execute test case12 - END_STROKE skipped in clip mode", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.stroke).mockClear(); + + const commands = new Float32Array([8]); // END_STROKE = 8 + execute(commands, true); + + expect($context.stroke).not.toHaveBeenCalled(); + }); + + it("execute test case13 - CLOSE_PATH command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.closePath).mockClear(); + + const commands = new Float32Array([12]); // CLOSE_PATH = 12 + execute(commands); + + expect($context.closePath).toHaveBeenCalledTimes(1); + }); + + it("execute test case14 - empty commands", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.beginPath).mockClear(); + + const commands = new Float32Array([]); + execute(commands); + + expect($context.beginPath).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/renderer/src/Shape/usecase/ShapeClipRenderUseCase.test.ts b/packages/renderer/src/Shape/usecase/ShapeClipRenderUseCase.test.ts new file mode 100644 index 00000000..9025a36c --- /dev/null +++ b/packages/renderer/src/Shape/usecase/ShapeClipRenderUseCase.test.ts @@ -0,0 +1,86 @@ +import { execute } from "./ShapeClipRenderUseCase"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../../RendererUtil", () => ({ + "$context": { + "reset": vi.fn(), + "setTransform": vi.fn(), + "useGrid": vi.fn(), + "clip": vi.fn() + } +})); + +vi.mock("../service/ShapeCommandService", () => ({ + "execute": vi.fn() +})); + +describe("ShapeClipRenderUseCase.js test", () => { + + it("execute test case1 - basic clip without grid", async () => + { + const { $context } = await import("../../RendererUtil"); + const shapeCommandService = await import("../service/ShapeCommandService"); + + vi.mocked($context.reset).mockClear(); + vi.mocked($context.setTransform).mockClear(); + vi.mocked($context.useGrid).mockClear(); + vi.mocked($context.clip).mockClear(); + vi.mocked(shapeCommandService.execute).mockClear(); + + // matrix(6) + isGridEnabled(1) + length(1) + commands(3) + const renderQueue = new Float32Array([ + 1, 0, 0, 1, 10, 20, // matrix + 0, // isGridEnabled = false + 3, // command length + 9, 0, 5 // commands (BEGIN_PATH, MOVE_TO partial) + ]); + + const resultIndex = execute(renderQueue, 0); + + expect($context.reset).toHaveBeenCalledTimes(1); + expect($context.setTransform).toHaveBeenCalledWith(1, 0, 0, 1, 10, 20); + expect($context.useGrid).toHaveBeenCalledWith(null); + expect(shapeCommandService.execute).toHaveBeenCalledTimes(1); + expect($context.clip).toHaveBeenCalledTimes(1); + expect(resultIndex).toBe(11); + }); + + it("execute test case2 - clip with grid enabled", async () => + { + const { $context } = await import("../../RendererUtil"); + const shapeCommandService = await import("../service/ShapeCommandService"); + + vi.mocked($context.reset).mockClear(); + vi.mocked($context.setTransform).mockClear(); + vi.mocked($context.useGrid).mockClear(); + vi.mocked($context.clip).mockClear(); + vi.mocked(shapeCommandService.execute).mockClear(); + + // matrix(6) + isGridEnabled(1) + gridData(24) + length(1) + commands(2) + const data = new Float32Array(6 + 1 + 24 + 1 + 2); + let idx = 0; + // matrix + data[idx++] = 2; data[idx++] = 0; data[idx++] = 0; + data[idx++] = 2; data[idx++] = 5; data[idx++] = 10; + // isGridEnabled = true + data[idx++] = 1; + // grid data (24 values) + for (let i = 0; i < 24; i++) { + data[idx++] = i; + } + // command length + data[idx++] = 2; + // commands + data[idx++] = 9; data[idx++] = 12; + + const resultIndex = execute(data, 0); + + expect($context.setTransform).toHaveBeenCalledWith(2, 0, 0, 2, 5, 10); + expect($context.useGrid).toHaveBeenCalledTimes(1); + const gridArg = vi.mocked($context.useGrid).mock.calls[0][0]; + expect(gridArg).not.toBeNull(); + expect(gridArg!.length).toBe(28); + expect($context.clip).toHaveBeenCalledTimes(1); + expect(resultIndex).toBe(idx); + }); +}); diff --git a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.test.ts b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.test.ts new file mode 100644 index 00000000..d9e8e232 --- /dev/null +++ b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.test.ts @@ -0,0 +1,127 @@ +import { execute } from "./ShapeRenderUseCase"; +import { describe, expect, it, vi } from "vitest"; + +const mockNode = { "x": 0, "y": 0, "w": 100, "h": 100 }; + +vi.mock("@next2d/cache", () => ({ + "$cacheStore": { + "get": vi.fn(() => mockNode), + "set": vi.fn(), + "has": vi.fn(() => false) + } +})); + +vi.mock("../../RendererUtil", () => ({ + "$context": { + "reset": vi.fn(), + "setTransform": vi.fn(), + "useGrid": vi.fn(), + "createNode": vi.fn(() => mockNode), + "beginNodeRendering": vi.fn(), + "endNodeRendering": vi.fn(), + "drawFill": vi.fn(), + "drawPixels": vi.fn(), + "drawDisplayObject": vi.fn(), + "drawArraysInstanced": vi.fn(), + "bind": vi.fn(), + "applyFilter": vi.fn(), + "currentAttachmentObject": null, + "atlasAttachmentObject": null, + "globalAlpha": 1, + "imageSmoothingEnabled": true, + "globalCompositeOperation": "normal" + } +})); + +vi.mock("../service/ShapeCommandService", () => ({ + "execute": vi.fn() +})); + +vi.mock("../../DisplayObject/service/DisplayObjectGetBlendModeService", () => ({ + "execute": vi.fn(() => "normal") +})); + +describe("ShapeRenderUseCase.js test", () => { + + it("execute test case1 - render with cache hit", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.drawDisplayObject).mockClear(); + vi.mocked($context.setTransform).mockClear(); + + // Build render queue for cached shape + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 50, 60); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 100, 100); + // baseBounds xMin, yMin, xMax, yMax + data.push(0, 0, 100, 100); + // isGridEnabled, isDrawable, isBitmap + data.push(0, 1, 0); + // uniqueKey, cacheKey + data.push(1, 0); + // xScale, yScale + data.push(1, 1); + // filterKey + data.push(1); + // hasCache = 1 (cached) + data.push(1); + // blendMode + data.push(0); + // useFilter = 0 + data.push(0); + + const renderQueue = new Float32Array(data); + const resultIndex = execute(renderQueue, 0); + + expect($context.drawDisplayObject).toHaveBeenCalledTimes(1); + expect(resultIndex).toBe(data.length); + }); + + it("execute test case2 - render with filter", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.applyFilter).mockClear(); + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 50, 60); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 100, 100); + // baseBounds + data.push(0, 0, 100, 100); + // isGridEnabled, isDrawable, isBitmap + data.push(0, 1, 0); + // uniqueKey, cacheKey + data.push(2, 0); + // xScale, yScale + data.push(1, 1); + // filterKey + data.push(2); + // hasCache = 1 + data.push(1); + // blendMode + data.push(0); + // useFilter = 1 + data.push(1); + // updated + data.push(1); + // filterBounds (4) + data.push(-5, -5, 110, 110); + // filter params length + data.push(3); + // filter params + data.push(1, 2, 1); + + const renderQueue = new Float32Array(data); + const resultIndex = execute(renderQueue, 0); + + expect($context.applyFilter).toHaveBeenCalledTimes(1); + expect(resultIndex).toBe(data.length); + }); +}); diff --git a/packages/renderer/src/TextField/service/TextFiledGetAlignOffsetService.test.ts b/packages/renderer/src/TextField/service/TextFiledGetAlignOffsetService.test.ts new file mode 100644 index 00000000..44bdb89f --- /dev/null +++ b/packages/renderer/src/TextField/service/TextFiledGetAlignOffsetService.test.ts @@ -0,0 +1,205 @@ +import { execute } from "./TextFiledGetAlignOffsetService"; +import { describe, expect, it } from "vitest"; + +describe("TextFiledGetAlignOffsetService.js test", () => { + + it("execute test case1 - left align returns leftMargin", () => + { + const textData = { + "widthTable": [100] + } as any; + + const textObject = { + "line": 0, + "textFormat": { + "align": "left", + "leftMargin": 5, + "rightMargin": 0 + } + } as any; + + const textSetting = { + "wordWrap": true, + "rawWidth": 200, + "autoSize": "left" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(5); + }); + + it("execute test case2 - center align", () => + { + const textData = { + "widthTable": [100] + } as any; + + const textObject = { + "line": 0, + "textFormat": { + "align": "center", + "leftMargin": 0, + "rightMargin": 0 + } + } as any; + + const textSetting = { + "wordWrap": true, + "rawWidth": 200, + "autoSize": "none" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(Math.max(0, 200 / 2 - 0 - 0 - 100 / 2 - 2)); + }); + + it("execute test case3 - right align", () => + { + const textData = { + "widthTable": [100] + } as any; + + const textObject = { + "line": 0, + "textFormat": { + "align": "right", + "leftMargin": 0, + "rightMargin": 0 + } + } as any; + + const textSetting = { + "wordWrap": true, + "rawWidth": 200, + "autoSize": "none" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(Math.max(0, 200 - 0 - 100 - 0 - 4)); + }); + + it("execute test case4 - autoSize center", () => + { + const textData = { + "widthTable": [80] + } as any; + + const textObject = { + "line": 0, + "textFormat": { + "align": "left", + "leftMargin": 0, + "rightMargin": 0 + } + } as any; + + const textSetting = { + "wordWrap": true, + "rawWidth": 200, + "autoSize": "center" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(Math.max(0, 200 / 2 - 0 - 0 - 80 / 2 - 2)); + }); + + it("execute test case5 - autoSize right", () => + { + const textData = { + "widthTable": [80] + } as any; + + const textObject = { + "line": 0, + "textFormat": { + "align": "left", + "leftMargin": 0, + "rightMargin": 0 + } + } as any; + + const textSetting = { + "wordWrap": true, + "rawWidth": 200, + "autoSize": "right" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(Math.max(0, 200 - 0 - 80 - 0 - 4)); + }); + + it("execute test case6 - lineWidth exceeds rawWidth without wordWrap", () => + { + const textData = { + "widthTable": [300] + } as any; + + const textObject = { + "line": 0, + "textFormat": { + "align": "center", + "leftMargin": 10, + "rightMargin": 5 + } + } as any; + + const textSetting = { + "wordWrap": false, + "rawWidth": 200, + "autoSize": "none" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(10); + }); + + it("execute test case7 - missing line in widthTable defaults to 0", () => + { + const textData = { + "widthTable": [100] + } as any; + + const textObject = { + "line": 5, + "textFormat": { + "align": "left", + "leftMargin": 0, + "rightMargin": 0 + } + } as any; + + const textSetting = { + "wordWrap": true, + "rawWidth": 200, + "autoSize": "none" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(0); + }); + + it("execute test case8 - margins with center align", () => + { + const textData = { + "widthTable": [100] + } as any; + + const textObject = { + "line": 0, + "textFormat": { + "align": "center", + "leftMargin": 10, + "rightMargin": 10 + } + } as any; + + const textSetting = { + "wordWrap": true, + "rawWidth": 300, + "autoSize": "none" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(Math.max(0, 300 / 2 - 10 - 10 - 100 / 2 - 2)); + }); +}); diff --git a/packages/renderer/src/TextField/usecase/TextFieldDrawOffscreenCanvasUseCase.test.ts b/packages/renderer/src/TextField/usecase/TextFieldDrawOffscreenCanvasUseCase.test.ts new file mode 100644 index 00000000..7f6e7c04 --- /dev/null +++ b/packages/renderer/src/TextField/usecase/TextFieldDrawOffscreenCanvasUseCase.test.ts @@ -0,0 +1,199 @@ +import { execute } from "./TextFieldDrawOffscreenCanvasUseCase"; +import { describe, expect, it, vi, beforeAll } from "vitest"; + +vi.mock("../../RendererUtil", () => ({ + "$intToRGBA": vi.fn((color: number) => ({ + "R": (color >> 16) & 0xFF, + "G": (color >> 8) & 0xFF, + "B": color & 0xFF, + "A": 1 + })) +})); + +vi.mock("../service/TextFiledGetAlignOffsetService", () => ({ + "execute": vi.fn(() => 0) +})); + +vi.mock("../service/TextFieldGenerateFontStyleService", () => ({ + "execute": vi.fn(() => "12px Arial") +})); + +// OffscreenCanvas mock with full 2D context +beforeAll(() => { + const originalOffscreenCanvas = globalThis.OffscreenCanvas; + (globalThis as any).OffscreenCanvas = class MockOffscreenCanvas { + width: number; + height: number; + constructor(w: number, h: number) { + this.width = w; + this.height = h; + } + getContext() { + return { + "fillRect": vi.fn(), + "beginPath": vi.fn(), + "moveTo": vi.fn(), + "lineTo": vi.fn(), + "quadraticCurveTo": vi.fn(), + "closePath": vi.fn(), + "isPointInPath": vi.fn(() => false), + "fill": vi.fn(), + "stroke": vi.fn(), + "save": vi.fn(), + "restore": vi.fn(), + "clip": vi.fn(), + "setTransform": vi.fn(), + "fillText": vi.fn(), + "strokeText": vi.fn(), + "rect": vi.fn(), + "font": "", + "fillStyle": "", + "strokeStyle": "", + "lineWidth": 1 + }; + } + }; + + return () => { + (globalThis as any).OffscreenCanvas = originalOffscreenCanvas; + }; +}); + +describe("TextFieldDrawOffscreenCanvasUseCase.js test", () => { + + it("execute test case1 - returns canvas with null text_data", () => + { + const textSetting = { + "width": 200, + "height": 100, + "background": false, + "border": false, + "backgroundColor": 0xFFFFFF, + "borderColor": 0x000000, + "autoSize": "none", + "stopIndex": -1, + "scrollX": 0, + "scrollY": 0, + "textWidth": 200, + "textHeight": 100, + "rawWidth": 200, + "rawHeight": 100, + "focusIndex": -1, + "selectIndex": -1, + "focusVisible": false, + "thickness": 0, + "thicknessColor": 0, + "wordWrap": false, + "defaultColor": 0, + "defaultSize": 12 + } as any; + + const canvas = execute(null, textSetting, 1, 1); + expect(canvas).toBeInstanceOf(OffscreenCanvas); + expect(canvas.width).toBe(200); + expect(canvas.height).toBe(100); + }); + + it("execute test case2 - draws background and border", () => + { + const textSetting = { + "width": 300, + "height": 150, + "background": true, + "border": true, + "backgroundColor": 0xFF0000, + "borderColor": 0x00FF00, + "autoSize": "none", + "stopIndex": -1, + "scrollX": 0, + "scrollY": 0, + "textWidth": 300, + "textHeight": 150, + "rawWidth": 300, + "rawHeight": 150, + "focusIndex": -1, + "selectIndex": -1, + "focusVisible": false, + "thickness": 0, + "thicknessColor": 0, + "wordWrap": false, + "defaultColor": 0, + "defaultSize": 12 + } as any; + + const canvas = execute(null, textSetting, 1, 1); + expect(canvas).toBeInstanceOf(OffscreenCanvas); + }); + + it("execute test case3 - renders text objects", () => + { + const textData = { + "textTable": [ + { + "mode": "wrap", + "line": 0, + "w": 0, + "h": 14, + "y": 0, + "text": "", + "textFormat": { + "align": "left", + "leftMargin": 0, + "rightMargin": 0, + "color": 0x000000, + "underline": false + } + }, + { + "mode": "text", + "line": 0, + "w": 50, + "h": 14, + "y": 12, + "text": "Hello", + "textFormat": { + "align": "left", + "leftMargin": 0, + "rightMargin": 0, + "color": 0x000000, + "underline": false + } + } + ], + "lineTable": [], + "widthTable": [50], + "heightTable": [16], + "ascentTable": [12] + } as any; + + const textSetting = { + "width": 200, + "height": 100, + "background": false, + "border": false, + "backgroundColor": 0xFFFFFF, + "borderColor": 0x000000, + "autoSize": "none", + "stopIndex": -1, + "scrollX": 0, + "scrollY": 0, + "textWidth": 200, + "textHeight": 100, + "rawWidth": 200, + "rawHeight": 100, + "focusIndex": -1, + "selectIndex": -1, + "focusVisible": false, + "thickness": 0, + "thicknessColor": 0, + "wordWrap": false, + "defaultColor": 0, + "defaultSize": 12 + } as any; + + const canvas = execute(textData, textSetting, 1, 1); + expect(canvas).toBeInstanceOf(OffscreenCanvas); + expect(canvas.width).toBe(200); + expect(canvas.height).toBe(100); + }); +}); diff --git a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.test.ts b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.test.ts new file mode 100644 index 00000000..4ff21fb6 --- /dev/null +++ b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.test.ts @@ -0,0 +1,154 @@ +import { execute } from "./TextFieldRenderUseCase"; +import { describe, expect, it, vi } from "vitest"; + +const mockNode = { "x": 0, "y": 0, "w": 100, "h": 50 }; + +vi.mock("@next2d/cache", () => ({ + "$cacheStore": { + "get": vi.fn(() => mockNode), + "set": vi.fn(), + "has": vi.fn(() => false) + } +})); + +vi.mock("../../RendererUtil", () => ({ + "$context": { + "reset": vi.fn(), + "setTransform": vi.fn(), + "createNode": vi.fn(() => mockNode), + "beginNodeRendering": vi.fn(), + "endNodeRendering": vi.fn(), + "drawElement": vi.fn(), + "drawDisplayObject": vi.fn(), + "bind": vi.fn(), + "applyFilter": vi.fn(), + "currentAttachmentObject": null, + "atlasAttachmentObject": null, + "globalAlpha": 1, + "imageSmoothingEnabled": true, + "globalCompositeOperation": "normal" + } +})); + +vi.mock("./TextFieldDrawOffscreenCanvasUseCase", () => ({ + "execute": vi.fn(() => new OffscreenCanvas(100, 50)) +})); + +vi.mock("../../DisplayObject/service/DisplayObjectGetBlendModeService", () => ({ + "execute": vi.fn(() => "normal") +})); + +describe("TextFieldRenderUseCase.js test", () => { + + it("execute test case1 - render with cache hit", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.drawDisplayObject).mockClear(); + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 10, 20); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 100, 50); + // baseBounds (4) + data.push(0, 0, 100, 50); + // uniqueKey, cacheKey + data.push(1, 0); + // changed + data.push(0); + // xScale, yScale + data.push(1, 1); + // filterKey + data.push(1); + // hasCache = 1 + data.push(1); + // blendMode + data.push(0); + // useFilter = 0 + data.push(0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0); + + expect($context.drawDisplayObject).toHaveBeenCalledTimes(1); + expect(result).toBe(data.length); + }); + + it("execute test case2 - render with filter", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.applyFilter).mockClear(); + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 10, 20); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 100, 50); + // baseBounds (4) + data.push(0, 0, 100, 50); + // uniqueKey, cacheKey + data.push(5, 0); + // changed + data.push(1); + // xScale, yScale + data.push(1, 1); + // filterKey + data.push(5); + // hasCache = 1 + data.push(1); + // blendMode + data.push(0); + // useFilter = 1 + data.push(1); + // updated + data.push(1); + // filterBounds (4) + data.push(-5, -5, 110, 60); + // filter params length + data.push(3); + // filter params + data.push(1, 2, 1); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0); + + expect($context.applyFilter).toHaveBeenCalledTimes(1); + expect(result).toBe(data.length); + }); + + it("execute test case3 - cache miss returns early when no node", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + vi.mocked($cacheStore.get).mockReturnValueOnce(null as any); + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 10, 20); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 100, 50); + // baseBounds (4) + data.push(0, 0, 100, 50); + // uniqueKey, cacheKey + data.push(99, 0); + // changed + data.push(0); + // xScale, yScale + data.push(1, 1); + // filterKey + data.push(99); + // hasCache = 1 + data.push(1); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0); + + // Should return early at hasCache position + 1 + expect(result).toBe(data.length); + }); +}); diff --git a/packages/renderer/src/Video/usecase/VideoRenderUseCase.test.ts b/packages/renderer/src/Video/usecase/VideoRenderUseCase.test.ts new file mode 100644 index 00000000..c90b2ad8 --- /dev/null +++ b/packages/renderer/src/Video/usecase/VideoRenderUseCase.test.ts @@ -0,0 +1,187 @@ +import { execute } from "./VideoRenderUseCase"; +import { describe, expect, it, vi } from "vitest"; + +const mockNode = { "x": 0, "y": 0, "w": 320, "h": 240 }; + +vi.mock("@next2d/cache", () => ({ + "$cacheStore": { + "get": vi.fn(() => mockNode), + "set": vi.fn(), + "has": vi.fn(() => false) + } +})); + +vi.mock("../../RendererUtil", () => ({ + "$context": { + "reset": vi.fn(), + "setTransform": vi.fn(), + "createNode": vi.fn(() => mockNode), + "beginNodeRendering": vi.fn(), + "endNodeRendering": vi.fn(), + "drawElement": vi.fn(), + "drawDisplayObject": vi.fn(), + "bind": vi.fn(), + "applyFilter": vi.fn(), + "currentAttachmentObject": null, + "atlasAttachmentObject": null, + "globalAlpha": 1, + "imageSmoothingEnabled": true, + "globalCompositeOperation": "normal" + } +})); + +vi.mock("../../DisplayObject/service/DisplayObjectGetBlendModeService", () => ({ + "execute": vi.fn(() => "normal") +})); + +describe("VideoRenderUseCase.js test", () => { + + it("execute test case1 - render with cache hit", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.drawDisplayObject).mockClear(); + vi.mocked($context.setTransform).mockClear(); + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 50, 60); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 320, 240); + // baseBounds (4) + data.push(0, 0, 320, 240); + // uniqueKey + data.push(1); + // changed + data.push(0); + // filterKey + data.push(1); + // hasCache = 1 + data.push(1); + // blendMode + data.push(0); + // useFilter = 0 + data.push(0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + expect($context.drawDisplayObject).toHaveBeenCalledTimes(1); + expect($context.setTransform).toHaveBeenCalledWith(1, 0, 0, 1, 50, 60); + expect(result).toBe(data.length); + }); + + it("execute test case2 - render with filter", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.applyFilter).mockClear(); + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 50, 60); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 320, 240); + // baseBounds (4) + data.push(0, 0, 320, 240); + // uniqueKey + data.push(3); + // changed + data.push(1); + // filterKey + data.push(3); + // hasCache = 1 + data.push(1); + // blendMode + data.push(0); + // useFilter = 1 + data.push(1); + // updated + data.push(1); + // filterBounds (4) + data.push(-5, -5, 330, 250); + // filter params length + data.push(3); + // filter params + data.push(1, 2, 1); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + expect($context.applyFilter).toHaveBeenCalledTimes(1); + expect(result).toBe(data.length); + }); + + it("execute test case3 - no cache, with image bitmap", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.drawElement).mockClear(); + vi.mocked($context.beginNodeRendering).mockClear(); + vi.mocked($context.endNodeRendering).mockClear(); + + const mockBitmap = { "width": 320, "height": 240 } as unknown as ImageBitmap; + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 50, 60); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 320, 240); + // baseBounds (4) + data.push(0, 0, 320, 240); + // uniqueKey + data.push(10); + // changed + data.push(1); + // filterKey + data.push(10); + // hasCache = 0 + data.push(0); + // hasNode = 0 + data.push(0); + // blendMode + data.push(0); + // useFilter = 0 + data.push(0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, [mockBitmap]); + + expect($context.beginNodeRendering).toHaveBeenCalledTimes(1); + expect($context.drawElement).toHaveBeenCalledWith(mockNode, mockBitmap, true); + expect($context.endNodeRendering).toHaveBeenCalledTimes(1); + expect(result).toBe(data.length); + }); + + it("execute test case4 - cache miss returns early when no node found", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + vi.mocked($cacheStore.get).mockReturnValueOnce(null as any); + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 0, 0); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 320, 240); + // baseBounds (4) + data.push(0, 0, 320, 240); + // uniqueKey + data.push(99); + // changed + data.push(0); + // filterKey + data.push(99); + // hasCache = 1 + data.push(1); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + expect(result).toBe(data.length); + }); +}); diff --git a/packages/webgl/src/Context/usecase/ContextContainerBeginLayerUseCase.test.ts b/packages/webgl/src/Context/usecase/ContextContainerBeginLayerUseCase.test.ts new file mode 100644 index 00000000..92c2d069 --- /dev/null +++ b/packages/webgl/src/Context/usecase/ContextContainerBeginLayerUseCase.test.ts @@ -0,0 +1,75 @@ +import { execute, $containerLayerStack } from "./ContextContainerBeginLayerUseCase"; +import { describe, expect, it, vi, beforeEach } from "vitest"; + +vi.mock("../../WebGLUtil", () => ({ + "$context": { + "drawArraysInstanced": vi.fn(), + "$mainAttachmentObject": { "width": 800, "height": 600, "label": "main" }, + "bind": vi.fn() + } +})); + +vi.mock("../../FrameBufferManager/usecase/FrameBufferManagerGetAttachmentObjectUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600, "label": "layer" })) +})); + +describe("ContextContainerBeginLayerUseCase.js test", () => { + + beforeEach(() => + { + $containerLayerStack.length = 0; + }); + + it("execute test case1 - pushes current main to stack", async () => + { + const { $context } = await import("../../WebGLUtil"); + const originalMain = $context.$mainAttachmentObject; + + execute(800, 600); + + expect($containerLayerStack.length).toBe(1); + expect($containerLayerStack[0]).toBe(originalMain); + }); + + it("execute test case2 - flushes instanced draw before switching", async () => + { + const { $context } = await import("../../WebGLUtil"); + vi.mocked($context.drawArraysInstanced).mockClear(); + + execute(800, 600); + + expect($context.drawArraysInstanced).toHaveBeenCalledTimes(1); + }); + + it("execute test case3 - sets new layer attachment as main", async () => + { + const { $context } = await import("../../WebGLUtil"); + + execute(400, 300); + + expect($context.$mainAttachmentObject).toEqual({ "width": 800, "height": 600, "label": "layer" }); + }); + + it("execute test case4 - binds layer attachment", async () => + { + const { $context } = await import("../../WebGLUtil"); + vi.mocked($context.bind).mockClear(); + + execute(800, 600); + + expect($context.bind).toHaveBeenCalledTimes(1); + }); + + it("execute test case5 - multiple layers stack correctly", async () => + { + const { $context } = await import("../../WebGLUtil"); + + $context.$mainAttachmentObject = { "label": "main1" } as any; + execute(800, 600); + + $context.$mainAttachmentObject = { "label": "main2" } as any; + execute(400, 300); + + expect($containerLayerStack.length).toBe(2); + }); +}); diff --git a/packages/webgl/src/Context/usecase/ContextContainerDrawCachedFilterUseCase.test.ts b/packages/webgl/src/Context/usecase/ContextContainerDrawCachedFilterUseCase.test.ts new file mode 100644 index 00000000..a709cfd7 --- /dev/null +++ b/packages/webgl/src/Context/usecase/ContextContainerDrawCachedFilterUseCase.test.ts @@ -0,0 +1,93 @@ +import { execute } from "./ContextContainerDrawCachedFilterUseCase"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@next2d/cache", () => ({ + "$cacheStore": { + "get": vi.fn() + } +})); + +vi.mock("../../WebGLUtil", () => ({ + "$context": { + "drawArraysInstanced": vi.fn(), + "reset": vi.fn(), + "globalCompositeOperation": "normal" + }, + "$devicePixelRatio": 1 +})); + +vi.mock("../../Blend/usecase/BlendDrawFilterToMainUseCase", () => ({ + "execute": vi.fn() +})); + +describe("ContextContainerDrawCachedFilterUseCase.js test", () => { + + it("execute test case1 - returns early if cached key does not match", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase"); + + vi.mocked($cacheStore.get).mockImplementation((key: string, prop: string) => { + if (prop === "fKey") { return "old_key"; } + return null; + }); + vi.mocked(blendMod.execute).mockClear(); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + const filterBounds = new Float32Array([-5, -5, 110, 110]); + + execute("normal", matrix, colorTransform, filterBounds, "1", "new_key"); + + expect(blendMod.execute).not.toHaveBeenCalled(); + }); + + it("execute test case2 - returns early if no texture object", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase"); + + vi.mocked($cacheStore.get).mockImplementation((_key: string, prop: string) => { + if (prop === "fKey") { return "match_key"; } + if (prop === "fTexture") { return null; } + return null; + }); + vi.mocked(blendMod.execute).mockClear(); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + const filterBounds = new Float32Array([-5, -5, 110, 110]); + + execute("normal", matrix, colorTransform, filterBounds, "1", "match_key"); + + expect(blendMod.execute).not.toHaveBeenCalled(); + }); + + it("execute test case3 - draws cached filter when key matches", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + const { $context } = await import("../../WebGLUtil"); + const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase"); + + const mockTexture = { "width": 100, "height": 100 }; + vi.mocked($cacheStore.get).mockImplementation((_key: string, prop: string) => { + if (prop === "fKey") { return "valid_key"; } + if (prop === "fTexture") { return mockTexture; } + return null; + }); + vi.mocked($context.drawArraysInstanced).mockClear(); + vi.mocked($context.reset).mockClear(); + vi.mocked(blendMod.execute).mockClear(); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + const filterBounds = new Float32Array([-5, -5, 110, 110]); + + execute("add", matrix, colorTransform, filterBounds, "1", "valid_key"); + + expect($context.drawArraysInstanced).toHaveBeenCalledTimes(1); + expect($context.reset).toHaveBeenCalledTimes(1); + expect($context.globalCompositeOperation).toBe("add"); + expect(blendMod.execute).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/webgl/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts b/packages/webgl/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts new file mode 100644 index 00000000..5164173b --- /dev/null +++ b/packages/webgl/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts @@ -0,0 +1,122 @@ +import { execute } from "./ContextContainerEndLayerUseCase"; +import { describe, expect, it, vi, beforeEach } from "vitest"; + +vi.mock("@next2d/cache", () => ({ + "$cacheStore": { + "set": vi.fn(), + "get": vi.fn() + } +})); + +vi.mock("../../WebGLUtil", () => ({ + "$context": { + "drawArraysInstanced": vi.fn(), + "$mainAttachmentObject": { "width": 800, "height": 600, "label": "layer" }, + "bind": vi.fn(), + "reset": vi.fn(), + "globalCompositeOperation": "normal" + }, + "$devicePixelRatio": 1 +})); + +vi.mock("./ContextContainerBeginLayerUseCase", () => ({ + "$containerLayerStack": [] as any[] +})); + +vi.mock("../../FrameBufferManager/usecase/FrameBufferManagerGetTextureFromBoundsUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600, "textureId": 1 })) +})); + +vi.mock("../../FrameBufferManager/usecase/FrameBufferManagerReleaseAttachmentObjectUseCase", () => ({ + "execute": vi.fn() +})); + +vi.mock("../../Blend/usecase/BlendDrawFilterToMainUseCase", () => ({ + "execute": vi.fn() +})); + +vi.mock("../../TextureManager/usecase/TextureManagerReleaseTextureObjectUseCase", () => ({ + "execute": vi.fn() +})); + +vi.mock("../../Filter/BlurFilter/usecase/FilterApplyBlurFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600, "textureId": 2 })) +})); +vi.mock("../../Filter/BevelFilter/usecase/FilterApplyBevelFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter/ColorMatrixFilter/usecase/FilterApplyColorMatrixFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter/ConvolutionFilter/usecase/FilterApplyConvolutionFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter/DisplacementMapFilter/usecase/FilterApplyDisplacementMapFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter/DropShadowFilter/usecase/FilterApplyDropShadowFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter/GlowFilter/usecase/FilterApplyGlowFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter/GradientBevelFilter/usecase/FilterApplyGradientBevelFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter/GradientGlowFilter/usecase/FilterApplyGradientGlowFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter", () => ({ + "$offset": { "x": 0, "y": 0 } +})); + +describe("ContextContainerEndLayerUseCase.js test", () => { + + beforeEach(async () => + { + vi.clearAllMocks(); + const { $containerLayerStack } = await import("./ContextContainerBeginLayerUseCase"); + $containerLayerStack.length = 0; + $containerLayerStack.push({ "width": 800, "height": 600, "label": "main" } as any); + + const { $context } = await import("../../WebGLUtil"); + $context.$mainAttachmentObject = { "width": 800, "height": 600, "label": "layer" } as any; + }); + + it("execute test case1 - blend only (no filter)", async () => + { + const { $context } = await import("../../WebGLUtil"); + const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase"); + const releaseMod = await import("../../TextureManager/usecase/TextureManagerReleaseTextureObjectUseCase"); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + execute("normal", matrix, colorTransform, false, null, null, "", ""); + + expect($context.drawArraysInstanced).toHaveBeenCalledTimes(1); + expect($context.reset).toHaveBeenCalledTimes(1); + expect(blendMod.execute).toHaveBeenCalledTimes(1); + expect(releaseMod.execute).toHaveBeenCalledTimes(1); + expect($context.bind).toHaveBeenCalled(); + }); + + it("execute test case2 - with filter (blur)", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase"); + const blurMod = await import("../../Filter/BlurFilter/usecase/FilterApplyBlurFilterUseCase"); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + const filterBounds = new Float32Array([-5, -5, 110, 110]); + // BlurFilter: type=1, blurX=4, blurY=4, quality=1 + const filterParams = new Float32Array([1, 4, 4, 1]); + + execute("normal", matrix, colorTransform, true, filterBounds, filterParams, "uk1", "fk1"); + + expect(blurMod.execute).toHaveBeenCalledTimes(1); + expect($cacheStore.set).toHaveBeenCalledWith("uk1", "fKey", "fk1"); + expect(blendMod.execute).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.test.ts b/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.test.ts new file mode 100644 index 00000000..17443da7 --- /dev/null +++ b/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.test.ts @@ -0,0 +1,74 @@ +import { execute } from "./BufferManagerUpdateIndirectBufferService"; +import { describe, expect, it, vi } from "vitest"; + +describe("BufferManagerUpdateIndirectBufferService.js test", () => { + + it("execute test case1", () => + { + let writtenData: Uint32Array | null = null; + let writtenOffset: number = -1; + + const mockBuffer = {} as unknown as GPUBuffer; + const mockDevice = { + "queue": { + "writeBuffer": vi.fn((buffer: GPUBuffer, offset: number, data: Uint32Array) => { + expect(buffer).toBe(mockBuffer); + writtenOffset = offset; + writtenData = new Uint32Array(data); + }) + } + } as unknown as GPUDevice; + + execute(mockDevice, mockBuffer, 12, 3, 0, 0); + + expect(mockDevice.queue.writeBuffer).toHaveBeenCalledTimes(1); + expect(writtenOffset).toBe(0); + expect(writtenData).not.toBeNull(); + expect(writtenData![0]).toBe(12); + expect(writtenData![1]).toBe(3); + expect(writtenData![2]).toBe(0); + expect(writtenData![3]).toBe(0); + }); + + it("execute test case2 - custom first_vertex and first_instance", () => + { + let writtenData: Uint32Array | null = null; + + const mockBuffer = {} as unknown as GPUBuffer; + const mockDevice = { + "queue": { + "writeBuffer": vi.fn((_buffer: GPUBuffer, _offset: number, data: Uint32Array) => { + writtenData = new Uint32Array(data); + }) + } + } as unknown as GPUDevice; + + execute(mockDevice, mockBuffer, 6, 1, 5, 10); + + expect(writtenData![0]).toBe(6); + expect(writtenData![1]).toBe(1); + expect(writtenData![2]).toBe(5); + expect(writtenData![3]).toBe(10); + }); + + it("execute test case3 - default parameters", () => + { + let writtenData: Uint32Array | null = null; + + const mockBuffer = {} as unknown as GPUBuffer; + const mockDevice = { + "queue": { + "writeBuffer": vi.fn((_buffer: GPUBuffer, _offset: number, data: Uint32Array) => { + writtenData = new Uint32Array(data); + }) + } + } as unknown as GPUDevice; + + execute(mockDevice, mockBuffer, 100, 50); + + expect(writtenData![0]).toBe(100); + expect(writtenData![1]).toBe(50); + expect(writtenData![2]).toBe(0); + expect(writtenData![3]).toBe(0); + }); +}); diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.test.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.test.ts new file mode 100644 index 00000000..4e572c54 --- /dev/null +++ b/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.test.ts @@ -0,0 +1,63 @@ +import { execute } from "./BufferManagerCleanupStorageBuffersUseCase"; +import { describe, expect, it, vi } from "vitest"; + +describe("BufferManagerCleanupStorageBuffersUseCase.js test", () => { + + it("execute test case1 - should remove old unused buffers", () => + { + const destroyFn = vi.fn(); + const pool = [ + { "buffer": { "destroy": destroyFn }, "inUse": false, "lastUsedFrame": 0, "size": 256 }, + { "buffer": { "destroy": vi.fn() }, "inUse": true, "lastUsedFrame": 0, "size": 256 }, + { "buffer": { "destroy": vi.fn() }, "inUse": false, "lastUsedFrame": 50, "size": 256 } + ] as any; + + execute(pool, 100, 60); + + expect(pool.length).toBe(2); + expect(destroyFn).toHaveBeenCalledTimes(1); + }); + + it("execute test case2 - should not remove in-use buffers", () => + { + const pool = [ + { "buffer": { "destroy": vi.fn() }, "inUse": true, "lastUsedFrame": 0, "size": 256 }, + { "buffer": { "destroy": vi.fn() }, "inUse": true, "lastUsedFrame": 10, "size": 256 } + ] as any; + + execute(pool, 100, 60); + + expect(pool.length).toBe(2); + }); + + it("execute test case3 - should not remove recently used buffers", () => + { + const pool = [ + { "buffer": { "destroy": vi.fn() }, "inUse": false, "lastUsedFrame": 95, "size": 256 } + ] as any; + + execute(pool, 100, 60); + + expect(pool.length).toBe(1); + }); + + it("execute test case4 - empty pool", () => + { + const pool: any[] = []; + execute(pool, 100, 60); + expect(pool.length).toBe(0); + }); + + it("execute test case5 - default max_age", () => + { + const destroyFn = vi.fn(); + const pool = [ + { "buffer": { "destroy": destroyFn }, "inUse": false, "lastUsedFrame": 0, "size": 256 } + ] as any; + + execute(pool, 61); + + expect(pool.length).toBe(0); + expect(destroyFn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.test.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.test.ts new file mode 100644 index 00000000..f483a1dc --- /dev/null +++ b/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.test.ts @@ -0,0 +1,55 @@ +import { execute } from "./BufferManagerReleaseStorageBufferUseCase"; +import { describe, expect, it } from "vitest"; + +describe("BufferManagerReleaseStorageBufferUseCase.js test", () => { + + it("execute test case1 - should mark matching buffer as not in use", () => + { + const targetBuffer = { "label": "target" } as unknown as GPUBuffer; + const pool = [ + { "buffer": { "label": "other" } as unknown as GPUBuffer, "inUse": true, "lastUsedFrame": 0, "size": 256 }, + { "buffer": targetBuffer, "inUse": true, "lastUsedFrame": 0, "size": 256 } + ] as any; + + execute(pool, targetBuffer); + + expect(pool[0].inUse).toBe(true); + expect(pool[1].inUse).toBe(false); + }); + + it("execute test case2 - should do nothing if buffer not found", () => + { + const targetBuffer = { "label": "target" } as unknown as GPUBuffer; + const pool = [ + { "buffer": { "label": "other" } as unknown as GPUBuffer, "inUse": true, "lastUsedFrame": 0, "size": 256 } + ] as any; + + execute(pool, targetBuffer); + + expect(pool[0].inUse).toBe(true); + }); + + it("execute test case3 - empty pool", () => + { + const targetBuffer = {} as unknown as GPUBuffer; + const pool: any[] = []; + + execute(pool, targetBuffer); + + expect(pool.length).toBe(0); + }); + + it("execute test case4 - should release first matching buffer only", () => + { + const targetBuffer = {} as unknown as GPUBuffer; + const pool = [ + { "buffer": targetBuffer, "inUse": true, "lastUsedFrame": 0, "size": 256 }, + { "buffer": targetBuffer, "inUse": true, "lastUsedFrame": 0, "size": 512 } + ] as any; + + execute(pool, targetBuffer); + + expect(pool[0].inUse).toBe(false); + expect(pool[1].inUse).toBe(true); + }); +}); diff --git a/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.test.ts b/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.test.ts new file mode 100644 index 00000000..8be54d7c --- /dev/null +++ b/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.test.ts @@ -0,0 +1,103 @@ +import { execute } from "./ContextFillWithStencilMainService"; +import { describe, expect, it, vi } from "vitest"; + +describe("ContextFillWithStencilMainService.js test", () => { + + const createMockRenderPassEncoder = () => ({ + "setPipeline": vi.fn(), + "setVertexBuffer": vi.fn(), + "setStencilReference": vi.fn(), + "setBindGroup": vi.fn(), + "draw": vi.fn() + }) as unknown as GPURenderPassEncoder; + + const createMockPipelineManager = (hasWrite = true, hasFill = true) => ({ + "getPipeline": vi.fn((name: string) => { + if (name === "stencil_write_main" && hasWrite) { + return { "label": "stencil_write_main" }; + } + if (name === "stencil_fill_main" && hasFill) { + return { "label": "stencil_fill_main" }; + } + return null; + }) + }) as any; + + it("execute test case1 - stencil write pass", () => + { + const encoder = createMockRenderPassEncoder(); + const pipelineManager = createMockPipelineManager(); + const vertexBuffer = {} as unknown as GPUBuffer; + const bindGroup = {} as unknown as GPUBindGroup; + + execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0); + + expect(pipelineManager.getPipeline).toHaveBeenCalledWith("stencil_write_main"); + expect(encoder.setStencilReference).toHaveBeenCalledWith(0); + expect(encoder.setVertexBuffer).toHaveBeenCalledWith(0, vertexBuffer); + }); + + it("execute test case2 - stencil fill pass", () => + { + const encoder = createMockRenderPassEncoder(); + const pipelineManager = createMockPipelineManager(); + const vertexBuffer = {} as unknown as GPUBuffer; + const bindGroup = {} as unknown as GPUBindGroup; + + execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0); + + expect(pipelineManager.getPipeline).toHaveBeenCalledWith("stencil_fill_main"); + }); + + it("execute test case3 - both passes execute in order", () => + { + const encoder = createMockRenderPassEncoder(); + const pipelineManager = createMockPipelineManager(); + const vertexBuffer = {} as unknown as GPUBuffer; + const bindGroup = {} as unknown as GPUBindGroup; + + execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0); + + expect(pipelineManager.getPipeline).toHaveBeenCalledTimes(2); + expect(encoder.setPipeline).toHaveBeenCalledTimes(2); + expect(encoder.draw).toHaveBeenCalledTimes(2); + expect(encoder.draw).toHaveBeenCalledWith(12, 1, 0, 0); + }); + + it("execute test case4 - bind group with dynamic offset", () => + { + const encoder = createMockRenderPassEncoder(); + const pipelineManager = createMockPipelineManager(); + const vertexBuffer = {} as unknown as GPUBuffer; + const bindGroup = {} as unknown as GPUBindGroup; + + execute(encoder, pipelineManager, vertexBuffer, 6, bindGroup, 256); + + expect(encoder.setBindGroup).toHaveBeenCalledWith(0, bindGroup, [256]); + }); + + it("execute test case5 - no write pipeline available", () => + { + const encoder = createMockRenderPassEncoder(); + const pipelineManager = createMockPipelineManager(false, true); + const vertexBuffer = {} as unknown as GPUBuffer; + const bindGroup = {} as unknown as GPUBindGroup; + + execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0); + + // write pass skipped, but fill pass should still execute + expect(encoder.draw).toHaveBeenCalledTimes(1); + }); + + it("execute test case6 - no pipelines available", () => + { + const encoder = createMockRenderPassEncoder(); + const pipelineManager = createMockPipelineManager(false, false); + const vertexBuffer = {} as unknown as GPUBuffer; + const bindGroup = {} as unknown as GPUBindGroup; + + execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0); + + expect(encoder.draw).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts new file mode 100644 index 00000000..1c558ee8 --- /dev/null +++ b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts @@ -0,0 +1,231 @@ +import { execute } from "./ContextContainerEndLayerUseCase"; +import { describe, expect, it, vi, beforeEach } from "vitest"; + +const mockAttachment = { + "width": 800, + "height": 600, + "texture": { "view": {} }, + "msaa": false, + "msaaTexture": null +}; + +const mockMainAttachment = { + "width": 800, + "height": 600, + "texture": { "view": {} }, + "msaa": false, + "msaaTexture": null +}; + +const mockPassEncoder = { + "setPipeline": vi.fn(), + "setBindGroup": vi.fn(), + "setViewport": vi.fn(), + "setScissorRect": vi.fn(), + "draw": vi.fn(), + "end": vi.fn() +}; + +vi.mock("@next2d/cache", () => ({ + "$cacheStore": { + "set": vi.fn(), + "get": vi.fn() + } +})); + +vi.mock("../../Filter/FilterOffset", () => ({ + "$offset": { "x": 0, "y": 0 } +})); + +vi.mock("../../WebGPUUtil", () => ({ + "WebGPUUtil": { + "getDevicePixelRatio": vi.fn(() => 1) + } +})); + +vi.mock("../../Filter/BlurFilter/FilterApplyBlurFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/GlowFilter/FilterApplyGlowFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/BevelFilter/FilterApplyBevelFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Blend/usecase/BlendApplyComplexBlendUseCase", () => ({ + "execute": vi.fn(() => mockAttachment) +})); + +describe("ContextContainerEndLayerUseCase.js test", () => { + + const createMockConfig = () => ({ + "device": { + "createBindGroup": vi.fn(() => ({})) + }, + "commandEncoder": { + "beginRenderPass": vi.fn(() => mockPassEncoder) + }, + "bufferManager": { + "acquireAndWriteUniformBuffer": vi.fn(() => ({})) + }, + "frameBufferManager": { + "createTemporaryAttachment": vi.fn(() => ({ + ...mockAttachment, + "texture": { "view": {} } + })), + "releaseTemporaryAttachment": vi.fn(), + "createRenderPassDescriptor": vi.fn(() => ({})) + }, + "pipelineManager": { + "getPipeline": vi.fn(() => ({})), + "getBindGroupLayout": vi.fn(() => ({})) + }, + "textureManager": { + "createSampler": vi.fn(() => ({})) + }, + "frameTextures": [] + }) as any; + + const createMockBufferManager = () => ({ + "acquireAndWriteUniformBuffer": vi.fn(() => ({})) + }) as any; + + beforeEach(() => + { + vi.clearAllMocks(); + mockPassEncoder.setPipeline.mockClear(); + mockPassEncoder.setBindGroup.mockClear(); + mockPassEncoder.draw.mockClear(); + mockPassEncoder.end.mockClear(); + }); + + it("execute test case1 - blend only (no filter)", () => + { + const config = createMockConfig(); + const bufferManager = createMockBufferManager(); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + execute( + mockAttachment as any, + mockMainAttachment as any, + "temp", + "normal", + matrix, + colorTransform, + false, null, null, + "", "", + 800, 600, + config, + bufferManager + ); + + // Should copy region and draw to main + expect(config.frameBufferManager.createTemporaryAttachment).toHaveBeenCalled(); + expect(config.frameBufferManager.releaseTemporaryAttachment).toHaveBeenCalled(); + }); + + it("execute test case2 - with filter (blur)", () => + { + const config = createMockConfig(); + const bufferManager = createMockBufferManager(); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + const filterBounds = new Float32Array([-5, -5, 110, 110]); + // BlurFilter: type=1, blurX=4, blurY=4, quality=1 + const filterParams = new Float32Array([1, 4, 4, 1]); + + execute( + mockAttachment as any, + mockMainAttachment as any, + "temp", + "normal", + matrix, + colorTransform, + true, filterBounds, filterParams, + "uk1", "fk1", + 800, 600, + config, + bufferManager + ); + + expect(config.frameBufferManager.createTemporaryAttachment).toHaveBeenCalled(); + }); + + it("execute test case3 - non-identity color transform in blend-only mode", () => + { + const config = createMockConfig(); + const bufferManager = createMockBufferManager(); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + // Non-identity color transform + const colorTransform = new Float32Array([0.5, 0.5, 0.5, 1, 10, 10, 10, 0]); + + execute( + mockAttachment as any, + mockMainAttachment as any, + "temp", + "normal", + matrix, + colorTransform, + false, null, null, + "", "", + 800, 600, + config, + bufferManager + ); + + // Should apply color transform (extra createTemporaryAttachment call) + expect(config.frameBufferManager.createTemporaryAttachment).toHaveBeenCalled(); + expect(config.frameBufferManager.releaseTemporaryAttachment).toHaveBeenCalled(); + }); + + it("execute test case4 - caches filter result with uniqueKey", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + const config = createMockConfig(); + const bufferManager = createMockBufferManager(); + + const matrix = new Float32Array([1, 0, 0, 1, 0, 0]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + const filterBounds = new Float32Array([0, 0, 100, 100]); + const filterParams = new Float32Array([1, 2, 2, 1]); // BlurFilter + + execute( + mockAttachment as any, + mockMainAttachment as any, + "temp", + "normal", + matrix, + colorTransform, + true, filterBounds, filterParams, + "cacheKey", "filterKey", + 800, 600, + config, + bufferManager + ); + + expect($cacheStore.set).toHaveBeenCalledWith("cacheKey", "fKey", "filterKey"); + expect($cacheStore.set).toHaveBeenCalledWith("cacheKey", "fTexture", expect.anything()); + }); +}); From 2b1cfa43cac9a1846610de242ce9a61888044c08 Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 26 Mar 2026 17:30:52 +0900 Subject: [PATCH 06/26] #265 update packages --- package-lock.json | 1078 +++++++++++++++++++++++++-------------------- package.json | 19 +- 2 files changed, 599 insertions(+), 498 deletions(-) diff --git a/package-lock.json b/package-lock.json index 374a66d2..18fc476e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,7 @@ "packages/*" ], "dependencies": { - "fflate": "^0.8.2", - "htmlparser2": "^10.1.0" + "htmlparser2": "^12.0.0" }, "devDependencies": { "@eslint/eslintrc": "^3.3.5", @@ -24,19 +23,19 @@ "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", "@types/node": "^25.5.0", - "@typescript-eslint/eslint-plugin": "^8.57.0", - "@typescript-eslint/parser": "^8.57.0", - "@vitest/web-worker": "^4.1.0", + "@typescript-eslint/eslint-plugin": "^8.57.2", + "@typescript-eslint/parser": "^8.57.2", + "@vitest/web-worker": "^4.1.1", "@webgpu/types": "^0.1.69", - "eslint": "^10.0.3", + "eslint": "^10.1.0", "eslint-plugin-unused-imports": "^4.4.1", "globals": "^17.4.0", - "jsdom": "^28.1.0", - "rollup": "^4.59.0", + "jsdom": "^29.0.1", + "rollup": "^4.60.0", "tslib": "^2.8.1", "typescript": "^5.9.3", - "vite": "^8.0.0", - "vitest": "^4.1.0", + "vite": "^8.0.3", + "vitest": "^4.1.1", "vitest-webgl-canvas-mock": "^1.1.0", "xml2js": "^0.6.2" }, @@ -62,13 +61,6 @@ "@next2d/webgpu": "file:packages/webgpu" } }, - "node_modules/@acemir/cssom": { - "version": "0.9.31", - "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", - "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", - "dev": true, - "license": "MIT" - }, "node_modules/@asamuzakjp/css-color": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", @@ -87,17 +79,20 @@ } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", - "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz", + "integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==", "dev": true, "license": "MIT", "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", + "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.6" + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/nwsapi": { @@ -216,9 +211,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.0.tgz", - "integrity": "sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", "dev": true, "funding": [ { @@ -230,7 +225,15 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0" + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } }, "node_modules/@csstools/css-tokenizer": { "version": "4.0.0", @@ -253,9 +256,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", - "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", "dev": true, "license": "MIT", "optional": true, @@ -265,9 +268,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", - "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", "dev": true, "license": "MIT", "optional": true, @@ -341,9 +344,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -674,20 +677,10 @@ "resolved": "packages/webgpu", "link": true }, - "node_modules/@oxc-project/runtime": { - "version": "0.115.0", - "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", - "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@oxc-project/types": { - "version": "0.115.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", - "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", "dev": true, "license": "MIT", "funding": { @@ -711,9 +704,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", "cpu": [ "arm64" ], @@ -728,9 +721,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", "cpu": [ "arm64" ], @@ -745,9 +738,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", - "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", "cpu": [ "x64" ], @@ -762,9 +755,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", - "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", "cpu": [ "x64" ], @@ -779,9 +772,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", - "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", "cpu": [ "arm" ], @@ -796,13 +789,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -813,13 +809,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", - "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -830,13 +829,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -847,13 +849,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -864,13 +869,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -881,13 +889,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", - "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -898,9 +909,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", "cpu": [ "arm64" ], @@ -915,9 +926,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", - "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", "cpu": [ "wasm32" ], @@ -932,9 +943,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", - "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", "cpu": [ "arm64" ], @@ -949,9 +960,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", - "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", "cpu": [ "x64" ], @@ -966,9 +977,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", - "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", "dev": true, "license": "MIT" }, @@ -1098,9 +1109,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", "cpu": [ "arm" ], @@ -1112,9 +1123,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", "cpu": [ "arm64" ], @@ -1126,9 +1137,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", "cpu": [ "arm64" ], @@ -1140,9 +1151,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", "cpu": [ "x64" ], @@ -1154,9 +1165,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", "cpu": [ "arm64" ], @@ -1168,9 +1179,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", "cpu": [ "x64" ], @@ -1182,13 +1193,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1196,13 +1210,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1210,13 +1227,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1224,13 +1244,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1238,13 +1261,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1252,13 +1278,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1266,13 +1295,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1280,13 +1312,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1294,13 +1329,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1308,13 +1346,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1322,13 +1363,16 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1336,13 +1380,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1350,13 +1397,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1364,9 +1414,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", "cpu": [ "x64" ], @@ -1378,9 +1428,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", "cpu": [ "arm64" ], @@ -1392,9 +1442,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", "cpu": [ "arm64" ], @@ -1406,9 +1456,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", "cpu": [ "ia32" ], @@ -1420,9 +1470,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", "cpu": [ "x64" ], @@ -1434,9 +1484,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", "cpu": [ "x64" ], @@ -1522,17 +1572,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", - "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/type-utils": "8.57.0", - "@typescript-eslint/utils": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -1545,7 +1595,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.0", + "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1561,16 +1611,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", - "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "engines": { @@ -1586,14 +1636,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", - "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.0", - "@typescript-eslint/types": "^8.57.0", + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "engines": { @@ -1608,14 +1658,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", - "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1626,9 +1676,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", - "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "license": "MIT", "engines": { @@ -1643,15 +1693,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", - "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -1668,9 +1718,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", - "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true, "license": "MIT", "engines": { @@ -1682,16 +1732,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", - "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.0", - "@typescript-eslint/tsconfig-utils": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1720,9 +1770,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1749,16 +1799,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", - "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1773,13 +1823,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", - "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1804,16 +1854,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", - "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.1.tgz", + "integrity": "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/spy": "4.1.1", + "@vitest/utils": "4.1.1", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" }, @@ -1822,13 +1872,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", - "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.1.tgz", + "integrity": "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.0", + "@vitest/spy": "4.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1837,7 +1887,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -1859,9 +1909,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", - "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.1.tgz", + "integrity": "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1872,13 +1922,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", - "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.1.tgz", + "integrity": "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.0", + "@vitest/utils": "4.1.1", "pathe": "^2.0.3" }, "funding": { @@ -1886,14 +1936,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", - "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.1.tgz", + "integrity": "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/pretty-format": "4.1.1", + "@vitest/utils": "4.1.1", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1902,9 +1952,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", - "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz", + "integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==", "dev": true, "license": "MIT", "funding": { @@ -1912,13 +1962,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", - "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz", + "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", + "@vitest/pretty-format": "4.1.1", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" }, @@ -1927,9 +1977,9 @@ } }, "node_modules/@vitest/web-worker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/web-worker/-/web-worker-4.1.0.tgz", - "integrity": "sha512-K0K2DXrgmvHdwAXHQx1j4qOqgid2Fhr2F2yiFVGbAf10vsoUQYQA1Twe/L8wRAv+DzAWe0OO4ikjz5ulMc1/CA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/web-worker/-/web-worker-4.1.1.tgz", + "integrity": "sha512-sjdtHgjfclId3/jHzBGQ8EmsiqfzzXrfIR0A17vGaqfngtdFum0TOWdZiKJVWuxBrJ+DBvjc3NwjEym9qf7S+g==", "dev": true, "license": "MIT", "dependencies": { @@ -1939,7 +1989,7 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.1.0" + "vitest": "4.1.1" } }, "node_modules/@webgpu/types": { @@ -1972,16 +2022,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -2141,22 +2181,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cssstyle": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", - "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^5.0.1", - "@csstools/css-syntax-patches-for-csstree": "^1.0.28", - "css-tree": "^3.1.0", - "lru-cache": "^11.2.6" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -2224,79 +2248,79 @@ } }, "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.0.0.tgz", + "integrity": "sha512-x+9D6nkC8tdXOQUS32egtZpZFLP90+HBZmWjuT920srbJvD/zPgFB9t4k3pEhlw5BQrXStQtRc1Y1zuriXk+Nw==", "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "entities": "^8.0.0" }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "type": "github", + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", + "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/fb55" } ], - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + } }, "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", + "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", "license": "BSD-2-Clause", "dependencies": { - "domelementtype": "^2.3.0" + "domelementtype": "^3.0.0" }, "engines": { - "node": ">= 4" + "node": ">=20.19.0" }, "funding": { + "type": "github", "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", + "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", "license": "BSD-2-Clause", "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" + "dom-serializer": "^3.0.0", + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0" + }, + "engines": { + "node": ">=20.19.0" }, "funding": { + "type": "github", "url": "https://github.com/fb55/domutils?sponsor=1" } }, "node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -2323,16 +2347,16 @@ } }, "node_modules/eslint": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", - "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.2", + "@eslint/config-helpers": "^0.5.3", "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", @@ -2345,7 +2369,7 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", - "espree": "^11.1.1", + "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2437,9 +2461,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2629,12 +2653,6 @@ } } }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT" - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2680,9 +2698,9 @@ } }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -2764,9 +2782,9 @@ } }, "node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-12.0.0.tgz", + "integrity": "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -2776,38 +2794,13 @@ ], "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "domutils": "^4.0.2", + "entities": "^8.0.0" }, "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" + "node": ">=20.19.0" } }, "node_modules/ignore": { @@ -2931,36 +2924,36 @@ } }, "node_modules/jsdom": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", - "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", + "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", "dev": true, "license": "MIT", "dependencies": { - "@acemir/cssom": "^0.9.31", - "@asamuzakjp/dom-selector": "^6.8.1", + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.3", "@bramus/specificity": "^2.4.2", - "@exodus/bytes": "^1.11.0", - "cssstyle": "^6.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", - "undici": "^7.21.0", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0", + "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" }, "peerDependencies": { "canvas": "^3.0.0" @@ -3159,6 +3152,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3180,6 +3176,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3201,6 +3200,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3222,6 +3224,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3294,9 +3299,9 @@ } }, "node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3518,9 +3523,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -3653,14 +3658,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", - "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.115.0", - "@rolldown/pluginutils": "1.0.0-rc.9" + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" @@ -3669,27 +3674,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.9", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", - "@rolldown/binding-darwin-x64": "1.0.0-rc.9", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" } }, "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3703,38 +3708,38 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" } }, "node_modules/sax": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", - "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3768,9 +3773,9 @@ } }, "node_modules/serialize-javascript": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", - "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -3896,9 +3901,9 @@ "license": "MIT" }, "node_modules/terser": { - "version": "5.46.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3922,9 +3927,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", "dev": true, "license": "MIT", "engines": { @@ -3959,22 +3964,22 @@ } }, "node_modules/tldts": { - "version": "7.0.25", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz", - "integrity": "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.25" + "tldts-core": "^7.0.27" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.25", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.25.tgz", - "integrity": "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", "dev": true, "license": "MIT" }, @@ -4005,9 +4010,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -4052,9 +4057,9 @@ } }, "node_modules/undici": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.0.tgz", - "integrity": "sha512-jxytwMHhsbdpBXxLAcuu0fzlQeXCNnWdDyRHpvWsUl8vd98UwYdl9YTyn8/HcpcJPC3pwUveefsa3zTxyD/ERg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.6.tgz", + "integrity": "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==", "dev": true, "license": "MIT", "engines": { @@ -4079,17 +4084,16 @@ } }, "node_modules/vite": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", - "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", - "picomatch": "^4.0.3", + "picomatch": "^4.0.4", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.9", + "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "bin": { @@ -4106,7 +4110,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.0.0-alpha.31", + "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -4173,19 +4177,19 @@ } }, "node_modules/vitest": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", - "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.1.tgz", + "integrity": "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.0", - "@vitest/mocker": "4.1.0", - "@vitest/pretty-format": "4.1.0", - "@vitest/runner": "4.1.0", - "@vitest/snapshot": "4.1.0", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/expect": "4.1.1", + "@vitest/mocker": "4.1.1", + "@vitest/pretty-format": "4.1.1", + "@vitest/runner": "4.1.1", + "@vitest/snapshot": "4.1.1", + "@vitest/spy": "4.1.1", + "@vitest/utils": "4.1.1", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -4197,7 +4201,7 @@ "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -4213,13 +4217,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.0", - "@vitest/browser-preview": "4.1.0", - "@vitest/browser-webdriverio": "4.1.0", - "@vitest/ui": "4.1.0", + "@vitest/browser-playwright": "4.1.1", + "@vitest/browser-preview": "4.1.1", + "@vitest/browser-webdriverio": "4.1.1", + "@vitest/ui": "4.1.1", "happy-dom": "*", "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -4510,6 +4514,104 @@ "@next2d/ui": "file:../ui" } }, + "packages/text/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "packages/text/node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "packages/text/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "packages/text/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "packages/text/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "packages/text/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "packages/text/node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, "packages/texture-packer": { "name": "@next2d/texture-packer", "version": "*", diff --git a/package.json b/package.json index f43e191f..1a7f36e2 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,7 @@ "url": "https://github.com/sponsors/Next2D" }, "dependencies": { - "fflate": "^0.8.2", - "htmlparser2": "^10.1.0" + "htmlparser2": "^12.0.0" }, "devDependencies": { "@eslint/eslintrc": "^3.3.5", @@ -60,19 +59,19 @@ "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", "@types/node": "^25.5.0", - "@typescript-eslint/eslint-plugin": "^8.57.0", - "@typescript-eslint/parser": "^8.57.0", - "@vitest/web-worker": "^4.1.0", + "@typescript-eslint/eslint-plugin": "^8.57.2", + "@typescript-eslint/parser": "^8.57.2", + "@vitest/web-worker": "^4.1.1", "@webgpu/types": "^0.1.69", - "eslint": "^10.0.3", + "eslint": "^10.1.0", "eslint-plugin-unused-imports": "^4.4.1", "globals": "^17.4.0", - "jsdom": "^28.1.0", - "rollup": "^4.59.0", + "jsdom": "^29.0.1", + "rollup": "^4.60.0", "tslib": "^2.8.1", "typescript": "^5.9.3", - "vite": "^8.0.0", - "vitest": "^4.1.0", + "vite": "^8.0.3", + "vitest": "^4.1.1", "vitest-webgl-canvas-mock": "^1.1.0", "xml2js": "^0.6.2" }, From 5f5a8467b4295a00f0536b72a237babf2947d7aa Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 26 Mar 2026 17:44:26 +0900 Subject: [PATCH 07/26] =?UTF-8?q?#265=20=E5=88=A9=E7=94=A8=E3=81=97?= =?UTF-8?q?=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84=E3=83=A9=E3=82=A4=E3=83=96?= =?UTF-8?q?=E3=83=A9=E3=83=AA=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 90 ++--------------------------------------------- package.json | 6 +--- 2 files changed, 4 insertions(+), 92 deletions(-) diff --git a/package-lock.json b/package-lock.json index 18fc476e..d80f75d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,22 +22,18 @@ "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", - "@types/node": "^25.5.0", "@typescript-eslint/eslint-plugin": "^8.57.2", "@typescript-eslint/parser": "^8.57.2", - "@vitest/web-worker": "^4.1.1", "@webgpu/types": "^0.1.69", "eslint": "^10.1.0", "eslint-plugin-unused-imports": "^4.4.1", "globals": "^17.4.0", "jsdom": "^29.0.1", "rollup": "^4.60.0", - "tslib": "^2.8.1", "typescript": "^5.9.3", "vite": "^8.0.3", "vitest": "^4.1.1", - "vitest-webgl-canvas-mock": "^1.1.0", - "xml2js": "^0.6.2" + "vitest-webgl-canvas-mock": "^1.1.0" }, "funding": { "type": "github", @@ -1554,16 +1550,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -1976,22 +1962,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/web-worker": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/web-worker/-/web-worker-4.1.1.tgz", - "integrity": "sha512-sjdtHgjfclId3/jHzBGQ8EmsiqfzzXrfIR0A17vGaqfngtdFum0TOWdZiKJVWuxBrJ+DBvjc3NwjEym9qf7S+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "obug": "^2.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "4.1.1" - } - }, "node_modules/@webgpu/types": { "version": "0.1.69", "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", @@ -3736,16 +3706,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -4027,7 +3987,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "optional": true }, "node_modules/type-check": { "version": "0.4.0", @@ -4066,13 +4027,6 @@ "node": ">=20.18.1" } }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4370,30 +4324,6 @@ "node": ">=18" } }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -4516,8 +4446,6 @@ }, "packages/text/node_modules/dom-serializer": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", @@ -4530,8 +4458,6 @@ }, "packages/text/node_modules/dom-serializer/node_modules/entities": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -4542,8 +4468,6 @@ }, "packages/text/node_modules/domelementtype": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "funding": [ { "type": "github", @@ -4554,8 +4478,6 @@ }, "packages/text/node_modules/domhandler": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" @@ -4569,8 +4491,6 @@ }, "packages/text/node_modules/domutils": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -4583,8 +4503,6 @@ }, "packages/text/node_modules/entities": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -4595,8 +4513,6 @@ }, "packages/text/node_modules/htmlparser2": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { diff --git a/package.json b/package.json index 1a7f36e2..283e2fd2 100644 --- a/package.json +++ b/package.json @@ -58,22 +58,18 @@ "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", - "@types/node": "^25.5.0", "@typescript-eslint/eslint-plugin": "^8.57.2", "@typescript-eslint/parser": "^8.57.2", - "@vitest/web-worker": "^4.1.1", "@webgpu/types": "^0.1.69", "eslint": "^10.1.0", "eslint-plugin-unused-imports": "^4.4.1", "globals": "^17.4.0", "jsdom": "^29.0.1", "rollup": "^4.60.0", - "tslib": "^2.8.1", "typescript": "^5.9.3", "vite": "^8.0.3", "vitest": "^4.1.1", - "vitest-webgl-canvas-mock": "^1.1.0", - "xml2js": "^0.6.2" + "vitest-webgl-canvas-mock": "^1.1.0" }, "peerDependencies": { "@next2d/cache": "file:packages/cache", From b9f84ba87a842e043cca6d08c440c8cea7d1b647 Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 26 Mar 2026 17:44:42 +0900 Subject: [PATCH 08/26] =?UTF-8?q?#265=20=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=83=8F=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Loader/usecase/LoaderLoadJsonUseCase.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/display/src/Loader/usecase/LoaderLoadJsonUseCase.ts b/packages/display/src/Loader/usecase/LoaderLoadJsonUseCase.ts index 3da35798..50ad82fb 100644 --- a/packages/display/src/Loader/usecase/LoaderLoadJsonUseCase.ts +++ b/packages/display/src/Loader/usecase/LoaderLoadJsonUseCase.ts @@ -17,14 +17,23 @@ import { $unzipWorker } from "../worker/UnzipWorker"; export const execute = async (loader: Loader, object: IAnimationToolData | IAnimationToolDataZlib): Promise => { if (object.type === "zlib") { - await new Promise((resolve): void => + await new Promise((resolve, reject): void => { $unzipWorker.onmessage = (event: MessageEvent): void => { + if (event.data && event.data.error) { + reject(new Error(event.data.error)); + return; + } loaderBuildService(loader, event.data as IAnimationToolData); resolve(); }; + $unzipWorker.onerror = (event: ErrorEvent): void => + { + reject(new Error(event.message)); + }; + const buffer: Uint8Array = new Uint8Array(object.buffer); $unzipWorker.postMessage(buffer, [buffer.buffer]); }); From 996cd54c608ba8d6ea8fa330ac6f8e2e7de8aefc Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 26 Mar 2026 20:01:22 +0900 Subject: [PATCH 09/26] =?UTF-8?q?#265=20zip=E8=A7=A3=E5=87=8D=E3=81=A0?= =?UTF-8?q?=E3=81=91=E3=81=AB=E7=89=B9=E5=8C=96=E3=81=97=E3=81=9F=E5=87=A6?= =?UTF-8?q?=E7=90=86=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Loader/worker/ZlibInflateWorker.ts | 410 +++++++++++++++++- 1 file changed, 400 insertions(+), 10 deletions(-) diff --git a/packages/display/src/Loader/worker/ZlibInflateWorker.ts b/packages/display/src/Loader/worker/ZlibInflateWorker.ts index 6653e1d6..f8f98825 100644 --- a/packages/display/src/Loader/worker/ZlibInflateWorker.ts +++ b/packages/display/src/Loader/worker/ZlibInflateWorker.ts @@ -1,26 +1,416 @@ "use strict"; -import { decompressSync } from "fflate"; +/** + * @description fflate非依存の高速 zlib/DEFLATE 解凍ワーカー (RFC 1950 / RFC 1951) + * High-performance zlib/DEFLATE decompression worker without fflate dependency. + */ + +const _$LEN_BASE = new Uint16Array([ + 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31, + 35,43,51,59,67,83,99,115,131,163,195,227,258 +]); +const _$LEN_EXTRA = new Uint8Array([ + 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2, + 3,3,3,3,4,4,4,4,5,5,5,5,0 +]); +const _$DIST_BASE = new Uint16Array([ + 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193, + 257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577 +]); +const _$DIST_EXTRA = new Uint8Array([ + 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6, + 7,7,8,8,9,9,10,10,11,11,12,12,13,13 +]); +const _$CL_ORDER = new Uint8Array([ + 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 +]); + +const _$MASK = new Uint32Array(17); +for (let i = 1; i < 17; i++) { _$MASK[i] = (1 << i) - 1 } + +const _$FIXED_LIT_CL = new Uint8Array(288); +for (let i = 0; i <= 143; i++) { _$FIXED_LIT_CL[i] = 8 } +for (let i = 144; i <= 255; i++) { _$FIXED_LIT_CL[i] = 9 } +for (let i = 256; i <= 279; i++) { _$FIXED_LIT_CL[i] = 7 } +for (let i = 280; i <= 287; i++) { _$FIXED_LIT_CL[i] = 8 } + +const _$FIXED_DIST_CL = new Uint8Array(32).fill(5); + +const _$decoder = new TextDecoder(); + +let _$outBuf = new Uint8Array(65536); + +const _$blCount = new Uint16Array(16); +const _$nextCode = new Uint16Array(16); +const _$clLens = new Uint8Array(19); +const _$codeLens = new Uint8Array(320); + +let _$dynClTbl: Uint32Array = new Uint32Array(128); +let _$dynLitTbl: Uint32Array = new Uint32Array(2048); +let _$dynDistTbl: Uint32Array = new Uint32Array(1024); + +/** + * @description canonical Huffmanルックアップテーブルを構築する。 + * 各エントリは (symbol << 4) | codeLength の形式で格納される。 + * Builds a canonical Huffman lookup table. Each entry stores (symbol << 4) | codeLength. + * + * @param {Uint8Array} codeLens - シンボルごとのコード長配列 + * @param {number} n - codeLensの有効要素数 + * @param {Uint32Array} [reuse] - 再利用するテーブルバッファ(GC回避用) + * @return {[Uint32Array, number]} [テーブル, 最大コード長] + * @method + * @private + */ +const _$buildTable = (codeLens: Uint8Array, n: number, reuse?: Uint32Array): [Uint32Array, number] => +{ + let max = 0; + _$blCount.fill(0); + for (let i = 0; i < n; i++) { + const l = codeLens[i]; + if (l) { + _$blCount[l]++; + if (l > max) { max = l } + } + } + if (!max) { + if (reuse) { + reuse[0] = 0; + return [reuse, 1]; + } + return [new Uint32Array(2), 1]; + } + + _$nextCode.fill(0); + for (let b = 1, c = 0; b <= max; b++) { + c = c + _$blCount[b - 1] << 1; + _$nextCode[b] = c; + } + + const size = 1 << max; + let tbl: Uint32Array; + if (reuse && reuse.length >= size) { + tbl = reuse; + tbl.fill(0, 0, size); + } else { + tbl = new Uint32Array(size); + } + + for (let s = 0; s < n; s++) { + const l = codeLens[s]; + if (!l) { continue } + let c = _$nextCode[l]++; + let r = 0; + for (let i = 0; i < l; i++) { + r = r << 1 | c & 1; + c >>= 1; + } + const entry = s << 4 | l; + for (let j = r; j < size; j += 1 << l) { + tbl[j] = entry; + } + } + + return [tbl, max]; +}; + +const [_$FIXED_LIT_TBL, _$FIXED_LIT_BITS] = _$buildTable(_$FIXED_LIT_CL, 288); +const [_$FIXED_DIST_TBL, _$FIXED_DIST_BITS] = _$buildTable(_$FIXED_DIST_CL, 32); +const _$FIXED_LIT_MASK = _$MASK[_$FIXED_LIT_BITS]; +const _$FIXED_DIST_MASK = _$MASK[_$FIXED_DIST_BITS]; + +let _$src: Uint8Array; +let _$pos: number; +let _$buf: number; +let _$cnt: number; + +/** + * @description ビットストリームからnビットを読み取って返す。 + * Reads n bits from the bit stream and returns the value. + * + * @param {number} n - 読み取るビット数 + * @return {number} 読み取った値 + * @method + * @private + */ +const _$bits = (n: number): number => +{ + while (_$cnt < n) { + _$buf |= _$src[_$pos++] << _$cnt; + _$cnt += 8; + } + const v = _$buf & _$MASK[n]; + _$buf >>>= n; + _$cnt -= n; + return v; +}; + +/** + * @description Huffmanルックアップテーブルから1シンボルをデコードする。 + * Decodes one symbol from the Huffman lookup table. + * + * @param {Uint32Array} tbl - Huffmanルックアップテーブル + * @param {number} maxBits - テーブルの最大コード長 + * @param {number} mask - ビットマスク ((1 << maxBits) - 1) + * @return {number} デコードされたシンボル値 + * @method + * @private + */ +const _$huf = (tbl: Uint32Array, maxBits: number, mask: number): number => +{ + while (_$cnt < maxBits) { + _$buf |= _$src[_$pos++] << _$cnt; + _$cnt += 8; + } + const e = tbl[_$buf & mask]; + _$buf >>>= e & 0xF; + _$cnt -= e & 0xF; + return e >> 4; +}; + +/** + * @description DEFLATEストリームを解凍する (RFC 1951)。 + * stored / fixed Huffman / dynamic Huffman の全ブロックタイプに対応。 + * Decompresses a DEFLATE stream. Supports all block types: stored, fixed and dynamic Huffman. + * + * @param {Uint8Array} data - 圧縮データ + * @param {number} offset - DEFLATEストリームの開始オフセット + * @return {Uint8Array} 解凍されたバイト列 + * @method + * @private + */ +const _$inflate = (data: Uint8Array, offset: number): Uint8Array => +{ + _$src = data; + _$pos = offset; + _$buf = 0; + _$cnt = 0; + + const estimatedSize = Math.max(data.length * 6, 4096); + if (_$outBuf.length < estimatedSize) { + _$outBuf = new Uint8Array(estimatedSize); + } + let out = _$outBuf; + let op = 0; + + let fin = 0; + while (!fin) { + fin = _$bits(1); + const bt = _$bits(2); + + if (bt === 0) { + const skip = _$cnt & 7; + _$buf >>>= skip; + _$cnt -= skip; + const len = _$bits(16); + _$bits(16); + + if (op + len > out.length) { + let sz = out.length; + while (sz < op + len) { sz <<= 1 } + const nb = new Uint8Array(sz); + nb.set(out); + out = nb; + } + + out.set(_$src.subarray(_$pos, _$pos + len), op); + _$pos += len; + op += len; + + } else if (bt === 3) { + throw new Error("Invalid DEFLATE block type"); + + } else { + let lt: Uint32Array, lm: number, lb: number; + let dt: Uint32Array, dm: number, db: number; + + if (bt === 1) { + lt = _$FIXED_LIT_TBL; + lb = _$FIXED_LIT_BITS; + lm = _$FIXED_LIT_MASK; + dt = _$FIXED_DIST_TBL; + db = _$FIXED_DIST_BITS; + dm = _$FIXED_DIST_MASK; + } else { + const hlit = _$bits(5) + 257; + const hdist = _$bits(5) + 1; + const hclen = _$bits(4) + 4; + + _$clLens.fill(0); + for (let i = 0; i < hclen; i++) { + _$clLens[_$CL_ORDER[i]] = _$bits(3); + } + let clb: number; + [_$dynClTbl, clb] = _$buildTable(_$clLens, 19, _$dynClTbl); + const clm = _$MASK[clb]; + + const total = hlit + hdist; + _$codeLens.fill(0, 0, total); + for (let i = 0; i < total;) { + const s = _$huf(_$dynClTbl, clb, clm); + if (s < 16) { + _$codeLens[i++] = s; + } else if (s === 16) { + const p = _$codeLens[i - 1]; + for (let r = _$bits(2) + 3; r > 0; r--) { _$codeLens[i++] = p } + } else if (s === 17) { + i += _$bits(3) + 3; + } else { + i += _$bits(7) + 11; + } + } + + [_$dynLitTbl, lb] = _$buildTable(_$codeLens.subarray(0, hlit), hlit, _$dynLitTbl); + lm = _$MASK[lb]; + lt = _$dynLitTbl; + [_$dynDistTbl, db] = _$buildTable(_$codeLens.subarray(hlit, total), hdist, _$dynDistTbl); + dm = _$MASK[db]; + dt = _$dynDistTbl; + } + + for (;;) { + + while (_$cnt < lb) { + _$buf |= _$src[_$pos++] << _$cnt; + _$cnt += 8; + } + const le = lt[_$buf & lm]; + const sl = le & 0xF; + _$buf >>>= sl; + _$cnt -= sl; + const sym = le >> 4; + + if (sym < 256) { + if (op >= out.length) { + const nb = new Uint8Array(out.length << 1); + nb.set(out); + out = nb; + } + out[op++] = sym; + + } else if (sym === 256) { + break; + + } else { + const li = sym - 257; + let length = _$LEN_BASE[li]; + const le2 = _$LEN_EXTRA[li]; + if (le2) { + while (_$cnt < le2) { + _$buf |= _$src[_$pos++] << _$cnt; + _$cnt += 8; + } + length += _$buf & _$MASK[le2]; + _$buf >>>= le2; + _$cnt -= le2; + } + + while (_$cnt < db) { + _$buf |= _$src[_$pos++] << _$cnt; + _$cnt += 8; + } + const de = dt[_$buf & dm]; + const dl = de & 0xF; + _$buf >>>= dl; + _$cnt -= dl; + const di = de >> 4; + + let dist = _$DIST_BASE[di]; + const de2 = _$DIST_EXTRA[di]; + if (de2) { + while (_$cnt < de2) { + _$buf |= _$src[_$pos++] << _$cnt; + _$cnt += 8; + } + dist += _$buf & _$MASK[de2]; + _$buf >>>= de2; + _$cnt -= de2; + } + + if (op + length > out.length) { + let sz = out.length; + while (sz < op + length) { sz <<= 1 } + const nb = new Uint8Array(sz); + nb.set(out); + out = nb; + } + + const sp = op - dist; + if (dist === 1) { + out.fill(out[sp], op, op + length); + op += length; + } else if (dist >= length) { + out.copyWithin(op, sp, sp + length); + op += length; + } else { + out.copyWithin(op, sp, sp + dist); + let copied = dist; + while (copied < length) { + const chunk = Math.min(copied, length - copied); + out.copyWithin(op + copied, op, op + chunk); + copied += chunk; + } + op += length; + } + } + } + } + } + + _$outBuf = out; + + return out.subarray(0, op); +}; /** - * @description zilbの圧縮されたデータを解凍します。 - * Unzips zlib-compressed data. + * @description zlibラッパー付きデータを解凍する (RFC 1950)。 + * zlibヘッダーを検出した場合は2バイトスキップし、それ以外は生DEFLATEとして処理する。 + * Decompresses zlib-wrapped data. Skips the 2-byte zlib header if detected, + * otherwise treats input as raw DEFLATE. * - * @param {MessageEvent} event + * @param {Uint8Array} input - zlib圧縮データまたは生DEFLATEストリーム + * @return {Uint8Array} 解凍されたバイト列 + * @method + * @private + */ +const _$zlibDecompress = (input: Uint8Array): Uint8Array => +{ + const cmf = input[0]; + const flg = input[1]; + + const isZlib = (cmf & 0x0F) === 8 + && (cmf * 256 + flg) % 31 === 0 + && !(flg & 0x20); + + return _$inflate(input, isZlib ? 2 : 0); +}; + +/** + * @description zlibの圧縮されたデータを解凍し、JSONとしてパースして返すワーカーエントリポイント。 + * Worker entry point that decompresses zlib data, decodes as URI-encoded string, + * parses as JSON and posts the result back. + * + * @param {MessageEvent} event - 圧縮データ (Uint8Array) を含むメッセージイベント * @return {void} * @method * @public */ self.addEventListener("message", (event: MessageEvent): void => { - const buffer = decompressSync(event.data); + try { - let json = ""; - for (let idx: number = 0; idx < buffer.length; idx += 4096) { - json += String.fromCharCode(...buffer.subarray(idx, idx + 4096)); - } + const buffer = _$zlibDecompress(event.data); + + self.postMessage(JSON.parse( + decodeURIComponent(_$decoder.decode(buffer)) + )); + + } catch (e) { - self.postMessage(JSON.parse(decodeURIComponent(json))); + self.postMessage({ + "error": e instanceof Error ? e.message : "Unknown decompression error" + }); + + } }); export default {}; \ No newline at end of file From 3384c888d6f57a5cc6e2240971b96634347a2be0 Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 26 Mar 2026 21:13:58 +0900 Subject: [PATCH 10/26] =?UTF-8?q?#266=20htmlText=E3=81=AE=E5=87=BA?= =?UTF-8?q?=E5=8A=9B=E7=B5=90=E6=9E=9C=E3=82=92e2e=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=81=A8=E3=81=97=E3=81=A6=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- e2e/pages/textfield/html-text.html | 321 ++++++++++++++++++ .../textfield-basic-webgl-darwin.png | Bin 52859 -> 52632 bytes .../textfield-html-text-webgl-darwin.png | Bin 0 -> 31303 bytes .../textfield-basic-webgpu-darwin.png | Bin 53555 -> 53296 bytes .../textfield-html-text-webgpu-darwin.png | Bin 0 -> 61683 bytes e2e/tests/textfield.spec.ts | 7 + 6 files changed, 328 insertions(+) create mode 100644 e2e/pages/textfield/html-text.html create mode 100644 e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-html-text-webgl-darwin.png create mode 100644 e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-html-text-webgpu-darwin.png diff --git a/e2e/pages/textfield/html-text.html b/e2e/pages/textfield/html-text.html new file mode 100644 index 00000000..a1be44cd --- /dev/null +++ b/e2e/pages/textfield/html-text.html @@ -0,0 +1,321 @@ + + + + + + Next2D E2E - TextField htmlText + + + + + + + diff --git a/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-basic-webgl-darwin.png b/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-basic-webgl-darwin.png index 89d2d6ace076925535bff68198a24c48bded0084..878a0b409aecd3d27a1fd783daad1b85011cf363 100644 GIT binary patch delta 13527 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

    %BrfWDk?kXrlt>z%%)}4mOK{V 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 diff --git a/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-html-text-webgl-darwin.png b/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-html-text-webgl-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..59287e4f393d995dab9969053d790943106ff4a1 GIT binary patch literal 31303 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

    8?MY?wVI^~ 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
    {p3jnOtc}smK7#SyP(>I&P3k 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 Vm)mMA;CD`-WtC*|q@TX|zW@@+Qj!1w delta 48165 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
    w6X zdUXV=kCsJquUpiRL#*zNhK%UGmaSMi-fOqUDmfDLK3cZq268XWg1Of3Y;6b!n=h+Xtq*Xu=^u7w@79Cd{!`d>TD~>Z+sr za$n*TLdF4ZT1X;yR-ak?)fVpG8(69~mX*L3VUrGc`%g20U#mi>?c(qul$@^n>nihU zN%NFm;E+D@sefL`wT`MnRVFY)00zL1?L9O7)rKRIu+#p25FP#-1Jg@@}uxZcshL85DwyCV)iNO^%Nryu$OZ*c*c__#Q-e}KW) zuhp8>64KIq=peFgK^$veUk7G^KehAv;JM}?q=n2bB#?+esU-^2aSP@9{-3;Sb!`py zwE#Q1Ko9h@v#8WW)hl2k2x>V|^eWhSdnlTumMh>qM14x)x$NK_QLLxSEi_KB*Co`8yWole-O%D?r&|BBNof%nL>5jdKly=Hglzmpf)g%N=adZbaZB8 z0{&fp{;ZKfIc?qZ%Y#^Qf;X38Fv80(!$yXh#oQ*kw|#9M*gWvuHV@4H)APC~LTp=X zxUyEuDKTuF_V&G~&F8WZUqV12@y$NiYq@vVsP+&cL5b44P}k3Xw#dm?GlW@GS@I-_ zGanyD?A1tCtgsX7vJi{5^}5g3VG+1-5_IJ-^#;qqrN^2uZi zXurY3aCPi+WmI9ZNv_0Psy;%3SzsR~L8S$ZlKCF#?3MEoSO$UFh$^S2K4Gmi?yzA2 zv6zEJY;Hrb)Plj10|r#sXVc(5%9CuYDuH!z0Tsm0ft&r?mj?PD&Y;%}Xy8MTDYNR5 z@+c~W;TLk7tx2hrk(s%UUiICeLebG5JtLSVv_`bewY+@4dUSVEK0p2A3j#t7etgE|Q`)S;S(q^JnpMn{K zfd=v*n^(l@@1S59Yrx)gX1p}{QOthHf7V4E zX-B+~6~f!Y&mF%-VPGd3?Txmer>DOd8KcY3xAf9t2kJT|H7o_5wgo?5NyfB(qe@rf zkNXkV9@z#!0{pDFnagI+vaw%0D4ZNL$UYV^+58lBUFTxQ6mZ=!FzE_9*#kp_S1)IS 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 literal 0 HcmV?d00001 diff --git a/e2e/tests/textfield.spec.ts b/e2e/tests/textfield.spec.ts index 465e3789..342d78d6 100644 --- a/e2e/tests/textfield.spec.ts +++ b/e2e/tests/textfield.spec.ts @@ -57,4 +57,11 @@ test.describe("TextFieldテスト", () => { await expect(page).toHaveScreenshot("textfield-auto-font-size.png"); }); + + test("htmlText(全タグ: b, i, u, font, span, p, div, br + スタイル属性)", async ({ page }) => { + await page.goto("/e2e/pages/textfield/html-text.html"); + await waitForCanvas(page); + + await expect(page).toHaveScreenshot("textfield-html-text.png"); + }); }); From f8131bef5a3d780b73c62e8c1700d111b2f8166b Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 26 Mar 2026 22:38:29 +0900 Subject: [PATCH 11/26] =?UTF-8?q?#266=20htmlText=E3=81=AE=E3=83=91?= =?UTF-8?q?=E3=83=BC=E3=82=B9=E5=87=A6=E7=90=86=E3=82=92=E9=AB=98=E9=80=9F?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 201 +----- package.json | 3 - packages/text/package.json | 3 - .../TextParserHtmlParserService.test.ts | 160 +++++ .../service/TextParserHtmlParserService.ts | 371 ++++++++++++ .../usecase/TextParserParseHtmlTextUseCase.ts | 3 +- .../usecase/TextParserParseTagUseCase.test.ts | 3 +- .../usecase/TextParserParseTagUseCase.ts | 571 +++++++++++++++++- packages/text/src/interface/IHtmlNode.ts | 27 + 9 files changed, 1104 insertions(+), 238 deletions(-) create mode 100644 packages/text/src/TextParser/service/TextParserHtmlParserService.test.ts create mode 100644 packages/text/src/TextParser/service/TextParserHtmlParserService.ts create mode 100644 packages/text/src/interface/IHtmlNode.ts diff --git a/package-lock.json b/package-lock.json index d80f75d3..cd69c459 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,6 @@ "workspaces": [ "packages/*" ], - "dependencies": { - "htmlparser2": "^12.0.0" - }, "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", @@ -2217,80 +2214,14 @@ "node": ">=8" } }, - "node_modules/dom-serializer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.0.0.tgz", - "integrity": "sha512-x+9D6nkC8tdXOQUS32egtZpZFLP90+HBZmWjuT920srbJvD/zPgFB9t4k3pEhlw5BQrXStQtRc1Y1zuriXk+Nw==", - "license": "MIT", - "dependencies": { - "domelementtype": "^3.0.0", - "domhandler": "^6.0.0", - "entities": "^8.0.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", - "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/domhandler": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", - "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^3.0.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", - "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^3.0.0", - "domelementtype": "^3.0.0", - "domhandler": "^6.0.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=20.19.0" + "node": ">=0.12" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -2751,28 +2682,6 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/htmlparser2": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-12.0.0.tgz", - "integrity": "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^3.0.0", - "domhandler": "^6.0.0", - "domutils": "^4.0.2", - "entities": "^8.0.0" - }, - "engines": { - "node": ">=20.19.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3438,19 +3347,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4433,9 +4329,6 @@ "name": "@next2d/text", "version": "*", "license": "MIT", - "dependencies": { - "htmlparser2": "^10.0.0" - }, "peerDependencies": { "@next2d/cache": "file:../cache", "@next2d/display": "file:../display", @@ -4444,90 +4337,6 @@ "@next2d/ui": "file:../ui" } }, - "packages/text/node_modules/dom-serializer": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "packages/text/node_modules/dom-serializer/node_modules/entities": { - "version": "4.5.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "packages/text/node_modules/domelementtype": { - "version": "2.3.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "packages/text/node_modules/domhandler": { - "version": "5.0.3", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "packages/text/node_modules/domutils": { - "version": "3.2.2", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "packages/text/node_modules/entities": { - "version": "7.0.1", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "packages/text/node_modules/htmlparser2": { - "version": "10.1.0", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" - } - }, "packages/texture-packer": { "name": "@next2d/texture-packer", "version": "*", diff --git a/package.json b/package.json index 283e2fd2..6b68985b 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,6 @@ "type": "github", "url": "https://github.com/sponsors/Next2D" }, - "dependencies": { - "htmlparser2": "^12.0.0" - }, "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", diff --git a/packages/text/package.json b/packages/text/package.json index 7a7e11fb..dee4c87c 100644 --- a/packages/text/package.json +++ b/packages/text/package.json @@ -23,9 +23,6 @@ "type": "git", "url": "git+https://github.com/Next2D/Player.git" }, - "dependencies": { - "htmlparser2": "^10.0.0" - }, "peerDependencies": { "@next2d/display": "file:../display", "@next2d/geom": "file:../geom", diff --git a/packages/text/src/TextParser/service/TextParserHtmlParserService.test.ts b/packages/text/src/TextParser/service/TextParserHtmlParserService.test.ts new file mode 100644 index 00000000..dc32b56a --- /dev/null +++ b/packages/text/src/TextParser/service/TextParserHtmlParserService.test.ts @@ -0,0 +1,160 @@ +import type { IHtmlElementNode, IHtmlTextNode } from "../../interface/IHtmlNode"; +import { execute } from "./TextParserHtmlParserService"; +import { describe, expect, it } from "vitest"; + +describe("TextParserHtmlParserService.ts test", () => +{ + it("empty string returns empty array", () => + { + const result = execute(""); + expect(result.length).toBe(0); + }); + + it("plain text without tags", () => + { + const result = execute("hello world"); + expect(result.length).toBe(1); + expect(result[0].type).toBe("text"); + expect((result[0] as IHtmlTextNode).value).toBe("hello world"); + }); + + it("single tag with text", () => + { + const result = execute("bold"); + expect(result.length).toBe(1); + + const el = result[0] as IHtmlElementNode; + expect(el.type).toBe("element"); + expect(el.tagName).toBe("B"); + expect(el.children.length).toBe(1); + expect((el.children[0] as IHtmlTextNode).value).toBe("bold"); + }); + + it("nested tags", () => + { + const result = execute("

    text

    "); + expect(result.length).toBe(1); + + const p = result[0] as IHtmlElementNode; + expect(p.tagName).toBe("P"); + expect(p.children.length).toBe(1); + + const b = p.children[0] as IHtmlElementNode; + expect(b.tagName).toBe("B"); + expect(b.children.length).toBe(1); + expect((b.children[0] as IHtmlTextNode).value).toBe("text"); + }); + + it("tag with attributes (double quotes)", () => + { + const result = execute('text'); + expect(result.length).toBe(1); + + const font = result[0] as IHtmlElementNode; + expect(font.tagName).toBe("FONT"); + expect(font.attributes.length).toBe(3); + expect(font.attributes[0]).toEqual({ name: "face", value: "Arial" }); + expect(font.attributes[1]).toEqual({ name: "size", value: "12" }); + expect(font.attributes[2]).toEqual({ name: "color", value: "#FF0000" }); + }); + + it("tag with attributes (single quotes)", () => + { + const result = execute("text"); + const font = result[0] as IHtmlElementNode; + expect(font.attributes[0]).toEqual({ name: "face", value: "Arial" }); + }); + + it("style attribute", () => + { + const result = execute('text'); + const span = result[0] as IHtmlElementNode; + expect(span.tagName).toBe("SPAN"); + expect(span.attributes.length).toBe(1); + expect(span.attributes[0].name).toBe("style"); + expect(span.attributes[0].value).toBe("font-size: 14px; color: red;"); + }); + + it("self-closing br tag", () => + { + const result = execute("a
    b"); + expect(result.length).toBe(3); + expect((result[0] as IHtmlTextNode).value).toBe("a"); + expect((result[1] as IHtmlElementNode).tagName).toBe("BR"); + expect((result[1] as IHtmlElementNode).children.length).toBe(0); + expect((result[2] as IHtmlTextNode).value).toBe("b"); + }); + + it("self-closing br/ tag", () => + { + const result = execute("a
    b"); + expect(result.length).toBe(3); + expect((result[1] as IHtmlElementNode).tagName).toBe("BR"); + expect((result[1] as IHtmlElementNode).children.length).toBe(0); + }); + + it("tag name is case-insensitive", () => + { + const result = execute("text"); + const el = result[0] as IHtmlElementNode; + expect(el.tagName).toBe("FONT"); + }); + + it("complex HTML matching existing test input", () => + { + const htmlText = `

    + + +
    +えお順 +

    `; + const cleaned = htmlText.trim().replace(/\r?\n/g, "").replace(/\t/g, ""); + const result = execute(cleaned); + + expect(result.length).toBe(1); + + const p = result[0] as IHtmlElementNode; + expect(p.tagName).toBe("P"); + + // children:
    えお順 + const children = p.children; + + // + const u = children.find((n) => n.type === "element" && n.tagName === "U") as IHtmlElementNode; + expect(u).toBeDefined(); + expect((u.children[0] as IHtmlTextNode).value).toBe("あ"); + + // + const b = children.find((n) => n.type === "element" && n.tagName === "B") as IHtmlElementNode; + expect(b).toBeDefined(); + expect((b.children[0] as IHtmlTextNode).value).toBe("い"); + + // + const i = children.find((n) => n.type === "element" && n.tagName === "I") as IHtmlElementNode; + expect(i).toBeDefined(); + expect((i.children[0] as IHtmlTextNode).value).toBe("う"); + + //
    + const br = children.find((n) => n.type === "element" && n.tagName === "BR") as IHtmlElementNode; + expect(br).toBeDefined(); + expect(br.children.length).toBe(0); + }); + + it("div tag with align attribute", () => + { + const result = execute('
    text
    '); + const div = result[0] as IHtmlElementNode; + expect(div.tagName).toBe("DIV"); + expect(div.attributes[0]).toEqual({ name: "align", value: "center" }); + expect((div.children[0] as IHtmlTextNode).value).toBe("text"); + }); + + it("multiple sibling tags", () => + { + const result = execute("abc"); + expect(result.length).toBe(3); + expect((result[0] as IHtmlElementNode).tagName).toBe("U"); + expect((result[1] as IHtmlElementNode).tagName).toBe("B"); + expect((result[2] as IHtmlElementNode).tagName).toBe("I"); + }); +}); diff --git a/packages/text/src/TextParser/service/TextParserHtmlParserService.ts b/packages/text/src/TextParser/service/TextParserHtmlParserService.ts new file mode 100644 index 00000000..74334d96 --- /dev/null +++ b/packages/text/src/TextParser/service/TextParserHtmlParserService.ts @@ -0,0 +1,371 @@ +import type { IAttributeObject } from "../../interface/IAttributeObject"; +import type { IHtmlNode } from "../../interface/IHtmlNode"; + +/** + * @description モジュールレベルのパーサ状態(クロージャ生成を回避) + * Module-level parser state (avoids closure allocation) + * + * @type {string} + * @private + */ +let _$html: string = ""; + +/** + * @description 現在の解析位置 + * Current parse position + * + * @type {number} + * @private + */ +let _$pos: number = 0; + +/** + * @description HTML文字列の長さ(キャッシュ) + * Cached length of the HTML string + * + * @type {number} + * @private + */ +let _$len: number = 0; + +/** + * @description void要素(BR等)の空children共有インスタンス + * 再利用によりアロケーションを回避する + * Shared empty children array for void elements (e.g. BR) + * Reused across calls to avoid allocation + * + * @type {IHtmlNode[]} + * @const + * @private + */ +const $EMPTY_CHILDREN: IHtmlNode[] = []; + +/** + * @description 軽量HTMLパーサ — TextField用の限定HTMLサブセットを1-passで解析 + * 対応タグ: B, I, U, P, BR, DIV, FONT, SPAN + * 対応属性: align, face, size, color, style, letterSpacing, + * leading, leftMargin, rightMargin, underline, bold, italic + * Lightweight HTML parser — single-pass parse for TextField HTML subset + * + * @param {string} html - 解析対象のHTML文字列 / HTML string to parse + * @return {IHtmlNode[]} 解析結果のノード配列 / Array of parsed nodes + * @method + * @protected + */ +export const execute = (html: string): IHtmlNode[] => +{ + _$html = html; + _$pos = 0; + _$len = html.length; + + const result = $parseChildren(""); + + _$html = ""; + + return result; +}; + +/** + * @description 子ノードを再帰的に解析する + * parentTagに一致する閉じタグを検出すると、そのスコープの解析を終了して返却する + * Recursively parse child nodes. + * Returns when a closing tag matching parent_tag is found. + * + * @param {string} parent_tag - 親要素のタグ名(大文字)。ルートの場合は空文字列 + * Parent element tag name (uppercase). Empty string for root. + * @return {IHtmlNode[]} 解析された子ノード配列 / Array of parsed child nodes + * @private + */ +const $parseChildren = (parent_tag: string): IHtmlNode[] => +{ + const nodes: IHtmlNode[] = []; + + while (_$pos < _$len) { + + const ltIdx = _$html.indexOf("<", _$pos); + + if (ltIdx === -1) { + if (_$pos < _$len) { + nodes[nodes.length] = { + "type": "text", + "value": _$html.substring(_$pos) + }; + _$pos = _$len; + } + break; + } + + if (ltIdx > _$pos) { + nodes[nodes.length] = { + "type": "text", + "value": _$html.substring(_$pos, ltIdx) + }; + } + + _$pos = ltIdx + 1; + + if (_$pos >= _$len) { + break; + } + + // closing tag: '", _$pos); + if (gtIdx === -1) { + _$pos = _$len; + break; + } + + if (parent_tag && $matchesUpperCase(_$pos, gtIdx, parent_tag)) { + _$pos = gtIdx + 1; + return nodes; + } + + _$pos = gtIdx + 1; + continue; + } + + const tagStart = _$pos; + while (_$pos < _$len) { + const c = _$html.charCodeAt(_$pos); + if (c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) { + break; + } + _$pos++; + } + const tagName = $toUpperCase(tagStart, _$pos); + + const attributes = $parseAttributes(); + + // self-closing '/>' + let selfClosing = false; + if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x2F) { + selfClosing = true; + _$pos++; + } + + if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x3E) { + _$pos++; + } + + if (selfClosing || tagName === "BR") { + nodes[nodes.length] = { + "type": "element", + "tagName": tagName, + "attributes": attributes, + "children": $EMPTY_CHILDREN + }; + } else { + nodes[nodes.length] = { + "type": "element", + "tagName": tagName, + "attributes": attributes, + "children": $parseChildren(tagName) + }; + } + } + + return nodes; +}; + +/** + * @description 現在位置から開始タグの属性を解析して IAttributeObject[] として返す + * 属性形式: name="value", name='value', name=value, name (boolean) + * '>' または '/' に到達した時点で解析を終了する + * Parse attributes from current position and return as IAttributeObject[]. + * Supports: name="value", name='value', name=value, name (boolean). + * Stops when '>' or '/' is encountered. + * + * @return {IAttributeObject[]} 解析された属性オブジェクトの配列 / Array of parsed attribute objects + * @private + */ +const $parseAttributes = (): IAttributeObject[] => +{ + const attrs: IAttributeObject[] = []; + + while (_$pos < _$len) { + + while (_$pos < _$len) { + const c = _$html.charCodeAt(_$pos); + if (c !== 0x20 && c !== 0x09) { + break; + } + _$pos++; + } + + if (_$pos >= _$len) { + break; + } + + const ch = _$html.charCodeAt(_$pos); + + // '>' or '/' + if (ch === 0x3E || ch === 0x2F) { + break; + } + + const nameStart = _$pos; + while (_$pos < _$len) { + const c = _$html.charCodeAt(_$pos); + if (c === 0x3D || c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) { + break; + } + _$pos++; + } + + if (_$pos === nameStart) { + break; + } + + const name = _$html.substring(nameStart, _$pos); + + while (_$pos < _$len) { + const c = _$html.charCodeAt(_$pos); + if (c !== 0x20 && c !== 0x09) { + break; + } + _$pos++; + } + + if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x3D) { + _$pos++; + + while (_$pos < _$len) { + const c = _$html.charCodeAt(_$pos); + if (c !== 0x20 && c !== 0x09) { + break; + } + _$pos++; + } + + let value: string; + const quote = _$html.charCodeAt(_$pos); + + if (quote === 0x22 || quote === 0x27) { + _$pos++; + const closeIdx = _$html.indexOf( + quote === 0x22 ? "\"" : "'", _$pos + ); + if (closeIdx === -1) { + value = _$html.substring(_$pos); + _$pos = _$len; + } else { + value = _$html.substring(_$pos, closeIdx); + _$pos = closeIdx + 1; + } + } else { + // unquoted value + const valStart = _$pos; + while (_$pos < _$len) { + const c = _$html.charCodeAt(_$pos); + if (c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) { + break; + } + _$pos++; + } + value = _$html.substring(valStart, _$pos); + } + + attrs[attrs.length] = { "name": name, "value": value }; + } else { + attrs[attrs.length] = { "name": name, "value": true }; + } + } + + return attrs; +}; + +/** + * @description 閉じタグ名を文字列生成せずにparentTag(大文字)と照合する + * _$html[start..end) の範囲を1文字ずつ大文字変換しながら比較する + * ゼロアロケーションで一致判定を行い、GC圧を回避する + * Compare closing tag name against parent_tag (uppercase) without string allocation. + * Converts each character to uppercase via charCode and compares in-place. + * Zero-allocation comparison to avoid GC pressure. + * + * @param {number} start - 照合開始位置('' の位置)/ End index (position of '>') + * @param {string} tag - 比較対象の親タグ名(大文字)/ Parent tag name to match (uppercase) + * @return {boolean} 一致する場合true / True if the closing tag matches + * @private + */ +const $matchesUpperCase = ( + start: number, + end: number, + tag: string +): boolean => +{ + while (start < end) { + const c = _$html.charCodeAt(start); + if (c !== 0x20 && c !== 0x09) { + break; + } + start++; + } + + while (end > start) { + const c = _$html.charCodeAt(end - 1); + if (c !== 0x20 && c !== 0x09) { + break; + } + end--; + } + + const tagLen = tag.length; + if (end - start !== tagLen) { + return false; + } + + for (let i = 0; i < tagLen; i++) { + let c = _$html.charCodeAt(start + i); + if (c >= 0x61 && c <= 0x7A) { + c -= 0x20; + } + if (c !== tag.charCodeAt(i)) { + return false; + } + } + + return true; +}; + +/** + * @description _$html[start..end) の部分文字列を大文字化して返す + * 1-2文字はString.fromCharCodeで直接生成(短いタグ名の最速パス) + * 3文字以上はsubstring().toUpperCase()にフォールバック + * Convert _$html[start..end) substring to uppercase. + * 1-2 char tags use String.fromCharCode (fastest path for short tag names). + * 3+ chars fall back to substring().toUpperCase(). + * + * @param {number} start - 開始位置 / Start index + * @param {number} end - 終了位置 / End index (exclusive) + * @return {string} 大文字化されたタグ名 / Uppercased tag name + * @private + */ +const $toUpperCase = (start: number, end: number): string => +{ + const length = end - start; + + // 1文字タグ (B, I, U, P) — 最頻出パスを最速処理 + if (length === 1) { + let c = _$html.charCodeAt(start); + if (c >= 0x61 && c <= 0x7A) { + c -= 0x20; + } + return String.fromCharCode(c); + } + + // 2文字タグ (BR, DIV先頭判定高速化) + if (length === 2) { + let c0 = _$html.charCodeAt(start); + let c1 = _$html.charCodeAt(start + 1); + if (c0 >= 0x61 && c0 <= 0x7A) { c0 -= 0x20 } + if (c1 >= 0x61 && c1 <= 0x7A) { c1 -= 0x20 } + return String.fromCharCode(c0, c1); + } + + // 3-4文字タグ (DIV, FONT, SPAN) + return _$html.substring(start, end).toUpperCase(); +}; diff --git a/packages/text/src/TextParser/usecase/TextParserParseHtmlTextUseCase.ts b/packages/text/src/TextParser/usecase/TextParserParseHtmlTextUseCase.ts index cec8df3b..1906071a 100644 --- a/packages/text/src/TextParser/usecase/TextParserParseHtmlTextUseCase.ts +++ b/packages/text/src/TextParser/usecase/TextParserParseHtmlTextUseCase.ts @@ -1,7 +1,6 @@ import type { TextFormat } from "../../TextFormat"; import type { IOptions } from "../../interface/IOptions"; import { TextData } from "../../TextData"; -import { parseDocument } from "htmlparser2"; import { execute as textParserCreateNewLineUseCase } from "./TextParserCreateNewLineUseCase"; import { execute as textParserAdjustmentHeightService } from "../service/TextParserAdjustmentHeightService"; import { execute as textParserParseTagUseCase } from "./TextParserParseTagUseCase"; @@ -44,7 +43,7 @@ export const execute = ( textParserCreateNewLineUseCase(textData, textFormat); textParserParseTagUseCase( - parseDocument(htmlText), textFormat, textData, options + htmlText, textFormat, textData, options ); textParserAdjustmentHeightService(textData); diff --git a/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.test.ts b/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.test.ts index 760d50f2..b37afda6 100644 --- a/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.test.ts +++ b/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.test.ts @@ -1,7 +1,6 @@ import { execute } from "./TextParserParseTagUseCase"; import { TextData } from "../../TextData"; import { TextFormat } from "../../TextFormat"; -import { parseDocument } from "htmlparser2"; import { describe, expect, it } from "vitest"; describe("TextParserParseTagUseCase.js test", () => @@ -33,7 +32,7 @@ describe("TextParserParseTagUseCase.js test", () => えお順

    `; - execute(parseDocument(htmlText.trim().replace(/\r?\n/g, "").replace(/\t/g, "")), textFormat, textData, { + execute(htmlText.trim().replace(/\r?\n/g, "").replace(/\t/g, ""), textFormat, textData, { "width": 200, "multiline": true, "wordWrap": true, diff --git a/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.ts b/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.ts index 98f71c0a..81c863fe 100644 --- a/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.ts +++ b/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.ts @@ -1,16 +1,41 @@ import type { TextFormat } from "../../TextFormat"; import type { TextData } from "../../TextData"; import type { IOptions } from "../../interface/IOptions"; -import type { Document, Element, ChildNode } from "domhandler"; +import type { IAttributeObject } from "../../interface/IAttributeObject"; +import type { ITextFormatAlign } from "../../interface/ITextFormatAlign"; import { execute as textParserParseTextUseCase } from "./TextParserParseTextUseCase"; import { execute as textParserCreateNewLineUseCase } from "./TextParserCreateNewLineUseCase"; -import { execute as textParserSetAttributesUseCase } from "./TextParserSetAttributesUseCase"; +import { execute as textParserParseStyleService } from "../service/TextParserParseStyleService"; +import { $toColorInt } from "../../TextUtil"; /** - * @description タグを解析してTextDataとTextFormatを設定 - * Analyze tags and set TextData and TextFormat + * @description 数値タグID — 文字列生成を完全排除 + * Numeric tag IDs — eliminates all tag string allocation + */ +const $TAG_NONE: number = 0; +const $TAG_B: number = 1; +const $TAG_I: number = 2; +const $TAG_U: number = 3; +const $TAG_P: number = 4; +const $TAG_BR: number = 5; +const $TAG_DIV: number = 6; +const $TAG_FONT: number = 7; +const $TAG_SPAN: number = 8; + +/** + * @description モジュールレベルのパーサ状態 + * Module-level parser state + */ +let _$html: string = ""; +let _$pos: number = 0; +let _$len: number = 0; + +/** + * @description HTMLタグを直接解析してTextDataを構築 + * 中間ツリーなし、タグ名文字列なし、clone()なし、属性オブジェクトなし + * Parse HTML directly — no tree, no tag strings, no clone, no attr objects * - * @param {Document} document + * @param {string} html * @param {TextFormat} text_format * @param {TextData} text_data * @param {IOptions} options @@ -19,68 +44,550 @@ import { execute as textParserSetAttributesUseCase } from "./TextParserSetAttrib * @protected */ export const execute = ( - document: Document, + html: string, + text_format: TextFormat, + text_data: TextData, + options: IOptions +): void => { + + _$html = html; + _$pos = 0; + _$len = html.length; + + $processChildren($TAG_NONE, text_format, text_data, options); + + _$html = ""; +}; + +/** + * @description 子要素をストリーミング解析・処理(再帰) + * save/restoreでTextFormatをスタック管理 — clone()ゼロ + * Stream-parse children with stack-based format — zero clone() + */ +const $processChildren = ( + parent_tag_id: number, text_format: TextFormat, text_data: TextData, options: IOptions ): void => { - for (let idx = 0; idx < document.children.length; ++idx) { + while (_$pos < _$len) { - const node = document.children[idx] as ChildNode; + const ltIdx: number = _$html.indexOf("<", _$pos); + + if (ltIdx === -1) { + textParserParseTextUseCase( + _$html.substring(_$pos), text_format, text_data, options + ); + _$pos = _$len; + break; + } - if (node.nodeType === 3) { + if (ltIdx > _$pos) { + textParserParseTextUseCase( + _$html.substring(_$pos, ltIdx), text_format, text_data, options + ); + } - textParserParseTextUseCase(node.nodeValue || "", text_format, text_data, options); + _$pos = ltIdx + 1; + if (_$pos >= _$len) { + break; + } + // closing tag: '", _$pos); + if (gtIdx === -1) { + _$pos = _$len; + break; + } + if (parent_tag_id !== $TAG_NONE && $identifyTag(_$pos, gtIdx) === parent_tag_id) { + _$pos = gtIdx + 1; + return; + } + _$pos = gtIdx + 1; continue; + } + const tagStart: number = _$pos; + while (_$pos < _$len) { + const c: number = _$html.charCodeAt(_$pos); + if (c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) { + break; + } + _$pos++; } + const tagId: number = $identifyTag(tagStart, _$pos); - const textFormat = text_format.clone(); - switch ((node as Element).name.toUpperCase()) { + switch (tagId) { - case "DIV": // div tag - case "P": // p tag - textParserSetAttributesUseCase((node as Element).attributes, textFormat, options); + case $TAG_B: { + const saved: boolean | null = text_format.bold; + text_format.bold = true; + $skipToClose(); + $processChildren(tagId, text_format, text_data, options); + text_format.bold = saved; + continue; + } - if (options.multiline) { - textParserCreateNewLineUseCase(text_data, textFormat); + case $TAG_I: { + const saved: boolean | null = text_format.italic; + text_format.italic = true; + $skipToClose(); + $processChildren(tagId, text_format, text_data, options); + text_format.italic = saved; + continue; + } + + case $TAG_U: { + const saved: boolean | null = text_format.underline; + text_format.underline = true; + $skipToClose(); + $processChildren(tagId, text_format, text_data, options); + text_format.underline = saved; + continue; + } + + case $TAG_DIV: + case $TAG_P: + case $TAG_FONT: + case $TAG_SPAN: { + const sFont: string | null = text_format.font; + const sSize: number | null = text_format.size; + const sColor: number | null = text_format.color; + const sBold: boolean | null = text_format.bold; + const sItalic: boolean | null = text_format.italic; + const sUnderline: boolean | null = text_format.underline; + const sAlign: ITextFormatAlign | null = text_format.align; + const sLeftMargin: number | null = text_format.leftMargin; + const sRightMargin: number | null = text_format.rightMargin; + const sLeading: number | null = text_format.leading; + const sLetterSpacing: number | null = text_format.letterSpacing; + + $applyAttributesInline(text_format, options); + $finishOpenTag(); + + if ((tagId === $TAG_P || tagId === $TAG_DIV) && options.multiline) { + textParserCreateNewLineUseCase(text_data, text_format); } - execute(node as Document, textFormat, text_data, options); + $processChildren(tagId, text_format, text_data, options); + + text_format.font = sFont; + text_format.size = sSize; + text_format.color = sColor; + text_format.bold = sBold; + text_format.italic = sItalic; + text_format.underline = sUnderline; + text_format.align = sAlign; + text_format.leftMargin = sLeftMargin; + text_format.rightMargin = sRightMargin; + text_format.leading = sLeading; + text_format.letterSpacing = sLetterSpacing; + continue; + } + + case $TAG_BR: + $skipToClose(); + if (options.multiline) { + textParserCreateNewLineUseCase(text_data, text_format); + } + continue; + default: + $skipToClose(); continue; - case "U": // underline - textFormat.underline = true; + } + } +}; + +/** + * @description '>' までスキップ(V8 SIMD最適化のindexOf使用) + * Skip to '>' using SIMD-optimized indexOf + */ +const $skipToClose = (): void => +{ + const idx: number = _$html.indexOf(">", _$pos); + _$pos = idx === -1 ? _$len : idx + 1; +}; + +/** + * @description 属性パース後のタグ末尾処理 + * Handle end of opening tag after attribute parsing + */ +const $finishOpenTag = (): void => +{ + if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x2F) { + _$pos++; + } + if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x3E) { + _$pos++; + } +}; + +/** + * @description 属性をインラインで解析・直接適用 + * IAttributeObject配列・オブジェクト・属性名文字列を一切生成しない + * Parse and apply attributes inline — zero arrays, objects, name strings + */ +const $applyAttributesInline = ( + text_format: TextFormat, + options: IOptions +): void => { + + for (;;) { + + while (_$pos < _$len) { + const c: number = _$html.charCodeAt(_$pos); + if (c !== 0x20 && c !== 0x09) { break; + } + _$pos++; + } + + if (_$pos >= _$len) { + break; + } - case "B": // bold - textFormat.bold = true; + const ch: number = _$html.charCodeAt(_$pos); + if (ch === 0x3E || ch === 0x2F) { + break; + } + + const nameStart: number = _$pos; + while (_$pos < _$len) { + const c: number = _$html.charCodeAt(_$pos); + if (c === 0x3D || c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) { break; + } + _$pos++; + } + + if (_$pos === nameStart) { + break; + } + + const name_len: number = _$pos - nameStart; + const name_c0: number = _$html.charCodeAt(nameStart); - case "I": // italic - textFormat.italic = true; + while (_$pos < _$len) { + const c: number = _$html.charCodeAt(_$pos); + if (c !== 0x20 && c !== 0x09) { break; + } + _$pos++; + } + + if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x3D) { + _$pos++; + + while (_$pos < _$len) { + const c: number = _$html.charCodeAt(_$pos); + if (c !== 0x20 && c !== 0x09) { + break; + } + _$pos++; + } + + let value: string; + const quote: number = _$html.charCodeAt(_$pos); + + if (quote === 0x22 || quote === 0x27) { + _$pos++; + const closeIdx: number = _$html.indexOf( + quote === 0x22 ? "\"" : "'", _$pos + ); + if (closeIdx === -1) { + value = _$html.substring(_$pos); + _$pos = _$len; + } else { + value = _$html.substring(_$pos, closeIdx); + _$pos = closeIdx + 1; + } + } else { + const valStart: number = _$pos; + while (_$pos < _$len) { + const c: number = _$html.charCodeAt(_$pos); + if (c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) { + break; + } + _$pos++; + } + value = _$html.substring(valStart, _$pos); + } + + $applyAttribute(name_len, name_c0, value, text_format, options); + } else { + $applyBooleanAttribute(name_len, name_c0, text_format); + } + } +}; + +/** + * @description 属性名をcharCodeで識別して値を直接適用 + * Identify attribute name by charCode and apply value directly + * + * name lengths: face(4), size(4), bold(4), style(5), align(5), color(5), + * italic(6), leading(7), underline(9), leftMargin(10), + * rightMargin(11), letterSpacing(14) + */ +const $applyAttribute = ( + name_len: number, + name_c0: number, + value: string, + text_format: TextFormat, + options: IOptions +): void => { + + switch (name_len) { + + case 4: + // face(0x66/0x46) | size(0x73/0x53) | bold(0x62/0x42) + if (name_c0 === 0x66 || name_c0 === 0x46) { + text_format.font = value; + } else if (name_c0 === 0x73 || name_c0 === 0x53) { + text_format.size = +value; + if (options.subFontSize) { + text_format.size -= options.subFontSize; + if (1 > text_format.size) { + text_format.size = 1; + } + } + } else if (name_c0 === 0x62 || name_c0 === 0x42) { + text_format.bold = true; + } + break; + + case 5: + // color(0x63/0x43) | style(0x73/0x53) | align(0x61/0x41) + if (name_c0 === 0x63 || name_c0 === 0x43) { + text_format.color = $toColorInt(value); + } else if (name_c0 === 0x73 || name_c0 === 0x53) { + $applyStyleAttributes( + textParserParseStyleService(value), + text_format, options + ); + } else if (name_c0 === 0x61 || name_c0 === 0x41) { + text_format.align = value as ITextFormatAlign; + } + break; + + case 6: + // italic(0x69/0x49) + if (name_c0 === 0x69 || name_c0 === 0x49) { + text_format.italic = true; + } + break; + + case 7: + // leading(0x6C/0x4C) + if (name_c0 === 0x6C || name_c0 === 0x4C) { + text_format.leading = parseInt(value); + } + break; + + case 9: + // underline(0x75/0x55) + if (name_c0 === 0x75 || name_c0 === 0x55) { + text_format.underline = true; + } + break; - case "FONT": // FONT tag - case "SPAN": // SPAN tag - textParserSetAttributesUseCase((node as Element).attributes, textFormat, options); + case 10: + // leftMargin(0x6C/0x4C) + if (name_c0 === 0x6C || name_c0 === 0x4C) { + text_format.leftMargin = parseInt(value); + } + break; + + case 11: + // rightMargin(0x72/0x52) + if (name_c0 === 0x72 || name_c0 === 0x52) { + text_format.rightMargin = parseInt(value); + } + break; + + case 14: + // letterSpacing(0x6C/0x4C) + if (name_c0 === 0x6C || name_c0 === 0x4C) { + text_format.letterSpacing = parseInt(value); + } + break; + + default: + break; + + } +}; + +/** + * @description 値なし属性をcharCodeで識別して直接適用 + * Apply boolean (no-value) attributes by charCode + */ +const $applyBooleanAttribute = ( + name_len: number, + name_c0: number, + text_format: TextFormat +): void => { + if (name_len === 4 && (name_c0 === 0x62 || name_c0 === 0x42)) { + text_format.bold = true; + } else if (name_len === 6 && (name_c0 === 0x69 || name_c0 === 0x49)) { + text_format.italic = true; + } else if (name_len === 9 && (name_c0 === 0x75 || name_c0 === 0x55)) { + text_format.underline = true; + } +}; + +/** + * @description style属性の解析結果をtext_formatに直接適用 + * Apply parsed style attributes directly to text_format + */ +const $applyStyleAttributes = ( + attributes: IAttributeObject[], + text_format: TextFormat, + options: IOptions +): void => { + for (let idx: number = 0; idx < attributes.length; ++idx) { + const attr: IAttributeObject = attributes[idx]; + switch (attr.name) { + + case "face": + text_format.font = attr.value as string; break; - case "BR": - if (!options.multiline) { - continue; + case "size": + text_format.size = +attr.value; + if (options.subFontSize) { + text_format.size -= options.subFontSize; + if (1 > text_format.size) { + text_format.size = 1; + } } - textParserCreateNewLineUseCase(text_data, textFormat); + break; + + case "color": + text_format.color = $toColorInt(attr.value); + break; + + case "align": + text_format.align = attr.value as ITextFormatAlign; + break; + + case "letterSpacing": + text_format.letterSpacing = parseInt(attr.value as string); + break; + + case "leading": + text_format.leading = parseInt(attr.value as string); + break; + + case "leftMargin": + text_format.leftMargin = parseInt(attr.value as string); + break; + + case "rightMargin": + text_format.rightMargin = parseInt(attr.value as string); + break; + + case "underline": + text_format.underline = true; + break; + + case "bold": + text_format.bold = true; + break; + + case "italic": + text_format.italic = true; break; default: break; } + } +}; + +/** + * @description 文字列未生成でタグを数値IDに変換 + * Zero-allocation tag identification via character comparison + */ +const $identifyTag = (start: number, end: number): number => +{ + while (start < end) { + const c: number = _$html.charCodeAt(start); + if (c !== 0x20 && c !== 0x09) { + break; + } + start++; + } + while (end > start) { + const c: number = _$html.charCodeAt(end - 1); + if (c !== 0x20 && c !== 0x09) { + break; + } + end--; + } + + const length: number = end - start; + + if (length === 1) { + let c: number = _$html.charCodeAt(start); + if (c >= 0x61) { c -= 0x20 } + if (c === 0x42) { return $TAG_B } // B + if (c === 0x49) { return $TAG_I } // I + if (c === 0x55) { return $TAG_U } // U + if (c === 0x50) { return $TAG_P } // P + return $TAG_NONE; + } + + if (length === 2) { + let c0: number = _$html.charCodeAt(start); + let c1: number = _$html.charCodeAt(start + 1); + if (c0 >= 0x61) { c0 -= 0x20 } + if (c1 >= 0x61) { c1 -= 0x20 } + if (c0 === 0x42 && c1 === 0x52) { return $TAG_BR } // BR + return $TAG_NONE; + } + + if (length === 3) { + let c0: number = _$html.charCodeAt(start); + if (c0 >= 0x61) { c0 -= 0x20 } + if (c0 === 0x44) { // D + let c1: number = _$html.charCodeAt(start + 1); + let c2: number = _$html.charCodeAt(start + 2); + if (c1 >= 0x61) { c1 -= 0x20 } + if (c2 >= 0x61) { c2 -= 0x20 } + if (c1 === 0x49 && c2 === 0x56) { return $TAG_DIV } // DIV + } + return $TAG_NONE; + } - execute(node as Document, textFormat, text_data, options); + if (length === 4) { + let c0: number = _$html.charCodeAt(start); + if (c0 >= 0x61) { c0 -= 0x20 } + if (c0 === 0x46) { // F + let c1: number = _$html.charCodeAt(start + 1); + let c2: number = _$html.charCodeAt(start + 2); + let c3: number = _$html.charCodeAt(start + 3); + if (c1 >= 0x61) { c1 -= 0x20 } + if (c2 >= 0x61) { c2 -= 0x20 } + if (c3 >= 0x61) { c3 -= 0x20 } + if (c1 === 0x4F && c2 === 0x4E && c3 === 0x54) { return $TAG_FONT } // FONT + } + if (c0 === 0x53) { // S + let c1: number = _$html.charCodeAt(start + 1); + let c2: number = _$html.charCodeAt(start + 2); + let c3: number = _$html.charCodeAt(start + 3); + if (c1 >= 0x61) { c1 -= 0x20 } + if (c2 >= 0x61) { c2 -= 0x20 } + if (c3 >= 0x61) { c3 -= 0x20 } + if (c1 === 0x50 && c2 === 0x41 && c3 === 0x4E) { return $TAG_SPAN } // SPAN + } + return $TAG_NONE; } + + return $TAG_NONE; }; \ No newline at end of file diff --git a/packages/text/src/interface/IHtmlNode.ts b/packages/text/src/interface/IHtmlNode.ts new file mode 100644 index 00000000..a136e1d6 --- /dev/null +++ b/packages/text/src/interface/IHtmlNode.ts @@ -0,0 +1,27 @@ +import type { IAttributeObject } from "./IAttributeObject"; + +/** + * @description テキストノード + * Text node + */ +export interface IHtmlTextNode { + readonly type: "text"; + readonly value: string; +} + +/** + * @description 要素ノード + * Element node + */ +export interface IHtmlElementNode { + readonly type: "element"; + readonly tagName: string; + readonly attributes: IAttributeObject[]; + readonly children: IHtmlNode[]; +} + +/** + * @description HTMLノード共用型 + * HTML node union type + */ +export type IHtmlNode = IHtmlTextNode | IHtmlElementNode; From a19809cf5d7236cf0d7a49430b992adcc4fa9480 Mon Sep 17 00:00:00 2001 From: ienaga Date: Thu, 26 Mar 2026 23:26:39 +0900 Subject: [PATCH 12/26] refactor(webgpu): add JSDoc and convert camelCase params to snake_case in Context.ts - Add bilingual (Japanese + English) @description to all class methods - Add @param and @return tags to all methods with parameters - Add JSDoc comments to all class properties - Add English translations to module-level constant JSDoc - Convert camelCase method parameters to snake_case: - viewportWidth/viewportHeight -> viewport_width/viewport_height - vertexBuffer -> vertex_buffer, vertexCount -> vertex_count - bindGroup -> bind_group, uniformOffset -> uniform_offset - useStencilPipeline -> use_stencil_pipeline - flipY -> flip_y - Internal method body variables left unchanged per convention Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/webgpu/src/Context.ts | 579 ++++++++++++++++++++++++++++----- 1 file changed, 493 insertions(+), 86 deletions(-) diff --git a/packages/webgpu/src/Context.ts b/packages/webgpu/src/Context.ts index 3efdded1..68693292 100644 --- a/packages/webgpu/src/Context.ts +++ b/packages/webgpu/src/Context.ts @@ -75,16 +75,19 @@ import { execute as contextContainerEndLayerUseCase } from "./Context/usecase/Co /** * @description スワップチェーン転送用のIdentity UV定数: scale=(1,1), offset=(0,0) + * Identity UV constant for swap-chain transfer: scale=(1,1), offset=(0,0) */ const $IDENTITY_UV = new Float32Array([1.0, 1.0, 0.0, 0.0]); /** * @description save()/restore()用の Float32Array プール + * Float32Array pool for save()/restore() operations */ const $matrixPool: Float32Array[] = []; /** * @description leaveMask() 用フルスクリーンメッシュ定数 + * Full-screen mesh constant for leaveMask() */ const $FULLSCREEN_MESH = new Float32Array([ // Triangle 1: (0,0), (1,0), (0,1) @@ -99,6 +102,7 @@ const $FULLSCREEN_MESH = new Float32Array([ /** * @description clearNodeArea() 用クワッド頂点定数 + * Quad vertex constant for clearNodeArea() */ const $QUAD_VERTICES = new Float32Array([ 0, 0, // 左上 @@ -111,11 +115,13 @@ const $QUAD_VERTICES = new Float32Array([ /** * @description containerDrawCachedFilter() 用 CT uniform プリアロケート + * Pre-allocated CT uniform for containerDrawCachedFilter() */ const $ctUniform8 = new Float32Array(8); /** * @description fill() 用 uniform プリアロケート (color + matrix = 16 floats = 64 bytes) + * Pre-allocated uniform for fill() (color + matrix = 16 floats = 64 bytes) */ const $fillUniform16 = new Float32Array(16); @@ -186,96 +192,131 @@ const $msaaDescriptor: GPURenderPassDescriptor = { */ export class Context { + /** @description 変換行列スタック / Transform matrix stack */ public readonly $stack: Float32Array[]; + /** @description 現在の2D変換行列 / Current 2D transform matrix */ public readonly $matrix: Float32Array; + /** @description 背景クリア色R / Background clear color R */ public $clearColorR: number; + /** @description 背景クリア色G / Background clear color G */ public $clearColorG: number; + /** @description 背景クリア色B / Background clear color B */ public $clearColorB: number; + /** @description 背景クリア色A / Background clear color A */ public $clearColorA: number; + /** @description メインアタッチメントオブジェクト / Main attachment object */ public $mainAttachmentObject: IAttachmentObject | null; + /** @description アタッチメントオブジェクトのスタック / Attachment object stack */ public readonly $stackAttachmentObject: IAttachmentObject[]; + /** @description グローバルアルファ値 / Global alpha value */ public globalAlpha: number; + /** @description グローバル合成操作 / Global composite operation */ public globalCompositeOperation: IBlendMode; + /** @description 画像スムージングの有効/無効 / Whether image smoothing is enabled */ public imageSmoothingEnabled: boolean; + /** @description 塗りつぶしスタイル / Fill style color */ public $fillStyle: Float32Array; + /** @description 線スタイル / Stroke style color */ public $strokeStyle: Float32Array; + /** @description マスク描画範囲 / Mask drawing bounds */ public readonly maskBounds: IBounds; + /** @description 線の太さ / Line thickness */ public thickness: number; + /** @description 線端の形状 / Line cap style */ public caps: number; + /** @description 線の結合スタイル / Line joint style */ public joints: number; + /** @description マイターリミット / Miter limit */ public miterLimit: number; + /** @description GPUデバイス / GPU device instance */ private device: GPUDevice; + /** @description GPUキャンバスコンテキスト / GPU canvas context */ private canvasContext: GPUCanvasContext; + /** @description 優先テクスチャフォーマット / Preferred texture format */ private preferredFormat: GPUTextureFormat; + /** @description コマンドエンコーダー / Command encoder */ private commandEncoder: GPUCommandEncoder | null = null; + /** @description レンダーパスエンコーダー / Render pass encoder */ private renderPassEncoder: GPURenderPassEncoder | null = null; - // Main canvas texture (for final display) - acquired once per frame + /** @description メインキャンバステクスチャ(最終表示用、フレームごとに1回取得) / Main canvas texture (for final display, acquired once per frame) */ private mainTexture: GPUTexture | null = null; + /** @description メインキャンバステクスチャビュー / Main canvas texture view */ private mainTextureView: GPUTextureView | null = null; + /** @description フレーム開始済みフラグ / Whether the frame has been started */ private frameStarted: boolean = false; - // フレームごとの一時テクスチャ(endFrame()でdestroy) + /** @description フレームごとの一時テクスチャ(endFrame()でdestroy) / Per-frame temporary textures (destroyed in endFrame()) */ private frameTextures: GPUTexture[] = []; - // フレームごとのプール管理テクスチャ(endFrame()でプールに返却) + /** @description フレームごとのプール管理テクスチャ(endFrame()でプールに返却) / Per-frame pooled textures (returned to pool in endFrame()) */ private pooledTextures: GPUTexture[] = []; - // フレームごとのレンダーテクスチャプール管理(endFrame()でプールに返却) + /** @description フレームごとのレンダーテクスチャプール管理(endFrame()でプールに返却) / Per-frame render texture pool (returned to pool in endFrame()) */ private pooledRenderTextures: GPUTexture[] = []; - // Current rendering target (could be main or atlas) + /** @description 現在のレンダーターゲット(メインまたはアトラス) / Current render target (could be main or atlas) */ private currentRenderTarget: GPUTextureView | null = null; - // Current viewport size (WebGL版と同じ: アトラス描画時はアトラスサイズを使用) + /** @description 現在のビューポート幅(アトラス描画時はアトラスサイズ) / Current viewport width (atlas size during atlas rendering) */ private viewportWidth: number = 0; + /** @description 現在のビューポート高さ(アトラス描画時はアトラスサイズ) / Current viewport height (atlas size during atlas rendering) */ private viewportHeight: number = 0; + /** @description パスコマンド / Path command handler */ private pathCommand: PathCommand; + /** @description バッファマネージャー / Buffer manager */ private bufferManager: BufferManager; + /** @description テクスチャマネージャー / Texture manager */ private textureManager: TextureManager; + /** @description フレームバッファマネージャー / Frame buffer manager */ private frameBufferManager: FrameBufferManager; + /** @description パイプラインマネージャー / Pipeline manager */ private pipelineManager: PipelineManager; + /** @description アタッチメントマネージャー / Attachment manager */ private attachmentManager: AttachmentManager; + /** @description 新しい描画状態フラグ / New draw state flag */ public newDrawState: boolean = false; - // コンテナレイヤースタック(フィルター/ブレンド用) + /** @description コンテナレイヤースタック(フィルター/ブレンド用) / Container layer stack (for filter/blend) */ private readonly $containerLayerStack: IAttachmentObject[] = []; + /** @description コンテナレイヤーのコンテンツサイズ / Container layer content sizes */ private containerLayerContentSizes: { width: number; height: number }[] = []; - // マスク描画モードフラグ(beginMask〜endMask間でtrue) + /** @description マスク描画モードフラグ(beginMask〜endMask間でtrue) / Mask drawing mode flag (true between beginMask and endMask) */ private inMaskMode: boolean = false; - // ノード領域クリア済みフラグ(beginNodeRendering〜endNodeRendering間で使用) + /** @description ノード領域クリア済みフラグ(beginNodeRendering〜endNodeRendering間で使用) / Node area cleared flag (used between beginNodeRendering and endNodeRendering) */ private nodeAreaCleared: boolean = false; - // 現在のノードのシザー範囲(クリア後に戻すため) + /** @description 現在のノードのシザー範囲(クリア後に戻すため) / Current node scissor rect (to restore after clearing) */ private currentNodeScissor: { x: number; y: number; w: number; h: number } | null = null; - // アトラスレンダーパス統合: 同一アトラスへの連続描画でパスを再利用 + /** @description アトラスレンダーパス統合: 同一アトラスへの連続描画でパスを再利用 / Atlas render pass integration: reuse pass for consecutive draws to the same atlas */ private nodeRenderPassAtlasIndex: number = -1; - // Dynamic Uniform BindGroup(fill/stencilパイプライン共有、フレームごとに1回作成) + /** @description Dynamic Uniform BindGroup(fill/stencilパイプライン共有、フレームごとに1回作成) / Dynamic Uniform BindGroup (shared by fill/stencil pipelines, created once per frame) */ private fillDynamicBindGroup: GPUBindGroup | null = null; + /** @description Dynamic Uniform BindGroupのバッファ / Dynamic Uniform BindGroup buffer */ private fillDynamicBindGroupBuffer: GPUBuffer | null = null; - // clearNodeArea() 用頂点バッファキャッシュ + /** @description clearNodeArea() 用頂点バッファキャッシュ / Vertex buffer cache for clearNodeArea() */ private nodeClearQuadBuffer: GPUBuffer | null = null; - // Storage Buffer + Indirect Drawing を使用するかどうか + /** @description Storage Buffer + Indirect Drawing を使用するかどうか / Whether to use Storage Buffer + Indirect Drawing */ private useOptimizedInstancing: boolean = true; - // リサイズ後にcanvasContextの再設定が必要かどうか - // ($resizeComplete()でcanvas.width/heightが設定された後、ensureMainTexture()でconfigure()を呼ぶ) + /** @description リサイズ後にcanvasContextの再設定が必要かどうか / Whether canvasContext reconfiguration is needed after resize */ private $needsReconfigure: boolean = false; - // Hot Path 用の事前割り当てバッファ + /** @description Hot Path 用の事前割り当てバッファ / Pre-allocated buffer for hot path */ private readonly $uniformData8 = new Float32Array(8); + /** @description Hot Path 用の事前割り当てシザーレクト / Pre-allocated scissor rect for hot path */ private readonly $scissorRect: { "x": number; "y": number; "w": number; "h": number } = { "x": 0, "y": 0, "w": 0, "h": 0 }; - // フィルター/コンテナレイヤー用のプリアロケートされた設定オブジェクト + /** @description フィルター/コンテナレイヤー用のプリアロケートされた設定オブジェクト / Pre-allocated config object for filter/container layers */ private readonly $filterConfig: { device: GPUDevice; commandEncoder: GPUCommandEncoder; @@ -287,6 +328,15 @@ export class Context frameTextures: GPUTexture[]; }; + /** + * @description WebGPUコンテキストを初期化する + * Initialize the WebGPU context + * + * @param {GPUDevice} device - GPUデバイス / GPU device instance + * @param {GPUCanvasContext} canvas_context - GPUキャンバスコンテキスト / GPU canvas context + * @param {GPUTextureFormat} preferred_format - 優先テクスチャフォーマット / Preferred texture format + * @param {number} device_pixel_ratio - デバイスピクセル比 / Device pixel ratio + */ constructor ( device: GPUDevice, canvas_context: GPUCanvasContext, @@ -388,6 +438,9 @@ export class Context /** * @description 転送範囲をリセット(フレーム開始) + * Reset transfer bounds (frame start) + * + * @return {void} */ clearTransferBounds (): void { @@ -398,6 +451,13 @@ export class Context /** * @description 背景色を更新 + * Update the background color + * + * @param {number} red - 赤色成分 / Red component + * @param {number} green - 緑色成分 / Green component + * @param {number} blue - 青色成分 / Blue component + * @param {number} alpha - アルファ成分 / Alpha component + * @return {void} */ updateBackgroundColor (red: number, green: number, blue: number, alpha: number): void { @@ -409,6 +469,9 @@ export class Context /** * @description 背景色で塗りつぶす(メインアタッチメント) + * Fill with background color (main attachment) + * + * @return {void} */ fillBackgroundColor (): void { @@ -464,6 +527,12 @@ export class Context /** * @description メインcanvasのサイズを変更 + * Resize the main canvas + * + * @param {number} width - 新しい幅 / New width + * @param {number} height - 新しい高さ / New height + * @param {boolean} cache_clear - キャッシュをクリアするか / Whether to clear cache + * @return {void} */ resize (width: number, height: number, cache_clear: boolean = true): void { @@ -571,6 +640,13 @@ export class Context /** * @description 指定範囲をクリアする + * Clear the specified rectangle area + * + * @param {number} _x - X座標 / X coordinate + * @param {number} _y - Y座標 / Y coordinate + * @param {number} _w - 幅 / Width + * @param {number} _h - 高さ / Height + * @return {void} */ clearRect (_x: number, _y: number, _w: number, _h: number): void { @@ -581,6 +657,9 @@ export class Context /** * @description 現在の2D変換行列を保存 + * Save the current 2D transform matrix + * + * @return {void} */ save (): void { @@ -591,6 +670,9 @@ export class Context /** * @description 2D変換行列を復元 + * Restore the 2D transform matrix + * + * @return {void} */ restore (): void { @@ -603,6 +685,15 @@ export class Context /** * @description 2D変換行列を設定 + * Set the 2D transform matrix + * + * @param {number} a - 水平スケール / Horizontal scale + * @param {number} b - 垂直スキュー / Vertical skew + * @param {number} c - 水平スキュー / Horizontal skew + * @param {number} d - 垂直スケール / Vertical scale + * @param {number} e - 水平移動 / Horizontal translation + * @param {number} f - 垂直移動 / Vertical translation + * @return {void} */ setTransform ( a: number, b: number, c: number, @@ -618,6 +709,15 @@ export class Context /** * @description 現在の2D変換行列に対して乗算を行います + * Multiply the current 2D transform matrix + * + * @param {number} a - 水平スケール / Horizontal scale + * @param {number} b - 垂直スキュー / Vertical skew + * @param {number} c - 水平スキュー / Horizontal skew + * @param {number} d - 垂直スケール / Vertical scale + * @param {number} e - 水平移動 / Horizontal translation + * @param {number} f - 垂直移動 / Vertical translation + * @return {void} */ transform ( a: number, b: number, c: number, @@ -636,6 +736,9 @@ export class Context /** * @description コンテキストの値を初期化する + * Reset all context values to their initial state + * + * @return {void} */ reset (): void { @@ -649,6 +752,9 @@ export class Context /** * @description パスを開始 + * Begin a new path + * + * @return {void} */ beginPath (): void { @@ -657,6 +763,11 @@ export class Context /** * @description パスを移動 + * Move the path to the specified point + * + * @param {number} x - X座標 / X coordinate + * @param {number} y - Y座標 / Y coordinate + * @return {void} */ moveTo (x: number, y: number): void { @@ -665,6 +776,11 @@ export class Context /** * @description パスを線で結ぶ + * Draw a line to the specified point + * + * @param {number} x - X座標 / X coordinate + * @param {number} y - Y座標 / Y coordinate + * @return {void} */ lineTo (x: number, y: number): void { @@ -673,6 +789,13 @@ export class Context /** * @description 二次ベジェ曲線を描画 + * Draw a quadratic Bézier curve + * + * @param {number} cx - 制御点X / Control point X + * @param {number} cy - 制御点Y / Control point Y + * @param {number} x - 終点X / End point X + * @param {number} y - 終点Y / End point Y + * @return {void} */ quadraticCurveTo (cx: number, cy: number, x: number, y: number): void { @@ -681,6 +804,13 @@ export class Context /** * @description 塗りつぶしスタイルを設定 + * Set the fill style color + * + * @param {number} red - 赤色成分 / Red component + * @param {number} green - 緑色成分 / Green component + * @param {number} blue - 青色成分 / Blue component + * @param {number} alpha - アルファ成分 / Alpha component + * @return {void} */ fillStyle (red: number, green: number, blue: number, alpha: number): void { @@ -692,6 +822,13 @@ export class Context /** * @description 線のスタイルを設定 + * Set the stroke style color + * + * @param {number} red - 赤色成分 / Red component + * @param {number} green - 緑色成分 / Green component + * @param {number} blue - 青色成分 / Blue component + * @param {number} alpha - アルファ成分 / Alpha component + * @return {void} */ strokeStyle (red: number, green: number, blue: number, alpha: number): void { @@ -703,6 +840,9 @@ export class Context /** * @description パスを閉じる + * Close the current path + * + * @return {void} */ closePath (): void { @@ -711,6 +851,12 @@ export class Context /** * @description 円弧を描画 + * Draw an arc + * + * @param {number} x - 中心X / Center X + * @param {number} y - 中心Y / Center Y + * @param {number} radius - 半径 / Radius + * @return {void} */ arc (x: number, y: number, radius: number): void { @@ -719,6 +865,15 @@ export class Context /** * @description 3次ベジェ曲線を描画 + * Draw a cubic Bézier curve + * + * @param {number} cx1 - 第1制御点X / First control point X + * @param {number} cy1 - 第1制御点Y / First control point Y + * @param {number} cx2 - 第2制御点X / Second control point X + * @param {number} cy2 - 第2制御点Y / Second control point Y + * @param {number} x - 終点X / End point X + * @param {number} y - 終点Y / End point Y + * @return {void} */ bezierCurveTo (cx1: number, cy1: number, cx2: number, cy2: number, x: number, y: number): void { @@ -727,7 +882,10 @@ export class Context /** * @description 描画メソッド共通: レンダーパスの確保とノード領域クリア + * Common drawing method: ensure render pass and clear node area * fill(), stroke(), gradientFill(), bitmapFill(), gradientStroke(), bitmapStroke() で使用 + * + * @return {void} */ private ensureFillRenderPass (): void { @@ -849,6 +1007,9 @@ export class Context /** * @description 塗りつぶしを実行(Loop-Blinn方式対応) + * Execute fill operation (with Loop-Blinn support) + * + * @return {void} */ fill (): void { @@ -896,6 +1057,9 @@ export class Context /** * @description Dynamic Uniform BindGroupを取得(フレーム内で初回呼び出し時に作成) + * Get or create the Dynamic Uniform BindGroup (created on first call within a frame) + * + * @return {GPUBindGroup} Dynamic Uniform BindGroup */ private getOrCreateFillDynamicBindGroup(): GPUBindGroup { @@ -922,14 +1086,28 @@ export class Context /** * @description fill/stroke用のcolor/matrix uniformを書き込む - * FillUniforms構造体: color(vec4) + matrix0(vec4) + matrix1(vec4) + matrix2(vec4) = 64 bytes - * @return Dynamic Uniform Buffer内のアライメント済みオフセット + * Write color/matrix uniform for fill/stroke operations + * FillUniforms構造体: color(vec4) + matrix0(vec4) + matrix1(vec4) + matrix2(vec4) = 64 bytes + * + * @param {number} red - 赤色成分 / Red component + * @param {number} green - 緑色成分 / Green component + * @param {number} blue - 青色成分 / Blue component + * @param {number} alpha - アルファ成分 / Alpha component + * @param {number} a - 変換行列a / Transform matrix a + * @param {number} b - 変換行列b / Transform matrix b + * @param {number} c - 変換行列c / Transform matrix c + * @param {number} d - 変換行列d / Transform matrix d + * @param {number} tx - 変換行列tx / Transform matrix tx + * @param {number} ty - 変換行列ty / Transform matrix ty + * @param {number} viewport_width - ビューポート幅 / Viewport width + * @param {number} viewport_height - ビューポート高さ / Viewport height + * @return {number} Dynamic Uniform Buffer内のアライメント済みオフセット / Aligned offset in the Dynamic Uniform Buffer */ private writeFillUniform( red: number, green: number, blue: number, alpha: number, a: number, b: number, c: number, d: number, tx: number, ty: number, - viewportWidth: number, viewportHeight: number + viewport_width: number, viewport_height: number ): number { // color @@ -938,18 +1116,18 @@ export class Context $fillUniform16[2] = blue; $fillUniform16[3] = alpha; // matrix0 (a, b, 0, pad) — ビューポート正規化 - $fillUniform16[4] = a / viewportWidth; - $fillUniform16[5] = b / viewportHeight; + $fillUniform16[4] = a / viewport_width; + $fillUniform16[5] = b / viewport_height; $fillUniform16[6] = 0; $fillUniform16[7] = 0; // matrix1 (c, d, 0, pad) - $fillUniform16[8] = c / viewportWidth; - $fillUniform16[9] = d / viewportHeight; + $fillUniform16[8] = c / viewport_width; + $fillUniform16[9] = d / viewport_height; $fillUniform16[10] = 0; $fillUniform16[11] = 0; // matrix2 (tx, ty, 1, pad) - $fillUniform16[12] = tx / viewportWidth; - $fillUniform16[13] = ty / viewportHeight; + $fillUniform16[12] = tx / viewport_width; + $fillUniform16[13] = ty / viewport_height; $fillUniform16[14] = 1; $fillUniform16[15] = 0; @@ -958,53 +1136,75 @@ export class Context /** * @description 2パスステンシルフィル(アトラス用) + * Two-pass stencil fill (for atlas) + * + * @param {GPUBuffer} vertex_buffer - 頂点バッファ / Vertex buffer + * @param {number} vertex_count - 頂点数 / Vertex count + * @param {GPUBindGroup} bind_group - バインドグループ / Bind group + * @param {number} uniform_offset - ユニフォームオフセット / Uniform offset + * @return {void} */ private fillWithStencil( - vertexBuffer: GPUBuffer, - vertexCount: number, - bindGroup: GPUBindGroup, - uniformOffset: number + vertex_buffer: GPUBuffer, + vertex_count: number, + bind_group: GPUBindGroup, + uniform_offset: number ): void { contextFillWithStencilService( this.renderPassEncoder!, this.pipelineManager, - vertexBuffer, - vertexCount, - bindGroup, - uniformOffset + vertex_buffer, + vertex_count, + bind_group, + uniform_offset ); } /** * @description 2パスステンシルフィル(メインキャンバス用) + * Two-pass stencil fill (for main canvas) + * + * @param {GPUBuffer} vertex_buffer - 頂点バッファ / Vertex buffer + * @param {number} vertex_count - 頂点数 / Vertex count + * @param {GPUBindGroup} bind_group - バインドグループ / Bind group + * @param {number} uniform_offset - ユニフォームオフセット / Uniform offset + * @return {void} */ private fillWithStencilMain( - vertexBuffer: GPUBuffer, - vertexCount: number, - bindGroup: GPUBindGroup, - uniformOffset: number + vertex_buffer: GPUBuffer, + vertex_count: number, + bind_group: GPUBindGroup, + uniform_offset: number ): void { contextFillWithStencilMainService( this.renderPassEncoder!, this.pipelineManager, - vertexBuffer, - vertexCount, - bindGroup, - uniformOffset + vertex_buffer, + vertex_count, + bind_group, + uniform_offset ); } /** * @description 単純なフィル(ステンシルなし、キャンバス描画用) + * Simple fill (no stencil, for canvas rendering) + * + * @param {GPUBuffer} vertex_buffer - 頂点バッファ / Vertex buffer + * @param {number} vertex_count - 頂点数 / Vertex count + * @param {boolean} use_stencil_pipeline - ステンシルパイプラインを使用するか / Whether to use stencil pipeline + * @param {GPUBindGroup} bind_group - バインドグループ / Bind group + * @param {number} uniform_offset - ユニフォームオフセット / Uniform offset + * @return {void} */ private fillSimple( - vertexBuffer: GPUBuffer, - vertexCount: number, - useStencilPipeline: boolean, - bindGroup: GPUBindGroup, - uniformOffset: number + vertex_buffer: GPUBuffer, + vertex_count: number, + use_stencil_pipeline: boolean, + bind_group: GPUBindGroup, + uniform_offset: number ): void { const clipLevel = this.$mainAttachmentObject?.clipLevel ?? 1; @@ -1012,19 +1212,23 @@ export class Context contextFillSimpleService( this.renderPassEncoder!, this.pipelineManager, - vertexBuffer, - vertexCount, - bindGroup, - uniformOffset, + vertex_buffer, + vertex_count, + bind_group, + uniform_offset, !!this.currentRenderTarget, - useStencilPipeline, + use_stencil_pipeline, clipLevel ); } /** * @description オフスクリーンアタッチメントにバインド - * WebGL: FrameBufferManagerBindAttachmentObjectService + * Bind to an offscreen attachment + * WebGL: FrameBufferManagerBindAttachmentObjectService + * + * @param {IAttachmentObject} attachment - バインドするアタッチメント / Attachment to bind + * @return {void} */ bindAttachment(attachment: IAttachmentObject): void { @@ -1040,7 +1244,10 @@ export class Context /** * @description メインキャンバスにバインド - * WebGL: FrameBufferManagerUnBindAttachmentObjectService + * Bind to the main canvas + * WebGL: FrameBufferManagerUnBindAttachmentObjectService + * + * @return {void} */ unbindAttachment(): void { @@ -1050,7 +1257,13 @@ export class Context /** * @description アタッチメントオブジェクトを取得 - * WebGL: FrameBufferManagerGetAttachmentObjectUseCase + * Get an attachment object + * WebGL: FrameBufferManagerGetAttachmentObjectUseCase + * + * @param {number} width - 幅 / Width + * @param {number} height - 高さ / Height + * @param {boolean} msaa - MSAA有効化フラグ / MSAA enable flag + * @return {IAttachmentObject} アタッチメントオブジェクト / Attachment object */ getAttachmentObject(width: number, height: number, msaa: boolean = false): IAttachmentObject { @@ -1059,7 +1272,11 @@ export class Context /** * @description アタッチメントオブジェクトを解放 - * WebGL: FrameBufferManagerReleaseAttachmentObjectUseCase + * Release an attachment object + * WebGL: FrameBufferManagerReleaseAttachmentObjectUseCase + * + * @param {IAttachmentObject} attachment - 解放するアタッチメント / Attachment to release + * @return {void} */ releaseAttachment(attachment: IAttachmentObject): void { @@ -1068,7 +1285,10 @@ export class Context /** * @description 線の描画を実行(WebGL版と同じ仕様) + * Execute stroke drawing (same specification as WebGL version) * WebGL版と同様に、ストロークを塗りとして描画する + * + * @return {void} */ stroke (): void { @@ -1119,6 +1339,15 @@ export class Context /** * @description グラデーションの塗りつぶしを実行 + * Execute gradient fill + * + * @param {number} type - グラデーションタイプ / Gradient type + * @param {number[]} stops - カラーストップ配列 / Color stop array + * @param {Float32Array} matrix - グラデーション変換行列 / Gradient transform matrix + * @param {number} spread - スプレッドメソッド / Spread method + * @param {number} interpolation - 補間方法 / Interpolation method + * @param {number} focal - 焦点距離 / Focal point ratio + * @return {void} */ gradientFill ( type: number, @@ -1176,6 +1405,15 @@ export class Context /** * @description ビットマップの塗りつぶしを実行 + * Execute bitmap fill + * + * @param {Uint8Array} pixels - ピクセルデータ / Pixel data + * @param {Float32Array} matrix - ビットマップ変換行列 / Bitmap transform matrix + * @param {number} width - ビットマップ幅 / Bitmap width + * @param {number} height - ビットマップ高さ / Bitmap height + * @param {boolean} repeat - 繰り返しフラグ / Repeat flag + * @param {boolean} smooth - スムージングフラグ / Smoothing flag + * @return {void} */ bitmapFill ( pixels: Uint8Array, @@ -1236,6 +1474,15 @@ export class Context /** * @description グラデーション線の描画を実行 + * Execute gradient stroke drawing + * + * @param {number} type - グラデーションタイプ / Gradient type + * @param {number[]} stops - カラーストップ配列 / Color stop array + * @param {Float32Array} matrix - グラデーション変換行列 / Gradient transform matrix + * @param {number} spread - スプレッドメソッド / Spread method + * @param {number} interpolation - 補間方法 / Interpolation method + * @param {number} focal - 焦点距離 / Focal point ratio + * @return {void} */ gradientStroke ( type: number, @@ -1293,6 +1540,15 @@ export class Context /** * @description ビットマップ線の描画を実行 + * Execute bitmap stroke drawing + * + * @param {Uint8Array} pixels - ピクセルデータ / Pixel data + * @param {Float32Array} matrix - ビットマップ変換行列 / Bitmap transform matrix + * @param {number} width - ビットマップ幅 / Bitmap width + * @param {number} height - ビットマップ高さ / Bitmap height + * @param {boolean} repeat - 繰り返しフラグ / Repeat flag + * @param {boolean} smooth - スムージングフラグ / Smoothing flag + * @return {void} */ bitmapStroke ( pixels: Uint8Array, @@ -1350,8 +1606,11 @@ export class Context /** * @description マスク処理を実行 + * Execute mask clipping operation * WebGL版と同様にステンシルバッファを使用したクリッピング * メインアタッチメントとアトラス両方でマスク処理をサポート + * + * @return {void} */ clip (): void { @@ -1420,6 +1679,10 @@ export class Context /** * @description アタッチメントオブジェクトをバインド + * Bind an attachment object + * + * @param {IAttachmentObject} attachment_object - バインドするアタッチメント / Attachment to bind + * @return {void} */ bind (attachment_object: IAttachmentObject): void { @@ -1432,8 +1695,11 @@ export class Context /** * @description 現在のアタッチメントオブジェクトを取得 + * Get the current attachment object * アトラスがバインドされていない場合はメインアタッチメントを返す * When no atlas is bound, returns the main attachment + * + * @return {IAttachmentObject | null} 現在のアタッチメント / Current attachment */ get currentAttachmentObject (): IAttachmentObject | null { @@ -1445,6 +1711,9 @@ export class Context /** * @description アトラス専用のアタッチメントオブジェクトを取得 + * Get the atlas-specific attachment object + * + * @return {IAttachmentObject | null} アトラスアタッチメント / Atlas attachment */ get atlasAttachmentObject (): IAttachmentObject | null { @@ -1453,6 +1722,10 @@ export class Context /** * @description グリッドの描画データをセット + * Set grid drawing data + * + * @param {Float32Array | null} grid_data - グリッドデータ / Grid data + * @return {void} */ useGrid (grid_data: Float32Array | null): void { @@ -1461,7 +1734,11 @@ export class Context /** * @description 指定のノード範囲で描画を開始(アトラステクスチャへの描画) + * Begin rendering for the specified node region (drawing to atlas texture) * 2パスステンシルフィル対応: ステンシルバッファ付きレンダーパスを使用 + * + * @param {Node} node - 描画対象ノード / Target node for rendering + * @return {void} */ beginNodeRendering (node: Node): void { @@ -1553,7 +1830,10 @@ export class Context /** * @description ノード領域がまだクリアされていない場合にクリアを実行 + * Clear the node area if it has not been cleared yet * 最初の描画操作(fill, gradientFill, gradientStroke等)で呼び出される + * + * @return {void} */ private ensureNodeAreaCleared (): void { @@ -1564,7 +1844,10 @@ export class Context /** * @description ノード領域をクリア(透明色 + ステンシル=0) + * Clear the node area (transparent color + stencil=0) * WebGL版の gl.clear(COLOR_BUFFER_BIT | STENCIL_BUFFER_BIT) と同等 + * + * @return {void} */ private clearNodeArea (): void { @@ -1610,7 +1893,10 @@ export class Context /** * @description 指定のノード範囲で描画を終了 + * End rendering for the current node region * レンダーパスは終了しない(次のbeginNodeRenderingで再利用するため) + * + * @return {void} */ endNodeRendering (): void { @@ -1629,6 +1915,9 @@ export class Context /** * @description 塗りの描画を実行 + * Execute fill drawing + * + * @return {void} */ drawFill (): void { @@ -1645,6 +1934,15 @@ export class Context /** * @description インスタンスを描画 + * Draw a display object instance + * + * @param {Node} node - 描画対象ノード / Target node + * @param {number} x_min - バウンディングボックス左端 / Bounding box left + * @param {number} y_min - バウンディングボックス上端 / Bounding box top + * @param {number} x_max - バウンディングボックス右端 / Bounding box right + * @param {number} y_max - バウンディングボックス下端 / Bounding box bottom + * @param {Float32Array} color_transform - カラー変換パラメータ / Color transform parameters + * @return {void} */ drawDisplayObject ( node: Node, @@ -1676,10 +1974,11 @@ export class Context * @description インスタンス配列を描画 * Draw instanced arrays * - * useOptimizedInstancingがtrueの場合、Storage BufferとIndirect Drawingを使用。 - * - Storage Buffer: メモリアロケーション削減、CPU負荷15-25%軽減 - * - Indirect Drawing: CPU-GPUオーバーヘッド5-15%削減 + * useOptimizedInstancingがtrueの場合、Storage BufferとIndirect Drawingを使用。 + * - Storage Buffer: メモリアロケーション削減、CPU負荷15-25%軽減 + * - Indirect Drawing: CPU-GPUオーバーヘッド5-15%削減 * + * @return {void} */ drawArraysInstanced (): void { @@ -1740,6 +2039,8 @@ export class Context * @description 最適化インスタンス描画の有効/無効を設定 * Enable or disable optimized instancing * + * @param {boolean} enabled - 有効にするか / Whether to enable + * @return {void} */ setOptimizedInstancing (enabled: boolean): void { @@ -1750,6 +2051,7 @@ export class Context * @description 最適化インスタンス描画が有効かどうか * Whether optimized instancing is enabled * + * @return {boolean} 有効ならtrue / True if enabled */ isOptimizedInstancingEnabled (): boolean { @@ -1758,6 +2060,9 @@ export class Context /** * @description 複雑なブレンドモードのキューを処理 + * Process the complex blend mode queue + * + * @return {void} */ private processComplexBlendQueue (): void { @@ -1778,6 +2083,9 @@ export class Context /** * @description インスタンス配列をクリア + * Clear instanced arrays + * + * @return {void} */ clearArraysInstanced (): void { @@ -1788,8 +2096,13 @@ export class Context /** * @description ピクセルバッファをNodeの指定箇所に転送 + * Transfer pixel buffer to the specified position of the Node * WebGPUでは、Shapeのシェーダーが-ndc.yでY軸反転しているため、 * Bitmapも同じ方向になるよう画像を上下反転して書き込む + * + * @param {Node} node - 描画対象ノード / Target node + * @param {Uint8Array} pixels - ピクセルデータ / Pixel data + * @return {void} */ drawPixels (node: Node, pixels: Uint8Array): void { @@ -1840,6 +2153,14 @@ export class Context /** * @description 一時テクスチャ経由でピクセルデータをMSAAテクスチャに描画 + * Draw pixel data to MSAA texture via a temporary texture + * + * @param {IAttachmentObject} attachment - アタッチメントオブジェクト / Attachment object + * @param {Node} node - 描画対象ノード / Target node + * @param {Uint8Array} pixels - ピクセルデータ / Pixel data + * @param {number} width - 幅 / Width + * @param {number} height - 高さ / Height + * @return {void} */ private drawPixelsToMsaa ( attachment: IAttachmentObject, @@ -1928,10 +2249,16 @@ export class Context /** * @description OffscreenCanvasをNodeの指定箇所に転送 + * Transfer OffscreenCanvas to the specified position of the Node * WebGPUでは、Shapeのシェーダーが-ndc.yでY軸反転しているため、 * Bitmapも同じ方向になるよう画像を上下反転して書き込む + * + * @param {Node} node - 描画対象ノード / Target node + * @param {OffscreenCanvas | ImageBitmap} element - 描画要素 / Element to draw + * @param {boolean} flip_y - Y軸反転フラグ / Y-axis flip flag + * @return {void} */ - drawElement (node: Node, element: OffscreenCanvas | ImageBitmap, flipY: boolean = false): void + drawElement (node: Node, element: OffscreenCanvas | ImageBitmap, flip_y: boolean = false): void { // WebGPU draw element // OffscreenCanvasまたはImageBitmapをアトラステクスチャに描画 @@ -1955,14 +2282,23 @@ export class Context // MSAAが有効な場合は一時テクスチャ経由でMSAAテクスチャに直接描画 // MSAAが無効な場合もシェーダー経由で描画(WebGLと同じ処理フロー) if (attachment.msaa && attachment.msaaTexture?.view) { - this.drawElementToMsaa(attachment, node, element, width, height, flipY); + this.drawElementToMsaa(attachment, node, element, width, height, flip_y); } else { - this.drawElementToTexture(attachment, node, element, width, height, flipY); + this.drawElementToTexture(attachment, node, element, width, height, flip_y); } } /** * @description 一時テクスチャ経由でMSAAテクスチャに直接描画 + * Draw to MSAA texture directly via a temporary texture + * + * @param {IAttachmentObject} attachment - アタッチメントオブジェクト / Attachment object + * @param {Node} node - 描画対象ノード / Target node + * @param {OffscreenCanvas | ImageBitmap} element - 描画要素 / Element to draw + * @param {number} width - 幅 / Width + * @param {number} height - 高さ / Height + * @param {boolean} flip_y - Y軸反転フラグ / Y-axis flip flag + * @return {void} */ private drawElementToMsaa ( attachment: IAttachmentObject, @@ -1970,7 +2306,7 @@ export class Context element: OffscreenCanvas | ImageBitmap, width: number, height: number, - flipY: boolean + flip_y: boolean ): void { // 一時テクスチャをプールから取得 @@ -1979,7 +2315,7 @@ export class Context this.device.queue.copyExternalImageToTexture( { "source": element as ImageBitmap, - "flipY": flipY + "flipY": flip_y }, { "texture": tempTexture, @@ -2052,6 +2388,15 @@ export class Context /** * @description 一時テクスチャ経由で通常テクスチャに描画(非MSAA版) + * Draw to a regular texture via a temporary texture (non-MSAA version) + * + * @param {IAttachmentObject} attachment - アタッチメントオブジェクト / Attachment object + * @param {Node} node - 描画対象ノード / Target node + * @param {OffscreenCanvas | ImageBitmap} element - 描画要素 / Element to draw + * @param {number} width - 幅 / Width + * @param {number} height - 高さ / Height + * @param {boolean} flip_y - Y軸反転フラグ / Y-axis flip flag + * @return {void} */ private drawElementToTexture ( attachment: IAttachmentObject, @@ -2059,7 +2404,7 @@ export class Context element: OffscreenCanvas | ImageBitmap, width: number, height: number, - flipY: boolean + flip_y: boolean ): void { // 一時テクスチャをプールから取得 @@ -2070,7 +2415,7 @@ export class Context this.device.queue.copyExternalImageToTexture( { "source": element as ImageBitmap, - "flipY": flipY + "flipY": flip_y }, { "texture": tempTexture, @@ -2143,6 +2488,20 @@ export class Context /** * @description フィルターを適用 + * Apply filter effects + * + * @param {Node} node - 描画対象ノード / Target node + * @param {string} _unique_key - ユニークキー / Unique key + * @param {boolean} _updated - 更新フラグ / Updated flag + * @param {number} width - 幅 / Width + * @param {number} height - 高さ / Height + * @param {boolean} _is_bitmap - ビットマップかどうか / Whether it is a bitmap + * @param {Float32Array} matrix - 変換行列 / Transform matrix + * @param {Float32Array} color_transform - カラー変換パラメータ / Color transform parameters + * @param {IBlendMode} blend_mode - ブレンドモード / Blend mode + * @param {Float32Array} bounds - バウンディングボックス / Bounding box + * @param {Float32Array} params - フィルターパラメータ / Filter parameters + * @return {void} */ applyFilter ( node: Node, @@ -2198,6 +2557,9 @@ export class Context * @description コンテナのフィルター/ブレンド用のレイヤーを開始 * Begin a container layer for filter/blend processing * + * @param {number} width - レイヤー幅 / Layer width + * @param {number} height - レイヤー高さ / Layer height + * @return {void} */ containerBeginLayer (width: number, height: number): void { @@ -2238,14 +2600,15 @@ export class Context * @description コンテナのフィルター/ブレンド用レイヤーを終了し、結果を元のメインに合成 * End the container layer and composite the result back to the original main * - * @param {IBlendMode} blend_mode - * @param {Float32Array} matrix - * @param {Float32Array | null} color_transform - * @param {boolean} use_filter - * @param {Float32Array | null} filter_bounds - * @param {Float32Array | null} filter_params - * @param {string} unique_key - * @param {string} filter_key + * @param {IBlendMode} blend_mode - ブレンドモード / Blend mode + * @param {Float32Array} matrix - 変換行列 / Transform matrix + * @param {Float32Array | null} color_transform - カラー変換パラメータ / Color transform parameters + * @param {boolean} use_filter - フィルター使用フラグ / Whether to use filter + * @param {Float32Array | null} filter_bounds - フィルターバウンド / Filter bounds + * @param {Float32Array | null} filter_params - フィルターパラメータ / Filter parameters + * @param {string} unique_key - ユニークキー / Unique key + * @param {string} filter_key - フィルターキー / Filter key + * @return {void} */ containerEndLayer ( blend_mode: IBlendMode, @@ -2302,12 +2665,13 @@ export class Context * @description キャッシュされたコンテナフィルターテクスチャをメインに描画 * Draw a cached container filter texture to the main attachment * - * @param {IBlendMode} blend_mode - * @param {Float32Array} matrix - * @param {Float32Array} color_transform - * @param {Float32Array} filter_bounds - * @param {string} unique_key - * @param {string} filter_key + * @param {IBlendMode} blend_mode - ブレンドモード / Blend mode + * @param {Float32Array} matrix - 変換行列 / Transform matrix + * @param {Float32Array} color_transform - カラー変換パラメータ / Color transform parameters + * @param {Float32Array} filter_bounds - フィルターバウンド / Filter bounds + * @param {string} unique_key - ユニークキー / Unique key + * @param {string} filter_key - フィルターキー / Filter key + * @return {void} */ containerDrawCachedFilter ( blend_mode: IBlendMode, @@ -2477,6 +2841,9 @@ export class Context /** * @description メインテクスチャを確保(フレーム開始時に一度だけgetCurrentTexture呼び出し) + * Ensure the main texture is acquired (calls getCurrentTexture once per frame) + * + * @return {void} */ private ensureMainTexture(): void { @@ -2498,6 +2865,9 @@ export class Context /** * @description 現在の描画ターゲットのテクスチャビューを取得 + * Get the texture view of the current render target + * + * @return {GPUTextureView} 現在のテクスチャビュー / Current texture view */ private getCurrentTextureView(): GPUTextureView { @@ -2513,6 +2883,9 @@ export class Context /** * @description コマンドエンコーダーが存在することを保証 + * Ensure the command encoder exists + * + * @return {void} */ private ensureCommandEncoder(): void { @@ -2526,6 +2899,9 @@ export class Context /** * @description フレーム開始(レンダリング開始前に呼ぶ) + * Begin a new frame (call before rendering starts) + * + * @return {void} */ beginFrame(): void { @@ -2542,6 +2918,10 @@ export class Context /** * @description フレームごとのプール管理テクスチャを追加(endFrame()でプールに返却) + * Add a pooled texture for the current frame (returned to pool in endFrame()) + * + * @param {GPUTexture} texture - プール管理テクスチャ / Pooled texture + * @return {void} */ addFrameTexture (texture: GPUTexture): void { @@ -2550,6 +2930,9 @@ export class Context /** * @description フレーム終了とコマンド送信(レンダリング完了後に呼ぶ) + * End the frame and submit commands (call after rendering is complete) + * + * @return {void} */ endFrame(): void { @@ -2621,6 +3004,9 @@ export class Context /** * @description コマンドを送信(後方互換性のため残す) + * Submit commands (kept for backward compatibility) + * + * @return {void} */ submit (): void { @@ -2629,7 +3015,12 @@ export class Context /** * @description ノードを作成 + * Create a node in the texture atlas * アトラスがいっぱいの場合は新しいアトラスを作成して再試行 + * + * @param {number} width - ノード幅 / Node width + * @param {number} height - ノード高さ / Node height + * @return {Node} 作成されたノード / Created node */ createNode (width: number, height: number): Node { @@ -2655,6 +3046,10 @@ export class Context /** * @description ノードを削除 + * Remove a node from the texture atlas + * + * @param {Node} node - 削除対象ノード / Node to remove + * @return {void} */ removeNode (node: Node): void { @@ -2669,7 +3064,10 @@ export class Context /** * @description フレームバッファの描画情報をキャンバスに転送 + * Transfer frame buffer contents to the canvas * スワップチェーンはCopyDstをサポートしないため、レンダーパスでブリット + * + * @return {void} */ transferMainCanvas (): void { @@ -2738,6 +3136,11 @@ export class Context /** * @description ImageBitmapを生成 + * Create an ImageBitmap from the current rendering result + * + * @param {number} width - 画像幅 / Image width + * @param {number} height - 画像高さ / Image height + * @return {Promise} 生成されたImageBitmap / Created ImageBitmap */ async createImageBitmap (width: number, height: number): Promise { @@ -2860,6 +3263,7 @@ export class Context * @description マスク描画の開始準備 * Prepare to start drawing the mask * + * @return {void} */ beginMask(): void { @@ -2927,10 +3331,11 @@ export class Context * @description マスクの描画範囲を設定 * Set the mask drawing bounds * - * @param {number} x_min - * @param {number} y_min - * @param {number} x_max - * @param {number} y_max + * @param {number} x_min - 最小X座標 / Minimum X coordinate + * @param {number} y_min - 最小Y座標 / Minimum Y coordinate + * @param {number} x_max - 最大X座標 / Maximum X coordinate + * @param {number} y_max - 最大Y座標 / Maximum Y coordinate + * @return {void} */ setMaskBounds( x_min: number, @@ -2945,6 +3350,7 @@ export class Context * @description マスクの描画を終了 * End mask drawing * + * @return {void} */ endMask(): void { @@ -2962,8 +3368,9 @@ export class Context /** * @description マスクの終了処理 - * Mask end processing + * Mask end processing (leave the mask) * + * @return {void} */ leaveMask(): void { From c595e24529773488ee1b1591e1b08aa0061eac08 Mon Sep 17 00:00:00 2001 From: ienaga Date: Fri, 27 Mar 2026 06:54:37 +0900 Subject: [PATCH 13/26] =?UTF-8?q?#267=20WebGPU=E3=81=AE=E3=83=AA=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/webgpu/src/AtlasManager.test.ts | 28 +- packages/webgpu/src/AtlasManager.ts | 116 +++++- packages/webgpu/src/AttachmentManager.ts | 47 ++- ...entManagerCreateAttachmentObjectService.ts | 6 +- ...tachmentManagerCreateColorBufferService.ts | 8 +- ...chmentManagerCreateStencilBufferService.ts | 12 +- ...chmentManagerCreateTextureObjectService.ts | 14 +- .../AttachmentManagerGetColorBufferService.ts | 18 +- ...ttachmentManagerGetStencilBufferService.ts | 22 +- .../AttachmentManagerGetTextureService.ts | 22 +- .../AttachmentManagerReleaseTextureService.ts | 16 +- ...chmentManagerGetAttachmentObjectUseCase.ts | 44 +-- ...tachmentManagerReleaseAttachmentUseCase.ts | 26 +- .../src/BezierConverter/BezierConverter.ts | 9 + ...onverterAdaptiveCubicToQuadUseCase.test.ts | 37 +- ...zierConverterAdaptiveCubicToQuadUseCase.ts | 41 +- packages/webgpu/src/Blend.ts | 20 + .../webgpu/src/Blend/BlendInstancedManager.ts | 57 +-- .../usecase/BlendApplyComplexBlendUseCase.ts | 51 ++- packages/webgpu/src/BufferManager.ts | 252 +++++++++--- ...ufferManagerReleaseUniformBufferService.ts | 4 +- ...BufferManagerReleaseVertexBufferService.ts | 4 +- ...ufferManagerUpdateIndirectBufferService.ts | 1 + ...fferManagerCleanupStorageBuffersUseCase.ts | 1 + ...ufferManagerReleaseStorageBufferUseCase.ts | 1 + .../ContextComputeBitmapMatrixService.ts | 7 + .../ContextComputeGradientMatrixService.ts | 24 +- .../service/ContextFillSimpleService.ts | 14 + .../ContextFillWithStencilMainService.ts | 11 + .../service/ContextFillWithStencilService.ts | 11 + .../usecase/ContextApplyFilterUseCase.ts | 207 +++++++--- .../usecase/ContextBitmapFillUseCase.ts | 47 +++ .../usecase/ContextBitmapStrokeUseCase.ts | 35 ++ .../src/Context/usecase/ContextClipUseCase.ts | 19 + .../ContextContainerEndLayerUseCase.ts | 361 +++++++++++------- .../ContextDrawArraysInstancedUseCase.ts | 21 + .../usecase/ContextDrawIndirectUseCase.ts | 99 +++-- .../usecase/ContextGradientFillUseCase.ts | 47 +++ .../usecase/ContextGradientStrokeUseCase.ts | 35 ++ .../ContextProcessComplexBlendQueueUseCase.ts | 270 ++++++++----- packages/webgpu/src/FillTexturePool.ts | 78 +++- .../FilterApplyBevelFilterUseCase.ts | 61 ++- .../FilterApplyBlurFilterUseCase.ts | 158 +++++--- .../webgpu/src/Filter/BlurFilterUseCase.ts | 47 ++- .../FilterApplyColorMatrixFilterUseCase.ts | 12 +- .../FilterApplyConvolutionFilterUseCase.ts | 34 +- ...FilterApplyDisplacementMapFilterUseCase.ts | 75 ++-- .../FilterApplyDropShadowFilterUseCase.ts | 46 +-- .../src/Filter/FilterGradientLUTCache.ts | 2 + .../FilterApplyGlowFilterUseCase.ts | 30 +- .../FilterApplyGradientBevelFilterUseCase.ts | 38 +- .../FilterApplyGradientGlowFilterUseCase.ts | 34 +- packages/webgpu/src/FrameBufferManager.ts | 127 +++++- ...anagerCreateRenderPassDescriptorService.ts | 37 +- ...reateStencilRenderPassDescriptorService.ts | 53 ++- ...ameBufferManagerCreateAttachmentUseCase.ts | 30 +- ...anagerReleaseTemporaryAttachmentUseCase.ts | 10 +- .../webgpu/src/Gradient/GradientLUTCache.ts | 72 +++- .../src/Gradient/GradientLUTGenerator.ts | 117 +++--- packages/webgpu/src/Mask.test.ts | 36 -- packages/webgpu/src/Mask.ts | 93 ++++- .../src/Mask/service/MaskUnionMaskService.ts | 83 ++-- .../Mesh/service/MeshFillGenerateService.ts | 8 +- .../service/MeshStrokeFillGenerateService.ts | 8 +- .../MeshBitmapStrokeGenerateUseCase.ts | 13 +- .../Mesh/usecase/MeshFillGenerateUseCase.ts | 11 +- .../MeshGradientStrokeGenerateUseCase.ts | 13 +- .../Mesh/usecase/MeshStrokeGenerateUseCase.ts | 329 +++++++++++----- packages/webgpu/src/PathCommand.ts | 35 +- .../GradientLUTCalculateResolutionService.ts | 26 +- .../GradientLUTInterpolateColorService.ts | 28 +- packages/webgpu/src/Shader/PipelineManager.ts | 341 ++++++++++++++--- .../src/Shader/ShaderInstancedManager.ts | 17 + packages/webgpu/src/Shader/ShaderSource.ts | 359 ++++++++++++++++- .../src/Shader/wgsl/common/SharedWgsl.ts | 28 ++ .../src/Shader/wgsl/fragment/BasicFragment.ts | 14 + .../Shader/wgsl/fragment/BitmapFragment.ts | 7 + .../Shader/wgsl/fragment/EffectFragment.ts | 42 ++ .../src/Shader/wgsl/fragment/FillFragment.ts | 7 + .../Shader/wgsl/fragment/FilterFragment.ts | 70 ++++ .../Shader/wgsl/fragment/GradientFragment.ts | 51 ++- .../Shader/wgsl/fragment/InstancedFragment.ts | 7 + .../src/Shader/wgsl/fragment/MaskFragment.ts | 7 + .../Shader/wgsl/fragment/StencilFragment.ts | 14 + .../src/Shader/wgsl/vertex/BasicVertex.ts | 7 + .../src/Shader/wgsl/vertex/BitmapVertex.ts | 7 + .../src/Shader/wgsl/vertex/FillVertex.ts | 7 + .../src/Shader/wgsl/vertex/FilterVertex.ts | 163 ++++++-- .../src/Shader/wgsl/vertex/GradientVertex.ts | 7 + .../src/Shader/wgsl/vertex/InstancedVertex.ts | 7 + .../src/Shader/wgsl/vertex/MaskVertex.ts | 7 + .../src/Shader/wgsl/vertex/StencilVertex.ts | 14 + packages/webgpu/src/TextureManager.ts | 77 +++- ...TextureManagerInitializeSamplersService.ts | 4 +- ...agerCreateTextureFromImageBitmapUseCase.ts | 8 +- ...reManagerCreateTextureFromPixelsUseCase.ts | 12 +- packages/webgpu/src/TexturePool.test.ts | 52 +-- packages/webgpu/src/TexturePool.ts | 99 +++-- .../service/TexturePoolCleanupService.ts | 14 +- .../service/TexturePoolReleaseService.ts | 10 +- .../usecase/TexturePoolAcquireUseCase.ts | 41 +- packages/webgpu/src/WebGPUUtil.ts | 108 ++++-- .../webgpu/src/interface/IAttachmentObject.ts | 36 ++ packages/webgpu/src/interface/IBlendMode.ts | 7 + packages/webgpu/src/interface/IBlendState.ts | 8 + packages/webgpu/src/interface/IBounds.ts | 23 ++ .../src/interface/IColorBufferObject.ts | 28 ++ .../webgpu/src/interface/IComplexBlendItem.ts | 48 +++ .../webgpu/src/interface/IFilterConfig.ts | 28 ++ .../webgpu/src/interface/IGradientStop.ts | 20 + .../src/interface/ILocalFilterConfig.ts | 32 ++ packages/webgpu/src/interface/IMeshResult.ts | 8 + packages/webgpu/src/interface/IPoint.ts | 12 + .../webgpu/src/interface/IPooledTexture.ts | 24 ++ .../webgpu/src/interface/IQuadraticSegment.ts | 8 + .../src/interface/IStencilBufferObject.ts | 28 ++ .../src/interface/IStorageBufferConfig.ts | 9 +- .../webgpu/src/interface/ITextureObject.ts | 28 ++ 118 files changed, 4305 insertions(+), 1459 deletions(-) diff --git a/packages/webgpu/src/AtlasManager.test.ts b/packages/webgpu/src/AtlasManager.test.ts index f12688a6..a8e48e2d 100644 --- a/packages/webgpu/src/AtlasManager.test.ts +++ b/packages/webgpu/src/AtlasManager.test.ts @@ -5,7 +5,6 @@ import { $getAtlasAttachmentObjects, $setAtlasAttachmentObject, $getAtlasAttachmentObject, - $hasAtlasAttachmentObject, $rootNodes, $getActiveTransferBounds, $clearTransferBounds, @@ -88,31 +87,6 @@ describe("AtlasManager", () => }); }); - describe("$hasAtlasAttachmentObject", () => - { - it("should return false when no attachment exists", () => - { - expect($hasAtlasAttachmentObject()).toBe(false); - }); - - it("should return true when attachment exists", () => - { - const mockAttachment = createMockAttachment(1, 512, 512); - $setAtlasAttachmentObject(mockAttachment); - - expect($hasAtlasAttachmentObject()).toBe(true); - }); - - it("should return false at non-existing index", () => - { - const mockAttachment = createMockAttachment(1, 512, 512); - $setAtlasAttachmentObject(mockAttachment); - - $setActiveAtlasIndex(5); - expect($hasAtlasAttachmentObject()).toBe(false); - }); - }); - describe("$rootNodes", () => { it("should be empty array after reset", () => @@ -282,7 +256,7 @@ describe("AtlasManager", () => $resetAtlas(); // Attachment should be removed - expect($hasAtlasAttachmentObject()).toBe(false); + expect($getAtlasAttachmentObjects().length).toBe(0); }); it("should clear transfer bounds", () => diff --git a/packages/webgpu/src/AtlasManager.ts b/packages/webgpu/src/AtlasManager.ts index 51203474..f53c9050 100644 --- a/packages/webgpu/src/AtlasManager.ts +++ b/packages/webgpu/src/AtlasManager.ts @@ -1,42 +1,105 @@ import type { IAttachmentObject } from "./interface/IAttachmentObject"; import type { TexturePacker } from "@next2d/texture-packer"; +/** + * @description テクスチャアトラス境界の初期最大値 + * Initial maximum value for texture atlas bounds + * @type {number} + */ const $MAX_VALUE: number = Number.MAX_VALUE; + +/** + * @description テクスチャアトラス境界の初期最小値 + * Initial minimum value for texture atlas bounds + * @type {number} + */ const $MIN_VALUE: number = -Number.MAX_VALUE; +/** + * @description 現在アクティブなアトラスのインデックス + * Index of the currently active atlas + * @type {number} + */ let $activeAtlasIndex: number = 0; +/** + * @description アクティブなアトラスインデックスを設定する + * Set the active atlas index + * @param {number} index - アトラスインデックス / atlas index + * @return {void} + */ export const $setActiveAtlasIndex = (index: number): void => { $activeAtlasIndex = index; }; +/** + * @description アクティブなアトラスインデックスを取得する + * Get the active atlas index + * @return {number} + */ export const $getActiveAtlasIndex = (): number => { return $activeAtlasIndex; }; +/** + * @description アトラスのアタッチメントオブジェクト配列 + * Array of atlas attachment objects + * @type {IAttachmentObject[]} + */ const $atlasAttachmentObjects: IAttachmentObject[] = []; +/** + * @description アトラスのアタッチメントオブジェクト配列を取得する + * Get the array of atlas attachment objects + * @return {IAttachmentObject[]} + */ export const $getAtlasAttachmentObjects = (): IAttachmentObject[] => { return $atlasAttachmentObjects; }; +/** + * @description アクティブなインデックスにアタッチメントオブジェクトを設定する + * Set an attachment object at the active atlas index + * @param {IAttachmentObject} attachment_object - アタッチメントオブジェクト / attachment object + * @return {void} + */ export const $setAtlasAttachmentObject = (attachment_object: IAttachmentObject): void => { $atlasAttachmentObjects[$activeAtlasIndex] = attachment_object; }; +/** + * @description アトラス生成関数の型定義 + * Type definition for atlas creator function + */ type AtlasCreator = (index: number) => IAttachmentObject; +/** + * @description アトラス生成関数の参照 + * Atlas creator function reference + * @type {AtlasCreator | null} + */ let $atlasCreator: AtlasCreator | null = null; +/** + * @description アトラス生成関数を設定する + * Set the atlas creator function + * @param {AtlasCreator} creator - アトラス生成関数 / atlas creator function + * @return {void} + */ export const $setAtlasCreator = (creator: AtlasCreator): void => { $atlasCreator = creator; }; +/** + * @description アクティブなインデックスのアタッチメントオブジェクトを取得する(未作成の場合はcreatorで生成) + * Get the attachment object at the active index (creates via creator if not yet created) + * @return {IAttachmentObject | null} + */ export const $getAtlasAttachmentObject = (): IAttachmentObject | null => { if (!($activeAtlasIndex in $atlasAttachmentObjects)) { @@ -50,6 +113,12 @@ export const $getAtlasAttachmentObject = (): IAttachmentObject | null => return $atlasAttachmentObjects[$activeAtlasIndex]; }; +/** + * @description 指定インデックスのアタッチメントオブジェクトを取得する + * Get the attachment object at a specified index + * @param {number} index - アトラスインデックス / atlas index + * @return {IAttachmentObject | null} + */ export const $getAtlasAttachmentObjectByIndex = (index: number): IAttachmentObject | null => { if (!(index in $atlasAttachmentObjects)) { @@ -58,15 +127,26 @@ export const $getAtlasAttachmentObjectByIndex = (index: number): IAttachmentObje return $atlasAttachmentObjects[index]; }; -export const $hasAtlasAttachmentObject = (): boolean => -{ - return $activeAtlasIndex in $atlasAttachmentObjects; -}; - +/** + * @description テクスチャパッカーのルートノード配列 + * Array of root nodes for texture packing + * @type {TexturePacker[]} + */ export const $rootNodes: TexturePacker[] = []; +/** + * @description アトラスごとの転送領域配列 + * Array of transfer bounds per atlas + * @type {Float32Array[]} + */ const $transferBounds: Float32Array[] = []; +/** + * @description 指定インデックスのアクティブな転送領域を取得する(未作成の場合は初期化) + * Get the active transfer bounds at the specified index (initializes if not yet created) + * @param {number} index - アトラスインデックス / atlas index + * @return {Float32Array} + */ export const $getActiveTransferBounds = (index: number): Float32Array => { if (!(index in $transferBounds)) { @@ -80,6 +160,11 @@ export const $getActiveTransferBounds = (index: number): Float32Array => return $transferBounds[index]; }; +/** + * @description 全ての転送領域を初期値にリセットする + * Reset all transfer bounds to their initial values + * @return {void} + */ export const $clearTransferBounds = (): void => { for (let idx = 0; idx < $transferBounds.length; ++idx) { @@ -93,18 +178,39 @@ export const $clearTransferBounds = (): void => } }; +/** + * @description 現在処理中のアトラスインデックス + * Index of the currently processed atlas + * @type {number} + */ let $currentAtlasIndex: number = 0; +/** + * @description 現在のアトラスインデックスを設定する + * Set the current atlas index + * @param {number} index - アトラスインデックス / atlas index + * @return {void} + */ export const $setCurrentAtlasIndex = (index: number): void => { $currentAtlasIndex = index; }; +/** + * @description 現在のアトラスインデックスを取得する + * Get the current atlas index + * @return {number} + */ export const $getCurrentAtlasIndex = (): number => { return $currentAtlasIndex; }; +/** + * @description アトラスの全状態をリセットする(テクスチャリソースの破棄を含む) + * Reset all atlas state including destroying texture resources + * @return {void} + */ export const $resetAtlas = (): void => { $rootNodes.length = 0; diff --git a/packages/webgpu/src/AttachmentManager.ts b/packages/webgpu/src/AttachmentManager.ts index b931875b..f170b153 100644 --- a/packages/webgpu/src/AttachmentManager.ts +++ b/packages/webgpu/src/AttachmentManager.ts @@ -5,6 +5,10 @@ import type { IStencilBufferObject } from "./interface/IStencilBufferObject"; import { execute as attachmentManagerGetAttachmentObjectUseCase } from "./AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase"; import { execute as attachmentManagerReleaseAttachmentUseCase } from "./AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase"; +/** + * @description アタッチメントリソースのプール管理クラス + * Pool manager class for attachment resources + */ export class AttachmentManager { private device: GPUDevice; @@ -13,8 +17,12 @@ export class AttachmentManager private colorBufferPool: IColorBufferObject[]; private stencilBufferPool: IStencilBufferObject[]; private idCounter: { attachmentId: number; textureId: number; stencilId: number }; - private currentAttachment: IAttachmentObject | null; + /** + * @description AttachmentManagerのコンストラクタ + * Constructor for AttachmentManager + * @param {GPUDevice} device - WebGPUデバイス / WebGPU device + */ constructor(device: GPUDevice) { this.device = device; @@ -23,9 +31,16 @@ export class AttachmentManager this.colorBufferPool = []; this.stencilBufferPool = []; this.idCounter = { "attachmentId": 0, "textureId": 0, "stencilId": 0 }; - this.currentAttachment = null; } + /** + * @description プールからアタッチメントオブジェクトを取得する + * Get an attachment object from the pool + * @param {number} width - テクスチャの幅 / Texture width + * @param {number} height - テクスチャの高さ / Texture height + * @param {boolean} [msaa=false] - MSAAを有効にするか / Whether to enable MSAA + * @return {IAttachmentObject} + */ getAttachmentObject( width: number, height: number, @@ -45,16 +60,33 @@ export class AttachmentManager ); } - bindAttachment(attachment: IAttachmentObject): void + /** + * @description 現在のアタッチメントをバインドする + * Bind the current attachment + * @param {IAttachmentObject} attachment - バインドするアタッチメント / Attachment to bind + * @return {void} + */ + bindAttachment(_attachment: IAttachmentObject): void { - this.currentAttachment = attachment; + // no-op: バインド状態はContext側で管理 } + /** + * @description 現在のアタッチメントのバインドを解除する + * Unbind the current attachment + * @return {void} + */ unbindAttachment(): void { - this.currentAttachment = null; + // no-op: バインド状態はContext側で管理 } + /** + * @description アタッチメントをプールに返却する + * Release an attachment back to the pool + * @param {IAttachmentObject} attachment - 返却するアタッチメント / Attachment to release + * @return {void} + */ releaseAttachment(attachment: IAttachmentObject): void { attachmentManagerReleaseAttachmentUseCase( @@ -66,6 +98,11 @@ export class AttachmentManager ); } + /** + * @description 全リソースを破棄する + * Dispose all resources + * @return {void} + */ dispose(): void { for (const pool of this.texturePool.values()) { diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateAttachmentObjectService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateAttachmentObjectService.ts index 5ca9deca..48b06f0a 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateAttachmentObjectService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateAttachmentObjectService.ts @@ -4,16 +4,16 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; * @description 新しいアタッチメントオブジェクトを作成 * Create a new attachment object * - * @param {{ attachmentId: number }} idCounter + * @param {{ attachmentId: number }} id_counter - ID管理カウンタ * @return {IAttachmentObject} * @method * @protected */ export const execute = ( - idCounter: { attachmentId: number } + id_counter: { attachmentId: number } ): IAttachmentObject => { return { - "id": idCounter.attachmentId++, + "id": id_counter.attachmentId++, "width": 0, "height": 0, "clipLevel": 0, diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateColorBufferService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateColorBufferService.ts index 38cc6932..8a21184f 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateColorBufferService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateColorBufferService.ts @@ -5,10 +5,10 @@ import type { IStencilBufferObject } from "../../interface/IStencilBufferObject" * @description カラーバッファを新規作成 * Create a new color buffer * - * @param {GPUDevice} device - * @param {number} width - * @param {number} height - * @param {IStencilBufferObject} stencil + * @param {GPUDevice} device - GPUデバイス + * @param {number} width - バッファ幅 + * @param {number} height - バッファ高さ + * @param {IStencilBufferObject} stencil - 関連するステンシルバッファ * @return {IColorBufferObject} * @method * @protected diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateStencilBufferService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateStencilBufferService.ts index 367d1639..3cf886ff 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateStencilBufferService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateStencilBufferService.ts @@ -4,10 +4,10 @@ import type { IStencilBufferObject } from "../../interface/IStencilBufferObject" * @description ステンシルバッファを新規作成 * Create a new stencil buffer * - * @param {GPUDevice} device - * @param {number} width - * @param {number} height - * @param {{ stencilId: number }} idCounter + * @param {GPUDevice} device - GPUデバイス + * @param {number} width - バッファ幅 + * @param {number} height - バッファ高さ + * @param {{ stencilId: number }} id_counter - ID管理カウンタ * @return {IStencilBufferObject} * @method * @protected @@ -16,7 +16,7 @@ export const execute = ( device: GPUDevice, width: number, height: number, - idCounter: { stencilId: number } + id_counter: { stencilId: number } ): IStencilBufferObject => { const texture = device.createTexture({ "size": { width, height }, @@ -25,7 +25,7 @@ export const execute = ( }); return { - "id": idCounter.stencilId++, + "id": id_counter.stencilId++, "resource": texture, "view": texture.createView(), width, diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateTextureObjectService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateTextureObjectService.ts index 5b03d7d7..115ca073 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateTextureObjectService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateTextureObjectService.ts @@ -4,11 +4,11 @@ import type { ITextureObject } from "../../interface/ITextureObject"; * @description テクスチャオブジェクトを新規作成 * Create a new texture object * - * @param {GPUDevice} device - * @param {number} width - * @param {number} height - * @param {boolean} smooth - * @param {{ textureId: number }} idCounter + * @param {GPUDevice} device - GPUデバイス + * @param {number} width - テクスチャ幅 + * @param {number} height - テクスチャ高さ + * @param {boolean} smooth - スムーズフィルタリングの有効フラグ + * @param {{ textureId: number }} id_counter - ID管理カウンタ * @return {ITextureObject} * @method * @protected @@ -18,7 +18,7 @@ export const execute = ( width: number, height: number, smooth: boolean, - idCounter: { textureId: number } + id_counter: { textureId: number } ): ITextureObject => { const texture = device.createTexture({ "size": { width, height }, @@ -32,7 +32,7 @@ export const execute = ( const view = texture.createView(); return { - "id": idCounter.textureId++, + "id": id_counter.textureId++, "resource": texture, view, width, diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetColorBufferService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetColorBufferService.ts index d43ed5c0..07e571e6 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetColorBufferService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetColorBufferService.ts @@ -6,27 +6,27 @@ import { execute as attachmentManagerCreateColorBufferService } from "./Attachme * @description カラーバッファを取得(プールから再利用または新規作成) * Get color buffer from pool or create new one * - * @param {GPUDevice} device - * @param {IColorBufferObject[]} colorBufferPool - * @param {number} width - * @param {number} height - * @param {IStencilBufferObject} stencil + * @param {GPUDevice} device - GPUデバイス + * @param {IColorBufferObject[]} color_buffer_pool - カラーバッファプール + * @param {number} width - バッファ幅 + * @param {number} height - バッファ高さ + * @param {IStencilBufferObject} stencil - 関連するステンシルバッファ * @return {IColorBufferObject} * @method * @protected */ export const execute = ( device: GPUDevice, - colorBufferPool: IColorBufferObject[], + color_buffer_pool: IColorBufferObject[], width: number, height: number, stencil: IStencilBufferObject ): IColorBufferObject => { // プールから適切なサイズのものを検索 - for (let i = 0; i < colorBufferPool.length; i++) { - const buffer = colorBufferPool[i]; + for (let i = 0; i < color_buffer_pool.length; i++) { + const buffer = color_buffer_pool[i]; if (buffer.width >= width && buffer.height >= height) { - colorBufferPool.splice(i, 1); + color_buffer_pool.splice(i, 1); buffer.stencil = stencil; buffer.dirty = false; return buffer; diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetStencilBufferService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetStencilBufferService.ts index b49cc1c4..70b5fdfb 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetStencilBufferService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetStencilBufferService.ts @@ -5,32 +5,32 @@ import { execute as attachmentManagerCreateStencilBufferService } from "./Attach * @description ステンシルバッファを取得(プールから再利用または新規作成) * Get stencil buffer from pool or create new one * - * @param {GPUDevice} device - * @param {IStencilBufferObject[]} stencilBufferPool - * @param {number} width - * @param {number} height - * @param {{ stencilId: number }} idCounter + * @param {GPUDevice} device - GPUデバイス + * @param {IStencilBufferObject[]} stencil_buffer_pool - ステンシルバッファプール + * @param {number} width - バッファ幅 + * @param {number} height - バッファ高さ + * @param {{ stencilId: number }} id_counter - ID管理カウンタ * @return {IStencilBufferObject} * @method * @protected */ export const execute = ( device: GPUDevice, - stencilBufferPool: IStencilBufferObject[], + stencil_buffer_pool: IStencilBufferObject[], width: number, height: number, - idCounter: { stencilId: number } + id_counter: { stencilId: number } ): IStencilBufferObject => { // プールから適切なサイズのものを検索 - for (let i = 0; i < stencilBufferPool.length; i++) { - const buffer = stencilBufferPool[i]; + for (let i = 0; i < stencil_buffer_pool.length; i++) { + const buffer = stencil_buffer_pool[i]; if (buffer.width >= width && buffer.height >= height) { - stencilBufferPool.splice(i, 1); + stencil_buffer_pool.splice(i, 1); buffer.dirty = false; return buffer; } } // 新規作成 - return attachmentManagerCreateStencilBufferService(device, width, height, idCounter); + return attachmentManagerCreateStencilBufferService(device, width, height, id_counter); }; diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetTextureService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetTextureService.ts index 300464bf..3c9f5608 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetTextureService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetTextureService.ts @@ -5,34 +5,34 @@ import { execute as attachmentManagerCreateTextureObjectService } from "./Attach * @description テクスチャオブジェクトを取得(プールから再利用または新規作成) * Get texture object from pool or create new one * - * @param {GPUDevice} device - * @param {Map} texturePool - * @param {number} width - * @param {number} height - * @param {boolean} smooth - * @param {{ textureId: number }} idCounter + * @param {GPUDevice} device - GPUデバイス + * @param {Map} texture_pool - テクスチャプール + * @param {number} width - テクスチャ幅 + * @param {number} height - テクスチャ高さ + * @param {boolean} smooth - スムーズフィルタリングの有効フラグ + * @param {{ textureId: number }} id_counter - ID管理カウンタ * @return {ITextureObject} * @method * @protected */ export const execute = ( device: GPUDevice, - texturePool: Map, + texture_pool: Map, width: number, height: number, smooth: boolean, - idCounter: { textureId: number } + id_counter: { textureId: number } ): ITextureObject => { const key = `${width}x${height}_${smooth ? "smooth" : "nearest"}`; // プールから再利用 - if (texturePool.has(key)) { - const pool = texturePool.get(key)!; + if (texture_pool.has(key)) { + const pool = texture_pool.get(key)!; if (pool.length > 0) { return pool.pop()!; } } // 新規作成 - return attachmentManagerCreateTextureObjectService(device, width, height, smooth, idCounter); + return attachmentManagerCreateTextureObjectService(device, width, height, smooth, id_counter); }; diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerReleaseTextureService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerReleaseTextureService.ts index f202edda..0627bdc4 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerReleaseTextureService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerReleaseTextureService.ts @@ -4,21 +4,21 @@ import type { ITextureObject } from "../../interface/ITextureObject"; * @description テクスチャをプールに返却 * Release texture back to pool * - * @param {Map} texturePool - * @param {ITextureObject} textureObject + * @param {Map} texture_pool - テクスチャプール + * @param {ITextureObject} texture_object - 返却するテクスチャオブジェクト * @return {void} * @method * @protected */ export const execute = ( - texturePool: Map, - textureObject: ITextureObject + texture_pool: Map, + texture_object: ITextureObject ): void => { - const key = `${textureObject.width}x${textureObject.height}_${textureObject.smooth ? "smooth" : "nearest"}`; + const key = `${texture_object.width}x${texture_object.height}_${texture_object.smooth ? "smooth" : "nearest"}`; - if (!texturePool.has(key)) { - texturePool.set(key, []); + if (!texture_pool.has(key)) { + texture_pool.set(key, []); } - texturePool.get(key)!.push(textureObject); + texture_pool.get(key)!.push(texture_object); }; diff --git a/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase.ts b/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase.ts index a6fb60ae..85a614ec 100644 --- a/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase.ts +++ b/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase.ts @@ -11,34 +11,34 @@ import { execute as attachmentManagerGetTextureService } from "../service/Attach * @description アタッチメントオブジェクトを取得 * Get attachment object * - * @param {GPUDevice} device - * @param {IAttachmentObject[]} attachmentPool - * @param {Map} texturePool - * @param {IColorBufferObject[]} colorBufferPool - * @param {IStencilBufferObject[]} stencilBufferPool - * @param {number} width - * @param {number} height - * @param {boolean} msaa - * @param {{ attachmentId: number, textureId: number, stencilId: number }} idCounter + * @param {GPUDevice} device - GPUデバイス + * @param {IAttachmentObject[]} attachment_pool - アタッチメントプール + * @param {Map} texture_pool - テクスチャプール + * @param {IColorBufferObject[]} color_buffer_pool - カラーバッファプール + * @param {IStencilBufferObject[]} stencil_buffer_pool - ステンシルバッファプール + * @param {number} width - バッファ幅 + * @param {number} height - バッファ高さ + * @param {boolean} msaa - MSAA有効フラグ + * @param {{ attachmentId: number, textureId: number, stencilId: number }} id_counter - ID管理カウンタ * @return {IAttachmentObject} * @method * @protected */ export const execute = ( device: GPUDevice, - attachmentPool: IAttachmentObject[], - texturePool: Map, - colorBufferPool: IColorBufferObject[], - stencilBufferPool: IStencilBufferObject[], + attachment_pool: IAttachmentObject[], + texture_pool: Map, + color_buffer_pool: IColorBufferObject[], + stencil_buffer_pool: IStencilBufferObject[], width: number, height: number, msaa: boolean, - idCounter: { attachmentId: number; textureId: number; stencilId: number } + id_counter: { attachmentId: number; textureId: number; stencilId: number } ): IAttachmentObject => { // プールから再利用 - const attachment = attachmentPool.length > 0 - ? attachmentPool.pop()! - : attachmentManagerCreateAttachmentObjectService(idCounter); + const attachment = attachment_pool.length > 0 + ? attachment_pool.pop()! + : attachmentManagerCreateAttachmentObjectService(id_counter); // サイズとフラグを更新 attachment.width = width; @@ -50,16 +50,16 @@ export const execute = ( // ステンシルバッファを取得または作成 const stencil = attachmentManagerGetStencilBufferService( device, - stencilBufferPool, + stencil_buffer_pool, width, height, - idCounter + id_counter ); // カラーバッファを取得または作成(ステンシルを参照) const color = attachmentManagerGetColorBufferService( device, - colorBufferPool, + color_buffer_pool, width, height, stencil @@ -70,11 +70,11 @@ export const execute = ( // テクスチャを取得 const texture = attachmentManagerGetTextureService( device, - texturePool, + texture_pool, width, height, true, - idCounter + id_counter ); attachment.texture = texture; diff --git a/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase.ts b/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase.ts index 6987a867..551edda5 100644 --- a/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase.ts +++ b/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase.ts @@ -8,40 +8,40 @@ import { execute as attachmentManagerReleaseTextureService } from "../service/At * @description アタッチメントを解放してプールに返却 * Release attachment and return to pool * - * @param {IAttachmentObject[]} attachmentPool - * @param {Map} texturePool - * @param {IColorBufferObject[]} colorBufferPool - * @param {IStencilBufferObject[]} stencilBufferPool - * @param {IAttachmentObject} attachment + * @param {IAttachmentObject[]} attachment_pool - アタッチメントプール + * @param {Map} texture_pool - テクスチャプール + * @param {IColorBufferObject[]} color_buffer_pool - カラーバッファプール + * @param {IStencilBufferObject[]} stencil_buffer_pool - ステンシルバッファプール + * @param {IAttachmentObject} attachment - 解放するアタッチメント * @return {void} * @method * @protected */ export const execute = ( - attachmentPool: IAttachmentObject[], - texturePool: Map, - colorBufferPool: IColorBufferObject[], - stencilBufferPool: IStencilBufferObject[], + attachment_pool: IAttachmentObject[], + texture_pool: Map, + color_buffer_pool: IColorBufferObject[], + stencil_buffer_pool: IStencilBufferObject[], attachment: IAttachmentObject ): void => { // テクスチャをプールに返却 if (attachment.texture) { - attachmentManagerReleaseTextureService(texturePool, attachment.texture); + attachmentManagerReleaseTextureService(texture_pool, attachment.texture); attachment.texture = null; } // カラーバッファをプールに返却 if (attachment.color) { - colorBufferPool.push(attachment.color); + color_buffer_pool.push(attachment.color); attachment.color = null; } // ステンシルバッファをプールに返却 if (attachment.stencil) { - stencilBufferPool.push(attachment.stencil); + stencil_buffer_pool.push(attachment.stencil); attachment.stencil = null; } // アタッチメントをプールに返却 - attachmentPool.push(attachment); + attachment_pool.push(attachment); }; diff --git a/packages/webgpu/src/BezierConverter/BezierConverter.ts b/packages/webgpu/src/BezierConverter/BezierConverter.ts index e2563c7d..942221e4 100644 --- a/packages/webgpu/src/BezierConverter/BezierConverter.ts +++ b/packages/webgpu/src/BezierConverter/BezierConverter.ts @@ -10,7 +10,16 @@ * * これにより品質を維持しながら不要な計算を削減。 */ +/** + * @description 三次ベジェ曲線を適応的に二次ベジェ曲線群に変換する関数 + * Function to adaptively convert cubic bezier to quadratic bezier segments + */ export { execute as adaptiveCubicToQuad } from "./usecase/BezierConverterAdaptiveCubicToQuadUseCase"; + +/** + * @description 二次ベジェ曲線セグメントのインターフェース + * Interface for quadratic bezier curve segment + */ export type { IQuadraticSegment } from "../interface/IQuadraticSegment"; diff --git a/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.test.ts b/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.test.ts index c31e4d84..42a04cce 100644 --- a/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.test.ts +++ b/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { execute, calculateAdaptiveThreshold } from "./BezierConverterAdaptiveCubicToQuadUseCase"; +import { execute } from "./BezierConverterAdaptiveCubicToQuadUseCase"; describe("BezierConverterAdaptiveCubicToQuadUseCase", () => { @@ -83,38 +83,3 @@ describe("BezierConverterAdaptiveCubicToQuadUseCase", () => } }); }); - -describe("calculateAdaptiveThreshold", () => -{ - it("should return smaller threshold for larger scale", () => - { - const threshold1 = calculateAdaptiveThreshold(1.0); - const threshold2 = calculateAdaptiveThreshold(2.0); - - expect(threshold2).toBeLessThan(threshold1); - }); - - it("should return larger threshold for smaller scale", () => - { - const threshold1 = calculateAdaptiveThreshold(1.0); - const threshold2 = calculateAdaptiveThreshold(0.5); - - expect(threshold2).toBeGreaterThan(threshold1); - }); - - it("should clamp to minimum threshold", () => - { - // 非常に大きなスケールでも最小値を下回らない - // 最小値は0.0625(0.25px squared) - const threshold = calculateAdaptiveThreshold(100.0); - expect(threshold).toBeGreaterThanOrEqual(0.0625); - }); - - it("should clamp to maximum threshold", () => - { - // 非常に小さなスケールでも最大値を超えない - // 最大値は4.0(2px squared) - const threshold = calculateAdaptiveThreshold(0.01); - expect(threshold).toBeLessThanOrEqual(4.0); - }); -}); diff --git a/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.ts b/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.ts index 0072c066..aecbeb74 100644 --- a/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.ts +++ b/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.ts @@ -32,7 +32,7 @@ const MAX_RECURSION_DEPTH = 8; * @param {IPoint} p1 - 制御点1 * @param {IPoint} p2 - 制御点2 * @param {IPoint} p3 - 終点 - * @param {number} flatnessThreshold - フラットネス閾値(オプション) + * @param {number} flatness_threshold - フラットネス閾値(オプション) * @return {IQuadraticSegment[]} 二次ベジェ曲線のセグメント配列 */ export const execute = ( @@ -40,12 +40,21 @@ export const execute = ( p1: IPoint, p2: IPoint, p3: IPoint, - flatnessThreshold: number = DEFAULT_FLATNESS_THRESHOLD + flatness_threshold: number = DEFAULT_FLATNESS_THRESHOLD ): IQuadraticSegment[] => { const result: IQuadraticSegment[] = []; - // 再帰的に分割を行う内部関数 + /** + * @description 再帰的に三次ベジェ曲線を分割し、二次ベジェ曲線に近似する内部関数 + * Internal recursive function that subdivides cubic bezier and approximates with quadratic bezier + * @param {IPoint} start - 始点 + * @param {IPoint} ctrl1 - 制御点1 + * @param {IPoint} ctrl2 - 制御点2 + * @param {IPoint} end - 終点 + * @param {number} depth - 現在の再帰深度 + * @return {void} + */ const subdivide = ( start: IPoint, ctrl1: IPoint, @@ -58,7 +67,7 @@ export const execute = ( const flatness = calculateFlatness(start, ctrl1, ctrl2, end); // フラットネスが閾値以下、または最大深度に達した場合は近似 - if (flatness <= flatnessThreshold || depth >= MAX_RECURSION_DEPTH) { + if (flatness <= flatness_threshold || depth >= MAX_RECURSION_DEPTH) { // 三次ベジェを二次ベジェに近似 // WebGL版と同じ: 分割後は単純に2つの制御点の中点を使用 const ctrl: IPoint = { @@ -99,27 +108,3 @@ export const execute = ( return result; }; - -/** - * @description スケールに応じたフラットネス閾値を計算 - * Calculate flatness threshold based on scale - * - * ズームレベルが高い場合は高品質な近似が必要。 - * スケール = sqrt(matrix[0]^2 + matrix[1]^2) などで計算可能。 - * - * @param {number} scale - 現在のスケール - * @return {number} 調整されたフラットネス閾値 - */ -export const calculateAdaptiveThreshold = (scale: number): number => { - // スケールが大きい場合は閾値を小さくして高品質に - // スケールが小さい場合は閾値を大きくしてパフォーマンス優先 - const baseThreshold = DEFAULT_FLATNESS_THRESHOLD; - - // スケールの逆数に比例した閾値 - // 最小値と最大値を設定して極端な値を防ぐ - const adjustedThreshold = baseThreshold / (scale * scale); - - // 閾値の範囲を制限(0.0625〜4.0) - // 0.0625 = 0.25px squared, 4.0 = 2px squared - return Math.max(0.0625, Math.min(4.0, adjustedThreshold)); -}; diff --git a/packages/webgpu/src/Blend.ts b/packages/webgpu/src/Blend.ts index 0ddf00c2..e0ae0925 100644 --- a/packages/webgpu/src/Blend.ts +++ b/packages/webgpu/src/Blend.ts @@ -3,13 +3,33 @@ import type { IBlendState } from "./interface/IBlendState"; export type { IBlendState }; +/** + * @description 現在のブレンドモード + * The current blend mode used for rendering + * + * @type {IBlendMode} + */ export let $currentBlendMode: IBlendMode = "normal"; +/** + * @description 現在のブレンドモードを設定する + * Set the current blend mode + * + * @param {IBlendMode} blend_mode - ブレンドモード / blend mode to apply + * @return {void} + */ export const $setCurrentBlendMode = (blend_mode: IBlendMode): void => { $currentBlendMode = blend_mode; }; +/** + * @description 指定されたブレンドモードに対応するWebGPUブレンドステートを返す + * Returns the WebGPU blend state configuration for the given blend mode + * + * @param {IBlendMode} mode - ブレンドモード / blend mode + * @return {IBlendState} + */ export const $getBlendState = (mode: IBlendMode): IBlendState => { switch (mode) { diff --git a/packages/webgpu/src/Blend/BlendInstancedManager.ts b/packages/webgpu/src/Blend/BlendInstancedManager.ts index 8d9438d7..6aad5917 100644 --- a/packages/webgpu/src/Blend/BlendInstancedManager.ts +++ b/packages/webgpu/src/Blend/BlendInstancedManager.ts @@ -8,28 +8,37 @@ import { $context } from "../WebGPUUtil"; /** * @description シンプルなブレンドモード(インスタンス描画可能) + * Simple blend modes that support instanced rendering + * @type {ReadonlySet} */ -const SIMPLE_BLEND_MODES: ReadonlySet = new Set([ +const $SIMPLE_BLEND_MODES: ReadonlySet = new Set([ "normal", "layer", "add", "screen", "alpha", "erase", "copy" ]); /** * @description 複雑なブレンドモード描画キュー + * Queue for complex blend mode draw items + * @type {IComplexBlendItem[]} */ const $complexBlendQueue: IComplexBlendItem[] = []; /** * @description Float32Array(8) プール(color_transform 用) + * Object pool for Float32Array(8) used by color transforms + * @type {Float32Array[]} */ const $ct8Pool: Float32Array[] = []; /** * @description Float32Array(9) プール(matrix 用) + * Object pool for Float32Array(9) used by matrices + * @type {Float32Array[]} */ const $m9Pool: Float32Array[] = []; /** * @description 複雑なブレンドモードの描画キューを取得 + * Returns the queue of complex blend mode draw items * @return {IComplexBlendItem[]} */ export const getComplexBlendQueue = (): IComplexBlendItem[] => @@ -38,7 +47,8 @@ export const getComplexBlendQueue = (): IComplexBlendItem[] => }; /** - * @description 複雑なブレンドモードの描画キューをクリア + * @description 複雑なブレンドモードの描画キューをクリアし、プールへ返却する + * Clears the complex blend queue and returns arrays to their pools * @return {void} */ export const clearComplexBlendQueue = (): void => @@ -54,37 +64,40 @@ export const clearComplexBlendQueue = (): void => /** * @description インスタンスシェーダーマネージャーのキャッシュ - * @private + * Cache map for instanced shader managers + * @type {Map} */ -const shaderManagers = new Map(); +const $shaderManagers = new Map(); /** - * @description インスタンスシェーダーマネージャーを取得 + * @description インスタンスシェーダーマネージャーを取得(なければ生成) + * Gets or creates the instanced shader manager * @return {ShaderInstancedManager} */ export const getInstancedShaderManager = (): ShaderInstancedManager => { const key = "blend_instanced"; - if (!shaderManagers.has(key)) { - shaderManagers.set(key, new ShaderInstancedManager()); + if (!$shaderManagers.has(key)) { + $shaderManagers.set(key, new ShaderInstancedManager()); } - return shaderManagers.get(key)!; + return $shaderManagers.get(key)!; }; /** - * @description DisplayObject単体の描画をインスタンス配列に追加 - * @param {Node} node - * @param {number} x_min - * @param {number} y_min - * @param {number} x_max - * @param {number} y_max - * @param {Float32Array} color_transform - * @param {Float32Array} matrix - * @param {string} blend_mode - * @param {number} viewport_width - * @param {number} viewport_height - * @param {number} render_max_size - * @param {number} global_alpha + * @description DisplayObject単体の描画をインスタンス配列に追加する + * Adds a single DisplayObject's draw data to the instanced array + * @param {Node} node - テクスチャアトラスノード / Texture atlas node + * @param {number} x_min - バウンディングボックス左端 / Bounding box left edge + * @param {number} y_min - バウンディングボックス上端 / Bounding box top edge + * @param {number} x_max - バウンディングボックス右端 / Bounding box right edge + * @param {number} y_max - バウンディングボックス下端 / Bounding box bottom edge + * @param {Float32Array} color_transform - カラートランスフォーム配列 / Color transform array + * @param {Float32Array} matrix - 変換行列配列 / Transformation matrix array + * @param {string} blend_mode - ブレンドモード名 / Blend mode name + * @param {number} viewport_width - ビューポート幅 / Viewport width + * @param {number} viewport_height - ビューポート高さ / Viewport height + * @param {number} render_max_size - レンダーテクスチャ最大サイズ / Render texture max size + * @param {number} global_alpha - グローバルアルファ値 / Global alpha value * @return {void} */ export const addDisplayObjectToInstanceArray = ( @@ -112,7 +125,7 @@ export const addDisplayObjectToInstanceArray = ( const ct6 = color_transform[6] / 255; const ct7 = 0; - if (SIMPLE_BLEND_MODES.has(blend_mode)) { + if ($SIMPLE_BLEND_MODES.has(blend_mode)) { // ブレンドモードまたはアトラスインデックスが変わった場合 if ($currentBlendMode !== blend_mode || $getCurrentAtlasIndex() !== node.index) { // 異なるブレンドモード/アトラスになるので、切り替え前にバッチを描画 diff --git a/packages/webgpu/src/Blend/usecase/BlendApplyComplexBlendUseCase.ts b/packages/webgpu/src/Blend/usecase/BlendApplyComplexBlendUseCase.ts index 8c7e5c5f..4f4c4ecc 100644 --- a/packages/webgpu/src/Blend/usecase/BlendApplyComplexBlendUseCase.ts +++ b/packages/webgpu/src/Blend/usecase/BlendApplyComplexBlendUseCase.ts @@ -4,11 +4,15 @@ import { ShaderSource } from "../../Shader/ShaderSource"; /** * @description プリアロケートされた uniform データ (12 floats = 48 bytes) + * Pre-allocated uniform data array (12 floats = 48 bytes) + * @type {Float32Array} */ const $uniform12 = new Float32Array(12); /** * @description プリアロケートされた BindGroupEntry 配列 (4 bindings) + * Pre-allocated BindGroupEntry array (4 bindings) + * @type {GPUBindGroupEntry[]} */ const $entries4: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, @@ -18,21 +22,28 @@ const $entries4: GPUBindGroupEntry[] = [ ]; /** - * @description 複雑なブレンドモードを適用 + * @description 複雑なブレンドモードを適用し、ブレンド結果のアタッチメントを返す + * Applies a complex blend mode and returns the resulting attachment + * @param {IAttachmentObject} src_attachment - ソースアタッチメント / Source attachment + * @param {IAttachmentObject} dst_attachment - デスティネーションアタッチメント / Destination attachment + * @param {string} blend_mode - ブレンドモード名 / Blend mode name + * @param {Float32Array} color_transform - カラートランスフォーム配列 / Color transform array + * @param {IFilterConfig} config - フィルター設定 / Filter configuration + * @return {IAttachmentObject} */ export const execute = ( - srcAttachment: IAttachmentObject, - dstAttachment: IAttachmentObject, - blendMode: string, - colorTransform: Float32Array, + src_attachment: IAttachmentObject, + dst_attachment: IAttachmentObject, + blend_mode: string, + color_transform: Float32Array, config: IFilterConfig ): IAttachmentObject => { const { device, commandEncoder, frameBufferManager, pipelineManager, textureManager } = config; // 出力サイズは両方の大きい方を使用 - const width = Math.max(srcAttachment.width, dstAttachment.width); - const height = Math.max(srcAttachment.height, dstAttachment.height); + const width = Math.max(src_attachment.width, dst_attachment.width); + const height = Math.max(src_attachment.height, dst_attachment.height); // 出力アタッチメントを作成 const destAttachment = frameBufferManager.createTemporaryAttachment(width, height); @@ -42,9 +53,9 @@ export const execute = ( const bindGroupLayout = pipelineManager.getBindGroupLayout("complex_blend"); if (!pipeline || !bindGroupLayout) { - console.error(`[WebGPU ComplexBlend] Pipeline not found for blend mode: ${blendMode}`); + console.error(`[WebGPU ComplexBlend] Pipeline not found for blend mode: ${blend_mode}`); // フォールバック: srcをそのまま返す - return srcAttachment; + return src_attachment; } // サンプラーを作成 @@ -55,15 +66,15 @@ export const execute = ( // addColor: vec4 (16 bytes) // blendMode: f32 + padding: vec3 (16 bytes) // Total: 48 bytes - const blendModeIndex = ShaderSource.getBlendModeIndex(blendMode); - $uniform12[0] = colorTransform[0]; - $uniform12[1] = colorTransform[1]; - $uniform12[2] = colorTransform[2]; - $uniform12[3] = colorTransform[3]; - $uniform12[4] = colorTransform[4]; - $uniform12[5] = colorTransform[5]; - $uniform12[6] = colorTransform[6]; - $uniform12[7] = colorTransform[7]; + const blendModeIndex = ShaderSource.getBlendModeIndex(blend_mode); + $uniform12[0] = color_transform[0]; + $uniform12[1] = color_transform[1]; + $uniform12[2] = color_transform[2]; + $uniform12[3] = color_transform[3]; + $uniform12[4] = color_transform[4]; + $uniform12[5] = color_transform[5]; + $uniform12[6] = color_transform[6]; + $uniform12[7] = color_transform[7]; $uniform12[8] = blendModeIndex; $uniform12[9] = 0; $uniform12[10] = 0; @@ -82,8 +93,8 @@ export const execute = ( // バインドグループを作成 ($entries4[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries4[1].resource = sampler; - $entries4[2].resource = dstAttachment.texture!.view; - $entries4[3].resource = srcAttachment.texture!.view; + $entries4[2].resource = dst_attachment.texture!.view; + $entries4[3].resource = src_attachment.texture!.view; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries4 diff --git a/packages/webgpu/src/BufferManager.ts b/packages/webgpu/src/BufferManager.ts index 89b93aa8..daea9045 100644 --- a/packages/webgpu/src/BufferManager.ts +++ b/packages/webgpu/src/BufferManager.ts @@ -11,9 +11,8 @@ import { execute as bufferManagerCreateIndirectBufferService } from "./BufferMan import { execute as updateIndirectBuffer } from "./BufferManager/service/BufferManagerUpdateIndirectBufferService"; /** - * @description Dynamic Uniform Buffer Allocator - * 1フレーム内の全fill uniform データを1本の大バッファにサブアロケートし、 - * BindGroup作成を1回に削減する。 + * @description 動的Uniformバッファアロケータ。1フレーム内の全uniformデータを1本の大バッファにサブアロケートし、BindGroup作成を1回に削減 + * Dynamic Uniform Buffer Allocator. Sub-allocates all uniform data within a frame into a single large buffer, reducing BindGroup creation to once */ export class DynamicUniformAllocator { @@ -27,6 +26,12 @@ export class DynamicUniformAllocator private stagingFloat32: Float32Array; private dirtyEnd: number = 0; + /** + * @description コンストラクタ。GPUデバイスとバッファ容量を設定 + * Constructor. Sets up GPU device and buffer capacity + * @param {GPUDevice} device - WebGPUデバイス + * @param {number} capacity - 初期バッファ容量(バイト単位、デフォルト: 65536) + */ constructor (device: GPUDevice, capacity: number = 65536) { this.device = device; @@ -36,8 +41,9 @@ export class DynamicUniformAllocator } /** - * @description フレーム開始時にオフセットをリセット - * 前フレームの旧バッファを安全に破棄(submit済みのため) + * @description フレーム開始時にオフセットをリセットし、前フレームの旧バッファを安全に破棄 + * Reset offset at frame start and safely destroy old buffers from previous frame + * @return {void} */ resetFrame (): void { @@ -52,6 +58,8 @@ export class DynamicUniformAllocator /** * @description バッファを取得(遅延生成) + * Get buffer with lazy initialization + * @return {GPUBuffer} GPUバッファ */ getBuffer (): GPUBuffer { @@ -65,10 +73,10 @@ export class DynamicUniformAllocator } /** - * @description uniform データをCPUステージングバッファにコピーし、アライメント済みオフセットを返す - * 実際のGPU書き込みはflush()で一括実行される - * @param data - 書き込むデータ - * @return アライメント済みオフセット(バイト単位) + * @description uniformデータをCPUステージングバッファにコピーし、アライメント済みオフセットを返す。実際のGPU書き込みはflush()で一括実行 + * Copy uniform data to CPU staging buffer and return aligned offset. Actual GPU write is batched in flush() + * @param {Float32Array} data - 書き込むデータ + * @return {number} アライメント済みオフセット(バイト単位) */ allocate (data: Float32Array): number { @@ -118,8 +126,9 @@ export class DynamicUniformAllocator } /** - * @description ステージングバッファの内容をGPUバッファに一括書き込み - * submit前に1回だけ呼び出す + * @description ステージングバッファの内容をGPUバッファに一括書き込み。submit前に1回だけ呼び出す + * Flush staging buffer content to GPU buffer in bulk. Call once before submit + * @return {void} */ flush (): void { @@ -129,6 +138,11 @@ export class DynamicUniformAllocator } } + /** + * @description バッファを破棄してリソースを解放 + * Dispose buffers and release resources + * @return {void} + */ dispose (): void { if (this.buffer) { @@ -143,6 +157,10 @@ export class DynamicUniformAllocator } } +/** + * @description GPUバッファの管理クラス。頂点・ユニフォーム・ストレージ・インダイレクトバッファのプール管理と再利用を提供 + * GPU buffer management class. Provides pooling and reuse for vertex, uniform, storage, and indirect buffers + */ export class BufferManager { private device: GPUDevice; @@ -160,6 +178,11 @@ export class BufferManager private frameUniformPoolBuffers: GPUBuffer[]; readonly dynamicUniform: DynamicUniformAllocator; + /** + * @description コンストラクタ。GPUデバイスを設定し、各種バッファプールを初期化 + * Constructor. Sets up GPU device and initializes buffer pools + * @param {GPUDevice} device - WebGPUデバイス + */ constructor (device: GPUDevice) { this.device = device; @@ -178,6 +201,13 @@ export class BufferManager this.dynamicUniform = new DynamicUniformAllocator(device); } + /** + * @description 名前付き頂点バッファを作成し、初期データを書き込む + * Create a named vertex buffer and write initial data + * @param {string} name - バッファ名 + * @param {Float32Array} data - 頂点データ + * @return {GPUBuffer} 作成された頂点バッファ + */ createVertexBuffer (name: string, data: Float32Array): GPUBuffer { const buffer = this.device.createBuffer({ @@ -193,6 +223,13 @@ export class BufferManager return buffer; } + /** + * @description 名前付きユニフォームバッファを作成 + * Create a named uniform buffer + * @param {string} name - バッファ名 + * @param {number} size - バッファサイズ(バイト単位) + * @return {GPUBuffer} 作成されたユニフォームバッファ + */ createUniformBuffer (name: string, size: number): GPUBuffer { const buffer = this.device.createBuffer({ @@ -204,6 +241,13 @@ export class BufferManager return buffer; } + /** + * @description 名前付きユニフォームバッファのデータを更新 + * Update data of a named uniform buffer + * @param {string} name - バッファ名 + * @param {Float32Array} data - 書き込むデータ + * @return {void} + */ updateUniformBuffer (name: string, data: Float32Array): void { const buffer = this.uniformBuffers.get(name); @@ -212,44 +256,84 @@ export class BufferManager } } + /** + * @description 名前で頂点バッファを取得 + * Get vertex buffer by name + * @param {string} name - バッファ名 + * @return {GPUBuffer | undefined} 頂点バッファ、存在しない場合はundefined + */ getVertexBuffer (name: string): GPUBuffer | undefined { return this.vertexBuffers.get(name); } + /** + * @description 名前でユニフォームバッファを取得 + * Get uniform buffer by name + * @param {string} name - バッファ名 + * @return {GPUBuffer | undefined} ユニフォームバッファ、存在しない場合はundefined + */ getUniformBuffer (name: string): GPUBuffer | undefined { return this.uniformBuffers.get(name); } + /** + * @description 矩形の頂点データを作成 + * Create rect vertices data + * @param {number} x - X座標 + * @param {number} y - Y座標 + * @param {number} width - 幅 + * @param {number} height - 高さ + * @return {Float32Array} 矩形の頂点データ + */ createRectVertices (x: number, y: number, width: number, height: number): Float32Array { return bufferManagerCreateRectVerticesService(x, y, width, height); } - acquireVertexBuffer (requiredSize: number, data?: Float32Array): GPUBuffer + /** + * @description プールから頂点バッファを取得(または新規作成) + * Acquire vertex buffer from pool or create new one + * @param {number} required_size - 必要なバイトサイズ + * @param {Float32Array} [data] - 初期データ + * @return {GPUBuffer} 取得された頂点バッファ + */ + acquireVertexBuffer (required_size: number, data?: Float32Array): GPUBuffer { const buffer = bufferManagerAcquireVertexBufferUseCase( this.device, this.vertexBufferBuckets, - requiredSize, + required_size, data ); this.frameVertexPoolBuffers.push(buffer); return buffer; } + /** + * @description 頂点バッファをプールに返却 + * Release vertex buffer back to pool + * @param {GPUBuffer} buffer - 返却するバッファ + * @return {void} + */ releaseVertexBuffer (buffer: GPUBuffer): void { bufferManagerReleaseVertexBufferService(this.vertexBufferBuckets, buffer); } - acquireUniformBuffer (requiredSize: number): GPUBuffer + /** + * @description プールからユニフォームバッファを取得(または新規作成) + * Acquire uniform buffer from pool or create new one + * @param {number} required_size - 必要なバイトサイズ + * @return {GPUBuffer} 取得されたユニフォームバッファ + */ + acquireUniformBuffer (required_size: number): GPUBuffer { const buffer = bufferManagerAcquireUniformBufferUseCase( this.device, this.uniformBufferBuckets, - requiredSize + required_size ); this.frameUniformPoolBuffers.push(buffer); return buffer; @@ -257,24 +341,36 @@ export class BufferManager /** * @description Uniform Bufferの取得と書き込みを一括で行うヘルパー - * acquireUniformBuffer + writeBuffer の2ステップを1呼び出しに統合 - * @param data - 書き込むデータ - * @param byteLength - 書き込みバイト数(省略時はdata.byteLength) - * @return GPUBuffer + * Helper to acquire and write uniform buffer in one call, combining acquireUniformBuffer + writeBuffer + * @param {Float32Array} data - 書き込むデータ + * @param {number} [byte_length] - 書き込みバイト数(省略時はdata.byteLength) + * @return {GPUBuffer} 取得されたユニフォームバッファ */ - acquireAndWriteUniformBuffer (data: Float32Array, byteLength?: number): GPUBuffer + acquireAndWriteUniformBuffer (data: Float32Array, byte_length?: number): GPUBuffer { - const writeBytes = byteLength ?? data.byteLength; + const writeBytes = byte_length ?? data.byteLength; const buffer = this.acquireUniformBuffer(writeBytes); this.device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, writeBytes); return buffer; } + /** + * @description ユニフォームバッファをプールに返却 + * Release uniform buffer back to pool + * @param {GPUBuffer} buffer - 返却するバッファ + * @return {void} + */ releaseUniformBuffer (buffer: GPUBuffer): void { bufferManagerReleaseUniformBufferService(this.uniformBufferBuckets, buffer); } + /** + * @description 名前指定でバッファを破棄(頂点・ユニフォーム両方) + * Destroy buffer by name (both vertex and uniform) + * @param {string} name - バッファ名 + * @return {void} + */ destroyBuffer (name: string): void { const vertexBuffer = this.vertexBuffers.get(name); @@ -290,6 +386,11 @@ export class BufferManager } } + /** + * @description 全バッファを破棄してリソースを解放 + * Dispose all buffers and release resources + * @return {void} + */ dispose (): void { for (const buffer of this.vertexBuffers.values()) { @@ -347,6 +448,11 @@ export class BufferManager this.dynamicUniform.dispose(); } + /** + * @description フレーム内で使用したバッファをクリアし、プールに返却 + * Clear frame buffers and return them to pool + * @return {void} + */ clearFrameBuffers (): void { for (const buffer of this.vertexBuffers.values()) { @@ -387,6 +493,11 @@ export class BufferManager } } + /** + * @description 全てのStorage Bufferを未使用状態に戻す + * Mark all storage buffers as not in use + * @return {void} + */ releaseAllStorageBuffers (): void { for (const entry of this.storageBufferPool) { @@ -394,82 +505,124 @@ export class BufferManager } } - acquireStorageBuffer (requiredSize: number): GPUBuffer + /** + * @description プールからStorage Bufferを取得(または新規作成) + * Acquire storage buffer from pool or create new one + * @param {number} required_size - 必要なバイトサイズ + * @return {GPUBuffer} 取得されたStorage Buffer + */ + acquireStorageBuffer (required_size: number): GPUBuffer { return bufferManagerAcquireStorageBufferUseCase( this.device, this.storageBufferPool, - requiredSize, + required_size, this.frameNumber ); } + /** + * @description Storage Bufferをプールに返却 + * Release storage buffer back to pool + * @param {GPUBuffer} buffer - 返却するバッファ + * @return {void} + */ releaseStorageBuffer (buffer: GPUBuffer): void { releaseStorageBufferUseCase(this.storageBufferPool, buffer); } + /** + * @description Storage Bufferにデータを書き込む + * Write data to storage buffer + * @param {GPUBuffer} buffer - 書き込み先バッファ + * @param {Float32Array | Uint32Array} data - 書き込むデータ + * @return {void} + */ writeStorageBuffer (buffer: GPUBuffer, data: Float32Array | Uint32Array): void { this.device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, data.byteLength); } + /** + * @description Indirect Bufferを取得または作成(シングルトン) + * Get or create indirect buffer (singleton) + * @param {number} vertex_count - 頂点数 + * @param {number} instance_count - インスタンス数 + * @param {number} first_vertex - 開始頂点インデックス + * @param {number} first_instance - 開始インスタンスインデックス + * @return {GPUBuffer} Indirect Buffer + */ getOrCreateIndirectBuffer ( - vertexCount: number, - instanceCount: number, - firstVertex: number = 0, - firstInstance: number = 0 + vertex_count: number, + instance_count: number, + first_vertex: number = 0, + first_instance: number = 0 ): GPUBuffer { if (!this.indirectBuffer) { this.indirectBuffer = bufferManagerCreateIndirectBufferService( this.device, - vertexCount, - instanceCount, - firstVertex, - firstInstance + vertex_count, + instance_count, + first_vertex, + first_instance ); } else { updateIndirectBuffer( this.device, this.indirectBuffer, - vertexCount, - instanceCount, - firstVertex, - firstInstance + vertex_count, + instance_count, + first_vertex, + first_instance ); } return this.indirectBuffer; } + /** + * @description 新しいIndirect Bufferを作成(プールから再利用または新規作成) + * Create new indirect buffer (reuse from pool or create new) + * @param {number} vertex_count - 頂点数 + * @param {number} instance_count - インスタンス数 + * @param {number} first_vertex - 開始頂点インデックス + * @param {number} first_instance - 開始インスタンスインデックス + * @return {GPUBuffer} 作成されたIndirect Buffer + */ createIndirectBuffer ( - vertexCount: number, - instanceCount: number, - firstVertex: number = 0, - firstInstance: number = 0 + vertex_count: number, + instance_count: number, + first_vertex: number = 0, + first_instance: number = 0 ): GPUBuffer { let buffer = this.indirectBufferPool.pop(); if (buffer) { updateIndirectBuffer( this.device, buffer, - vertexCount, - instanceCount, - firstVertex, - firstInstance + vertex_count, + instance_count, + first_vertex, + first_instance ); } else { buffer = bufferManagerCreateIndirectBufferService( this.device, - vertexCount, - instanceCount, - firstVertex, - firstInstance + vertex_count, + instance_count, + first_vertex, + first_instance ); } this.frameIndirectBuffers.push(buffer); return buffer; } + /** + * @description 単位矩形(0,0,1,1)の頂点バッファを取得(遅延生成) + * Get unit rect (0,0,1,1) vertex buffer with lazy creation + * @return {GPUBuffer} 単位矩形の頂点バッファ + */ getUnitRectBuffer (): GPUBuffer { if (!this.unitRectBuffer) { @@ -485,6 +638,11 @@ export class BufferManager return this.unitRectBuffer; } + /** + * @description 現在のフレーム番号を取得 + * Get current frame number + * @return {number} フレーム番号 + */ getFrameNumber (): number { return this.frameNumber; diff --git a/packages/webgpu/src/BufferManager/service/BufferManagerReleaseUniformBufferService.ts b/packages/webgpu/src/BufferManager/service/BufferManagerReleaseUniformBufferService.ts index ea30eb88..c04b8bad 100644 --- a/packages/webgpu/src/BufferManager/service/BufferManagerReleaseUniformBufferService.ts +++ b/packages/webgpu/src/BufferManager/service/BufferManagerReleaseUniformBufferService.ts @@ -4,7 +4,7 @@ * @type {number} * @const */ -const MAX_BUCKET_SIZE: number = 32; +const $MAX_BUCKET_SIZE: number = 32; /** * @description ユニフォームバッファをプールに返却 @@ -29,7 +29,7 @@ export const execute = ( buckets.set(size, bucket); } - if (bucket.length >= MAX_BUCKET_SIZE) { + if (bucket.length >= $MAX_BUCKET_SIZE) { // バケットが満杯の場合、このバッファを破棄 buffer.destroy(); return; diff --git a/packages/webgpu/src/BufferManager/service/BufferManagerReleaseVertexBufferService.ts b/packages/webgpu/src/BufferManager/service/BufferManagerReleaseVertexBufferService.ts index 359e5922..a546046b 100644 --- a/packages/webgpu/src/BufferManager/service/BufferManagerReleaseVertexBufferService.ts +++ b/packages/webgpu/src/BufferManager/service/BufferManagerReleaseVertexBufferService.ts @@ -4,7 +4,7 @@ * @type {number} * @const */ -const MAX_BUCKET_SIZE: number = 32; +const $MAX_BUCKET_SIZE: number = 32; /** * @description 頂点バッファをプールに返却 @@ -29,7 +29,7 @@ export const execute = ( buckets.set(size, bucket); } - if (bucket.length >= MAX_BUCKET_SIZE) { + if (bucket.length >= $MAX_BUCKET_SIZE) { // バケットが満杯の場合、このバッファを破棄 buffer.destroy(); return; diff --git a/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.ts b/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.ts index 1ddec2ad..43fcf90f 100644 --- a/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.ts +++ b/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.ts @@ -8,6 +8,7 @@ * @param {number} instance_count - インスタンス数 * @param {number} first_vertex - 開始頂点インデックス * @param {number} first_instance - 開始インスタンスインデックス + * @return {void} */ export const execute = ( device: GPUDevice, diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.ts index 6304374c..6cb69ac7 100644 --- a/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.ts +++ b/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.ts @@ -9,6 +9,7 @@ import type { IPooledStorageBuffer } from "../../interface/IStorageBufferConfig" * @param {IPooledStorageBuffer[]} pool - Storage Bufferプール * @param {number} current_frame - 現在のフレーム番号 * @param {number} max_age - 最大保持フレーム数 + * @return {void} */ export const execute = ( pool: IPooledStorageBuffer[], diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.ts index da94118e..02734f59 100644 --- a/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.ts +++ b/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.ts @@ -6,6 +6,7 @@ import type { IPooledStorageBuffer } from "../../interface/IStorageBufferConfig" * * @param {IPooledStorageBuffer[]} pool - Storage Bufferプール * @param {GPUBuffer} buffer - 返却するバッファ + * @return {void} */ export const execute = ( pool: IPooledStorageBuffer[], diff --git a/packages/webgpu/src/Context/service/ContextComputeBitmapMatrixService.ts b/packages/webgpu/src/Context/service/ContextComputeBitmapMatrixService.ts index 1d0f48b2..1b4cd789 100644 --- a/packages/webgpu/src/Context/service/ContextComputeBitmapMatrixService.ts +++ b/packages/webgpu/src/Context/service/ContextComputeBitmapMatrixService.ts @@ -1,3 +1,10 @@ +/** + * @description ビットマップ行列とコンテキスト行列からテクスチャマッピング用の逆行列を計算する + * Computes the inverse matrix for texture mapping from bitmap and context matrices + * @param {Float32Array} bitmap_matrix ビットマップ変換行列 / Bitmap transformation matrix + * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix + * @return {Float32Array} 列優先形式の3x3逆行列 / Column-major 3x3 inverse matrix + */ export const execute = (bitmap_matrix: Float32Array, context_matrix: Float32Array): Float32Array => { // ビットマップ行列 [a, b, c, d, tx, ty] const ba = bitmap_matrix[0]; diff --git a/packages/webgpu/src/Context/service/ContextComputeGradientMatrixService.ts b/packages/webgpu/src/Context/service/ContextComputeGradientMatrixService.ts index fb90e4e3..d3142308 100644 --- a/packages/webgpu/src/Context/service/ContextComputeGradientMatrixService.ts +++ b/packages/webgpu/src/Context/service/ContextComputeGradientMatrixService.ts @@ -1,15 +1,23 @@ +/** + * @description グラデーション行列からグラデーション描画用の逆行列とリニアポイントを計算する + * Computes inverse matrix and linear points for gradient rendering from gradient matrix + * @param {Float32Array} gradient_matrix グラデーション変換行列 / Gradient transformation matrix + * @param {Float32Array} _context_matrix コンテキスト変換行列(未使用) / Context transformation matrix (unused) + * @param {number} type グラデーションタイプ (0: linear, 1: radial) / Gradient type (0: linear, 1: radial) + * @return {{ inverseMatrix: Float32Array; linearPoints: Float32Array | null }} 逆行列とリニアポイント / Inverse matrix and linear points + */ export const execute = ( - gradientMatrix: Float32Array, - _contextMatrix: Float32Array, + gradient_matrix: Float32Array, + _context_matrix: Float32Array, type: number ): { inverseMatrix: Float32Array; linearPoints: Float32Array | null } => { // グラデーション行列 - const ga = gradientMatrix[0]; - const gb = gradientMatrix[1]; - const gc = gradientMatrix[2]; - const gd = gradientMatrix[3]; - const gtx = gradientMatrix[4]; - const gty = gradientMatrix[5]; + const ga = gradient_matrix[0]; + const gb = gradient_matrix[1]; + const gc = gradient_matrix[2]; + const gd = gradient_matrix[3]; + const gtx = gradient_matrix[4]; + const gty = gradient_matrix[5]; if (type === 0) { // === Linear gradient === diff --git a/packages/webgpu/src/Context/service/ContextFillSimpleService.ts b/packages/webgpu/src/Context/service/ContextFillSimpleService.ts index b6c48365..397e605c 100644 --- a/packages/webgpu/src/Context/service/ContextFillSimpleService.ts +++ b/packages/webgpu/src/Context/service/ContextFillSimpleService.ts @@ -1,6 +1,20 @@ import type { PipelineManager } from "../../Shader/PipelineManager"; import { $isMaskDrawing, $getMaskStencilReference } from "../../Mask"; +/** + * @description シンプルなフィル描画を実行する + * Executes simple fill rendering + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {GPUBuffer} vertex_buffer 頂点バッファ / Vertex buffer + * @param {number} vertex_count 頂点数 / Vertex count + * @param {GPUBindGroup} bind_group バインドグループ / Bind group + * @param {number} uniform_offset ユニフォームオフセット / Uniform offset + * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target + * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline + * @param {number} _clip_level クリップレベル(未使用) / Clip level (unused) + * @return {void} + */ export const execute = ( render_pass_encoder: GPURenderPassEncoder, pipeline_manager: PipelineManager, diff --git a/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.ts b/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.ts index ce39f071..35ab516a 100644 --- a/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.ts +++ b/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.ts @@ -1,5 +1,16 @@ import type { PipelineManager } from "../../Shader/PipelineManager"; +/** + * @description メインキャンバス向けのステンシル書き込みとフィル描画を2パスで実行する + * Executes two-pass stencil write and fill rendering for the main canvas + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {GPUBuffer} vertex_buffer 頂点バッファ / Vertex buffer + * @param {number} vertex_count 頂点数 / Vertex count + * @param {GPUBindGroup} bind_group バインドグループ / Bind group + * @param {number} uniform_offset ユニフォームオフセット / Uniform offset + * @return {void} + */ export const execute = ( render_pass_encoder: GPURenderPassEncoder, pipeline_manager: PipelineManager, diff --git a/packages/webgpu/src/Context/service/ContextFillWithStencilService.ts b/packages/webgpu/src/Context/service/ContextFillWithStencilService.ts index d6a68ae9..b64cbe5c 100644 --- a/packages/webgpu/src/Context/service/ContextFillWithStencilService.ts +++ b/packages/webgpu/src/Context/service/ContextFillWithStencilService.ts @@ -1,5 +1,16 @@ import type { PipelineManager } from "../../Shader/PipelineManager"; +/** + * @description アトラスターゲット向けのステンシル書き込みとフィル描画を2パスで実行する + * Executes two-pass stencil write and fill rendering for the atlas target + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {GPUBuffer} vertex_buffer 頂点バッファ / Vertex buffer + * @param {number} vertex_count 頂点数 / Vertex count + * @param {GPUBindGroup} bind_group バインドグループ / Bind group + * @param {number} uniform_offset ユニフォームオフセット / Uniform offset + * @return {void} + */ export const execute = ( render_pass_encoder: GPURenderPassEncoder, pipeline_manager: PipelineManager, diff --git a/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts b/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts index 007c6c0a..4f17dd6c 100644 --- a/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts @@ -22,35 +22,85 @@ import { $getMaskStencilReference } from "../../Mask"; +/** + * @description ユニフォームデータの事前確保配列(4要素) + * Pre-allocated uniform data array (4 elements) + */ const $uniform4 = new Float32Array(4); +/** + * @description ユニフォームデータの事前確保配列A(6要素) + * Pre-allocated uniform data array A (6 elements) + */ const $uniform6a = new Float32Array(6); +/** + * @description ユニフォームデータの事前確保配列B(6要素) + * Pre-allocated uniform data array B (6 elements) + */ const $uniform6b = new Float32Array(6); +/** + * @description ユニフォームデータの事前確保配列(8要素) + * Pre-allocated uniform data array (8 elements) + */ const $uniform8 = new Float32Array(8); +/** + * @description ユニフォームデータの事前確保配列(12要素) + * Pre-allocated uniform data array (12 elements) + */ const $uniform12 = new Float32Array(12); +/** + * @description ユニフォームデータの事前確保配列(20要素) + * Pre-allocated uniform data array (20 elements) + */ const $uniform20 = new Float32Array(20); // プリアロケート BindGroup Entry 配列 +/** + * @description バインドグループエントリの事前確保配列 + * Pre-allocated bind group entry array + */ const $entries3: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, { "binding": 1, "resource": null as unknown as GPUSampler }, { "binding": 2, "resource": null as unknown as GPUTextureView } ]; -const SIMPLE_BLEND_MODES: ReadonlySet = new Set([ +/** + * @description シンプルなブレンドモードのセット + * Set of simple blend modes + */ +const $SIMPLE_BLEND_MODES: ReadonlySet = new Set([ "normal", "layer", "add", "screen", "alpha", "erase", "copy" ] as IBlendMode[]); -const Y_FLIP_UNIFORM = new Float32Array([1, -1, 0, 1]); - -const isIdentityColorTransform = (ct: Float32Array): boolean => { +/** + * @description Y軸反転用ユニフォームデータ + * Uniform data for Y-axis flip + */ +const $Y_FLIP_UNIFORM = new Float32Array([1, -1, 0, 1]); + +/** + * @description カラートランスフォームが恒等変換かどうかを判定する + * Checks whether the color transform is an identity transform + * @param {Float32Array} ct カラートランスフォーム配列 / Color transform array + * @return {boolean} 恒等変換の場合true / True if identity transform + */ +const $isIdentityColorTransform = (ct: Float32Array): boolean => { return ct[0] === 1 && ct[1] === 1 && ct[2] === 1 && ct[3] === 1 && ct[4] === 0 && ct[5] === 0 && ct[6] === 0 && ct[7] === 0; }; -const applyColorTransform = ( +/** + * @description フィルター結果にカラートランスフォームを適用する + * Applies color transform to the filter result + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {IAttachmentObject} attachment ソースアタッチメント / Source attachment + * @param {Float32Array} color_transform カラートランスフォーム配列 / Color transform array + * @return {IAttachmentObject} カラートランスフォーム適用後のアタッチメント / Attachment with color transform applied + */ +const $applyColorTransform = ( config: ILocalFilterConfig, attachment: IAttachmentObject, - colorTransform: Float32Array + color_transform: Float32Array ): IAttachmentObject => { const ctAttachment = config.frameBufferManager.createTemporaryAttachment( attachment.width, attachment.height @@ -65,13 +115,13 @@ const applyColorTransform = ( // uniform: mul(vec4) + add(vec4) = 32 bytes // add値は0-255スケールの生値をそのまま渡す(WebGLのフィルターCTパスと同じ) - $uniform8[0] = colorTransform[0]; - $uniform8[1] = colorTransform[1]; - $uniform8[2] = colorTransform[2]; - $uniform8[3] = colorTransform[3]; - $uniform8[4] = colorTransform[4]; - $uniform8[5] = colorTransform[5]; - $uniform8[6] = colorTransform[6]; + $uniform8[0] = color_transform[0]; + $uniform8[1] = color_transform[1]; + $uniform8[2] = color_transform[2]; + $uniform8[3] = color_transform[3]; + $uniform8[4] = color_transform[4]; + $uniform8[5] = color_transform[5]; + $uniform8[6] = color_transform[6]; $uniform8[7] = 0; const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer($uniform8); @@ -98,7 +148,15 @@ const applyColorTransform = ( return ctAttachment; }; -const getTextureFromNode = ( +/** + * @description アトラスノードからテクスチャを取得する + * Extracts texture from an atlas node + * @param {Node} node テクスチャパッカーノード / Texture packer node + * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder + * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager + * @return {IAttachmentObject} 取得したアタッチメント / Extracted attachment + */ +const $getTextureFromNode = ( node: Node, command_encoder: GPUCommandEncoder, frame_buffer_manager: FrameBufferManager @@ -126,19 +184,36 @@ const getTextureFromNode = ( } ); } else { - console.error("[WebGPU Filter] getTextureFromNode: FAILED - missing atlas or textures"); + console.error("[WebGPU Filter] $getTextureFromNode: FAILED - missing atlas or textures"); } return attachment; }; -const isSimpleBlendMode = (blendMode: IBlendMode): boolean => { - return SIMPLE_BLEND_MODES.has(blendMode); +/** + * @description ブレンドモードがシンプルかどうかを判定する + * Checks whether the blend mode is a simple blend mode + * @param {IBlendMode} blend_mode ブレンドモード / Blend mode + * @return {boolean} シンプルブレンドモードの場合true / True if simple blend mode + */ +const $isSimpleBlendMode = (blend_mode: IBlendMode): boolean => { + return $SIMPLE_BLEND_MODES.has(blend_mode); }; -const copyMainAttachmentRegion = ( +/** + * @description メインアタッチメントの矩形領域をコピーする + * Copies a rectangular region from the main attachment + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment + * @param {number} x X座標 / X coordinate + * @param {number} y Y座標 / Y coordinate + * @param {number} width 幅 / Width + * @param {number} height 高さ / Height + * @return {IAttachmentObject} コピーされたアタッチメント / Copied attachment + */ +const $copyMainAttachmentRegion = ( config: ILocalFilterConfig, - mainAttachment: IAttachmentObject, + main_attachment: IAttachmentObject, x: number, y: number, width: number, @@ -151,15 +226,15 @@ const copyMainAttachmentRegion = ( const pipeline = config.pipelineManager.getPipeline("complex_blend_copy"); const bindGroupLayout = config.pipelineManager.getBindGroupLayout("texture_copy"); - if (!pipeline || !bindGroupLayout || !mainAttachment.texture || !dstAttachment.texture) { + if (!pipeline || !bindGroupLayout || !main_attachment.texture || !dstAttachment.texture) { return dstAttachment; } // ユニフォームバッファ: scale (vec2) + offset (vec2) - const scaleX = width / mainAttachment.width; - const scaleY = height / mainAttachment.height; - const offsetX = x / mainAttachment.width; - const offsetY = y / mainAttachment.height; + const scaleX = width / main_attachment.width; + const scaleY = height / main_attachment.height; + const offsetX = x / main_attachment.width; + const offsetY = y / main_attachment.height; $uniform4[0] = scaleX; $uniform4[1] = scaleY; @@ -171,7 +246,7 @@ const copyMainAttachmentRegion = ( ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; - $entries3[2].resource = mainAttachment.texture.view; + $entries3[2].resource = main_attachment.texture.view; const bindGroup = config.device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries3 @@ -192,31 +267,41 @@ const copyMainAttachmentRegion = ( return dstAttachment; }; -const drawBlendResultToMain = ( +/** + * @description ブレンド結果をメインアタッチメントに描画する + * Draws blend result to the main attachment + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {IAttachmentObject} src_attachment ソースアタッチメント / Source attachment + * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment + * @param {number} x X座標 / X coordinate + * @param {number} y Y座標 / Y coordinate + * @return {void} + */ +const $drawBlendResultToMain = ( config: ILocalFilterConfig, - srcAttachment: IAttachmentObject, - mainAttachment: IAttachmentObject, + src_attachment: IAttachmentObject, + main_attachment: IAttachmentObject, x: number, y: number ): void => { // フィルター+複雑なブレンド用のパイプライン(Y軸反転あり)を使用 // MSAA有効時はMSAA版パイプラインを使用してmsaaTextureに描画→texture.viewにresolve - const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; + const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view; const pipelineName = useMsaa ? "filter_complex_blend_output_msaa" : "filter_complex_blend_output"; const pipeline = config.pipelineManager.getPipeline(pipelineName); const bindGroupLayout = config.pipelineManager.getBindGroupLayout("positioned_texture"); - if (!pipeline || !bindGroupLayout || !srcAttachment.texture || !mainAttachment.texture) { + if (!pipeline || !bindGroupLayout || !src_attachment.texture || !main_attachment.texture) { return; } // ユニフォームデータ: offset, size, viewport, padding $uniform8[0] = x; $uniform8[1] = y; - $uniform8[2] = srcAttachment.width; - $uniform8[3] = srcAttachment.height; - $uniform8[4] = mainAttachment.width; - $uniform8[5] = mainAttachment.height; + $uniform8[2] = src_attachment.width; + $uniform8[3] = src_attachment.height; + $uniform8[4] = main_attachment.width; + $uniform8[5] = main_attachment.height; $uniform8[6] = 0; $uniform8[7] = 0; const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer($uniform8); @@ -225,7 +310,7 @@ const drawBlendResultToMain = ( ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; - $entries3[2].resource = srcAttachment.texture.view; + $entries3[2].resource = src_attachment.texture.view; const bindGroup = config.device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries3 @@ -233,8 +318,8 @@ const drawBlendResultToMain = ( // メインアタッチメントへの描画(loadで既存内容を保持) // MSAA有効時はmsaaTextureに描画してtexture.viewにresolve - const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture.view; - const resolveTarget = useMsaa ? mainAttachment.texture.view : null; + const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture.view; + const resolveTarget = useMsaa ? main_attachment.texture.view : null; const renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor( colorView, 0, 0, 0, 0, @@ -249,7 +334,20 @@ const drawBlendResultToMain = ( passEncoder.end(); }; -const drawFilterToMain = ( +/** + * @description フィルター適用結果をメインキャンバスに描画する + * Draws filter result to the main canvas + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {IAttachmentObject} filter_attachment フィルターアタッチメント / Filter attachment + * @param {Float32Array} color_transform カラートランスフォーム配列 / Color transform array + * @param {IBlendMode} blend_mode ブレンドモード / Blend mode + * @param {number} x X座標 / X coordinate + * @param {number} y Y座標 / Y coordinate + * @param {GPUTextureView} _main_texture_view メインテクスチャビュー(未使用) / Main texture view (unused) + * @param {BufferManager} _buffer_manager バッファマネージャ(未使用) / Buffer manager (unused) + * @return {void} + */ +const $drawFilterToMain = ( config: ILocalFilterConfig, filter_attachment: IAttachmentObject, color_transform: Float32Array, @@ -304,7 +402,7 @@ const drawFilterToMain = ( } // シンプルなブレンドモードの場合 - if (isSimpleBlendMode(blend_mode)) { + if ($isSimpleBlendMode(blend_mode)) { // MSAA有効時はMSAA版パイプラインを使用してmsaaTextureに描画→texture.viewにresolve const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; @@ -431,7 +529,7 @@ const drawFilterToMain = ( } else { // 複雑なブレンドモード(multiply, overlay, darken, lighten, hardlight等) // 1. メインアタッチメントから描画先の矩形をコピー - const dstAttachment = copyMainAttachmentRegion( + const dstAttachment = $copyMainAttachmentRegion( config, mainAttachment, drawX, drawY, drawWidth, drawHeight ); @@ -464,7 +562,7 @@ const drawFilterToMain = ( ); // 4. 結果をメインアタッチメントに描画 - drawBlendResultToMain( + $drawBlendResultToMain( config, blendedAttachment, mainAttachment, @@ -478,6 +576,23 @@ const drawFilterToMain = ( } }; +/** + * @description フィルターを適用してメインキャンバスに描画する + * Applies filters and draws the result to the main canvas + * @param {Node} node テクスチャパッカーノード / Texture packer node + * @param {number} width 幅 / Width + * @param {number} height 高さ / Height + * @param {boolean} is_bitmap ビットマップフラグ / Whether the source is a bitmap + * @param {Float32Array} matrix 変換行列 / Transformation matrix + * @param {Float32Array} color_transform カラートランスフォーム配列 / Color transform array + * @param {IBlendMode} blend_mode ブレンドモード / Blend mode + * @param {Float32Array} bounds バウンディングボックス / Bounding box + * @param {Float32Array} params フィルターパラメータ配列 / Filter parameters array + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {GPUTextureView} main_texture_view メインテクスチャビュー / Main texture view + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @return {void} + */ export const execute = ( node: Node, width: number, @@ -497,7 +612,7 @@ export const execute = ( $offset.y = 0; // ノードからテクスチャを取得 - let filterAttachment = getTextureFromNode(node, config.commandEncoder, config.frameBufferManager); + let filterAttachment = $getTextureFromNode(node, config.commandEncoder, config.frameBufferManager); // アトラスのY反転を補正 // WebGPUではアトラスに描画する際にY軸が反転して格納される: @@ -513,7 +628,7 @@ export const execute = ( const sampler = config.textureManager.createSampler("filter_flip_sampler", false); // scale=(1, -1), offset=(0, 1) で UV.y = texCoord.y * (-1) + 1 = 1 - texCoord.y - const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer(Y_FLIP_UNIFORM); + const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer($Y_FLIP_UNIFORM); ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; @@ -939,8 +1054,8 @@ export const execute = ( // ColorTransformが恒等変換でない場合、フィルター結果に適用 // WebGL版と同じ: フィルターチェーン適用後、メイン描画前にColorTransformを適用 - if (!isIdentityColorTransform(color_transform)) { - const ctAttachment = applyColorTransform(config, filterAttachment, color_transform); + if (!$isIdentityColorTransform(color_transform)) { + const ctAttachment = $applyColorTransform(config, filterAttachment, color_transform); config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); filterAttachment = ctAttachment; } @@ -954,7 +1069,7 @@ export const execute = ( const drawX = -offsetX + xMin + matrix[4]; const drawY = -offsetY + yMin + matrix[5]; - drawFilterToMain( + $drawFilterToMain( config, filterAttachment, color_transform, diff --git a/packages/webgpu/src/Context/usecase/ContextBitmapFillUseCase.ts b/packages/webgpu/src/Context/usecase/ContextBitmapFillUseCase.ts index a92aaa51..501550ce 100644 --- a/packages/webgpu/src/Context/usecase/ContextBitmapFillUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextBitmapFillUseCase.ts @@ -9,20 +9,67 @@ import { $getMaskStencilReference } from "../../Mask"; +/** + * @description ビットマップサンプラーのキャッシュ + * Cache for bitmap samplers + */ const $bitmapSamplerCache = new Map(); +/** + * @description ユニフォームデータの事前確保配列(32要素) + * Pre-allocated uniform data array (32 elements) + */ const $uniformData32 = new Float32Array(32); +/** + * @description ステンシルデータの事前確保配列(16要素) + * Pre-allocated stencil data array (16 elements) + */ const $stencilData16 = new Float32Array(16); +/** + * @description ステンシル用動的バインドグループのキャッシュ + * Cached dynamic bind group for stencil operations + */ let $stencilDynamicBindGroup: GPUBindGroup | null = null; +/** + * @description ステンシル用動的バッファのキャッシュ + * Cached dynamic buffer for stencil operations + */ let $stencilDynamicBuffer: GPUBuffer | null = null; +/** + * @description バインドグループエントリの事前確保配列 + * Pre-allocated bind group entry array + */ const $entries3: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, { "binding": 1, "resource": null as unknown as GPUSampler }, { "binding": 2, "resource": null as unknown as GPUTextureView } ]; +/** + * @description ビットマップフィル描画を実行する + * Executes bitmap fill rendering + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {IPath[]} path_vertices パス頂点配列 / Path vertices array + * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix + * @param {Float32Array} fill_style フィルスタイル(RGBA) / Fill style (RGBA) + * @param {Uint8Array} pixels ピクセルデータ / Pixel data + * @param {Float32Array} bitmap_matrix ビットマップ変換行列 / Bitmap transformation matrix + * @param {number} width テクスチャ幅 / Texture width + * @param {number} height テクスチャ高さ / Texture height + * @param {boolean} repeat リピート有無 / Whether to repeat + * @param {boolean} smooth スムーズフィルタ有無 / Whether to use smooth filtering + * @param {number} viewport_width ビューポート幅 / Viewport width + * @param {number} viewport_height ビューポート高さ / Viewport height + * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target + * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline + * @param {number} _clip_level クリップレベル(未使用) / Clip level (unused) + * @return {GPUTexture | null} ビットマップテクスチャまたはnull / Bitmap texture or null + */ export const execute = ( device: GPUDevice, render_pass_encoder: GPURenderPassEncoder, diff --git a/packages/webgpu/src/Context/usecase/ContextBitmapStrokeUseCase.ts b/packages/webgpu/src/Context/usecase/ContextBitmapStrokeUseCase.ts index eaed4048..6a4a3cf6 100644 --- a/packages/webgpu/src/Context/usecase/ContextBitmapStrokeUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextBitmapStrokeUseCase.ts @@ -5,16 +5,51 @@ import { execute as meshBitmapStrokeGenerateUseCase } from "../../Mesh/usecase/M import { execute as contextComputeBitmapMatrixService } from "../service/ContextComputeBitmapMatrixService"; import { $acquireFillTexture, $releaseFillTexture } from "../../FillTexturePool"; +/** + * @description ビットマップサンプラーのキャッシュ + * Cache for bitmap samplers + */ const $bitmapSamplerCache = new Map(); +/** + * @description ユニフォームデータの事前確保配列(32要素) + * Pre-allocated uniform data array (32 elements) + */ const $uniformData32 = new Float32Array(32); +/** + * @description バインドグループエントリの事前確保配列 + * Pre-allocated bind group entry array + */ const $entries3: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, { "binding": 1, "resource": null as unknown as GPUSampler }, { "binding": 2, "resource": null as unknown as GPUTextureView } ]; +/** + * @description ビットマップストローク描画を実行する + * Executes bitmap stroke rendering + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {IPath[]} vertices パス頂点配列 / Path vertices array + * @param {number} thickness ストローク太さ / Stroke thickness + * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix + * @param {Float32Array} stroke_style ストロークスタイル(RGBA) / Stroke style (RGBA) + * @param {Uint8Array} pixels ピクセルデータ / Pixel data + * @param {Float32Array} bitmap_matrix ビットマップ変換行列 / Bitmap transformation matrix + * @param {number} width テクスチャ幅 / Texture width + * @param {number} height テクスチャ高さ / Texture height + * @param {boolean} repeat リピート有無 / Whether to repeat + * @param {boolean} smooth スムーズフィルタ有無 / Whether to use smooth filtering + * @param {number} viewport_width ビューポート幅 / Viewport width + * @param {number} viewport_height ビューポート高さ / Viewport height + * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target + * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline + * @return {GPUTexture | null} ビットマップテクスチャまたはnull / Bitmap texture or null + */ export const execute = ( device: GPUDevice, render_pass_encoder: GPURenderPassEncoder, diff --git a/packages/webgpu/src/Context/usecase/ContextClipUseCase.ts b/packages/webgpu/src/Context/usecase/ContextClipUseCase.ts index c86eb906..bb5f6641 100644 --- a/packages/webgpu/src/Context/usecase/ContextClipUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextClipUseCase.ts @@ -6,8 +6,27 @@ import { execute as meshFillGenerateUseCase } from "../../Mesh/usecase/MeshFillG import { execute as maskUnionMaskService } from "../../Mask/service/MaskUnionMaskService"; import { $clipLevels } from "../../Mask"; +/** + * @description クリップ用ユニフォームデータの事前確保配列(16要素) + * Pre-allocated uniform data array for clipping (16 elements) + */ const $clipUniform16 = new Float32Array(16); +/** + * @description クリップ(マスク)描画を実行する + * Executes clip (mask) rendering + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {IAttachmentObject} current_attachment 現在のアタッチメント / Current attachment + * @param {IPath[]} path_vertices パス頂点配列 / Path vertices array + * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix + * @param {Float32Array} fill_style フィルスタイル(RGBA) / Fill style (RGBA) + * @param {number} global_alpha グローバルアルファ値 / Global alpha value + * @param {boolean} is_main_attachment メインアタッチメントフラグ / Whether this is the main attachment + * @return {void} + */ export const execute = ( device: GPUDevice, render_pass_encoder: GPURenderPassEncoder, diff --git a/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts index 08f4a3c3..8ad48391 100644 --- a/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts @@ -16,24 +16,54 @@ import { execute as filterApplyGradientGlowFilterUseCase } from "../../Filter/Gr import { execute as filterApplyDisplacementMapFilterUseCase } from "../../Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase"; import { execute as blendApplyComplexBlendUseCase } from "../../Blend/usecase/BlendApplyComplexBlendUseCase"; +/** + * @description ユニフォームデータの事前確保配列(4要素) + * Pre-allocated uniform data array (4 elements) + */ const $uniform4 = new Float32Array(4); +/** + * @description ユニフォームデータの事前確保配列(8要素) + * Pre-allocated uniform data array (8 elements) + */ const $uniform8 = new Float32Array(8); +/** + * @description ユニフォームデータの事前確保配列(20要素) + * Pre-allocated uniform data array (20 elements) + */ const $uniform20 = new Float32Array(20); // プリアロケート BindGroup Entry 配列 +/** + * @description バインドグループエントリの事前確保配列 + * Pre-allocated bind group entry array + */ const $entries3: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, { "binding": 1, "resource": null as unknown as GPUSampler }, { "binding": 2, "resource": null as unknown as GPUTextureView } ]; -const SIMPLE_BLEND_MODES: ReadonlySet = new Set([ +/** + * @description シンプルなブレンドモードのセット + * Set of simple blend modes + */ +const $SIMPLE_BLEND_MODES: ReadonlySet = new Set([ "normal", "layer", "add", "screen", "alpha", "erase", "copy" ] as IBlendMode[]); +/** + * @description 恒等カラートランスフォーム + * Identity color transform + */ const $identityColorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); -const isIdentityColorTransform = (ct: Float32Array | null): boolean => { +/** + * @description カラートランスフォームが恒等変換かどうかを判定する + * Checks whether the color transform is an identity transform + * @param {Float32Array | null} ct カラートランスフォーム配列 / Color transform array + * @return {boolean} 恒等変換の場合true / True if identity transform + */ +const $isIdentityColorTransform = (ct: Float32Array | null): boolean => { if (!ct) { return true; } @@ -41,10 +71,18 @@ const isIdentityColorTransform = (ct: Float32Array | null): boolean => { && ct[4] === 0 && ct[5] === 0 && ct[6] === 0 && ct[7] === 0; }; -const applyColorTransform = ( +/** + * @description アタッチメントにカラートランスフォームを適用する + * Applies color transform to an attachment + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {IAttachmentObject} attachment ソースアタッチメント / Source attachment + * @param {Float32Array} color_transform カラートランスフォーム配列 / Color transform array + * @return {IAttachmentObject} カラートランスフォーム適用後のアタッチメント / Attachment with color transform applied + */ +const $applyColorTransform = ( config: ILocalFilterConfig, attachment: IAttachmentObject, - colorTransform: Float32Array + color_transform: Float32Array ): IAttachmentObject => { const ctAttachment = config.frameBufferManager.createTemporaryAttachment( attachment.width, attachment.height @@ -57,13 +95,13 @@ const applyColorTransform = ( return attachment; } - $uniform8[0] = colorTransform[0]; - $uniform8[1] = colorTransform[1]; - $uniform8[2] = colorTransform[2]; - $uniform8[3] = colorTransform[3]; - $uniform8[4] = colorTransform[4]; - $uniform8[5] = colorTransform[5]; - $uniform8[6] = colorTransform[6]; + $uniform8[0] = color_transform[0]; + $uniform8[1] = color_transform[1]; + $uniform8[2] = color_transform[2]; + $uniform8[3] = color_transform[3]; + $uniform8[4] = color_transform[4]; + $uniform8[5] = color_transform[5]; + $uniform8[6] = color_transform[6]; $uniform8[7] = 0; const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer($uniform8); @@ -90,9 +128,20 @@ const applyColorTransform = ( return ctAttachment; }; -const copyRegionToFilterAttachment = ( +/** + * @description ソースアタッチメントの領域をフィルター用アタッチメントにコピーする + * Copies a region from source attachment to a filter attachment + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {IAttachmentObject} src_attachment ソースアタッチメント / Source attachment + * @param {number} x X座標 / X coordinate + * @param {number} y Y座標 / Y coordinate + * @param {number} width 幅 / Width + * @param {number} height 高さ / Height + * @return {IAttachmentObject} コピーされたアタッチメント / Copied attachment + */ +const $copyRegionToFilterAttachment = ( config: ILocalFilterConfig, - srcAttachment: IAttachmentObject, + src_attachment: IAttachmentObject, x: number, y: number, width: number, @@ -105,16 +154,16 @@ const copyRegionToFilterAttachment = ( const pipeline = config.pipelineManager.getPipeline("texture_copy_rgba8"); const bindGroupLayout = config.pipelineManager.getBindGroupLayout("texture_copy"); - if (!pipeline || !bindGroupLayout || !srcAttachment.texture || !dstAttachment.texture) { + if (!pipeline || !bindGroupLayout || !src_attachment.texture || !dstAttachment.texture) { return dstAttachment; } // BlurFilterVertex (yFlipTexCoord=true): // texCoord.y=0(fb上端) → uv.y=y/H, texCoord.y=1(fb下端) → uv.y=(y+h)/H - const scaleX = width / srcAttachment.width; - const scaleY = height / srcAttachment.height; - const offsetX = x / srcAttachment.width; - const offsetY = y / srcAttachment.height; + const scaleX = width / src_attachment.width; + const scaleY = height / src_attachment.height; + const offsetX = x / src_attachment.width; + const offsetY = y / src_attachment.height; $uniform4[0] = scaleX; $uniform4[1] = scaleY; @@ -125,7 +174,7 @@ const copyRegionToFilterAttachment = ( const sampler = config.textureManager.createSampler("container_copy_sampler", false); ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; - $entries3[2].resource = srcAttachment.texture.view; + $entries3[2].resource = src_attachment.texture.view; const bindGroup = config.device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries3 @@ -144,35 +193,47 @@ const copyRegionToFilterAttachment = ( return dstAttachment; }; -const drawFilterResultToMain = ( +/** + * @description フィルター結果をメインアタッチメントに描画する + * Draws filter result to the main attachment + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {IAttachmentObject} filter_attachment フィルターアタッチメント / Filter attachment + * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment + * @param {IBlendMode} blend_mode ブレンドモード / Blend mode + * @param {number} x X座標 / X coordinate + * @param {number} y Y座標 / Y coordinate + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @return {void} + */ +const $drawFilterResultToMain = ( config: ILocalFilterConfig, - filterAttachment: IAttachmentObject, - mainAttachment: IAttachmentObject, - blendMode: IBlendMode, + filter_attachment: IAttachmentObject, + main_attachment: IAttachmentObject, + blend_mode: IBlendMode, x: number, y: number, - bufferManager: BufferManager + buffer_manager: BufferManager ): void => { - if (!mainAttachment.texture || !filterAttachment.texture) { + if (!main_attachment.texture || !filter_attachment.texture) { return; } // WebGLと同じサブピクセル精度を維持するため、Math.floorを使用しない let drawX = x; let drawY = y; - let drawWidth = filterAttachment.width; - let drawHeight = filterAttachment.height; + let drawWidth = filter_attachment.width; + let drawHeight = filter_attachment.height; let uvOffsetX = 0; let uvOffsetY = 0; if (drawX < 0) { - uvOffsetX = -drawX / filterAttachment.width; + uvOffsetX = -drawX / filter_attachment.width; drawWidth += drawX; drawX = 0; } if (drawY < 0) { - uvOffsetY = -drawY / filterAttachment.height; + uvOffsetY = -drawY / filter_attachment.height; drawHeight += drawY; drawY = 0; } @@ -181,8 +242,8 @@ const drawFilterResultToMain = ( return; } - const mainWidth = mainAttachment.width; - const mainHeight = mainAttachment.height; + const mainWidth = main_attachment.width; + const mainHeight = main_attachment.height; if (drawX + drawWidth > mainWidth) { drawWidth = mainWidth - drawX; } @@ -190,12 +251,12 @@ const drawFilterResultToMain = ( drawHeight = mainHeight - drawY; } - if (SIMPLE_BLEND_MODES.has(blendMode)) { + if ($SIMPLE_BLEND_MODES.has(blend_mode)) { - const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; + const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view; let pipelineName: string; - switch (blendMode) { + switch (blend_mode) { case "add": pipelineName = useMsaa ? "filter_output_add_msaa" : "filter_output_add"; break; @@ -225,24 +286,24 @@ const drawFilterResultToMain = ( const sampler = config.textureManager.createSampler("container_output_sampler", true); - const uvScaleX = drawWidth / filterAttachment.width; - const uvScaleY = drawHeight / filterAttachment.height; + const uvScaleX = drawWidth / filter_attachment.width; + const uvScaleY = drawHeight / filter_attachment.height; $uniform4[0] = uvScaleX; $uniform4[1] = uvScaleY; $uniform4[2] = uvOffsetX; $uniform4[3] = uvOffsetY; - const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform4); + const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform4); ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; - $entries3[2].resource = filterAttachment.texture.view; + $entries3[2].resource = filter_attachment.texture.view; const bindGroup = config.device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries3 }); - const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture.view; - const resolveTarget = useMsaa ? mainAttachment.texture.view : null; + const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture.view; + const resolveTarget = useMsaa ? main_attachment.texture.view : null; const renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor( colorView, 0, 0, 0, 0, "load", resolveTarget ); @@ -272,8 +333,8 @@ const drawFilterResultToMain = ( } else { // 複雑なブレンドモード - const dstAttachment = copyRegionToFilterAttachment( - config, mainAttachment, drawX, drawY, drawWidth, drawHeight + const dstAttachment = $copyRegionToFilterAttachment( + config, main_attachment, drawX, drawY, drawWidth, drawHeight ); $uniform8[0] = $identityColorTransform[0]; @@ -286,7 +347,7 @@ const drawFilterResultToMain = ( $uniform8[7] = 0; const blendedAttachment = blendApplyComplexBlendUseCase( - filterAttachment, dstAttachment, blendMode, $uniform8, { + filter_attachment, dstAttachment, blend_mode, $uniform8, { "device": config.device, "commandEncoder": config.commandEncoder, "bufferManager": config.bufferManager, @@ -298,22 +359,22 @@ const drawFilterResultToMain = ( ); // 結果をメインに描画 - const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; + const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view; const resultPipelineName = useMsaa ? "filter_complex_blend_output_msaa" : "filter_complex_blend_output"; const resultPipeline = config.pipelineManager.getPipeline(resultPipelineName); const resultLayout = config.pipelineManager.getBindGroupLayout("positioned_texture"); - if (resultPipeline && resultLayout && blendedAttachment.texture && mainAttachment.texture) { + if (resultPipeline && resultLayout && blendedAttachment.texture && main_attachment.texture) { $uniform8[0] = drawX; $uniform8[1] = drawY; $uniform8[2] = blendedAttachment.width; $uniform8[3] = blendedAttachment.height; - $uniform8[4] = mainAttachment.width; - $uniform8[5] = mainAttachment.height; + $uniform8[4] = main_attachment.width; + $uniform8[5] = main_attachment.height; $uniform8[6] = 0; $uniform8[7] = 0; - const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform8); + const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform8); const sampler = config.textureManager.createSampler("container_blend_output_sampler", false); @@ -325,8 +386,8 @@ const drawFilterResultToMain = ( "entries": $entries3 }); - const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture.view; - const resolveTarget = useMsaa ? mainAttachment.texture.view : null; + const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture.view; + const resolveTarget = useMsaa ? main_attachment.texture.view : null; const renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor( colorView, 0, 0, 0, 0, "load", resolveTarget ); @@ -343,11 +404,21 @@ const drawFilterResultToMain = ( } }; -const applyFilterChain = ( - filterAttachment: IAttachmentObject, +/** + * @description フィルターチェーンをアタッチメントに適用する + * Applies a chain of filters to an attachment + * @param {IAttachmentObject} filter_attachment フィルターアタッチメント / Filter attachment + * @param {Float32Array} matrix 変換行列 / Transformation matrix + * @param {Float32Array} params フィルターパラメータ配列 / Filter parameters array + * @param {number} device_pixel_ratio デバイスピクセル比 / Device pixel ratio + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @return {IAttachmentObject} フィルター適用後のアタッチメント / Attachment with filters applied + */ +const $applyFilterChain = ( + filter_attachment: IAttachmentObject, matrix: Float32Array, params: Float32Array, - devicePixelRatio: number, + device_pixel_ratio: number, config: ILocalFilterConfig ): IAttachmentObject => { @@ -362,30 +433,30 @@ const applyFilterChain = ( case 0: // BevelFilter { const newAtt = filterApplyBevelFilterUseCase( - filterAttachment, matrix, + filter_attachment, matrix, params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], Boolean(params[idx++]), - devicePixelRatio, config + device_pixel_ratio, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; case 1: // BlurFilter { const newAtt = filterApplyBlurFilterUseCase( - filterAttachment, matrix, + filter_attachment, matrix, params[idx++], params[idx++], params[idx++], - devicePixelRatio, config + device_pixel_ratio, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; @@ -412,12 +483,12 @@ const applyFilterChain = ( $uniform20[18] = params[idx++]; $uniform20[19] = params[idx++]; const newAtt = filterApplyColorMatrixFilterUseCase( - filterAttachment, $uniform20, config + filter_attachment, $uniform20, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; @@ -432,17 +503,17 @@ const applyFilterChain = ( } const newAtt = filterApplyConvolutionFilterUseCase( - filterAttachment, + filter_attachment, matrixX, matrixY, convMatrix, params[idx++], params[idx++], Boolean(params[idx++]), Boolean(params[idx++]), params[idx++], params[idx++], config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; @@ -455,49 +526,49 @@ const applyFilterChain = ( } const newAtt = filterApplyDisplacementMapFilterUseCase( - filterAttachment, matrix, + filter_attachment, matrix, dmBuffer, params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], - devicePixelRatio, config + device_pixel_ratio, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; case 5: // DropShadowFilter { const newAtt = filterApplyDropShadowFilterUseCase( - filterAttachment, matrix, + filter_attachment, matrix, params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], Boolean(params[idx++]), Boolean(params[idx++]), Boolean(params[idx++]), - devicePixelRatio, config + device_pixel_ratio, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; case 6: // GlowFilter { const newAtt = filterApplyGlowFilterUseCase( - filterAttachment, matrix, + filter_attachment, matrix, params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], Boolean(params[idx++]), Boolean(params[idx++]), - devicePixelRatio, config + device_pixel_ratio, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; @@ -519,16 +590,16 @@ const applyFilterChain = ( for (let i = 0; i < gbRatiosLen; i++) { gbRatios[i] = params[idx++] } const newAtt = filterApplyGradientBevelFilterUseCase( - filterAttachment, matrix, + filter_attachment, matrix, gbDist, gbAngle, gbColors, gbAlphas, gbRatios, params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], Boolean(params[idx++]), - devicePixelRatio, config + device_pixel_ratio, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; @@ -550,43 +621,63 @@ const applyFilterChain = ( for (let i = 0; i < ggRatiosLen; i++) { ggRatios[i] = params[idx++] } const newAtt = filterApplyGradientGlowFilterUseCase( - filterAttachment, matrix, + filter_attachment, matrix, ggDist, ggAngle, ggColors, ggAlphas, ggRatios, params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], Boolean(params[idx++]), - devicePixelRatio, config + device_pixel_ratio, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; } } - return filterAttachment; + return filter_attachment; }; +/** + * @description コンテナレイヤーの終了処理を実行する(フィルター適用+ブレンド+メインへの描画) + * Executes container layer end processing (filter application + blending + drawing to main) + * @param {IAttachmentObject} temp_attachment 一時アタッチメント / Temporary attachment + * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment + * @param {string} _temp_name 一時名(未使用) / Temporary name (unused) + * @param {IBlendMode} blend_mode ブレンドモード / Blend mode + * @param {Float32Array} matrix 変換行列 / Transformation matrix + * @param {Float32Array | null} color_transform カラートランスフォーム配列 / Color transform array + * @param {boolean} use_filter フィルター使用フラグ / Whether to use filter + * @param {Float32Array | null} filter_bounds フィルターバウンディングボックス / Filter bounding box + * @param {Float32Array | null} params フィルターパラメータ配列 / Filter parameters array + * @param {string} unique_key ユニークキー / Unique key + * @param {string} filter_key フィルターキー / Filter key + * @param {number} _content_width コンテンツ幅(未使用) / Content width (unused) + * @param {number} _content_height コンテンツ高さ(未使用) / Content height (unused) + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @return {void} + */ export const execute = ( - tempAttachment: IAttachmentObject, - mainAttachment: IAttachmentObject, - _tempName: string, - blendMode: IBlendMode, + temp_attachment: IAttachmentObject, + main_attachment: IAttachmentObject, + _temp_name: string, + blend_mode: IBlendMode, matrix: Float32Array, - colorTransform: Float32Array | null, - useFilter: boolean, - filterBounds: Float32Array | null, + color_transform: Float32Array | null, + use_filter: boolean, + filter_bounds: Float32Array | null, params: Float32Array | null, - uniqueKey: string, - filterKey: string, - _contentWidth: number, - _contentHeight: number, + unique_key: string, + filter_key: string, + _content_width: number, + _content_height: number, config: ILocalFilterConfig, - bufferManager: BufferManager + buffer_manager: BufferManager ): void => { - if (useFilter && matrix && filterBounds && params) { + if (use_filter && matrix && filter_bounds && params) { // containerEndLayerが呼ばれる=ディスプレイレイヤーがコンテンツ変更を検出して再レンダリングを要求 // 常に新鮮なテクスチャを抽出してフィルターを適用する @@ -595,26 +686,26 @@ export const execute = ( // WebGL版と同じ: レイヤー全体をフィルター用にコピー // レイヤーはコンテンツサイズで作成され、childrenは相対座標で描画されているため // (0, 0, layerWidth, layerHeight) = コンテンツ全体 - let filterAttachment = copyRegionToFilterAttachment( - config, tempAttachment, - 0, 0, tempAttachment.width, tempAttachment.height + let filterAttachment = $copyRegionToFilterAttachment( + config, temp_attachment, + 0, 0, temp_attachment.width, temp_attachment.height ); // 一時アタッチメントを遅延解放(コマンドバッファsubmit後に解放) // destroyAttachmentは即座にGPUテクスチャを破棄するため、 // コマンドエンコーダに記録済みのレンダーパスが参照するテクスチャが無効になる - config.frameBufferManager.releaseTemporaryAttachment(tempAttachment); + config.frameBufferManager.releaseTemporaryAttachment(temp_attachment); // フィルターチェーンを適用 const devicePixelRatio = WebGPUUtil.getDevicePixelRatio(); - filterAttachment = applyFilterChain( + filterAttachment = $applyFilterChain( filterAttachment, matrix, params, devicePixelRatio, config ); // キャッシュに保存 - if (uniqueKey) { - $cacheStore.set(uniqueKey, "fKey", filterKey); - $cacheStore.set(uniqueKey, "fTexture", filterAttachment); + if (unique_key) { + $cacheStore.set(unique_key, "fKey", filter_key); + $cacheStore.set(unique_key, "fTexture", filterAttachment); } // フィルター結果をメインに描画 @@ -623,23 +714,23 @@ export const execute = ( // キャッシュにはフィルター結果のみ保存(CTは毎フレーム適用する) let drawAttachment = filterAttachment; let ctAttachment: IAttachmentObject | null = null; - if (!isIdentityColorTransform(colorTransform)) { - ctAttachment = applyColorTransform(config, filterAttachment, colorTransform!); + if (!$isIdentityColorTransform(color_transform)) { + ctAttachment = $applyColorTransform(config, filterAttachment, color_transform!); drawAttachment = ctAttachment; } const scaleX = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1]); const scaleY = Math.sqrt(matrix[2] * matrix[2] + matrix[3] * matrix[3]); - const boundsXMin = filterBounds[0] * (scaleX / devicePixelRatio); - const boundsYMin = filterBounds[1] * (scaleY / devicePixelRatio); + const boundsXMin = filter_bounds[0] * (scaleX / devicePixelRatio); + const boundsYMin = filter_bounds[1] * (scaleY / devicePixelRatio); // WebGL版と同じ: boundsXMin + matrix[4] で絶対位置 const drawX = boundsXMin + matrix[4]; const drawY = boundsYMin + matrix[5]; - drawFilterResultToMain( - config, drawAttachment, mainAttachment, - blendMode, drawX, drawY, bufferManager + $drawFilterResultToMain( + config, drawAttachment, main_attachment, + blend_mode, drawX, drawY, buffer_manager ); // CT一時アタッチメントを解放 @@ -647,7 +738,7 @@ export const execute = ( config.frameBufferManager.releaseTemporaryAttachment(ctAttachment); } // キャッシュされていないフィルター結果のみ解放 - if (!uniqueKey) { + if (!unique_key) { config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); } } @@ -655,25 +746,25 @@ export const execute = ( } else { // ブレンドのみ:レイヤー全体をフィルター用にコピーしてメインに描画 - let fullAttachment = copyRegionToFilterAttachment( - config, tempAttachment, - 0, 0, tempAttachment.width, tempAttachment.height + let fullAttachment = $copyRegionToFilterAttachment( + config, temp_attachment, + 0, 0, temp_attachment.width, temp_attachment.height ); // 一時アタッチメントを遅延解放(コマンドバッファsubmit後に解放) - config.frameBufferManager.releaseTemporaryAttachment(tempAttachment); + config.frameBufferManager.releaseTemporaryAttachment(temp_attachment); // ColorTransformが恒等変換でない場合、適用 - if (!isIdentityColorTransform(colorTransform)) { - const ctAttachment = applyColorTransform(config, fullAttachment, colorTransform!); + if (!$isIdentityColorTransform(color_transform)) { + const ctAttachment = $applyColorTransform(config, fullAttachment, color_transform!); config.frameBufferManager.releaseTemporaryAttachment(fullAttachment); fullAttachment = ctAttachment; } // WebGL版と同じ: matrix[4], matrix[5] = layerBounds の絶対位置に描画 - drawFilterResultToMain( - config, fullAttachment, mainAttachment, - blendMode, matrix[4], matrix[5], bufferManager + $drawFilterResultToMain( + config, fullAttachment, main_attachment, + blend_mode, matrix[4], matrix[5], buffer_manager ); config.frameBufferManager.releaseTemporaryAttachment(fullAttachment); diff --git a/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts b/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts index 1c66d814..bd0051b4 100644 --- a/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts @@ -13,7 +13,15 @@ import { } from "../../Mask"; import { $getAtlasAttachmentObject } from "../../AtlasManager"; +/** + * @description キャッシュ済みバインドグループ + * Cached bind group + */ let $cachedBindGroup: GPUBindGroup | null = null; +/** + * @description キャッシュ済みアトラステクスチャビュー + * Cached atlas texture view + */ let $cachedAtlasView: GPUTextureView | null = null; /** @@ -36,6 +44,19 @@ const $getPipelineName = (mode: IBlendMode): string => { } }; +/** + * @description インスタンス描画を実行する + * Executes instanced array drawing + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder + * @param {GPURenderPassEncoder | null} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager + * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @return {GPURenderPassEncoder | null} レンダーパスエンコーダまたはnull / Render pass encoder or null + */ export const execute = ( device: GPUDevice, command_encoder: GPUCommandEncoder, diff --git a/packages/webgpu/src/Context/usecase/ContextDrawIndirectUseCase.ts b/packages/webgpu/src/Context/usecase/ContextDrawIndirectUseCase.ts index b51c01b6..314b552a 100644 --- a/packages/webgpu/src/Context/usecase/ContextDrawIndirectUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextDrawIndirectUseCase.ts @@ -13,31 +13,54 @@ import { } from "../../Mask"; import { $getAtlasAttachmentObject } from "../../AtlasManager"; +/** + * @description キャッシュ済みバインドグループ + * Cached bind group + */ let $cachedBindGroup: GPUBindGroup | null = null; +/** + * @description キャッシュ済みアトラステクスチャビュー + * Cached atlas texture view + */ let $cachedAtlasView: GPUTextureView | null = null; +/** + * @description Indirect描画を使用したインスタンス描画を実行する + * Executes instanced drawing with indirect draw support + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder + * @param {GPURenderPassEncoder | null} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager + * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {boolean} use_indirect Indirect描画使用フラグ / Whether to use indirect drawing + * @param {boolean} use_storage_buffer StorageBuffer使用フラグ / Whether to use storage buffer + * @return {GPURenderPassEncoder | null} レンダーパスエンコーダまたはnull / Render pass encoder or null + */ export const execute = ( device: GPUDevice, - commandEncoder: GPUCommandEncoder, - renderPassEncoder: GPURenderPassEncoder | null, - mainAttachment: IAttachmentObject, - bufferManager: BufferManager, - frameBufferManager: FrameBufferManager, - textureManager: TextureManager, - pipelineManager: PipelineManager, - useIndirect: boolean = true, - useStorageBuffer: boolean = true + command_encoder: GPUCommandEncoder, + render_pass_encoder: GPURenderPassEncoder | null, + main_attachment: IAttachmentObject, + buffer_manager: BufferManager, + frame_buffer_manager: FrameBufferManager, + texture_manager: TextureManager, + pipeline_manager: PipelineManager, + use_indirect: boolean = true, + use_storage_buffer: boolean = true ): GPURenderPassEncoder | null => { const shaderManager = getInstancedShaderManager(); if (shaderManager.count === 0) { - return renderPassEncoder; + return render_pass_encoder; } // 既存のレンダーパスを終了 - if (renderPassEncoder) { - renderPassEncoder.end(); - renderPassEncoder = null; + if (render_pass_encoder) { + render_pass_encoder.end(); + render_pass_encoder = null; } const isMasked = $isMaskTestEnabled(); @@ -66,11 +89,11 @@ export const execute = ( }; const pipelineName = getPipelineName(blendMode); - const normalPipeline = pipelineManager.getPipeline(pipelineName); - const maskedPipeline = pipelineManager.getPipeline("instanced_masked"); + const normalPipeline = pipeline_manager.getPipeline(pipelineName); + const maskedPipeline = pipeline_manager.getPipeline("instanced_masked"); const useStencil = isMasked && maskedPipeline - && (mainAttachment.msaaStencil?.view || mainAttachment.stencil?.view); + && (main_attachment.msaaStencil?.view || main_attachment.stencil?.view); const pipeline = useStencil ? maskedPipeline : normalPipeline; @@ -84,32 +107,32 @@ export const execute = ( if (useStencil) { // MSAA対応 - const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; - const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture!.view; - const stencilView = useMsaa && mainAttachment.msaaStencil?.view - ? mainAttachment.msaaStencil.view : mainAttachment.stencil!.view; - const resolveTarget = useMsaa ? mainAttachment.texture!.view : null; + const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view; + const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture!.view; + const stencilView = useMsaa && main_attachment.msaaStencil?.view + ? main_attachment.msaaStencil.view : main_attachment.stencil!.view; + const resolveTarget = useMsaa ? main_attachment.texture!.view : null; - const renderPassDescriptor = frameBufferManager.createStencilRenderPassDescriptor( + const renderPassDescriptor = frame_buffer_manager.createStencilRenderPassDescriptor( colorView, stencilView, "load", "load", resolveTarget ); - passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); } else { // 通常のレンダーパス(MSAA対応) - const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; - const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture!.view; - const resolveTarget = useMsaa ? mainAttachment.texture!.view : null; - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( + const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view; + const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture!.view; + const resolveTarget = useMsaa ? main_attachment.texture!.view : null; + const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor( colorView, 0, 0, 0, 0, "load", resolveTarget ); - passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); } passEncoder.setPipeline(pipeline); @@ -127,22 +150,22 @@ export const execute = ( // インスタンスバッファを作成または取得 let instanceBuffer: GPUBuffer; - if (useStorageBuffer) { + if (use_storage_buffer) { // Storage Buffer最適化: プールから再利用してメモリアロケーション削減 // Storage BufferはVERTEXフラグ付きで作成されているため、setVertexBufferで使用可能 - instanceBuffer = bufferManager.acquireStorageBuffer(instanceData.byteLength); - bufferManager.writeStorageBuffer(instanceBuffer, instanceData); + instanceBuffer = buffer_manager.acquireStorageBuffer(instanceData.byteLength); + buffer_manager.writeStorageBuffer(instanceBuffer, instanceData); } else { // 従来方式: プールから再利用 - instanceBuffer = bufferManager.acquireVertexBuffer(instanceData.byteLength, instanceData); + instanceBuffer = buffer_manager.acquireVertexBuffer(instanceData.byteLength, instanceData); } // 頂点バッファ(矩形)を取得(キャッシュ済み) - const vertexBuffer = bufferManager.getUnitRectBuffer(); + const vertexBuffer = buffer_manager.getUnitRectBuffer(); // アトラステクスチャをバインド(複数アトラス対応) // AtlasManagerから取得、フォールバックとしてFrameBufferManagerから取得 - const atlasAttachment = $getAtlasAttachmentObject() || frameBufferManager.getAttachment("atlas"); + const atlasAttachment = $getAtlasAttachmentObject() || frame_buffer_manager.getAttachment("atlas"); if (!atlasAttachment) { console.error("[WebGPU] Atlas attachment not found"); passEncoder.end(); @@ -150,9 +173,9 @@ export const execute = ( } // アトラス用サンプラーを取得(キャッシュ済み) - const sampler = textureManager.createSampler("atlas_instanced_sampler", false); + const sampler = texture_manager.createSampler("atlas_instanced_sampler", false); - const bindGroupLayout = pipelineManager.getBindGroupLayout("instanced"); + const bindGroupLayout = pipeline_manager.getBindGroupLayout("instanced"); if (!bindGroupLayout) { console.error("[WebGPU] Instanced bind group layout not found"); passEncoder.end(); @@ -183,13 +206,13 @@ export const execute = ( passEncoder.setVertexBuffer(1, instanceBuffer); passEncoder.setBindGroup(0, $cachedBindGroup); - if (useIndirect) { + if (use_indirect) { // Indirect Drawing: CPU-GPU間のオーバーヘッドを削減 // 注意: 1フレーム内で複数回呼び出される場合があるため、 // 毎回新しいIndirect Bufferを作成する必要がある // (共有バッファを使うとqueue.writeBufferの更新が全てGPU実行前に行われ、 // 全てのdrawIndirectが最後の更新値を使用してしまう) - const indirectBuffer = bufferManager.createIndirectBuffer( + const indirectBuffer = buffer_manager.createIndirectBuffer( 6, // vertexCount (2 triangles = 6 vertices) shaderManager.count, // instanceCount 0, // firstVertex diff --git a/packages/webgpu/src/Context/usecase/ContextGradientFillUseCase.ts b/packages/webgpu/src/Context/usecase/ContextGradientFillUseCase.ts index ccb5073c..f22e9550 100644 --- a/packages/webgpu/src/Context/usecase/ContextGradientFillUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextGradientFillUseCase.ts @@ -11,20 +11,67 @@ import { $getMaskStencilReference } from "../../Mask"; +/** + * @description グラデーションサンプラーのキャッシュ + * Cached gradient sampler + */ let $gradientSampler: GPUSampler | null = null; +/** + * @description ユニフォームデータの事前確保配列(36要素) + * Pre-allocated uniform data array (36 elements) + */ const $uniformData36 = new Float32Array(36); +/** + * @description ステンシルデータの事前確保配列(16要素) + * Pre-allocated stencil data array (16 elements) + */ const $stencilData16 = new Float32Array(16); +/** + * @description ステンシル用動的バインドグループのキャッシュ + * Cached dynamic bind group for stencil operations + */ let $stencilDynamicBindGroup: GPUBindGroup | null = null; +/** + * @description ステンシル用動的バッファのキャッシュ + * Cached dynamic buffer for stencil operations + */ let $stencilDynamicBuffer: GPUBuffer | null = null; +/** + * @description バインドグループエントリの事前確保配列 + * Pre-allocated bind group entry array + */ const $entries3: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, { "binding": 1, "resource": null as unknown as GPUSampler }, { "binding": 2, "resource": null as unknown as GPUTextureView } ]; +/** + * @description グラデーションフィル描画を実行する + * Executes gradient fill rendering + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {IPath[]} path_vertices パス頂点配列 / Path vertices array + * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix + * @param {Float32Array} fill_style フィルスタイル(RGBA) / Fill style (RGBA) + * @param {number} type グラデーションタイプ / Gradient type + * @param {number[]} stops グラデーションストップ配列 / Gradient stops array + * @param {Float32Array} gradient_matrix グラデーション変換行列 / Gradient transformation matrix + * @param {number} spread スプレッドモード / Spread mode + * @param {number} interpolation 補間モード / Interpolation mode + * @param {number} focal 焦点距離 / Focal point + * @param {number} viewport_width ビューポート幅 / Viewport width + * @param {number} viewport_height ビューポート高さ / Viewport height + * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target + * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline + * @param {number} _clip_level クリップレベル(未使用) / Clip level (unused) + * @return {GPUTexture | null} LUTテクスチャまたはnull / LUT texture or null + */ export const execute = ( device: GPUDevice, render_pass_encoder: GPURenderPassEncoder, diff --git a/packages/webgpu/src/Context/usecase/ContextGradientStrokeUseCase.ts b/packages/webgpu/src/Context/usecase/ContextGradientStrokeUseCase.ts index f4a35384..3cd2789b 100644 --- a/packages/webgpu/src/Context/usecase/ContextGradientStrokeUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextGradientStrokeUseCase.ts @@ -7,16 +7,51 @@ import { execute as contextComputeGradientMatrixService } from "../service/Conte import { $getLUTFromCache, $putLUTToCache } from "../../Gradient/GradientLUTCache"; import { $acquireFillTexture } from "../../FillTexturePool"; +/** + * @description グラデーションサンプラーのキャッシュ + * Cached gradient sampler + */ let $gradientSampler: GPUSampler | null = null; +/** + * @description ユニフォームデータの事前確保配列(36要素) + * Pre-allocated uniform data array (36 elements) + */ const $uniformData36 = new Float32Array(36); +/** + * @description バインドグループエントリの事前確保配列 + * Pre-allocated bind group entry array + */ const $entries3: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, { "binding": 1, "resource": null as unknown as GPUSampler }, { "binding": 2, "resource": null as unknown as GPUTextureView } ]; +/** + * @description グラデーションストローク描画を実行する + * Executes gradient stroke rendering + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {IPath[]} vertices パス頂点配列 / Path vertices array + * @param {number} thickness ストローク太さ / Stroke thickness + * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix + * @param {Float32Array} stroke_style ストロークスタイル(RGBA) / Stroke style (RGBA) + * @param {number} type グラデーションタイプ / Gradient type + * @param {number[]} stops グラデーションストップ配列 / Gradient stops array + * @param {Float32Array} gradient_matrix グラデーション変換行列 / Gradient transformation matrix + * @param {number} spread スプレッドモード / Spread mode + * @param {number} interpolation 補間モード / Interpolation mode + * @param {number} focal 焦点距離 / Focal point + * @param {number} viewport_width ビューポート幅 / Viewport width + * @param {number} viewport_height ビューポート高さ / Viewport height + * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target + * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline + * @return {GPUTexture | null} LUTテクスチャまたはnull / LUT texture or null + */ export const execute = ( device: GPUDevice, render_pass_encoder: GPURenderPassEncoder, diff --git a/packages/webgpu/src/Context/usecase/ContextProcessComplexBlendQueueUseCase.ts b/packages/webgpu/src/Context/usecase/ContextProcessComplexBlendQueueUseCase.ts index c3717047..95d86aca 100644 --- a/packages/webgpu/src/Context/usecase/ContextProcessComplexBlendQueueUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextProcessComplexBlendQueueUseCase.ts @@ -8,135 +8,201 @@ import { execute as blendApplyComplexBlendUseCase } from "../../Blend/usecase/Bl import { $getAtlasAttachmentObject } from "../../AtlasManager"; // プリアロケート配列 +/** + * @description ユニフォームデータの事前確保配列(4要素) + * Pre-allocated uniform data array (4 elements) + */ const $uniform4 = new Float32Array(4); +/** + * @description ユニフォームデータの事前確保配列(6要素) + * Pre-allocated uniform data array (6 elements) + */ const $uniform6 = new Float32Array(6); +/** + * @description ユニフォームデータの事前確保配列(8要素) + * Pre-allocated uniform data array (8 elements) + */ const $uniform8 = new Float32Array(8); +/** + * @description ユニフォームデータの事前確保配列(12要素) + * Pre-allocated uniform data array (12 elements) + */ const $uniform12 = new Float32Array(12); // プリアロケート BindGroup Entry 配列 +/** + * @description バインドグループエントリの事前確保配列 + * Pre-allocated bind group entry array + */ const $entries3: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, { "binding": 1, "resource": null as unknown as GPUSampler }, { "binding": 2, "resource": null as unknown as GPUTextureView } ]; -const copyTextureRegionViaRenderPass = ( +/** + * @description レンダーパスを使用してテクスチャ領域をコピーする + * Copies a texture region via render pass + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder + * @param {GPUTextureView} src_view ソーステクスチャビュー / Source texture view + * @param {IAttachmentObject} dst_attachment デスティネーションアタッチメント / Destination attachment + * @param {number} src_x ソースX座標 / Source X coordinate + * @param {number} src_y ソースY座標 / Source Y coordinate + * @param {number} src_width ソース幅 / Source width + * @param {number} src_height ソース高さ / Source height + * @param {number} copy_width コピー幅 / Copy width + * @param {number} copy_height コピー高さ / Copy height + * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager + * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @return {void} + */ +const $copyTextureRegionViaRenderPass = ( device: GPUDevice, - commandEncoder: GPUCommandEncoder, - srcView: GPUTextureView, - dstAttachment: IAttachmentObject, - srcX: number, - srcY: number, - srcWidth: number, - srcHeight: number, - copyWidth: number, - copyHeight: number, - frameBufferManager: FrameBufferManager, - textureManager: TextureManager, - pipelineManager: PipelineManager, - bufferManager: BufferManager + command_encoder: GPUCommandEncoder, + src_view: GPUTextureView, + dst_attachment: IAttachmentObject, + src_x: number, + src_y: number, + src_width: number, + src_height: number, + copy_width: number, + copy_height: number, + frame_buffer_manager: FrameBufferManager, + texture_manager: TextureManager, + pipeline_manager: PipelineManager, + buffer_manager: BufferManager ): void => { - const pipeline = pipelineManager.getPipeline("complex_blend_copy"); - const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy"); + const pipeline = pipeline_manager.getPipeline("complex_blend_copy"); + const bindGroupLayout = pipeline_manager.getBindGroupLayout("texture_copy"); if (!pipeline || !bindGroupLayout) { return; } - $uniform4[0] = copyWidth / srcWidth; - $uniform4[1] = copyHeight / srcHeight; - $uniform4[2] = srcX / srcWidth; - $uniform4[3] = srcY / srcHeight; - const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform4); + $uniform4[0] = copy_width / src_width; + $uniform4[1] = copy_height / src_height; + $uniform4[2] = src_x / src_width; + $uniform4[3] = src_y / src_height; + const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform4); - const sampler = textureManager.createSampler("complex_blend_copy_sampler", false); + const sampler = texture_manager.createSampler("complex_blend_copy_sampler", false); ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; - $entries3[2].resource = srcView; + $entries3[2].resource = src_view; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries3 }); - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( - dstAttachment.texture!.view, + const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor( + dst_attachment.texture!.view, 0, 0, 0, 0, "clear" ); - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.draw(6, 1, 0, 0); passEncoder.end(); }; -const drawToMainAttachment = ( +/** + * @description ブレンド結果をメインアタッチメントに描画する + * Draws blend result to the main attachment + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder + * @param {IAttachmentObject} src_attachment ソースアタッチメント / Source attachment + * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment + * @param {number} dst_x デスティネーションX座標 / Destination X coordinate + * @param {number} dst_y デスティネーションY座標 / Destination Y coordinate + * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager + * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @return {void} + */ +const $drawToMainAttachment = ( device: GPUDevice, - commandEncoder: GPUCommandEncoder, - srcAttachment: IAttachmentObject, - mainAttachment: IAttachmentObject, - dstX: number, - dstY: number, - frameBufferManager: FrameBufferManager, - textureManager: TextureManager, - pipelineManager: PipelineManager, - bufferManager: BufferManager + command_encoder: GPUCommandEncoder, + src_attachment: IAttachmentObject, + main_attachment: IAttachmentObject, + dst_x: number, + dst_y: number, + frame_buffer_manager: FrameBufferManager, + texture_manager: TextureManager, + pipeline_manager: PipelineManager, + buffer_manager: BufferManager ): void => { - const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; + const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view; const pipelineName = useMsaa ? "complex_blend_output_msaa" : "complex_blend_output"; - const pipeline = pipelineManager.getPipeline(pipelineName); - const bindGroupLayout = pipelineManager.getBindGroupLayout("positioned_texture"); + const pipeline = pipeline_manager.getPipeline(pipelineName); + const bindGroupLayout = pipeline_manager.getBindGroupLayout("positioned_texture"); if (!pipeline || !bindGroupLayout) { return; } - $uniform8[0] = dstX; - $uniform8[1] = dstY; - $uniform8[2] = srcAttachment.width; - $uniform8[3] = srcAttachment.height; - $uniform8[4] = mainAttachment.width; - $uniform8[5] = mainAttachment.height; + $uniform8[0] = dst_x; + $uniform8[1] = dst_y; + $uniform8[2] = src_attachment.width; + $uniform8[3] = src_attachment.height; + $uniform8[4] = main_attachment.width; + $uniform8[5] = main_attachment.height; $uniform8[6] = 0; $uniform8[7] = 0; - const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform8); + const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform8); - const sampler = textureManager.createSampler("complex_blend_output_sampler", false); + const sampler = texture_manager.createSampler("complex_blend_output_sampler", false); ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; - $entries3[2].resource = srcAttachment.texture!.view; + $entries3[2].resource = src_attachment.texture!.view; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries3 }); - const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture!.view; - const resolveTarget = useMsaa ? mainAttachment.texture!.view : null; - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( + const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture!.view; + const resolveTarget = useMsaa ? main_attachment.texture!.view : null; + const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor( colorView, 0, 0, 0, 0, "load", resolveTarget ); - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.draw(6, 1, 0, 0); passEncoder.end(); }; +/** + * @description 複雑なブレンドモードキューを処理する + * Processes the complex blend mode queue + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder + * @param {IAttachmentObject | null} main_attachment メインアタッチメント / Main attachment + * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager + * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @return {void} + */ export const execute = ( device: GPUDevice, - commandEncoder: GPUCommandEncoder, - mainAttachment: IAttachmentObject | null, - frameBufferManager: FrameBufferManager, - textureManager: TextureManager, - pipelineManager: PipelineManager, - bufferManager: BufferManager + command_encoder: GPUCommandEncoder, + main_attachment: IAttachmentObject | null, + frame_buffer_manager: FrameBufferManager, + texture_manager: TextureManager, + pipeline_manager: PipelineManager, + buffer_manager: BufferManager ): void => { const queue = getComplexBlendQueue(); @@ -144,12 +210,12 @@ export const execute = ( return; } - if (!mainAttachment || !mainAttachment.texture) { + if (!main_attachment || !main_attachment.texture) { clearComplexBlendQueue(); return; } - const atlasAttachment = $getAtlasAttachmentObject() || frameBufferManager.getAttachment("atlas"); + const atlasAttachment = $getAtlasAttachmentObject() || frame_buffer_manager.getAttachment("atlas"); if (!atlasAttachment || !atlasAttachment.texture) { clearComplexBlendQueue(); return; @@ -168,7 +234,7 @@ export const execute = ( const dstX = Math.max(0, Math.floor(matrix[6])); const dstY = Math.max(0, Math.floor(matrix[7])); - if (dstX >= mainAttachment.width || dstY >= mainAttachment.height) { + if (dstX >= main_attachment.width || dstY >= main_attachment.height) { continue; } @@ -177,8 +243,8 @@ export const execute = ( const blendWidth = hasScale ? width : node.w; const blendHeight = hasScale ? height : node.h; - const clippedWidth = Math.min(blendWidth, mainAttachment.width - dstX); - const clippedHeight = Math.min(blendHeight, mainAttachment.height - dstY); + const clippedWidth = Math.min(blendWidth, main_attachment.width - dstX); + const clippedHeight = Math.min(blendHeight, main_attachment.height - dstY); if (clippedWidth <= 0 || clippedHeight <= 0) { continue; } @@ -187,10 +253,10 @@ export const execute = ( let srcAttachment: IAttachmentObject; if (hasScale) { - srcAttachment = frameBufferManager.createTemporaryAttachment(blendWidth, blendHeight); + srcAttachment = frame_buffer_manager.createTemporaryAttachment(blendWidth, blendHeight); - const scalePipeline = pipelineManager.getPipeline("complex_blend_scale"); - const scaleBindGroupLayout = pipelineManager.getBindGroupLayout("texture_scale"); + const scalePipeline = pipeline_manager.getPipeline("complex_blend_scale"); + const scaleBindGroupLayout = pipeline_manager.getBindGroupLayout("texture_scale"); if (scalePipeline && scaleBindGroupLayout) { const halfW = blendWidth / 2; @@ -205,8 +271,8 @@ export const execute = ( $uniform6[4] = -halfNodeW * matrix[0] - halfNodeH * matrix[3] + halfW; $uniform6[5] = -halfNodeW * matrix[1] - halfNodeH * matrix[4] + halfH; - const originalAttachment = frameBufferManager.createTemporaryAttachment(node.w, node.h); - commandEncoder.copyTextureToTexture( + const originalAttachment = frame_buffer_manager.createTemporaryAttachment(node.w, node.h); + command_encoder.copyTextureToTexture( { "texture": atlasAttachment.texture.resource, "origin": { "x": node.x, "y": node.y, "z": 0 } @@ -230,9 +296,9 @@ export const execute = ( $uniform12[9] = blendHeight; $uniform12[10] = 0; $uniform12[11] = 0; - const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform12, 48); + const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform12, 48); - const sampler = textureManager.createSampler("scale_sampler", true); + const sampler = texture_manager.createSampler("scale_sampler", true); ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; $entries3[2].resource = originalAttachment.texture!.view; @@ -241,21 +307,21 @@ export const execute = ( "entries": $entries3 }); - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( + const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor( srcAttachment.texture!.view, 0, 0, 0, 0, "clear" ); - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(scalePipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.draw(6, 1, 0, 0); passEncoder.end(); - frameBufferManager.releaseTemporaryAttachment(originalAttachment); + frame_buffer_manager.releaseTemporaryAttachment(originalAttachment); } else { - commandEncoder.copyTextureToTexture( + command_encoder.copyTextureToTexture( { "texture": atlasAttachment.texture.resource, "origin": { "x": node.x, "y": node.y, "z": 0 } @@ -268,8 +334,8 @@ export const execute = ( ); } } else { - srcAttachment = frameBufferManager.createTemporaryAttachment(blendWidth, blendHeight); - commandEncoder.copyTextureToTexture( + srcAttachment = frame_buffer_manager.createTemporaryAttachment(blendWidth, blendHeight); + command_encoder.copyTextureToTexture( { "texture": atlasAttachment.texture.resource, "origin": { "x": node.x, "y": node.y, "z": 0 } @@ -283,23 +349,23 @@ export const execute = ( } // 2. デスティネーションテクスチャを作成(メインからレンダーパスでコピー) - const dstAttachment = frameBufferManager.createTemporaryAttachment(blendWidth, blendHeight); + const dstAttachment = frame_buffer_manager.createTemporaryAttachment(blendWidth, blendHeight); - copyTextureRegionViaRenderPass( + $copyTextureRegionViaRenderPass( device, - commandEncoder, - mainAttachment.texture.view, + command_encoder, + main_attachment.texture.view, dstAttachment, dstX, dstY, - mainAttachment.width, - mainAttachment.height, + main_attachment.width, + main_attachment.height, blendWidth, blendHeight, - frameBufferManager, - textureManager, - pipelineManager, - bufferManager + frame_buffer_manager, + texture_manager, + pipeline_manager, + buffer_manager ); // 3. カラートランスフォームを準備(add値は生値) @@ -319,34 +385,34 @@ export const execute = ( blend_mode, $uniform8, { - device, - commandEncoder, - bufferManager, - frameBufferManager, - pipelineManager, - textureManager, + "device": device, + "commandEncoder": command_encoder, + "bufferManager": buffer_manager, + "frameBufferManager": frame_buffer_manager, + "pipelineManager": pipeline_manager, + "textureManager": texture_manager, "frameTextures": [] } ); // 5. 結果をメインアタッチメントに描画 - drawToMainAttachment( + $drawToMainAttachment( device, - commandEncoder, + command_encoder, blendedAttachment, - mainAttachment, + main_attachment, dstX, dstY, - frameBufferManager, - textureManager, - pipelineManager, - bufferManager + frame_buffer_manager, + texture_manager, + pipeline_manager, + buffer_manager ); // 6. 一時テクスチャを解放 - frameBufferManager.releaseTemporaryAttachment(srcAttachment); - frameBufferManager.releaseTemporaryAttachment(dstAttachment); - frameBufferManager.releaseTemporaryAttachment(blendedAttachment); + frame_buffer_manager.releaseTemporaryAttachment(srcAttachment); + frame_buffer_manager.releaseTemporaryAttachment(dstAttachment); + frame_buffer_manager.releaseTemporaryAttachment(blendedAttachment); } clearComplexBlendQueue(); diff --git a/packages/webgpu/src/FillTexturePool.ts b/packages/webgpu/src/FillTexturePool.ts index 75e75797..d6c977bb 100644 --- a/packages/webgpu/src/FillTexturePool.ts +++ b/packages/webgpu/src/FillTexturePool.ts @@ -1,6 +1,16 @@ -// GPUTexture → GPUTextureView キャッシュ(createView()呼び出し削減) +/** + * @description GPUTexture → GPUTextureView キャッシュ(createView()呼び出し削減) + * GPUTexture to GPUTextureView cache to reduce createView() calls + * @type {WeakMap} + */ const $viewCache = new WeakMap(); +/** + * @description キャッシュからビューを取得、なければ生成してキャッシュに保存 + * Get view from cache, or create and cache a new one + * @param {GPUTexture} texture + * @return {GPUTextureView} + */ export const $getOrCreateView = (texture: GPUTexture): GPUTextureView => { let view = $viewCache.get(texture); if (!view) { @@ -10,15 +20,44 @@ export const $getOrCreateView = (texture: GPUTexture): GPUTextureView => { return view; }; -// GPUTextureUsage.TEXTURE_BINDING(0x04) | GPUTextureUsage.COPY_DST(0x02) = 0x06 -const FILL_TEXTURE_USAGE = 0x06; +/** + * @description 塗りテクスチャ用のGPUTextureUsageフラグ + * GPUTextureUsage flags for fill textures + * TEXTURE_BINDING(0x04) | COPY_DST(0x02) = 0x06 + * @type {number} + */ +const $FILL_TEXTURE_USAGE = 0x06; -// GPUTextureUsage.TEXTURE_BINDING(0x04) | GPUTextureUsage.COPY_DST(0x02) | GPUTextureUsage.RENDER_ATTACHMENT(0x10) = 0x16 -const RENDER_TEXTURE_USAGE = 0x16; +/** + * @description レンダーテクスチャ用のGPUTextureUsageフラグ + * GPUTextureUsage flags for render textures + * TEXTURE_BINDING(0x04) | COPY_DST(0x02) | RENDER_ATTACHMENT(0x10) = 0x16 + * @type {number} + */ +const $RENDER_TEXTURE_USAGE = 0x16; +/** + * @description 塗りテクスチャのオブジェクトプール + * Object pool for fill textures + * @type {Map} + */ const $pool: Map = new Map(); + +/** + * @description レンダーテクスチャのオブジェクトプール + * Object pool for render textures + * @type {Map} + */ const $renderPool: Map = new Map(); +/** + * @description プールから塗りテクスチャを取得、なければ新規作成 + * Acquire a fill texture from the pool, or create a new one + * @param {GPUDevice} device + * @param {number} width + * @param {number} height + * @return {GPUTexture} + */ export const $acquireFillTexture = (device: GPUDevice, width: number, height: number): GPUTexture => { const key = `${width}_${height}`; @@ -29,10 +68,16 @@ export const $acquireFillTexture = (device: GPUDevice, width: number, height: nu return device.createTexture({ "size": { width, height }, "format": "rgba8unorm", - "usage": FILL_TEXTURE_USAGE + "usage": $FILL_TEXTURE_USAGE }); }; +/** + * @description 塗りテクスチャをプールに返却 + * Release a fill texture back to the pool + * @param {GPUTexture} texture + * @return {void} + */ export const $releaseFillTexture = (texture: GPUTexture): void => { const key = `${texture.width}_${texture.height}`; @@ -44,6 +89,14 @@ export const $releaseFillTexture = (texture: GPUTexture): void => list.push(texture); }; +/** + * @description プールからレンダーテクスチャを取得、なければ新規作成 + * Acquire a render texture from the pool, or create a new one + * @param {GPUDevice} device + * @param {number} width + * @param {number} height + * @return {GPUTexture} + */ export const $acquireRenderTexture = (device: GPUDevice, width: number, height: number): GPUTexture => { const key = `${width}_${height}`; @@ -54,10 +107,16 @@ export const $acquireRenderTexture = (device: GPUDevice, width: number, height: return device.createTexture({ "size": { width, height }, "format": "rgba8unorm", - "usage": RENDER_TEXTURE_USAGE + "usage": $RENDER_TEXTURE_USAGE }); }; +/** + * @description レンダーテクスチャをプールに返却 + * Release a render texture back to the pool + * @param {GPUTexture} texture + * @return {void} + */ export const $releaseRenderTexture = (texture: GPUTexture): void => { const key = `${texture.width}_${texture.height}`; @@ -69,6 +128,11 @@ export const $releaseRenderTexture = (texture: GPUTexture): void => list.push(texture); }; +/** + * @description 全テクスチャプールを破棄してクリア + * Destroy and clear all texture pools + * @return {void} + */ export const $clearFillTexturePool = (): void => { for (const [, list] of $pool) { diff --git a/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts b/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts index 944a5c50..9a6f0a8c 100644 --- a/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts +++ b/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts @@ -31,27 +31,46 @@ const $entries4: GPUBindGroupEntry[] = [ /** * @description ベベルフィルターを適用 - * WebGL版と同様に、erase前処理で差分テクスチャを作成してからブラーを適用 + * Apply bevel filter * + * WebGL版と同様に、erase前処理で差分テクスチャを作成してからブラーを適用 * UV変換方式で元テクスチャとブラーテクスチャを直接サンプリング。 * 合成時のcopyTextureToTextureと一時テクスチャを使用しない最適化版。 + * + * @param {IAttachmentObject} source_attachment - 入力テクスチャ + * @param {Float32Array} matrix - 変換行列 + * @param {number} distance - ベベルの距離 + * @param {number} angle - ベベルの角度(度) + * @param {number} highlight_color - ハイライト色 (32bit整数) + * @param {number} highlight_alpha - ハイライトアルファ + * @param {number} shadow_color - シャドウ色 (32bit整数) + * @param {number} shadow_alpha - シャドウアルファ + * @param {number} blur_x - X方向ブラー量 + * @param {number} blur_y - Y方向ブラー量 + * @param {number} strength - ベベル強度 + * @param {number} quality - クオリティ + * @param {number} type - タイプ (0: full, 1: inner, 2: outer) + * @param {boolean} knockout - ノックアウトモード + * @param {number} device_pixel_ratio - デバイスピクセル比 + * @param {IFilterConfig} config - WebGPUリソース設定 + * @return {IAttachmentObject} - フィルター適用後のアタッチメント */ export const execute = ( - sourceAttachment: IAttachmentObject, + source_attachment: IAttachmentObject, matrix: Float32Array, distance: number, angle: number, - highlightColor: number, - highlightAlpha: number, - shadowColor: number, - shadowAlpha: number, - blurX: number, - blurY: number, + highlight_color: number, + highlight_alpha: number, + shadow_color: number, + shadow_alpha: number, + blur_x: number, + blur_y: number, strength: number, quality: number, type: number, knockout: boolean, - devicePixelRatio: number, + device_pixel_ratio: number, config: IFilterConfig ): IAttachmentObject => { @@ -60,8 +79,8 @@ export const execute = ( // 元のオフセットを保存 const baseOffsetX = $offset.x; const baseOffsetY = $offset.y; - const baseWidth = sourceAttachment.width; - const baseHeight = sourceAttachment.height; + const baseWidth = source_attachment.width; + const baseHeight = source_attachment.height; // スケールを計算 const xScale = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1]); @@ -69,8 +88,8 @@ export const execute = ( // オフセットを計算(WebGL版と同じ) const radian = angle * DEG_TO_RAD; - const x = Math.cos(radian) * distance * (xScale / devicePixelRatio); - const y = Math.sin(radian) * distance * (yScale / devicePixelRatio); + const x = Math.cos(radian) * distance * (xScale / device_pixel_ratio); + const y = Math.sin(radian) * distance * (yScale / device_pixel_ratio); // === Erase前処理:差分テクスチャを作成 === const eraseAttachment = frameBufferManager.createTemporaryAttachment(baseWidth, baseHeight); @@ -78,7 +97,7 @@ export const execute = ( // Step 1: ソーステクスチャを元の位置にコピー(erase前処理のcopyTextureToTextureは残す) commandEncoder.copyTextureToTexture( { - "texture": sourceAttachment.texture!.resource, + "texture": source_attachment.texture!.resource, "origin": { "x": 0, "y": 0, "z": 0 } }, { @@ -122,7 +141,7 @@ export const execute = ( ($entries3[0].resource as GPUBufferBinding).buffer = eraseUniformBuffer; $entries3[1].resource = eraseSampler; - $entries3[2].resource = sourceAttachment.texture!.view; + $entries3[2].resource = source_attachment.texture!.view; const eraseBindGroup = device.createBindGroup({ "layout": eraseBindGroupLayout, "entries": $entries3 @@ -142,8 +161,8 @@ export const execute = ( // === 差分テクスチャにブラーを適用 === const blurAttachment = filterApplyBlurFilterUseCase( eraseAttachment, matrix, - blurX, blurY, quality, - devicePixelRatio, config + blur_x, blur_y, quality, + device_pixel_ratio, config ); // eraseアタッチメントを解放 @@ -193,7 +212,7 @@ export const execute = ( if (!pipeline || !bindGroupLayout) { console.error("[WebGPU BevelFilter] Pipeline not found"); frameBufferManager.releaseTemporaryAttachment(blurAttachment); - return sourceAttachment; + return source_attachment; } // サンプラーを作成 @@ -206,8 +225,8 @@ export const execute = ( // baseScale, baseOffset (16 bytes) // blurScale, blurOffset (16 bytes) // Total: 80 bytes → 16 floats + 4 floats = 20 floats (80 bytes) - const [hr, hg, hb, ha] = intToPremultipliedRGBA(highlightColor, highlightAlpha); - const [sr, sg, sb, sa] = intToPremultipliedRGBA(shadowColor, shadowAlpha); + const [hr, hg, hb, ha] = intToPremultipliedRGBA(highlight_color, highlight_alpha); + const [sr, sg, sb, sa] = intToPremultipliedRGBA(shadow_color, shadow_alpha); $uniform20[0] = hr; $uniform20[1] = hg; @@ -244,7 +263,7 @@ export const execute = ( ($entries4[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries4[1].resource = sampler; $entries4[2].resource = blurAttachment.texture!.view; - $entries4[3].resource = sourceAttachment.texture!.view; + $entries4[3].resource = source_attachment.texture!.view; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries4 diff --git a/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts b/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts index c9cce695..67a97c91 100644 --- a/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts +++ b/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts @@ -21,29 +21,29 @@ const $entries3: GPUBindGroupEntry[] = [ * @description ブラーフィルターを適用 * Apply blur filter * - * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ(アタッチメント) + * @param {IAttachmentObject} source_attachment - 入力テクスチャ(アタッチメント) * @param {Float32Array} matrix - 変換行列 - * @param {number} blurX - X方向のブラー量 - * @param {number} blurY - Y方向のブラー量 + * @param {number} blur_x - X方向のブラー量 + * @param {number} blur_y - Y方向のブラー量 * @param {number} quality - クオリティ (1-15) - * @param {number} devicePixelRatio - デバイスピクセル比 + * @param {number} device_pixel_ratio - デバイスピクセル比 * @param {IFilterConfig} config - WebGPUリソース設定 * @return {IAttachmentObject} - フィルター適用後のアタッチメント */ export const execute = ( - sourceAttachment: IAttachmentObject, + source_attachment: IAttachmentObject, matrix: Float32Array, - blurX: number, - blurY: number, + blur_x: number, + blur_y: number, quality: number, - devicePixelRatio: number, + device_pixel_ratio: number, config: IFilterConfig ): IAttachmentObject => { const { device, commandEncoder, frameBufferManager, pipelineManager, textureManager } = config; // ブラーパラメータを計算 - const blurParams = calculateBlurParams(matrix, blurX, blurY, quality, devicePixelRatio); + const blurParams = calculateBlurParams(matrix, blur_x, blur_y, quality, device_pixel_ratio); const { baseBlurX, baseBlurY, offsetX, offsetY, bufferScaleX, bufferScaleY } = blurParams; // オフセットを更新 @@ -51,8 +51,8 @@ export const execute = ( $offset.y += offsetY; // ブラー用バッファサイズを計算 - const width = sourceAttachment.width + offsetX * 2; - const height = sourceAttachment.height + offsetY * 2; + const width = source_attachment.width + offsetX * 2; + const height = source_attachment.height + offsetY * 2; const bufferWidth = Math.ceil(width * bufferScaleX); const bufferHeight = Math.ceil(height * bufferScaleY); @@ -66,7 +66,7 @@ export const execute = ( // ソーステクスチャをattachment0にコピー(スケーリング付き) copyTextureToAttachment( device, commandEncoder, frameBufferManager, pipelineManager, - sourceAttachment, attachment0, sampler, + source_attachment, attachment0, sampler, bufferScaleX, bufferScaleY, offsetX * bufferScaleX, offsetY * bufferScaleY, config.bufferManager @@ -82,7 +82,7 @@ export const execute = ( for (let q = 0; q < quality; ++q) { // 水平ブラー - if (blurX > 0) { + if (blur_x > 0) { const srcIndex = attachmentIndex; attachmentIndex = (attachmentIndex + 1) % 2; @@ -94,7 +94,7 @@ export const execute = ( } // 垂直ブラー - if (blurY > 0) { + if (blur_y > 0) { const srcIndex = attachmentIndex; attachmentIndex = (attachmentIndex + 1) % 2; @@ -135,31 +135,39 @@ export const execute = ( /** * @description テクスチャをアタッチメントにコピー(オフセット位置に配置、スケーリング対応) + * Copy texture to attachment with offset placement and scaling support * - * @param source - ソーステクスチャ - * @param dest - デストテクスチャ(ソースより大きい) - * @param bufferScaleX - X方向のバッファスケール - * @param bufferScaleY - Y方向のバッファスケール - * @param pixelOffsetX - デスト内でのX方向オフセット(ピクセル単位、スケーリング済み) - * @param pixelOffsetY - デスト内でのY方向オフセット(ピクセル単位、スケーリング済み) + * @param {GPUDevice} device - GPUデバイス + * @param {GPUCommandEncoder} command_encoder - コマンドエンコーダー + * @param {IFilterConfig["frameBufferManager"]} frame_buffer_manager - フレームバッファマネージャー + * @param {IFilterConfig["pipelineManager"]} pipeline_manager - パイプラインマネージャー + * @param {IAttachmentObject} source - ソーステクスチャ + * @param {IAttachmentObject} dest - デストテクスチャ(ソースより大きい) + * @param {GPUSampler} sampler - サンプラー + * @param {number} buffer_scale_x - X方向のバッファスケール + * @param {number} buffer_scale_y - Y方向のバッファスケール + * @param {number} pixel_offset_x - デスト内でのX方向オフセット(ピクセル単位、スケーリング済み) + * @param {number} pixel_offset_y - デスト内でのY方向オフセット(ピクセル単位、スケーリング済み) + * @param {IFilterConfig["bufferManager"]} [buffer_manager] - バッファマネージャー + * @return {void} */ const copyTextureToAttachment = ( device: GPUDevice, - commandEncoder: GPUCommandEncoder, - frameBufferManager: IFilterConfig["frameBufferManager"], - pipelineManager: IFilterConfig["pipelineManager"], + command_encoder: GPUCommandEncoder, + frame_buffer_manager: IFilterConfig["frameBufferManager"], + pipeline_manager: IFilterConfig["pipelineManager"], source: IAttachmentObject, dest: IAttachmentObject, sampler: GPUSampler, - bufferScaleX: number, - bufferScaleY: number, - pixelOffsetX: number, - pixelOffsetY: number, - bufferManager?: IFilterConfig["bufferManager"] + buffer_scale_x: number, + buffer_scale_y: number, + pixel_offset_x: number, + pixel_offset_y: number, + buffer_manager?: IFilterConfig["bufferManager"] ): void => { // texture_copy_rgba8を使用し、ビューポートでオフセットを制御 - const pipeline = pipelineManager.getPipeline("texture_copy_rgba8"); - const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy"); + const pipeline = pipeline_manager.getPipeline("texture_copy_rgba8"); + const bindGroupLayout = pipeline_manager.getBindGroupLayout("texture_copy"); if (!pipeline || !bindGroupLayout) { console.error("[WebGPU BlurFilter] texture_copy_rgba8 pipeline not found"); @@ -167,8 +175,8 @@ const copyTextureToAttachment = ( } // デスト内でのソース描画サイズ(スケーリング後) - const scaledSourceWidth = source.width * bufferScaleX; - const scaledSourceHeight = source.height * bufferScaleY; + const scaledSourceWidth = source.width * buffer_scale_x; + const scaledSourceHeight = source.height * buffer_scale_y; // シェーダー: uv = texCoord * scale + offset // ソース全体をサンプリングするので scale = 1, offset = 0 @@ -182,13 +190,13 @@ const copyTextureToAttachment = ( $uniform4[1] = scaleY; $uniform4[2] = offsetX; $uniform4[3] = offsetY; - const uniformBuffer = bufferManager - ? bufferManager.acquireAndWriteUniformBuffer($uniform4) + const uniformBuffer = buffer_manager + ? buffer_manager.acquireAndWriteUniformBuffer($uniform4) : device.createBuffer({ "size": $uniform4.byteLength, "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); - if (!bufferManager) { + if (!buffer_manager) { device.queue.writeBuffer(uniformBuffer, 0, $uniform4); } @@ -200,22 +208,22 @@ const copyTextureToAttachment = ( "entries": $entries3 }); - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( + const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor( dest.texture!.view, 0, 0, 0, 0, "clear" ); - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); // ビューポートを設定してオフセット位置に描画 passEncoder.setViewport( - pixelOffsetX, pixelOffsetY, + pixel_offset_x, pixel_offset_y, scaledSourceWidth, scaledSourceHeight, 0, 1 ); passEncoder.setScissorRect( - Math.floor(pixelOffsetX), Math.floor(pixelOffsetY), + Math.floor(pixel_offset_x), Math.floor(pixel_offset_y), Math.ceil(scaledSourceWidth), Math.ceil(scaledSourceHeight) ); @@ -226,21 +234,34 @@ const copyTextureToAttachment = ( /** * @description 方向ブラーを適用 + * Apply directional blur pass + * + * @param {GPUDevice} device - GPUデバイス + * @param {GPUCommandEncoder} command_encoder - コマンドエンコーダー + * @param {IFilterConfig["frameBufferManager"]} frame_buffer_manager - フレームバッファマネージャー + * @param {IFilterConfig["pipelineManager"]} pipeline_manager - パイプラインマネージャー + * @param {IAttachmentObject} source - ソーステクスチャ + * @param {IAttachmentObject} dest - デストテクスチャ + * @param {GPUSampler} sampler - サンプラー + * @param {boolean} is_horizontal - 水平方向かどうか + * @param {number} blur - ブラー量 + * @param {IFilterConfig["bufferManager"]} [buffer_manager] - バッファマネージャー + * @return {void} */ const applyDirectionalBlur = ( device: GPUDevice, - commandEncoder: GPUCommandEncoder, - frameBufferManager: IFilterConfig["frameBufferManager"], - pipelineManager: IFilterConfig["pipelineManager"], + command_encoder: GPUCommandEncoder, + frame_buffer_manager: IFilterConfig["frameBufferManager"], + pipeline_manager: IFilterConfig["pipelineManager"], source: IAttachmentObject, dest: IAttachmentObject, sampler: GPUSampler, - isHorizontal: boolean, + is_horizontal: boolean, blur: number, - bufferManager?: IFilterConfig["bufferManager"] + buffer_manager?: IFilterConfig["bufferManager"] ): void => { const params = calculateDirectionalBlurParams( - isHorizontal, blur, + is_horizontal, blur, source.width, source.height ); @@ -248,8 +269,8 @@ const applyDirectionalBlur = ( // halfBlurに対応するパイプラインを取得(1〜16の範囲でクランプ) const clampedHalfBlur = Math.max(1, Math.min(16, halfBlur)); - const pipeline = pipelineManager.getPipeline(`blur_filter_${clampedHalfBlur}`); - const bindGroupLayout = pipelineManager.getBindGroupLayout("blur_filter"); + const pipeline = pipeline_manager.getPipeline(`blur_filter_${clampedHalfBlur}`); + const bindGroupLayout = pipeline_manager.getBindGroupLayout("blur_filter"); if (!pipeline || !bindGroupLayout) { console.error(`[WebGPU BlurFilter] blur_filter_${clampedHalfBlur} pipeline not found`); @@ -261,13 +282,13 @@ const applyDirectionalBlur = ( $uniform4[1] = offsetY; $uniform4[2] = fraction; $uniform4[3] = samples; - const uniformBuffer = bufferManager - ? bufferManager.acquireAndWriteUniformBuffer($uniform4) + const uniformBuffer = buffer_manager + ? buffer_manager.acquireAndWriteUniformBuffer($uniform4) : device.createBuffer({ "size": $uniform4.byteLength, "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); - if (!bufferManager) { + if (!buffer_manager) { device.queue.writeBuffer(uniformBuffer, 0, $uniform4); } @@ -279,11 +300,11 @@ const applyDirectionalBlur = ( "entries": $entries3 }); - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( + const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor( dest.texture!.view, 0, 0, 0, 0, "clear" ); - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.draw(6, 1, 0, 0); @@ -293,20 +314,31 @@ const applyDirectionalBlur = ( /** * @description テクスチャをアップスケール(ソース全体をデスト全体にマッピング) + * Upscale texture by mapping entire source to entire destination + * + * @param {GPUDevice} device - GPUデバイス + * @param {GPUCommandEncoder} command_encoder - コマンドエンコーダー + * @param {IFilterConfig["frameBufferManager"]} frame_buffer_manager - フレームバッファマネージャー + * @param {IFilterConfig["pipelineManager"]} pipeline_manager - パイプラインマネージャー + * @param {IAttachmentObject} source - ソーステクスチャ + * @param {IAttachmentObject} dest - デストテクスチャ + * @param {GPUSampler} sampler - サンプラー + * @param {IFilterConfig["bufferManager"]} [buffer_manager] - バッファマネージャー + * @return {void} */ const upscaleTexture = ( device: GPUDevice, - commandEncoder: GPUCommandEncoder, - frameBufferManager: IFilterConfig["frameBufferManager"], - pipelineManager: IFilterConfig["pipelineManager"], + command_encoder: GPUCommandEncoder, + frame_buffer_manager: IFilterConfig["frameBufferManager"], + pipeline_manager: IFilterConfig["pipelineManager"], source: IAttachmentObject, dest: IAttachmentObject, sampler: GPUSampler, - bufferManager?: IFilterConfig["bufferManager"] + buffer_manager?: IFilterConfig["bufferManager"] ): void => { // temp_アタッチメントはrgba8unormフォーマットなので、texture_copy_rgba8パイプラインを使用 - const pipeline = pipelineManager.getPipeline("texture_copy_rgba8"); - const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy"); + const pipeline = pipeline_manager.getPipeline("texture_copy_rgba8"); + const bindGroupLayout = pipeline_manager.getBindGroupLayout("texture_copy"); if (!pipeline || !bindGroupLayout) { console.error("[WebGPU BlurFilter] texture_copy_rgba8 pipeline not found"); @@ -320,13 +352,13 @@ const upscaleTexture = ( $uniform4[1] = 1; $uniform4[2] = 0; $uniform4[3] = 0; - const uniformBuffer = bufferManager - ? bufferManager.acquireAndWriteUniformBuffer($uniform4) + const uniformBuffer = buffer_manager + ? buffer_manager.acquireAndWriteUniformBuffer($uniform4) : device.createBuffer({ "size": $uniform4.byteLength, "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); - if (!bufferManager) { + if (!buffer_manager) { device.queue.writeBuffer(uniformBuffer, 0, $uniform4); } @@ -338,11 +370,11 @@ const upscaleTexture = ( "entries": $entries3 }); - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( + const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor( dest.texture!.view, 0, 0, 0, 0, "clear" ); - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.draw(6, 1, 0, 0); diff --git a/packages/webgpu/src/Filter/BlurFilterUseCase.ts b/packages/webgpu/src/Filter/BlurFilterUseCase.ts index 2954d3d1..5735d330 100644 --- a/packages/webgpu/src/Filter/BlurFilterUseCase.ts +++ b/packages/webgpu/src/Filter/BlurFilterUseCase.ts @@ -6,24 +6,27 @@ /** * @description ブラー計算用のステップ値 * Step values for blur calculation + * @type {number[]} */ -const BLUR_STEP: number[] = [0.5, 1.05, 1.4, 1.55, 1.75, 1.9, 2, 2.15, 2.2, 2.3, 2.5, 3, 3, 3.5, 3.5]; +const $BLUR_STEP: number[] = [0.5, 1.05, 1.4, 1.55, 1.75, 1.9, 2, 2.15, 2.2, 2.3, 2.5, 3, 3, 3.5, 3.5]; /** * @description ブラーフィルターパラメータを計算 - * @param {Float32Array} matrix - 変換行列 - * @param {number} blurX - X方向のブラー量 - * @param {number} blurY - Y方向のブラー量 - * @param {number} quality - クオリティ (1-15) - * @param {number} devicePixelRatio - デバイスピクセル比 + * Calculate blur filter parameters + * + * @param {Float32Array} matrix - 変換行列 + * @param {number} blur_x - X方向のブラー量 + * @param {number} blur_y - Y方向のブラー量 + * @param {number} quality - クオリティ (1-15) + * @param {number} device_pixel_ratio - デバイスピクセル比 * @return {object} */ export const calculateBlurParams = ( matrix: Float32Array, - blurX: number, - blurY: number, + blur_x: number, + blur_y: number, quality: number, - devicePixelRatio: number + device_pixel_ratio: number ): { baseBlurX: number; baseBlurY: number; @@ -35,10 +38,10 @@ export const calculateBlurParams = ( const xScale = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1]); const yScale = Math.sqrt(matrix[2] * matrix[2] + matrix[3] * matrix[3]); - const baseBlurX = blurX * (xScale / devicePixelRatio); - const baseBlurY = blurY * (yScale / devicePixelRatio); + const baseBlurX = blur_x * (xScale / device_pixel_ratio); + const baseBlurY = blur_y * (yScale / device_pixel_ratio); - const step = BLUR_STEP[Math.min(quality - 1, BLUR_STEP.length - 1)]; + const step = $BLUR_STEP[Math.min(quality - 1, $BLUR_STEP.length - 1)]; const offsetX = Math.round(baseBlurX * step); const offsetY = Math.round(baseBlurY * step); @@ -78,17 +81,19 @@ export const calculateBlurParams = ( /** * @description 方向ブラーのパラメータを計算 - * @param {boolean} isHorizontal - 水平方向かどうか - * @param {number} blur - ブラー量 - * @param {number} textureWidth - テクスチャ幅 - * @param {number} textureHeight - テクスチャ高さ + * Calculate directional blur parameters + * + * @param {boolean} is_horizontal - 水平方向かどうか + * @param {number} blur - ブラー量 + * @param {number} texture_width - テクスチャ幅 + * @param {number} texture_height - テクスチャ高さ * @return {object} */ export const calculateDirectionalBlurParams = ( - isHorizontal: boolean, + is_horizontal: boolean, blur: number, - textureWidth: number, - textureHeight: number + texture_width: number, + texture_height: number ): { offsetX: number; offsetY: number; @@ -101,8 +106,8 @@ export const calculateDirectionalBlurParams = ( const samples = 1 + blur; // テクセルオフセットを計算 - const offsetX = isHorizontal ? 1 / textureWidth : 0; - const offsetY = isHorizontal ? 0 : 1 / textureHeight; + const offsetX = is_horizontal ? 1 / texture_width : 0; + const offsetY = is_horizontal ? 0 : 1 / texture_height; return { offsetX, diff --git a/packages/webgpu/src/Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase.ts b/packages/webgpu/src/Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase.ts index b9ef9282..3ae6c859 100644 --- a/packages/webgpu/src/Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase.ts +++ b/packages/webgpu/src/Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase.ts @@ -19,13 +19,13 @@ const $entries3: GPUBindGroupEntry[] = [ * @description カラーマトリックスフィルターを適用 * Apply color matrix filter * - * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ(アタッチメント) + * @param {IAttachmentObject} source_attachment - 入力テクスチャ(アタッチメント) * @param {Float32Array} matrix - 4x5カラーマトリックス (20 floats) * @param {IFilterConfig} config - WebGPUリソース設定 * @return {IAttachmentObject} - フィルター適用後のアタッチメント */ export const execute = ( - sourceAttachment: IAttachmentObject, + source_attachment: IAttachmentObject, matrix: Float32Array, config: IFilterConfig ): IAttachmentObject => { @@ -34,8 +34,8 @@ export const execute = ( // 出力アタッチメントを作成 const destAttachment = frameBufferManager.createTemporaryAttachment( - sourceAttachment.width, - sourceAttachment.height + source_attachment.width, + source_attachment.height ); const pipeline = pipelineManager.getPipeline("color_matrix_filter"); @@ -43,7 +43,7 @@ export const execute = ( if (!pipeline || !bindGroupLayout) { console.error("[WebGPU ColorMatrixFilter] Pipeline not found"); - return sourceAttachment; + return source_attachment; } // サンプラーを作成 @@ -93,7 +93,7 @@ export const execute = ( // バインドグループを作成 ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; - $entries3[2].resource = sourceAttachment.texture!.view; + $entries3[2].resource = source_attachment.texture!.view; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries3 diff --git a/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts b/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts index 16b951d5..f5d8bb0e 100644 --- a/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts +++ b/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts @@ -22,15 +22,29 @@ const $pipelineCache = new Map { const { device, commandEncoder, frameBufferManager, textureManager } = config; - const width = sourceAttachment.width; - const height = sourceAttachment.height; + const width = source_attachment.width; + const height = source_attachment.height; // WebGL版と同じ: baseWidth/baseHeightはビットマップサイズを使用 - const baseWidth = bitmapWidth; - const baseHeight = bitmapHeight; + const baseWidth = bitmap_width; + const baseHeight = bitmap_height; // 出力アタッチメントを作成 const destAttachment = frameBufferManager.createTemporaryAttachment(width, height); // マップテクスチャを作成 const mapTexture = device.createTexture({ - "size": { "width": bitmapWidth, "height": bitmapHeight }, + "size": { "width": bitmap_width, "height": bitmap_height }, "format": "rgba8unorm", "usage": GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST }); device.queue.writeTexture( { "texture": mapTexture }, - bitmapBuffer.buffer, - { "bytesPerRow": bitmapWidth * 4, "offset": bitmapBuffer.byteOffset }, - { "width": bitmapWidth, "height": bitmapHeight } + bitmap_buffer.buffer, + { "bytesPerRow": bitmap_width * 4, "offset": bitmap_buffer.byteOffset }, + { "width": bitmap_width, "height": bitmap_height } ); // パイプラインをキャッシュから取得または作成 - const cacheKey = `${componentX},${componentY},${mode}`; + const cacheKey = `${component_x},${component_y},${mode}`; let cached = $pipelineCache.get(cacheKey); if (!cached) { const fragmentShaderCode = ShaderSource.getDisplacementMapFilterFragmentShader( - componentX, componentY, mode + component_x, component_y, mode ); const vertexShaderModule = device.createShaderModule({ @@ -162,16 +181,16 @@ export const execute = ( const uniformSize = needsSubstituteColor ? 48 : 32; // uvToStScale - $uniform12[0] = baseWidth / bitmapWidth; - $uniform12[1] = baseHeight / bitmapHeight; + $uniform12[0] = baseWidth / bitmap_width; + $uniform12[1] = baseHeight / bitmap_height; // uvToStOffset - $uniform12[2] = mapPointX / bitmapWidth; - $uniform12[3] = (baseHeight - bitmapHeight - mapPointY) / bitmapHeight; + $uniform12[2] = map_point_x / bitmap_width; + $uniform12[3] = (baseHeight - bitmap_height - map_point_y) / bitmap_height; // scale - $uniform12[4] = scaleX / baseWidth; - $uniform12[5] = scaleY / baseHeight; + $uniform12[4] = scale_x / baseWidth; + $uniform12[5] = scale_y / baseHeight; // padding $uniform12[6] = 0; @@ -199,7 +218,7 @@ export const execute = ( // バインドグループを作成 ($entries4[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries4[1].resource = sampler; - $entries4[2].resource = sourceAttachment.texture!.view; + $entries4[2].resource = source_attachment.texture!.view; $entries4[3].resource = mapTexture.createView(); const bindGroup = device.createBindGroup({ "layout": cached.bindGroupLayout, diff --git a/packages/webgpu/src/Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase.ts b/packages/webgpu/src/Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase.ts index 8c769005..841c4054 100644 --- a/packages/webgpu/src/Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase.ts +++ b/packages/webgpu/src/Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase.ts @@ -23,38 +23,38 @@ const $entries4: GPUBindGroupEntry[] = [ * @description ドロップシャドウフィルターを適用 * Apply drop shadow filter * - * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ + * @param {IAttachmentObject} source_attachment - 入力テクスチャ * @param {Float32Array} matrix - 変換行列 * @param {number} distance - シャドウの距離 * @param {number} angle - シャドウの角度(度) * @param {number} color - シャドウ色 (32bit整数) * @param {number} alpha - アルファ - * @param {number} blurX - X方向ブラー量 - * @param {number} blurY - Y方向ブラー量 + * @param {number} blur_x - X方向ブラー量 + * @param {number} blur_y - Y方向ブラー量 * @param {number} strength - シャドウ強度 * @param {number} quality - クオリティ * @param {boolean} inner - インナーシャドウ * @param {boolean} knockout - ノックアウトモード - * @param {boolean} hideObject - 元オブジェクトを隠す - * @param {number} devicePixelRatio - デバイスピクセル比 - * @param {IDropShadowConfig} config - WebGPUリソース設定 + * @param {boolean} hide_object - 元オブジェクトを隠す + * @param {number} device_pixel_ratio - デバイスピクセル比 + * @param {IFilterConfig} config - WebGPUリソース設定 * @return {IAttachmentObject} - フィルター適用後のアタッチメント */ export const execute = ( - sourceAttachment: IAttachmentObject, + source_attachment: IAttachmentObject, matrix: Float32Array, distance: number, angle: number, color: number, alpha: number, - blurX: number, - blurY: number, + blur_x: number, + blur_y: number, strength: number, quality: number, inner: boolean, knockout: boolean, - hideObject: boolean, - devicePixelRatio: number, + hide_object: boolean, + device_pixel_ratio: number, config: IFilterConfig ): IAttachmentObject => { @@ -63,14 +63,14 @@ export const execute = ( // 元のオフセットを保存 const baseOffsetX = $offset.x; const baseOffsetY = $offset.y; - const baseWidth = sourceAttachment.width; - const baseHeight = sourceAttachment.height; + const baseWidth = source_attachment.width; + const baseHeight = source_attachment.height; // ブラーフィルターを適用 const blurAttachment = filterApplyBlurFilterUseCase( - sourceAttachment, matrix, - blurX, blurY, quality, - devicePixelRatio, config + source_attachment, matrix, + blur_x, blur_y, quality, + device_pixel_ratio, config ); const blurWidth = blurAttachment.width; @@ -87,8 +87,8 @@ export const execute = ( // シャドウのオフセットを計算 const radian = angle * DEG_TO_RAD; - const shadowX = Math.cos(radian) * distance * (xScale / devicePixelRatio); - const shadowY = Math.sin(radian) * distance * (yScale / devicePixelRatio); + const shadowX = Math.cos(radian) * distance * (xScale / device_pixel_ratio); + const shadowY = Math.sin(radian) * distance * (yScale / device_pixel_ratio); // 出力キャンバスのサイズを計算 const w = inner ? baseWidth : blurWidth + Math.max(0, Math.abs(shadowX) - offsetDiffX); @@ -110,11 +110,11 @@ export const execute = ( // タイプとノックアウト状態を決定 const isInner = inner; let isKnockout = knockout; - let isHideObject = hideObject; + let isHideObject = hide_object; if (inner) { - isKnockout = knockout || hideObject; - } else if (!knockout && hideObject) { + isKnockout = knockout || hide_object; + } else if (!knockout && hide_object) { // フルモード(シャドウのみ表示) isKnockout = true; isHideObject = true; @@ -130,7 +130,7 @@ export const execute = ( if (!pipeline || !bindGroupLayout) { console.error("[WebGPU DropShadowFilter] Pipeline not found"); frameBufferManager.releaseTemporaryAttachment(blurAttachment); - return sourceAttachment; + return source_attachment; } // サンプラーを作成 @@ -194,7 +194,7 @@ export const execute = ( ($entries4[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries4[1].resource = sampler; $entries4[2].resource = blurAttachment.texture!.view; - $entries4[3].resource = sourceAttachment.texture!.view; + $entries4[3].resource = source_attachment.texture!.view; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries4 diff --git a/packages/webgpu/src/Filter/FilterGradientLUTCache.ts b/packages/webgpu/src/Filter/FilterGradientLUTCache.ts index f2fd19d7..1722dfc2 100644 --- a/packages/webgpu/src/Filter/FilterGradientLUTCache.ts +++ b/packages/webgpu/src/Filter/FilterGradientLUTCache.ts @@ -15,6 +15,8 @@ let $filterGradientAttachment: IAttachmentObject | null = null; /** * @description GPUDeviceの参照 + * Reference to GPUDevice + * @type {GPUDevice | null} * @private */ let $device: GPUDevice | null = null; diff --git a/packages/webgpu/src/Filter/GlowFilter/FilterApplyGlowFilterUseCase.ts b/packages/webgpu/src/Filter/GlowFilter/FilterApplyGlowFilterUseCase.ts index c4b06255..4120f9eb 100644 --- a/packages/webgpu/src/Filter/GlowFilter/FilterApplyGlowFilterUseCase.ts +++ b/packages/webgpu/src/Filter/GlowFilter/FilterApplyGlowFilterUseCase.ts @@ -26,32 +26,32 @@ const $entries4: GPUBindGroupEntry[] = [ * UV変換方式で元テクスチャとブラーテクスチャを直接サンプリング。 * copyTextureToTextureと一時テクスチャを使用しない最適化版。 * - * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ + * @param {IAttachmentObject} source_attachment - 入力テクスチャ * @param {Float32Array} matrix - 変換行列 * @param {number} color - グロー色 (32bit整数) * @param {number} alpha - アルファ - * @param {number} blurX - X方向ブラー量 - * @param {number} blurY - Y方向ブラー量 + * @param {number} blur_x - X方向ブラー量 + * @param {number} blur_y - Y方向ブラー量 * @param {number} strength - グロー強度 * @param {number} quality - クオリティ * @param {boolean} inner - インナーグロー * @param {boolean} knockout - ノックアウトモード - * @param {number} devicePixelRatio - デバイスピクセル比 + * @param {number} device_pixel_ratio - デバイスピクセル比 * @param {IFilterConfig} config - WebGPUリソース設定 * @return {IAttachmentObject} - フィルター適用後のアタッチメント */ export const execute = ( - sourceAttachment: IAttachmentObject, + source_attachment: IAttachmentObject, matrix: Float32Array, color: number, alpha: number, - blurX: number, - blurY: number, + blur_x: number, + blur_y: number, strength: number, quality: number, inner: boolean, knockout: boolean, - devicePixelRatio: number, + device_pixel_ratio: number, config: IFilterConfig ): IAttachmentObject => { @@ -60,13 +60,13 @@ export const execute = ( // 元のオフセットを保存 const baseOffsetX = $offset.x; const baseOffsetY = $offset.y; - const baseWidth = sourceAttachment.width; - const baseHeight = sourceAttachment.height; + const baseWidth = source_attachment.width; + const baseHeight = source_attachment.height; const blurAttachment = filterApplyBlurFilterUseCase( - sourceAttachment, matrix, - blurX, blurY, quality, - devicePixelRatio, config + source_attachment, matrix, + blur_x, blur_y, quality, + device_pixel_ratio, config ); const blurWidth = blurAttachment.width; @@ -110,7 +110,7 @@ export const execute = ( if (!pipeline || !bindGroupLayout) { console.error("[WebGPU GlowFilter] Pipeline not found"); frameBufferManager.releaseTemporaryAttachment(blurAttachment); - return sourceAttachment; + return source_attachment; } // サンプラーを作成 @@ -154,7 +154,7 @@ export const execute = ( ($entries4[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries4[1].resource = sampler; $entries4[2].resource = blurAttachment.texture!.view; - $entries4[3].resource = sourceAttachment.texture!.view; + $entries4[3].resource = source_attachment.texture!.view; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries4 diff --git a/packages/webgpu/src/Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase.ts b/packages/webgpu/src/Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase.ts index 1cd58a7f..a7776e85 100644 --- a/packages/webgpu/src/Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase.ts +++ b/packages/webgpu/src/Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase.ts @@ -40,38 +40,38 @@ const $entries5: GPUBindGroupEntry[] = [ * 2. ベベルベースにブラー適用 * 3. UV変換方式で最終合成(isInsideでハード境界クリッピング) * - * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ + * @param {IAttachmentObject} source_attachment - 入力テクスチャ * @param {Float32Array} matrix - 変換行列 * @param {number} distance - ベベルの距離 * @param {number} angle - ベベルの角度(度) * @param {Float32Array} colors - 色配列 * @param {Float32Array} alphas - アルファ配列 * @param {Float32Array} ratios - 比率配列 - * @param {number} blurX - X方向ブラー量 - * @param {number} blurY - Y方向ブラー量 + * @param {number} blur_x - X方向ブラー量 + * @param {number} blur_y - Y方向ブラー量 * @param {number} strength - ベベル強度 * @param {number} quality - クオリティ * @param {number} type - タイプ (0: full, 1: inner, 2: outer) * @param {boolean} knockout - ノックアウトモード - * @param {number} devicePixelRatio - デバイスピクセル比 - * @param {IGradientBevelConfig} config - WebGPUリソース設定 + * @param {number} device_pixel_ratio - デバイスピクセル比 + * @param {IFilterConfig} config - WebGPUリソース設定 * @return {IAttachmentObject} - フィルター適用後のアタッチメント */ export const execute = ( - sourceAttachment: IAttachmentObject, + source_attachment: IAttachmentObject, matrix: Float32Array, distance: number, angle: number, colors: Float32Array, alphas: Float32Array, ratios: Float32Array, - blurX: number, - blurY: number, + blur_x: number, + blur_y: number, strength: number, quality: number, type: number, knockout: boolean, - devicePixelRatio: number, + device_pixel_ratio: number, config: IFilterConfig ): IAttachmentObject => { @@ -80,8 +80,8 @@ export const execute = ( // 元のオフセットを保存 const baseOffsetX = $offset.x; const baseOffsetY = $offset.y; - const baseWidth = sourceAttachment.width; - const baseHeight = sourceAttachment.height; + const baseWidth = source_attachment.width; + const baseHeight = source_attachment.height; // 変換行列からスケールを取得 const xScale = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1]); @@ -89,8 +89,8 @@ export const execute = ( // ベベルのオフセットを計算 const radian = angle * DEG_TO_RAD; - const x = Math.cos(radian) * distance * (xScale / devicePixelRatio); - const y = Math.sin(radian) * distance * (yScale / devicePixelRatio); + const x = Math.cos(radian) * distance * (xScale / device_pixel_ratio); + const y = Math.sin(radian) * distance * (yScale / device_pixel_ratio); // ===== Step 1: ベベルベーステクスチャ作成 ===== // WebGL版と同じ: original * (1 - shifted_original.a) @@ -100,7 +100,7 @@ export const execute = ( if (!bevelBasePipeline || !bevelBaseLayout) { console.error("[WebGPU GradientBevelFilter] bevel_base pipeline not found"); - return sourceAttachment; + return source_attachment; } const bevelBaseAttachment = frameBufferManager.createTemporaryAttachment(baseWidth, baseHeight); @@ -124,7 +124,7 @@ export const execute = ( ($entries3[0].resource as GPUBufferBinding).buffer = bevelBaseUniformBuffer; $entries3[1].resource = bevelBaseSampler; - $entries3[2].resource = sourceAttachment.texture!.view; + $entries3[2].resource = source_attachment.texture!.view; const bevelBaseBindGroup = device.createBindGroup({ "layout": bevelBaseLayout, "entries": $entries3 @@ -144,8 +144,8 @@ export const execute = ( // WebGL版と同じ: bevelBaseをブラーする(元テクスチャではなく) const blurAttachment = filterApplyBlurFilterUseCase( bevelBaseAttachment, matrix, - blurX, blurY, quality, - devicePixelRatio, config + blur_x, blur_y, quality, + device_pixel_ratio, config ); // ベベルベースは不要になったので解放 @@ -219,7 +219,7 @@ export const execute = ( if (!pipeline || !bindGroupLayout) { console.error("[WebGPU GradientBevelFilter] Pipeline not found"); frameBufferManager.releaseTemporaryAttachment(blurAttachment); - return sourceAttachment; + return source_attachment; } const sampler = textureManager.createSampler("gradient_bevel_sampler", true); @@ -252,7 +252,7 @@ export const execute = ( ($entries5[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries5[1].resource = sampler; $entries5[2].resource = blurAttachment.texture!.view; - $entries5[3].resource = sourceAttachment.texture!.view; + $entries5[3].resource = source_attachment.texture!.view; $entries5[4].resource = lutView; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, diff --git a/packages/webgpu/src/Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase.ts b/packages/webgpu/src/Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase.ts index cf943840..521a1bf4 100644 --- a/packages/webgpu/src/Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase.ts +++ b/packages/webgpu/src/Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase.ts @@ -30,38 +30,38 @@ const $entries5: GPUBindGroupEntry[] = [ * 2. グラデーションLUT生成(専用テクスチャ) * 3. UV変換方式で最終合成(isInsideでハード境界クリッピング) * - * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ + * @param {IAttachmentObject} source_attachment - 入力テクスチャ * @param {Float32Array} matrix - 変換行列 * @param {number} distance - グローの距離 * @param {number} angle - グローの角度(度) * @param {Float32Array} colors - 色配列 * @param {Float32Array} alphas - アルファ配列 * @param {Float32Array} ratios - 比率配列 - * @param {number} blurX - X方向ブラー量 - * @param {number} blurY - Y方向ブラー量 + * @param {number} blur_x - X方向ブラー量 + * @param {number} blur_y - Y方向ブラー量 * @param {number} strength - グロー強度 * @param {number} quality - クオリティ * @param {number} type - タイプ (0: full, 1: inner, 2: outer) * @param {boolean} knockout - ノックアウトモード - * @param {number} devicePixelRatio - デバイスピクセル比 + * @param {number} device_pixel_ratio - デバイスピクセル比 * @param {IFilterConfig} config - WebGPUリソース設定 * @return {IAttachmentObject} - フィルター適用後のアタッチメント */ export const execute = ( - sourceAttachment: IAttachmentObject, + source_attachment: IAttachmentObject, matrix: Float32Array, distance: number, angle: number, colors: Float32Array, alphas: Float32Array, ratios: Float32Array, - blurX: number, - blurY: number, + blur_x: number, + blur_y: number, strength: number, quality: number, type: number, knockout: boolean, - devicePixelRatio: number, + device_pixel_ratio: number, config: IFilterConfig ): IAttachmentObject => { @@ -70,14 +70,14 @@ export const execute = ( // 元のオフセットを保存 const baseOffsetX = $offset.x; const baseOffsetY = $offset.y; - const baseWidth = sourceAttachment.width; - const baseHeight = sourceAttachment.height; + const baseWidth = source_attachment.width; + const baseHeight = source_attachment.height; // ブラーフィルターを適用 const blurAttachment = filterApplyBlurFilterUseCase( - sourceAttachment, matrix, - blurX, blurY, quality, - devicePixelRatio, config + source_attachment, matrix, + blur_x, blur_y, quality, + device_pixel_ratio, config ); const blurWidth = blurAttachment.width; @@ -94,8 +94,8 @@ export const execute = ( // グローのオフセットを計算 const radian = angle * DEG_TO_RAD; - const x = Math.cos(radian) * distance * (xScale / devicePixelRatio); - const y = Math.sin(radian) * distance * (yScale / devicePixelRatio); + const x = Math.cos(radian) * distance * (xScale / device_pixel_ratio); + const y = Math.sin(radian) * distance * (yScale / device_pixel_ratio); // ===== WebGL版と同じサイズ・位置計算 ===== const isInner = type === 1; @@ -154,7 +154,7 @@ export const execute = ( if (!pipeline || !bindGroupLayout) { console.error("[WebGPU GradientGlowFilter] Pipeline not found"); frameBufferManager.releaseTemporaryAttachment(blurAttachment); - return sourceAttachment; + return source_attachment; } const sampler = textureManager.createSampler("gradient_glow_sampler", true); @@ -187,7 +187,7 @@ export const execute = ( ($entries5[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries5[1].resource = sampler; $entries5[2].resource = blurAttachment.texture!.view; - $entries5[3].resource = sourceAttachment.texture!.view; + $entries5[3].resource = source_attachment.texture!.view; $entries5[4].resource = lutView; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, diff --git a/packages/webgpu/src/FrameBufferManager.ts b/packages/webgpu/src/FrameBufferManager.ts index 632edbb4..09417273 100644 --- a/packages/webgpu/src/FrameBufferManager.ts +++ b/packages/webgpu/src/FrameBufferManager.ts @@ -6,6 +6,10 @@ import { execute as frameBufferManagerCreateRenderPassDescriptorService } from " import { execute as frameBufferManagerCreateStencilRenderPassDescriptorService } from "./FrameBufferManager/service/FrameBufferManagerCreateStencilRenderPassDescriptorService"; import { TexturePool } from "./TexturePool"; +/** + * @description フレームバッファとアタッチメントの管理クラス + * Manager class for frame buffers and attachments + */ export class FrameBufferManager { private device: GPUDevice; @@ -16,6 +20,12 @@ export class FrameBufferManager private texturePool: TexturePool; private pendingReleases: IAttachmentObject[] = []; + /** + * @description FrameBufferManagerのコンストラクタ + * Constructor for FrameBufferManager + * @param {GPUDevice} device - WebGPUデバイス / WebGPU device + * @param {GPUTextureFormat} format - テクスチャフォーマット / Texture format + */ constructor(device: GPUDevice, format: GPUTextureFormat) { this.device = device; @@ -26,11 +36,26 @@ export class FrameBufferManager this.texturePool = new TexturePool(device); } + /** + * @description フレームの開始処理を行う + * Begin a new frame + * @return {void} + */ beginFrame(): void { this.texturePool.beginFrame(); } + /** + * @description 新しいアタッチメントを作成する + * Create a new attachment + * @param {string} name - アタッチメント名 / Attachment name + * @param {number} width - テクスチャの幅 / Texture width + * @param {number} height - テクスチャの高さ / Texture height + * @param {boolean} [msaa=false] - MSAAを有効にするか / Whether to enable MSAA + * @param {boolean} [mask=false] - マスクを有効にするか / Whether to enable mask + * @return {IAttachmentObject} + */ createAttachment( name: string, width: number, @@ -52,49 +77,94 @@ export class FrameBufferManager ); } + /** + * @description 名前でアタッチメントを取得する + * Get an attachment by name + * @param {string} name - アタッチメント名 / Attachment name + * @return {IAttachmentObject | undefined} + */ getAttachment(name: string): IAttachmentObject | undefined { return this.attachments.get(name); } + /** + * @description 現在のアタッチメントを設定する + * Set the current attachment + * @param {IAttachmentObject | null} attachment - 設定するアタッチメント / Attachment to set + * @return {void} + */ setCurrentAttachment(attachment: IAttachmentObject | null): void { this.currentAttachment = attachment; } + /** + * @description 現在のアタッチメントを取得する + * Get the current attachment + * @return {IAttachmentObject | null} + */ getCurrentAttachment(): IAttachmentObject | null { return this.currentAttachment; } + /** + * @description レンダーパスディスクリプタを作成する + * Create a render pass descriptor + * @param {GPUTextureView} view - カラーテクスチャビュー / Color texture view + * @param {number} [r=0] - クリアカラーR値 / Clear color R value + * @param {number} [g=0] - クリアカラーG値 / Clear color G value + * @param {number} [b=0] - クリアカラーB値 / Clear color B value + * @param {number} [a=0] - クリアカラーA値 / Clear color A value + * @param {GPULoadOp} [load_op="clear"] - ロードオペレーション / Load operation + * @param {GPUTextureView|null} [resolve_target=null] - MSAAリゾルブターゲット / MSAA resolve target + * @return {GPURenderPassDescriptor} + */ createRenderPassDescriptor( view: GPUTextureView, r: number = 0, g: number = 0, b: number = 0, a: number = 0, - loadOp: GPULoadOp = "clear", - resolveTarget: GPUTextureView | null = null + load_op: GPULoadOp = "clear", + resolve_target: GPUTextureView | null = null ): GPURenderPassDescriptor { - return frameBufferManagerCreateRenderPassDescriptorService(view, r, g, b, a, loadOp, resolveTarget); + return frameBufferManagerCreateRenderPassDescriptorService(view, r, g, b, a, load_op, resolve_target); } + /** + * @description ステンシル付きレンダーパスディスクリプタを作成する + * Create a render pass descriptor with stencil + * @param {GPUTextureView} color_view - カラーテクスチャビュー / Color texture view + * @param {GPUTextureView} stencil_view - ステンシルテクスチャビュー / Stencil texture view + * @param {GPULoadOp} [color_load_op="load"] - カラーロードオペレーション / Color load operation + * @param {GPULoadOp} [stencil_load_op="clear"] - ステンシルロードオペレーション / Stencil load operation + * @param {GPUTextureView|null} [resolve_target=null] - MSAAリゾルブターゲット / MSAA resolve target + * @return {GPURenderPassDescriptor} + */ createStencilRenderPassDescriptor( - colorView: GPUTextureView, - stencilView: GPUTextureView, - colorLoadOp: GPULoadOp = "load", - stencilLoadOp: GPULoadOp = "clear", - resolveTarget: GPUTextureView | null = null + color_view: GPUTextureView, + stencil_view: GPUTextureView, + color_load_op: GPULoadOp = "load", + stencil_load_op: GPULoadOp = "clear", + resolve_target: GPUTextureView | null = null ): GPURenderPassDescriptor { return frameBufferManagerCreateStencilRenderPassDescriptorService( - colorView, - stencilView, - colorLoadOp, - stencilLoadOp, - resolveTarget + color_view, + stencil_view, + color_load_op, + stencil_load_op, + resolve_target ); } + /** + * @description アタッチメントを破棄する + * Destroy an attachment by name + * @param {string} name - アタッチメント名 / Attachment name + * @return {void} + */ destroyAttachment(name: string): void { const attachment = this.attachments.get(name); @@ -109,12 +179,27 @@ export class FrameBufferManager } } + /** + * @description アタッチメントをリサイズする + * Resize an attachment + * @param {string} name - アタッチメント名 / Attachment name + * @param {number} width - 新しい幅 / New width + * @param {number} height - 新しい高さ / New height + * @return {IAttachmentObject} + */ resizeAttachment(name: string, width: number, height: number): IAttachmentObject { this.destroyAttachment(name); return this.createAttachment(name, width, height); } + /** + * @description 一時的なアタッチメントを作成する + * Create a temporary attachment + * @param {number} width - テクスチャの幅 / Texture width + * @param {number} height - テクスチャの高さ / Texture height + * @return {IAttachmentObject} + */ createTemporaryAttachment(width: number, height: number): IAttachmentObject { const name = `temp_${this.idCounter.nextId}`; @@ -155,6 +240,12 @@ export class FrameBufferManager return attachment; } + /** + * @description 一時アタッチメントをリリースキューに追加する + * Add a temporary attachment to the release queue + * @param {IAttachmentObject} attachment - リリースするアタッチメント / Attachment to release + * @return {void} + */ releaseTemporaryAttachment(attachment: IAttachmentObject): void { frameBufferManagerReleaseTemporaryAttachmentUseCase( @@ -164,6 +255,11 @@ export class FrameBufferManager ); } + /** + * @description 保留中のリリースを実行する + * Flush all pending releases + * @return {void} + */ flushPendingReleases(): void { for (const att of this.pendingReleases) { @@ -183,6 +279,11 @@ export class FrameBufferManager this.pendingReleases = []; } + /** + * @description 全リソースを破棄する + * Dispose all resources + * @return {void} + */ dispose(): void { for (const attachment of this.attachments.values()) { diff --git a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateRenderPassDescriptorService.ts b/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateRenderPassDescriptorService.ts index d3355a92..13d10558 100644 --- a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateRenderPassDescriptorService.ts +++ b/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateRenderPassDescriptorService.ts @@ -1,16 +1,45 @@ +/** + * @description クリアカラー値のプリアロケートオブジェクト + * Pre-allocated clear color value object + * @type {GPUColorDict} + */ const $clearValue: GPUColorDict = { "r": 0, "g": 0, "b": 0, "a": 0 }; + +/** + * @description カラーアタッチメントのプリアロケートオブジェクト + * Pre-allocated color attachment object + * @type {GPURenderPassColorAttachment} + */ const $colorAttachment: GPURenderPassColorAttachment = { "view": null as unknown as GPUTextureView, "clearValue": $clearValue, "loadOp": "clear", "storeOp": "store" }; + +/** + * @description レンダーパス記述子のプリアロケートオブジェクト + * Pre-allocated render pass descriptor object + * @type {GPURenderPassDescriptor} + */ const $descriptor: GPURenderPassDescriptor = { "colorAttachments": [$colorAttachment] }; /** * @description レンダーパス記述子を作成(プリアロケート再利用) + * Create render pass descriptor (pre-allocated reuse) + * + * @param {GPUTextureView} view - レンダーターゲットのテクスチャビュー + * @param {number} r - クリアカラーの赤成分 + * @param {number} g - クリアカラーの緑成分 + * @param {number} b - クリアカラーの青成分 + * @param {number} a - クリアカラーのアルファ成分 + * @param {GPULoadOp} load_op - ロード操作 + * @param {GPUTextureView | null} resolve_target - MSAAリゾルブターゲット + * @return {GPURenderPassDescriptor} + * @method + * @protected */ export const execute = ( view: GPUTextureView, @@ -18,15 +47,15 @@ export const execute = ( g: number = 0, b: number = 0, a: number = 0, - loadOp: GPULoadOp = "clear", - resolveTarget: GPUTextureView | null = null + load_op: GPULoadOp = "clear", + resolve_target: GPUTextureView | null = null ): GPURenderPassDescriptor => { $colorAttachment.view = view; $clearValue.r = r; $clearValue.g = g; $clearValue.b = b; $clearValue.a = a; - $colorAttachment.loadOp = loadOp; - $colorAttachment.resolveTarget = resolveTarget ?? undefined; + $colorAttachment.loadOp = load_op; + $colorAttachment.resolveTarget = resolve_target ?? undefined; return $descriptor; }; diff --git a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateStencilRenderPassDescriptorService.ts b/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateStencilRenderPassDescriptorService.ts index 0a592389..cb3bff17 100644 --- a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateStencilRenderPassDescriptorService.ts +++ b/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateStencilRenderPassDescriptorService.ts @@ -1,16 +1,39 @@ +/** + * @description クリアカラー値のプリアロケートオブジェクト + * Pre-allocated clear color value object + * @type {GPUColorDict} + */ const $clearValue: GPUColorDict = { "r": 0, "g": 0, "b": 0, "a": 0 }; + +/** + * @description カラーアタッチメントのプリアロケートオブジェクト + * Pre-allocated color attachment object + * @type {GPURenderPassColorAttachment} + */ const $colorAttachment: GPURenderPassColorAttachment = { "view": null as unknown as GPUTextureView, "clearValue": $clearValue, "loadOp": "load", "storeOp": "store" }; + +/** + * @description 深度ステンシルアタッチメントのプリアロケートオブジェクト + * Pre-allocated depth stencil attachment object + * @type {GPURenderPassDepthStencilAttachment} + */ const $depthStencilAttachment: GPURenderPassDepthStencilAttachment = { "view": null as unknown as GPUTextureView, "stencilClearValue": 0, "stencilLoadOp": "clear", "stencilStoreOp": "store" }; + +/** + * @description ステンシル付きレンダーパス記述子のプリアロケートオブジェクト + * Pre-allocated render pass descriptor with stencil + * @type {GPURenderPassDescriptor} + */ const $descriptor: GPURenderPassDescriptor = { "colorAttachments": [$colorAttachment], "depthStencilAttachment": $depthStencilAttachment @@ -18,18 +41,28 @@ const $descriptor: GPURenderPassDescriptor = { /** * @description ステンシル付きレンダーパス記述子を作成(プリアロケート再利用) + * Create render pass descriptor with stencil (pre-allocated reuse) + * + * @param {GPUTextureView} color_view - カラーアタッチメントのテクスチャビュー + * @param {GPUTextureView} stencil_view - ステンシルアタッチメントのテクスチャビュー + * @param {GPULoadOp} color_load_op - カラーのロード操作 + * @param {GPULoadOp} stencil_load_op - ステンシルのロード操作 + * @param {GPUTextureView | null} resolve_target - MSAAリゾルブターゲット + * @return {GPURenderPassDescriptor} + * @method + * @protected */ export const execute = ( - colorView: GPUTextureView, - stencilView: GPUTextureView, - colorLoadOp: GPULoadOp = "load", - stencilLoadOp: GPULoadOp = "clear", - resolveTarget: GPUTextureView | null = null + color_view: GPUTextureView, + stencil_view: GPUTextureView, + color_load_op: GPULoadOp = "load", + stencil_load_op: GPULoadOp = "clear", + resolve_target: GPUTextureView | null = null ): GPURenderPassDescriptor => { - $colorAttachment.view = colorView; - $colorAttachment.loadOp = colorLoadOp; - $colorAttachment.resolveTarget = resolveTarget ?? undefined; - $depthStencilAttachment.view = stencilView; - $depthStencilAttachment.stencilLoadOp = stencilLoadOp; + $colorAttachment.view = color_view; + $colorAttachment.loadOp = color_load_op; + $colorAttachment.resolveTarget = resolve_target ?? undefined; + $depthStencilAttachment.view = stencil_view; + $depthStencilAttachment.stencilLoadOp = stencil_load_op; return $descriptor; }; diff --git a/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerCreateAttachmentUseCase.ts b/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerCreateAttachmentUseCase.ts index a17d0596..ab5a96ba 100644 --- a/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerCreateAttachmentUseCase.ts +++ b/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerCreateAttachmentUseCase.ts @@ -7,15 +7,15 @@ import { $samples } from "../../WebGPUUtil"; * @description アタッチメントオブジェクトを作成 * Create attachment object * - * @param {GPUDevice} device - * @param {GPUTextureFormat} format - * @param {Map} attachments - * @param {string} name - * @param {number} width - * @param {number} height - * @param {boolean} msaa - * @param {boolean} mask - * @param {{ nextId: number, textureId: number, stencilId: number }} idCounter + * @param {GPUDevice} device - GPUデバイス + * @param {GPUTextureFormat} format - テクスチャフォーマット + * @param {Map} attachments - アタッチメント管理マップ + * @param {string} name - アタッチメント名 + * @param {number} width - テクスチャ幅 + * @param {number} height - テクスチャ高さ + * @param {boolean} msaa - MSAA有効フラグ + * @param {boolean} mask - マスク有効フラグ + * @param {{ nextId: number, textureId: number, stencilId: number }} id_counter - ID管理カウンタ * @return {IAttachmentObject} * @method * @protected @@ -29,7 +29,7 @@ export const execute = ( height: number, msaa: boolean, mask: boolean, - idCounter: { nextId: number; textureId: number; stencilId: number } + id_counter: { nextId: number; textureId: number; stencilId: number } ): IAttachmentObject => { // アトラスかどうか判定(atlas, atlas_0, atlas_1, ...) const isAtlas = name === "atlas" || name.startsWith("atlas_"); @@ -57,7 +57,7 @@ export const execute = ( // ITextureObject形式で格納(解決先テクスチャ) const texture: ITextureObject = { - "id": idCounter.textureId++, + "id": id_counter.textureId++, "resource": gpuTexture, "view": textureView, width, @@ -78,7 +78,7 @@ export const execute = ( const msaaTextureView = msaaGpuTexture.createView(); msaaTexture = { - "id": idCounter.textureId++, + "id": id_counter.textureId++, "resource": msaaGpuTexture, "view": msaaTextureView, width, @@ -103,7 +103,7 @@ export const execute = ( const stencilView = stencilTexture.createView(); stencil = { - "id": idCounter.stencilId++, + "id": id_counter.stencilId++, "resource": stencilTexture, "view": stencilView, width, @@ -123,7 +123,7 @@ export const execute = ( const msaaStencilView = msaaStencilTexture.createView(); msaaStencil = { - "id": idCounter.stencilId++, + "id": id_counter.stencilId++, "resource": msaaStencilTexture, "view": msaaStencilView, width, @@ -135,7 +135,7 @@ export const execute = ( } const attachment: IAttachmentObject = { - "id": idCounter.nextId++, + "id": id_counter.nextId++, width, height, "clipLevel": 0, diff --git a/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerReleaseTemporaryAttachmentUseCase.ts b/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerReleaseTemporaryAttachmentUseCase.ts index cb110ebd..71022778 100644 --- a/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerReleaseTemporaryAttachmentUseCase.ts +++ b/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerReleaseTemporaryAttachmentUseCase.ts @@ -5,16 +5,16 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; * Releases a temporary attachment after filter processing * テクスチャは即座に破棄せず、フレーム終了時に遅延解放します * - * @param {Map} attachments - * @param {IAttachmentObject[]} pendingReleases - * @param {IAttachmentObject} attachment + * @param {Map} attachments - アタッチメント管理マップ + * @param {IAttachmentObject[]} pending_releases - 遅延解放キュー + * @param {IAttachmentObject} attachment - 解放するアタッチメント * @return {void} * @method * @protected */ export const execute = ( attachments: Map, - pendingReleases: IAttachmentObject[], + pending_releases: IAttachmentObject[], attachment: IAttachmentObject ): void => { // 名前を検索して削除(Map から削除するが、テクスチャは破棄しない) @@ -22,7 +22,7 @@ export const execute = ( if (att.id === attachment.id) { attachments.delete(name); // フレーム終了時に遅延解放するためキューに追加 - pendingReleases.push(att); + pending_releases.push(att); break; } } diff --git a/packages/webgpu/src/Gradient/GradientLUTCache.ts b/packages/webgpu/src/Gradient/GradientLUTCache.ts index ad05cad5..11d884b5 100644 --- a/packages/webgpu/src/Gradient/GradientLUTCache.ts +++ b/packages/webgpu/src/Gradient/GradientLUTCache.ts @@ -16,6 +16,8 @@ const $gradientAttachmentObjects: Map = new Map(); /** * @description GPUDeviceの参照 + * Reference to the GPUDevice + * @type {GPUDevice | null} * @private */ let $device: GPUDevice | null = null; @@ -114,18 +116,50 @@ export const $clearGradientAttachmentObjects = (): void => // === Gradient LUT テクスチャキャッシュ === +/** + * @description グラデーションLUTキャッシュエントリのインターフェース + * Interface for gradient LUT cache entry + */ interface IGradientLUTEntry { texture: GPUTexture; view: GPUTextureView; lastUsedFrame: number; } +/** + * @description キー文字列からLUTエントリへのキャッシュマップ + * Cache map from key string to LUT entry + * @type {Map} + * @private + */ const $lutCache: Map = new Map(); + +/** + * @description 現在のフレーム番号(TTL計算に使用) + * Current frame number used for TTL calculation + * @type {number} + * @private + */ let $currentFrame: number = 0; + +/** + * @description LUTキャッシュのTTL(フレーム数) + * TTL for LUT cache in number of frames + * @type {number} + * @constant + * @private + */ const $LUT_TTL: number = 60; /** - * @description グラデーションLUTのキャッシュキーを生成 + * @description グラデーションLUTのキャッシュキーを生成する + * Build a cache key string for a gradient LUT + * + * @param {number[]} stops - グラデーションストップ配列 / Gradient stop array + * @param {number} spread - スプレッドメソッド / Spread method + * @param {number} interpolation - 補間方法 / Interpolation method + * @return {string} キャッシュキー文字列 / Cache key string + * @private */ const $buildLUTKey = ( stops: number[], @@ -137,7 +171,15 @@ const $buildLUTKey = ( }; /** - * @description キャッシュからLUTテクスチャを取得。ヒットしなければnullを返す。 + * @description キャッシュからLUTテクスチャを取得する。ヒットしなければnullを返す。 + * Retrieve a LUT texture from the cache. Returns null on cache miss. + * + * @param {number[]} stops - グラデーションストップ配列 / Gradient stop array + * @param {number} spread - スプレッドメソッド / Spread method + * @param {number} interpolation - 補間方法 / Interpolation method + * @return {IGradientLUTEntry | null} キャッシュエントリまたはnull / Cache entry or null + * @method + * @protected */ export const $getLUTFromCache = ( stops: number[], @@ -155,7 +197,17 @@ export const $getLUTFromCache = ( }; /** - * @description LUTテクスチャをキャッシュに格納 + * @description LUTテクスチャをキャッシュに格納する + * Store a LUT texture into the cache + * + * @param {number[]} stops - グラデーションストップ配列 / Gradient stop array + * @param {number} spread - スプレッドメソッド / Spread method + * @param {number} interpolation - 補間方法 / Interpolation method + * @param {GPUTexture} texture - GPUテクスチャ / GPU texture + * @param {GPUTextureView} view - GPUテクスチャビュー / GPU texture view + * @return {void} + * @method + * @protected */ export const $putLUTToCache = ( stops: number[], @@ -174,7 +226,12 @@ export const $putLUTToCache = ( }; /** - * @description フレーム終了時にTTL超過エントリを解放 + * @description フレーム終了時にTTL超過エントリを解放する + * Release entries that exceed TTL at the end of each frame + * + * @return {void} + * @method + * @protected */ export const $cleanupLUTCache = (): void => { @@ -188,7 +245,12 @@ export const $cleanupLUTCache = (): void => }; /** - * @description 全LUTキャッシュを破棄 + * @description 全LUTキャッシュを破棄する + * Destroy and clear the entire LUT cache + * + * @return {void} + * @method + * @protected */ export const $clearLUTCache = (): void => { diff --git a/packages/webgpu/src/Gradient/GradientLUTGenerator.ts b/packages/webgpu/src/Gradient/GradientLUTGenerator.ts index 58fbb4a8..ba23563d 100644 --- a/packages/webgpu/src/Gradient/GradientLUTGenerator.ts +++ b/packages/webgpu/src/Gradient/GradientLUTGenerator.ts @@ -4,30 +4,38 @@ */ /** - * @description ストップ数に応じた適応解像度を取得 - * @param {number} stopsLength - * @return {number} + * @description ストップ数に応じた適応解像度を取得する + * Get adaptive resolution based on the number of gradient stops + * + * @param {number} stops_length - ストップ数 / Number of gradient stops + * @return {number} 解像度 (256, 512, or 1024) / Resolution + * @method + * @protected */ -export const getAdaptiveResolution = (stopsLength: number): number => +export const getAdaptiveResolution = (stops_length: number): number => { - if (stopsLength <= 4) { + if (stops_length <= 4) { return 256; } - if (stopsLength <= 8) { + if (stops_length <= 8) { return 512; } return 1024; }; /** - * @description グラデーションLUTテクスチャデータを生成 + * @description グラデーションLUTテクスチャデータを生成する + * Generate gradient LUT texture data. * stops配列: [offset, R, G, B, A, offset, R, G, B, A, ...] * 注意: R, G, B, A は 0-255 範囲 * LUTは0-1の範囲の色を生成し、spread処理はシェーダー側で行う - * @param {number[]} stops - グラデーションストップ配列 - * @param {number} _spread - スプレッドメソッド(未使用、シェーダー側で処理) - * @param {number} interpolation - 補間方法 (0: linearRGB, 1: RGB) ※WebGL互換 - * @return {Uint8Array} + * + * @param {number[]} stops - グラデーションストップ配列 / Gradient stop array + * @param {number} _spread - スプレッドメソッド(未使用、シェーダー側で処理)/ Spread method (unused, handled by shader) + * @param {number} interpolation - 補間方法 (0: linearRGB, 1: RGB) / Interpolation method (WebGL compatible) + * @return {Uint8Array} LUTテクスチャデータ / LUT texture data + * @method + * @protected */ export const generateGradientLUT = ( stops: number[], @@ -48,7 +56,7 @@ export const generateGradientLUT = ( const t = i / (resolution - 1); // 色を補間(色は0-255範囲で返される) - const color = interpolateColor(stops, t, interpolation); + const color = $interpolateColor(stops, t, interpolation); // WebGL版と同じ: プリマルチプライドアルファは適用しない // LUTにはストレート(非プリマルチプライド)の色を格納 @@ -67,14 +75,18 @@ export const generateGradientLUT = ( }; /** - * @description 色を補間 + * @description 色を補間する + * Interpolate color between gradient stops. * 色は0-255範囲で入力され、0-255範囲で出力される - * @param {number[]} stops - * @param {number} t - * @param {number} interpolation - 0: linearRGB, 1: RGB(WebGL互換) - * @return {{ r: number, g: number, b: number, a: number }} + * Colors are input in 0-255 range and output in 0-255 range. + * + * @param {number[]} stops - グラデーションストップ配列 / Gradient stop array + * @param {number} t - 補間位置 (0-1) / Interpolation position (0-1) + * @param {number} interpolation - 補間方法 (0: linearRGB, 1: RGB) / Interpolation method (WebGL compatible) + * @return {{ r: number, g: number, b: number, a: number }} 補間された色 / Interpolated color + * @private */ -const interpolateColor = ( +const $interpolateColor = ( stops: number[], t: number, interpolation: number @@ -135,57 +147,76 @@ const interpolateColor = ( // linearRGB補間(ガンマ補正) // 0-255 → 0-1に正規化してからリニア変換 return { - "r": linearToSRGB(lerp(sRGBToLinear(startR / 255), sRGBToLinear(endR / 255), localT)) * 255, - "g": linearToSRGB(lerp(sRGBToLinear(startG / 255), sRGBToLinear(endG / 255), localT)) * 255, - "b": linearToSRGB(lerp(sRGBToLinear(startB / 255), sRGBToLinear(endB / 255), localT)) * 255, - "a": lerp(startA, endA, localT) + "r": $linearToSRGB($lerp($sRGBToLinear(startR / 255), $sRGBToLinear(endR / 255), localT)) * 255, + "g": $linearToSRGB($lerp($sRGBToLinear(startG / 255), $sRGBToLinear(endG / 255), localT)) * 255, + "b": $linearToSRGB($lerp($sRGBToLinear(startB / 255), $sRGBToLinear(endB / 255), localT)) * 255, + "a": $lerp(startA, endA, localT) }; } // RGB補間(リニア、デフォルト)- 0-255範囲でそのまま補間 return { - "r": lerp(startR, endR, localT), - "g": lerp(startG, endG, localT), - "b": lerp(startB, endB, localT), - "a": lerp(startA, endA, localT) + "r": $lerp(startR, endR, localT), + "g": $lerp(startG, endG, localT), + "b": $lerp(startB, endB, localT), + "a": $lerp(startA, endA, localT) }; }; /** - * @description 線形補間 + * @description 線形補間を行う + * Perform linear interpolation between two values + * + * @param {number} a - 開始値 / Start value + * @param {number} b - 終了値 / End value + * @param {number} t - 補間係数 (0-1) / Interpolation factor (0-1) + * @return {number} 補間結果 / Interpolated result + * @private */ -const lerp = (a: number, b: number, t: number): number => +const $lerp = (a: number, b: number, t: number): number => { return a + (b - a) * t; }; /** - * @description sRGBからリニアへ変換(入力: 0-1正規化値) + * @description sRGBからリニア色空間へ変換する(入力: 0-1正規化値) + * Convert from sRGB to linear color space (input: 0-1 normalized value). * WebGL版と同じガンマ値 2.23333333 を使用 + * + * @param {number} value - sRGB色空間の正規化値 (0-1) / Normalized value in sRGB color space + * @return {number} リニア色空間の値 / Value in linear color space + * @private */ -const sRGBToLinear = (value: number): number => +const $sRGBToLinear = (value: number): number => { - // WebGL版と同じ簡易ガンマ補正 return Math.pow(value, 2.23333333); }; /** - * @description リニアからsRGBへ変換(出力: 0-1正規化値) + * @description リニア色空間からsRGBへ変換する(出力: 0-1正規化値) + * Convert from linear color space to sRGB (output: 0-1 normalized value). * WebGL版と同じガンマ値 0.45454545 (= 1/2.2) を使用 + * + * @param {number} value - リニア色空間の値 / Value in linear color space + * @return {number} sRGB色空間の正規化値 / Normalized value in sRGB color space + * @private */ -const linearToSRGB = (value: number): number => +const $linearToSRGB = (value: number): number => { - // WebGL版と同じ簡易ガンマ補正 return Math.pow(value, 0.45454545); }; /** - * @description フィルター用グラデーションLUTテクスチャデータを生成 + * @description フィルター用グラデーションLUTテクスチャデータを生成する + * Generate gradient LUT texture data for filters. * ratios, colors, alphas配列から1D LUTを生成 - * @param {Float32Array} ratios - 比率配列 (0-255) - * @param {Float32Array} colors - 色配列 (32bit整数) - * @param {Float32Array} alphas - アルファ配列 (0-1) - * @return {Uint8Array} + * + * @param {Float32Array} ratios - 比率配列 (0-255) / Ratio array (0-255) + * @param {Float32Array} colors - 色配列 (32bit整数) / Color array (32-bit integers) + * @param {Float32Array} alphas - アルファ配列 (0-1) / Alpha array (0-1) + * @return {Uint8Array} LUTテクスチャデータ / LUT texture data + * @method + * @protected */ export const generateFilterGradientLUT = ( ratios: Float32Array, @@ -234,10 +265,10 @@ export const generateFilterGradientLUT = ( localT = (t - start.offset) / (end.offset - start.offset); } - const r = lerp(start.r, end.r, localT); - const g = lerp(start.g, end.g, localT); - const b = lerp(start.b, end.b, localT); - const a = lerp(start.a, end.a, localT); + const r = $lerp(start.r, end.r, localT); + const g = $lerp(start.g, end.g, localT); + const b = $lerp(start.b, end.b, localT); + const a = $lerp(start.a, end.a, localT); // プリマルチプライドアルファで書き込み const offset = i * 4; diff --git a/packages/webgpu/src/Mask.test.ts b/packages/webgpu/src/Mask.test.ts index 15310a93..292383df 100644 --- a/packages/webgpu/src/Mask.test.ts +++ b/packages/webgpu/src/Mask.test.ts @@ -6,9 +6,6 @@ import { $isMaskTestEnabled, $setMaskStencilReference, $getMaskStencilReference, - $pushMaskAttachment, - $popMaskAttachment, - $hasMaskAttachment, $clipBounds, $clipLevels, $resetMaskState @@ -72,37 +69,6 @@ describe("Mask", () => }); }); - describe("mask attachment stack", () => - { - it("should default to empty", () => - { - expect($hasMaskAttachment()).toBe(false); - }); - - it("should push and pop attachments", () => - { - const attachment1 = { "id": 1 }; - const attachment2 = { "id": 2 }; - - $pushMaskAttachment(attachment1); - expect($hasMaskAttachment()).toBe(true); - - $pushMaskAttachment(attachment2); - expect($hasMaskAttachment()).toBe(true); - - expect($popMaskAttachment()).toBe(attachment2); - expect($hasMaskAttachment()).toBe(true); - - expect($popMaskAttachment()).toBe(attachment1); - expect($hasMaskAttachment()).toBe(false); - }); - - it("should return undefined when popping empty stack", () => - { - expect($popMaskAttachment()).toBeUndefined(); - }); - }); - describe("clip bounds and levels", () => { it("should be Maps", () => @@ -137,7 +103,6 @@ describe("Mask", () => $setMaskDrawing(true); $setMaskTestEnabled(true); $setMaskStencilReference(10); - $pushMaskAttachment({ "id": 1 }); $clipBounds.set(1, new Float32Array([0, 0, 100, 100])); $clipLevels.set(1, 5); @@ -148,7 +113,6 @@ describe("Mask", () => expect($isMaskDrawing()).toBe(false); expect($isMaskTestEnabled()).toBe(false); expect($getMaskStencilReference()).toBe(0); - expect($hasMaskAttachment()).toBe(false); expect($clipBounds.size).toBe(0); expect($clipLevels.size).toBe(0); }); diff --git a/packages/webgpu/src/Mask.ts b/packages/webgpu/src/Mask.ts index 5551a197..bdcdce6b 100644 --- a/packages/webgpu/src/Mask.ts +++ b/packages/webgpu/src/Mask.ts @@ -1,66 +1,123 @@ +/** + * @description マスク描画中かどうかの状態フラグ + * Flag indicating whether mask drawing is in progress + * + * @type {boolean} + */ let $maskDrawingState: boolean = false; +/** + * @description マスク描画状態を設定する + * Set the mask drawing state + * + * @param {boolean} state - マスク描画中かどうか / whether mask drawing is active + * @return {void} + */ export const $setMaskDrawing = (state: boolean): void => { $maskDrawingState = state; }; +/** + * @description マスク描画中かどうかを返す + * Returns whether mask drawing is currently active + * + * @return {boolean} + */ export const $isMaskDrawing = (): boolean => { return $maskDrawingState; }; +/** + * @description マスクテストが有効かどうかのフラグ + * Flag indicating whether mask (stencil) testing is enabled + * + * @type {boolean} + */ let $maskTestEnabled: boolean = false; +/** + * @description マスクステンシル参照値 + * Mask stencil reference value used for stencil comparison + * + * @type {number} + */ let $maskStencilReference: number = 0; +/** + * @description マスクテストの有効/無効を設定する + * Enable or disable mask (stencil) testing + * + * @param {boolean} enabled - 有効にするかどうか / whether to enable + * @return {void} + */ export const $setMaskTestEnabled = (enabled: boolean): void => { $maskTestEnabled = enabled; }; +/** + * @description マスクテストが有効かどうかを返す + * Returns whether mask (stencil) testing is enabled + * + * @return {boolean} + */ export const $isMaskTestEnabled = (): boolean => { return $maskTestEnabled; }; +/** + * @description マスクステンシル参照値を設定する + * Set the mask stencil reference value + * + * @param {number} value - ステンシル参照値 / stencil reference value + * @return {void} + */ export const $setMaskStencilReference = (value: number): void => { $maskStencilReference = value; }; +/** + * @description マスクステンシル参照値を取得する + * Get the current mask stencil reference value + * + * @return {number} + */ export const $getMaskStencilReference = (): number => { return $maskStencilReference; }; -const $maskAttachmentStack: any[] = []; - -export const $pushMaskAttachment = (attachment: any): void => -{ - $maskAttachmentStack.push(attachment); -}; - -export const $popMaskAttachment = (): any => -{ - return $maskAttachmentStack.pop(); -}; - -export const $hasMaskAttachment = (): boolean => -{ - return $maskAttachmentStack.length > 0; -}; - +/** + * @description クリップ境界のマップ(キー: ID、値: バウンディングボックス) + * Map of clip bounds (key: ID, value: bounding box as Float32Array) + * + * @type {Map} + */ export const $clipBounds: Map = new Map(); +/** + * @description クリップレベルのマップ(キー: ID、値: クリップ深度) + * Map of clip levels (key: ID, value: clip depth) + * + * @type {Map} + */ export const $clipLevels: Map = new Map(); +/** + * @description マスク関連の全状態をリセットする + * Reset all mask-related state to initial values + * + * @return {void} + */ export const $resetMaskState = (): void => { $maskDrawingState = false; $maskTestEnabled = false; $maskStencilReference = 0; - $maskAttachmentStack.length = 0; $clipBounds.clear(); $clipLevels.clear(); }; diff --git a/packages/webgpu/src/Mask/service/MaskUnionMaskService.ts b/packages/webgpu/src/Mask/service/MaskUnionMaskService.ts index eedd97e3..cf63f14e 100644 --- a/packages/webgpu/src/Mask/service/MaskUnionMaskService.ts +++ b/packages/webgpu/src/Mask/service/MaskUnionMaskService.ts @@ -3,20 +3,12 @@ import type { PipelineManager } from "../../Shader/PipelineManager"; import type { IAttachmentObject } from "../../interface/IAttachmentObject"; /** - * @description マスクの合成処理(ネストされたマスク対応) - * WebGL版と同様に、レベル7を超えたステンシルビットをマージする + * @description フルスクリーン矩形の頂点データ(4 floats/vertex: position + bezier) + * Full-screen rectangle vertex data (4 floats/vertex: position + bezier) * - * @param {GPUDevice} device - * @param {GPURenderPassEncoder} renderPassEncoder - * @param {BufferManager} bufferManager - * @param {PipelineManager} pipelineManager - * @param {IAttachmentObject} currentAttachment - * @return {void} - * @method - * @protected + * @type {Float32Array} + * @constant */ - -// フルスクリーン矩形(4 floats/vertex: position + bezier) const $rectVertices = new Float32Array([ // Triangle 1 -1, -1, 0.5, 0.5, @@ -28,7 +20,13 @@ const $rectVertices = new Float32Array([ 1, 1, 0.5, 0.5 ]); -// FillUniforms: identity matrix + white color (NDC座標なので変換不要) +/** + * @description FillUniformsデータ: 恒等行列と白色(NDC座標なので変換不要) + * FillUniforms data: identity matrix + white color (no transform needed for NDC coordinates) + * + * @type {Float32Array} + * @constant + */ const $uniformData16 = new Float32Array([ 1, 1, 1, 1, // color: white 0.5, 0, 0, 0, // matrix0: (0.5, 0, 0, pad) → identity-like for NDC passthrough @@ -36,27 +34,42 @@ const $uniformData16 = new Float32Array([ 0.5, 0.5, 1, 0 // matrix2: (0.5, 0.5, 1, pad) ]); +/** + * @description マスクの合成処理(ネストされたマスク対応) + * Union mask processing for nested masks. + * WebGL版と同様に、レベル7を超えたステンシルビットをマージする + * Merges stencil bits exceeding level 7, same as WebGL version. + * + * @param {GPUDevice} device - GPUデバイス / GPU device + * @param {GPURenderPassEncoder} render_pass_encoder - レンダーパスエンコーダ / Render pass encoder + * @param {BufferManager} buffer_manager - バッファマネージャ / Buffer manager + * @param {PipelineManager} pipeline_manager - パイプラインマネージャ / Pipeline manager + * @param {IAttachmentObject} current_attachment - 現在のアタッチメントオブジェクト / Current attachment object + * @return {void} + * @method + * @protected + */ export const execute = ( device: GPUDevice, - renderPassEncoder: GPURenderPassEncoder, - bufferManager: BufferManager, - pipelineManager: PipelineManager, - currentAttachment: IAttachmentObject + render_pass_encoder: GPURenderPassEncoder, + buffer_manager: BufferManager, + pipeline_manager: PipelineManager, + current_attachment: IAttachmentObject ): void => { - if (!currentAttachment) { + if (!current_attachment) { return; } - const clipLevel = currentAttachment.clipLevel; + const clipLevel = current_attachment.clipLevel; const mask = 1 << clipLevel - 1; - const vertexBuffer = bufferManager.acquireVertexBuffer($rectVertices.byteLength, $rectVertices); + const vertexBuffer = buffer_manager.acquireVertexBuffer($rectVertices.byteLength, $rectVertices); // Dynamic Uniform Bufferにデータを書き込み - const uniformOffset = bufferManager.dynamicUniform.allocate($uniformData16); + const uniformOffset = buffer_manager.dynamicUniform.allocate($uniformData16); // Dynamic BindGroupを取得 - const layout = pipelineManager.getBindGroupLayout("fill_dynamic"); + const layout = pipeline_manager.getBindGroupLayout("fill_dynamic"); if (!layout) { return; } @@ -65,29 +78,29 @@ export const execute = ( "entries": [{ "binding": 0, "resource": { - "buffer": bufferManager.dynamicUniform.getBuffer(), + "buffer": buffer_manager.dynamicUniform.getBuffer(), "size": 256 } }] }); // === Pass 1: ステンシルビットのマージ === - const mergePipeline = pipelineManager.getPipeline(`mask_union_merge_${clipLevel}`); + const mergePipeline = pipeline_manager.getPipeline(`mask_union_merge_${clipLevel}`); if (mergePipeline) { - renderPassEncoder.setPipeline(mergePipeline); - renderPassEncoder.setStencilReference(mask); - renderPassEncoder.setVertexBuffer(0, vertexBuffer); - renderPassEncoder.setBindGroup(0, bindGroup, [uniformOffset]); - renderPassEncoder.draw(6, 1, 0, 0); + render_pass_encoder.setPipeline(mergePipeline); + render_pass_encoder.setStencilReference(mask); + render_pass_encoder.setVertexBuffer(0, vertexBuffer); + render_pass_encoder.setBindGroup(0, bindGroup, [uniformOffset]); + render_pass_encoder.draw(6, 1, 0, 0); } // === Pass 2: 上位ビットのクリア === - const clearPipeline = pipelineManager.getPipeline(`mask_union_clear_${clipLevel}`); + const clearPipeline = pipeline_manager.getPipeline(`mask_union_clear_${clipLevel}`); if (clearPipeline) { - renderPassEncoder.setPipeline(clearPipeline); - renderPassEncoder.setStencilReference(0); - renderPassEncoder.setVertexBuffer(0, vertexBuffer); - renderPassEncoder.setBindGroup(0, bindGroup, [uniformOffset]); - renderPassEncoder.draw(6, 1, 0, 0); + render_pass_encoder.setPipeline(clearPipeline); + render_pass_encoder.setStencilReference(0); + render_pass_encoder.setVertexBuffer(0, vertexBuffer); + render_pass_encoder.setBindGroup(0, bindGroup, [uniformOffset]); + render_pass_encoder.draw(6, 1, 0, 0); } }; diff --git a/packages/webgpu/src/Mesh/service/MeshFillGenerateService.ts b/packages/webgpu/src/Mesh/service/MeshFillGenerateService.ts index 89bb9515..9913385c 100644 --- a/packages/webgpu/src/Mesh/service/MeshFillGenerateService.ts +++ b/packages/webgpu/src/Mesh/service/MeshFillGenerateService.ts @@ -10,10 +10,10 @@ import type { IPath } from "../../interface/IPath"; * * color/matrixはuniform bufferで供給される * - * @param {IPath} vertex - * @param {Float32Array} buffer - * @param {number} index - 現在の頂点インデックス - * @return {number} 新しい頂点インデックス + * @param {IPath} vertex - 頂点パスデータ / Vertex path data + * @param {Float32Array} buffer - 出力先バッファ / Output buffer + * @param {number} index - 現在の頂点インデックス / Current vertex index + * @return {number} 新しい頂点インデックス / New vertex index * @method * @protected */ diff --git a/packages/webgpu/src/Mesh/service/MeshStrokeFillGenerateService.ts b/packages/webgpu/src/Mesh/service/MeshStrokeFillGenerateService.ts index a2520667..deae055c 100644 --- a/packages/webgpu/src/Mesh/service/MeshStrokeFillGenerateService.ts +++ b/packages/webgpu/src/Mesh/service/MeshStrokeFillGenerateService.ts @@ -10,10 +10,10 @@ import type { IPath } from "../../interface/IPath"; * * color/matrixはuniform bufferで供給される * - * @param {IPath} vertex - * @param {Float32Array} buffer - * @param {number} index - 現在の頂点インデックス - * @return {number} 新しい頂点インデックス + * @param {IPath} vertex - 頂点パスデータ / Vertex path data + * @param {Float32Array} buffer - 出力先バッファ / Output buffer + * @param {number} index - 現在の頂点インデックス / Current vertex index + * @return {number} 新しい頂点インデックス / New vertex index * @method * @protected */ diff --git a/packages/webgpu/src/Mesh/usecase/MeshBitmapStrokeGenerateUseCase.ts b/packages/webgpu/src/Mesh/usecase/MeshBitmapStrokeGenerateUseCase.ts index 0a44eb67..220c0a70 100644 --- a/packages/webgpu/src/Mesh/usecase/MeshBitmapStrokeGenerateUseCase.ts +++ b/packages/webgpu/src/Mesh/usecase/MeshBitmapStrokeGenerateUseCase.ts @@ -8,6 +8,13 @@ import { execute as meshStrokeFillGenerateService } from "../service/MeshStrokeF */ let $meshTempBuffer: Float32Array = new Float32Array(32); +/** + * @description 2のべき乗に切り上げる + * Round up to the next power of two + * + * @param {number} v - 切り上げ対象の値 / Value to round up + * @return {number} 2のべき乗の値 / Next power of two + */ const $upperPowerOfTwo = (v: number): number => { v--; @@ -24,9 +31,9 @@ const $upperPowerOfTwo = (v: number): number => * @description ビットマップストローク用のメッシュを生成する * Generate a mesh for bitmap stroke * - * @param {IPath[]} vertices - * @param {number} thickness - * @return {IMeshResult} + * @param {IPath[]} vertices - パス頂点配列 / Array of path vertices + * @param {number} thickness - 線の太さ / Line thickness + * @return {IMeshResult} メッシュ結果 / Mesh result * @method * @protected */ diff --git a/packages/webgpu/src/Mesh/usecase/MeshFillGenerateUseCase.ts b/packages/webgpu/src/Mesh/usecase/MeshFillGenerateUseCase.ts index 420fad5b..17fdfcf7 100644 --- a/packages/webgpu/src/Mesh/usecase/MeshFillGenerateUseCase.ts +++ b/packages/webgpu/src/Mesh/usecase/MeshFillGenerateUseCase.ts @@ -7,6 +7,13 @@ import { execute as meshFillGenerateService } from "../service/MeshFillGenerateS */ let $meshTempBuffer: Float32Array = new Float32Array(32); +/** + * @description 2のべき乗に切り上げる + * Round up to the next power of two + * + * @param {number} v - 切り上げ対象の値 / Value to round up + * @return {number} 2のべき乗の値 / Next power of two + */ const $upperPowerOfTwo = (v: number): number => { v--; @@ -23,8 +30,8 @@ const $upperPowerOfTwo = (v: number): number => * @description 塗りのメッシュを生成する * Generate a fill mesh * - * @param {IPath[]} vertices - * @return {IMeshResult} + * @param {IPath[]} vertices - パス頂点配列 / Array of path vertices + * @return {IMeshResult} メッシュ結果 / Mesh result * @method * @protected */ diff --git a/packages/webgpu/src/Mesh/usecase/MeshGradientStrokeGenerateUseCase.ts b/packages/webgpu/src/Mesh/usecase/MeshGradientStrokeGenerateUseCase.ts index 2ab5b446..5877ab6c 100644 --- a/packages/webgpu/src/Mesh/usecase/MeshGradientStrokeGenerateUseCase.ts +++ b/packages/webgpu/src/Mesh/usecase/MeshGradientStrokeGenerateUseCase.ts @@ -8,6 +8,13 @@ import { execute as meshStrokeFillGenerateService } from "../service/MeshStrokeF */ let $meshTempBuffer: Float32Array = new Float32Array(32); +/** + * @description 2のべき乗に切り上げる + * Round up to the next power of two + * + * @param {number} v - 切り上げ対象の値 / Value to round up + * @return {number} 2のべき乗の値 / Next power of two + */ const $upperPowerOfTwo = (v: number): number => { v--; @@ -24,9 +31,9 @@ const $upperPowerOfTwo = (v: number): number => * @description グラデーションストローク用のメッシュを生成する * Generate a mesh for gradient stroke * - * @param {IPath[]} vertices - * @param {number} thickness - * @return {IMeshResult} + * @param {IPath[]} vertices - パス頂点配列 / Array of path vertices + * @param {number} thickness - 線の太さ / Line thickness + * @return {IMeshResult} メッシュ結果 / Mesh result * @method * @protected */ diff --git a/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.ts b/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.ts index c828f442..9e4aee37 100644 --- a/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.ts +++ b/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.ts @@ -4,26 +4,50 @@ import { $context } from "../../WebGPUUtil"; /** * @description Canvas 2Dコンテキスト(点が矩形内にあるか判定用) + * Canvas 2D context for point-in-rectangle testing */ -const canvas = new OffscreenCanvas(1, 1); -const $canvasContext = canvas.getContext("2d") as OffscreenCanvasRenderingContext2D; +const $canvas: OffscreenCanvas = new OffscreenCanvas(1, 1); + +/** + * @description 矩形内の点判定用2Dコンテキスト + * 2D context used for point-in-path detection + */ +const $canvasContext = $canvas.getContext("2d") as OffscreenCanvasRenderingContext2D; /** * @description 再利用可能なPointオブジェクト(GC回避) + * Reusable Point objects to avoid GC overhead */ const $startPoint: IPoint = { "x": 0, "y": 0 }; + +/** + * @description 再利用可能な制御点オブジェクト(GC回避) + * Reusable control point object to avoid GC overhead + */ const $controlPoint: IPoint = { "x": 0, "y": 0 }; + +/** + * @description 再利用可能な終了点オブジェクト(GC回避) + * Reusable end point object to avoid GC overhead + */ const $endPoint: IPoint = { "x": 0, "y": 0 }; + +/** + * @description 再利用可能な前の点オブジェクト(GC回避) + * Reusable previous point object to avoid GC overhead + */ const $prevPoint: IPoint = { "x": 0, "y": 0 }; /** * @description 法線ベクトルを計算(WebGL版のMeshCalculateNormalVectorServiceと同じ) - * @param {number} x - 方向ベクトルのx成分 - * @param {number} y - 方向ベクトルのy成分 - * @param {number} thickness - 線の太さ(半分の値) - * @return {IPoint} + * Calculate the normal vector (same as WebGL MeshCalculateNormalVectorService) + * + * @param {number} x - 方向ベクトルのx成分 / X component of direction vector + * @param {number} y - 方向ベクトルのy成分 / Y component of direction vector + * @param {number} thickness - 線の太さ(半分の値) / Half line thickness + * @return {IPoint} 法線ベクトル / Normal vector */ -const calculateNormalVector = (x: number, y: number, thickness: number): IPoint => +const $calculateNormalVector = (x: number, y: number, thickness: number): IPoint => { const magnitude = Math.sqrt(x * x + y * y); if (magnitude === 0) { @@ -37,16 +61,26 @@ const calculateNormalVector = (x: number, y: number, thickness: number): IPoint /** * @description 線形補間(lerp) + * Linear interpolation between two points + * + * @param {IPoint} p0 - 始点 / Start point + * @param {IPoint} p1 - 終点 / End point + * @param {number} t - 補間パラメータ(0〜1) / Interpolation parameter (0-1) + * @return {IPoint} 補間された点 / Interpolated point */ -const lerp = (p0: IPoint, p1: IPoint, t: number): IPoint => ({ +const $lerp = (p0: IPoint, p1: IPoint, t: number): IPoint => ({ "x": p0.x + (p1.x - p0.x) * t, "y": p0.y + (p1.y - p0.y) * t }); /** * @description ベクトルの正規化 + * Normalize a vector to unit length + * + * @param {IPoint} point - 正規化する点 / Point to normalize + * @return {IPoint} 正規化された点 / Normalized point */ -const normalize = (point: IPoint): IPoint => { +const $normalize = (point: IPoint): IPoint => { const length = Math.sqrt(point.x * point.x + point.y * point.y); return length === 0 ? { "x": 0, "y": 0 } @@ -55,8 +89,15 @@ const normalize = (point: IPoint): IPoint => { /** * @description 二次ベジェ曲線上の座標を計算 + * Calculate a point on a quadratic Bezier curve + * + * @param {number} t - 曲線パラメータ(0〜1) / Curve parameter (0-1) + * @param {IPoint} s0 - 始点 / Start point + * @param {IPoint} s1 - 制御点 / Control point + * @param {IPoint} s2 - 終点 / End point + * @return {IPoint} 曲線上の座標 / Point on the curve */ -const getQuadraticBezierPoint = ( +const $getQuadraticBezierPoint = ( t: number, s0: IPoint, s1: IPoint, @@ -68,8 +109,15 @@ const getQuadraticBezierPoint = ( /** * @description 二次ベジェ曲線上の接線ベクトルを計算 + * Calculate the tangent vector on a quadratic Bezier curve + * + * @param {number} t - 曲線パラメータ(0〜1) / Curve parameter (0-1) + * @param {IPoint} s0 - 始点 / Start point + * @param {IPoint} s1 - 制御点 / Control point + * @param {IPoint} s2 - 終点 / End point + * @return {IPoint} 接線ベクトル / Tangent vector */ -const getQuadraticBezierTangent = ( +const $getQuadraticBezierTangent = ( t: number, s0: IPoint, s1: IPoint, @@ -81,16 +129,23 @@ const getQuadraticBezierTangent = ( /** * @description 二次ベジェ曲線を分割 + * Split a quadratic Bezier curve at a given parameter + * + * @param {IPoint} s0 - 始点 / Start point + * @param {IPoint} s1 - 制御点 / Control point + * @param {IPoint} s2 - 終点 / End point + * @param {number} t - 分割パラメータ / Split parameter + * @return {Array} 分割された2つの曲線 / Two split curves */ -const splitQuadraticBezier = ( +const $splitQuadraticBezier = ( s0: IPoint, s1: IPoint, s2: IPoint, t: number = 0.5 ): Array => { - const M0 = lerp(s0, s1, t); - const M1 = lerp(s1, s2, t); - const M01 = lerp(M0, M1, t); + const M0 = $lerp(s0, s1, t); + const M1 = $lerp(s1, s2, t); + const M01 = $lerp(M0, M1, t); return [ [s0, M0, M01], [M01, M1, s2] @@ -99,8 +154,15 @@ const splitQuadraticBezier = ( /** * @description ベジェ曲線を複数回分割 + * Split a Bezier curve multiple times + * + * @param {IPoint} s0 - 始点 / Start point + * @param {IPoint} s1 - 制御点 / Control point + * @param {IPoint} s2 - 終点 / End point + * @param {number} n - 分割回数 / Number of splits + * @return {Array} 分割されたセグメント配列 / Array of split segments */ -const splitBezierMultipleTimes = ( +const $splitBezierMultipleTimes = ( s0: IPoint, s1: IPoint, s2: IPoint, @@ -110,7 +172,7 @@ const splitBezierMultipleTimes = ( for (let i = 0; i < n; i++) { const newSegments: Array = []; for (const seg of segments) { - const splitted = splitQuadraticBezier(seg[0], seg[1], seg[2], 0.5); + const splitted = $splitQuadraticBezier(seg[0], seg[1], seg[2], 0.5); newSegments.push(splitted[0], splitted[1]); } segments = newSegments; @@ -120,8 +182,15 @@ const splitBezierMultipleTimes = ( /** * @description 2次ベジェのオフセットを計算 + * Calculate offset of a quadratic Bezier curve + * + * @param {IPoint} s0 - 始点 / Start point + * @param {IPoint} s1 - 制御点 / Control point + * @param {IPoint} s2 - 終点 / End point + * @param {number} offset - オフセット距離 / Offset distance + * @return {IPoint[]} オフセットされた点配列 / Array of offset points */ -const approximateOffsetQuadratic = ( +const $approximateOffsetQuadratic = ( s0: IPoint, s1: IPoint, s2: IPoint, @@ -131,9 +200,9 @@ const approximateOffsetQuadratic = ( const newPoints: IPoint[] = []; for (const t of tValues) { - const pos = getQuadraticBezierPoint(t, s0, s1, s2); - const tan = getQuadraticBezierTangent(t, s0, s1, s2); - const n = normalize({ "x": -tan.y, "y": tan.x }); + const pos = $getQuadraticBezierPoint(t, s0, s1, s2); + const tan = $getQuadraticBezierTangent(t, s0, s1, s2); + const n = $normalize({ "x": -tan.y, "y": tan.x }); newPoints.push({ "x": pos.x + n.x * offset, "y": pos.y + n.y * offset @@ -144,22 +213,29 @@ const approximateOffsetQuadratic = ( /** * @description カーブの矩形を計算(WebGL版のMeshCalculateCurveRectangleUseCaseと同じ) + * Calculate curve rectangle (same as WebGL MeshCalculateCurveRectangleUseCase) + * + * @param {IPoint} start_point - 始点 / Start point + * @param {IPoint} control_point - 制御点 / Control point + * @param {IPoint} end_point - 終点 / End point + * @param {number} thickness - 線の太さ(半分の値) / Half line thickness + * @return {IPath} カーブ矩形パス / Curve rectangle path */ -const calculateCurveRectangle = ( - startPoint: IPoint, - controlPoint: IPoint, - endPoint: IPoint, +const $calculateCurveRectangle = ( + start_point: IPoint, + control_point: IPoint, + end_point: IPoint, thickness: number ): IPath => { // WebGL版と同じ分割数(5回分割 = 32セグメント) - const segments = splitBezierMultipleTimes(startPoint, controlPoint, endPoint, 5); + const segments = $splitBezierMultipleTimes(start_point, control_point, end_point, 5); const leftCurves: Array = []; const rightCurves: Array = []; for (const seg of segments) { - leftCurves.push(approximateOffsetQuadratic(seg[0], seg[1], seg[2], +thickness)); - rightCurves.push(approximateOffsetQuadratic(seg[0], seg[1], seg[2], -thickness)); + leftCurves.push($approximateOffsetQuadratic(seg[0], seg[1], seg[2], +thickness)); + rightCurves.push($approximateOffsetQuadratic(seg[0], seg[1], seg[2], -thickness)); } // セグメント間の連続性を確保:各セグメントの終点を次のセグメントの始点に強制一致 @@ -208,42 +284,44 @@ const calculateCurveRectangle = ( /** * @description 直線の矩形を計算(WebGL版のMeshCalculateLineRectangleUseCaseと同じ) - * @param {IPoint} startPoint - 開始点 - * @param {IPoint} endPoint - 終了点 - * @param {number} thickness - 線の太さ(半分の値) - * @return {IPath} 矩形パス + * Calculate line rectangle (same as WebGL MeshCalculateLineRectangleUseCase) + * + * @param {IPoint} start_point - 開始点 / Start point + * @param {IPoint} end_point - 終了点 / End point + * @param {number} thickness - 線の太さ(半分の値) / Half line thickness + * @return {IPath} 矩形パス / Rectangle path */ -const calculateLineRectangle = ( - startPoint: IPoint, - endPoint: IPoint, +const $calculateLineRectangle = ( + start_point: IPoint, + end_point: IPoint, thickness: number ): IPath => { const vector: IPoint = { - "x": endPoint.x - startPoint.x, - "y": endPoint.y - startPoint.y + "x": end_point.x - start_point.x, + "y": end_point.y - start_point.y }; - const normal = calculateNormalVector(vector.x, vector.y, thickness); + const normal = $calculateNormalVector(vector.x, vector.y, thickness); const shiftedUpStart: IPoint = { - "x": startPoint.x + normal.x, - "y": startPoint.y + normal.y + "x": start_point.x + normal.x, + "y": start_point.y + normal.y }; const shiftedUpEnd: IPoint = { - "x": endPoint.x + normal.x, - "y": endPoint.y + normal.y + "x": end_point.x + normal.x, + "y": end_point.y + normal.y }; const shiftedDownEnd: IPoint = { - "x": endPoint.x - normal.x, - "y": endPoint.y - normal.y + "x": end_point.x - normal.x, + "y": end_point.y - normal.y }; const shiftedDownStart: IPoint = { - "x": startPoint.x - normal.x, - "y": startPoint.y - normal.y + "x": start_point.x - normal.x, + "y": start_point.y - normal.y }; return [ @@ -257,9 +335,16 @@ const calculateLineRectangle = ( /** * @description メッシュのパスの中で指定座標が含まれる線を探す + * Find paths overlapping with specified coordinates * WebGL版のMeshFindOverlappingPathsServiceと同じ + * + * @param {number} x - 中心x座標 / Center x coordinate + * @param {number} y - 中心y座標 / Center y coordinate + * @param {number} r - 半径 / Radius + * @param {IPath} paths - パスデータ / Path data + * @return {number[]} 重複する座標配列 / Array of overlapping coordinates */ -const findOverlappingPaths = ( +const $findOverlappingPaths = ( x: number, y: number, r: number, @@ -293,9 +378,14 @@ const findOverlappingPaths = ( /** * @description 矩形内に含まれてない座標を返却 + * Return coordinates that are not inside the rectangle * WebGL版のMeshIsPointInsideRectangleServiceと同じ + * + * @param {number[]} points - 判定対象の座標配列 / Array of points to test + * @param {IPath} rectangle - 矩形パス / Rectangle path + * @return {number[] | null} 矩形外の座標またはnull / Point outside rectangle or null */ -const findPointOutsideRectangle = ( +const $findPointOutsideRectangle = ( points: number[], rectangle: IPath ): number[] | null => { @@ -340,19 +430,27 @@ const findPointOutsideRectangle = ( /** * @description ベベル結合を生成(WebGL版のMeshGenerateCalculateBevelJoinUseCaseと同じ) + * Generate bevel join (same as WebGL MeshGenerateCalculateBevelJoinUseCase) + * + * @param {number} x - 結合点のx座標 / Join point x coordinate + * @param {number} y - 結合点のy座標 / Join point y coordinate + * @param {number} r - 半径 / Radius + * @param {IPath[]} rectangles - 矩形パス配列 / Array of rectangle paths + * @param {boolean} is_last - 最後の結合かどうか / Whether this is the last join + * @return {void} */ -const generateBevelJoin = ( +const $generateBevelJoin = ( x: number, y: number, r: number, rectangles: IPath[], - isLast: boolean = false + is_last: boolean = false ): void => { // WebGL版と同じ: isLastフラグでインデックスを切り替え - const indexA = isLast ? 0 : rectangles.length - 1; - const indexB = isLast ? rectangles.length - 1 : rectangles.length - 2; - const pathsA = findOverlappingPaths(x, y, r, rectangles[indexA]); - const pathsB = findOverlappingPaths(x, y, r, rectangles[indexB]); + const indexA = is_last ? 0 : rectangles.length - 1; + const indexB = is_last ? rectangles.length - 1 : rectangles.length - 2; + const pathsA = $findOverlappingPaths(x, y, r, rectangles[indexA]); + const pathsB = $findOverlappingPaths(x, y, r, rectangles[indexB]); // パスが並行であれば終了 if (pathsA[0] === pathsB[0] && pathsA[1] === pathsB[1] @@ -361,12 +459,12 @@ const generateBevelJoin = ( return; } - const pointA = findPointOutsideRectangle(pathsA, rectangles[indexB]); + const pointA = $findPointOutsideRectangle(pathsA, rectangles[indexB]); if (!pointA) { return; } - const pointB = findPointOutsideRectangle(pathsB, rectangles[indexA]); + const pointB = $findPointOutsideRectangle(pathsB, rectangles[indexA]); if (!pointB) { return; } @@ -381,26 +479,34 @@ const generateBevelJoin = ( /** * @description ラウンド結合を生成(WebGL版のMeshGenerateCalculateRoundJoinUseCaseと同じ) + * Generate round join (same as WebGL MeshGenerateCalculateRoundJoinUseCase) + * + * @param {number} x - 結合点のx座標 / Join point x coordinate + * @param {number} y - 結合点のy座標 / Join point y coordinate + * @param {number} r - 半径 / Radius + * @param {IPath[]} rectangles - 矩形パス配列 / Array of rectangle paths + * @param {boolean} is_last - 最後の結合かどうか / Whether this is the last join + * @return {void} */ -const generateRoundJoin = ( +const $generateRoundJoin = ( x: number, y: number, r: number, rectangles: IPath[], - isLast: boolean = false + is_last: boolean = false ): void => { // WebGL版と同じ: isLastフラグでインデックスを切り替え - const indexA = isLast ? 0 : rectangles.length - 1; - const indexB = isLast ? rectangles.length - 1 : rectangles.length - 2; - const pathsA = findOverlappingPaths(x, y, r, rectangles[indexA]); - const pathsB = findOverlappingPaths(x, y, r, rectangles[indexB]); + const indexA = is_last ? 0 : rectangles.length - 1; + const indexB = is_last ? rectangles.length - 1 : rectangles.length - 2; + const pathsA = $findOverlappingPaths(x, y, r, rectangles[indexA]); + const pathsB = $findOverlappingPaths(x, y, r, rectangles[indexB]); - const pointA = findPointOutsideRectangle(pathsA, rectangles[indexB]); + const pointA = $findPointOutsideRectangle(pathsA, rectangles[indexB]); if (!pointA) { return; } - const pointB = findPointOutsideRectangle(pathsB, rectangles[indexA]); + const pointB = $findPointOutsideRectangle(pathsB, rectangles[indexA]); if (!pointB) { return; } @@ -432,19 +538,28 @@ const generateRoundJoin = ( /** * @description マイター結合を生成(WebGL版のMeshGenerateCalculateMiterJoinUseCaseと同じ) + * Generate miter join (same as WebGL MeshGenerateCalculateMiterJoinUseCase) + * + * @param {IPoint} start_point - 結合点 / Join point + * @param {IPoint} end_point - 終了点 / End point + * @param {IPoint} prev_point - 前の点 / Previous point + * @param {number} r - 半径 / Radius + * @param {IPath[]} rectangles - 矩形パス配列 / Array of rectangle paths + * @param {boolean} is_last - 最後の結合かどうか / Whether this is the last join + * @return {void} */ -const generateMiterJoin = ( - startPoint: IPoint, - endPoint: IPoint, - prevPoint: IPoint, +const $generateMiterJoin = ( + start_point: IPoint, + end_point: IPoint, + prev_point: IPoint, r: number, rectangles: IPath[], - isLast: boolean = false + is_last: boolean = false ): void => { - const indexA = isLast ? 0 : rectangles.length - 1; - const indexB = isLast ? rectangles.length - 1 : rectangles.length - 2; - const pathsA = findOverlappingPaths(startPoint.x, startPoint.y, r, rectangles[indexA]); - const pathsB = findOverlappingPaths(startPoint.x, startPoint.y, r, rectangles[indexB]); + const indexA = is_last ? 0 : rectangles.length - 1; + const indexB = is_last ? rectangles.length - 1 : rectangles.length - 2; + const pathsA = $findOverlappingPaths(start_point.x, start_point.y, r, rectangles[indexA]); + const pathsB = $findOverlappingPaths(start_point.x, start_point.y, r, rectangles[indexB]); // パスが並行であれば終了 if (pathsA[0] === pathsB[0] && pathsA[1] === pathsB[1] @@ -453,26 +568,26 @@ const generateMiterJoin = ( return; } - const pointA = findPointOutsideRectangle(pathsA, rectangles[indexB]); + const pointA = $findPointOutsideRectangle(pathsA, rectangles[indexB]); if (!pointA) { return; } - const pointB = findPointOutsideRectangle(pathsB, rectangles[indexA]); + const pointB = $findPointOutsideRectangle(pathsB, rectangles[indexA]); if (!pointB) { return; } - const aVx = endPoint.x - startPoint.x; - const aVy = endPoint.y - startPoint.y; + const aVx = end_point.x - start_point.x; + const aVy = end_point.y - start_point.y; const lengthA = Math.hypot(aVx, aVy); const normalizeA = { "x": aVx / lengthA, "y": aVy / lengthA }; - const bVx = prevPoint.x - startPoint.x; - const bVy = prevPoint.y - startPoint.y; + const bVx = prev_point.x - start_point.x; + const bVy = prev_point.y - start_point.y; const lengthB = Math.hypot(bVx, bVy); const normalizeB = { "x": bVx / lengthB, @@ -485,7 +600,7 @@ const generateMiterJoin = ( const denom = d1x * d2y - d1y * d2x; if (denom === 0) { rectangles.splice(-1, 0, [ - startPoint.x, startPoint.y, false, + start_point.x, start_point.y, false, pointA[0], pointA[1], false, pointB[0], pointB[1], false ]); @@ -498,10 +613,10 @@ const generateMiterJoin = ( const iy = pointA[1] + t * d1y; rectangles.splice(-1, 0, [ - startPoint.x, startPoint.y, false, + start_point.x, start_point.y, false, pointA[0], pointA[1], false, ix, iy, false, - startPoint.x, startPoint.y, false, + start_point.x, start_point.y, false, pointB[0], pointB[1], false, ix, iy, false ]); @@ -509,8 +624,14 @@ const generateMiterJoin = ( /** * @description ラウンドキャップを生成(WebGL版のMeshGenerateCalculateRoundCapServiceと同じ) + * Generate round cap (same as WebGL MeshGenerateCalculateRoundCapService) + * + * @param {IPath} vertices - パス頂点 / Path vertices + * @param {number} thickness - 線の太さ(半分の値) / Half line thickness + * @param {IPath[]} rectangles - 矩形パス配列 / Array of rectangle paths + * @return {void} */ -const generateRoundCap = ( +const $generateRoundCap = ( vertices: IPath, thickness: number, rectangles: IPath[] @@ -558,8 +679,14 @@ const generateRoundCap = ( /** * @description スクエアキャップを生成(WebGL版のMeshGenerateCalculateSquareCapServiceと同じ) + * Generate square cap (same as WebGL MeshGenerateCalculateSquareCapService) + * + * @param {IPath} vertices - パス頂点 / Path vertices + * @param {number} thickness - 線の太さ(半分の値) / Half line thickness + * @param {IPath[]} rectangles - 矩形パス配列 / Array of rectangle paths + * @return {void} */ -const generateSquareCap = ( +const $generateSquareCap = ( vertices: IPath, thickness: number, rectangles: IPath[] @@ -621,9 +748,9 @@ const generateSquareCap = ( * @description 線の外周を算出して塗りのフォーマットで返却(WebGL版と同じ) * Calculate the outer circumference of the line and return it in the format of the fill * - * @param {IPath} vertices - パス頂点 [x, y, isCurve, ...] - * @param {number} thickness - 線の太さ(半分の値) - * @return {IPath[]} パス配列 + * @param {IPath} vertices - パス頂点 [x, y, isCurve, ...] / Path vertices + * @param {number} thickness - 線の太さ(半分の値) / Half line thickness + * @return {IPath[]} パス配列 / Array of paths */ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath[] => { @@ -660,11 +787,11 @@ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath endPoint.y = y; if (vertices[idx - 1] as boolean) { rectangles.push( - calculateCurveRectangle(startPoint, controlPoint, endPoint, thickness) + $calculateCurveRectangle(startPoint, controlPoint, endPoint, thickness) ); } else { rectangles.push( - calculateLineRectangle(startPoint, endPoint, thickness) + $calculateLineRectangle(startPoint, endPoint, thickness) ); } @@ -672,7 +799,7 @@ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath switch ($context.joints) { case 0: // bevel - generateBevelJoin( + $generateBevelJoin( startPoint.x, startPoint.y, thickness, rectangles ); break; @@ -680,14 +807,14 @@ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath case 1: // miter prevPoint.x = vertices[idx - 6] as number; prevPoint.y = vertices[idx - 5] as number; - generateMiterJoin( + $generateMiterJoin( startPoint, endPoint, prevPoint, thickness, rectangles ); break; case 2: // round - generateRoundJoin( + $generateRoundJoin( startPoint.x, startPoint.y, thickness, rectangles ); break; @@ -715,7 +842,7 @@ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath switch ($context.joints) { case 0: // bevel - generateBevelJoin( + $generateBevelJoin( startX, startY, thickness, rectangles, true ); break; @@ -727,14 +854,14 @@ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath endPoint.y = vertices[4] as number; prevPoint.x = vertices[vertices.length - 6] as number; prevPoint.y = vertices[vertices.length - 5] as number; - generateMiterJoin( + $generateMiterJoin( startPoint, endPoint, prevPoint, thickness, rectangles, true ); break; case 2: // round - generateRoundJoin( + $generateRoundJoin( startX, startY, thickness, rectangles, true ); break; @@ -749,13 +876,13 @@ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath switch ($context.caps) { case 1: // round - generateRoundCap( + $generateRoundCap( vertices, thickness, rectangles ); break; case 2: // square - generateSquareCap( + $generateSquareCap( vertices, thickness, rectangles ); break; @@ -771,9 +898,11 @@ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath /** * @description ストロークメッシュを生成(WebGL版のMeshStrokeGenerateUseCaseと同じ) - * @param {IPath[]} vertices - パス頂点配列 - * @param {number} thickness - 線の太さ(フル値、内部で/2される) - * @return {IPath[]} + * Generate stroke mesh (same as WebGL MeshStrokeGenerateUseCase) + * + * @param {IPath[]} vertices - パス頂点配列 / Array of path vertices + * @param {number} thickness - 線の太さ(フル値、内部で/2される) / Full line thickness (halved internally) + * @return {IPath[]} 塗りフォーマットのパス配列 / Array of fill-format paths */ export const generateStrokeMesh = (vertices: IPath[], thickness: number): IPath[] => { diff --git a/packages/webgpu/src/PathCommand.ts b/packages/webgpu/src/PathCommand.ts index e50b8744..5d548eb2 100644 --- a/packages/webgpu/src/PathCommand.ts +++ b/packages/webgpu/src/PathCommand.ts @@ -12,11 +12,40 @@ import { adaptiveCubicToQuad } from "./BezierConverter/BezierConverter"; */ export class PathCommand { + /** + * @description 現在のパスデータ + * Current path data array + */ private $currentPath: IPath; + + /** + * @description 確定済みパスの配列 + * Array of finalized path vertices + */ private $vertices: IPath[]; + + /** + * @description 現在のX座標 + * Current X coordinate + */ private $currentX: number; + + /** + * @description 現在のY座標 + * Current Y coordinate + */ private $currentY: number; + + /** + * @description サブパスの開始X座標 + * Start X coordinate of current sub-path + */ private $startX: number; + + /** + * @description サブパスの開始Y座標 + * Start Y coordinate of current sub-path + */ private $startY: number; /** @@ -111,12 +140,6 @@ export class PathCommand */ private $flatnessThreshold: number = 0.25; - /** - * @description フラットネス閾値を設定 - * Set flatness threshold for adaptive bezier tessellation - * @param {number} scale - 現在のスケール(行列のスケール成分) - * @return {void} - */ /** * @description 三次ベジェ曲線を二次ベジェ曲線に適応的に近似 * Adaptively approximate cubic bezier with quadratic beziers diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.ts index cbf2193a..d8eb9d77 100644 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.ts +++ b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.ts @@ -2,17 +2,17 @@ * @description グラデーションストップ数に応じた適応的解像度を計算 * Calculate adaptive resolution based on number of gradient stops * - * @param {number} stopsCount - グラデーションストップの数 - * @param {number} [minResolution=64] - 最小解像度 - * @param {number} [maxResolution=512] - 最大解像度 + * @param {number} stops_count - グラデーションストップの数 + * @param {number} [min_resolution=64] - 最小解像度 + * @param {number} [max_resolution=512] - 最大解像度 * @return {number} * @method * @protected */ export const execute = ( - stopsCount: number, - minResolution: number = 64, - maxResolution: number = 512 + stops_count: number, + min_resolution: number = 64, + max_resolution: number = 512 ): number => { // ストップ数に応じて解像度を調整 @@ -20,17 +20,17 @@ export const execute = ( // 3-4ストップ: 128px // 5-8ストップ: 256px // 9以上: 512px - if (stopsCount <= 2) { - return Math.max(minResolution, 64); + if (stops_count <= 2) { + return Math.max(min_resolution, 64); } - if (stopsCount <= 4) { - return Math.min(maxResolution, Math.max(minResolution, 128)); + if (stops_count <= 4) { + return Math.min(max_resolution, Math.max(min_resolution, 128)); } - if (stopsCount <= 8) { - return Math.min(maxResolution, Math.max(minResolution, 256)); + if (stops_count <= 8) { + return Math.min(max_resolution, Math.max(min_resolution, 256)); } - return maxResolution; + return max_resolution; }; diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.ts index 8180844f..56b0a5df 100644 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.ts +++ b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.ts @@ -4,8 +4,8 @@ import type { IGradientStop } from "../../../interface/IGradientStop"; * @description 2つのストップ間で色を補間 * Interpolate color between two stops * - * @param {IGradientStop} startStop - * @param {IGradientStop} endStop + * @param {IGradientStop} start_stop - 開始ストップ + * @param {IGradientStop} end_stop - 終了ストップ * @param {number} t - 補間係数 (0-1) * @param {number} interpolation - 0: RGB, 1: Linear RGB * @return {{ r: number, g: number, b: number, a: number }} @@ -13,8 +13,8 @@ import type { IGradientStop } from "../../../interface/IGradientStop"; * @protected */ export const execute = ( - startStop: IGradientStop, - endStop: IGradientStop, + start_stop: IGradientStop, + end_stop: IGradientStop, t: number, interpolation: number ): { r: number; g: number; b: number; a: number } => { @@ -25,24 +25,24 @@ export const execute = ( if (interpolation === 1) { // Linear RGB補間(ガンマ補正あり) - const sr = Math.pow(startStop.r, 2.2); - const sg = Math.pow(startStop.g, 2.2); - const sb = Math.pow(startStop.b, 2.2); - const er = Math.pow(endStop.r, 2.2); - const eg = Math.pow(endStop.g, 2.2); - const eb = Math.pow(endStop.b, 2.2); + const sr = Math.pow(start_stop.r, 2.2); + const sg = Math.pow(start_stop.g, 2.2); + const sb = Math.pow(start_stop.b, 2.2); + const er = Math.pow(end_stop.r, 2.2); + const eg = Math.pow(end_stop.g, 2.2); + const eb = Math.pow(end_stop.b, 2.2); r = Math.pow(sr + (er - sr) * t, 1 / 2.2); g = Math.pow(sg + (eg - sg) * t, 1 / 2.2); b = Math.pow(sb + (eb - sb) * t, 1 / 2.2); } else { // 通常のRGB補間 - r = startStop.r + (endStop.r - startStop.r) * t; - g = startStop.g + (endStop.g - startStop.g) * t; - b = startStop.b + (endStop.b - startStop.b) * t; + r = start_stop.r + (end_stop.r - start_stop.r) * t; + g = start_stop.g + (end_stop.g - start_stop.g) * t; + b = start_stop.b + (end_stop.b - start_stop.b) * t; } - const a = startStop.a + (endStop.a - startStop.a) * t; + const a = start_stop.a + (end_stop.a - start_stop.a) * t; return { r, g, b, a }; }; diff --git a/packages/webgpu/src/Shader/PipelineManager.ts b/packages/webgpu/src/Shader/PipelineManager.ts index d18dc0b7..fd85a135 100644 --- a/packages/webgpu/src/Shader/PipelineManager.ts +++ b/packages/webgpu/src/Shader/PipelineManager.ts @@ -1,7 +1,11 @@ import { ShaderSource } from "./ShaderSource"; import { $samples } from "../WebGPUUtil"; -const VERTEX_BUFFER_LAYOUT_4F: GPUVertexBufferLayout = { +/** + * @description 4フロートストライドの頂点バッファレイアウト(position: float32x2, uv: float32x2) + * Vertex buffer layout with 4-float stride (position: float32x2, uv: float32x2) + */ +const $VERTEX_BUFFER_LAYOUT_4F: GPUVertexBufferLayout = { "arrayStride": 4 * 4, "attributes": [ { "shaderLocation": 0, "offset": 0, "format": "float32x2" }, @@ -9,7 +13,11 @@ const VERTEX_BUFFER_LAYOUT_4F: GPUVertexBufferLayout = { ] }; -const BLEND_PREMULTIPLIED_ALPHA: GPUBlendState = { +/** + * @description プリマルチプライドアルファのブレンドステート + * Premultiplied alpha blend state for standard compositing + */ +const $BLEND_PREMULTIPLIED_ALPHA: GPUBlendState = { "color": { "srcFactor": "one", "dstFactor": "one-minus-src-alpha", @@ -22,16 +30,54 @@ const BLEND_PREMULTIPLIED_ALPHA: GPUBlendState = { } }; +/** + * @description WebGPUレンダーパイプラインの管理クラス。パイプラインとバインドグループレイアウトの生成・キャッシュを行う + * Manager class for WebGPU render pipelines. Creates, caches, and manages pipelines and bind group layouts + */ export class PipelineManager { + /** + * @description GPUデバイスの参照 + * Reference to the GPU device + */ private device: GPUDevice; + /** + * @description 出力テクスチャフォーマット + * Output texture format + */ private format: GPUTextureFormat; + /** + * @description パイプライン名からGPURenderPipelineへのキャッシュマップ + * Cache map from pipeline name to GPURenderPipeline + */ private pipelines: Map; + /** + * @description バインドグループレイアウト名からGPUBindGroupLayoutへのキャッシュマップ + * Cache map from layout name to GPUBindGroupLayout + */ private bindGroupLayouts: Map; + /** + * @description MSAAサンプル数 + * MSAA sample count + */ private sampleCount: number; + /** + * @description シェーダーモジュールのキャッシュ + * Shader module cache by key + */ private shaderModuleCache: Map = new Map(); + /** + * @description フィルター用バインドグループレイアウトキャッシュ(テクスチャ数別) + * Filter bind group layout cache indexed by texture count + */ private filterBindGroupLayouts: Map = new Map(); + /** + * @description PipelineManagerのコンストラクタ。GPUデバイスとフォーマットを設定し、パイプラインを初期化する + * Construct PipelineManager. Sets GPU device, format, and initializes pipelines + * @param {GPUDevice} device - GPUデバイス / GPU device instance + * @param {GPUTextureFormat} format - テクスチャフォーマット / Texture format for output + */ constructor(device: GPUDevice, format: GPUTextureFormat) { this.device = device; @@ -43,6 +89,13 @@ export class PipelineManager this.initialize(); } + /** + * @description シェーダーモジュールをキャッシュから取得、または新規作成する + * Get a shader module from cache, or create and cache a new one + * @param {string} key - キャッシュキー / Cache key + * @param {string} code - WGSLシェーダーコード / WGSL shader source code + * @return {GPUShaderModule} シェーダーモジュール / The shader module + */ private getOrCreateShaderModule(key: string, code: string): GPUShaderModule { let module = this.shaderModuleCache.get(key); @@ -53,6 +106,10 @@ export class PipelineManager return module; } + /** + * @description 初期パイプライン群を一括作成する + * Create all initial render pipelines + */ private initialize(): void { this.createFillPipeline(); @@ -69,7 +126,15 @@ export class PipelineManager this.createNodeClearPipeline(); } + /** + * @description 初期化済みの遅延グループ名セット + * Set of initialized lazy group names + */ private lazyInitGroups: Set = new Set(); + /** + * @description パイプライン名から遅延初期化グループ名への読み取り専用マップ + * Read-only map from pipeline name to lazy initialization group name + */ private readonly lazyGroupMap: ReadonlyMap = new Map([ ...Array.from({ "length": 16 }, (_, i): [string, string] => [`blur_filter_${i + 1}`, "blur_filter"]), ["blur_filter", "blur_filter"], @@ -99,6 +164,11 @@ export class PipelineManager ["filter_complex_blend_output_msaa", "complex_blend"] ]); + /** + * @description 指定された名前に対応する遅延初期化グループをまだ初期化されていなければ初期化する + * Ensure the lazy initialization group for the given name is initialized + * @param {string} name - パイプライン名 / Pipeline name to look up its lazy group + */ private ensureLazyGroup(name: string): void { const group = this.lazyGroupMap.get(name); @@ -126,6 +196,10 @@ export class PipelineManager } } + /** + * @description すべての遅延初期化グループを事前にロードする + * Preload all lazy initialization groups eagerly + */ preloadLazyGroups(): void { const groups = ["blur_filter", "texture_copy", "bitmap_sync", "filter", "complex_blend"]; @@ -134,6 +208,10 @@ export class PipelineManager } } + /** + * @description 塗りつぶし描画用パイプラインを作成する(RGBA/BGRA/ステンシル対応) + * Create fill render pipelines (RGBA, BGRA, and stencil variants) + */ private createFillPipeline(): void { // Dynamic Offset対応のBindGroupLayout(fill + stencil共有) @@ -157,8 +235,8 @@ export class PipelineManager const fragmentShaderModule = this.getOrCreateShaderModule("fillFragment", ShaderSource.getFillFragmentShader()); - const vertexBufferLayout = VERTEX_BUFFER_LAYOUT_4F; - const blendState = BLEND_PREMULTIPLIED_ALPHA; + const vertexBufferLayout = $VERTEX_BUFFER_LAYOUT_4F; + const blendState = $BLEND_PREMULTIPLIED_ALPHA; const pipelineRGBA = this.device.createRenderPipeline({ "layout": pipelineLayout, "vertex": { @@ -273,9 +351,13 @@ export class PipelineManager this.pipelines.set("fill_bgra_stencil", pipelineBGRAStencil); } + /** + * @description ステンシル塗りつぶし用パイプラインを作成する(書き込み・塗りつぶし・アトラス・メイン・マスク対応) + * Create stencil fill pipelines (write, fill, atlas, main, and masked variants) + */ private createStencilFillPipelines(): void { - const vertexBufferLayout = VERTEX_BUFFER_LAYOUT_4F; + const vertexBufferLayout = $VERTEX_BUFFER_LAYOUT_4F; // fill_dynamicレイアウトを共有(hasDynamicOffset: true) const dynamicLayout = this.bindGroupLayouts.get("fill_dynamic")!; @@ -625,9 +707,13 @@ export class PipelineManager this.pipelines.set("stencil_fill_masked", stencilFillMaskedPipeline); } + /** + * @description クリッピング用パイプラインを作成する(レベル別の書き込み・クリア) + * Create clip pipelines (level-based write and clear variants) + */ private createClipPipeline(): void { - const vertexBufferLayout = VERTEX_BUFFER_LAYOUT_4F; + const vertexBufferLayout = $VERTEX_BUFFER_LAYOUT_4F; const dynamicLayout = this.bindGroupLayouts.get("fill_dynamic")!; const clipPipelineLayout = this.device.createPipelineLayout({ "bindGroupLayouts": [dynamicLayout] @@ -770,9 +856,13 @@ export class PipelineManager } } + /** + * @description マスク合成用パイプラインを作成する(レベル別のマージ・クリア) + * Create mask union pipelines (level-based merge and clear variants) + */ private createMaskUnionPipelines(): void { - const vertexBufferLayout = VERTEX_BUFFER_LAYOUT_4F; + const vertexBufferLayout = $VERTEX_BUFFER_LAYOUT_4F; const dynamicLayout = this.bindGroupLayouts.get("fill_dynamic")!; const maskUnionPipelineLayout = this.device.createPipelineLayout({ "bindGroupLayouts": [dynamicLayout] @@ -864,6 +954,10 @@ export class PipelineManager } } + /** + * @description マスク描画用パイプラインを作成する + * Create mask render pipeline + */ private createMaskPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -935,6 +1029,10 @@ export class PipelineManager this.pipelines.set("mask", pipeline); } + /** + * @description 基本描画パイプラインを作成する(RGBA/BGRA) + * Create basic render pipelines (RGBA and BGRA variants) + */ private createBasicPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -971,7 +1069,7 @@ export class PipelineManager ] }; - const blendState = BLEND_PREMULTIPLIED_ALPHA; + const blendState = $BLEND_PREMULTIPLIED_ALPHA; const pipelineRGBA = this.device.createRenderPipeline({ "layout": pipelineLayout, "vertex": { @@ -1021,6 +1119,10 @@ export class PipelineManager this.pipelines.set("basic_bgra", pipelineBGRA); } + /** + * @description テクスチャ描画用パイプラインを作成する + * Create texture render pipeline + */ private createTexturePipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -1102,6 +1204,10 @@ export class PipelineManager this.pipelines.set("texture", pipeline); } + /** + * @description インスタンス描画用パイプラインを作成する(ブレンドバリアント・マスク対応) + * Create instanced render pipelines (blend variants and masked) + */ private createInstancedPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -1321,6 +1427,10 @@ export class PipelineManager this.pipelines.set("instanced_masked", maskedPipeline); } + /** + * @description グラデーション塗りつぶし用パイプラインを作成する(RGBA/BGRA/ステンシル対応) + * Create gradient fill pipelines (RGBA, BGRA, stencil, and stroke variants) + */ private createGradientPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -1358,8 +1468,8 @@ export class PipelineManager const stencilFragmentShaderModule = this.getOrCreateShaderModule("gradientFillStencilFragment", ShaderSource.getGradientFillStencilFragmentShader()); this.gradientStencilFragmentShaderModule = stencilFragmentShaderModule; - const vertexBufferLayout = VERTEX_BUFFER_LAYOUT_4F; - const blendState = BLEND_PREMULTIPLIED_ALPHA; + const vertexBufferLayout = $VERTEX_BUFFER_LAYOUT_4F; + const blendState = $BLEND_PREMULTIPLIED_ALPHA; const pipelineRGBA = this.device.createRenderPipeline({ "label": "gradient_fill_no_stencil_pipeline", "layout": pipelineLayout, @@ -1675,6 +1785,10 @@ export class PipelineManager this.pipelines.set("gradient_fill_bgra_stencil", pipelineBGRAStencil); } + /** + * @description ビットマップ塗りつぶし用パイプラインを作成する(RGBA/BGRA/ステンシル・ストローク対応) + * Create bitmap fill pipelines (RGBA, BGRA, stencil, and stroke variants) + */ private createBitmapFillPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -1707,8 +1821,8 @@ export class PipelineManager const fragmentShaderModule = this.getOrCreateShaderModule("bitmapFillFragment", ShaderSource.getBitmapFillFragmentShader()); - const vertexBufferLayout = VERTEX_BUFFER_LAYOUT_4F; - const blendState = BLEND_PREMULTIPLIED_ALPHA; + const vertexBufferLayout = $VERTEX_BUFFER_LAYOUT_4F; + const blendState = $BLEND_PREMULTIPLIED_ALPHA; const pipelineRGBA = this.device.createRenderPipeline({ "layout": pipelineLayout, "vertex": { @@ -1908,6 +2022,10 @@ export class PipelineManager this.pipelines.set("bitmap_fill_bgra_stencil", pipelineBGRAStencil); } + /** + * @description ブレンド描画用パイプラインを作成する(デュアルテクスチャブレンド) + * Create blend render pipeline for dual-texture blending + */ private createBlendPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -2004,6 +2122,10 @@ export class PipelineManager this.pipelines.set("blend", pipeline); } + /** + * @description ブラーフィルター用パイプラインを作成する(halfBlur 1〜16のバリアント) + * Create blur filter pipelines (halfBlur 1-16 variants) + */ private createBlurFilterPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -2071,6 +2193,10 @@ export class PipelineManager } } + /** + * @description テクスチャコピー用パイプラインを作成する(各種ブレンド・フィルター出力・MSAA対応) + * Create texture copy pipelines (various blend modes, filter output, MSAA variants) + */ private createTextureCopyPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -2207,6 +2333,10 @@ export class PipelineManager this.createTextureScalePipeline(); } + /** + * @description 位置指定テクスチャ描画用パイプラインを作成する(RGBA/ビットマップレンダー対応) + * Create positioned texture pipelines (RGBA, bitmap render, MSAA variants) + */ private createPositionedTexturePipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -2401,6 +2531,10 @@ export class PipelineManager this.pipelines.set("bitmap_render", pipelineNonMsaa); } + /** + * @description テクスチャスケーリング用パイプラインを作成する + * Create texture scale pipelines + */ private createTextureScalePipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -2500,6 +2634,10 @@ export class PipelineManager this.pipelines.set("texture_scale_blend", blendPipeline); } + /** + * @description ビットマップ同期描画用パイプラインを作成する(MSAA 4x) + * Create bitmap sync render pipeline (MSAA 4x) + */ private createBitmapSyncPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -2565,6 +2703,10 @@ export class PipelineManager this.pipelines.set("bitmap_sync", pipeline); } + /** + * @description カラーマトリクスフィルター・ベベル・グロー・ドロップシャドウ等のフィルターパイプラインを作成する + * Create filter pipelines (color matrix, bevel, glow, drop shadow, gradient glow/bevel) + */ private createColorMatrixFilterPipeline(): void { const BLEND_REPLACE: GPUBlendState = { @@ -2584,6 +2726,10 @@ export class PipelineManager this.createFilterPipelineWithLayout("gradient_bevel_filter", ShaderSource.getGradientBevelFilterFragmentShader(), 3, BLEND_ALPHA); } + /** + * @description 複合ブレンド用パイプラインを作成する + * Create complex blend pipelines + */ private createComplexBlendPipelines(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -2656,6 +2802,10 @@ export class PipelineManager this.createComplexBlendOutputPipeline(); } + /** + * @description 複合ブレンドのコピー・スケール用パイプラインを作成する + * Create complex blend copy and scale pipelines + */ private createComplexBlendCopyPipeline(): void { const BLEND_REPLACE: GPUBlendState = { @@ -2682,6 +2832,10 @@ export class PipelineManager } } + /** + * @description 複合ブレンドの出力用パイプラインを作成する(MSAA対応含む) + * Create complex blend output pipelines (including MSAA variants) + */ private createComplexBlendOutputPipeline(): void { const bindGroupLayout = this.bindGroupLayouts.get("positioned_texture"); @@ -2715,56 +2869,76 @@ export class PipelineManager } } + /** + * @description 指定されたフラグメントシェーダーとテクスチャ数でフィルターパイプラインを作成する + * Create a filter pipeline with the specified fragment shader and texture count + * @param {string} name - パイプライン名 / Pipeline name + * @param {string} fragment_shader_code - フラグメントシェーダーコード / Fragment shader WGSL source code + * @param {number} texture_count - テクスチャバインディング数 / Number of texture bindings + * @param {GPUBlendState} blend - ブレンドステート / Blend state configuration + */ private createFilterPipelineWithLayout( name: string, - fragmentShaderCode: string, - textureCount: number, + fragment_shader_code: string, + texture_count: number, blend: GPUBlendState ): void { - let bindGroupLayout = this.filterBindGroupLayouts.get(textureCount); + let bindGroupLayout = this.filterBindGroupLayouts.get(texture_count); if (!bindGroupLayout) { const entries: GPUBindGroupLayoutEntry[] = [ { "binding": 0, "visibility": GPUShaderStage.FRAGMENT, "buffer": { "type": "uniform" } }, { "binding": 1, "visibility": GPUShaderStage.FRAGMENT, "sampler": {} } ]; - for (let i = 0; i < textureCount; i++) { + for (let i = 0; i < texture_count; i++) { entries.push({ "binding": 2 + i, "visibility": GPUShaderStage.FRAGMENT, "texture": {} }); } bindGroupLayout = this.device.createBindGroupLayout({ "entries": entries }); - this.filterBindGroupLayouts.set(textureCount, bindGroupLayout); + this.filterBindGroupLayouts.set(texture_count, bindGroupLayout); } this.bindGroupLayouts.set(name, bindGroupLayout); const pipelineLayout = this.device.createPipelineLayout({ "bindGroupLayouts": [bindGroupLayout] }); const vertexShaderModule = this.getOrCreateShaderModule("blurFilterVertex", ShaderSource.getBlurFilterVertexShader()); - const fragmentShaderModule = this.getOrCreateShaderModule(`filter_${name}`, fragmentShaderCode); + const fragmentShaderModule = this.getOrCreateShaderModule(`filter_${name}`, fragment_shader_code); this.pipelines.set(name, this.createFullscreenQuadPipeline( pipelineLayout, vertexShaderModule, fragmentShaderModule, "rgba8unorm", blend )); } + /** + * @description フルスクリーンクアッド描画用の汎用パイプラインを作成する + * Create a generic fullscreen quad render pipeline + * @param {GPUPipelineLayout} pipeline_layout - パイプラインレイアウト / Pipeline layout + * @param {GPUShaderModule} vertex_module - 頂点シェーダーモジュール / Vertex shader module + * @param {GPUShaderModule} fragment_module - フラグメントシェーダーモジュール / Fragment shader module + * @param {GPUTextureFormat} format - テクスチャフォーマット / Target texture format + * @param {GPUBlendState} blend - ブレンドステート / Blend state configuration + * @param {number} multisample_count - MSAAサンプル数(任意) / Optional MSAA sample count + * @param {GPUDepthStencilState} depth_stencil - 深度ステンシルステート(任意) / Optional depth-stencil state + * @return {GPURenderPipeline} レンダーパイプライン / The created render pipeline + */ private createFullscreenQuadPipeline( - pipelineLayout: GPUPipelineLayout, - vertexModule: GPUShaderModule, - fragmentModule: GPUShaderModule, + pipeline_layout: GPUPipelineLayout, + vertex_module: GPUShaderModule, + fragment_module: GPUShaderModule, format: GPUTextureFormat, blend: GPUBlendState, - multisampleCount?: number, - depthStencil?: GPUDepthStencilState + multisample_count?: number, + depth_stencil?: GPUDepthStencilState ): GPURenderPipeline { const descriptor: GPURenderPipelineDescriptor = { - "layout": pipelineLayout, + "layout": pipeline_layout, "vertex": { - "module": vertexModule, + "module": vertex_module, "entryPoint": "main", "buffers": [] }, "fragment": { - "module": fragmentModule, + "module": fragment_module, "entryPoint": "main", "targets": [{ "format": format, "blend": blend }] }, @@ -2774,17 +2948,23 @@ export class PipelineManager } }; - if (multisampleCount && multisampleCount > 1) { - descriptor.multisample = { "count": multisampleCount }; + if (multisample_count && multisample_count > 1) { + descriptor.multisample = { "count": multisample_count }; } - if (depthStencil) { - descriptor.depthStencil = depthStencil; + if (depth_stencil) { + descriptor.depthStencil = depth_stencil; } return this.device.createRenderPipeline(descriptor); } + /** + * @description 名前からレンダーパイプラインを取得する(遅延初期化を含む) + * Get a render pipeline by name, initializing lazy groups if needed + * @param {string} name - パイプライン名 / Pipeline name + * @return {GPURenderPipeline | undefined} パイプラインまたはundefined / The pipeline or undefined + */ getPipeline(name: string): GPURenderPipeline | undefined { let pipeline = this.pipelines.get(name); @@ -2796,15 +2976,18 @@ export class PipelineManager } /** - * @description フィルターパイプラインのoverride定数バリアントを取得 - * GPU warp divergenceを排除するコンパイル時分岐特殊化 + * @description フィルターパイプラインのoverride定数バリアントを取得する。GPU warp divergenceを排除するコンパイル時分岐特殊化 + * Get a filter pipeline variant with override constants. Compile-time branch specialization to eliminate GPU warp divergence + * @param {string} base_name - ベースパイプライン名 / Base pipeline name + * @param {Record} constants - オーバーライド定数 / Override constant values + * @return {GPURenderPipeline | undefined} パイプラインまたはundefined / The pipeline or undefined */ - getFilterPipeline(baseName: string, constants: Record): GPURenderPipeline | undefined + getFilterPipeline(base_name: string, constants: Record): GPURenderPipeline | undefined { // キャッシュキーを生成 const keys = Object.keys(constants).sort(); const suffix = keys.map((k) => `${k}${constants[k]}`).join("_"); - const cacheKey = `${baseName}_${suffix}`; + const cacheKey = `${base_name}_${suffix}`; let pipeline = this.pipelines.get(cacheKey); if (pipeline) { @@ -2812,14 +2995,14 @@ export class PipelineManager } // ベースグループのロードを確保 - this.ensureLazyGroup(baseName); + this.ensureLazyGroup(base_name); - const fragmentModule = this.shaderModuleCache.get(`filter_${baseName}`); + const fragmentModule = this.shaderModuleCache.get(`filter_${base_name}`); const vertexModule = this.shaderModuleCache.get("blurFilterVertex"); - const bindGroupLayout = this.bindGroupLayouts.get(baseName); + const bindGroupLayout = this.bindGroupLayouts.get(base_name); if (!fragmentModule || !vertexModule || !bindGroupLayout) { - return this.pipelines.get(baseName); + return this.pipelines.get(base_name); } const pipelineLayout = this.device.createPipelineLayout({ @@ -2856,57 +3039,85 @@ export class PipelineManager } /** - * @description グラデーションタイプとスプレッドモードに応じた特殊化パイプラインを取得 - * override定数でGPU warp divergenceを排除 + * @description グラデーションタイプとスプレッドモードに応じた特殊化パイプラインを取得する。override定数でGPU warp divergenceを排除 + * Get a gradient pipeline specialized by gradient type and spread mode. Uses override constants to eliminate GPU warp divergence + * @param {string} base_name - ベースパイプライン名 / Base pipeline name + * @param {number} gradient_type - グラデーションタイプ / Gradient type identifier + * @param {number} spread_mode - スプレッドモード / Spread mode identifier + * @return {GPURenderPipeline | undefined} パイプラインまたはundefined / The pipeline or undefined */ - getGradientPipeline(baseName: string, gradientType: number, spreadMode: number): GPURenderPipeline | undefined + getGradientPipeline(base_name: string, gradient_type: number, spread_mode: number): GPURenderPipeline | undefined { - const key = `${baseName}_t${gradientType}s${spreadMode}`; + const key = `${base_name}_t${gradient_type}s${spread_mode}`; let pipeline = this.pipelines.get(key); if (pipeline) { return pipeline; } if (!this.gradientPipelineLayout) { - return this.getPipeline(baseName); + return this.getPipeline(base_name); } // ベースパイプラインと同じ構成でoverride定数を変えて作成 - pipeline = this.createGradientVariant(baseName, gradientType, spreadMode); + pipeline = this.createGradientVariant(base_name, gradient_type, spread_mode); if (pipeline) { this.pipelines.set(key, pipeline); return pipeline; } // フォールバック: デフォルト定数のベースパイプラインを使用 - return this.getPipeline(baseName); + return this.getPipeline(base_name); } + /** + * @description グラデーション用パイプラインレイアウト + * Pipeline layout for gradient pipelines + */ private gradientPipelineLayout: GPUPipelineLayout | null = null; + /** + * @description グラデーション用頂点シェーダーモジュール + * Vertex shader module for gradient pipelines + */ private gradientVertexShaderModule: GPUShaderModule | null = null; + /** + * @description グラデーション用フラグメントシェーダーモジュール + * Fragment shader module for gradient pipelines + */ private gradientFragmentShaderModule: GPUShaderModule | null = null; + /** + * @description グラデーション用ステンシルフラグメントシェーダーモジュール + * Stencil fragment shader module for gradient pipelines + */ private gradientStencilFragmentShaderModule: GPUShaderModule | null = null; - private createGradientVariant(baseName: string, gradientType: number, spreadMode: number): GPURenderPipeline | undefined + /** + * @description グラデーションバリアントパイプラインを作成する(override定数による特殊化) + * Create a gradient variant pipeline with override constants for specialization + * @param {string} base_name - ベースパイプライン名 / Base pipeline name + * @param {number} gradient_type - グラデーションタイプ / Gradient type identifier + * @param {number} spread_mode - スプレッドモード / Spread mode identifier + * @return {GPURenderPipeline | undefined} パイプラインまたはundefined / The pipeline or undefined + */ + private createGradientVariant(base_name: string, gradient_type: number, spread_mode: number): GPURenderPipeline | undefined { if (!this.gradientPipelineLayout) { return undefined; } const constants = { - "GRADIENT_TYPE": gradientType, - "SPREAD_MODE": spreadMode + "GRADIENT_TYPE": gradient_type, + "SPREAD_MODE": spread_mode }; - const vertexBufferLayout = VERTEX_BUFFER_LAYOUT_4F; - const blendState = BLEND_PREMULTIPLIED_ALPHA; + const vertexBufferLayout = $VERTEX_BUFFER_LAYOUT_4F; + const blendState = $BLEND_PREMULTIPLIED_ALPHA; // ベース名からパイプライン構成を決定 - const isStencilFragment = baseName.includes("stencil_atlas") || baseName === "gradient_fill_stencil_main"; + const isStencilFragment = base_name.includes("stencil_atlas") || base_name === "gradient_fill_stencil_main"; const fragModule = isStencilFragment ? this.gradientStencilFragmentShaderModule! : this.gradientFragmentShaderModule!; - const isBGRA = baseName.includes("bgra") || baseName === "gradient_fill_stencil_main"; + const isBGRA = base_name.includes("bgra") || base_name === "gradient_fill_stencil_main"; const format: GPUTextureFormat = isBGRA ? this.format : "rgba8unorm"; - const needsYFlip = baseName.includes("bgra") || baseName === "gradient_fill_stencil_main"; + const needsYFlip = base_name.includes("bgra") || base_name === "gradient_fill_stencil_main"; const vertexConstants: Record = {}; if (needsYFlip) { @@ -2916,7 +3127,7 @@ export class PipelineManager let depthStencil: GPUDepthStencilState | undefined; let sampleCount = this.sampleCount; - if (baseName.includes("stroke")) { + if (base_name.includes("stroke")) { depthStencil = { "format": "stencil8", "stencilFront": { "compare": "always", "failOp": "keep", "depthFailOp": "keep", "passOp": "keep" }, @@ -2924,7 +3135,7 @@ export class PipelineManager "stencilReadMask": 0x00, "stencilWriteMask": 0x00 }; - } else if (baseName === "gradient_fill_stencil" || baseName === "gradient_fill_stencil_atlas") { + } else if (base_name === "gradient_fill_stencil" || base_name === "gradient_fill_stencil_atlas") { depthStencil = { "format": "stencil8", "stencilFront": { "compare": "not-equal", "failOp": "keep", "depthFailOp": "zero", "passOp": "zero" }, @@ -2932,7 +3143,7 @@ export class PipelineManager "stencilReadMask": 0xFF, "stencilWriteMask": 0xFF }; - } else if (baseName === "gradient_fill_stencil_main") { + } else if (base_name === "gradient_fill_stencil_main") { depthStencil = { "format": "stencil8", "stencilFront": { "compare": "not-equal", "failOp": "keep", "depthFailOp": "zero", "passOp": "zero" }, @@ -2941,7 +3152,7 @@ export class PipelineManager "stencilWriteMask": 0xFF }; sampleCount = 1; - } else if (baseName === "gradient_fill_bgra_stencil") { + } else if (base_name === "gradient_fill_bgra_stencil") { depthStencil = { "format": "stencil8", "stencilFront": { "compare": "equal", "failOp": "keep", "depthFailOp": "keep", "passOp": "keep" }, @@ -2949,7 +3160,7 @@ export class PipelineManager "stencilReadMask": 0xFF, "stencilWriteMask": 0x00 }; - } else if (baseName === "gradient_fill_bgra_no_msaa") { + } else if (base_name === "gradient_fill_bgra_no_msaa") { sampleCount = 1; } @@ -2978,6 +3189,12 @@ export class PipelineManager return this.device.createRenderPipeline(descriptor); } + /** + * @description 名前からバインドグループレイアウトを取得する(遅延初期化を含む) + * Get a bind group layout by name, initializing lazy groups if needed + * @param {string} name - バインドグループレイアウト名 / Bind group layout name + * @return {GPUBindGroupLayout | undefined} レイアウトまたはundefined / The layout or undefined + */ getBindGroupLayout(name: string): GPUBindGroupLayout | undefined { let layout = this.bindGroupLayouts.get(name); @@ -2988,6 +3205,10 @@ export class PipelineManager return layout; } + /** + * @description ノードクリア用パイプラインを作成する(カラーとステンシルの同時クリア) + * Create node clear pipeline for simultaneous color and stencil clear + */ private createNodeClearPipeline(): void { const vertexBufferLayout: GPUVertexBufferLayout = { @@ -3055,6 +3276,10 @@ export class PipelineManager this.pipelines.set("node_clear_atlas", nodeClearPipeline); } + /** + * @description すべてのパイプライン・レイアウト・シェーダーモジュールを解放する + * Dispose all pipelines, layouts, and shader module caches + */ dispose(): void { this.pipelines.clear(); diff --git a/packages/webgpu/src/Shader/ShaderInstancedManager.ts b/packages/webgpu/src/Shader/ShaderInstancedManager.ts index b2d3d233..9627c0a2 100644 --- a/packages/webgpu/src/Shader/ShaderInstancedManager.ts +++ b/packages/webgpu/src/Shader/ShaderInstancedManager.ts @@ -2,16 +2,33 @@ import { renderQueue } from "@next2d/render-queue"; /** * @description WebGPU用インスタンスシェーダーマネージャー + * WebGPU instanced shader manager for batch rendering */ export class ShaderInstancedManager { + /** + * @description 現在のインスタンス描画カウント + * Current instance draw count + * + * @type {number} + */ public count: number; + /** + * @description ShaderInstancedManagerを初期化する + * Initialize ShaderInstancedManager + */ constructor() { this.count = 0; } + /** + * @description インスタンスカウントとレンダーキューオフセットをリセットする + * Reset instance count and render queue offset + * + * @return {void} + */ clear(): void { this.count = renderQueue.offset = 0; diff --git a/packages/webgpu/src/Shader/ShaderSource.ts b/packages/webgpu/src/Shader/ShaderSource.ts index f2563f62..15106b0a 100644 --- a/packages/webgpu/src/Shader/ShaderSource.ts +++ b/packages/webgpu/src/Shader/ShaderSource.ts @@ -36,126 +36,275 @@ import { } from "./wgsl/fragment/EffectFragment"; import { WgslIsInside, WgslVertexOutput } from "./wgsl/common/SharedWgsl"; +/** + * @description WebGPU用シェーダーソース管理クラス + * WebGPU shader source management class providing all vertex and fragment shaders + */ export class ShaderSource { + /** + * @description 塗り用頂点シェーダーを取得する + * Get fill vertex shader + * + * @return {string} + */ static getFillVertexShader (): string { return FillVertex; } + /** + * @description 塗り用フラグメントシェーダーを取得する + * Get fill fragment shader + * + * @return {string} + */ static getFillFragmentShader (): string { return FillFragment; } + /** + * @description ステンシル書き込み用頂点シェーダーを取得する + * Get stencil write vertex shader + * + * @return {string} + */ static getStencilWriteVertexShader (): string { return StencilWriteVertex; } + /** + * @description ステンシル書き込み用フラグメントシェーダーを取得する + * Get stencil write fragment shader + * + * @return {string} + */ static getStencilWriteFragmentShader (): string { return StencilWriteFragment; } + /** + * @description ステンシル塗り用頂点シェーダーを取得する + * Get stencil fill vertex shader + * + * @return {string} + */ static getStencilFillVertexShader (): string { return StencilFillVertex; } + /** + * @description ステンシル塗り用フラグメントシェーダーを取得する + * Get stencil fill fragment shader + * + * @return {string} + */ static getStencilFillFragmentShader (): string { return StencilFillFragment; } + /** + * @description マスク用頂点シェーダーを取得する + * Get mask vertex shader + * + * @return {string} + */ static getMaskVertexShader (): string { return MaskVertex; } + /** + * @description マスク用フラグメントシェーダーを取得する + * Get mask fragment shader + * + * @return {string} + */ static getMaskFragmentShader (): string { return MaskFragment; } + /** + * @description 基本頂点シェーダーを取得する + * Get basic vertex shader + * + * @return {string} + */ static getBasicVertexShader (): string { return BasicVertex; } + /** + * @description 基本フラグメントシェーダーを取得する + * Get basic fragment shader + * + * @return {string} + */ static getBasicFragmentShader (): string { return BasicFragment; } + /** + * @description テクスチャフラグメントシェーダーを取得する + * Get texture fragment shader + * + * @return {string} + */ static getTextureFragmentShader (): string { return TextureFragment; } + /** + * @description インスタンス描画用頂点シェーダーを取得する + * Get instanced vertex shader + * + * @return {string} + */ static getInstancedVertexShader (): string { return InstancedVertex; } + /** + * @description インスタンス描画用フラグメントシェーダーを取得する + * Get instanced fragment shader + * + * @return {string} + */ static getInstancedFragmentShader (): string { return InstancedFragment; } + /** + * @description グラデーション塗り用頂点シェーダーを取得する + * Get gradient fill vertex shader + * + * @return {string} + */ static getGradientFillVertexShader (): string { return GradientFillVertex; } + /** + * @description グラデーション塗り用フラグメントシェーダーを取得する + * Get gradient fill fragment shader + * + * @return {string} + */ static getGradientFillFragmentShader (): string { return GradientFillFragment; } + /** + * @description ステンシル用グラデーション塗りフラグメントシェーダーを取得する + * Get gradient fill stencil fragment shader + * + * @return {string} + */ static getGradientFillStencilFragmentShader (): string { return GradientFillStencilFragment; } + /** + * @description グラデーションフラグメントシェーダーを取得する + * Get gradient fragment shader + * + * @return {string} + */ static getGradientFragmentShader (): string { return GradientFragment; } + /** + * @description ビットマップ塗り用頂点シェーダーを取得する + * Get bitmap fill vertex shader + * + * @return {string} + */ static getBitmapFillVertexShader (): string { return BitmapFillVertex; } + /** + * @description ビットマップ塗り用フラグメントシェーダーを取得する + * Get bitmap fill fragment shader + * + * @return {string} + */ static getBitmapFillFragmentShader (): string { return BitmapFillFragment; } + /** + * @description ブレンド用フラグメントシェーダーを取得する + * Get blend fragment shader + * + * @return {string} + */ static getBlendFragmentShader (): string { return BlendGenericFragment; } + /** + * @description ブラーフィルター用頂点シェーダーを取得する + * Get blur filter vertex shader + * + * @return {string} + */ static getBlurFilterVertexShader (): string { return BlurFilterVertex; } + /** + * @description ビットマップ同期用頂点シェーダーを取得する + * Get bitmap sync vertex shader + * + * @return {string} + */ static getBitmapSyncVertexShader (): string { return BitmapSyncVertex; } + /** + * @description ビットマップ同期用フラグメントシェーダーを取得する + * Get bitmap sync fragment shader + * + * @return {string} + */ static getBitmapSyncFragmentShader (): string { return BitmapSyncFragment; } - static getBlurFilterFragmentShader (halfBlur: number): string + /** + * @description ブラーフィルター用フラグメントシェーダーを生成する + * Generate blur filter fragment shader + * + * @param {number} half_blur - ブラーの半径値 + * @return {string} + */ + static getBlurFilterFragmentShader (half_blur: number): string { - const halfBlurFixed = halfBlur.toFixed(1); + const halfBlurFixed = half_blur.toFixed(1); return /* wgsl */` ${WgslVertexOutput} @@ -188,76 +337,158 @@ fn main(input: VertexOutput) -> @location(0) vec4 { `; } + /** + * @description テクスチャコピー用フラグメントシェーダーを取得する + * Get texture copy fragment shader + * + * @return {string} + */ static getTextureCopyFragmentShader (): string { return TextureCopyFragment; } + /** + * @description ブラー用テクスチャコピーフラグメントシェーダーを取得する + * Get blur texture copy fragment shader + * + * @return {string} + */ static getBlurTextureCopyFragmentShader (): string { return BlurTextureCopyFragment; } + /** + * @description フィルター出力用フラグメントシェーダーを取得する + * Get filter output fragment shader + * + * @return {string} + */ static getFilterOutputFragmentShader (): string { return FilterOutputFragment; } + /** + * @description カラー変換フラグメントシェーダーを取得する + * Get color transform fragment shader + * + * @return {string} + */ static getColorTransformFragmentShader (): string { return ColorTransformFragment; } + /** + * @description Y軸反転付きカラー変換フラグメントシェーダーを取得する + * Get Y-flip color transform fragment shader + * + * @return {string} + */ static getYFlipColorTransformFragmentShader (): string { return YFlipColorTransformFragment; } + /** + * @description カラーマトリクスフィルターフラグメントシェーダーを取得する + * Get color matrix filter fragment shader + * + * @return {string} + */ static getColorMatrixFilterFragmentShader (): string { return ColorMatrixFilterFragment; } + /** + * @description グローフィルターフラグメントシェーダーを取得する + * Get glow filter fragment shader + * + * @return {string} + */ static getGlowFilterFragmentShader (): string { return GlowFilterFragment; } + /** + * @description ドロップシャドウフィルターフラグメントシェーダーを取得する + * Get drop shadow filter fragment shader + * + * @return {string} + */ static getDropShadowFilterFragmentShader (): string { return DropShadowFilterFragment; } + /** + * @description グラデーショングローフィルターフラグメントシェーダーを取得する + * Get gradient glow filter fragment shader + * + * @return {string} + */ static getGradientGlowFilterFragmentShader (): string { return GradientGlowFilterFragment; } + /** + * @description グラデーションベベルフィルターフラグメントシェーダーを取得する + * Get gradient bevel filter fragment shader + * + * @return {string} + */ static getGradientBevelFilterFragmentShader (): string { return GradientBevelFilterFragment; } + /** + * @description ベベルフィルターフラグメントシェーダーを取得する + * Get bevel filter fragment shader + * + * @return {string} + */ static getBevelFilterFragmentShader (): string { return BevelFilterFragment; } + /** + * @description ベベルフィルターベース処理フラグメントシェーダーを取得する + * Get bevel filter base fragment shader + * + * @return {string} + */ static getBevelBaseFragmentShader (): string { return BevelBaseFragment; } + /** + * @description コンボリューション(畳み込み)フィルターフラグメントシェーダーを生成する + * Generate convolution filter fragment shader + * + * @param {number} matrix_x - コンボリューション行列のX次元サイズ + * @param {number} matrix_y - コンボリューション行列のY次元サイズ + * @param {boolean} [preserve_alpha=true] - 元のアルファ値を保持するかどうか + * @param {boolean} [clamp=true] - UV座標を範囲内にクランプするかどうか + * @return {string} + */ static getConvolutionFilterFragmentShader ( - matrixX: number, - matrixY: number, - preserveAlpha: boolean = true, + matrix_x: number, + matrix_y: number, + preserve_alpha: boolean = true, clamp: boolean = true ): string { - const halfX = Math.floor(matrixX * 0.5); - const halfY = Math.floor(matrixY * 0.5); - const size = matrixX * matrixY; + const halfX = Math.floor(matrix_x * 0.5); + const halfY = Math.floor(matrix_y * 0.5); + const size = matrix_x * matrix_y; let matrixStatement = ""; for (let idx = 0; idx < size; idx++) { @@ -265,7 +496,7 @@ fn main(input: VertexOutput) -> @location(0) vec4 { result = result + getWeightedColor(${idx}, getMatrixWeight(${idx}));`; } - const preserveAlphaStatement = preserveAlpha + const preserveAlphaStatement = preserve_alpha ? "result.a = textureSample(sourceTexture, sourceSampler, input.texCoord).a;" : ""; @@ -304,8 +535,8 @@ fn getMatrixWeight(index: i32) -> f32 { fn getWeightedColor(i: i32, weight: f32) -> vec4 { let rcpSize = uniforms.rcpSize; - let iDivX = i / ${matrixX}; - let iModX = i - ${matrixX} * iDivX; + let iDivX = i / ${matrix_x}; + let iModX = i - ${matrix_x} * iDivX; let offset = vec2(f32(iModX - ${halfX}), f32(${halfY} - iDivX)); var uv = input.texCoord + offset * rcpSize; var color = textureSample(sourceTexture, sourceSampler, uv); @@ -355,14 +586,27 @@ fn fs_main(fragInput: VertexOutput) -> @location(0) vec4 { `; } + /** + * @description 複合ブレンドフラグメントシェーダーを取得する + * Get complex blend fragment shader + * + * @return {string} + */ static getComplexBlendFragmentShader (): string { return ShaderSource.getUnifiedComplexBlendFragmentShader(); } - static getBlendModeIndex (blendMode: string): number + /** + * @description ブレンドモード名からインデックスを取得する + * Get blend mode index from blend mode name + * + * @param {string} blend_mode - ブレンドモード名 + * @return {number} + */ + static getBlendModeIndex (blend_mode: string): number { - switch (blendMode) { + switch (blend_mode) { case "subtract": return 0; case "multiply": return 1; case "lighten": return 2; @@ -375,6 +619,12 @@ fn fs_main(fragInput: VertexOutput) -> @location(0) vec4 { } } + /** + * @description 統合複合ブレンドフラグメントシェーダーを取得する + * Get unified complex blend fragment shader + * + * @return {string} + */ static getUnifiedComplexBlendFragmentShader (): string { return /* wgsl */` @@ -468,16 +718,25 @@ fn main(input: VertexOutput) -> @location(0) vec4 { `; } + /** + * @description ディスプレースメントマップフィルターフラグメントシェーダーを生成する + * Generate displacement map filter fragment shader + * + * @param {number} component_x - X方向の色コンポーネント (1:R, 2:G, 4:B, 8:A) + * @param {number} component_y - Y方向の色コンポーネント (1:R, 2:G, 4:B, 8:A) + * @param {number} mode - マッピングモード (0:wrap, 1:color, 2:repeat, 3:clamp) + * @return {string} + */ static getDisplacementMapFilterFragmentShader ( - componentX: number, - componentY: number, + component_x: number, + component_y: number, mode: number ): string { let cx: string; let cy: string; - switch (componentX) { + switch (component_x) { case 1: cx = "mapColor.r"; break; @@ -495,7 +754,7 @@ fn main(input: VertexOutput) -> @location(0) vec4 { break; } - switch (componentY) { + switch (component_y) { case 1: cy = "mapColor.r"; break; @@ -587,56 +846,122 @@ fn main(input: VertexOutput) -> @location(0) vec4 { `; } + /** + * @description ノードクリア用頂点シェーダーを取得する + * Get node clear vertex shader + * + * @return {string} + */ static getNodeClearVertexShader (): string { return NodeClearVertex; } + /** + * @description ノードクリア用フラグメントシェーダーを取得する + * Get node clear fragment shader + * + * @return {string} + */ static getNodeClearFragmentShader (): string { return NodeClearFragment; } + /** + * @description 位置指定テクスチャ用頂点シェーダーを取得する + * Get positioned texture vertex shader + * + * @return {string} + */ static getPositionedTextureVertexShader (): string { return PositionedTextureVertex; } + /** + * @description テクスチャスケール用頂点シェーダーを取得する + * Get texture scale vertex shader + * + * @return {string} + */ static getTextureScaleVertexShader (): string { return TextureScaleVertex; } + /** + * @description テクスチャスケールブレンド用頂点シェーダーを取得する + * Get texture scale blend vertex shader + * + * @return {string} + */ static getTextureScaleBlendVertexShader (): string { return TextureScaleBlendVertex; } + /** + * @description 複合ブレンドスケール用頂点シェーダーを取得する + * Get complex blend scale vertex shader + * + * @return {string} + */ static getComplexBlendScaleVertexShader (): string { return ComplexBlendScaleVertex; } + /** + * @description 複合ブレンド用頂点シェーダーを取得する + * Get complex blend vertex shader + * + * @return {string} + */ static getComplexBlendVertexShader (): string { return ComplexBlendVertex; } + /** + * @description 複合ブレンドコピー用頂点シェーダーを取得する + * Get complex blend copy vertex shader + * + * @return {string} + */ static getComplexBlendCopyVertexShader (): string { return ComplexBlendCopyVertex; } + /** + * @description 複合ブレンド出力用頂点シェーダーを取得する + * Get complex blend output vertex shader + * + * @return {string} + */ static getComplexBlendOutputVertexShader (): string { return ComplexBlendOutputVertex; } + /** + * @description フィルター複合ブレンド出力用頂点シェーダーを取得する + * Get filter complex blend output vertex shader + * + * @return {string} + */ static getFilterComplexBlendOutputVertexShader (): string { return FilterComplexBlendOutputVertex; } + /** + * @description 位置指定テクスチャ用フラグメントシェーダーを取得する + * Get positioned texture fragment shader + * + * @return {string} + */ static getPositionedTextureFragmentShader (): string { return PositionedTextureFragment; diff --git a/packages/webgpu/src/Shader/wgsl/common/SharedWgsl.ts b/packages/webgpu/src/Shader/wgsl/common/SharedWgsl.ts index 06380866..46c090d1 100644 --- a/packages/webgpu/src/Shader/wgsl/common/SharedWgsl.ts +++ b/packages/webgpu/src/Shader/wgsl/common/SharedWgsl.ts @@ -1,9 +1,23 @@ +/** + * @description UV座標が0〜1の範囲内にあるかを判定するWGSLヘルパー関数 + * WGSL helper function that checks if UV coordinates are within the 0-1 range + * + * @type {string} + * @constant + */ export const WgslIsInside = ` fn isInside(uv: vec2) -> f32 { let s = step(vec2(0.0), uv) * step(uv, vec2(1.0)); return s.x * s.y; }`; +/** + * @description フルスクリーン四角形の頂点座標定義(NDC空間) + * Full-screen quad vertex positions definition in NDC space + * + * @type {string} + * @constant + */ export const WgslFullscreenPositions = ` const positions = array, 6>( vec2(-1.0, -1.0), @@ -14,6 +28,13 @@ export const WgslFullscreenPositions = ` vec2( 1.0, 1.0) );`; +/** + * @description 単位四角形の頂点座標定義(0〜1空間) + * Unit quad vertex positions definition in 0-1 space + * + * @type {string} + * @constant + */ export const WgslUnitQuadVertices = ` const vertices = array, 6>( vec2(0.0, 0.0), @@ -24,6 +45,13 @@ export const WgslUnitQuadVertices = ` vec2(1.0, 1.0) );`; +/** + * @description 標準的な頂点シェーダー出力構造体のWGSL定義 + * WGSL definition of the standard vertex shader output struct + * + * @type {string} + * @constant + */ export const WgslVertexOutput = ` struct VertexOutput { @builtin(position) position: vec4, diff --git a/packages/webgpu/src/Shader/wgsl/fragment/BasicFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/BasicFragment.ts index 4b2982e0..d1632b23 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/BasicFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/BasicFragment.ts @@ -1,3 +1,10 @@ +/** + * @description 頂点カラーをそのまま出力する基本フラグメントシェーダー + * Basic fragment shader that outputs vertex color directly + * + * @type {string} + * @constant + */ export const BasicFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, @@ -11,6 +18,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description テクスチャサンプリングと頂点カラーを乗算するフラグメントシェーダー + * Fragment shader that multiplies texture sampling with vertex color + * + * @type {string} + * @constant + */ export const TextureFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, diff --git a/packages/webgpu/src/Shader/wgsl/fragment/BitmapFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/BitmapFragment.ts index 82cbc744..a46dc26c 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/BitmapFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/BitmapFragment.ts @@ -1,3 +1,10 @@ +/** + * @description ビットマップ塗りフラグメントシェーダー(ベジェクリッピング・リピート対応) + * Bitmap fill fragment shader with bezier clipping and repeat support + * + * @type {string} + * @constant + */ export const BitmapFillFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, diff --git a/packages/webgpu/src/Shader/wgsl/fragment/EffectFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/EffectFragment.ts index 5c94d6e8..cb4ea66e 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/EffectFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/EffectFragment.ts @@ -1,5 +1,12 @@ import { WgslIsInside, WgslVertexOutput } from "../common/SharedWgsl"; +/** + * @description グローフィルター用フラグメントシェーダー(内側・外側・ノックアウト対応) + * Glow filter fragment shader with inner, outer, and knockout support + * + * @type {string} + * @constant + */ export const GlowFilterFragment = /* wgsl */` ${WgslVertexOutput} @@ -55,6 +62,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description ドロップシャドウフィルター用フラグメントシェーダー(内側・外側・ノックアウト・非表示対応) + * Drop shadow filter fragment shader with inner, outer, knockout, and hide-object support + * + * @type {string} + * @constant + */ export const DropShadowFilterFragment = /* wgsl */` ${WgslVertexOutput} @@ -114,6 +128,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description グラデーショングローフィルター用フラグメントシェーダー(LUTベース) + * Gradient glow filter fragment shader using LUT + * + * @type {string} + * @constant + */ export const GradientGlowFilterFragment = /* wgsl */` ${WgslVertexOutput} @@ -172,6 +193,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description グラデーションベベルフィルター用フラグメントシェーダー(LUTベース) + * Gradient bevel filter fragment shader using LUT + * + * @type {string} + * @constant + */ export const GradientBevelFilterFragment = /* wgsl */` ${WgslVertexOutput} @@ -240,6 +268,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description ベベルフィルター用フラグメントシェーダー(ハイライト・シャドウカラー指定) + * Bevel filter fragment shader with highlight and shadow color specification + * + * @type {string} + * @constant + */ export const BevelFilterFragment = /* wgsl */` ${WgslVertexOutput} @@ -307,6 +342,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description ベベルフィルターのベース処理用フラグメントシェーダー(オフセット差分計算) + * Bevel filter base fragment shader for offset difference calculation + * + * @type {string} + * @constant + */ export const BevelBaseFragment = /* wgsl */` ${WgslVertexOutput} diff --git a/packages/webgpu/src/Shader/wgsl/fragment/FillFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/FillFragment.ts index d10cd208..a6342582 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/FillFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/FillFragment.ts @@ -1,3 +1,10 @@ +/** + * @description ベジェ曲線ベースのアンチエイリアス付き塗りフラグメントシェーダー + * Bezier curve-based fill fragment shader with anti-aliasing + * + * @type {string} + * @constant + */ export const FillFragment = /* wgsl */` struct FragmentInput { @builtin(position) position: vec4, diff --git a/packages/webgpu/src/Shader/wgsl/fragment/FilterFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/FilterFragment.ts index a74d025b..89c2974e 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/FilterFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/FilterFragment.ts @@ -1,5 +1,12 @@ import { WgslVertexOutput } from "../common/SharedWgsl"; +/** + * @description テクスチャコピー用フラグメントシェーダー(スケール・オフセット付き) + * Texture copy fragment shader with scale and offset + * + * @type {string} + * @constant + */ export const TextureCopyFragment = /* wgsl */` ${WgslVertexOutput} @@ -19,6 +26,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description ブラー用テクスチャコピーフラグメントシェーダー(境界クランプ付き) + * Blur texture copy fragment shader with boundary clamping + * + * @type {string} + * @constant + */ export const BlurTextureCopyFragment = /* wgsl */` ${WgslVertexOutput} @@ -41,6 +55,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description フィルター出力用フラグメントシェーダー(境界チェック付きコピー) + * Filter output fragment shader with boundary-checked copy + * + * @type {string} + * @constant + */ export const FilterOutputFragment = /* wgsl */` ${WgslVertexOutput} @@ -63,6 +84,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description カラー変換フラグメントシェーダー(乗算・加算カラー適用) + * Color transform fragment shader with multiply and add color application + * + * @type {string} + * @constant + */ export const ColorTransformFragment = /* wgsl */` ${WgslVertexOutput} @@ -87,6 +115,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description Y軸反転付きカラー変換フラグメントシェーダー + * Y-flip color transform fragment shader + * + * @type {string} + * @constant + */ export const YFlipColorTransformFragment = /* wgsl */` ${WgslVertexOutput} @@ -114,6 +149,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description カラーマトリクスフィルター用フラグメントシェーダー + * Color matrix filter fragment shader + * + * @type {string} + * @constant + */ export const ColorMatrixFilterFragment = /* wgsl */` ${WgslVertexOutput} @@ -139,6 +181,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description ノードクリア用フラグメントシェーダー(透明色出力) + * Node clear fragment shader that outputs transparent color + * + * @type {string} + * @constant + */ export const NodeClearFragment = /* wgsl */` @fragment fn main() -> @location(0) vec4 { @@ -146,6 +195,13 @@ fn main() -> @location(0) vec4 { } `; +/** + * @description 位置指定テクスチャサンプリング用フラグメントシェーダー + * Positioned texture sampling fragment shader + * + * @type {string} + * @constant + */ export const PositionedTextureFragment = /* wgsl */` ${WgslVertexOutput} @@ -158,6 +214,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description ビットマップ同期用フラグメントシェーダー(テクスチャ直接サンプリング) + * Bitmap sync fragment shader with direct texture sampling + * + * @type {string} + * @constant + */ export const BitmapSyncFragment = /* wgsl */` ${WgslVertexOutput} @@ -170,6 +233,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description 汎用ブレンドフラグメントシェーダー(Normal/Multiply/Screen/Add) + * Generic blend fragment shader supporting Normal, Multiply, Screen, and Add modes + * + * @type {string} + * @constant + */ export const BlendGenericFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, diff --git a/packages/webgpu/src/Shader/wgsl/fragment/GradientFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/GradientFragment.ts index 53742de4..dfa1bc35 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/GradientFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/GradientFragment.ts @@ -1,4 +1,11 @@ -const GradientUniformsAndSpread = ` +/** + * @description グラデーションのUniform定義とスプレッドモード処理のWGSLコード + * WGSL code for gradient uniform definitions and spread mode handling + * + * @type {string} + * @constant + */ +const $GradientUniformsAndSpread = ` struct GradientUniforms { inverseMatrix: mat3x3, gradientType: f32, @@ -26,7 +33,14 @@ fn applySpread(t: f32) -> f32 { } `; -const GradientCalculation = ` +/** + * @description 線形・放射グラデーションのt値計算WGSLコード + * WGSL code for calculating t value in linear and radial gradients + * + * @type {string} + * @constant + */ +const $GradientCalculation = ` var t: f32; if (GRADIENT_TYPE == 0u) { let a = gradient.linearPoints.xy; @@ -69,6 +83,13 @@ const GradientCalculation = ` let gradientColor = textureSampleLevel(gradientTexture, gradientSampler, vec2(t, 0.5), 0); `; +/** + * @description グラデーション塗りフラグメントシェーダー(頂点カラー乗算付き) + * Gradient fill fragment shader with vertex color multiplication + * + * @type {string} + * @constant + */ export const GradientFillFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, @@ -76,17 +97,24 @@ struct VertexOutput { @location(1) bezier: vec2, @location(2) color: vec4, } -${GradientUniformsAndSpread} +${$GradientUniformsAndSpread} @fragment fn main(input: VertexOutput) -> @location(0) vec4 { let p = input.v_uv; -${GradientCalculation} +${$GradientCalculation} let result = gradientColor * input.color; return vec4(result.rgb * result.a, result.a); } `; +/** + * @description ステンシル用グラデーション塗りフラグメントシェーダー + * Gradient fill fragment shader for stencil rendering + * + * @type {string} + * @constant + */ export const GradientFillStencilFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, @@ -94,28 +122,35 @@ struct VertexOutput { @location(1) bezier: vec2, @location(2) color: vec4, } -${GradientUniformsAndSpread} +${$GradientUniformsAndSpread} @fragment fn main(input: VertexOutput) -> @location(0) vec4 { let p = input.v_uv; -${GradientCalculation} +${$GradientCalculation} return vec4(gradientColor.rgb * gradientColor.a, gradientColor.a); } `; +/** + * @description テクスチャ座標ベースのグラデーションフラグメントシェーダー + * Texture coordinate-based gradient fragment shader + * + * @type {string} + * @constant + */ export const GradientFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, @location(0) texCoord: vec2, @location(1) color: vec4, } -${GradientUniformsAndSpread} +${$GradientUniformsAndSpread} @fragment fn main(input: VertexOutput) -> @location(0) vec4 { let p = input.texCoord; -${GradientCalculation} +${$GradientCalculation} let result = gradientColor * input.color; return vec4(result.rgb * result.a, result.a); } diff --git a/packages/webgpu/src/Shader/wgsl/fragment/InstancedFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/InstancedFragment.ts index c78d99fc..2991d147 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/InstancedFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/InstancedFragment.ts @@ -1,3 +1,10 @@ +/** + * @description インスタンス描画用フラグメントシェーダー(カラー変換付き) + * Instanced rendering fragment shader with color transform + * + * @type {string} + * @constant + */ export const InstancedFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, diff --git a/packages/webgpu/src/Shader/wgsl/fragment/MaskFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/MaskFragment.ts index 00a04701..75f479e3 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/MaskFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/MaskFragment.ts @@ -1,3 +1,10 @@ +/** + * @description ベジェ曲線ベースのマスク用フラグメントシェーダー + * Bezier curve-based mask fragment shader + * + * @type {string} + * @constant + */ export const MaskFragment = /* wgsl */` struct FragmentInput { @location(0) bezier: vec2, diff --git a/packages/webgpu/src/Shader/wgsl/fragment/StencilFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/StencilFragment.ts index 3547cd01..8820e4a0 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/StencilFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/StencilFragment.ts @@ -1,3 +1,10 @@ +/** + * @description ステンシル書き込み用フラグメントシェーダー(ベジェ曲線アンチエイリアス) + * Stencil write fragment shader with bezier curve anti-aliasing + * + * @type {string} + * @constant + */ export const StencilWriteFragment = /* wgsl */` struct FragmentInput { @builtin(position) position: vec4, @@ -20,6 +27,13 @@ fn main(input: FragmentInput) -> @location(0) vec4 { } `; +/** + * @description ステンシル塗り用フラグメントシェーダー(プリマルチプライドアルファ出力) + * Stencil fill fragment shader with premultiplied alpha output + * + * @type {string} + * @constant + */ export const StencilFillFragment = /* wgsl */` struct FragmentInput { @builtin(position) position: vec4, diff --git a/packages/webgpu/src/Shader/wgsl/vertex/BasicVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/BasicVertex.ts index e2dcdd91..7dce4ecc 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/BasicVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/BasicVertex.ts @@ -1,3 +1,10 @@ +/** + * @description 基本頂点シェーダー(行列変換・プリマルチプライドカラー出力) + * Basic vertex shader with matrix transform and premultiplied color output + * + * @type {string} + * @constant + */ export const BasicVertex = /* wgsl */` override yFlipSign: f32 = 1.0; diff --git a/packages/webgpu/src/Shader/wgsl/vertex/BitmapVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/BitmapVertex.ts index e90d921e..94235007 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/BitmapVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/BitmapVertex.ts @@ -1,3 +1,10 @@ +/** + * @description ビットマップ塗り用頂点シェーダー(ワールド座標出力付き) + * Bitmap fill vertex shader with world position output + * + * @type {string} + * @constant + */ export const BitmapFillVertex = /* wgsl */` override yFlipSign: f32 = 1.0; diff --git a/packages/webgpu/src/Shader/wgsl/vertex/FillVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/FillVertex.ts index a24f8660..90237ace 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/FillVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/FillVertex.ts @@ -1,3 +1,10 @@ +/** + * @description 塗り用頂点シェーダー(ベジェ曲線パラメータ付き行列変換) + * Fill vertex shader with matrix transform and bezier parameters + * + * @type {string} + * @constant + */ export const FillVertex = /* wgsl */` override yFlipSign: f32 = 1.0; diff --git a/packages/webgpu/src/Shader/wgsl/vertex/FilterVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/FilterVertex.ts index 2d8338f0..504e3f96 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/FilterVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/FilterVertex.ts @@ -1,6 +1,13 @@ import { WgslFullscreenPositions, WgslUnitQuadVertices, WgslVertexOutput } from "../common/SharedWgsl"; -const createFullscreenQuadVertex = (yFlipTexCoord: boolean): string => /* wgsl */` +/** + * @description フルスクリーン四角形の頂点シェーダーを生成する + * Generate a full-screen quad vertex shader + * + * @param {boolean} y_flip_tex_coord - テクスチャY座標を反転するかどうか + * @return {string} + */ +const $createFullscreenQuadVertex = (y_flip_tex_coord: boolean): string => /* wgsl */` ${WgslVertexOutput} @vertex @@ -8,12 +15,12 @@ fn main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { var output: VertexOutput; ${WgslFullscreenPositions} var texCoords = array, 6>( - vec2(0.0, ${yFlipTexCoord ? "1.0" : "0.0"}), - vec2(1.0, ${yFlipTexCoord ? "1.0" : "0.0"}), - vec2(0.0, ${yFlipTexCoord ? "0.0" : "1.0"}), - vec2(0.0, ${yFlipTexCoord ? "0.0" : "1.0"}), - vec2(1.0, ${yFlipTexCoord ? "1.0" : "0.0"}), - vec2(1.0, ${yFlipTexCoord ? "0.0" : "1.0"}) + vec2(0.0, ${y_flip_tex_coord ? "1.0" : "0.0"}), + vec2(1.0, ${y_flip_tex_coord ? "1.0" : "0.0"}), + vec2(0.0, ${y_flip_tex_coord ? "0.0" : "1.0"}), + vec2(0.0, ${y_flip_tex_coord ? "0.0" : "1.0"}), + vec2(1.0, ${y_flip_tex_coord ? "1.0" : "0.0"}), + vec2(1.0, ${y_flip_tex_coord ? "0.0" : "1.0"}) ); output.position = vec4(positions[vertexIndex], 0.0, 1.0); output.texCoord = texCoords[vertexIndex]; @@ -21,10 +28,40 @@ ${WgslFullscreenPositions} } `; -export const BlurFilterVertex = createFullscreenQuadVertex(true); -export const ComplexBlendVertex = createFullscreenQuadVertex(false); -export const ComplexBlendCopyVertex = createFullscreenQuadVertex(false); +/** + * @description ブラーフィルター用フルスクリーン頂点シェーダー(Y軸テクスチャ反転) + * Full-screen vertex shader for blur filter with Y-axis texture flip + * + * @type {string} + * @constant + */ +export const BlurFilterVertex = $createFullscreenQuadVertex(true); +/** + * @description 複合ブレンド用フルスクリーン頂点シェーダー + * Full-screen vertex shader for complex blend + * + * @type {string} + * @constant + */ +export const ComplexBlendVertex = $createFullscreenQuadVertex(false); + +/** + * @description 複合ブレンドコピー用フルスクリーン頂点シェーダー + * Full-screen vertex shader for complex blend copy + * + * @type {string} + * @constant + */ +export const ComplexBlendCopyVertex = $createFullscreenQuadVertex(false); + +/** + * @description ノードクリア用頂点シェーダー(NDC変換のみ) + * Node clear vertex shader with NDC transform only + * + * @type {string} + * @constant + */ export const NodeClearVertex = /* wgsl */` struct VertexInput { @location(0) position: vec2, @@ -43,6 +80,13 @@ fn main(input: VertexInput) -> VertexOutput { } `; +/** + * @description 位置指定テクスチャ描画用頂点シェーダー(ビューポート変換付き) + * Positioned texture rendering vertex shader with viewport transform + * + * @type {string} + * @constant + */ export const PositionedTextureVertex = /* wgsl */` struct PositionUniforms { offset: vec2, @@ -72,6 +116,13 @@ ${WgslUnitQuadVertices} } `; +/** + * @description ビットマップ同期用頂点シェーダー(ノード矩形ベース配置) + * Bitmap sync vertex shader with node rectangle-based positioning + * + * @type {string} + * @constant + */ export const BitmapSyncVertex = /* wgsl */` struct BitmapSyncUniforms { nodeRect: vec4, @@ -102,7 +153,14 @@ ${WgslUnitQuadVertices} } `; -const ScaleUniformsAndStruct = ` +/** + * @description スケール変換用Uniform定義と頂点出力構造体のWGSLコード + * WGSL code for scale transform uniform definitions and vertex output struct + * + * @type {string} + * @constant + */ +const $ScaleUniformsAndStruct = ` struct ScaleUniforms { matrix: vec4, translate: vec2, @@ -119,7 +177,14 @@ struct VertexOutput { @group(0) @binding(0) var uniforms: ScaleUniforms; `; -const ScaleTransformBody = ` +/** + * @description スケール変換の頂点位置計算処理のWGSLコード + * WGSL code for scale transform vertex position calculation + * + * @type {string} + * @constant + */ +const $ScaleTransformBody = ` var pos = vertex * uniforms.srcSize; let a = uniforms.matrix.x; let b = uniforms.matrix.y; @@ -134,25 +199,62 @@ const ScaleTransformBody = ` output.position = vec4(position.x, -position.y, 0.0, 1.0); `; -const createScaleVertex = (yFlipTexCoord: boolean): string => /* wgsl */` -${ScaleUniformsAndStruct} +/** + * @description スケール変換用頂点シェーダーを生成する + * Generate a scale transform vertex shader + * + * @param {boolean} y_flip_tex_coord - テクスチャY座標を反転するかどうか + * @return {string} + */ +const $createScaleVertex = (y_flip_tex_coord: boolean): string => /* wgsl */` +${$ScaleUniformsAndStruct} @vertex fn main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { var output: VertexOutput; ${WgslUnitQuadVertices} let vertex = vertices[vertexIndex]; - output.texCoord = ${yFlipTexCoord ? "vec2(vertex.x, 1.0 - vertex.y)" : "vertex"}; -${ScaleTransformBody} + output.texCoord = ${y_flip_tex_coord ? "vec2(vertex.x, 1.0 - vertex.y)" : "vertex"}; +${$ScaleTransformBody} return output; } `; -export const TextureScaleVertex = createScaleVertex(false); -export const TextureScaleBlendVertex = createScaleVertex(true); -export const ComplexBlendScaleVertex = createScaleVertex(false); +/** + * @description テクスチャスケール用頂点シェーダー + * Texture scale vertex shader + * + * @type {string} + * @constant + */ +export const TextureScaleVertex = $createScaleVertex(false); + +/** + * @description ブレンド用テクスチャスケール頂点シェーダー(Y軸反転) + * Texture scale vertex shader for blend with Y-axis flip + * + * @type {string} + * @constant + */ +export const TextureScaleBlendVertex = $createScaleVertex(true); -const createOutputVertex = (yFlipTexCoord: boolean): string => /* wgsl */` +/** + * @description 複合ブレンドスケール用頂点シェーダー + * Complex blend scale vertex shader + * + * @type {string} + * @constant + */ +export const ComplexBlendScaleVertex = $createScaleVertex(false); + +/** + * @description 出力用頂点シェーダーを生成する(位置指定・ビューポート変換) + * Generate an output vertex shader with positioning and viewport transform + * + * @param {boolean} y_flip_tex_coord - テクスチャY座標を反転するかどうか + * @return {string} + */ +const $createOutputVertex = (y_flip_tex_coord: boolean): string => /* wgsl */` struct PositionUniforms { offset: vec2, size: vec2, @@ -172,7 +274,7 @@ fn main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { var output: VertexOutput; ${WgslUnitQuadVertices} let vertex = vertices[vertexIndex]; - output.texCoord = ${yFlipTexCoord ? "vec2(vertex.x, 1.0 - vertex.y)" : "vertex"}; + output.texCoord = ${y_flip_tex_coord ? "vec2(vertex.x, 1.0 - vertex.y)" : "vertex"}; var position = vertex * uniforms.size + uniforms.offset; position = position / uniforms.viewport; position = position * 2.0 - 1.0; @@ -181,5 +283,20 @@ ${WgslUnitQuadVertices} } `; -export const ComplexBlendOutputVertex = createOutputVertex(false); -export const FilterComplexBlendOutputVertex = createOutputVertex(true); +/** + * @description 複合ブレンド出力用頂点シェーダー + * Complex blend output vertex shader + * + * @type {string} + * @constant + */ +export const ComplexBlendOutputVertex = $createOutputVertex(false); + +/** + * @description フィルター複合ブレンド出力用頂点シェーダー(Y軸反転) + * Filter complex blend output vertex shader with Y-axis flip + * + * @type {string} + * @constant + */ +export const FilterComplexBlendOutputVertex = $createOutputVertex(true); diff --git a/packages/webgpu/src/Shader/wgsl/vertex/GradientVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/GradientVertex.ts index 313654d8..86b30f79 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/GradientVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/GradientVertex.ts @@ -1,3 +1,10 @@ +/** + * @description グラデーション塗り用頂点シェーダー(逆行列によるUV座標計算) + * Gradient fill vertex shader with inverse matrix UV coordinate calculation + * + * @type {string} + * @constant + */ export const GradientFillVertex = /* wgsl */` override yFlipSign: f32 = 1.0; diff --git a/packages/webgpu/src/Shader/wgsl/vertex/InstancedVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/InstancedVertex.ts index 86cf39fb..c0a43452 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/InstancedVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/InstancedVertex.ts @@ -1,3 +1,10 @@ +/** + * @description インスタンス描画用頂点シェーダー(インスタンスごとの変換・カラー) + * Instanced rendering vertex shader with per-instance transform and color + * + * @type {string} + * @constant + */ export const InstancedVertex = /* wgsl */` struct VertexInput { @location(0) position: vec2, diff --git a/packages/webgpu/src/Shader/wgsl/vertex/MaskVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/MaskVertex.ts index 21328b8c..a710846a 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/MaskVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/MaskVertex.ts @@ -1,3 +1,10 @@ +/** + * @description マスク用頂点シェーダー(ベジェ曲線パラメータ付き行列変換) + * Mask vertex shader with matrix transform and bezier parameters + * + * @type {string} + * @constant + */ export const MaskVertex = /* wgsl */` struct VertexInput { @location(0) position: vec2, diff --git a/packages/webgpu/src/Shader/wgsl/vertex/StencilVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/StencilVertex.ts index 729e8055..b1e2aee9 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/StencilVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/StencilVertex.ts @@ -1,3 +1,10 @@ +/** + * @description ステンシル書き込み用頂点シェーダー(ベジェ曲線パラメータ付き) + * Stencil write vertex shader with bezier parameters + * + * @type {string} + * @constant + */ export const StencilWriteVertex = /* wgsl */` override yFlipSign: f32 = 1.0; @@ -32,6 +39,13 @@ fn main(input: VertexInput) -> VertexOutput { } `; +/** + * @description ステンシル塗り用頂点シェーダー(カラー出力付き) + * Stencil fill vertex shader with color output + * + * @type {string} + * @constant + */ export const StencilFillVertex = /* wgsl */` override yFlipSign: f32 = 1.0; diff --git a/packages/webgpu/src/TextureManager.ts b/packages/webgpu/src/TextureManager.ts index ac588f53..9069d6b3 100644 --- a/packages/webgpu/src/TextureManager.ts +++ b/packages/webgpu/src/TextureManager.ts @@ -2,12 +2,21 @@ import { execute as textureManagerInitializeSamplersService } from "./TextureMan import { execute as textureManagerCreateTextureFromPixelsUseCase } from "./TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase"; import { execute as textureManagerCreateTextureFromImageBitmapUseCase } from "./TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase"; +/** + * @description テクスチャとサンプラーの管理クラス + * Manager class for textures and samplers + */ export class TextureManager { private device: GPUDevice; private textures: Map; private samplers: Map; + /** + * @description TextureManagerのコンストラクタ + * Constructor for TextureManager + * @param {GPUDevice} device - WebGPUデバイス / WebGPU device + */ constructor (device: GPUDevice) { this.device = device; @@ -17,6 +26,15 @@ export class TextureManager textureManagerInitializeSamplersService(device, this.samplers); } + /** + * @description 新しいGPUテクスチャを作成する + * Create a new GPU texture + * @param {string} name - テクスチャ名 / Texture name + * @param {number} width - テクスチャの幅 / Texture width + * @param {number} height - テクスチャの高さ / Texture height + * @param {GPUTextureFormat} [format="rgba8unorm"] - テクスチャフォーマット / Texture format + * @return {GPUTexture} + */ createTexture ( name: string, width: number, @@ -35,6 +53,15 @@ export class TextureManager return texture; } + /** + * @description ピクセルデータからテクスチャを作成する + * Create a texture from pixel data + * @param {string} name - テクスチャ名 / Texture name + * @param {Uint8Array} pixels - ピクセルデータ / Pixel data + * @param {number} width - テクスチャの幅 / Texture width + * @param {number} height - テクスチャの高さ / Texture height + * @return {GPUTexture} + */ createTextureFromPixels ( name: string, pixels: Uint8Array, @@ -51,16 +78,32 @@ export class TextureManager ); } - createTextureFromImageBitmap (name: string, imageBitmap: ImageBitmap): GPUTexture + /** + * @description ImageBitmapからテクスチャを作成する + * Create a texture from an ImageBitmap + * @param {string} name - テクスチャ名 / Texture name + * @param {ImageBitmap} image_bitmap - 画像ビットマップ / Image bitmap source + * @return {GPUTexture} + */ + createTextureFromImageBitmap (name: string, image_bitmap: ImageBitmap): GPUTexture { return textureManagerCreateTextureFromImageBitmapUseCase( this.device, this.textures, name, - imageBitmap + image_bitmap ); } + /** + * @description 既存テクスチャのピクセルデータを更新する + * Update pixel data of an existing texture + * @param {string} name - テクスチャ名 / Texture name + * @param {Uint8Array} pixels - ピクセルデータ / Pixel data + * @param {number} width - テクスチャの幅 / Texture width + * @param {number} height - テクスチャの高さ / Texture height + * @return {void} + */ updateTexture ( name: string, pixels: Uint8Array, @@ -78,16 +121,35 @@ export class TextureManager } } + /** + * @description 名前でテクスチャを取得する + * Get a texture by name + * @param {string} name - テクスチャ名 / Texture name + * @return {GPUTexture | undefined} + */ getTexture (name: string): GPUTexture | undefined { return this.textures.get(name); } + /** + * @description 名前でサンプラーを取得する + * Get a sampler by name + * @param {string} name - サンプラー名 / Sampler name + * @return {GPUSampler | undefined} + */ getSampler (name: string): GPUSampler | undefined { return this.samplers.get(name); } + /** + * @description サンプラーを作成する(既存の場合は返却) + * Create a sampler (returns existing if found) + * @param {string} name - サンプラー名 / Sampler name + * @param {boolean} [smooth=true] - スムージングを有効にするか / Whether to enable smoothing + * @return {GPUSampler} + */ createSampler (name: string, smooth: boolean = true): GPUSampler { const existing = this.samplers.get(name); @@ -107,6 +169,12 @@ export class TextureManager return sampler; } + /** + * @description テクスチャを破棄する + * Destroy a texture by name + * @param {string} name - テクスチャ名 / Texture name + * @return {void} + */ destroyTexture (name: string): void { const texture = this.textures.get(name); @@ -116,6 +184,11 @@ export class TextureManager } } + /** + * @description 全テクスチャとサンプラーを破棄する + * Dispose all textures and samplers + * @return {void} + */ dispose (): void { for (const texture of this.textures.values()) { diff --git a/packages/webgpu/src/TextureManager/service/TextureManagerInitializeSamplersService.ts b/packages/webgpu/src/TextureManager/service/TextureManagerInitializeSamplersService.ts index 2f62e0ac..3510bfa3 100644 --- a/packages/webgpu/src/TextureManager/service/TextureManagerInitializeSamplersService.ts +++ b/packages/webgpu/src/TextureManager/service/TextureManagerInitializeSamplersService.ts @@ -2,8 +2,8 @@ * @description サンプラーを初期化 * Initialize samplers * - * @param {GPUDevice} device - * @param {Map} samplers + * @param {GPUDevice} device - GPUデバイス + * @param {Map} samplers - サンプラー管理マップ * @return {void} * @method * @protected diff --git a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.ts b/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.ts index 9fe1f821..a34c5eec 100644 --- a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.ts +++ b/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.ts @@ -2,10 +2,10 @@ * @description ImageBitmapからテクスチャを作成 * Create texture from ImageBitmap * - * @param {GPUDevice} device - * @param {Map} textures - * @param {string} name - * @param {ImageBitmap} image_bitmap + * @param {GPUDevice} device - GPUデバイス + * @param {Map} textures - テクスチャ管理マップ + * @param {string} name - テクスチャ名 + * @param {ImageBitmap} image_bitmap - 画像ビットマップ * @return {GPUTexture} * @method * @protected diff --git a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.ts b/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.ts index 9cf07105..7c29084a 100644 --- a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.ts +++ b/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.ts @@ -2,12 +2,12 @@ * @description ピクセルデータからテクスチャを作成 * Create texture from pixel data * - * @param {GPUDevice} device - * @param {Map} textures - * @param {string} name - * @param {Uint8Array} pixels - * @param {number} width - * @param {number} height + * @param {GPUDevice} device - GPUデバイス + * @param {Map} textures - テクスチャ管理マップ + * @param {string} name - テクスチャ名 + * @param {Uint8Array} pixels - ピクセルデータ + * @param {number} width - テクスチャ幅 + * @param {number} height - テクスチャ高さ * @return {GPUTexture} * @method * @protected diff --git a/packages/webgpu/src/TexturePool.test.ts b/packages/webgpu/src/TexturePool.test.ts index e3dfc9e7..8b78145c 100644 --- a/packages/webgpu/src/TexturePool.test.ts +++ b/packages/webgpu/src/TexturePool.test.ts @@ -1,9 +1,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { - TexturePool, - initTexturePool, - getTexturePool, - clearTexturePool + TexturePool } from "./TexturePool"; // Mock GPUTextureUsage @@ -105,7 +102,6 @@ describe("TexturePool", () => beforeEach(() => { vi.clearAllMocks(); - clearTexturePool(); }); describe("TexturePool class", () => @@ -306,50 +302,4 @@ describe("TexturePool", () => }); }); }); - - describe("global functions", () => - { - describe("initTexturePool", () => - { - it("should initialize global pool", () => - { - const device = createMockDevice(); - - initTexturePool(device); - - expect(getTexturePool()).not.toBeNull(); - }); - }); - - describe("getTexturePool", () => - { - it("should return pool after initialization", () => - { - const device = createMockDevice(); - initTexturePool(device); - - expect(getTexturePool()).toBeInstanceOf(TexturePool); - }); - }); - - describe("clearTexturePool", () => - { - it("should dispose pool", () => - { - const device = createMockDevice(); - initTexturePool(device); - const pool = getTexturePool(); - const tex = pool!.acquire(128, 128); - - clearTexturePool(); - - expect(tex.destroy).toHaveBeenCalled(); - }); - - it("should not throw when pool is null", () => - { - expect(() => clearTexturePool()).not.toThrow(); - }); - }); - }); }); diff --git a/packages/webgpu/src/TexturePool.ts b/packages/webgpu/src/TexturePool.ts index 65891cbf..4aa3e7e7 100644 --- a/packages/webgpu/src/TexturePool.ts +++ b/packages/webgpu/src/TexturePool.ts @@ -5,13 +5,17 @@ import { execute as texturePoolCleanupService } from "./TexturePool/service/Text /** * @description プールの最大サイズ + * Maximum pool size for texture reuse + * @type {number} */ -const MAX_POOL_SIZE = 32; +const $MAX_POOL_SIZE = 32; /** * @description キャッシュのクリーンアップ閾値(フレーム数) + * Cache cleanup threshold in frames (3 seconds at 60FPS) + * @type {number} */ -const CACHE_CLEANUP_THRESHOLD = 180; // 3秒(60FPS想定) +const $CACHE_CLEANUP_THRESHOLD = 180; /** * @description テクスチャプールマネージャー(Power-of-2バケット版) @@ -23,13 +27,38 @@ const CACHE_CLEANUP_THRESHOLD = 180; // 3秒(60FPS想定) */ export class TexturePool { + /** + * @description WebGPUデバイスの参照 + * Reference to the WebGPU device + * @type {GPUDevice} + */ private device: GPUDevice; + + /** + * @description Power-of-2バケットによるテクスチャプール + * Texture pool organized by power-of-2 buckets + * @type {ITexturePoolBuckets} + */ private buckets: ITexturePoolBuckets; + + /** + * @description 現在のフレーム番号 + * Current frame number for LRU tracking + * @type {number} + */ private currentFrame: number; + + /** + * @description プール内のテクスチャ総数 + * Total count of textures in the pool + * @type {number[]} + */ private totalCount: number[]; /** - * @param {GPUDevice} device + * @description テクスチャプールを生成する + * Create a new texture pool instance + * @param {GPUDevice} device - WebGPUデバイス / WebGPU device * @constructor */ constructor(device: GPUDevice) @@ -41,7 +70,8 @@ export class TexturePool } /** - * @description フレーム開始時に呼び出し + * @description フレーム開始時に呼び出し、定期的にプールをクリーンアップする + * Called at the beginning of each frame; periodically cleans up the pool * @return {void} */ beginFrame(): void @@ -50,16 +80,17 @@ export class TexturePool // 定期的にプールをクリーンアップ(LRU回収) if (this.currentFrame % 60 === 0) { - texturePoolCleanupService(this.buckets, this.currentFrame, CACHE_CLEANUP_THRESHOLD, this.totalCount); + texturePoolCleanupService(this.buckets, this.currentFrame, $CACHE_CLEANUP_THRESHOLD, this.totalCount); } } /** - * @description テクスチャを取得または作成 - * @param {number} width - テクスチャの幅 - * @param {number} height - テクスチャの高さ - * @param {GPUTextureFormat} format - テクスチャフォーマット - * @param {GPUTextureUsageFlags} usage - テクスチャ使用フラグ + * @description テクスチャを取得または作成する + * Acquire a texture from the pool or create a new one + * @param {number} width - テクスチャの幅 / texture width + * @param {number} height - テクスチャの高さ / texture height + * @param {GPUTextureFormat} format - テクスチャフォーマット / texture format + * @param {GPUTextureUsageFlags} usage - テクスチャ使用フラグ / texture usage flags * @return {GPUTexture} */ acquire( @@ -78,14 +109,15 @@ export class TexturePool format, usage, this.currentFrame, - MAX_POOL_SIZE, + $MAX_POOL_SIZE, this.totalCount ); } /** - * @description テクスチャをプールに返却 - * @param {GPUTexture} texture - 返却するテクスチャ + * @description テクスチャをプールに返却する + * Release a texture back to the pool for reuse + * @param {GPUTexture} texture - 返却するテクスチャ / texture to release * @return {void} */ release(texture: GPUTexture): void @@ -94,7 +126,8 @@ export class TexturePool } /** - * @description プール統計を取得 + * @description プール統計を取得する + * Get pool statistics including total, in-use, and available counts * @return {{ total: number, inUse: number, available: number }} */ getStats(): { total: number; inUse: number; available: number } @@ -120,7 +153,8 @@ export class TexturePool } /** - * @description 解放 + * @description 全テクスチャを破棄しプールを解放する + * Destroy all textures and dispose of the pool * @return {void} */ dispose(): void @@ -134,38 +168,3 @@ export class TexturePool this.totalCount[0] = 0; } } - -/** - * @description グローバルテクスチャプールインスタンス - */ -let $texturePool: TexturePool | null = null; - -/** - * @description テクスチャプールを初期化 - * @param {GPUDevice} device - * @return {void} - */ -export const initTexturePool = (device: GPUDevice): void => -{ - $texturePool = new TexturePool(device); -}; - -/** - * @description テクスチャプールを取得 - * @return {TexturePool | null} - */ -export const getTexturePool = (): TexturePool | null => -{ - return $texturePool; -}; - -/** - * @description テクスチャプールをクリア - * @return {void} - */ -export const clearTexturePool = (): void => -{ - if ($texturePool) { - $texturePool.dispose(); - } -}; diff --git a/packages/webgpu/src/TexturePool/service/TexturePoolCleanupService.ts b/packages/webgpu/src/TexturePool/service/TexturePoolCleanupService.ts index cb2ede79..5dd180a8 100644 --- a/packages/webgpu/src/TexturePool/service/TexturePoolCleanupService.ts +++ b/packages/webgpu/src/TexturePool/service/TexturePoolCleanupService.ts @@ -4,21 +4,21 @@ import type { ITexturePoolBuckets } from "../../interface/IPooledTexture"; * @description 古いプールエントリをクリーンアップ(バケットMap版 LRU回収) * Cleanup old pool entries (bucket Map version, LRU eviction) * - * @param {ITexturePoolBuckets} buckets - * @param {number} currentFrame + * @param {ITexturePoolBuckets} buckets - テクスチャプールバケット + * @param {number} current_frame - 現在のフレーム番号 * @param {number} threshold - フレーム数閾値 - * @param {number[]} totalCount - [0]に現在の合計数を格納 + * @param {number[]} total_count - [0]に現在の合計数を格納 * @return {void} * @method * @protected */ export const execute = ( buckets: ITexturePoolBuckets, - currentFrame: number, + current_frame: number, threshold: number, - totalCount: number[] + total_count: number[] ): void => { - const frameThreshold = currentFrame - threshold; + const frameThreshold = current_frame - threshold; for (const [key, bucket] of buckets) { for (let i = bucket.length - 1; i >= 0; i--) { @@ -26,7 +26,7 @@ export const execute = ( if (!entry.inUse && entry.lastUsedFrame < frameThreshold) { entry.texture.destroy(); bucket.splice(i, 1); - totalCount[0]--; + total_count[0]--; } } if (bucket.length === 0) { diff --git a/packages/webgpu/src/TexturePool/service/TexturePoolReleaseService.ts b/packages/webgpu/src/TexturePool/service/TexturePoolReleaseService.ts index 15d796ef..1a0e14e1 100644 --- a/packages/webgpu/src/TexturePool/service/TexturePoolReleaseService.ts +++ b/packages/webgpu/src/TexturePool/service/TexturePoolReleaseService.ts @@ -4,9 +4,9 @@ import type { ITexturePoolBuckets } from "../../interface/IPooledTexture"; * @description テクスチャをプールに返却(バケットMap版) * Release texture back to pool (bucket Map version) * - * @param {ITexturePoolBuckets} buckets - * @param {GPUTexture} texture - * @param {number} currentFrame + * @param {ITexturePoolBuckets} buckets - テクスチャプールバケット + * @param {GPUTexture} texture - 返却するテクスチャ + * @param {number} current_frame - 現在のフレーム番号 * @return {void} * @method * @protected @@ -14,13 +14,13 @@ import type { ITexturePoolBuckets } from "../../interface/IPooledTexture"; export const execute = ( buckets: ITexturePoolBuckets, texture: GPUTexture, - currentFrame: number + current_frame: number ): void => { for (const bucket of buckets.values()) { for (let i = 0; i < bucket.length; i++) { if (bucket[i].texture === texture) { bucket[i].inUse = false; - bucket[i].lastUsedFrame = currentFrame; + bucket[i].lastUsedFrame = current_frame; return; } } diff --git a/packages/webgpu/src/TexturePool/usecase/TexturePoolAcquireUseCase.ts b/packages/webgpu/src/TexturePool/usecase/TexturePoolAcquireUseCase.ts index 97ca9558..0c337983 100644 --- a/packages/webgpu/src/TexturePool/usecase/TexturePoolAcquireUseCase.ts +++ b/packages/webgpu/src/TexturePool/usecase/TexturePoolAcquireUseCase.ts @@ -2,10 +2,11 @@ import type { IPooledTexture, ITexturePoolBuckets } from "../../interface/IPoole /** * @description バケットキーを生成(exactサイズ + フォーマット) + * Build bucket key from exact size and format * - * @param {number} width - * @param {number} height - * @param {GPUTextureFormat} format + * @param {number} width - テクスチャ幅 + * @param {number} height - テクスチャ高さ + * @param {GPUTextureFormat} format - テクスチャフォーマット * @return {string} */ const buildKey = (width: number, height: number, format: GPUTextureFormat): string => @@ -17,15 +18,15 @@ const buildKey = (width: number, height: number, format: GPUTextureFormat): stri * @description テクスチャを取得または作成(バケットMap検索) * Acquire texture from pool or create new one (bucket Map lookup) * - * @param {GPUDevice} device - * @param {ITexturePoolBuckets} buckets - * @param {number} width - * @param {number} height - * @param {GPUTextureFormat} format - * @param {GPUTextureUsageFlags} usage - * @param {number} currentFrame - * @param {number} maxPoolSize - * @param {number[]} totalCount - [0]に現在の合計数を格納 + * @param {GPUDevice} device - GPUデバイス + * @param {ITexturePoolBuckets} buckets - テクスチャプールバケット + * @param {number} width - テクスチャ幅 + * @param {number} height - テクスチャ高さ + * @param {GPUTextureFormat} format - テクスチャフォーマット + * @param {GPUTextureUsageFlags} usage - テクスチャ使用フラグ + * @param {number} current_frame - 現在のフレーム番号 + * @param {number} max_pool_size - プールの最大サイズ + * @param {number[]} total_count - [0]に現在の合計数を格納 * @return {GPUTexture} * @method * @protected @@ -37,9 +38,9 @@ export const execute = ( height: number, format: GPUTextureFormat, usage: GPUTextureUsageFlags, - currentFrame: number, - maxPoolSize: number, - totalCount: number[] + current_frame: number, + max_pool_size: number, + total_count: number[] ): GPUTexture => { const key = buildKey(width, height, format); @@ -50,14 +51,14 @@ export const execute = ( const entry = bucket[i]; if (!entry.inUse) { entry.inUse = true; - entry.lastUsedFrame = currentFrame; + entry.lastUsedFrame = current_frame; return entry.texture; } } } // プールが満杯なら最も古い未使用エントリを削除(LRU回収) - if (totalCount[0] >= maxPoolSize) { + if (total_count[0] >= max_pool_size) { let oldestFrame = Infinity; let oldestKey = ""; let oldestIdx = -1; @@ -80,7 +81,7 @@ export const execute = ( if (bEntries.length === 0) { buckets.delete(oldestKey); } - totalCount[0]--; + total_count[0]--; } } @@ -96,7 +97,7 @@ export const execute = ( width, height, format, - "lastUsedFrame": currentFrame, + "lastUsedFrame": current_frame, "inUse": true }; @@ -105,7 +106,7 @@ export const execute = ( } else { buckets.set(key, [entry]); } - totalCount[0]++; + total_count[0]++; return texture; }; diff --git a/packages/webgpu/src/WebGPUUtil.ts b/packages/webgpu/src/WebGPUUtil.ts index 6c5be6ac..b9e99aff 100644 --- a/packages/webgpu/src/WebGPUUtil.ts +++ b/packages/webgpu/src/WebGPUUtil.ts @@ -4,23 +4,52 @@ * * @type {number} * @default 4 - * @protected - * - * @note WebGL版と同じくMSAA 4xをデフォルトで有効化 - * 曲線のアンチエイリアス品質向上のため */ export const $samples: number = 4; +/** + * @description WebGPU描画ユーティリティクラス + * Utility class for WebGPU rendering operations + */ export class WebGPUUtil { + /** + * @description GPUデバイスインスタンス + * GPU device instance + * + * @type {GPUDevice | null} + */ private static device: GPUDevice | null = null; + + /** + * @description デバイスピクセル比率 + * Device pixel ratio for high-DPI rendering + * + * @type {number} + */ private static devicePixelRatio: number = 1; + + /** + * @description レンダリング最大サイズ(テクスチャアトラス用) + * Maximum render size for texture atlas + * + * @type {number} + */ private static renderMaxSize: number = 8192; + + /** + * @description Float32Array(4) のオブジェクトプール + * Object pool for Float32Array(4) instances + * + * @type {Float32Array[]} + */ private static float32Array4Pool: Float32Array[] = []; /** - * @description Set GPUDevice - * @param {GPUDevice} gpu_device + * @description GPUデバイスを設定する + * Set the GPUDevice instance + * + * @param {GPUDevice} gpu_device - GPUデバイス / GPU device * @return {void} */ public static setDevice(gpu_device: GPUDevice): void @@ -29,7 +58,9 @@ export class WebGPUUtil } /** - * @description Get GPUDevice + * @description GPUデバイスを取得する + * Get the GPUDevice instance + * * @return {GPUDevice} */ public static getDevice(): GPUDevice @@ -41,8 +72,10 @@ export class WebGPUUtil } /** - * @description Set device pixel ratio - * @param {number} ratio + * @description デバイスピクセル比率を設定する + * Set the device pixel ratio + * + * @param {number} ratio - ピクセル比率 / pixel ratio * @return {void} */ public static setDevicePixelRatio(ratio: number): void @@ -51,7 +84,9 @@ export class WebGPUUtil } /** - * @description Get device pixel ratio + * @description デバイスピクセル比率を取得する + * Get the device pixel ratio + * * @return {number} */ public static getDevicePixelRatio(): number @@ -60,8 +95,10 @@ export class WebGPUUtil } /** - * @description Set render max size - * @param {number} size + * @description レンダリング最大サイズを設定する + * Set the maximum render size + * + * @param {number} size - 最大サイズ / maximum size in pixels * @return {void} */ public static setRenderMaxSize(size: number): void @@ -70,7 +107,9 @@ export class WebGPUUtil } /** - * @description Get render max size (for atlas) + * @description レンダリング最大サイズを取得する(アトラス用) + * Get the maximum render size (for texture atlas) + * * @return {number} */ public static getRenderMaxSize(): number @@ -79,8 +118,10 @@ export class WebGPUUtil } /** - * @description Create Float32Array - * @param {number} length + * @description 指定長のFloat32Arrayを生成する + * Create a new Float32Array with the specified length + * + * @param {number} length - 配列の長さ / array length * @return {Float32Array} */ public static createFloat32Array(length: number): Float32Array @@ -89,8 +130,10 @@ export class WebGPUUtil } /** - * @description Create generic array - * @return {Array} + * @description 汎用の空配列を生成する + * Create a new empty generic array + * + * @return {T[]} */ public static createArray(): T[] { @@ -98,7 +141,9 @@ export class WebGPUUtil } /** - * @description Get Float32Array(4) from pool + * @description Float32Array(4) をプールから取得する(なければ新規作成) + * Get a Float32Array(4) from the pool, or create a new one + * * @return {Float32Array} */ public static getFloat32Array4(): Float32Array @@ -109,8 +154,10 @@ export class WebGPUUtil } /** - * @description Return Float32Array(4) to pool - * @param {Float32Array} array + * @description Float32Array(4) をプールに返却する + * Return a Float32Array(4) to the pool for reuse + * + * @param {Float32Array} array - 返却する配列 / array to return * @return {void} */ public static poolFloat32Array4(array: Float32Array): void @@ -123,12 +170,18 @@ export class WebGPUUtil /** * @description グローバルコンテキスト(WebGLUtilの$contextに相当) + * Global context instance (equivalent to $context in WebGLUtil) + * + * @type {any} */ export let $context: any = null; /** - * @description コンテキストを設定 - * @param {any} context + * @description グローバルコンテキストを設定する + * Set the global context instance + * + * @param {any} context - コンテキスト / context instance + * @return {void} */ export const $setContext = (context: any): void => { @@ -136,7 +189,9 @@ export const $setContext = (context: any): void => }; /** - * @description Float32Array(4) をプールから取得 + * @description Float32Array(4) をプールから取得する + * Get a Float32Array(4) from the pool + * * @return {Float32Array} */ export const $getFloat32Array4 = (): Float32Array => @@ -145,8 +200,11 @@ export const $getFloat32Array4 = (): Float32Array => }; /** - * @description Float32Array(4) をプールに返却 - * @param {Float32Array} array + * @description Float32Array(4) をプールに返却する + * Return a Float32Array(4) to the pool + * + * @param {Float32Array} array - 返却する配列 / array to return + * @return {void} */ export const $poolFloat32Array4 = (array: Float32Array): void => { diff --git a/packages/webgpu/src/interface/IAttachmentObject.ts b/packages/webgpu/src/interface/IAttachmentObject.ts index 7a3fcc10..6d6e4206 100644 --- a/packages/webgpu/src/interface/IAttachmentObject.ts +++ b/packages/webgpu/src/interface/IAttachmentObject.ts @@ -11,14 +11,50 @@ import type { IStencilBufferObject } from "./IStencilBufferObject"; */ export interface IAttachmentObject { + /** + * @description アタッチメントの一意な識別子 + * Unique identifier for the attachment + */ id: number; + /** + * @description アタッチメントの幅(ピクセル) + * Width of the attachment in pixels + */ width: number; + /** + * @description アタッチメントの高さ(ピクセル) + * Height of the attachment in pixels + */ height: number; + /** + * @description 現在のクリップ(マスク)ネストレベル + * Current clip (mask) nesting level + */ clipLevel: number; + /** + * @description MSAAが有効かどうか + * Whether MSAA is enabled + */ msaa: boolean; + /** + * @description マスクモードが有効かどうか + * Whether mask mode is enabled + */ mask: boolean; + /** + * @description カラーバッファオブジェクト + * Color buffer object + */ color: IColorBufferObject | null; + /** + * @description テクスチャオブジェクト + * Texture object + */ texture: ITextureObject | null; + /** + * @description ステンシルバッファオブジェクト + * Stencil buffer object + */ stencil: IStencilBufferObject | null; /** * @description MSAAテクスチャ(sampleCount > 1 の場合に使用) diff --git a/packages/webgpu/src/interface/IBlendMode.ts b/packages/webgpu/src/interface/IBlendMode.ts index 95fe0434..2370ad76 100644 --- a/packages/webgpu/src/interface/IBlendMode.ts +++ b/packages/webgpu/src/interface/IBlendMode.ts @@ -1,3 +1,10 @@ +/** + * @description ブレンドモードの型定義 + * Blend mode type definition + * + * 描画オブジェクトに適用可能なブレンドモードを定義します。 + * Defines the blend modes that can be applied to display objects. + */ export type IBlendMode = | "normal" | "layer" diff --git a/packages/webgpu/src/interface/IBlendState.ts b/packages/webgpu/src/interface/IBlendState.ts index ba9a54d1..1c52dff3 100644 --- a/packages/webgpu/src/interface/IBlendState.ts +++ b/packages/webgpu/src/interface/IBlendState.ts @@ -3,6 +3,14 @@ * WebGPU blend state definitions */ export interface IBlendState { + /** + * @description カラーチャンネルのブレンド設定 + * Blend component configuration for the color channel + */ color: GPUBlendComponent; + /** + * @description アルファチャンネルのブレンド設定 + * Blend component configuration for the alpha channel + */ alpha: GPUBlendComponent; } diff --git a/packages/webgpu/src/interface/IBounds.ts b/packages/webgpu/src/interface/IBounds.ts index 762980e7..9d83e462 100644 --- a/packages/webgpu/src/interface/IBounds.ts +++ b/packages/webgpu/src/interface/IBounds.ts @@ -1,7 +1,30 @@ +/** + * @description バウンディングボックスのインターフェース + * Bounding box interface + * + * 描画オブジェクトの矩形領域を最小・最大座標で定義します。 + * Defines a rectangular area of a display object using min/max coordinates. + */ export interface IBounds { + /** + * @description X軸の最小値 + * Minimum value on the X axis + */ xMin: number; + /** + * @description Y軸の最小値 + * Minimum value on the Y axis + */ yMin: number; + /** + * @description X軸の最大値 + * Maximum value on the X axis + */ xMax: number; + /** + * @description Y軸の最大値 + * Maximum value on the Y axis + */ yMax: number; } diff --git a/packages/webgpu/src/interface/IColorBufferObject.ts b/packages/webgpu/src/interface/IColorBufferObject.ts index 4565f6be..e014555f 100644 --- a/packages/webgpu/src/interface/IColorBufferObject.ts +++ b/packages/webgpu/src/interface/IColorBufferObject.ts @@ -9,11 +9,39 @@ import type { IStencilBufferObject } from "./IStencilBufferObject"; */ export interface IColorBufferObject { + /** + * @description GPUテクスチャリソース + * GPU texture resource + */ resource: GPUTexture; + /** + * @description テクスチャビュー(レンダーパスへのアタッチ用) + * Texture view for attaching to render passes + */ view: GPUTextureView; + /** + * @description 対応するステンシルバッファオブジェクト + * Associated stencil buffer object + */ stencil: IStencilBufferObject; + /** + * @description カラーバッファの幅(ピクセル) + * Width of the color buffer in pixels + */ width: number; + /** + * @description カラーバッファの高さ(ピクセル) + * Height of the color buffer in pixels + */ height: number; + /** + * @description バッファの面積(width × height) + * Area of the buffer (width × height) + */ area: number; + /** + * @description バッファが変更されたかどうか + * Whether the buffer has been modified + */ dirty: boolean; } diff --git a/packages/webgpu/src/interface/IComplexBlendItem.ts b/packages/webgpu/src/interface/IComplexBlendItem.ts index ac27a265..35569d4f 100644 --- a/packages/webgpu/src/interface/IComplexBlendItem.ts +++ b/packages/webgpu/src/interface/IComplexBlendItem.ts @@ -5,16 +5,64 @@ import type { Node } from "@next2d/texture-packer"; * Complex blend mode rendering queue */ export interface IComplexBlendItem { + /** + * @description テクスチャアトラスのノード + * Texture atlas node + */ node: Node; + /** + * @description 描画領域のX最小座標 + * Minimum X coordinate of the rendering area + */ x_min: number; + /** + * @description 描画領域のY最小座標 + * Minimum Y coordinate of the rendering area + */ y_min: number; + /** + * @description 描画領域のX最大座標 + * Maximum X coordinate of the rendering area + */ x_max: number; + /** + * @description 描画領域のY最大座標 + * Maximum Y coordinate of the rendering area + */ y_max: number; + /** + * @description カラー変換配列 + * Color transform array + */ color_transform: Float32Array; + /** + * @description 変換行列 + * Transformation matrix + */ matrix: Float32Array; + /** + * @description ブレンドモード名 + * Blend mode name + */ blend_mode: string; + /** + * @description ビューポートの幅 + * Viewport width + */ viewport_width: number; + /** + * @description ビューポートの高さ + * Viewport height + */ viewport_height: number; + /** + * @description レンダリング最大サイズ + * Maximum render size + */ render_max_size: number; + /** + * @description グローバル透明度 + * Global alpha transparency + */ global_alpha: number; } diff --git a/packages/webgpu/src/interface/IFilterConfig.ts b/packages/webgpu/src/interface/IFilterConfig.ts index d069b384..6f788e07 100644 --- a/packages/webgpu/src/interface/IFilterConfig.ts +++ b/packages/webgpu/src/interface/IFilterConfig.ts @@ -5,12 +5,28 @@ import type { IAttachmentObject } from "./IAttachmentObject"; * Common filter processing configuration */ export interface IFilterConfig { + /** + * @description GPUデバイス + * GPU device instance + */ device: GPUDevice; + /** + * @description GPUコマンドエンコーダー + * GPU command encoder + */ commandEncoder: GPUCommandEncoder; + /** + * @description ユニフォームバッファの管理インターフェース + * Uniform buffer management interface + */ bufferManager?: { acquireUniformBuffer(requiredSize: number): GPUBuffer; acquireAndWriteUniformBuffer(data: Float32Array, byteLength?: number): GPUBuffer; }; + /** + * @description フレームバッファの管理インターフェース + * Frame buffer management interface + */ frameBufferManager: { createTemporaryAttachment(width: number, height: number): IAttachmentObject; releaseTemporaryAttachment(attachment: IAttachmentObject): void; @@ -20,13 +36,25 @@ export interface IFilterConfig { loadOp: GPULoadOp ): GPURenderPassDescriptor; }; + /** + * @description パイプラインの管理インターフェース + * Pipeline management interface + */ pipelineManager: { getPipeline(name: string): GPURenderPipeline | undefined; getFilterPipeline(baseName: string, constants: Record): GPURenderPipeline | undefined; getBindGroupLayout(name: string): GPUBindGroupLayout | undefined; }; + /** + * @description テクスチャの管理インターフェース + * Texture management interface + */ textureManager: { createSampler(name: string, smooth: boolean): GPUSampler; }; + /** + * @description フレームテクスチャの配列 + * Array of frame textures + */ frameTextures: GPUTexture[]; } diff --git a/packages/webgpu/src/interface/IGradientStop.ts b/packages/webgpu/src/interface/IGradientStop.ts index 86176eae..d6cc6ec0 100644 --- a/packages/webgpu/src/interface/IGradientStop.ts +++ b/packages/webgpu/src/interface/IGradientStop.ts @@ -3,9 +3,29 @@ * Gradient stop type definition */ export interface IGradientStop { + /** + * @description グラデーション位置(0.0〜1.0) + * Gradient position ratio (0.0 to 1.0) + */ ratio: number; + /** + * @description 赤チャンネル値(0〜255) + * Red channel value (0 to 255) + */ r: number; + /** + * @description 緑チャンネル値(0〜255) + * Green channel value (0 to 255) + */ g: number; + /** + * @description 青チャンネル値(0〜255) + * Blue channel value (0 to 255) + */ b: number; + /** + * @description アルファチャンネル値(0〜255) + * Alpha channel value (0 to 255) + */ a: number; } diff --git a/packages/webgpu/src/interface/ILocalFilterConfig.ts b/packages/webgpu/src/interface/ILocalFilterConfig.ts index df101f2f..1abd6e3d 100644 --- a/packages/webgpu/src/interface/ILocalFilterConfig.ts +++ b/packages/webgpu/src/interface/ILocalFilterConfig.ts @@ -9,12 +9,44 @@ import type { TextureManager } from "../TextureManager"; * Local filter configuration for ContextApplyFilterUseCase */ export interface ILocalFilterConfig { + /** + * @description GPUデバイス + * GPU device instance + */ device: GPUDevice; + /** + * @description GPUコマンドエンコーダー + * GPU command encoder + */ commandEncoder: GPUCommandEncoder; + /** + * @description バッファマネージャー + * Buffer manager instance + */ bufferManager: BufferManager; + /** + * @description フレームバッファマネージャー + * Frame buffer manager instance + */ frameBufferManager: FrameBufferManager; + /** + * @description パイプラインマネージャー + * Pipeline manager instance + */ pipelineManager: PipelineManager; + /** + * @description テクスチャマネージャー + * Texture manager instance + */ textureManager: TextureManager; + /** + * @description メインのアタッチメントオブジェクト(任意) + * Main attachment object (optional) + */ mainAttachment?: IAttachmentObject; + /** + * @description フレームテクスチャの配列 + * Array of frame textures + */ frameTextures: GPUTexture[]; } diff --git a/packages/webgpu/src/interface/IMeshResult.ts b/packages/webgpu/src/interface/IMeshResult.ts index 934ad3ef..1bd68431 100644 --- a/packages/webgpu/src/interface/IMeshResult.ts +++ b/packages/webgpu/src/interface/IMeshResult.ts @@ -3,6 +3,14 @@ * Common interface for mesh generation results */ export interface IMeshResult { + /** + * @description 頂点データバッファ + * Vertex data buffer + */ buffer: Float32Array; + /** + * @description インデックスの数(描画する三角形の頂点数) + * Number of indices (vertex count for drawing triangles) + */ indexCount: number; } diff --git a/packages/webgpu/src/interface/IPoint.ts b/packages/webgpu/src/interface/IPoint.ts index a5980d9a..981f0d24 100644 --- a/packages/webgpu/src/interface/IPoint.ts +++ b/packages/webgpu/src/interface/IPoint.ts @@ -1,5 +1,17 @@ +/** + * @description 2D座標点のインターフェース + * 2D coordinate point interface + */ export interface IPoint { + /** + * @description X座標 + * X coordinate + */ x: number; + /** + * @description Y座標 + * Y coordinate + */ y: number; } diff --git a/packages/webgpu/src/interface/IPooledTexture.ts b/packages/webgpu/src/interface/IPooledTexture.ts index c129337e..4a1e0d1e 100644 --- a/packages/webgpu/src/interface/IPooledTexture.ts +++ b/packages/webgpu/src/interface/IPooledTexture.ts @@ -3,11 +3,35 @@ * Pooled texture interface */ export interface IPooledTexture { + /** + * @description GPUテクスチャリソース + * GPU texture resource + */ texture: GPUTexture; + /** + * @description テクスチャの幅(ピクセル) + * Width of the texture in pixels + */ width: number; + /** + * @description テクスチャの高さ(ピクセル) + * Height of the texture in pixels + */ height: number; + /** + * @description テクスチャフォーマット + * Texture format + */ format: GPUTextureFormat; + /** + * @description 最後に使用されたフレーム番号 + * Last frame number when this texture was used + */ lastUsedFrame: number; + /** + * @description 使用中フラグ + * Whether this texture is currently in use + */ inUse: boolean; } diff --git a/packages/webgpu/src/interface/IQuadraticSegment.ts b/packages/webgpu/src/interface/IQuadraticSegment.ts index 84476bdd..35527b2e 100644 --- a/packages/webgpu/src/interface/IQuadraticSegment.ts +++ b/packages/webgpu/src/interface/IQuadraticSegment.ts @@ -5,6 +5,14 @@ import type { IPoint } from "./IPoint"; * Quadratic bezier segment approximation */ export interface IQuadraticSegment { + /** + * @description 制御点 + * Control point + */ ctrl: IPoint; + /** + * @description 終点 + * End point + */ end: IPoint; } diff --git a/packages/webgpu/src/interface/IStencilBufferObject.ts b/packages/webgpu/src/interface/IStencilBufferObject.ts index c3f7ac7b..431ec929 100644 --- a/packages/webgpu/src/interface/IStencilBufferObject.ts +++ b/packages/webgpu/src/interface/IStencilBufferObject.ts @@ -7,11 +7,39 @@ */ export interface IStencilBufferObject { + /** + * @description ステンシルバッファの一意な識別子 + * Unique identifier for the stencil buffer + */ id: number; + /** + * @description GPUテクスチャリソース + * GPU texture resource + */ resource: GPUTexture; + /** + * @description テクスチャビュー(レンダーパスへのアタッチ用) + * Texture view for attaching to render passes + */ view: GPUTextureView; + /** + * @description ステンシルバッファの幅(ピクセル) + * Width of the stencil buffer in pixels + */ width: number; + /** + * @description ステンシルバッファの高さ(ピクセル) + * Height of the stencil buffer in pixels + */ height: number; + /** + * @description バッファの面積(width × height) + * Area of the buffer (width × height) + */ area: number; + /** + * @description バッファが変更されたかどうか + * Whether the buffer has been modified + */ dirty: boolean; } diff --git a/packages/webgpu/src/interface/IStorageBufferConfig.ts b/packages/webgpu/src/interface/IStorageBufferConfig.ts index edb3657f..10467f1c 100644 --- a/packages/webgpu/src/interface/IStorageBufferConfig.ts +++ b/packages/webgpu/src/interface/IStorageBufferConfig.ts @@ -5,16 +5,19 @@ export interface IStorageBufferConfig { /** * @description バッファサイズ(バイト) + * Buffer size in bytes */ size: number; /** - * @description 使用目的 + * @description 使用目的フラグ + * Usage flags for the buffer */ usage: GPUBufferUsageFlags; /** * @description ラベル(デバッグ用) + * Label for debugging purposes */ label?: string; } @@ -26,21 +29,25 @@ export interface IStorageBufferConfig { export interface IPooledStorageBuffer { /** * @description GPUバッファ + * GPU buffer instance */ buffer: GPUBuffer; /** * @description バッファサイズ(バイト) + * Buffer size in bytes */ size: number; /** * @description 使用中フラグ + * Whether this buffer is currently in use */ inUse: boolean; /** * @description 最後に使用されたフレーム番号 + * Last frame number when this buffer was used */ lastUsedFrame: number; } diff --git a/packages/webgpu/src/interface/ITextureObject.ts b/packages/webgpu/src/interface/ITextureObject.ts index 8ee1d5e3..032bc703 100644 --- a/packages/webgpu/src/interface/ITextureObject.ts +++ b/packages/webgpu/src/interface/ITextureObject.ts @@ -7,11 +7,39 @@ */ export interface ITextureObject { + /** + * @description テクスチャの一意な識別子 + * Unique identifier for the texture + */ id: number; + /** + * @description GPUテクスチャリソース + * GPU texture resource + */ resource: GPUTexture; + /** + * @description テクスチャビュー(シェーダーバインド用) + * Texture view for shader binding + */ view: GPUTextureView; + /** + * @description テクスチャの幅(ピクセル) + * Width of the texture in pixels + */ width: number; + /** + * @description テクスチャの高さ(ピクセル) + * Height of the texture in pixels + */ height: number; + /** + * @description テクスチャの面積(width × height) + * Area of the texture (width × height) + */ area: number; + /** + * @description スムージング(バイリニアフィルタリング)が有効かどうか + * Whether smoothing (bilinear filtering) is enabled + */ smooth: boolean; } From afe740e2fcb283f2152422326e60db51fcc2dbf7 Mon Sep 17 00:00:00 2001 From: ienaga Date: Fri, 27 Mar 2026 07:16:49 +0900 Subject: [PATCH 14/26] =?UTF-8?q?#267=20WebGPU=E3=81=AE=E3=83=AA=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/webgpu/src/AttachmentManager.test.ts | 13 - packages/webgpu/src/AttachmentManager.ts | 21 -- packages/webgpu/src/BufferManager.test.ts | 240 +----------------- packages/webgpu/src/BufferManager.ts | 209 --------------- ...ManagerAcquireStorageBufferUseCase.test.ts | 34 +-- ...ManagerReleaseStorageBufferUseCase.test.ts | 55 ---- ...ufferManagerReleaseStorageBufferUseCase.ts | 22 -- packages/webgpu/src/Context.test.ts | 17 +- packages/webgpu/src/Context.ts | 105 -------- .../webgpu/src/FrameBufferManager.test.ts | 16 -- packages/webgpu/src/FrameBufferManager.ts | 14 - ...dientLUTCalculateResolutionService.test.ts | 61 ----- .../GradientLUTCalculateResolutionService.ts | 36 --- .../GradientLUTGeneratePixelsService.test.ts | 104 -------- .../GradientLUTGeneratePixelsService.ts | 80 ------ ...GradientLUTInterpolateColorService.test.ts | 70 ----- .../GradientLUTInterpolateColorService.ts | 48 ---- .../GradientLUTParseStopsService.test.ts | 58 ----- .../service/GradientLUTParseStopsService.ts | 30 --- .../webgpu/src/Shader/ShaderSource.test.ts | 59 ----- packages/webgpu/src/Shader/ShaderSource.ts | 24 +- packages/webgpu/src/TextureManager.test.ts | 164 +----------- packages/webgpu/src/TextureManager.ts | 96 ------- ...reateTextureFromImageBitmapUseCase.test.ts | 188 -------------- ...agerCreateTextureFromImageBitmapUseCase.ts | 41 --- ...agerCreateTextureFromPixelsUseCase.test.ts | 206 --------------- ...reManagerCreateTextureFromPixelsUseCase.ts | 40 --- packages/webgpu/src/TexturePool.test.ts | 58 +---- packages/webgpu/src/TexturePool.ts | 27 -- 29 files changed, 40 insertions(+), 2096 deletions(-) delete mode 100644 packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.test.ts delete mode 100644 packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.ts delete mode 100644 packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.test.ts delete mode 100644 packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.ts delete mode 100644 packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTGeneratePixelsService.test.ts delete mode 100644 packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTGeneratePixelsService.ts delete mode 100644 packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.test.ts delete mode 100644 packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.ts delete mode 100644 packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTParseStopsService.test.ts delete mode 100644 packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTParseStopsService.ts delete mode 100644 packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.test.ts delete mode 100644 packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.ts delete mode 100644 packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.test.ts delete mode 100644 packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.ts diff --git a/packages/webgpu/src/AttachmentManager.test.ts b/packages/webgpu/src/AttachmentManager.test.ts index 19a459b2..d47a1f18 100644 --- a/packages/webgpu/src/AttachmentManager.test.ts +++ b/packages/webgpu/src/AttachmentManager.test.ts @@ -102,19 +102,6 @@ describe("AttachmentManager", () => }); }); - describe("bindAttachment / unbindAttachment", () => - { - it("should bind and unbind attachment without error", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - const attachment = manager.getAttachmentObject(100, 100); - - expect(() => manager.bindAttachment(attachment)).not.toThrow(); - expect(() => manager.unbindAttachment()).not.toThrow(); - }); - }); - describe("releaseAttachment", () => { it("should release attachment back to pool", () => diff --git a/packages/webgpu/src/AttachmentManager.ts b/packages/webgpu/src/AttachmentManager.ts index f170b153..95d2c1fd 100644 --- a/packages/webgpu/src/AttachmentManager.ts +++ b/packages/webgpu/src/AttachmentManager.ts @@ -60,27 +60,6 @@ export class AttachmentManager ); } - /** - * @description 現在のアタッチメントをバインドする - * Bind the current attachment - * @param {IAttachmentObject} attachment - バインドするアタッチメント / Attachment to bind - * @return {void} - */ - bindAttachment(_attachment: IAttachmentObject): void - { - // no-op: バインド状態はContext側で管理 - } - - /** - * @description 現在のアタッチメントのバインドを解除する - * Unbind the current attachment - * @return {void} - */ - unbindAttachment(): void - { - // no-op: バインド状態はContext側で管理 - } - /** * @description アタッチメントをプールに返却する * Release an attachment back to the pool diff --git a/packages/webgpu/src/BufferManager.test.ts b/packages/webgpu/src/BufferManager.test.ts index ed36bf52..cef33579 100644 --- a/packages/webgpu/src/BufferManager.test.ts +++ b/packages/webgpu/src/BufferManager.test.ts @@ -59,13 +59,6 @@ vi.mock("./BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase", () = }) })); -vi.mock("./BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase", () => ({ - "execute": vi.fn((pool, buffer) => { - const entry = pool.find((e: any) => e.buffer === buffer); - if (entry) entry.inUse = false; - }) -})); - vi.mock("./BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase", () => ({ "execute": vi.fn() })); @@ -110,112 +103,6 @@ describe("BufferManager", () => expect(manager).toBeDefined(); }); - it("should initialize with zero frame number", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - expect(manager.getFrameNumber()).toBe(0); - }); - - }); - - describe("createVertexBuffer", () => - { - it("should create vertex buffer with data", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - const data = new Float32Array([1, 2, 3, 4]); - - const buffer = manager.createVertexBuffer("test", data); - - expect(buffer).toBeDefined(); - expect(device.createBuffer).toHaveBeenCalled(); - }); - - it("should store buffer by name", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - const data = new Float32Array([1, 2, 3]); - - manager.createVertexBuffer("myBuffer", data); - const retrieved = manager.getVertexBuffer("myBuffer"); - - expect(retrieved).toBeDefined(); - }); - }); - - describe("createUniformBuffer", () => - { - it("should create uniform buffer with specified size", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - const buffer = manager.createUniformBuffer("uniforms", 64); - - expect(buffer).toBeDefined(); - expect(device.createBuffer).toHaveBeenCalled(); - }); - - it("should store buffer by name", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.createUniformBuffer("myUniforms", 128); - const retrieved = manager.getUniformBuffer("myUniforms"); - - expect(retrieved).toBeDefined(); - }); - }); - - describe("updateUniformBuffer", () => - { - it("should write data to existing buffer", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - const data = new Float32Array([1, 2, 3, 4]); - - manager.createUniformBuffer("uniforms", 64); - manager.updateUniformBuffer("uniforms", data); - - expect(device.queue.writeBuffer).toHaveBeenCalled(); - }); - - it("should not throw when buffer does not exist", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - const data = new Float32Array([1, 2, 3, 4]); - - expect(() => manager.updateUniformBuffer("nonexistent", data)).not.toThrow(); - }); - }); - - describe("getVertexBuffer", () => - { - it("should return undefined for non-existent buffer", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - expect(manager.getVertexBuffer("nonexistent")).toBeUndefined(); - }); - }); - - describe("getUniformBuffer", () => - { - it("should return undefined for non-existent buffer", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - expect(manager.getUniformBuffer("nonexistent")).toBeUndefined(); - }); }); describe("createRectVertices", () => @@ -246,18 +133,6 @@ describe("BufferManager", () => }); - describe("releaseVertexBuffer", () => - { - it("should release buffer back to pool", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - const buffer = manager.acquireVertexBuffer(256); - - expect(() => manager.releaseVertexBuffer(buffer)).not.toThrow(); - }); - }); - describe("acquireUniformBuffer", () => { it("should acquire buffer from pool or create new", () => @@ -272,57 +147,14 @@ describe("BufferManager", () => }); - describe("releaseUniformBuffer", () => - { - it("should release buffer back to pool", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - const buffer = manager.acquireUniformBuffer(64); - - expect(() => manager.releaseUniformBuffer(buffer)).not.toThrow(); - }); - }); - - describe("destroyBuffer", () => - { - it("should destroy vertex buffer by name", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.createVertexBuffer("test", new Float32Array([1, 2, 3])); - manager.destroyBuffer("test"); - - expect(manager.getVertexBuffer("test")).toBeUndefined(); - }); - - it("should destroy uniform buffer by name", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.createUniformBuffer("test", 64); - manager.destroyBuffer("test"); - - expect(manager.getUniformBuffer("test")).toBeUndefined(); - }); - }); - describe("dispose", () => { - it("should clear all buffers", () => + it("should not throw when disposing", () => { const device = createMockDevice(); const manager = new BufferManager(device); - manager.createVertexBuffer("v1", new Float32Array([1, 2, 3])); - manager.createUniformBuffer("u1", 64); - - manager.dispose(); - - expect(manager.getVertexBuffer("v1")).toBeUndefined(); - expect(manager.getUniformBuffer("u1")).toBeUndefined(); + expect(() => manager.dispose()).not.toThrow(); }); }); @@ -341,18 +173,6 @@ describe("BufferManager", () => }); - describe("releaseStorageBuffer", () => - { - it("should release storage buffer", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - const buffer = manager.acquireStorageBuffer(1024); - - expect(() => manager.releaseStorageBuffer(buffer)).not.toThrow(); - }); - }); - describe("writeStorageBuffer", () => { it("should write data to storage buffer", () => @@ -384,26 +204,12 @@ describe("BufferManager", () => describe("clearFrameBuffers", () => { - it("should clear named buffers", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.createVertexBuffer("frame", new Float32Array([1, 2, 3])); - manager.clearFrameBuffers(); - - expect(manager.getVertexBuffer("frame")).toBeUndefined(); - }); - - it("should increment frame number", () => + it("should not throw when clearing", () => { const device = createMockDevice(); const manager = new BufferManager(device); - const beforeFrame = manager.getFrameNumber(); - manager.clearFrameBuffers(); - - expect(manager.getFrameNumber()).toBe(beforeFrame + 1); + expect(() => manager.clearFrameBuffers()).not.toThrow(); }); it("should release all storage buffers", () => @@ -416,30 +222,6 @@ describe("BufferManager", () => }); }); - describe("getOrCreateIndirectBuffer", () => - { - it("should create indirect buffer on first call", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - const buffer = manager.getOrCreateIndirectBuffer(6, 10); - - expect(buffer).toBeDefined(); - }); - - it("should return same buffer on subsequent calls", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - const buffer1 = manager.getOrCreateIndirectBuffer(6, 10); - const buffer2 = manager.getOrCreateIndirectBuffer(6, 20); - - expect(buffer1).toBe(buffer2); - }); - }); - describe("createIndirectBuffer", () => { it("should create new indirect buffer each time", () => @@ -455,18 +237,4 @@ describe("BufferManager", () => }); }); - describe("getFrameNumber", () => - { - it("should track frame count", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.clearFrameBuffers(); - manager.clearFrameBuffers(); - manager.clearFrameBuffers(); - - expect(manager.getFrameNumber()).toBe(3); - }); - }); }); diff --git a/packages/webgpu/src/BufferManager.ts b/packages/webgpu/src/BufferManager.ts index daea9045..e037d27d 100644 --- a/packages/webgpu/src/BufferManager.ts +++ b/packages/webgpu/src/BufferManager.ts @@ -5,7 +5,6 @@ import { execute as bufferManagerAcquireUniformBufferUseCase } from "./BufferMan import { execute as bufferManagerReleaseVertexBufferService } from "./BufferManager/service/BufferManagerReleaseVertexBufferService"; import { execute as bufferManagerReleaseUniformBufferService } from "./BufferManager/service/BufferManagerReleaseUniformBufferService"; import { execute as bufferManagerAcquireStorageBufferUseCase } from "./BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase"; -import { execute as releaseStorageBufferUseCase } from "./BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase"; import { execute as cleanupStorageBuffersUseCase } from "./BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase"; import { execute as bufferManagerCreateIndirectBufferService } from "./BufferManager/service/BufferManagerCreateIndirectBufferService"; import { execute as updateIndirectBuffer } from "./BufferManager/service/BufferManagerUpdateIndirectBufferService"; @@ -164,12 +163,9 @@ export class DynamicUniformAllocator export class BufferManager { private device: GPUDevice; - private vertexBuffers: Map; - private uniformBuffers: Map; private vertexBufferBuckets: Map; private uniformBufferBuckets: Map; private storageBufferPool: IPooledStorageBuffer[]; - private indirectBuffer: GPUBuffer | null; private indirectBufferPool: GPUBuffer[]; private frameIndirectBuffers: GPUBuffer[]; private frameNumber: number; @@ -186,12 +182,9 @@ export class BufferManager constructor (device: GPUDevice) { this.device = device; - this.vertexBuffers = new Map(); - this.uniformBuffers = new Map(); this.vertexBufferBuckets = new Map(); this.uniformBufferBuckets = new Map(); this.storageBufferPool = []; - this.indirectBuffer = null; this.indirectBufferPool = []; this.frameIndirectBuffers = []; this.frameNumber = 0; @@ -201,83 +194,6 @@ export class BufferManager this.dynamicUniform = new DynamicUniformAllocator(device); } - /** - * @description 名前付き頂点バッファを作成し、初期データを書き込む - * Create a named vertex buffer and write initial data - * @param {string} name - バッファ名 - * @param {Float32Array} data - 頂点データ - * @return {GPUBuffer} 作成された頂点バッファ - */ - createVertexBuffer (name: string, data: Float32Array): GPUBuffer - { - const buffer = this.device.createBuffer({ - "size": data.byteLength, - "usage": GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, - "mappedAtCreation": true - }); - - new Float32Array(buffer.getMappedRange()).set(data); - buffer.unmap(); - - this.vertexBuffers.set(name, buffer); - return buffer; - } - - /** - * @description 名前付きユニフォームバッファを作成 - * Create a named uniform buffer - * @param {string} name - バッファ名 - * @param {number} size - バッファサイズ(バイト単位) - * @return {GPUBuffer} 作成されたユニフォームバッファ - */ - createUniformBuffer (name: string, size: number): GPUBuffer - { - const buffer = this.device.createBuffer({ - "size": size, - "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST - }); - - this.uniformBuffers.set(name, buffer); - return buffer; - } - - /** - * @description 名前付きユニフォームバッファのデータを更新 - * Update data of a named uniform buffer - * @param {string} name - バッファ名 - * @param {Float32Array} data - 書き込むデータ - * @return {void} - */ - updateUniformBuffer (name: string, data: Float32Array): void - { - const buffer = this.uniformBuffers.get(name); - if (buffer) { - this.device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, data.byteLength); - } - } - - /** - * @description 名前で頂点バッファを取得 - * Get vertex buffer by name - * @param {string} name - バッファ名 - * @return {GPUBuffer | undefined} 頂点バッファ、存在しない場合はundefined - */ - getVertexBuffer (name: string): GPUBuffer | undefined - { - return this.vertexBuffers.get(name); - } - - /** - * @description 名前でユニフォームバッファを取得 - * Get uniform buffer by name - * @param {string} name - バッファ名 - * @return {GPUBuffer | undefined} ユニフォームバッファ、存在しない場合はundefined - */ - getUniformBuffer (name: string): GPUBuffer | undefined - { - return this.uniformBuffers.get(name); - } - /** * @description 矩形の頂点データを作成 * Create rect vertices data @@ -311,17 +227,6 @@ export class BufferManager return buffer; } - /** - * @description 頂点バッファをプールに返却 - * Release vertex buffer back to pool - * @param {GPUBuffer} buffer - 返却するバッファ - * @return {void} - */ - releaseVertexBuffer (buffer: GPUBuffer): void - { - bufferManagerReleaseVertexBufferService(this.vertexBufferBuckets, buffer); - } - /** * @description プールからユニフォームバッファを取得(または新規作成) * Acquire uniform buffer from pool or create new one @@ -354,38 +259,6 @@ export class BufferManager return buffer; } - /** - * @description ユニフォームバッファをプールに返却 - * Release uniform buffer back to pool - * @param {GPUBuffer} buffer - 返却するバッファ - * @return {void} - */ - releaseUniformBuffer (buffer: GPUBuffer): void - { - bufferManagerReleaseUniformBufferService(this.uniformBufferBuckets, buffer); - } - - /** - * @description 名前指定でバッファを破棄(頂点・ユニフォーム両方) - * Destroy buffer by name (both vertex and uniform) - * @param {string} name - バッファ名 - * @return {void} - */ - destroyBuffer (name: string): void - { - const vertexBuffer = this.vertexBuffers.get(name); - if (vertexBuffer) { - vertexBuffer.destroy(); - this.vertexBuffers.delete(name); - } - - const uniformBuffer = this.uniformBuffers.get(name); - if (uniformBuffer) { - uniformBuffer.destroy(); - this.uniformBuffers.delete(name); - } - } - /** * @description 全バッファを破棄してリソースを解放 * Dispose all buffers and release resources @@ -393,16 +266,6 @@ export class BufferManager */ dispose (): void { - for (const buffer of this.vertexBuffers.values()) { - buffer.destroy(); - } - this.vertexBuffers.clear(); - - for (const buffer of this.uniformBuffers.values()) { - buffer.destroy(); - } - this.uniformBuffers.clear(); - for (const bucket of this.vertexBufferBuckets.values()) { for (const buffer of bucket) { buffer.destroy(); @@ -422,11 +285,6 @@ export class BufferManager } this.storageBufferPool = []; - if (this.indirectBuffer) { - this.indirectBuffer.destroy(); - this.indirectBuffer = null; - } - for (const buffer of this.indirectBufferPool) { buffer.destroy(); } @@ -455,16 +313,6 @@ export class BufferManager */ clearFrameBuffers (): void { - for (const buffer of this.vertexBuffers.values()) { - buffer.destroy(); - } - this.vertexBuffers.clear(); - - for (const buffer of this.uniformBuffers.values()) { - buffer.destroy(); - } - this.uniformBuffers.clear(); - // フレーム内で取得したプールバッファをプールに返却 for (const buffer of this.frameVertexPoolBuffers) { bufferManagerReleaseVertexBufferService(this.vertexBufferBuckets, buffer); @@ -521,17 +369,6 @@ export class BufferManager ); } - /** - * @description Storage Bufferをプールに返却 - * Release storage buffer back to pool - * @param {GPUBuffer} buffer - 返却するバッファ - * @return {void} - */ - releaseStorageBuffer (buffer: GPUBuffer): void - { - releaseStorageBufferUseCase(this.storageBufferPool, buffer); - } - /** * @description Storage Bufferにデータを書き込む * Write data to storage buffer @@ -544,42 +381,6 @@ export class BufferManager this.device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, data.byteLength); } - /** - * @description Indirect Bufferを取得または作成(シングルトン) - * Get or create indirect buffer (singleton) - * @param {number} vertex_count - 頂点数 - * @param {number} instance_count - インスタンス数 - * @param {number} first_vertex - 開始頂点インデックス - * @param {number} first_instance - 開始インスタンスインデックス - * @return {GPUBuffer} Indirect Buffer - */ - getOrCreateIndirectBuffer ( - vertex_count: number, - instance_count: number, - first_vertex: number = 0, - first_instance: number = 0 - ): GPUBuffer { - if (!this.indirectBuffer) { - this.indirectBuffer = bufferManagerCreateIndirectBufferService( - this.device, - vertex_count, - instance_count, - first_vertex, - first_instance - ); - } else { - updateIndirectBuffer( - this.device, - this.indirectBuffer, - vertex_count, - instance_count, - first_vertex, - first_instance - ); - } - return this.indirectBuffer; - } - /** * @description 新しいIndirect Bufferを作成(プールから再利用または新規作成) * Create new indirect buffer (reuse from pool or create new) @@ -638,14 +439,4 @@ export class BufferManager return this.unitRectBuffer; } - /** - * @description 現在のフレーム番号を取得 - * Get current frame number - * @return {number} フレーム番号 - */ - getFrameNumber (): number - { - return this.frameNumber; - } - } diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase.test.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase.test.ts index 2801ffc0..881f9766 100644 --- a/packages/webgpu/src/BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase.test.ts +++ b/packages/webgpu/src/BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase.test.ts @@ -24,9 +24,21 @@ vi.mock("../service/BufferManagerCreateStorageBufferService", () => ({ })); import { execute } from "./BufferManagerAcquireStorageBufferUseCase"; -import { execute as releaseStorageBuffer } from "./BufferManagerReleaseStorageBufferUseCase"; import { execute as cleanupStorageBuffers } from "./BufferManagerCleanupStorageBuffersUseCase"; +/** + * @description テスト用ヘルパー: Storage Bufferをプールに返却 + * Test helper: Release storage buffer back to pool + */ +const releaseStorageBuffer = (pool: IPooledStorageBuffer[], buffer: GPUBuffer): void => { + for (const entry of pool) { + if (entry.buffer === buffer) { + entry.inUse = false; + return; + } + } +}; + describe("BufferManagerAcquireStorageBufferUseCase", () => { let mockDevice: GPUDevice; @@ -95,26 +107,6 @@ describe("BufferManagerAcquireStorageBufferUseCase", () => }); }); - describe("releaseStorageBuffer", () => - { - it("should mark buffer as not in use", () => - { - const buffer = execute(mockDevice, pool, 1024, 0); - expect(pool[0].inUse).toBe(true); - - releaseStorageBuffer(pool, buffer); - expect(pool[0].inUse).toBe(false); - }); - - it("should handle buffer not in pool", () => - { - const fakeBuffer = {} as GPUBuffer; - - // Should not throw - expect(() => releaseStorageBuffer(pool, fakeBuffer)).not.toThrow(); - }); - }); - describe("cleanupStorageBuffers", () => { it("should remove old unused buffers", () => diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.test.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.test.ts deleted file mode 100644 index f483a1dc..00000000 --- a/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { execute } from "./BufferManagerReleaseStorageBufferUseCase"; -import { describe, expect, it } from "vitest"; - -describe("BufferManagerReleaseStorageBufferUseCase.js test", () => { - - it("execute test case1 - should mark matching buffer as not in use", () => - { - const targetBuffer = { "label": "target" } as unknown as GPUBuffer; - const pool = [ - { "buffer": { "label": "other" } as unknown as GPUBuffer, "inUse": true, "lastUsedFrame": 0, "size": 256 }, - { "buffer": targetBuffer, "inUse": true, "lastUsedFrame": 0, "size": 256 } - ] as any; - - execute(pool, targetBuffer); - - expect(pool[0].inUse).toBe(true); - expect(pool[1].inUse).toBe(false); - }); - - it("execute test case2 - should do nothing if buffer not found", () => - { - const targetBuffer = { "label": "target" } as unknown as GPUBuffer; - const pool = [ - { "buffer": { "label": "other" } as unknown as GPUBuffer, "inUse": true, "lastUsedFrame": 0, "size": 256 } - ] as any; - - execute(pool, targetBuffer); - - expect(pool[0].inUse).toBe(true); - }); - - it("execute test case3 - empty pool", () => - { - const targetBuffer = {} as unknown as GPUBuffer; - const pool: any[] = []; - - execute(pool, targetBuffer); - - expect(pool.length).toBe(0); - }); - - it("execute test case4 - should release first matching buffer only", () => - { - const targetBuffer = {} as unknown as GPUBuffer; - const pool = [ - { "buffer": targetBuffer, "inUse": true, "lastUsedFrame": 0, "size": 256 }, - { "buffer": targetBuffer, "inUse": true, "lastUsedFrame": 0, "size": 512 } - ] as any; - - execute(pool, targetBuffer); - - expect(pool[0].inUse).toBe(false); - expect(pool[1].inUse).toBe(true); - }); -}); diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.ts deleted file mode 100644 index 02734f59..00000000 --- a/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { IPooledStorageBuffer } from "../../interface/IStorageBufferConfig"; - -/** - * @description Storage Bufferをプールに返却 - * Release Storage Buffer back to pool - * - * @param {IPooledStorageBuffer[]} pool - Storage Bufferプール - * @param {GPUBuffer} buffer - 返却するバッファ - * @return {void} - */ -export const execute = ( - pool: IPooledStorageBuffer[], - buffer: GPUBuffer -): void => { - - for (const entry of pool) { - if (entry.buffer === buffer) { - entry.inUse = false; - return; - } - } -}; diff --git a/packages/webgpu/src/Context.test.ts b/packages/webgpu/src/Context.test.ts index a3bc7133..1dd90849 100644 --- a/packages/webgpu/src/Context.test.ts +++ b/packages/webgpu/src/Context.test.ts @@ -211,15 +211,6 @@ describe("Context", () => }); }); - describe("clearRect", () => - { - it("should be a no-op in WebGPU (clear happens at render pass start)", () => - { - // clearRect does nothing in WebGPU - clear is done at render pass start - expect(() => context.clearRect(0, 0, 100, 100)).not.toThrow(); - }); - }); - describe("fillStyle", () => { it("should update fill style when set", () => @@ -422,7 +413,7 @@ describe("Context", () => vi.spyOn(context["pipelineManager"], "getPipeline").mockReturnValue({} as GPURenderPipeline); // Mock buffer manager - vi.spyOn(context["bufferManager"], "createVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); + vi.spyOn(context["bufferManager"], "acquireVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); const mockNode = { "x": 100, "y": 200, "w": 50, "h": 30 }; @@ -464,7 +455,7 @@ describe("Context", () => "depthStencilAttachment": { "view": {}, "stencilLoadOp": "clear", "stencilStoreOp": "store" } } as unknown as GPURenderPassDescriptor); vi.spyOn(context["pipelineManager"], "getPipeline").mockReturnValue({} as GPURenderPipeline); - vi.spyOn(context["bufferManager"], "createVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); + vi.spyOn(context["bufferManager"], "acquireVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); const mockNode = { "x": 0, "y": 0, "w": 100, "h": 100 }; @@ -502,7 +493,7 @@ describe("Context", () => "depthStencilAttachment": { "view": {}, "stencilLoadOp": "clear", "stencilStoreOp": "store" } } as unknown as GPURenderPassDescriptor); vi.spyOn(context["pipelineManager"], "getPipeline").mockReturnValue({} as GPURenderPipeline); - vi.spyOn(context["bufferManager"], "createVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); + vi.spyOn(context["bufferManager"], "acquireVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); const mockNode = { "x": 0, "y": 0, "w": 10, "h": 10 }; const mockPixels = new Uint8Array(10 * 10 * 4); @@ -559,7 +550,7 @@ describe("Context", () => "depthStencilAttachment": { "view": {}, "stencilLoadOp": "clear", "stencilStoreOp": "store" } } as unknown as GPURenderPassDescriptor); vi.spyOn(context["pipelineManager"], "getPipeline").mockReturnValue({} as GPURenderPipeline); - vi.spyOn(context["bufferManager"], "createVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); + vi.spyOn(context["bufferManager"], "acquireVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); // Mock copyExternalImageToTexture mockQueue.copyExternalImageToTexture = vi.fn(); diff --git a/packages/webgpu/src/Context.ts b/packages/webgpu/src/Context.ts index 68693292..cb62f365 100644 --- a/packages/webgpu/src/Context.ts +++ b/packages/webgpu/src/Context.ts @@ -9,7 +9,6 @@ import { PathCommand } from "./PathCommand"; import { BufferManager } from "./BufferManager"; import { TextureManager } from "./TextureManager"; import { FrameBufferManager } from "./FrameBufferManager"; -import { AttachmentManager } from "./AttachmentManager"; import { PipelineManager } from "./Shader/PipelineManager"; import { $rootNodes, @@ -274,8 +273,6 @@ export class Context private frameBufferManager: FrameBufferManager; /** @description パイプラインマネージャー / Pipeline manager */ private pipelineManager: PipelineManager; - /** @description アタッチメントマネージャー / Attachment manager */ - private attachmentManager: AttachmentManager; /** @description 新しい描画状態フラグ / New draw state flag */ public newDrawState: boolean = false; @@ -403,7 +400,6 @@ export class Context this.pipelineManager = new PipelineManager(device, preferred_format); // 遅延パイプライン群を即座に先行作成(初回アクセス時のレイテンシ解消) this.pipelineManager.preloadLazyGroups(); - this.attachmentManager = new AttachmentManager(device); // グラデーションLUT共有アタッチメントにGPUDeviceを設定 $setGradientLUTDevice(device); @@ -638,23 +634,6 @@ export class Context this.bind(this.$mainAttachmentObject); } - /** - * @description 指定範囲をクリアする - * Clear the specified rectangle area - * - * @param {number} _x - X座標 / X coordinate - * @param {number} _y - Y座標 / Y coordinate - * @param {number} _w - 幅 / Width - * @param {number} _h - 高さ / Height - * @return {void} - */ - clearRect (_x: number, _y: number, _w: number, _h: number): void - { - // WebGPU clear rect implementation - // WebGPUではclearはレンダーパス開始時に行うため、ここでは何もしない - // 実際のクリアはbeginNodeRenderingやbeginFrameで行われる - } - /** * @description 現在の2D変換行列を保存 * Save the current 2D transform matrix @@ -1222,67 +1201,6 @@ export class Context ); } - /** - * @description オフスクリーンアタッチメントにバインド - * Bind to an offscreen attachment - * WebGL: FrameBufferManagerBindAttachmentObjectService - * - * @param {IAttachmentObject} attachment - バインドするアタッチメント / Attachment to bind - * @return {void} - */ - bindAttachment(attachment: IAttachmentObject): void - { - this.attachmentManager.bindAttachment(attachment); - - // 現在のレンダーターゲットをオフスクリーンに切り替え - // color?.view または texture?.view を使用 - const view = attachment.color?.view ?? attachment.texture?.view; - if (view) { - this.currentRenderTarget = view; - } - } - - /** - * @description メインキャンバスにバインド - * Bind to the main canvas - * WebGL: FrameBufferManagerUnBindAttachmentObjectService - * - * @return {void} - */ - unbindAttachment(): void - { - this.attachmentManager.unbindAttachment(); - this.currentRenderTarget = null; - } - - /** - * @description アタッチメントオブジェクトを取得 - * Get an attachment object - * WebGL: FrameBufferManagerGetAttachmentObjectUseCase - * - * @param {number} width - 幅 / Width - * @param {number} height - 高さ / Height - * @param {boolean} msaa - MSAA有効化フラグ / MSAA enable flag - * @return {IAttachmentObject} アタッチメントオブジェクト / Attachment object - */ - getAttachmentObject(width: number, height: number, msaa: boolean = false): IAttachmentObject - { - return this.attachmentManager.getAttachmentObject(width, height, msaa); - } - - /** - * @description アタッチメントオブジェクトを解放 - * Release an attachment object - * WebGL: FrameBufferManagerReleaseAttachmentObjectUseCase - * - * @param {IAttachmentObject} attachment - 解放するアタッチメント / Attachment to release - * @return {void} - */ - releaseAttachment(attachment: IAttachmentObject): void - { - this.attachmentManager.releaseAttachment(attachment); - } - /** * @description 線の描画を実行(WebGL版と同じ仕様) * Execute stroke drawing (same specification as WebGL version) @@ -2035,29 +1953,6 @@ export class Context this.processComplexBlendQueue(); } - /** - * @description 最適化インスタンス描画の有効/無効を設定 - * Enable or disable optimized instancing - * - * @param {boolean} enabled - 有効にするか / Whether to enable - * @return {void} - */ - setOptimizedInstancing (enabled: boolean): void - { - this.useOptimizedInstancing = enabled; - } - - /** - * @description 最適化インスタンス描画が有効かどうか - * Whether optimized instancing is enabled - * - * @return {boolean} 有効ならtrue / True if enabled - */ - isOptimizedInstancingEnabled (): boolean - { - return this.useOptimizedInstancing; - } - /** * @description 複雑なブレンドモードのキューを処理 * Process the complex blend mode queue diff --git a/packages/webgpu/src/FrameBufferManager.test.ts b/packages/webgpu/src/FrameBufferManager.test.ts index c621d6cd..f5cccf76 100644 --- a/packages/webgpu/src/FrameBufferManager.test.ts +++ b/packages/webgpu/src/FrameBufferManager.test.ts @@ -356,22 +356,6 @@ describe("FrameBufferManager", () => }); }); - describe("resizeAttachment", () => - { - it("should destroy old and create new attachment", () => - { - const device = createMockDevice(); - const manager = new FrameBufferManager(device, "bgra8unorm"); - const oldAttachment = manager.createAttachment("test", 100, 100); - - const newAttachment = manager.resizeAttachment("test", 200, 200); - - expect(oldAttachment.texture!.resource.destroy).toHaveBeenCalled(); - expect(newAttachment.width).toBe(200); - expect(newAttachment.height).toBe(200); - }); - }); - describe("createTemporaryAttachment", () => { it("should create temporary attachment", () => diff --git a/packages/webgpu/src/FrameBufferManager.ts b/packages/webgpu/src/FrameBufferManager.ts index 09417273..f99bf029 100644 --- a/packages/webgpu/src/FrameBufferManager.ts +++ b/packages/webgpu/src/FrameBufferManager.ts @@ -179,20 +179,6 @@ export class FrameBufferManager } } - /** - * @description アタッチメントをリサイズする - * Resize an attachment - * @param {string} name - アタッチメント名 / Attachment name - * @param {number} width - 新しい幅 / New width - * @param {number} height - 新しい高さ / New height - * @return {IAttachmentObject} - */ - resizeAttachment(name: string, width: number, height: number): IAttachmentObject - { - this.destroyAttachment(name); - return this.createAttachment(name, width, height); - } - /** * @description 一時的なアタッチメントを作成する * Create a temporary attachment diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.test.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.test.ts deleted file mode 100644 index d8d3922d..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { execute } from "./GradientLUTCalculateResolutionService"; -import { describe, expect, it } from "vitest"; - -describe("GradientLUTCalculateResolutionService.ts method test", () => -{ - it("test case - 2 stops returns 64", () => - { - const result = execute(2); - - expect(result).toBe(64); - }); - - it("test case - 3 stops returns 128", () => - { - const result = execute(3); - - expect(result).toBe(128); - }); - - it("test case - 4 stops returns 128", () => - { - const result = execute(4); - - expect(result).toBe(128); - }); - - it("test case - 5 stops returns 256", () => - { - const result = execute(5); - - expect(result).toBe(256); - }); - - it("test case - 8 stops returns 256", () => - { - const result = execute(8); - - expect(result).toBe(256); - }); - - it("test case - 9 stops returns 512", () => - { - const result = execute(9); - - expect(result).toBe(512); - }); - - it("test case - respects minResolution parameter", () => - { - const result = execute(2, 128); - - expect(result).toBe(128); - }); - - it("test case - respects maxResolution parameter", () => - { - const result = execute(10, 64, 256); - - expect(result).toBe(256); - }); -}); diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.ts deleted file mode 100644 index d8eb9d77..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @description グラデーションストップ数に応じた適応的解像度を計算 - * Calculate adaptive resolution based on number of gradient stops - * - * @param {number} stops_count - グラデーションストップの数 - * @param {number} [min_resolution=64] - 最小解像度 - * @param {number} [max_resolution=512] - 最大解像度 - * @return {number} - * @method - * @protected - */ -export const execute = ( - stops_count: number, - min_resolution: number = 64, - max_resolution: number = 512 -): number => { - - // ストップ数に応じて解像度を調整 - // 2ストップ: 64px - // 3-4ストップ: 128px - // 5-8ストップ: 256px - // 9以上: 512px - if (stops_count <= 2) { - return Math.max(min_resolution, 64); - } - - if (stops_count <= 4) { - return Math.min(max_resolution, Math.max(min_resolution, 128)); - } - - if (stops_count <= 8) { - return Math.min(max_resolution, Math.max(min_resolution, 256)); - } - - return max_resolution; -}; diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTGeneratePixelsService.test.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTGeneratePixelsService.test.ts deleted file mode 100644 index d4f808f0..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTGeneratePixelsService.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { execute } from "./GradientLUTGeneratePixelsService"; -import type { IGradientStop } from "./GradientLUTParseStopsService"; -import { describe, expect, it } from "vitest"; - -describe("GradientLUTGeneratePixelsService.ts method test", () => -{ - it("test case - empty stops returns empty array", () => - { - const stops: IGradientStop[] = []; - - const result = execute(stops, 64, 0); - - expect(result.length).toBe(64 * 4); - expect(result.every(v => v === 0)).toBe(true); - }); - - it("test case - single stop fills with same color", () => - { - const stops: IGradientStop[] = [ - { ratio: 0.5, r: 1, g: 0, b: 0, a: 1 } - ]; - - const result = execute(stops, 4, 0); - - expect(result.length).toBe(16); - // 全ピクセルが同じ色(赤) - for (let i = 0; i < 4; i++) { - expect(result[i * 4]).toBe(255); // r - expect(result[i * 4 + 1]).toBe(0); // g - expect(result[i * 4 + 2]).toBe(0); // b - expect(result[i * 4 + 3]).toBe(255); // a - } - }); - - it("test case - two stops gradient from red to blue", () => - { - const stops: IGradientStop[] = [ - { ratio: 0, r: 1, g: 0, b: 0, a: 1 }, - { ratio: 1, r: 0, g: 0, b: 1, a: 1 } - ]; - - const result = execute(stops, 3, 0); - - // ピクセル0: 赤 - expect(result[0]).toBe(255); - expect(result[1]).toBe(0); - expect(result[2]).toBe(0); - - // ピクセル1: 紫(中間) - expect(result[4]).toBe(128); - expect(result[5]).toBe(0); - expect(result[6]).toBe(128); - - // ピクセル2: 青 - expect(result[8]).toBe(0); - expect(result[9]).toBe(0); - expect(result[10]).toBe(255); - }); - - it("test case - alpha channel interpolation", () => - { - const stops: IGradientStop[] = [ - { ratio: 0, r: 1, g: 1, b: 1, a: 0 }, - { ratio: 1, r: 1, g: 1, b: 1, a: 1 } - ]; - - const result = execute(stops, 3, 0); - - // ピクセル0: alpha=0 - expect(result[3]).toBe(0); - - // ピクセル1: alpha=0.5 - expect(result[7]).toBe(128); - - // ピクセル2: alpha=1 - expect(result[11]).toBe(255); - }); - - it("test case - three stops gradient", () => - { - const stops: IGradientStop[] = [ - { ratio: 0, r: 1, g: 0, b: 0, a: 1 }, // 赤 - { ratio: 0.5, r: 0, g: 1, b: 0, a: 1 }, // 緑 - { ratio: 1, r: 0, g: 0, b: 1, a: 1 } // 青 - ]; - - const result = execute(stops, 5, 0); - - // ピクセル0: 赤 - expect(result[0]).toBe(255); - expect(result[1]).toBe(0); - expect(result[2]).toBe(0); - - // ピクセル2: 緑 - expect(result[8]).toBe(0); - expect(result[9]).toBe(255); - expect(result[10]).toBe(0); - - // ピクセル4: 青 - expect(result[16]).toBe(0); - expect(result[17]).toBe(0); - expect(result[18]).toBe(255); - }); -}); diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTGeneratePixelsService.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTGeneratePixelsService.ts deleted file mode 100644 index 30dea70a..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTGeneratePixelsService.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { IGradientStop } from "../../../interface/IGradientStop"; -import { execute as gradientLUTInterpolateColorService } from "./GradientLUTInterpolateColorService"; - -/** - * @description グラデーションLUTのピクセルデータを生成 - * Generate pixel data for gradient LUT - * - * @param {IGradientStop[]} stops - ソート済みのグラデーションストップ - * @param {number} resolution - LUTの解像度(ピクセル数) - * @param {number} interpolation - 0: RGB, 1: Linear RGB - * @return {Uint8Array} - * @method - * @protected - */ -export const execute = ( - stops: IGradientStop[], - resolution: number, - interpolation: number -): Uint8Array => { - - const pixels = new Uint8Array(resolution * 4); - - if (stops.length === 0) { - return pixels; - } - - if (stops.length === 1) { - // 単一ストップの場合は全体を同じ色で塗る - const stop = stops[0]; - for (let i = 0; i < resolution; i++) { - const offset = i * 4; - pixels[offset] = Math.round(stop.r * 255); - pixels[offset + 1] = Math.round(stop.g * 255); - pixels[offset + 2] = Math.round(stop.b * 255); - pixels[offset + 3] = Math.round(stop.a * 255); - } - return pixels; - } - - for (let i = 0; i < resolution; i++) { - - const ratio = i / (resolution - 1); - - // 該当するストップ区間を見つける - let startStopIndex = 0; - for (let j = 0; j < stops.length - 1; j++) { - if (ratio >= stops[j].ratio && ratio <= stops[j + 1].ratio) { - startStopIndex = j; - break; - } - if (ratio > stops[j + 1].ratio) { - startStopIndex = j + 1; - } - } - - const startStop = stops[startStopIndex]; - const endStop = stops[Math.min(startStopIndex + 1, stops.length - 1)]; - - // 区間内での補間係数を計算 - let t = 0; - const rangeWidth = endStop.ratio - startStop.ratio; - if (rangeWidth > 0) { - t = (ratio - startStop.ratio) / rangeWidth; - t = Math.max(0, Math.min(1, t)); - } - - // 色を補間 - const color = gradientLUTInterpolateColorService( - startStop, endStop, t, interpolation - ); - - const offset = i * 4; - pixels[offset] = Math.round(color.r * 255); - pixels[offset + 1] = Math.round(color.g * 255); - pixels[offset + 2] = Math.round(color.b * 255); - pixels[offset + 3] = Math.round(color.a * 255); - } - - return pixels; -}; diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.test.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.test.ts deleted file mode 100644 index 6a54bb51..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { execute } from "./GradientLUTInterpolateColorService"; -import type { IGradientStop } from "./GradientLUTParseStopsService"; -import { describe, expect, it } from "vitest"; - -describe("GradientLUTInterpolateColorService.ts method test", () => -{ - it("test case - interpolate at t=0 returns start color (RGB mode)", () => - { - const startStop: IGradientStop = { ratio: 0, r: 1, g: 0, b: 0, a: 1 }; - const endStop: IGradientStop = { ratio: 1, r: 0, g: 0, b: 1, a: 1 }; - - const result = execute(startStop, endStop, 0, 0); - - expect(result.r).toBe(1); - expect(result.g).toBe(0); - expect(result.b).toBe(0); - expect(result.a).toBe(1); - }); - - it("test case - interpolate at t=1 returns end color (RGB mode)", () => - { - const startStop: IGradientStop = { ratio: 0, r: 1, g: 0, b: 0, a: 1 }; - const endStop: IGradientStop = { ratio: 1, r: 0, g: 0, b: 1, a: 1 }; - - const result = execute(startStop, endStop, 1, 0); - - expect(result.r).toBe(0); - expect(result.g).toBe(0); - expect(result.b).toBe(1); - expect(result.a).toBe(1); - }); - - it("test case - interpolate at t=0.5 returns midpoint (RGB mode)", () => - { - const startStop: IGradientStop = { ratio: 0, r: 0, g: 0, b: 0, a: 0 }; - const endStop: IGradientStop = { ratio: 1, r: 1, g: 1, b: 1, a: 1 }; - - const result = execute(startStop, endStop, 0.5, 0); - - expect(result.r).toBe(0.5); - expect(result.g).toBe(0.5); - expect(result.b).toBe(0.5); - expect(result.a).toBe(0.5); - }); - - it("test case - interpolate with Linear RGB mode (interpolation=1)", () => - { - const startStop: IGradientStop = { ratio: 0, r: 0, g: 0, b: 0, a: 0 }; - const endStop: IGradientStop = { ratio: 1, r: 1, g: 1, b: 1, a: 1 }; - - const result = execute(startStop, endStop, 0.5, 1); - - // Linear RGB補間では結果が異なる - expect(result.r).toBeGreaterThan(0); - expect(result.r).toBeLessThan(1); - expect(result.a).toBe(0.5); // アルファは常に線形補間 - }); - - it("test case - alpha is always linearly interpolated", () => - { - const startStop: IGradientStop = { ratio: 0, r: 1, g: 1, b: 1, a: 0 }; - const endStop: IGradientStop = { ratio: 1, r: 1, g: 1, b: 1, a: 1 }; - - const resultRGB = execute(startStop, endStop, 0.5, 0); - const resultLinear = execute(startStop, endStop, 0.5, 1); - - expect(resultRGB.a).toBe(0.5); - expect(resultLinear.a).toBe(0.5); - }); -}); diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.ts deleted file mode 100644 index 56b0a5df..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { IGradientStop } from "../../../interface/IGradientStop"; - -/** - * @description 2つのストップ間で色を補間 - * Interpolate color between two stops - * - * @param {IGradientStop} start_stop - 開始ストップ - * @param {IGradientStop} end_stop - 終了ストップ - * @param {number} t - 補間係数 (0-1) - * @param {number} interpolation - 0: RGB, 1: Linear RGB - * @return {{ r: number, g: number, b: number, a: number }} - * @method - * @protected - */ -export const execute = ( - start_stop: IGradientStop, - end_stop: IGradientStop, - t: number, - interpolation: number -): { r: number; g: number; b: number; a: number } => { - - let r: number; - let g: number; - let b: number; - - if (interpolation === 1) { - // Linear RGB補間(ガンマ補正あり) - const sr = Math.pow(start_stop.r, 2.2); - const sg = Math.pow(start_stop.g, 2.2); - const sb = Math.pow(start_stop.b, 2.2); - const er = Math.pow(end_stop.r, 2.2); - const eg = Math.pow(end_stop.g, 2.2); - const eb = Math.pow(end_stop.b, 2.2); - - r = Math.pow(sr + (er - sr) * t, 1 / 2.2); - g = Math.pow(sg + (eg - sg) * t, 1 / 2.2); - b = Math.pow(sb + (eb - sb) * t, 1 / 2.2); - } else { - // 通常のRGB補間 - r = start_stop.r + (end_stop.r - start_stop.r) * t; - g = start_stop.g + (end_stop.g - start_stop.g) * t; - b = start_stop.b + (end_stop.b - start_stop.b) * t; - } - - const a = start_stop.a + (end_stop.a - start_stop.a) * t; - - return { r, g, b, a }; -}; diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTParseStopsService.test.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTParseStopsService.test.ts deleted file mode 100644 index ffa725a9..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTParseStopsService.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { execute } from "./GradientLUTParseStopsService"; -import { describe, expect, it } from "vitest"; - -describe("GradientLUTParseStopsService.ts method test", () => -{ - it("test case - parse single stop", () => - { - const stops = [0.5, 1, 0, 0, 1]; // ratio=0.5, r=1, g=0, b=0, a=1 - - const result = execute(stops); - - expect(result.length).toBe(1); - expect(result[0].ratio).toBe(0.5); - expect(result[0].r).toBe(1); - expect(result[0].g).toBe(0); - expect(result[0].b).toBe(0); - expect(result[0].a).toBe(1); - }); - - it("test case - parse multiple stops", () => - { - const stops = [ - 0, 1, 0, 0, 1, // ratio=0, red - 1, 0, 0, 1, 1 // ratio=1, blue - ]; - - const result = execute(stops); - - expect(result.length).toBe(2); - expect(result[0].ratio).toBe(0); - expect(result[1].ratio).toBe(1); - }); - - it("test case - sorts stops by ratio", () => - { - const stops = [ - 1, 0, 0, 1, 1, // ratio=1 - 0.5, 0, 1, 0, 1, // ratio=0.5 - 0, 1, 0, 0, 1 // ratio=0 - ]; - - const result = execute(stops); - - expect(result.length).toBe(3); - expect(result[0].ratio).toBe(0); - expect(result[1].ratio).toBe(0.5); - expect(result[2].ratio).toBe(1); - }); - - it("test case - empty stops", () => - { - const stops: number[] = []; - - const result = execute(stops); - - expect(result.length).toBe(0); - }); -}); diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTParseStopsService.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTParseStopsService.ts deleted file mode 100644 index 6f62628b..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTParseStopsService.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { IGradientStop } from "../../../interface/IGradientStop"; - -/** - * @description グラデーションストップ配列をパースしてソート - * Parse and sort gradient stops array - * - * @param {number[]} stops - [ratio, r, g, b, a, ratio, r, g, b, a, ...] - * @return {IGradientStop[]} - * @method - * @protected - */ -export const execute = (stops: number[]): IGradientStop[] => -{ - const gradientStops: IGradientStop[] = []; - - for (let i = 0; i < stops.length; i += 5) { - gradientStops.push({ - "ratio": stops[i], - "r": stops[i + 1], - "g": stops[i + 2], - "b": stops[i + 3], - "a": stops[i + 4] - }); - } - - // ストップポイントをratio順にソート - gradientStops.sort((a, b) => a.ratio - b.ratio); - - return gradientStops; -}; diff --git a/packages/webgpu/src/Shader/ShaderSource.test.ts b/packages/webgpu/src/Shader/ShaderSource.test.ts index a6ef2a89..0fa42ca8 100644 --- a/packages/webgpu/src/Shader/ShaderSource.test.ts +++ b/packages/webgpu/src/Shader/ShaderSource.test.ts @@ -349,24 +349,6 @@ describe("ShaderSource", () => }); }); - describe("getGradientFragmentShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = ShaderSource.getGradientFragmentShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = ShaderSource.getGradientFragmentShader(); - - expect(shader).toContain("@fragment"); - }); - }); - describe("getBitmapFillVertexShader", () => { it("should return a valid WGSL vertex shader string", () => @@ -730,46 +712,6 @@ describe("ShaderSource", () => }); }); - describe("getComplexBlendFragmentShader", () => - { - it("should return a valid WGSL fragment shader (unified)", () => - { - const shader = ShaderSource.getComplexBlendFragmentShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = ShaderSource.getComplexBlendFragmentShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should include blend function", () => - { - const shader = ShaderSource.getComplexBlendFragmentShader(); - - expect(shader).toContain("fn blend"); - }); - - it("should support step-based blend modes (lighten/darken)", () => - { - const shader = ShaderSource.getComplexBlendFragmentShader(); - - expect(shader).toContain("step(srcRgb, dstRgb)"); - expect(shader).toContain("step(dstRgb, srcRgb)"); - }); - - it("should include blendMode uniform", () => - { - const shader = ShaderSource.getComplexBlendFragmentShader(); - - expect(shader).toContain("blendMode"); - }); - }); - describe("getDisplacementMapFilterFragmentShader", () => { it("should return a valid WGSL fragment shader string", () => @@ -931,7 +873,6 @@ describe("ShaderSource", () => { name: "getTextureFragmentShader", fn: () => ShaderSource.getTextureFragmentShader() }, { name: "getInstancedFragmentShader", fn: () => ShaderSource.getInstancedFragmentShader() }, { name: "getGradientFillFragmentShader", fn: () => ShaderSource.getGradientFillFragmentShader() }, - { name: "getGradientFragmentShader", fn: () => ShaderSource.getGradientFragmentShader() }, { name: "getBitmapFillFragmentShader", fn: () => ShaderSource.getBitmapFillFragmentShader() }, { name: "getBlendFragmentShader", fn: () => ShaderSource.getBlendFragmentShader() }, { name: "getTextureCopyFragmentShader", fn: () => ShaderSource.getTextureCopyFragmentShader() }, diff --git a/packages/webgpu/src/Shader/ShaderSource.ts b/packages/webgpu/src/Shader/ShaderSource.ts index 15106b0a..094c55fd 100644 --- a/packages/webgpu/src/Shader/ShaderSource.ts +++ b/packages/webgpu/src/Shader/ShaderSource.ts @@ -12,7 +12,7 @@ import { StencilWriteFragment, StencilFillFragment } from "./wgsl/fragment/Stenc import { MaskFragment } from "./wgsl/fragment/MaskFragment"; import { BasicFragment, TextureFragment } from "./wgsl/fragment/BasicFragment"; import { InstancedFragment } from "./wgsl/fragment/InstancedFragment"; -import { GradientFillFragment, GradientFillStencilFragment, GradientFragment } from "./wgsl/fragment/GradientFragment"; +import { GradientFillFragment, GradientFillStencilFragment } from "./wgsl/fragment/GradientFragment"; import { BitmapFillFragment } from "./wgsl/fragment/BitmapFragment"; import { TextureCopyFragment, @@ -218,17 +218,6 @@ export class ShaderSource return GradientFillStencilFragment; } - /** - * @description グラデーションフラグメントシェーダーを取得する - * Get gradient fragment shader - * - * @return {string} - */ - static getGradientFragmentShader (): string - { - return GradientFragment; - } - /** * @description ビットマップ塗り用頂点シェーダーを取得する * Get bitmap fill vertex shader @@ -586,17 +575,6 @@ fn fs_main(fragInput: VertexOutput) -> @location(0) vec4 { `; } - /** - * @description 複合ブレンドフラグメントシェーダーを取得する - * Get complex blend fragment shader - * - * @return {string} - */ - static getComplexBlendFragmentShader (): string - { - return ShaderSource.getUnifiedComplexBlendFragmentShader(); - } - /** * @description ブレンドモード名からインデックスを取得する * Get blend mode index from blend mode name diff --git a/packages/webgpu/src/TextureManager.test.ts b/packages/webgpu/src/TextureManager.test.ts index 3bdc1914..3b4ccbc2 100644 --- a/packages/webgpu/src/TextureManager.test.ts +++ b/packages/webgpu/src/TextureManager.test.ts @@ -9,7 +9,7 @@ const GPUTextureUsage = { }; (globalThis as any).GPUTextureUsage = GPUTextureUsage; -// Mock service and usecase modules +// Mock service module vi.mock("./TextureManager/service/TextureManagerInitializeSamplersService", () => ({ "execute": vi.fn((device, samplers) => { samplers.set("default", { "label": "defaultSampler" }); @@ -18,32 +18,6 @@ vi.mock("./TextureManager/service/TextureManagerInitializeSamplersService", () = }) })); -vi.mock("./TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase", () => ({ - "execute": vi.fn((device, textures, name, pixels, width, height) => { - const texture = { - "width": width, - "height": height, - "destroy": vi.fn(), - "createView": vi.fn() - }; - textures.set(name, texture); - return texture; - }) -})); - -vi.mock("./TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase", () => ({ - "execute": vi.fn((device, textures, name, imageBitmap) => { - const texture = { - "width": imageBitmap.width, - "height": imageBitmap.height, - "destroy": vi.fn(), - "createView": vi.fn() - }; - textures.set(name, texture); - return texture; - }) -})); - describe("TextureManager", () => { const createMockDevice = (): GPUDevice => @@ -57,10 +31,7 @@ describe("TextureManager", () => })), "createSampler": vi.fn((descriptor) => ({ "label": `sampler-${descriptor.magFilter}` - })), - "queue": { - "writeTexture": vi.fn() - } + })) } as unknown as GPUDevice; }; @@ -84,8 +55,9 @@ describe("TextureManager", () => const device = createMockDevice(); const manager = new TextureManager(device); - // Default sampler should be initialized - expect(manager.getSampler("default")).toBeDefined(); + // Verify pre-initialized sampler is returned by createSampler + const sampler = manager.createSampler("default"); + expect(sampler).toEqual({ "label": "defaultSampler" }); }); }); @@ -142,80 +114,6 @@ describe("TextureManager", () => }); }); - describe("createTextureFromPixels", () => - { - it("should create texture from pixel data", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - const pixels = new Uint8Array(64 * 64 * 4); - - const texture = manager.createTextureFromPixels("pixelTex", pixels, 64, 64); - - expect(texture).toBeDefined(); - }); - - it("should store texture by name", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - const pixels = new Uint8Array(32 * 32 * 4); - - manager.createTextureFromPixels("pixels", pixels, 32, 32); - - expect(manager.getTexture("pixels")).toBeDefined(); - }); - }); - - describe("createTextureFromImageBitmap", () => - { - it("should create texture from ImageBitmap", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - const imageBitmap = { "width": 128, "height": 128 } as ImageBitmap; - - const texture = manager.createTextureFromImageBitmap("bitmapTex", imageBitmap); - - expect(texture).toBeDefined(); - }); - - it("should store texture by name", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - const imageBitmap = { "width": 256, "height": 256 } as ImageBitmap; - - manager.createTextureFromImageBitmap("bitmap", imageBitmap); - - expect(manager.getTexture("bitmap")).toBeDefined(); - }); - }); - - describe("updateTexture", () => - { - it("should update existing texture with pixel data", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - const pixels = new Uint8Array(64 * 64 * 4); - - manager.createTexture("test", 64, 64); - manager.updateTexture("test", pixels, 64, 64); - - expect(device.queue.writeTexture).toHaveBeenCalled(); - }); - - it("should not throw when texture does not exist", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - const pixels = new Uint8Array(64 * 64 * 4); - - expect(() => manager.updateTexture("nonexistent", pixels, 64, 64)).not.toThrow(); - }); - }); - describe("getTexture", () => { it("should return undefined for non-existent texture", () => @@ -227,25 +125,6 @@ describe("TextureManager", () => }); }); - describe("getSampler", () => - { - it("should return initialized sampler", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - - expect(manager.getSampler("default")).toBeDefined(); - }); - - it("should return undefined for non-existent sampler", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - - expect(manager.getSampler("nonexistent")).toBeUndefined(); - }); - }); - describe("createSampler", () => { it("should create new sampler", () => @@ -305,32 +184,10 @@ describe("TextureManager", () => const device = createMockDevice(); const manager = new TextureManager(device); - manager.createSampler("newSampler", true); - - expect(manager.getSampler("newSampler")).toBeDefined(); - }); - }); - - describe("destroyTexture", () => - { - it("should destroy texture by name", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - - const texture = manager.createTexture("test", 128, 128); - manager.destroyTexture("test"); + const created = manager.createSampler("newSampler", true); + const retrieved = manager.createSampler("newSampler"); - expect(texture.destroy).toHaveBeenCalled(); - expect(manager.getTexture("test")).toBeUndefined(); - }); - - it("should not throw when texture does not exist", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - - expect(() => manager.destroyTexture("nonexistent")).not.toThrow(); + expect(retrieved).toBe(created); }); }); @@ -369,7 +226,10 @@ describe("TextureManager", () => manager.createSampler("custom", true); manager.dispose(); - expect(manager.getSampler("custom")).toBeUndefined(); + // After dispose, createSampler should create a new sampler instead of returning the old one + const sampler = manager.createSampler("custom", true); + expect(device.createSampler).toHaveBeenCalled(); + expect(sampler).toBeDefined(); }); }); }); diff --git a/packages/webgpu/src/TextureManager.ts b/packages/webgpu/src/TextureManager.ts index 9069d6b3..ae6aadc8 100644 --- a/packages/webgpu/src/TextureManager.ts +++ b/packages/webgpu/src/TextureManager.ts @@ -1,6 +1,4 @@ import { execute as textureManagerInitializeSamplersService } from "./TextureManager/service/TextureManagerInitializeSamplersService"; -import { execute as textureManagerCreateTextureFromPixelsUseCase } from "./TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase"; -import { execute as textureManagerCreateTextureFromImageBitmapUseCase } from "./TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase"; /** * @description テクスチャとサンプラーの管理クラス @@ -53,74 +51,6 @@ export class TextureManager return texture; } - /** - * @description ピクセルデータからテクスチャを作成する - * Create a texture from pixel data - * @param {string} name - テクスチャ名 / Texture name - * @param {Uint8Array} pixels - ピクセルデータ / Pixel data - * @param {number} width - テクスチャの幅 / Texture width - * @param {number} height - テクスチャの高さ / Texture height - * @return {GPUTexture} - */ - createTextureFromPixels ( - name: string, - pixels: Uint8Array, - width: number, - height: number - ): GPUTexture { - return textureManagerCreateTextureFromPixelsUseCase( - this.device, - this.textures, - name, - pixels, - width, - height - ); - } - - /** - * @description ImageBitmapからテクスチャを作成する - * Create a texture from an ImageBitmap - * @param {string} name - テクスチャ名 / Texture name - * @param {ImageBitmap} image_bitmap - 画像ビットマップ / Image bitmap source - * @return {GPUTexture} - */ - createTextureFromImageBitmap (name: string, image_bitmap: ImageBitmap): GPUTexture - { - return textureManagerCreateTextureFromImageBitmapUseCase( - this.device, - this.textures, - name, - image_bitmap - ); - } - - /** - * @description 既存テクスチャのピクセルデータを更新する - * Update pixel data of an existing texture - * @param {string} name - テクスチャ名 / Texture name - * @param {Uint8Array} pixels - ピクセルデータ / Pixel data - * @param {number} width - テクスチャの幅 / Texture width - * @param {number} height - テクスチャの高さ / Texture height - * @return {void} - */ - updateTexture ( - name: string, - pixels: Uint8Array, - width: number, - height: number - ): void { - const texture = this.textures.get(name); - if (texture) { - this.device.queue.writeTexture( - { texture }, - pixels.buffer, - { "bytesPerRow": width * 4, "offset": pixels.byteOffset }, - { width, height } - ); - } - } - /** * @description 名前でテクスチャを取得する * Get a texture by name @@ -132,17 +62,6 @@ export class TextureManager return this.textures.get(name); } - /** - * @description 名前でサンプラーを取得する - * Get a sampler by name - * @param {string} name - サンプラー名 / Sampler name - * @return {GPUSampler | undefined} - */ - getSampler (name: string): GPUSampler | undefined - { - return this.samplers.get(name); - } - /** * @description サンプラーを作成する(既存の場合は返却) * Create a sampler (returns existing if found) @@ -169,21 +88,6 @@ export class TextureManager return sampler; } - /** - * @description テクスチャを破棄する - * Destroy a texture by name - * @param {string} name - テクスチャ名 / Texture name - * @return {void} - */ - destroyTexture (name: string): void - { - const texture = this.textures.get(name); - if (texture) { - texture.destroy(); - this.textures.delete(name); - } - } - /** * @description 全テクスチャとサンプラーを破棄する * Dispose all textures and samplers diff --git a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.test.ts b/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.test.ts deleted file mode 100644 index d3f147a3..00000000 --- a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { execute } from "./TextureManagerCreateTextureFromImageBitmapUseCase"; - -// Mock GPUTextureUsage -const GPUTextureUsage = { - TEXTURE_BINDING: 0x04, - COPY_DST: 0x02, - RENDER_ATTACHMENT: 0x10 -}; -(globalThis as any).GPUTextureUsage = GPUTextureUsage; - -describe("TextureManagerCreateTextureFromImageBitmapUseCase", () => -{ - const createMockDevice = () => - { - const mockTexture = { "label": "mockTexture" }; - return { - "createTexture": vi.fn(() => mockTexture), - "queue": { - "copyExternalImageToTexture": vi.fn() - }, - "_mockTexture": mockTexture - } as unknown as GPUDevice & { _mockTexture: any }; - }; - - const createMockImageBitmap = (width: number = 100, height: number = 100): ImageBitmap => - { - return { width, height } as unknown as ImageBitmap; - }; - - describe("texture creation", () => - { - it("should create texture with ImageBitmap dimensions", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(512, 256); - - execute(device, textures, "test", imageBitmap); - - expect(device.createTexture).toHaveBeenCalledWith( - expect.objectContaining({ - "size": { "width": 512, "height": 256 } - }) - ); - }); - - it("should create texture with rgba8unorm format", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(); - - execute(device, textures, "test", imageBitmap); - - expect(device.createTexture).toHaveBeenCalledWith( - expect.objectContaining({ - "format": "rgba8unorm" - }) - ); - }); - - it("should create texture with correct usage flags", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(); - - execute(device, textures, "test", imageBitmap); - - const expectedUsage = - GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT; - - expect(device.createTexture).toHaveBeenCalledWith( - expect.objectContaining({ - "usage": expectedUsage - }) - ); - }); - }); - - describe("image copy", () => - { - it("should copy ImageBitmap to texture", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(); - - execute(device, textures, "test", imageBitmap); - - expect(device.queue.copyExternalImageToTexture).toHaveBeenCalled(); - }); - - it("should use ImageBitmap as source with flipY", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(); - - execute(device, textures, "test", imageBitmap); - - expect(device.queue.copyExternalImageToTexture).toHaveBeenCalledWith( - { "source": imageBitmap, "flipY": true }, - expect.anything(), - expect.anything() - ); - }); - - it("should copy to created texture with premultipliedAlpha", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(); - - execute(device, textures, "test", imageBitmap); - - expect(device.queue.copyExternalImageToTexture).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - "texture": (device as any)._mockTexture, - "premultipliedAlpha": true - }), - expect.anything() - ); - }); - - it("should use ImageBitmap dimensions for copy size", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(320, 240); - - execute(device, textures, "test", imageBitmap); - - expect(device.queue.copyExternalImageToTexture).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - { "width": 320, "height": 240 } - ); - }); - }); - - describe("texture storage", () => - { - it("should add texture to map with name", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(); - - execute(device, textures, "myBitmapTexture", imageBitmap); - - expect(textures.has("myBitmapTexture")).toBe(true); - expect(textures.get("myBitmapTexture")).toBe((device as any)._mockTexture); - }); - - it("should overwrite existing texture with same name", () => - { - const device = createMockDevice(); - const textures = new Map(); - const existingTexture = { "label": "existing" } as unknown as GPUTexture; - textures.set("test", existingTexture); - const imageBitmap = createMockImageBitmap(); - - execute(device, textures, "test", imageBitmap); - - expect(textures.get("test")).toBe((device as any)._mockTexture); - expect(textures.get("test")).not.toBe(existingTexture); - }); - }); - - describe("return value", () => - { - it("should return created texture", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(); - - const result = execute(device, textures, "test", imageBitmap); - - expect(result).toBe((device as any)._mockTexture); - }); - }); -}); diff --git a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.ts b/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.ts deleted file mode 100644 index a34c5eec..00000000 --- a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @description ImageBitmapからテクスチャを作成 - * Create texture from ImageBitmap - * - * @param {GPUDevice} device - GPUデバイス - * @param {Map} textures - テクスチャ管理マップ - * @param {string} name - テクスチャ名 - * @param {ImageBitmap} image_bitmap - 画像ビットマップ - * @return {GPUTexture} - * @method - * @protected - */ -export const execute = ( - device: GPUDevice, - textures: Map, - name: string, - image_bitmap: ImageBitmap -): GPUTexture => { - const texture = device.createTexture({ - "size": { "width": image_bitmap.width, "height": image_bitmap.height }, - "format": "rgba8unorm", - "usage": GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT - }); - - device.queue.copyExternalImageToTexture( - { - "source": image_bitmap, - "flipY": true - }, - { - texture, - "premultipliedAlpha": true - }, - { "width": image_bitmap.width, "height": image_bitmap.height } - ); - - textures.set(name, texture); - return texture; -}; diff --git a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.test.ts b/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.test.ts deleted file mode 100644 index 66e4cb5f..00000000 --- a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { execute } from "./TextureManagerCreateTextureFromPixelsUseCase"; - -// Mock GPUTextureUsage -const GPUTextureUsage = { - TEXTURE_BINDING: 0x04, - COPY_DST: 0x02, - RENDER_ATTACHMENT: 0x10 -}; -(globalThis as any).GPUTextureUsage = GPUTextureUsage; - -describe("TextureManagerCreateTextureFromPixelsUseCase", () => -{ - const createMockDevice = () => - { - const mockTexture = { "label": "mockTexture" }; - return { - "createTexture": vi.fn(() => mockTexture), - "queue": { - "writeTexture": vi.fn() - }, - "_mockTexture": mockTexture - } as unknown as GPUDevice & { _mockTexture: any }; - }; - - describe("texture creation", () => - { - it("should create texture with correct size", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(256 * 128 * 4); - - execute(device, textures, "test", pixels, 256, 128); - - expect(device.createTexture).toHaveBeenCalledWith( - expect.objectContaining({ - "size": { "width": 256, "height": 128 } - }) - ); - }); - - it("should create texture with rgba8unorm format", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(100 * 100 * 4); - - execute(device, textures, "test", pixels, 100, 100); - - expect(device.createTexture).toHaveBeenCalledWith( - expect.objectContaining({ - "format": "rgba8unorm" - }) - ); - }); - - it("should create texture with correct usage flags", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(100 * 100 * 4); - - execute(device, textures, "test", pixels, 100, 100); - - const expectedUsage = - GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT; - - expect(device.createTexture).toHaveBeenCalledWith( - expect.objectContaining({ - "usage": expectedUsage - }) - ); - }); - }); - - describe("pixel data writing", () => - { - it("should write pixel data to texture", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(100 * 100 * 4); - - execute(device, textures, "test", pixels, 100, 100); - - expect(device.queue.writeTexture).toHaveBeenCalled(); - }); - - it("should write to texture with correct target", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(100 * 100 * 4); - - execute(device, textures, "test", pixels, 100, 100); - - expect(device.queue.writeTexture).toHaveBeenCalledWith( - { "texture": (device as any)._mockTexture }, - expect.anything(), - expect.anything(), - expect.anything() - ); - }); - - it("should write with correct bytesPerRow", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(256 * 128 * 4); - - execute(device, textures, "test", pixels, 256, 128); - - expect(device.queue.writeTexture).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.objectContaining({ - "bytesPerRow": 256 * 4 // width * 4 bytes per pixel - }), - expect.anything() - ); - }); - - it("should write with correct extent", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(320 * 240 * 4); - - execute(device, textures, "test", pixels, 320, 240); - - expect(device.queue.writeTexture).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - { "width": 320, "height": 240 } - ); - }); - }); - - describe("texture storage", () => - { - it("should add texture to map with name", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(100 * 100 * 4); - - execute(device, textures, "myTexture", pixels, 100, 100); - - expect(textures.has("myTexture")).toBe(true); - expect(textures.get("myTexture")).toBe((device as any)._mockTexture); - }); - - it("should overwrite existing texture with same name", () => - { - const device = createMockDevice(); - const textures = new Map(); - const existingTexture = { "label": "existing" } as unknown as GPUTexture; - textures.set("test", existingTexture); - const pixels = new Uint8Array(100 * 100 * 4); - - execute(device, textures, "test", pixels, 100, 100); - - expect(textures.get("test")).toBe((device as any)._mockTexture); - expect(textures.get("test")).not.toBe(existingTexture); - }); - }); - - describe("return value", () => - { - it("should return created texture", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(100 * 100 * 4); - - const result = execute(device, textures, "test", pixels, 100, 100); - - expect(result).toBe((device as any)._mockTexture); - }); - }); - - describe("byte offset handling", () => - { - it("should pass pixel buffer to writeTexture", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(100 * 100 * 4); - - execute(device, textures, "test", pixels, 100, 100); - - expect(device.queue.writeTexture).toHaveBeenCalledWith( - expect.anything(), - pixels.buffer, - expect.objectContaining({ - "offset": 0 - }), - expect.anything() - ); - }); - }); -}); diff --git a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.ts b/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.ts deleted file mode 100644 index 7c29084a..00000000 --- a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @description ピクセルデータからテクスチャを作成 - * Create texture from pixel data - * - * @param {GPUDevice} device - GPUデバイス - * @param {Map} textures - テクスチャ管理マップ - * @param {string} name - テクスチャ名 - * @param {Uint8Array} pixels - ピクセルデータ - * @param {number} width - テクスチャ幅 - * @param {number} height - テクスチャ高さ - * @return {GPUTexture} - * @method - * @protected - */ -export const execute = ( - device: GPUDevice, - textures: Map, - name: string, - pixels: Uint8Array, - width: number, - height: number -): GPUTexture => { - const texture = device.createTexture({ - "size": { width, height }, - "format": "rgba8unorm", - "usage": GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT - }); - - device.queue.writeTexture( - { texture }, - pixels.buffer, - { "bytesPerRow": width * 4, "offset": pixels.byteOffset }, - { width, height } - ); - - textures.set(name, texture); - return texture; -}; diff --git a/packages/webgpu/src/TexturePool.test.ts b/packages/webgpu/src/TexturePool.test.ts index 8b78145c..65c2cbf1 100644 --- a/packages/webgpu/src/TexturePool.test.ts +++ b/packages/webgpu/src/TexturePool.test.ts @@ -114,17 +114,6 @@ describe("TexturePool", () => expect(pool).toBeDefined(); }); - it("should initialize with empty stats", () => - { - const device = createMockDevice(); - const pool = new TexturePool(device); - - const stats = pool.getStats(); - expect(stats.total).toBe(0); - expect(stats.inUse).toBe(0); - expect(stats.available).toBe(0); - }); - describe("beginFrame", () => { it("should increment frame counter", () => @@ -168,18 +157,6 @@ describe("TexturePool", () => expect(texture.height).toBe(256); }); - it("should update stats when acquiring", () => - { - const device = createMockDevice(); - const pool = new TexturePool(device); - - pool.acquire(128, 128); - - const stats = pool.getStats(); - expect(stats.total).toBe(1); - expect(stats.inUse).toBe(1); - }); - it("should reuse released texture with same dimensions", () => { const device = createMockDevice(); @@ -226,17 +203,14 @@ describe("TexturePool", () => describe("release", () => { - it("should mark texture as available", () => + it("should not throw when releasing", () => { const device = createMockDevice(); const pool = new TexturePool(device); const texture = pool.acquire(256, 256); - pool.release(texture); - const stats = pool.getStats(); - expect(stats.inUse).toBe(0); - expect(stats.available).toBe(1); + expect(() => pool.release(texture)).not.toThrow(); }); it("should allow reuse after release", () => @@ -246,28 +220,9 @@ describe("TexturePool", () => const texture1 = pool.acquire(256, 256); pool.release(texture1); + const texture2 = pool.acquire(256, 256); - const stats = pool.getStats(); - expect(stats.available).toBe(1); - }); - }); - - describe("getStats", () => - { - it("should return accurate pool statistics", () => - { - const device = createMockDevice(); - const pool = new TexturePool(device); - - pool.acquire(128, 128); - pool.acquire(256, 256); - const tex3 = pool.acquire(512, 512); - pool.release(tex3); - - const stats = pool.getStats(); - expect(stats.total).toBe(3); - expect(stats.inUse).toBe(2); - expect(stats.available).toBe(1); + expect(texture1).toBe(texture2); }); }); @@ -287,7 +242,7 @@ describe("TexturePool", () => expect(tex2.destroy).toHaveBeenCalled(); }); - it("should reset pool to empty", () => + it("should not throw after dispose", () => { const device = createMockDevice(); const pool = new TexturePool(device); @@ -297,8 +252,7 @@ describe("TexturePool", () => pool.dispose(); - const stats = pool.getStats(); - expect(stats.total).toBe(0); + expect(() => pool.acquire(64, 64)).not.toThrow(); }); }); }); diff --git a/packages/webgpu/src/TexturePool.ts b/packages/webgpu/src/TexturePool.ts index 4aa3e7e7..5e3e2c2a 100644 --- a/packages/webgpu/src/TexturePool.ts +++ b/packages/webgpu/src/TexturePool.ts @@ -125,33 +125,6 @@ export class TexturePool texturePoolReleaseService(this.buckets, texture, this.currentFrame); } - /** - * @description プール統計を取得する - * Get pool statistics including total, in-use, and available counts - * @return {{ total: number, inUse: number, available: number }} - */ - getStats(): { total: number; inUse: number; available: number } - { - let inUse = 0; - let available = 0; - - for (const bucket of this.buckets.values()) { - for (const entry of bucket) { - if (entry.inUse) { - inUse++; - } else { - available++; - } - } - } - - return { - "total": this.totalCount[0], - inUse, - available - }; - } - /** * @description 全テクスチャを破棄しプールを解放する * Destroy all textures and dispose of the pool From 9c308872d04c1502aceec7cfa92fcfcb8b165974 Mon Sep 17 00:00:00 2001 From: ienaga Date: Fri, 27 Mar 2026 09:39:53 +0900 Subject: [PATCH 15/26] =?UTF-8?q?#260=20e2e=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- e2e/pages/shape/cache-as-bitmap.html | 161 +++++++++++++++++ e2e/pages/textfield/cache-as-bitmap.html | 166 ++++++++++++++++++ .../cache-as-bitmap-webgl-darwin.png | Bin 0 -> 13922 bytes ...textfield-cache-as-bitmap-webgl-darwin.png | Bin 0 -> 33096 bytes .../cache-as-bitmap-webgpu-darwin.png | Bin 0 -> 13595 bytes ...extfield-cache-as-bitmap-webgpu-darwin.png | Bin 0 -> 33505 bytes e2e/tests/shape.spec.ts | 9 + e2e/tests/textfield.spec.ts | 7 + 8 files changed, 343 insertions(+) create mode 100644 e2e/pages/shape/cache-as-bitmap.html create mode 100644 e2e/pages/textfield/cache-as-bitmap.html create mode 100644 e2e/snapshots/webgl/shape.spec.ts-snapshots/cache-as-bitmap-webgl-darwin.png create mode 100644 e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgl-darwin.png create mode 100644 e2e/snapshots/webgpu/shape.spec.ts-snapshots/cache-as-bitmap-webgpu-darwin.png create mode 100644 e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgpu-darwin.png diff --git a/e2e/pages/shape/cache-as-bitmap.html b/e2e/pages/shape/cache-as-bitmap.html new file mode 100644 index 00000000..f91f6446 --- /dev/null +++ b/e2e/pages/shape/cache-as-bitmap.html @@ -0,0 +1,161 @@ + + + + + + Next2D E2E - Shape cacheAsBitmap + + + + + + + diff --git a/e2e/pages/textfield/cache-as-bitmap.html b/e2e/pages/textfield/cache-as-bitmap.html new file mode 100644 index 00000000..02d5002b --- /dev/null +++ b/e2e/pages/textfield/cache-as-bitmap.html @@ -0,0 +1,166 @@ + + + + + + Next2D E2E - TextField cacheAsBitmap + + + + + + + diff --git a/e2e/snapshots/webgl/shape.spec.ts-snapshots/cache-as-bitmap-webgl-darwin.png b/e2e/snapshots/webgl/shape.spec.ts-snapshots/cache-as-bitmap-webgl-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..bc78c52076c365e33cd2cf3f77f9c0a828c2e9b0 GIT binary patch literal 13922 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 C>N*tw literal 0 HcmV?d00001 diff --git a/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgl-darwin.png b/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgl-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..465f1ffa9409dfdadd31269aee5c600b8aff59ec GIT binary patch literal 33096 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
    LJ9=vumK_c|2t)lI0 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%0Au@;GvWzOXMI{0|SbjzyTJLNY`?)d~?`(1=&l$= z=(sOM@QYed3tl3_T6;V5?Na!P9hb}^^pX87w|gJGD~QEURRYiLq!deIA|DH#dl(}r zE-Gq$l~3j8m?P3$US%CdIxK90p68kf`(RHGkRHv~AC6I>TnOs=tv5XAPwQ6@-;2AN zl_Rg&!;T%uPW@Xn(z^b8K!>mZjFYEw{M!R1!^?&SrHj{NYtpZ_pa)7ybP22JC@|!* z+-;>$l$8a^!I)ps?o=PK;y}iz4Rvd6iZ5Td(#{%HM%qRnZq2;t@c{Lgz;D)mYZZX~ zqJu=5C$RFbjLD|)07+WoV)Iu`iYxc)Cluap>gJ#iQXM?aaE2$cF&&GX%1O>iw$|N#`e#)02#NvatU0C0s%5Tf zeosNMQg%ggpY>h6_!HJ8m6AoHyf!+R+ zz4<2P^XLlKsyn)4##s(`_sTR;?0EOJ$QrEH5~@&I|{xlhOqh|toO%-wItSlSGc@QaAxBpB`a zKM5m{nVApO)jcq8g)tC;Cg!6+d;O2_5|TC}1>UC*OM%|}h;AAN2#bxQqa%q0C6aK6)LL&V(-??iprRaVqGxs742SqCUYk%o9bOr7$JG}(fEhINx zH%!vP*j(KlR7%qF)W3w<5Su$8x(&)6AjfxKE_5Mz0Ku@(D$tS{3iJ;zmjM%)n3wkYv+9M(F-;uDETH*eeA!JjwLu)pI%7<`|u5=tPBS%)EPGN@pCRp zLZHlz5MaG!=i{CGu_LGdNtQ?EA^cMDS?D_J+>~rKwl0E^SVEr-Vd7CH+oo0l>G3I* zB|&vbT_LR~;AGyuUTzZ(wnboeL(#uGorBVN*RHi`iyQVyaQ%Mxh}F}dV5FJ?x2ba>#>*d%<2xv+o%`f?!4^h?e7&W)Nt08DxYd; zq&~K{wbitiau33utP8sxr9J*18-C+j}v{KkqTH1 zDEYK*RRSfKa2oj3%#|W^g2O)k41j}Pmqg*Xd%lg6!3MveOhpqjaGLE8xG zv?78koD%4JVN6tnk(H16F906OUlnFXlmGlVGlf%%J8e*N%=WtPXkAEiGU0m2*?ZPJ z+j?#KrOH4oiiwW4g6e{wABJK4(zg}GbA$AYZ<+6s?KVgNR6GFQ10W4&0Mp*me}Pgs z4Uu&tYdAJ;JhPVfd?y>dDpy0mFi97l3w#=Ufqai4EM&A}FTMTpHVpI-3n~>@0^c(} zDVxWPW@8U(e{XJjrK&I*nk!e%QmI9~z7Ya=zw%)qt$=78_WrteT^bJ*6>V0Q~mn}2C}VZFFi`O7X9E#NM-Hxve_H) zVoe7B6(w4LqTeyo|Hw9$c`W(FjY^%kUG&Yf%Cky7V@0+=^v>^tePZkW*W#*V&k4M; z%``W^u&ThXGQ1eOqHSuU!|iqy5)|w*OzY2V8R5sizPj4}@V5!4E~}M-!EmXj-UzoU z<3Jy&exG5;r>*8;59Fv(KK-a1L5L$9->Lkqvq|*9<=<-W3ANw31QG!Vu8^hzOTYW@ zzbm^sdC!qk;2;3eaF+gu20rx452C?;NobzF%>P?&hyNdpV*kIxDW%D6xn86{?Tht; zULTt-Bc9%+b8~f$J%#U1?*r?(07eGDo&O*I`ui=>3b^2eNB8e){a3wWFgv5sShF2T gaH9L^C%PBm!iY@94Xwn%H;6|HPwr>md;0Q!04Nv4N&o-= literal 0 HcmV?d00001 diff --git a/e2e/snapshots/webgpu/shape.spec.ts-snapshots/cache-as-bitmap-webgpu-darwin.png b/e2e/snapshots/webgpu/shape.spec.ts-snapshots/cache-as-bitmap-webgpu-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..f729c9800b8f1565dd3f25e675b4ef8843bccd0c GIT binary patch literal 13595 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 literal 0 HcmV?d00001 diff --git a/e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgpu-darwin.png b/e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgpu-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..6f7556d9284949e8d13481a68b86fc05b4a0010f GIT binary patch literal 33505 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 { await expect(page).toHaveScreenshot("graphics-clone.png"); }); }); + + test.describe("cacheAsBitmap", () => { + test("Matrix指定によるビットマップキャッシュ(等倍・2倍・親スケール・回転・ネスト)", async ({ page }) => { + await page.goto("/e2e/pages/shape/cache-as-bitmap.html"); + await waitForCanvas(page); + + await expect(page).toHaveScreenshot("cache-as-bitmap.png"); + }); + }); }); diff --git a/e2e/tests/textfield.spec.ts b/e2e/tests/textfield.spec.ts index 342d78d6..1971a9d4 100644 --- a/e2e/tests/textfield.spec.ts +++ b/e2e/tests/textfield.spec.ts @@ -64,4 +64,11 @@ test.describe("TextFieldテスト", () => { await expect(page).toHaveScreenshot("textfield-html-text.png"); }); + + test("cacheAsBitmap - Matrix指定によるビットマップキャッシュ(等倍・2倍・親スケール・回転・ネスト)", async ({ page }) => { + await page.goto("/e2e/pages/textfield/cache-as-bitmap.html"); + await waitForCanvas(page); + + await expect(page).toHaveScreenshot("textfield-cache-as-bitmap.png"); + }); }); From adbdffba90cd665bf80966b77d98b1850f96db07 Mon Sep 17 00:00:00 2001 From: ienaga Date: Fri, 27 Mar 2026 09:40:10 +0900 Subject: [PATCH 16/26] =?UTF-8?q?#260=20=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=B1=E3=83=BC=E3=82=B9=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/display/src/DisplayObject.test.ts | 94 ++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 packages/display/src/DisplayObject.test.ts diff --git a/packages/display/src/DisplayObject.test.ts b/packages/display/src/DisplayObject.test.ts new file mode 100644 index 00000000..3263e429 --- /dev/null +++ b/packages/display/src/DisplayObject.test.ts @@ -0,0 +1,94 @@ +import { DisplayObject } from "./DisplayObject"; +import { Matrix } from "@next2d/geom"; +import { describe, expect, it } from "vitest"; + +describe("DisplayObject cacheAsBitmap test", () => +{ + it("default value is null", () => + { + const displayObject = new DisplayObject(); + expect(displayObject.cacheAsBitmap).toBe(null); + }); + + it("setting Matrix marks changed", () => + { + const displayObject = new DisplayObject(); + displayObject.changed = false; + + expect(displayObject.changed).toBe(false); + + const matrix = new Matrix(2, 0, 0, 2, 0, 0); + displayObject.cacheAsBitmap = matrix; + + expect(displayObject.cacheAsBitmap).toBe(matrix); + expect(displayObject.changed).toBe(true); + }); + + it("setting null when already null does not mark changed", () => + { + const displayObject = new DisplayObject(); + displayObject.changed = false; + + expect(displayObject.changed).toBe(false); + expect(displayObject.cacheAsBitmap).toBe(null); + + displayObject.cacheAsBitmap = null; + + expect(displayObject.changed).toBe(false); + }); + + it("setting to null after Matrix marks changed", () => + { + const displayObject = new DisplayObject(); + const matrix = new Matrix(2, 0, 0, 2, 0, 0); + displayObject.cacheAsBitmap = matrix; + displayObject.changed = false; + + expect(displayObject.changed).toBe(false); + + displayObject.cacheAsBitmap = null; + + expect(displayObject.cacheAsBitmap).toBe(null); + expect(displayObject.changed).toBe(true); + }); + + it("setting same Matrix instance does not mark changed", () => + { + const displayObject = new DisplayObject(); + const matrix = new Matrix(2, 0, 0, 2, 0, 0); + displayObject.cacheAsBitmap = matrix; + displayObject.changed = false; + + expect(displayObject.changed).toBe(false); + + displayObject.cacheAsBitmap = matrix; + + expect(displayObject.changed).toBe(false); + }); + + it("propagates changed to parent", () => + { + const displayObject = new DisplayObject(); + const parent = new DisplayObject(); + displayObject.parent = parent; + + parent.changed = false; + displayObject.changed = false; + + displayObject.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + + expect(displayObject.changed).toBe(true); + expect(parent.changed).toBe(true); + }); + + it("ignores invalid values (non-Matrix, non-null)", () => + { + const displayObject = new DisplayObject(); + displayObject.changed = false; + + displayObject.cacheAsBitmap = "invalid" as any; + + expect(displayObject.cacheAsBitmap).toBe(null); + expect(displayObject.changed).toBe(false); + }); +}); From 214f465402c98faf5a26c5377b90dc1374ad7629 Mon Sep 17 00:00:00 2001 From: ienaga Date: Fri, 27 Mar 2026 09:40:32 +0900 Subject: [PATCH 17/26] =?UTF-8?q?#260=20cacheAsBitmap=E3=81=AE=E6=A9=9F?= =?UTF-8?q?=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/display/src/DisplayObject.ts | 47 ++++++++++++++++++- .../ShapeGenerateRenderQueueUseCase.ts | 24 ++++++++-- .../TextFieldGenerateRenderQueueUseCase.ts | 25 ++++++++-- .../src/Shape/usecase/ShapeRenderUseCase.ts | 8 ++-- .../usecase/TextFieldRenderUseCase.ts | 8 ++-- 5 files changed, 95 insertions(+), 17 deletions(-) diff --git a/packages/display/src/DisplayObject.ts b/packages/display/src/DisplayObject.ts index ad2eb86f..12b70c18 100644 --- a/packages/display/src/DisplayObject.ts +++ b/packages/display/src/DisplayObject.ts @@ -8,10 +8,10 @@ import type { MovieClip } from "./MovieClip"; import type { ISprite } from "./interface/ISprite"; import type { ColorTransform, - Matrix, Rectangle, Point } from "@next2d/geom"; +import { Matrix } from "@next2d/geom"; import { EventDispatcher } from "@next2d/events"; import { execute as displayObjectApplyChangesService } from "./DisplayObject/service/DisplayObjectApplyChangesService"; import { execute as displayObjectConcatenatedMatrixUseCase } from "./DisplayObject/usecase/DisplayObjectConcatenatedMatrixUseCase"; @@ -397,6 +397,21 @@ export class DisplayObject extends EventDispatcher */ private _$visible: boolean; + /** + * @description ビットマップキャッシュ用のMatrix。nullでない場合、指定Matrix × stageのrendererScaleで + * Shape/TextFieldのベクター描画をキャッシュし、ステージのリサイズがあるまで再利用します。 + * 先祖のMatrixの影響を受けず、キャッシュの品質は指定Matrixとstageスケールのみで決定されます。 + * Matrix for bitmap caching. When not null, caches Shape/TextField vector rendering + * at the specified Matrix × stage rendererScale, reusing until stage resize. + * Cache quality is determined only by the specified Matrix and stage scale, + * independent of ancestor transforms. + * + * @type {Matrix | null} + * @default null + * @private + */ + private _$cacheAsBitmap: Matrix | null; + /** * @description 表示オブジェクト単位の変数を保持するマップ * Map that holds variables for display objects @@ -484,6 +499,7 @@ export class DisplayObject extends EventDispatcher this.$blendMode = null; this._$visible = true; + this._$cacheAsBitmap = null; this._$scale9Grid = null; this._$variables = null; @@ -785,6 +801,35 @@ export class DisplayObject extends EventDispatcher displayObjectApplyChangesService(this); } + /** + * @description ビットマップキャッシュ用のMatrix。nullでない場合、指定Matrix × stageのrendererScaleで + * Shape/TextFieldのベクター描画をキャッシュし、ステージのリサイズがあるまで再利用します。 + * 先祖のMatrixの影響を受けず、キャッシュの品質は指定Matrixとstageスケールのみで決定されます。 + * Matrix for bitmap caching. When not null, caches Shape/TextField vector rendering + * at the specified Matrix × stage rendererScale, reusing until stage resize. + * Cache quality is determined only by the specified Matrix and stage scale, + * independent of ancestor transforms. + * + * @member {Matrix | null} + * @default null + * @public + */ + get cacheAsBitmap (): Matrix | null + { + return this._$cacheAsBitmap; + } + set cacheAsBitmap (cache_as_bitmap: Matrix | null) + { + if (cache_as_bitmap !== null && !(cache_as_bitmap instanceof Matrix)) { + return ; + } + if (this._$cacheAsBitmap === cache_as_bitmap) { + return ; + } + this._$cacheAsBitmap = cache_as_bitmap; + displayObjectApplyChangesService(this); + } + /** * @description 表示オブジェクトの幅を示します(ピクセル単位)。 * Indicates the width of the display object, in pixels. diff --git a/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts b/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts index ccdac919..a504ec7d 100644 --- a/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts +++ b/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts @@ -166,10 +166,26 @@ export const execute = ( + tMatrix[3] * tMatrix[3] ); - const xScaleRounded = Math.round(xScale * 100) / 100; - const yScaleRounded = Math.round(yScale * 100) / 100; + // cacheAsBitmap: 指定Matrix × stageのrendererScaleでキャッシュ品質を決定 + const cacheMatrix = shape.cacheAsBitmap; + let renderXScale: number; + let renderYScale: number; + if (cacheMatrix) { + const m = cacheMatrix.rawData; + renderXScale = Math.sqrt(m[0] * m[0] + m[1] * m[1]) * stage.rendererScale; + renderYScale = Math.sqrt(m[2] * m[2] + m[3] * m[3]) * stage.rendererScale; + Matrix.release(m); + } else { + renderXScale = xScale; + renderYScale = yScale; + } + + const xScaleRounded = Math.round(renderXScale * 100) / 100; + const yScaleRounded = Math.round(renderYScale * 100) / 100; - if (!shape.isBitmap + if (cacheMatrix && shape.cacheKey) { + // cacheAsBitmap: キャッシュキーを固定し、スケール変更時も再描画しない + } else if (!shape.isBitmap && !shape.cacheKey || shape.cacheParams[0] !== xScaleRounded || shape.cacheParams[1] !== yScaleRounded @@ -196,7 +212,7 @@ export const execute = ( graphics.xMax, graphics.yMax, +isGridEnabled, +isDrawable, +shape.isBitmap, +shape.uniqueKey, cacheKey, - xScale, yScale, + renderXScale, renderYScale, shape.instanceId // フィルターキャッシュ用のユニークキー ); diff --git a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts index 31bacd25..f4930486 100644 --- a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts +++ b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts @@ -6,6 +6,7 @@ import { execute as displayObjectBlendToNumberService } from "../../DisplayObjec import { execute as displayObjectGenerateHashService } from "../../DisplayObject/service/DisplayObjectGenerateHashService"; import { $cacheStore } from "@next2d/cache"; import { renderQueue } from "@next2d/render-queue"; +import { stage } from "../../Stage"; import { $clamp, $RENDERER_TEXT_TYPE, @@ -158,10 +159,26 @@ export const execute = ( + tMatrix[3] * tMatrix[3] ); - const xScaleRounded = Math.round(xScale * 100) / 100; - const yScaleRounded = Math.round(yScale * 100) / 100; + // cacheAsBitmap: 指定Matrix × stageのrendererScaleでキャッシュ品質を決定 + const cacheMatrix = (text_field as any).cacheAsBitmap; + let renderXScale: number; + let renderYScale: number; + if (cacheMatrix) { + const m = cacheMatrix.rawData; + renderXScale = Math.sqrt(m[0] * m[0] + m[1] * m[1]) * stage.rendererScale; + renderYScale = Math.sqrt(m[2] * m[2] + m[3] * m[3]) * stage.rendererScale; + Matrix.release(m); + } else { + renderXScale = xScale; + renderYScale = yScale; + } + + const xScaleRounded = Math.round(renderXScale * 100) / 100; + const yScaleRounded = Math.round(renderYScale * 100) / 100; - if (text_field.changed + if (cacheMatrix && text_field.cacheKey) { + // cacheAsBitmap: キャッシュキーを固定し、スケール変更時も再描画しない + } else if (text_field.changed && !text_field.cacheKey || text_field.cacheParams[0] !== xScaleRounded || text_field.cacheParams[1] !== yScaleRounded @@ -185,7 +202,7 @@ export const execute = ( text_field.xMin, text_field.yMin, text_field.xMax, text_field.yMax, +text_field.uniqueKey, cacheKey, +text_field.changed, - xScale, yScale, + renderXScale, renderYScale, text_field.instanceId // フィルターキャッシュ用のユニークキー ); diff --git a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts index 923fd353..e66c4b17 100644 --- a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts +++ b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts @@ -207,8 +207,9 @@ export const execute = (render_queue: Float32Array, index: number): number => const radianY = Math.atan2(-matrix[2], matrix[3]); if (radianX || radianY) { - const tx = xMin * xScale; - const ty = yMin * yScale; + // tMatrixから直接スクリーン座標を算出(cacheAsBitmapのスケール差に対応) + const tx = matrix[0] * xMin + matrix[2] * yMin + matrix[4]; + const ty = matrix[1] * xMin + matrix[3] * yMin + matrix[5]; const cosX = Math.cos(radianX); const sinX = Math.sin(radianX); @@ -217,8 +218,7 @@ export const execute = (render_queue: Float32Array, index: number): number => $context.setTransform( cosX, sinX, -sinY, cosY, - tx * cosX - ty * sinY + matrix[4], - tx * sinX + ty * cosY + matrix[5] + tx, ty ); } else { diff --git a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts index 52cf396f..52fe52ef 100644 --- a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts +++ b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts @@ -191,8 +191,9 @@ export const execute = (render_queue: Float32Array, index: number): number => const radianY = Math.atan2(-matrix[2], matrix[3]); if (radianX || radianY) { - const tx = xMin * xScale; - const ty = yMin * yScale; + // tMatrixから直接スクリーン座標を算出(cacheAsBitmapのスケール差に対応) + const tx = matrix[0] * xMin + matrix[2] * yMin + matrix[4]; + const ty = matrix[1] * xMin + matrix[3] * yMin + matrix[5]; const cosX = Math.cos(radianX); const sinX = Math.sin(radianX); @@ -201,8 +202,7 @@ export const execute = (render_queue: Float32Array, index: number): number => $context.setTransform( cosX, sinX, -sinY, cosY, - tx * cosX - ty * sinY + matrix[4], - tx * sinX + ty * cosY + matrix[5] + tx, ty ); } else { From 27bcfe4bf87d2a9b9e8ec6c223d4c131d0381353 Mon Sep 17 00:00:00 2001 From: ienaga Date: Fri, 27 Mar 2026 10:10:05 +0900 Subject: [PATCH 18/26] fix: cacheAsBitmap cache reuse + bitmap-like drawing path - Remove Matrix.release(rawData) on persistent cacheAsBitmap Matrix (rawData returns internal Float32Array; releasing it corrupts the Matrix on subsequent frames, causing cache key mismatch every frame) - Signal render mode via render queue: 0=vector, 1=bitmap, 2=cacheAsBitmap - Add bitmap-like drawing path for cacheAsBitmap in Shape/TextField renderers (setTransform with matrix/cacheScale compensation for correct screen size) - Revert rotation path to original (cacheAsBitmap uses its own drawing path) - Encode cacheAsBitmap flag in TextField changed field using bit flags Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ShapeGenerateRenderQueueUseCase.ts | 10 ++-- .../TextFieldGenerateRenderQueueUseCase.ts | 10 ++-- .../src/Shape/usecase/ShapeRenderUseCase.ts | 26 +++++++-- .../usecase/TextFieldRenderUseCase.ts | 53 ++++++++++++------- specs/cn/display-object.md | 27 ++++++++++ specs/en/display-object.md | 27 ++++++++++ specs/ja/display-object.md | 27 ++++++++++ 7 files changed, 148 insertions(+), 32 deletions(-) diff --git a/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts b/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts index a504ec7d..25b58574 100644 --- a/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts +++ b/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts @@ -174,7 +174,6 @@ export const execute = ( const m = cacheMatrix.rawData; renderXScale = Math.sqrt(m[0] * m[0] + m[1] * m[1]) * stage.rendererScale; renderYScale = Math.sqrt(m[2] * m[2] + m[3] * m[3]) * stage.rendererScale; - Matrix.release(m); } else { renderXScale = xScale; renderYScale = yScale; @@ -183,8 +182,11 @@ export const execute = ( const xScaleRounded = Math.round(renderXScale * 100) / 100; const yScaleRounded = Math.round(renderYScale * 100) / 100; - if (cacheMatrix && shape.cacheKey) { - // cacheAsBitmap: キャッシュキーを固定し、スケール変更時も再描画しない + if (cacheMatrix && shape.cacheKey + && shape.cacheParams[0] === xScaleRounded + && shape.cacheParams[1] === yScaleRounded + ) { + // cacheAsBitmap: スケール未変更のためキャッシュキーを維持 } else if (!shape.isBitmap && !shape.cacheKey || shape.cacheParams[0] !== xScaleRounded @@ -210,7 +212,7 @@ export const execute = ( xMin, yMin, xMax, yMax, graphics.xMin, graphics.yMin, graphics.xMax, graphics.yMax, - +isGridEnabled, +isDrawable, +shape.isBitmap, + +isGridEnabled, +isDrawable, shape.isBitmap ? 1 : cacheMatrix ? 2 : 0, +shape.uniqueKey, cacheKey, renderXScale, renderYScale, shape.instanceId // フィルターキャッシュ用のユニークキー diff --git a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts index f4930486..e9b32aa8 100644 --- a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts +++ b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts @@ -167,7 +167,6 @@ export const execute = ( const m = cacheMatrix.rawData; renderXScale = Math.sqrt(m[0] * m[0] + m[1] * m[1]) * stage.rendererScale; renderYScale = Math.sqrt(m[2] * m[2] + m[3] * m[3]) * stage.rendererScale; - Matrix.release(m); } else { renderXScale = xScale; renderYScale = yScale; @@ -176,8 +175,11 @@ export const execute = ( const xScaleRounded = Math.round(renderXScale * 100) / 100; const yScaleRounded = Math.round(renderYScale * 100) / 100; - if (cacheMatrix && text_field.cacheKey) { - // cacheAsBitmap: キャッシュキーを固定し、スケール変更時も再描画しない + if (cacheMatrix && text_field.cacheKey + && text_field.cacheParams[0] === xScaleRounded + && text_field.cacheParams[1] === yScaleRounded + ) { + // cacheAsBitmap: スケール未変更のためキャッシュキーを維持 } else if (text_field.changed && !text_field.cacheKey || text_field.cacheParams[0] !== xScaleRounded @@ -201,7 +203,7 @@ export const execute = ( xMin, yMin, xMax, yMax, text_field.xMin, text_field.yMin, text_field.xMax, text_field.yMax, - +text_field.uniqueKey, cacheKey, +text_field.changed, + +text_field.uniqueKey, cacheKey, +text_field.changed | (cacheMatrix ? 2 : 0), renderXScale, renderYScale, text_field.instanceId // フィルターキャッシュ用のユニークキー ); diff --git a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts index e66c4b17..e8911246 100644 --- a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts +++ b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts @@ -33,7 +33,9 @@ export const execute = (render_queue: Float32Array, index: number): number => const isGridEnabled = Boolean(render_queue[index++]); const isDrawable = Boolean(render_queue[index++]); - const isBitmap = Boolean(render_queue[index++]); + const renderMode = render_queue[index++]; // 0=vector, 1=bitmap, 2=cacheAsBitmap + const isBitmap = renderMode === 1; + const isCacheAsBitmap = renderMode === 2; // cache uniqueKey const uniqueKey = `${render_queue[index++]}`; @@ -196,6 +198,20 @@ export const execute = (render_queue: Float32Array, index: number): number => matrix[4], matrix[5] ); + $context.drawDisplayObject( + node, + bounds[0], bounds[1], bounds[2], bounds[3], + colorTransform + ); + } else if (isCacheAsBitmap) { + + // cacheAsBitmap: Bitmapと同様の描画パスで、cacheScaleを補正 + $context.setTransform( + matrix[0] / xScale, matrix[1] / xScale, + matrix[2] / yScale, matrix[3] / yScale, + matrix[4], matrix[5] + ); + $context.drawDisplayObject( node, bounds[0], bounds[1], bounds[2], bounds[3], @@ -207,9 +223,8 @@ export const execute = (render_queue: Float32Array, index: number): number => const radianY = Math.atan2(-matrix[2], matrix[3]); if (radianX || radianY) { - // tMatrixから直接スクリーン座標を算出(cacheAsBitmapのスケール差に対応) - const tx = matrix[0] * xMin + matrix[2] * yMin + matrix[4]; - const ty = matrix[1] * xMin + matrix[3] * yMin + matrix[5]; + const tx = xMin * xScale; + const ty = yMin * yScale; const cosX = Math.cos(radianX); const sinX = Math.sin(radianX); @@ -218,7 +233,8 @@ export const execute = (render_queue: Float32Array, index: number): number => $context.setTransform( cosX, sinX, -sinY, cosY, - tx, ty + tx * cosX - ty * sinY + matrix[4], + tx * sinX + ty * cosY + matrix[5] ); } else { diff --git a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts index 52fe52ef..babacae6 100644 --- a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts +++ b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts @@ -43,8 +43,10 @@ export const execute = (render_queue: Float32Array, index: number): number => const uniqueKey = `${render_queue[index++]}`; const cacheKey = render_queue[index++]; - // text state - const changed = Boolean(render_queue[index++]); + // text state (bit 0 = changed, bit 1 = cacheAsBitmap) + const changedFlag = render_queue[index++]; + const changed = Boolean(changedFlag & 1); + const isCacheAsBitmap = Boolean(changedFlag & 2); const xScale = render_queue[index++]; const yScale = render_queue[index++]; @@ -187,29 +189,42 @@ export const execute = (render_queue: Float32Array, index: number): number => $context.imageSmoothingEnabled = true; $context.globalCompositeOperation = displayObjectGetBlendModeService(blendMode); - const radianX = Math.atan2(matrix[1], matrix[0]); - const radianY = Math.atan2(-matrix[2], matrix[3]); - if (radianX || radianY) { - - // tMatrixから直接スクリーン座標を算出(cacheAsBitmapのスケール差に対応) - const tx = matrix[0] * xMin + matrix[2] * yMin + matrix[4]; - const ty = matrix[1] * xMin + matrix[3] * yMin + matrix[5]; - - const cosX = Math.cos(radianX); - const sinX = Math.sin(radianX); - const cosY = Math.cos(radianY); - const sinY = Math.sin(radianY); + if (isCacheAsBitmap) { + // cacheAsBitmap: Bitmapと同様の描画パスで、cacheScaleを補正 $context.setTransform( - cosX, sinX, -sinY, cosY, - tx, ty + matrix[0] / xScale, matrix[1] / xScale, + matrix[2] / yScale, matrix[3] / yScale, + matrix[4], matrix[5] ); } else { - $context.setTransform(1, 0, 0, 1, - bounds[0], bounds[1] - ); + const radianX = Math.atan2(matrix[1], matrix[0]); + const radianY = Math.atan2(-matrix[2], matrix[3]); + if (radianX || radianY) { + + const tx = xMin * xScale; + const ty = yMin * yScale; + + const cosX = Math.cos(radianX); + const sinX = Math.sin(radianX); + const cosY = Math.cos(radianY); + const sinY = Math.sin(radianY); + + $context.setTransform( + cosX, sinX, -sinY, cosY, + tx * cosX - ty * sinY + matrix[4], + tx * sinX + ty * cosY + matrix[5] + ); + + } else { + + $context.setTransform(1, 0, 0, 1, + bounds[0], bounds[1] + ); + + } } diff --git a/specs/cn/display-object.md b/specs/cn/display-object.md index 27371142..1b8f2d5a 100644 --- a/specs/cn/display-object.md +++ b/specs/cn/display-object.md @@ -44,6 +44,7 @@ DisplayObject 是 Next2D Player 中所有显示对象的基类。 | `scaleX` | number | 从参考点应用的对象水平缩放值 | | `scaleY` | number | 从参考点应用的对象垂直缩放值 | | `visible` | boolean | 显示对象是否可见(默认:true) | +| `cacheAsBitmap` | Matrix \| null | 位图缓存用的 Matrix。以指定 Matrix × 舞台缩放比例缓存 Shape/TextField,在舞台调整大小之前重复使用。不受祖先 Matrix 的影响(默认:null) | | `x` | number | 相对于父 DisplayObjectContainer 本地坐标的 X 坐标 | | `y` | number | 相对于父 DisplayObjectContainer 本地坐标的 Y 坐标 | @@ -158,6 +159,32 @@ const state = displayObject.getGlobalVariable("gameState"); displayObject.clearGlobalVariable(); // 清除全部 ``` +### cacheAsBitmap 示例 + +```typescript +const { Shape, Sprite } = next2d.display; +const { Matrix } = next2d.geom; + +// 以 1 倍比例缓存 +const shape = new Shape(); +shape.graphics.beginFill(0xFF0000).drawCircle(50, 50, 40).endFill(); +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// 以 2 倍分辨率缓存(高质量) +const hqShape = new Shape(); +hqShape.graphics.beginFill(0x00FF00).drawRect(0, 0, 100, 80).endFill(); +hqShape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); + +// 不受父级缩放影响(缓存质量由指定的 Matrix 固定) +const container = new Sprite(); +container.scaleX = 3; +container.scaleY = 3; +container.addChild(shape); + +// 禁用缓存 +shape.cacheAsBitmap = null; +``` + ## 相关 - [MovieClip](/cn/reference/player/movie-clip) diff --git a/specs/en/display-object.md b/specs/en/display-object.md index f8faec70..e28fdac1 100644 --- a/specs/en/display-object.md +++ b/specs/en/display-object.md @@ -44,6 +44,7 @@ DisplayObject is the base class for all display objects in Next2D Player. | `scaleX` | number | Horizontal scale value of the object applied from the reference point | | `scaleY` | number | Vertical scale value of the object applied from the reference point | | `visible` | boolean | Whether the display object is visible (default: true) | +| `cacheAsBitmap` | Matrix \| null | Matrix for bitmap caching. Caches Shape/TextField at the specified Matrix × stage scale, reusing until stage resize. Independent of ancestor transforms (default: null) | | `x` | number | X coordinate relative to the local coordinates of the parent DisplayObjectContainer | | `y` | number | Y coordinate relative to the local coordinates of the parent DisplayObjectContainer | @@ -158,6 +159,32 @@ const state = displayObject.getGlobalVariable("gameState"); displayObject.clearGlobalVariable(); // Clear all ``` +### cacheAsBitmap Example + +```typescript +const { Shape, Sprite } = next2d.display; +const { Matrix } = next2d.geom; + +// Cache at 1x scale +const shape = new Shape(); +shape.graphics.beginFill(0xFF0000).drawCircle(50, 50, 40).endFill(); +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// Cache at 2x resolution (high quality) +const hqShape = new Shape(); +hqShape.graphics.beginFill(0x00FF00).drawRect(0, 0, 100, 80).endFill(); +hqShape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); + +// Not affected by parent scale (cache quality is fixed by the specified Matrix) +const container = new Sprite(); +container.scaleX = 3; +container.scaleY = 3; +container.addChild(shape); + +// Disable caching +shape.cacheAsBitmap = null; +``` + ## Related - [MovieClip](/en/reference/player/movie-clip) diff --git a/specs/ja/display-object.md b/specs/ja/display-object.md index 93ab9486..7402746e 100644 --- a/specs/ja/display-object.md +++ b/specs/ja/display-object.md @@ -44,6 +44,7 @@ DisplayObjectは、Next2D Playerにおける全ての表示オブジェクトの | `scaleX` | number | 基準点から適用されるオブジェクトの水平スケール値 | | `scaleY` | number | 基準点から適用されるオブジェクトの垂直スケール値 | | `visible` | boolean | 表示オブジェクトが可視かどうか(デフォルト: true) | +| `cacheAsBitmap` | Matrix \| null | ビットマップキャッシュ用のMatrix。指定Matrix × stageスケールでShape/TextFieldをキャッシュし、ステージリサイズまで再利用する。先祖のMatrixの影響を受けない(デフォルト: null) | | `x` | number | 親DisplayObjectContainerのローカル座標を基準にしたX座標 | | `y` | number | 親DisplayObjectContainerのローカル座標を基準にしたY座標 | @@ -158,6 +159,32 @@ const state = displayObject.getGlobalVariable("gameState"); displayObject.clearGlobalVariable(); // 全てクリア ``` +### cacheAsBitmapの例 + +```typescript +const { Shape, Sprite } = next2d.display; +const { Matrix } = next2d.geom; + +// 等倍でキャッシュ +const shape = new Shape(); +shape.graphics.beginFill(0xFF0000).drawCircle(50, 50, 40).endFill(); +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// 2倍の解像度でキャッシュ(高品質) +const hqShape = new Shape(); +hqShape.graphics.beginFill(0x00FF00).drawRect(0, 0, 100, 80).endFill(); +hqShape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); + +// 親のスケールに影響されない(キャッシュ品質は指定Matrixで固定) +const container = new Sprite(); +container.scaleX = 3; +container.scaleY = 3; +container.addChild(shape); + +// キャッシュを解除 +shape.cacheAsBitmap = null; +``` + ## 関連項目 - [MovieClip](/ja/reference/player/movie-clip) From 1ca4db6b2d02567a74f60ac00d5e8058df60a577 Mon Sep 17 00:00:00 2001 From: ienaga Date: Fri, 27 Mar 2026 14:50:31 +0900 Subject: [PATCH 19/26] =?UTF-8?q?#260=20=E5=BF=85=E8=A6=81=E3=81=AA?= =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=9F=E3=83=B3=E3=82=B0=E3=81=A7=E8=A8=88?= =?UTF-8?q?=E7=AE=97=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF=E3=82=92=E5=AE=9F?= =?UTF-8?q?=E8=A1=8C=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ShapeGenerateRenderQueueUseCase.ts | 20 ++++++++----------- .../TextFieldGenerateRenderQueueUseCase.ts | 20 ++++++++----------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts b/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts index 25b58574..8c1be0c0 100644 --- a/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts +++ b/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts @@ -156,16 +156,6 @@ export const execute = ( } } - const xScale = Math.sqrt( - tMatrix[0] * tMatrix[0] - + tMatrix[1] * tMatrix[1] - ); - - const yScale = Math.sqrt( - tMatrix[2] * tMatrix[2] - + tMatrix[3] * tMatrix[3] - ); - // cacheAsBitmap: 指定Matrix × stageのrendererScaleでキャッシュ品質を決定 const cacheMatrix = shape.cacheAsBitmap; let renderXScale: number; @@ -175,8 +165,14 @@ export const execute = ( renderXScale = Math.sqrt(m[0] * m[0] + m[1] * m[1]) * stage.rendererScale; renderYScale = Math.sqrt(m[2] * m[2] + m[3] * m[3]) * stage.rendererScale; } else { - renderXScale = xScale; - renderYScale = yScale; + renderXScale = Math.sqrt( + tMatrix[0] * tMatrix[0] + + tMatrix[1] * tMatrix[1] + ); + renderYScale = Math.sqrt( + tMatrix[2] * tMatrix[2] + + tMatrix[3] * tMatrix[3] + ); } const xScaleRounded = Math.round(renderXScale * 100) / 100; diff --git a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts index e9b32aa8..5757b80a 100644 --- a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts +++ b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts @@ -149,16 +149,6 @@ export const execute = ( } } - const xScale = Math.sqrt( - tMatrix[0] * tMatrix[0] - + tMatrix[1] * tMatrix[1] - ); - - const yScale = Math.sqrt( - tMatrix[2] * tMatrix[2] - + tMatrix[3] * tMatrix[3] - ); - // cacheAsBitmap: 指定Matrix × stageのrendererScaleでキャッシュ品質を決定 const cacheMatrix = (text_field as any).cacheAsBitmap; let renderXScale: number; @@ -168,8 +158,14 @@ export const execute = ( renderXScale = Math.sqrt(m[0] * m[0] + m[1] * m[1]) * stage.rendererScale; renderYScale = Math.sqrt(m[2] * m[2] + m[3] * m[3]) * stage.rendererScale; } else { - renderXScale = xScale; - renderYScale = yScale; + renderXScale = Math.sqrt( + tMatrix[0] * tMatrix[0] + + tMatrix[1] * tMatrix[1] + ); + renderYScale = Math.sqrt( + tMatrix[2] * tMatrix[2] + + tMatrix[3] * tMatrix[3] + ); } const xScaleRounded = Math.round(renderXScale * 100) / 100; From 1e346ab5b46a94fc2437a2180d5988d607501eab Mon Sep 17 00:00:00 2001 From: ienaga Date: Fri, 27 Mar 2026 18:59:41 +0900 Subject: [PATCH 20/26] feat: make cacheAsBitmap Matrix scale 1.0-based relative to own scaleX/scaleY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cache quality = cacheMatrix.scale × ownScale × stage.rendererScale - Matrix(1,0,0,1) at scaleX=1 → 1x quality (base) - Matrix(1,0,0,1) at scaleX=3 → 3x quality (matches own scale) - Matrix(2,0,0,2) at scaleX=1 → 2x quality (double) - Parent scale changes → cache reused (ownScale unchanged) - Hit tests, width, height remain vector-based (no change needed) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../usecase/ShapeGenerateRenderQueueUseCase.ts | 17 ++++++++++++++--- .../TextFieldGenerateRenderQueueUseCase.ts | 17 ++++++++++++++--- specs/cn/display-object.md | 15 +++++++++++---- specs/en/display-object.md | 15 +++++++++++---- specs/ja/display-object.md | 15 +++++++++++---- 5 files changed, 61 insertions(+), 18 deletions(-) diff --git a/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts b/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts index 8c1be0c0..be35157a 100644 --- a/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts +++ b/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts @@ -156,14 +156,25 @@ export const execute = ( } } - // cacheAsBitmap: 指定Matrix × stageのrendererScaleでキャッシュ品質を決定 + // cacheAsBitmap: 指定Matrix × 自身のスケール × stageのrendererScaleでキャッシュ品質を決定 + // 1.0基準: Matrix(1,0,0,1)はdisplayObjectの等倍スケールを意味する const cacheMatrix = shape.cacheAsBitmap; let renderXScale: number; let renderYScale: number; if (cacheMatrix) { const m = cacheMatrix.rawData; - renderXScale = Math.sqrt(m[0] * m[0] + m[1] * m[1]) * stage.rendererScale; - renderYScale = Math.sqrt(m[2] * m[2] + m[3] * m[3]) * stage.rendererScale; + const cacheScaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const cacheScaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + + const ownScaleX = rawMatrix + ? Math.sqrt(rawMatrix[0] * rawMatrix[0] + rawMatrix[1] * rawMatrix[1]) + : 1; + const ownScaleY = rawMatrix + ? Math.sqrt(rawMatrix[2] * rawMatrix[2] + rawMatrix[3] * rawMatrix[3]) + : 1; + + renderXScale = cacheScaleX * ownScaleX * stage.rendererScale; + renderYScale = cacheScaleY * ownScaleY * stage.rendererScale; } else { renderXScale = Math.sqrt( tMatrix[0] * tMatrix[0] diff --git a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts index 5757b80a..1e73a00c 100644 --- a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts +++ b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts @@ -149,14 +149,25 @@ export const execute = ( } } - // cacheAsBitmap: 指定Matrix × stageのrendererScaleでキャッシュ品質を決定 + // cacheAsBitmap: 指定Matrix × 自身のスケール × stageのrendererScaleでキャッシュ品質を決定 + // 1.0基準: Matrix(1,0,0,1)はdisplayObjectの等倍スケールを意味する const cacheMatrix = (text_field as any).cacheAsBitmap; let renderXScale: number; let renderYScale: number; if (cacheMatrix) { const m = cacheMatrix.rawData; - renderXScale = Math.sqrt(m[0] * m[0] + m[1] * m[1]) * stage.rendererScale; - renderYScale = Math.sqrt(m[2] * m[2] + m[3] * m[3]) * stage.rendererScale; + const cacheScaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const cacheScaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + + const ownScaleX = rawMatrix + ? Math.sqrt(rawMatrix[0] * rawMatrix[0] + rawMatrix[1] * rawMatrix[1]) + : 1; + const ownScaleY = rawMatrix + ? Math.sqrt(rawMatrix[2] * rawMatrix[2] + rawMatrix[3] * rawMatrix[3]) + : 1; + + renderXScale = cacheScaleX * ownScaleX * stage.rendererScale; + renderYScale = cacheScaleY * ownScaleY * stage.rendererScale; } else { renderXScale = Math.sqrt( tMatrix[0] * tMatrix[0] diff --git a/specs/cn/display-object.md b/specs/cn/display-object.md index 1b8f2d5a..ce5fe982 100644 --- a/specs/cn/display-object.md +++ b/specs/cn/display-object.md @@ -44,7 +44,7 @@ DisplayObject 是 Next2D Player 中所有显示对象的基类。 | `scaleX` | number | 从参考点应用的对象水平缩放值 | | `scaleY` | number | 从参考点应用的对象垂直缩放值 | | `visible` | boolean | 显示对象是否可见(默认:true) | -| `cacheAsBitmap` | Matrix \| null | 位图缓存用的 Matrix。以指定 Matrix × 舞台缩放比例缓存 Shape/TextField,在舞台调整大小之前重复使用。不受祖先 Matrix 的影响(默认:null) | +| `cacheAsBitmap` | Matrix \| null | 位图缓存用的 Matrix。以 1.0 为基准,应用于 displayObject 自身的 scaleX/scaleY。缓存质量 = 指定 Matrix × 自身缩放 × 舞台缩放。缓存时不受祖先 Matrix 影响,但绘制时应用祖先 Matrix。命中测试、宽度和高度基于矢量(默认:null) | | `x` | number | 相对于父 DisplayObjectContainer 本地坐标的 X 坐标 | | `y` | number | 相对于父 DisplayObjectContainer 本地坐标的 Y 坐标 | @@ -165,22 +165,29 @@ displayObject.clearGlobalVariable(); // 清除全部 const { Shape, Sprite } = next2d.display; const { Matrix } = next2d.geom; -// 以 1 倍比例缓存 +// 以 1 倍比例缓存(1.0 基准 = 相对于 displayObject 自身的 scaleX/scaleY) const shape = new Shape(); shape.graphics.beginFill(0xFF0000).drawCircle(50, 50, 40).endFill(); shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); -// 以 2 倍分辨率缓存(高质量) +// 以 2 倍分辨率缓存(自身缩放的 2 倍质量) const hqShape = new Shape(); hqShape.graphics.beginFill(0x00FF00).drawRect(0, 0, 100, 80).endFill(); hqShape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); -// 不受父级缩放影响(缓存质量由指定的 Matrix 固定) +// scaleX/scaleY 会被反映(缓存质量 = Matrix × 自身缩放 × 舞台缩放) +shape.scaleX = 2; // 缓存质量: 1 × 2 × stageScale +shape.scaleY = 2; + +// 父级缩放不影响缓存质量(但绘制时会应用) const container = new Sprite(); container.scaleX = 3; container.scaleY = 3; container.addChild(shape); +// 命中测试、宽度和高度基于矢量 +const bounds = shape.getBounds(shape); // 返回矢量边界 + // 禁用缓存 shape.cacheAsBitmap = null; ``` diff --git a/specs/en/display-object.md b/specs/en/display-object.md index e28fdac1..3bf37f5c 100644 --- a/specs/en/display-object.md +++ b/specs/en/display-object.md @@ -44,7 +44,7 @@ DisplayObject is the base class for all display objects in Next2D Player. | `scaleX` | number | Horizontal scale value of the object applied from the reference point | | `scaleY` | number | Vertical scale value of the object applied from the reference point | | `visible` | boolean | Whether the display object is visible (default: true) | -| `cacheAsBitmap` | Matrix \| null | Matrix for bitmap caching. Caches Shape/TextField at the specified Matrix × stage scale, reusing until stage resize. Independent of ancestor transforms (default: null) | +| `cacheAsBitmap` | Matrix \| null | Matrix for bitmap caching. 1.0-based, applied relative to the displayObject's own scaleX/scaleY. Cache quality = specified Matrix × own scale × stage scale. Independent of ancestor transforms for caching, but ancestor transforms are applied when drawing. Hit tests, width, and height remain vector-based (default: null) | | `x` | number | X coordinate relative to the local coordinates of the parent DisplayObjectContainer | | `y` | number | Y coordinate relative to the local coordinates of the parent DisplayObjectContainer | @@ -165,22 +165,29 @@ displayObject.clearGlobalVariable(); // Clear all const { Shape, Sprite } = next2d.display; const { Matrix } = next2d.geom; -// Cache at 1x scale +// Cache at 1x scale (1.0-based = relative to displayObject's own scaleX/scaleY) const shape = new Shape(); shape.graphics.beginFill(0xFF0000).drawCircle(50, 50, 40).endFill(); shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); -// Cache at 2x resolution (high quality) +// Cache at 2x resolution (2x the object's own scale quality) const hqShape = new Shape(); hqShape.graphics.beginFill(0x00FF00).drawRect(0, 0, 100, 80).endFill(); hqShape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); -// Not affected by parent scale (cache quality is fixed by the specified Matrix) +// scaleX/scaleY are reflected (cache quality = Matrix × own scale × stage scale) +shape.scaleX = 2; // Cache quality: 1 × 2 × stageScale +shape.scaleY = 2; + +// Parent scale does not affect cache quality (but is applied when drawing) const container = new Sprite(); container.scaleX = 3; container.scaleY = 3; container.addChild(shape); +// Hit tests, width, and height remain vector-based +const bounds = shape.getBounds(shape); // Returns vector bounds + // Disable caching shape.cacheAsBitmap = null; ``` diff --git a/specs/ja/display-object.md b/specs/ja/display-object.md index 7402746e..4abf057b 100644 --- a/specs/ja/display-object.md +++ b/specs/ja/display-object.md @@ -44,7 +44,7 @@ DisplayObjectは、Next2D Playerにおける全ての表示オブジェクトの | `scaleX` | number | 基準点から適用されるオブジェクトの水平スケール値 | | `scaleY` | number | 基準点から適用されるオブジェクトの垂直スケール値 | | `visible` | boolean | 表示オブジェクトが可視かどうか(デフォルト: true) | -| `cacheAsBitmap` | Matrix \| null | ビットマップキャッシュ用のMatrix。指定Matrix × stageスケールでShape/TextFieldをキャッシュし、ステージリサイズまで再利用する。先祖のMatrixの影響を受けない(デフォルト: null) | +| `cacheAsBitmap` | Matrix \| null | ビットマップキャッシュ用のMatrix。1.0基準でdisplayObjectのscaleX/scaleYに適用される。キャッシュ品質 = 指定Matrix × 自身のスケール × stageスケール。先祖のMatrixの影響は受けないが、描画時には先祖のMatrixが適用される。ヒットテスト・幅・高さはベクター基準(デフォルト: null) | | `x` | number | 親DisplayObjectContainerのローカル座標を基準にしたX座標 | | `y` | number | 親DisplayObjectContainerのローカル座標を基準にしたY座標 | @@ -165,22 +165,29 @@ displayObject.clearGlobalVariable(); // 全てクリア const { Shape, Sprite } = next2d.display; const { Matrix } = next2d.geom; -// 等倍でキャッシュ +// 等倍でキャッシュ(1.0基準 = displayObjectのscaleX/scaleYに対する等倍) const shape = new Shape(); shape.graphics.beginFill(0xFF0000).drawCircle(50, 50, 40).endFill(); shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); -// 2倍の解像度でキャッシュ(高品質) +// 2倍の解像度でキャッシュ(自身のスケールの2倍品質) const hqShape = new Shape(); hqShape.graphics.beginFill(0x00FF00).drawRect(0, 0, 100, 80).endFill(); hqShape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); -// 親のスケールに影響されない(キャッシュ品質は指定Matrixで固定) +// scaleX/scaleYが反映される(キャッシュ品質 = Matrix × 自身のスケール × stageスケール) +shape.scaleX = 2; // キャッシュ品質: 1 × 2 × stageScale +shape.scaleY = 2; + +// 親のスケールはキャッシュ品質に影響しない(描画位置には反映される) const container = new Sprite(); container.scaleX = 3; container.scaleY = 3; container.addChild(shape); +// ヒットテスト・幅・高さはベクター基準 +const bounds = shape.getBounds(shape); // ベクターの境界を返す + // キャッシュを解除 shape.cacheAsBitmap = null; ``` From 961e47ecbb640d254a53e36dcfb717337b8d516a Mon Sep 17 00:00:00 2001 From: ienaga Date: Fri, 27 Mar 2026 23:27:06 +0900 Subject: [PATCH 21/26] =?UTF-8?q?#260=20cacheAsBitmap=E3=81=A7=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E3=81=97=E3=81=9FMatrix=E3=82=92DisplayObject?= =?UTF-8?q?=E3=81=AEmatrix=E3=81=AB=E5=8A=A0=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/display/src/DisplayObject.ts | 16 ++++++- .../DisplayObjectHitTestPointUseCase.ts | 26 +++++++++++- .../usecase/ShapeCalcBoundsMatrixUseCase.ts | 31 +++++++++++++- .../ShapeGenerateRenderQueueUseCase.ts | 38 +++++++++++++---- .../src/Shape/usecase/ShapeHitTestUseCase.ts | 30 ++++++++++++- .../TextFieldCalcBoundsMatrixUseCase.ts | 31 +++++++++++++- .../TextFieldGenerateRenderQueueUseCase.ts | 42 +++++++++++++++---- .../usecase/TextFieldHitTestUseCase.ts | 30 ++++++++++++- .../src/Shape/usecase/ShapeRenderUseCase.ts | 6 ++- .../usecase/TextFieldRenderUseCase.ts | 6 ++- 10 files changed, 229 insertions(+), 27 deletions(-) diff --git a/packages/display/src/DisplayObject.ts b/packages/display/src/DisplayObject.ts index 12b70c18..a76d230d 100644 --- a/packages/display/src/DisplayObject.ts +++ b/packages/display/src/DisplayObject.ts @@ -752,9 +752,15 @@ export class DisplayObject extends EventDispatcher */ get scaleX (): number { - return this.$scaleX === null + const base = this.$scaleX === null ? displayObjectGetScaleXUseCase(this) : this.$scaleX; + + if (this._$cacheAsBitmap) { + const m = this._$cacheAsBitmap.rawData; + return base * Math.sqrt(m[0] * m[0] + m[1] * m[1]); + } + return base; } set scaleX (scale_x: number) { @@ -771,9 +777,15 @@ export class DisplayObject extends EventDispatcher */ get scaleY (): number { - return this.$scaleY === null + const base = this.$scaleY === null ? displayObjectGetScaleYUseCase(this) : this.$scaleY; + + if (this._$cacheAsBitmap) { + const m = this._$cacheAsBitmap.rawData; + return base * Math.sqrt(m[2] * m[2] + m[3] * m[3]); + } + return base; } set scaleY (scale_y: number) { diff --git a/packages/display/src/DisplayObject/usecase/DisplayObjectHitTestPointUseCase.ts b/packages/display/src/DisplayObject/usecase/DisplayObjectHitTestPointUseCase.ts index d03c534f..23dbf390 100644 --- a/packages/display/src/DisplayObject/usecase/DisplayObjectHitTestPointUseCase.ts +++ b/packages/display/src/DisplayObject/usecase/DisplayObjectHitTestPointUseCase.ts @@ -18,7 +18,9 @@ import { import { $MATRIX_ARRAY_IDENTITY, $poolBoundsArray, - $colorContext + $colorContext, + $getFloat32Array6, + $poolFloat32Array6 } from "../../DisplayObjectUtil"; /** @@ -108,14 +110,34 @@ export const execute = ( const rawBounds = displayObjectGetRawBoundsUseCase(display_object); const martix = displayObjectGetRawMatrixUseCase(display_object); + + // cacheAsBitmap倍率をhitTest用のmatrixに適用 + const cacheMatrix = display_object.cacheAsBitmap; + let hitMatrix = martix ? martix : $MATRIX_ARRAY_IDENTITY; + let scaledMatrix: Float32Array | null = null; + if (cacheMatrix && hitMatrix) { + const m = cacheMatrix.rawData; + const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + scaledMatrix = $getFloat32Array6( + hitMatrix[0] * csx, hitMatrix[1] * csx, + hitMatrix[2] * csy, hitMatrix[3] * csy, + hitMatrix[4], hitMatrix[5] + ); + hitMatrix = scaledMatrix; + } + const bounds = displayObjectCalcBoundsMatrixService( rawBounds[0], rawBounds[1], rawBounds[2], rawBounds[3], - martix ? martix : $MATRIX_ARRAY_IDENTITY + hitMatrix ); // pool $poolBoundsArray(rawBounds); + if (scaledMatrix) { + $poolFloat32Array6(scaledMatrix); + } const rectangle = new Rectangle( bounds[0], bounds[1], diff --git a/packages/display/src/Shape/usecase/ShapeCalcBoundsMatrixUseCase.ts b/packages/display/src/Shape/usecase/ShapeCalcBoundsMatrixUseCase.ts index d8691b7e..84a5f3d7 100644 --- a/packages/display/src/Shape/usecase/ShapeCalcBoundsMatrixUseCase.ts +++ b/packages/display/src/Shape/usecase/ShapeCalcBoundsMatrixUseCase.ts @@ -3,7 +3,11 @@ import { Matrix } from "@next2d/geom"; import { execute as displayObjectCalcBoundsMatrixService } from "../../DisplayObject/service/DisplayObjectCalcBoundsMatrixService"; import { execute as shapeGetRawBoundsService } from "../service/ShapeGetRawBoundsService"; import { execute as displayObjectGetRawMatrixUseCase } from "../../DisplayObject/usecase/DisplayObjectGetRawMatrixUseCase"; -import { $poolBoundsArray } from "../../DisplayObjectUtil"; +import { + $poolBoundsArray, + $getFloat32Array6, + $poolFloat32Array6 +} from "../../DisplayObjectUtil"; /** * @description Shapeの描画範囲を計算します。 @@ -19,7 +23,27 @@ export const execute = (shape: Shape, matrix: Float32Array | null = null): Float { const rawBounds = shapeGetRawBoundsService(shape); - const rawMatrix = displayObjectGetRawMatrixUseCase(shape); + let rawMatrix = displayObjectGetRawMatrixUseCase(shape); + + // cacheAsBitmap倍率をrawMatrixに適用 + const cacheMatrix = shape.cacheAsBitmap; + let scaledMatrix: Float32Array | null = null; + if (cacheMatrix) { + const m = cacheMatrix.rawData; + const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + if (rawMatrix) { + scaledMatrix = $getFloat32Array6( + rawMatrix[0] * csx, rawMatrix[1] * csx, + rawMatrix[2] * csy, rawMatrix[3] * csy, + rawMatrix[4], rawMatrix[5] + ); + } else { + scaledMatrix = $getFloat32Array6(csx, 0, 0, csy, 0, 0); + } + rawMatrix = scaledMatrix; + } + if (!rawMatrix) { if (matrix) { const calcBounds = displayObjectCalcBoundsMatrixService( @@ -41,6 +65,9 @@ export const execute = (shape: Shape, matrix: Float32Array | null = null): Float matrix ? Matrix.multiply(matrix, rawMatrix) : rawMatrix ); + if (scaledMatrix) { + $poolFloat32Array6(scaledMatrix); + } $poolBoundsArray(rawBounds); return calcBounds; diff --git a/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts b/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts index be35157a..c3af0def 100644 --- a/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts +++ b/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts @@ -92,10 +92,10 @@ export const execute = ( tMatrix ); - const xMin = bounds[0]; - const yMin = bounds[1]; - const xMax = bounds[2]; - const yMax = bounds[3]; + let xMin = bounds[0]; + let yMin = bounds[1]; + let xMax = bounds[2]; + let yMax = bounds[3]; $poolBoundsArray(bounds); const width = Math.ceil(Math.abs(xMax - xMin)); @@ -161,10 +161,12 @@ export const execute = ( const cacheMatrix = shape.cacheAsBitmap; let renderXScale: number; let renderYScale: number; + let cacheScaleX = 1; + let cacheScaleY = 1; if (cacheMatrix) { const m = cacheMatrix.rawData; - const cacheScaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]); - const cacheScaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + cacheScaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + cacheScaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]); const ownScaleX = rawMatrix ? Math.sqrt(rawMatrix[0] * rawMatrix[0] + rawMatrix[1] * rawMatrix[1]) @@ -175,6 +177,26 @@ export const execute = ( renderXScale = cacheScaleX * ownScaleX * stage.rendererScale; renderYScale = cacheScaleY * ownScaleY * stage.rendererScale; + + // cacheMatrix倍率をスクリーン座標のboundsにも反映 + if (cacheScaleX !== 1 || cacheScaleY !== 1) { + const modMatrix = $getFloat32Array6( + tMatrix[0] * cacheScaleX, tMatrix[1] * cacheScaleX, + tMatrix[2] * cacheScaleY, tMatrix[3] * cacheScaleY, + tMatrix[4], tMatrix[5] + ); + const modBounds = displayObjectCalcBoundsMatrixService( + graphics.xMin, graphics.yMin, + graphics.xMax, graphics.yMax, + modMatrix + ); + xMin = modBounds[0]; + yMin = modBounds[1]; + xMax = modBounds[2]; + yMax = modBounds[3]; + $poolBoundsArray(modBounds); + $poolFloat32Array6(modMatrix); + } } else { renderXScale = Math.sqrt( tMatrix[0] * tMatrix[0] @@ -213,7 +235,9 @@ export const execute = ( // rennder on renderQueue.pushShapeBuffer( 1, $RENDERER_SHAPE_TYPE, - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], tMatrix[4], tMatrix[5], + tMatrix[0] * cacheScaleX, tMatrix[1] * cacheScaleX, + tMatrix[2] * cacheScaleY, tMatrix[3] * cacheScaleY, + tMatrix[4], tMatrix[5], tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7], xMin, yMin, xMax, yMax, diff --git a/packages/display/src/Shape/usecase/ShapeHitTestUseCase.ts b/packages/display/src/Shape/usecase/ShapeHitTestUseCase.ts index ea8d0320..c7072439 100644 --- a/packages/display/src/Shape/usecase/ShapeHitTestUseCase.ts +++ b/packages/display/src/Shape/usecase/ShapeHitTestUseCase.ts @@ -3,6 +3,10 @@ import type { Shape } from "../../Shape"; import { Matrix } from "@next2d/geom"; import { execute as displayObjectGetRawMatrixUseCase } from "../../DisplayObject/usecase/DisplayObjectGetRawMatrixUseCase"; import { execute as graphicsHitTestService } from "../../Graphics/service/GraphicsHitTestService"; +import { + $getFloat32Array6, + $poolFloat32Array6 +} from "../../DisplayObjectUtil"; /** * @description Shape のヒット判定 @@ -31,11 +35,35 @@ export const execute = ( return false; } - const rawMatrix = displayObjectGetRawMatrixUseCase(shape); + let rawMatrix = displayObjectGetRawMatrixUseCase(shape); + + // cacheAsBitmap倍率をrawMatrixに適用 + const cacheMatrix = shape.cacheAsBitmap; + let scaledMatrix: Float32Array | null = null; + if (cacheMatrix) { + const m = cacheMatrix.rawData; + const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + if (rawMatrix) { + scaledMatrix = $getFloat32Array6( + rawMatrix[0] * csx, rawMatrix[1] * csx, + rawMatrix[2] * csy, rawMatrix[3] * csy, + rawMatrix[4], rawMatrix[5] + ); + } else { + scaledMatrix = $getFloat32Array6(csx, 0, 0, csy, 0, 0); + } + rawMatrix = scaledMatrix; + } + const tMatrix = rawMatrix ? Matrix.multiply(matrix, rawMatrix) : matrix; + if (scaledMatrix) { + $poolFloat32Array6(scaledMatrix); + } + hit_context.beginPath(); hit_context.setTransform( tMatrix[0], tMatrix[1], tMatrix[2], diff --git a/packages/display/src/TextField/usecase/TextFieldCalcBoundsMatrixUseCase.ts b/packages/display/src/TextField/usecase/TextFieldCalcBoundsMatrixUseCase.ts index 26e4efa9..3d2e1188 100644 --- a/packages/display/src/TextField/usecase/TextFieldCalcBoundsMatrixUseCase.ts +++ b/packages/display/src/TextField/usecase/TextFieldCalcBoundsMatrixUseCase.ts @@ -3,7 +3,11 @@ import { Matrix } from "@next2d/geom"; import { execute as displayObjectCalcBoundsMatrixService } from "../../DisplayObject/service/DisplayObjectCalcBoundsMatrixService"; import { execute as textFieldGetRawBoundsService } from "../service/TextFieldGetRawBoundsService"; import { execute as displayObjectGetRawMatrixUseCase } from "../../DisplayObject/usecase/DisplayObjectGetRawMatrixUseCase"; -import { $poolBoundsArray } from "../../DisplayObjectUtil"; +import { + $poolBoundsArray, + $getFloat32Array6, + $poolFloat32Array6 +} from "../../DisplayObjectUtil"; /** * @description TextFieldの描画範囲を計算します。 @@ -19,7 +23,27 @@ export const execute = (text_field: TextField, matrix: Float32Array | null = nul { const rawBounds = textFieldGetRawBoundsService(text_field); - const rawMatrix = displayObjectGetRawMatrixUseCase(text_field); + let rawMatrix = displayObjectGetRawMatrixUseCase(text_field); + + // cacheAsBitmap倍率をrawMatrixに適用 + const cacheMatrix = (text_field as any).cacheAsBitmap; + let scaledMatrix: Float32Array | null = null; + if (cacheMatrix) { + const m = cacheMatrix.rawData; + const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + if (rawMatrix) { + scaledMatrix = $getFloat32Array6( + rawMatrix[0] * csx, rawMatrix[1] * csx, + rawMatrix[2] * csy, rawMatrix[3] * csy, + rawMatrix[4], rawMatrix[5] + ); + } else { + scaledMatrix = $getFloat32Array6(csx, 0, 0, csy, 0, 0); + } + rawMatrix = scaledMatrix; + } + if (!rawMatrix) { if (matrix) { const calcBounds = displayObjectCalcBoundsMatrixService( @@ -41,6 +65,9 @@ export const execute = (text_field: TextField, matrix: Float32Array | null = nul matrix ? Matrix.multiply(matrix, rawMatrix) : rawMatrix ); + if (scaledMatrix) { + $poolFloat32Array6(scaledMatrix); + } $poolBoundsArray(rawBounds); return calcBounds; diff --git a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts index 1e73a00c..3196a30c 100644 --- a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts +++ b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts @@ -13,7 +13,9 @@ import { $getArray, $poolArray, $poolBoundsArray, - $getBoundsArray + $getBoundsArray, + $getFloat32Array6, + $poolFloat32Array6 } from "../../DisplayObjectUtil"; import { ColorTransform, @@ -86,10 +88,10 @@ export const execute = ( tMatrix ); - const xMin = bounds[0]; - const yMin = bounds[1]; - const xMax = bounds[2]; - const yMax = bounds[3]; + let xMin = bounds[0]; + let yMin = bounds[1]; + let xMax = bounds[2]; + let yMax = bounds[3]; $poolBoundsArray(bounds); const width = Math.ceil(Math.abs(xMax - xMin)); @@ -154,10 +156,12 @@ export const execute = ( const cacheMatrix = (text_field as any).cacheAsBitmap; let renderXScale: number; let renderYScale: number; + let cacheScaleX = 1; + let cacheScaleY = 1; if (cacheMatrix) { const m = cacheMatrix.rawData; - const cacheScaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]); - const cacheScaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + cacheScaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + cacheScaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]); const ownScaleX = rawMatrix ? Math.sqrt(rawMatrix[0] * rawMatrix[0] + rawMatrix[1] * rawMatrix[1]) @@ -168,6 +172,26 @@ export const execute = ( renderXScale = cacheScaleX * ownScaleX * stage.rendererScale; renderYScale = cacheScaleY * ownScaleY * stage.rendererScale; + + // cacheMatrix倍率をスクリーン座標のboundsにも反映 + if (cacheScaleX !== 1 || cacheScaleY !== 1) { + const modMatrix = $getFloat32Array6( + tMatrix[0] * cacheScaleX, tMatrix[1] * cacheScaleX, + tMatrix[2] * cacheScaleY, tMatrix[3] * cacheScaleY, + tMatrix[4], tMatrix[5] + ); + const modBounds = displayObjectCalcBoundsMatrixService( + text_field.xMin, text_field.yMin, + text_field.xMax, text_field.yMax, + modMatrix + ); + xMin = modBounds[0]; + yMin = modBounds[1]; + xMax = modBounds[2]; + yMax = modBounds[3]; + $poolBoundsArray(modBounds); + $poolFloat32Array6(modMatrix); + } } else { renderXScale = Math.sqrt( tMatrix[0] * tMatrix[0] @@ -204,7 +228,9 @@ export const execute = ( // rennder on renderQueue.pushTextFieldBuffer( 1, $RENDERER_TEXT_TYPE, - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], tMatrix[4], tMatrix[5], + tMatrix[0] * cacheScaleX, tMatrix[1] * cacheScaleX, + tMatrix[2] * cacheScaleY, tMatrix[3] * cacheScaleY, + tMatrix[4], tMatrix[5], tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7], xMin, yMin, xMax, yMax, diff --git a/packages/display/src/TextField/usecase/TextFieldHitTestUseCase.ts b/packages/display/src/TextField/usecase/TextFieldHitTestUseCase.ts index 986be8ea..c6f51ab8 100644 --- a/packages/display/src/TextField/usecase/TextFieldHitTestUseCase.ts +++ b/packages/display/src/TextField/usecase/TextFieldHitTestUseCase.ts @@ -2,6 +2,10 @@ import type { IPlayerHitObject } from "../../interface/IPlayerHitObject"; import type { TextField } from "@next2d/text"; import { Matrix } from "@next2d/geom"; import { execute as displayObjectGetRawMatrixUseCase } from "../../DisplayObject/usecase/DisplayObjectGetRawMatrixUseCase"; +import { + $getFloat32Array6, + $poolFloat32Array6 +} from "../../DisplayObjectUtil"; /** * @description TextField のヒット判定 @@ -28,11 +32,35 @@ export const execute = ( return false; } - const rawMatrix = displayObjectGetRawMatrixUseCase(text_field); + let rawMatrix = displayObjectGetRawMatrixUseCase(text_field); + + // cacheAsBitmap倍率をrawMatrixに適用 + const cacheMatrix = (text_field as any).cacheAsBitmap; + let scaledMatrix: Float32Array | null = null; + if (cacheMatrix) { + const m = cacheMatrix.rawData; + const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + if (rawMatrix) { + scaledMatrix = $getFloat32Array6( + rawMatrix[0] * csx, rawMatrix[1] * csx, + rawMatrix[2] * csy, rawMatrix[3] * csy, + rawMatrix[4], rawMatrix[5] + ); + } else { + scaledMatrix = $getFloat32Array6(csx, 0, 0, csy, 0, 0); + } + rawMatrix = scaledMatrix; + } + const tMatrix = rawMatrix ? Matrix.multiply(matrix, rawMatrix) : matrix; + if (scaledMatrix) { + $poolFloat32Array6(scaledMatrix); + } + hit_context.setTransform( tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], tMatrix[4], tMatrix[5] diff --git a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts index e8911246..26a00d74 100644 --- a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts +++ b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts @@ -206,10 +206,14 @@ export const execute = (render_queue: Float32Array, index: number): number => } else if (isCacheAsBitmap) { // cacheAsBitmap: Bitmapと同様の描画パスで、cacheScaleを補正 + // baseBounds原点(xMin,yMin)のスクリーン座標をtranslationに反映 + const screenX = matrix[0] * xMin + matrix[2] * yMin + matrix[4]; + const screenY = matrix[1] * xMin + matrix[3] * yMin + matrix[5]; + $context.setTransform( matrix[0] / xScale, matrix[1] / xScale, matrix[2] / yScale, matrix[3] / yScale, - matrix[4], matrix[5] + screenX, screenY ); $context.drawDisplayObject( diff --git a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts index babacae6..540be8f6 100644 --- a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts +++ b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts @@ -192,10 +192,14 @@ export const execute = (render_queue: Float32Array, index: number): number => if (isCacheAsBitmap) { // cacheAsBitmap: Bitmapと同様の描画パスで、cacheScaleを補正 + // baseBounds原点(xMin,yMin)のスクリーン座標をtranslationに反映 + const screenX = matrix[0] * xMin + matrix[2] * yMin + matrix[4]; + const screenY = matrix[1] * xMin + matrix[3] * yMin + matrix[5]; + $context.setTransform( matrix[0] / xScale, matrix[1] / xScale, matrix[2] / yScale, matrix[3] / yScale, - matrix[4], matrix[5] + screenX, screenY ); } else { From cb7f81afcf2f7c59fa93487b14e8f89063d4b3df Mon Sep 17 00:00:00 2001 From: ienaga Date: Sat, 28 Mar 2026 00:04:33 +0900 Subject: [PATCH 22/26] =?UTF-8?q?#260=20update=20e2e=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cache-as-bitmap-webgl-darwin.png | Bin 13922 -> 13718 bytes ...textfield-cache-as-bitmap-webgl-darwin.png | Bin 33096 -> 33734 bytes .../cache-as-bitmap-webgpu-darwin.png | Bin 13595 -> 13791 bytes ...extfield-cache-as-bitmap-webgpu-darwin.png | Bin 33505 -> 34136 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/e2e/snapshots/webgl/shape.spec.ts-snapshots/cache-as-bitmap-webgl-darwin.png b/e2e/snapshots/webgl/shape.spec.ts-snapshots/cache-as-bitmap-webgl-darwin.png index bc78c52076c365e33cd2cf3f77f9c0a828c2e9b0..ab46ec4a511540eeaf851ceef7f7abbdf5971ffb 100644 GIT binary patch literal 13718 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 VvCJ6ts2x55hDVK$yw)X!{SW_J^!5M% literal 13922 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 C>N*tw diff --git a/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgl-darwin.png b/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgl-darwin.png index 465f1ffa9409dfdadd31269aee5c600b8aff59ec..e9b77f05cce0406b4496abf61d71456bf9a6a75e 100644 GIT binary patch literal 33734 zcmeFZS5#A9^feklL_t8MDMf539i&5Of>=O$LLhXM8bT)^-2x~GSSZqa4WTFWE+V~$ zE=_uu-tXr3jsL@ayiecw?!8ZkFd%22z1LoA?z!eXpET5-QByKgLLd<87tfz)K_KMd zN7C*~q~IdYu1o@fT!p-N@)+hGw=!;R57VER-)k~qnIU0$Uw`k{jaV|)vQF~xc$aX_ z7qnX8obRvC>fN0s-}YQ^`^wZ!dr#@b!+1_U4rR-Qv3QnF?~6}gv2<oIYq809$XEu1cdNzVe^xl|!O9t)_N$QPvJ-^&=`RDs@|Ihz|sS+}cu|Qh( zKQ-RVYM;Z{<X8ANBCWNMS0wY8+@ZrEAo_wV1gwzl@x z#{2ZwM&InM#S=b@Xi}s`Slo01Q&UcIWUtTZ$|C56!TtoE6iW9GQ9vMMq$82YUc$gP32^sG=wmu7SorGdYJL6V z2rqas+zO`LpPCcS-I!zWCg}wiK49q0rJ($vj~?H$mm`xPxelnd+-Yb3Utk7T5MSJNS)5>)Ion2+7{31J zswmWQa~OMLqK8od3QNl_`vHm7d?8i^F9^t8T)-U zb8{B%k6hm2YieYLg}>|h=s%bFrIM~WJs_}~^0W{^Nt3u4F=}`|tHz;(I6grr1TuSwRr}gL04^K;fq7YL7Jq%_$c?l(@rKNcM zaBy2&TVG$_z`y_t3k&5?e0=Lz1U?xciDBN4e4;xqG$ny-8ojgAXu5~%n$WM?vkW{Ckfdb5;{b{96A*=;f4}YY-*ne8$W?buoJIs7% zq5#4cuz+?b4s!NGa22kbX)6v&p?hR`qK zO)#4Uj9QMcWmG(J^c(61xkX6c9Z{z0^cJMLA>{yNHPMhjcyUnb*K0Q9Pt25-(L8=| z0#f7TlRMxMILnp_w@==7Cd%czWPZpf!+y--PTz{wXo8c=wY_ak&shl20N z6%<;@g1jdq;{h&Xrz@AS8h%$KiAetG5Qy0PqXTEQ?eu;Bg>O>^<-&)NA-z+-@XS@X0USCfXl_o>UV1= z@k6DFBh;uzc^uw#?&PSpEQ_WB5ELKfJm+hsKEW>n}FM2g-3@fP|+llx*W0wxY zReK*1xC?`VyAtKXj>>|#LXoJy0|RikC$M^-gMaBlwFG$;6_vtz-plMInLKC_=(Qy6 zEC0os>7}|X)*OibDlEONU7ykdqqjaZGz8*8xW20*Ac!aBXii+oikf|`9@~$OqT?}I z+948&S3iqF`65W2rM>x*|12#p7b|D)=)u1<&6)8f(wbs0I>9&(r!jMc1%}^EYEph{ zpe*0GmT>WJjyrqSfe3im4Th_Jnwl^V5TA*OiHLS8*GaJDLO*}bx`FcLK1JKxV>2p1 za^uc(Lc-w@(^;pm=SD_Gw{G3ivB4`_7*G-%tb9i}YPkP!@8wiLMMPe+$u7PD34AWb z2*riFBj?dsnndWvmgdOB`gqYj=s;3gSE~p9NwZxe*oZ}igiupwT>fQm9!Wg?Of|!- zH2PR&*(pkXbUFD-9->552v`3q#w6o;Uhzfl9Bdgnq{1~X`B=6DjNC7O^0TWe8SJ8Y z1K7meKlpfgIUVxy^LZ?}`xia!nqHC%D;ZRK&b3GX`t{UYF5Q^n!;V*@wh(NiZzJa8 z-MDwNot>esG3JBQIYX#N>RUycB23izT%2;K*2u$Lb*^~Je2qJo1Hp?(=>I9Yv9mmgd(RUAXIP1l#$WMJQn91T<+8#AwK5yp zTeFFp^Wh$(%i*V)Ro)My1L2`MWdmkb_4G4474Hf=X{-#;H$$=pT`xWk%TZ%m(R?}j zlRt$(oQuJW`4cr!X~+tpVxz%-qlY$Y2X5)-aHr;zHtj8G2YC*y6m4nkm<|`#^s8A{ ztfN-A)0(2nxp%nQ7^*nzA4dv`dd=xPJ?edD{S>Jn6S+pHh09_ z-TF_A!QaEUNd^7XS5ANJUONADU#gyTBU?Q;BqU^RZceo8d?Q7Cqy&EwuNk$lar68R zSN=bQf&TZyIDo=7R;#>^FmK+xvA6HK(*=qX$QauTU@81k<;ibcJ_lS&OG}-do%-wT z(ZZslqVrve_BJ-X&eTb6gM)(r`iwt1+TTpIC@CmN^2?MvIy~ezD4VQF0pIfE#&ik&h5^wE*^&f1br4SVknVGz{u9tHZD3^v(BN}7N8p!7ue-7 zZ|Dd!C6-&(xWc7{?JIyb?VX+6Ls7y7YBD7yC051DFX~Cc_Zpm4wG}JwFJhr$VvLD; zx_Wv&ue`la@Tft6_~yY4HN;2-xf9L)EDz?dmM>Z0a5!peYDO2h1+3&pObl{2j`r8u z4>$th=Hjxlwx*`8UTJR%=1)iG(f%c}f}jd+E^K=q9rZ}sR1*&(&ng0Mx)W(u`>&Q1 zhpVyDHmLRx^|j%43Yb5vGWfRrqS}2^VU+KT&)fbSg#xH*W_cA1;>-F=PtQs+?!h$6 zF8HJ6l@*xDCG$Uhnd;a8%)vkNc{=|s^Y)oj^<++6eSIut0%hlJP;0Dlad_q!mVU|W z6fv&9*w4m>?yabh>QErZoMeku`0)1m2T>;{{T=RpUlh4d;9in!6Ln%im@9th{+oy zHyGOZ6H4Z}1=l>UWpjt<1n><_Z%eS6Qov^Q4GlvO8$S+{{nVG3KtT%!*$N#lP@T~D%oU}Ld z@i7W>3|>x8=h)->i8x@)g)pVw-RdxC;-WrBVA-vjJmhn4-+zH18D&I@^{o$& zX8#Wt5L-g~ZK_X8lGTVL-fi&9j4QhV?l6e+z(9ai{Ht)+s$*-Q+DhlhtkBydNS zofqZ`{;a`T$m>eH9O4{Tsf~F z%=w0ub3p|TSr7U6_&lzV3>{#4jxL(S=6}oiS4>nx<-EMH_uT8Ya;@W;KH>S(WKC^; zU80UA;;TLni-!-TIEj`4>&=^=>6C_z2r*&{SOeWLxJobPGX&%vaZI@5%CQ!VgBm~VGwg;T1~^EC8s6ciMzN;s3M zdhqPM_2@4e(bJPXBO@MV&=XGGkrUg9L)cU7X1Mr@5YnH0wyw$Ixty85GrLn}`_Gy|j!>%F&p&{6ED=DNC?%g+2 z3o4#!{a|Ar(Pn#V_iC24cHW$^Uw9PX=cW+{SFG5(2t~9vetP%XZ*WvEx#6K{Z+!>9 zqhP9+ih9bdi+4lo@+ISg1}&3uxLoYRApe-kUs?$zT?(Uur=MGoX>_5M9Xx-vR1KU5 z%}^$2Y?%xpQX_FEYOM0Pc5wj?jauD}mm@bBeK!9@p69LeA|&K$k!wD1}?_Sv0XLl-?};!9x602r@LSyp*DlO7$PJs zy%N;-vG(*3tC0E>Py`CUSXA?Q-UVE;FS%^g#tU3`CLMo9j8@{Tje)?au5DIy>JTu0D2Lh*8IgHf4DuaqU_pzbeC zK?CYo<4HJUh38(LO|9|+a+aDo%jdH^Z9$t2`gonbgM;}*Lq|-q?OfCGzx=HO?fjSO z>Z6H(wTPZ;eOwlb>`}qyE2Jpd`Rg}TU5~d#jr;CTQF!Kcq(}5lTPyLlFPAHaBMHZ! zSS$P;Zak_zE0ExyQ#W28;qr5jWnX|^RoBV9C`6UCkHeYbU4l;nE3cJ^wh;UI4J%-_ zH@ri+-|JrPADdT(9fbQy-+PSdLRMu}T>H)pIav zRWkn*#MM5M@bjS6B6$(eA`v&lYk%9i$rZI&j4(8hDnBmKh3GhG%U(e|sP#I~B2f?4 zTrdA;buVrU_D09ub7u)&M*8OU>nD?hIdU7+rsQ~s z1Ntct*~V4w*DP@U(uLG6ZO1e#JhGB0*0)3;*p=hM9aLqj+k^;Kn+4g~J^aIPcLj{1 z_bKG5xmL*~k|C!UC_PBfm(pbioTHaDFSiKSw5UopjEB)nP!X>% z$l~C*EzNQ;L0%Qk^PSty(>9;CGYWlaq3p~y^;xe3VL`=M$D8qq>~5qzFi>I{Qd%_F?%CBwaJP$>I} z8p8^l{qOrhPotL=$ao4X6h9SffBM83YLWMbX73a}UY31zz_9TrLPtlLzCB_%&#-d0 zkyegV*`EpT{q*V6Ae@u+=BH2ZFCOL3$WOfWZhCl|1|~L0svErdrEV{sj*^a!j&e0w zC-;W$!txZz`P^hAHzHnK=KFhLpuV-MuJuatK-ReP?*o>BHU0eHBQ#-jgFeo^Y_&G+ToKg_JG?Nt&|QaS2RW<9(uU4%Hd(HZ%r;~8M#ZmWV# z#%uoh>Wy4->%~6>k&%(%-vRpKv;1{gtn*5%;2nbAZ%tR&9qnSOYiijqqVI=}#guV7 zQEECGcXO;&viv;YUb_cCmU6_BAeE|elS~r}&S~szWI7QL6OjpRfIm`fM#hBWV>&gG zT3=^^GgpDXu(xqku}w~3bD?A(iEGt#lbz^ar>Jrp3}CdybZghr>@a1IXksSw)5n@qPu|`Uxfn_p--@Hm^HBOud7>oA z9z-t|E)A!G2lW3`Ed3QTo?m;K(~JGp>l~~IYMQA*dagu`MWl2ULo+wIEqUh`wI~^X zn+~vFM?I{AmjARiicL5LVUe(i;MH=XX2jAlV{NON+uGPCV_F$R6N80$gT@l?4`KXc zST4<1t%KFWo#jnc4`H5!@~`l?y^_s{D?41A{lk__y@oy=a(_CJzhBz5Ps|1G(|j0= z__Cwp^|e@*_ejF!TY4O^A9=-Jf3%_4sGh+wk!eHJnD2As6NZ_M6e7$Q$&O@3>`c|v z)F@6^fu_;qk?R9lgNxjsX*O{!g@(yF?WwIl-sMq-DSuYJr%ZLI{YL(gB3y{$`xnaX zCzpR#eA5Zi_Rvg#@u_{yC@kzTLPU1ci=;%TM}&rpOj}fo-J^mj-%dU${+B$*k{3IW z*qW6oL!En@*{(3vABNU@^NaE2BX}?~lbUGNX-VA8Lkc2KYYTVbh#=CjI?tTv$D8du zMy*VUf=hq@ijYM|n~w4_Rw8o{?v%SLVvKudXn@_pjMK$hFd}mN9l!07q*7njKf=1-Rfu*apg&TQMzt6~|lF4eII^c58q%G*yFi>Zw| zzaVe$?AqMH&0r@p6MeJ-1ok6)vIGC+KLrpm3v$8a{u3p+2$4PMrsvq#NV@6kueh|i ztgN3a#atZ_;hNPoe@J!XW=vU-EzuoMg#MF?jX*bfR7bS9t8$-EI~iughPmL*yI51&I)yW!a4Ub1IGI1856Adq7i{!vF;zJ3pcS z-}{WLH!JxZjxT^;KWr9XuD%H2{;XhMh95XQlsYd^lW9}3b&!p83B-GJ8~$x_J4Bg zvQB&poONVsJonVXz>#=u+2icA`j(u}aaMD&W!pFdV9bc#!RhC-TQg=2LfmgAxi|zI zLIF{-rtdx@36ZB^Fu4yXA@#83<>e_y6iOw`)z#G_H$Q*y6<}CyxnVHd?z627Zqm7H zfGox~`}>p2h>40W7G`EPv&ms6emSqNthmhL@@XcwC4_`hn*aPUk^!Kj^3|(gDPiH% z=8g{4s3Sjs3DcTE1(AYGd?bw!HMy*oD2uM_`}gb-9*>_bl|NWp2()l??7H zI2aH1_lK`;)Q*g~xw-A!!<5T);dA~Eg+f_bIWRoiRL`WUeX}^AW-#)7hNg~P&L<9Z2;$<}eL)ZP zyhWC&7ViXcAO-z{sodV9;{)d?9u`o!mozDirVOD^Zm_UWt>0pYk`^>Rfj&ADUHqsW zCdIz)#4!19n6a2NS4K`p$A-{b4W|_o6KhDV_VMyR?AHw$k5PaeX%Poz0LucJh}=HA z(;FKbtD~jxO&$ql(6}M(U0TXH8LV-bdS|3LJ_P5C!0RKC+wcDYV7Rv`A}&5ktNO{A z?wOKsObjN|@qf60garQmNN;ue_iT_juyH{gn@|=8RSFo_lu$9gwz66PokC7xnqKBv zc(X|B?k?e*DS4*x{OAYlT=qlIv%!pah$rXdGYo}Qw&x*B(CFzB;Rk-Q}TZEW>}NtS3O`QL3<1k z5+jMkwR2d;!CcABY&~yKKX_uHJ5N6GMf5<@&e_@712C6_A^Y$T&CSiOly#@DPd^}D z_e&;-Hz_JVk@+bmFK%HrD$3G4pEIql6m1uxc|Dth3^Rn6js*=bS4l2^B_U9kFoqB4^aU9$O|)=O3HEsv=<iRN-=;zrC6=IE-!m7AKBLeEF9%Uj8p4@S~56q2EP9T(RuSSa35^MDF&qn{vm+E zSG;f9L>tkzqOg~gntHF6DsN|JXK`_{tRO=o6O#XyY8QppeDYS#1u}QMD9LBJsK!|0 zUGhBgO^L$B&dkI%MTf1q4)5CM zhqp_L0I)^+1Bn4z*E?ww#|SzJ-Fcv0xemE{t}hq-E7Oc*F>Dh_K&7+uC>GV1Gj4u)C) zoNTz6V7=0a*IprLiLvRQ>`yk??zL z+CIZK?4{JNR8Yp6%j+FEmAgS`IP`fDEr-uwq_kc0bJZIZ>1S?-UFjyTjJai&AclcE zvxC=n&Kgd!gp!g4-;f7c>x6#+w|0zQoYB$3w1{VyMpKvM!gp^RdhpOJw&nlG%x;o# zT|A55nMsdc@{K&*vUy=k9Qi)q?5?D(bgeo)Cmv%L6M9Nv&5k{31nF(NHdhy+P8l8j z^eOUNa&j`%ZS8eH|Jds3ixIR@E8--R?uGLh@17+h*6u>%(8RoLsRda`b9b}?={6r@6|gq-yfuiw&vDY>;Y;G zU9s<;)E;K^elbd6nk}oQ{Py)I;FRQuRzUmfvyM9cD-z?_7cII#v|r3WDw}D{O%<+Izpiq;a)+8 zuy5*)O}+UkUppr`skj8Vwvn~Ut+zm#XzzDY(eNtT#?JPX-;eLsl)cQP^TEqpo2FWK znKvQ;)=s2@e$8|f7Pmdym};DyOzQor#?AF-)X8T5%kv0lO3Y5$q7`b0`U~6zZD&Uq zjA$1&H&QjA>$$-28Q-q+882)l=9M8Iqh7Ak#=e)rUYo8r$~GSIF79q3&R+~+6^#9W zxgWI>@V&>8NpHlXHEX7z$NI?D z)-VjrQgP%?6yGOm`@X_NeU{!|MF%5AdQ*kRMk2Pk$Ik5pQCq6fY)5ED3A_DCP@s8% zd2!0@+dPWUVTN+d1=h1aqulyv?OCQM?CX)wEB6Y>n4uy9hGDI>Rl}i^tKGvoM6o3A zG0gMVTSa$p-Dp?X=XvsE$p$+-PUi;<;sv$IfbEigGeAk4Ka_mVA)6M$N5773;gf zLAmlHl)LX4BWXFI@%uydmGa09Dtm)8h7xn)zrO+nJ~<_+#opnR<;eThDda>RW?dDy z+SXes^~ytypTymMgj(&BU3d@a2Z6K%R49R+|17mMBz4tdZ_4>JHk&QiCuTU5${?Tyf;47PNj5Ad&}JQfmxZ(fX&(f_dNbXB z--Ci}(^5!RmTg&oMxEpP%NqQ2tPEo4cS%3r^Tno&$jg6}neG?{8Zc?;WqHMX`6z_b z;w$8Ae0><+iyTec{xLBgzGW#qb3Kd5^Wo2a5j_V7l?AdPEHRkp%jQlx2R%wUw>x6ZetT!gQaQo&xW0UWfBBHoza`+Z4SjrLh_G}0`bED~1t3@a zP3bqM;&Bzvu5>h2lBmT~G^}oDz*t}w&-QniBh_)R9`o3ukno!5z;pVBfXLNla)a8+ zcyXS-{VX7bH?>{ceB*+`!w-$(A5}uQ&X%CD78!NZPEiHS(7dA6S8aL)f?72C zY*5<}+QH9kQbRk{po}_~qYOeKQroy5{LRmW$IeKORISu?2C-laP_LR;?#ucWhxJC* z%ifL+4Pii3w{L6|@T|`e-uYS)=AGC*+PUjkRi>6TUc(irqReZ2P7aAUeezeym!s1r z1s!K~D)mOY!mkEd9B6Bg6|uqgt-Ei%IHKl5k8$hz23WK>dd_JGw@-vE?oF?aS03*4 zyrd##ZL!pTFfTA>q(cho^(C8$q&nFYb6`A7?`%0|Ft+@s_h)W=#MD&q2botq+uru0}5hIoVLuhMi zYVA6cY{vD=4#r3>6wzmO1m9a7wP+Y0afxY+TmM)3zkA8FhL%37pI;s`fCTDhZu|0R zWt)w2eCE|B;m7rF?G(u;lR$S_elz(+^y*K_BIVo@C;jUF&W`!SuOX<;gIaVtw7=s_ zHN^K|n(UrF6OZ7WdY{d2F*~mTjnlUy$@vX@^%>E%{F{Yy$-$Qi)0oHMhULi5Y~IL0 zf!(je76;z~AP2M8M)x)Eir&E`DIPV&97LQs#vWs%I=*^csC#Ukm~?iH zdqMBL`s#f+1>vI5%~_F`mq>KOl+8I(o?FGH9z@;}%D96M?D4f(4r<8@>{_qqSy>88 z<3~6QH}G!zo=NNu7`&JGErI&wzlVILq!(8xZlpgl=bpE%!0#84@m3~S0+rva)FQq& z#r2G3qU`S3-NIw30zSn;Aw$V&&udI5Mf1j=6km1l{OT9`r790^3kzFz)wat%c zvuAlM%+2U0O2xPOb?|vu5MdRZ*9mEu_vSj2&z?QQ6GxQvJ^Cy3!@AcNK5$&t-;rlO zshQU1JDg@XB3&p`ppvJuoE)9@y$x6y{v)w>A@2|2Ueng?v^kIltFaal&aMwKg zkvjX^Uls0~)3kEOS7k|YGd+>RjpY#q{yI`%G)bP47ts}B zew#CeA}cF<(S^Au5e%Do#bjn>R`{|)TkPX>dF5QfG4T+e=U9#Wn#IqogJx0$EYm?Ot zf9+GsqUiOxq4WgBULcQ-f@XyAlu(TK&-#uL1xX|dVHb3Iik|So_6yhEiT*hCZSoYP z>s66NAePS57Q)19zhoI!7c9Ed{SOx)loR1=*e{=|ij5uy{*OKi9JQu&YSg+dVpDP!g`AT zCuJ{aZU-(jV<$Vs3=G(OglM*Vq7;p*9Bvk?)%+BHB6!$oRm=)++oe0&qK@-|0S2w^ z&Tw{PFK#o!6dVgw3_@A55l2@)a=5|zAaboIOP9Lhl#8HeXE^+ZV4>QdL1l<6y{ zhIOeuSel{QJd$Vkws=SG`8ZfzL|s*N@h9rW%kKj(9O8@mTQ;vQ(w4?IUjABee5m6P z44sq*ifJ=f7niaz+T`&I13^w0U;q8TB_~cpa2qk5cAtaXj&Bj;DT2YyX0&4oE|B9;?_sM&8~6TppxES z%Twjd+~;HmvHEWN?0MJfnc5ux*q!vhXA@WQf2zjsjHc;*U>rGId~>*+jhVxE+ZM8T zGtp(lS*zjy&n@0lC920#b{|~3!(@%Fc@qz6e;mHhs-dneXiL!1V^5TYhU7gYu!v77 zwoG(x@-RP=HS6)^&{x2P8mW!_(^y3>FC414xV=PPY<+T=}o%HK1WgB+2VX+ZubFO z>cZcCv3qPQ_TzkU3A>bt(GG6|*WmE5eCmE0VwZ}^15D_8UznTgV383r*$vr@h0 z={t(fE*+Y)X3%;jHxhK)Rb$4y@5#tCvnWL-eK%EuSJqlFUpwsru}agbh|_2J5;*x= z_9B(NP~SGIv6uJAQOEX*OHf-)on>s;y%N=eG&IX&eMM`@xNXPN3mOKKaE;Q3%(hlm z&)E~z#uA5a_4s{X?0-)|%c@bn!qntbx>A*ULeo_H4s&Y|+0itgjAIKol(kuD9`&(a z*=zB6yqZ?T1{pTTI@0m-hz_|nVZN9S%yoCR&IWm6+$|AQqx378{V^Ygi-x0D_M?V| z1UF5;$3HGL%AM(J(HSc)AH@`ysT3YDjMjf)(8N8rQZYlJDHu1s@8t&BHU7;H4uMb0 z6LTmI&_n-*;WdO8#?>CPPV9C--~PjYz+~qAWqofbDZh?t(JP%%7fh8IOLM5gibngI zl?}!4u4r)%9*yCt9W}6(+6@X};5IZ3-DKj@>1;-$m=Lk6!8T#L6^3l+Hj^17R6e$- zqlxeXUA=yIF>Rl+MDfxAU&cE8Q~2^Rlh(u#U=8T2HnfiDD(|6=Q$qJ~7v^ac&Uo;d z9WIRNXI3$=xn8_)md;AMMttm;i4^IhvhDc;Me z&6c5%@J|oA0sC%JA?@bCXdZ0n;5e-PS<_GR3elwX$|gK@R@-RcCMRFogLKN+2k8fl zyy0Bl))|!2%CeN)cNTz^N`D4PM{MT|(mEH^zUH$U@Q9|f;h?}?@it;C zW>z|V`?c_U^DYW`I5N^$m|nEJW_#G=Lym4izgd~=3Mg^htIoEFnWil0tgP&U9`buj z*zHX1ON`do?wU(H3Cmc!&AvAk?we{0k%(3x*M$!YE&qk|J~>Y+=e{OV!Eesp9=3D0 zK0huT$(LMf&EbPfx-}Ghdv*Gy->2<0OTqEe9|vgO+NvVtX|uOPzYhsS{_c5)1F~P^ zA6nL0HG9kNwIj=Y$nTM@e~DxewcC|Fc`~JH>m*4xxc~bzeq3H2&>fHR`G<_xlmQ=8 z&6F_Y0zkFPPJ`k|47xm~NYT+?4F)BuLJ7y#Ue7l z^M=?Yt*WZ()HQH-hJgR`9en@$)$9NN2>xGT1^xe*@&Cax#$g~Eu0U=t1=-KDY8Dj} zn*(x8Nsy~*2*Q?~A7*}|2J<8#5}Tkgygk>!GU`@1L+ZCa7`?lTp@10o0QZH<)$AKy zv#k;3<>jlZXc^$H1sz(g)s>Z>P|y!@U9AKbF*e9#r2?H5#5k6;;1=j0xvuDHYZsT4 zY_ALpQ$QZ-S~RCaq(wz%!M$04#V6m!&26vc`?(i%CTk~C@;h7+NviQ;f zfrv6R17i$YE!%$5^aqFITP+%LB&pjA9=%PnKf=t+?BU@7lZ%dvs{+kFaC6{8J2jCa zrj?LRekj=O0=h%OOfS!fThpQ&us3X^GfOpjSXpZt6zHiAx`6(b0 zk3bQs`39soWlYh)h7l*@Ddt=tAt#sL2-?cKfByVo_t`gfch`Nj=*lq#TKDHFS}(6L zlDff^?{GehBDTaG=FyQZWCfsBxrP`<9PuS3L(!E5Grjz3P;6*u=sPIC%r_sBNuDIqE0g*`s0Of~ zfVmyRHvaeT-?6c{E!W9*PO!a{Ydv6>m7YbfPt@9@(F0%84RbQDhve|kKtffkW{RfK zL)zx2S4!GIxF0TNCWa@Xo6~I^^z~zR4O<(}Hxc#@giL}9Ca4KH_ndyi(Xg{HGu!>C z%6{Ke7Gwt42H^YvOqFdD<2h{UMM-?4~I5C#Smr0~F0u(W;_C8qS@}Q0R_y%@}{T z3Fv$SL${R0fB`FnRT&6bPD5232W^_bj-U&!vHw%m^7$6VV1&t^EviklqSK_A9Af;= zD2XKQwv+=*SH}CWB#4{H5LsPTRW;V%pVkb}a(-Ukp9D33=O$2_HtCGD7JLZem0_2G^+I(&*21{Kqo}0 zDH7O~&NdopnH{aIxe_u&b&f%!aAU*4($X@cO$V~CFyeRu`rjZUXg%dCo1wH^YY0^l z;ND&uzYaoYB(#kLLgODP3{jJU!(4_eRVjSI0YwHNSgdVPaj~`;FB}OH3`|YW*mz=S zNTU@$D4N~U^5P_$i{6%r8e-h3T#tF>OqF-<-aX1jq6!hXIWoE|skZ?8zPG(?Wo!wu zA<)Qr07c&N76<|*EAm&Pj*fFQ5Sl=+y3niNmVhzEbzc{3i-wP`H>)cvv2KbCO-f4F zQRuAZO!dSi2kqumsoehW`lWUttw|Xmu0Wbs|5SUPZ*~wVtLbE`WpNXz;ZQ74h&O%? z3k%ezUY0!KhwNX^oX$HYnS`d?_W~}d%5XOa2aVZqDklE^{(jCY2n1rH#v8i*IUQLT zV4$zxmz+t-d+xWnltEc=h56X?8oK|1PV7WG)YOjLLB;)SIP|fN0gxa=C4ko z7ilWC!8#WR#K@CnU1-H7%cwki7HGo@A~8BKaj-Bolo!~$;3kfzIpqNX7w3Zo)f1#I zGhJ*3GGY-k@Opr)zzBnfS0?w57u9`>2q3d-t>M|Cc9Ddw=$IHvDF6I-=laE=P!+hj zzs+h{iOOG^7Z8oB3?`6>+p?VfcT5n7L0}}=-rjC#XaJ#V^8irJ9x4%$?H><1F2SvP z%0N|xmg4GJs<04U=w075qC#P^W%uTNqd+~41CZ4JU0 z04-E&`^bO5@BB?OzRBD{_)s6<*6 zr5xdE2|s=m46bUx17lgWN%y7xct*2DVXV{>Wd}l0*48a~`vuUr;u+E}LO^+R{5btO z8x^p?F1u^0ujJhq0`DUM$w6jueckuuaOd!_qB^hf8Fh9{JJ8F~NC6{UMq{(v`X6ie z5bQq{fJn&vUZo1V1?7UOHsUBbF<6~bPgrlAa2)=xE;^68SyO_%ZnXR>gas5Xm>{^P zmxYOmeEK(#$jZwlmu;VN(6iBl4~)x6@QCj!2}^tc(}4(-diP(%fB82PCMN}vP$SIb z_9$($cwXQJs*y++1nT-IquF7y}451I_@F zrWWz%)2GX`a0YpoH21moo8*Qv0kWhZO^AG^p#&Sk7ib!0XJ<8(3R&)vb5~{kN?8QH z<3Huj%}ACdM_`G{urGZ`;wSJLng`N+%?{;SS;@0rlV~9`T<5 zq%6;oOnT+EtJLe2r6sKpMAyTI4|k-6{g&zg;*z55uLN#nsk^YRPnu&eKiv1$4B?vF zudS`EU0nJD9dga>=x*Gnj)zZ^Ky$YF030A=Spa^NMCCBtRTl>{;0Of~=0mJBy^h%j+jVU9TZ?CW{2wC03>Vz%ty1%uahuhX{ ze+?@!s?A>yL2VQsdnwWXbj9l5d5X|N1||S3+fS#o3}RfS=W9BfRSt7%>I)W?W}xn_ zkp{Y0iI3iH1|I#PZLJOD;0I01wcT;YoxMF&v$?pEn@{u3{^rc)rXy;e6f$vbjSNI@ z830s^qX$cVT9vBQ8Sk5b?|Z=!ja~)fQV$iS8%;bhCFlV#Q}y%ILj;LKH}G zBLYD#tQHj%G})uO=(m39T$w&X`EWj$P}~{z__?%1qq3W zq|N*Wsx^R%xcVw1b`_MHnlJ`Y{DxX2!?Wj`oMjA@ltKWe_tnmMAS+O>79^a&M)mgg zwzX|PhzvY=Wgz5nR3{V7qyiMTSwLJ z2Je&Ay#TI$Na(kvx@1o{8!583d$Vxz_wSE6oIsy^;HDV_bVoJ5XGUaMIbJirLV6Jo z$m*gv2{RSIIlg7NX4?$+xC@Fnc)dqLTpa8J@mBkYCTfBqPw}GP_w#J?jCai7NuW3J zG1i6)pRhy9Ha}&ynrLZZK^z>!p@V{p1k8<#yGveAIg6t9bwo zt1D-A;mWW3AjT5@ulBw(s>yX*H^>5)2;x!@6~&5D1QbM?fQpEsG$913B2ohZq}PB2 zq$^USMk&$>9YXIYy(aW3(nFIT;LfZ)_SyT6amTphj`Qc9dv5+H#hCB=-gmxpKJ$5= ziNopX=-8FB2#Jbrl7}QTPaY6e*KaT^v>0$`$S*6ixH@B7kVzmoLHwZQWgIBE<;$_~ zZ4R-Np-e;Xfo~8G90mpK&U!{F7esP6?YemFUB=Fr=YKPVtY#6CU?L9>5THp#sm%Lu zR`cAzgFR7SUmsZYDBP7x95gyqIR^Dh13Y!$RyvnwXMc4=88vXi@+<*@1(ZZuj_Izg>{ES&@Lc`DI?mlg~RxP|UOCQWCcLT1`EooUaFkC!nV zgW4|i^_sko-`veOFDzv<`-ljv+cOAP8Ylw$E#0?Uk~1^mHjti48;(-%HYigtpKsp2 zT?$nakM&+AGot(eHH5OKHd2X{TZh2=VGFiE#GVm^P@wSJZWe)32+XGDT;-mLe^Vo9 zEJ9&o_S)Xnv`&{P{`>-BbeaW-T~}_4ifUM~lz3(}Ruk{^D=u}!(|B0EgDV+%Qnj6N z^ z-1fUDz#$Z*4N=+3)b!Cu4JBAgtX>)VxofG3o(q97&K=v03?}=GqeUPVZatmB)z{C@ z^}92vF{?VOwOyyks!CVI9fa#!!xdO*(1BU=3DH(sLnVf|8pv~*ArH!3aeK9sNAPps zA(s{+Uz~$YCqAAh=og!&o=yXG@tO&3FP~&dad8p*RSyG0iHj-=4u*zxljLYM6v{Z` zOThZW{@fvWcPxW=e9}-iVf~(V^bc-3t@yR54M*t&{DN4gh*r~g|W7_ zHaDM#Q?qq(BM(dE*x5c1Ab+nczGsD4G6ei#>8vv6{e#%XJjjtD6*yrV>VllND;GQy zXX)ynEDjHB6*U5Ma_`@1w8~|1O^~Ywq zl1}^oKK$k65i}UD#cMWFeFtuQ%|96(8%xw5866#+Yzo)O+_f3pMgE!XTa}vFO(LZw zxB?&CaP*38_v`<_0%}jG5_-QeBlO4s_V6t5(6h~dQkoorQ!8%^~zQc}pk}Z_HgHj_jMpD1-BZY3<`0ALR+O)E=0;?g$ z@_?j-L`hLma{Lb<#GqZ1-#nBxDKWCNvqREyUADQZk7!VkxLIdF*Wb~h;$mnHoTF?> zJfLk7ybCeuhQ4-3Gt8FS+dn9d%`8K-(SI6wPbU276ZTrnnrQ6?qx#*gZQ(CUYP}mQQji!iF%M%ban=>-vf;c z@quR2*srN6Bm;SNRvc>Sr`f<+qCj_hU9+*ipT_KZ%r7u2>5f$}{_Sgbg9$-_k+Q!W$cVk|v~phn{6e?~y{f8Zt%)to5PxDU7t(85VDry>7i9b*eHP)$*U!yO zEA6)3O~(CW0l0;E-Pwzy+UD&|xI$po; zVO>ct8ylj(>`tB)P1b6L&O9Ziv7)u`V*!2V>1%_LClRlOh}`E8UoPsDIU{$RSRdH> zCrA3^WWoV-l#rDLK#%(#Wh){g0?e_X8k^Ng3!5hRUWYV*K`stVwB8O}5gTE11M@vF zzkyhrCvcjby#(|efXVy-lCubF;Bx?B(pmlOwVmhMuzR`o4m{_1>1{#|pFbD#oaW9k z+n1(Dmb2$)l{>dIeZjb@v!g@*tVv2QocMo;Lh1)=nx39M-C;U0F%gnVNJ$|ddpjrf zWcttk(~o&C4{)h-sdGDm8tIv{mDN(r6ymZ!!*#^vgzW6>9#uMxNtVPHXXW_$`7vPW z1rZHYU|!zdrVueN|0%3LU_Fx;vk=K*lE3)09GQqpTF5hQvc)D|nELgL<+E@p;30by zh&SSWB4ABueRw$F(4C8rJ>I!z_KL+iqbfHJADktB(K(LCckPY=TR%*?WACH(ySs2UAj3`1Ef3U0S|UlWZ_JXL&yPzwR~ zAdLvrM-4iCq*tg52yDPVa(bNO;hAtpkow+khr%;9Of{~WQ zDTobK!EO_X1>6zqgzoWhj5H95it+d$YkQn_IXn*q+j8gFK2cW@Ae{9!3fccj0Q z{5?j>tr&Qj?sFPo@YmMbiZM4Igh*l~)Dw(CA|dGs<#-Zd@9exiR3feT*-Qa)SwMkE zy}c)OyAc(;>rn-Fpy@&j0UJpFkKC7Le!pW@bBHM!8+4i4Yb5Nq5}s|yvPcE3OC`^7 zvZV|W&h7eg(T}_$7uZb2u)iYXHbWf$w2-=o)6gt90({D735U%1_=)ZmRoEJkm_Sa< zY^hW3N+>8$x6aGRSV&9M)4FW!ov4vUxh*16xcEVH4%%Bxcsmri3|wMmWZv+!0YD0R z{_f-K^U%wXuYj`Q&r7f~8`v2TR%d{edmTlYaKfQNxAzKc_I;G1_HjEG87le6Tp6Mw z$X6a$&K+*seu(`+lHE6$vW@<|FIqTvB>hK_%Zr|$Qlf}Z8ABz*=Tm7_fXy#!K|z3` zR~NO_Q?awlH<~?y29;8uosEs^4Z9J~P*3Ed9iclTZW-*wfc}`do!j_b|74H*MC8)u zrYLO3Kz1;-x>Av16K{E|^n-)j4Cd=$=fYQ(Ztrzn=k{4CZ7WwSFVZDhi5jo&<%NiT z5@~oH{=venoN;H*n6%#4_P-ho z=m=s;Jrkg=gIKIxo<5Y`_5Sowy@iR1a%fL*QfLggm&&!E=z5i{)YZYHsjt8Pslp~O z>5)iW=1~>-GwkeLm61i?+6M;EPCd!9X{7v%k$%Be(AjwH<_MuZTiqa~5?!Z@dT;jK zIWHJ*lw{WtjOJ6jire_T4Vhfn?0B8uwrbiJF`(#c9!~%Cr>0t&$<$wY+x_ybN!n@b z=+~!p)9^C_aC08=wSj3W$hLoH7WfmVdBX>3Mqi4@#}v_)8&8cCk0mY%tINBNuMV8T zS$ezWm5dHnOhw(07+K%Mt~e@bCkC0z-*5L5m~f`}t%h4udvds2`(JI`q((Eg zcwf!AhV8Jm?uNVHZ<#|$yV?x1jL&J3+*n4w??!E?rnT;F5xtlEM_do!SM}|>3D_Cc zxK8sVtOF6>KeEc3Gfvvp#boS9SPHncU2WELJ%(9VRqh+RDm}vH{$_APsOS2AxA!o1 zRHAR2&7FOCdDU%>{q5WW3As zWE(h(dY>i7QBwyi820gce*4vy!-ekG1KfMY-fl?<3munpp+f}gld5JP%ILp>3Q-?P zqv5G4rrM)ecbh3b59HSX`+S+!($m9>CY6Sdbc7_1oWoL7zA8QfPaLJ++oPjjVr}j0 zxQ9#;oMn-br)8yNF^RR%fTc517}@%n(w=4=YP42t^p%qG*4)Py`nMD7&@?fN45Wvc zz$B(Uqf>)k&U+wFXiUDqfz}03OijFJ1>I9!jph&YMdtQ!^&f%e7DOv$E2JaQFEWIv z)75>z#LQVmXm`x}9E;Cd7>H65>6D|1FHyJozCIw$^?#0xQGe#7p;~S;6t2L$}=VKUyL`cC$ZDs$pQ0R`Lzy_lA%^fy{59g5+y^6y)7}lZ0!v(LQhgH zk6tGwx5Cl!`@yDjZnct=CEX0}s}oXd%dcuG1XWBJvW0PnYAWc?$<^RiIZ2Z`-BL~h zB;TUOhIv2tg=@D58l~3#^qmYV1s+A~+>FPP2S?fxO{T)JLqX$|u$kXj`^{YXf>4f4 zkL?Xg>0ao$O8&T#Sse5Bm=Ka~uIE>XQpc^26%#w-<+DlVxVmCI<49A0%MeYnwmhaW zx)miV#P=#xk+nHp)YMdYrnOq@%hIr7mm`-4O4*jMEJrIB?GoZ5t<@_cH4;vmjAexJpj zplNb|Z6mI9K37kP+_ux9VeCgKoNdhC5jH44PpMppCz7IDWTy5|BaSX4lBbp{mm9gM zHJe~kWwxu~85p*A5cg9+R-m6jQ4X)>=FV+rB3?c=;Mov5drPlCXMK|VV*Z+zNs9*V z@k%a1nrY|amul|16s}YgaY4ajnz*vH@oxkM%f`6Vg5>l4D^}ArTcyTr(B@S$iTV8b zJ)Y1rZ7eyp6Z!PMfB5BYyU^5g!D>kjpXLwMlpCa7L?K(PT33mBTf3i#7T~&-mD!(QV-`qvEi7 zV;{z&@!{elzP%?Faoc9I*y({+4NbSUw%%(It|FxrnRl+VS3T3qiQNzg+@w$}Cs)jV zjW#m~F7w@)N96VDJdt-WGFY4CJb3;T^gbGWeFG;Z3S{iV`i4ul;y=+nHi<|>@_nm! zyZ?-&?5vg$4aNs5q)Z>CMYBfhQ-tRv_2(?)?SEnc7oK{x7Pd~l^)MrGj6j)e7Wiw@ z^`qF6DUjW(kEkDY1EiAo-lN{9Zr^g#1in6!gtmgAeyG!gUzm`&OFfAXcIBUy4?XRe z%raaTj&-$rPYq1hF_x(F9KRy{>x%F(g_lEw3TwSL8Si^6_v-~JM*>pw*U5%BJ{i@* zciPWyVM#%IShwNn?cj>?jWVUu{hldnaU=GZL}pWa8=Jg0-*+vUj+rh68Q>~5J76pM zhCrXLF>Fln-i4!Hf0}(1WpNB(H9)>#(vjrF@^sCzh9bd1@t%IyH-k@1yL=VceJS!A zaU7GhkkIiHGAcyD+{O;|38p>`Srn5cX{Jgx?ICc!W{c8dmRX-nLpPGm_{XxVt5)J zHH*JHf=#`W5}Dd0ETmqJ^e~4W<9%T!BR7dM=QT61&wcUYp6tZYU{+gUHoqd@ct~A| z(qZ?~1vBDS;MmZ4fZh^IFpQY5q2cPCe-R{G`ZAOqQu|esLi2}QzwqR}Aatk$dQ zuI_`(ZJ%>SWh>91Ry*G})|^I}Dqk|wbw=Gl>T4ahG&k?YHTIky-quyGR^tGj?C^F- z4rd1fpzFxiu%xAQ|BT-BJV+N$o_^Y=CBTzn_RTqNOxWX=EE=8m&0NH|v%YWH{0$ev zJ1|FB70lQDl4OyU#OGnj!U_t580Uoc%)j!9k?$Umc#^|1Q&P^S_nA8K1<`l;+lF1R zu@*#Jo&#aPj(rVnvu_9JzJeI-T-9Z+t&MPALxC`0Pi-w*t8P){%a zdByQH^-0$!L%;pXET^7ol^>umy9Exd?%$#?h3(C?l~nuo`HD;mU&zi(Wv6@!!a3Yh zf5A$meCQ#Vj@NFpV5{y0*y^6VbdV z%H0p{o^M|kUsBgpmCTLKUfH>E1po3yY>tUom;YI=Z7;(6unNj&9p|THkMYXrXY>{y z-e71D$&Pdd+!Dp^Hl;DJNB#dBQ~|1gj5%@iN&`j5Iaq zzh5|o+sHPC-w4rBw%Fi;{?`2T^p{m*#Cz+{hrjggmn;C?kpPW2NCCm&2W%GsqWQmc zTFXQU>F4mC0l*QW95I+Jy<)#3AS~E%n1$Xq{0*QB`M<^3|6tKmzwuvm`b_0tj)1fQ zp$u%pflTnYC*EOku-+>z!_AQ#4LDfk4;jP*x~Ip0oW?8J3|)XWm<}UHSD>1tNQnOu zVMdIgk~)Ab_&8`2PHiA>zSO-9W^|s_FRKVq0MznC7RMKW+ZS*O%gQn)3sC^9QVq@n z`{FkihluJOg=?m0$2__dY|^`*S{3D}u$$;dsstE}P8f`|I0ZYW z0P}^kA82Pk%bVe(1d|aVfr7sYv*8@pP`XQ(07nCzqTL4#Q??T)?ww!?aTm}n?d|EA z4D1IdY1WR31#JKhPZvqJFAUOLt~`MFH=k$NCEyu zIThyccsIQip@UjU6Z-Ju3iJgQk$nDKfvHPrafzwnTb2t^Y8z|NFKtmvS^V;;hl@Iz zf^FstO|MB0V5}$h)W{ad1Gg3XgKKJPT5)nkPOCk7hntagV5F=|-HMkN%b0p{khHNhpB=rpqZUXkWyzA0+h2KVJXOPs0s~Wo zA_t^?8UW9tF&N!E^%Swxcd@H2s>m?4TYhQbP?|S3L)t<@(NtcAeVn%P-76n^h7kElovd)v1T=eTLp zuA}Yj_{P#uL6{-&K=Gv-A>+Zs$GBgj(n@DRR5JQjqH#iHG(k+Obef8NUBCX})N=p? zR9f6lAwLg&J>y*anWu8h3A~=F^jUVmu08cdz68UC$>c-Hk5MlnNQrL1J56hb*4RWs|S@i*MW>BUtt^+ z_b7{D<^%x%;kL;&NJ+z*n!S+JF&UvmI^`|$TvGpG1T$`Wm zN4HY=ObCxpZ_yqf^C(4H^Q|)Kvp`gMx}a8voD}D57BD52%$U-EYh_5yVRbaKd{aTh z9J^RWQAYnFsFHP9Vn(#LtmN~sQh$LD#>AF8U-LPFQlZdZ$R{Mbv;%p}{T%s{f-CxQ zR8LQL(JtAR`iX%h;GgjBfRXa`?=8*E;Ol9utHub(ob-JG)y7Vs9Y*0s>f|{4C|We< z)Ldz7qLW7|uoYQZ*Ciy_Hv}{2l>OT*|IDb%je0o4$7%@+6)%mv>AtgW0qW@JE%0b7 z3Z63;J3cCW$#Cbak}8**A2(CVyX+DXEs~)%N>}f4Ik(!f#0Bi^k3R8`z=F0*d^Qdn zHTSOG=P;FMYcAS+wY=?IIFgYBIPOx*UmhR%LuDrTDBMsblkH=wm^hNW&$4hqZA78FC#fDG#BYcWjM!lu81jK0!+2m0 ziwQJL@}5O67RbkQZEdWt^JGe#+l0p3gwI<>d@lWklf0&LFYgZ4gcStw-W$-<>LltA z3X6+1+wu`q7q|Dtwo=h@nx#b(ydx+Z&nmUL9tJYm{q&t1x_XCLf8G$8cz6hTTCG?{F zd0xqQHdg2Sr;%TDvwx$zntxJWvmtmH(b_R=C(oTLMET~EiE__tOwv-rF!?4Wc3Dre zPpmvHuG>>fXyMzsipsp*TrxcHY+<7(P01Qu1@vuJ^>n-Ivt797hO)Ca_Z|hEI1!zJ za@naFESyv^h;QZk;0e!25!|mjS70<3g$>O36A)q8H852V*4R{)RdI!&qjOQjqRYk#Lybu(iEftn0(TYYdkI+7vBq$(;TdbYWP7 zLhk{8)3k<{Y<8&Tz?7canHBpp*67lsp=~h|s4U6cTagSi&T&gvU?aGw*cr%TT=s61 zyB3~V9Cj<-z$@}kj{@x<`h&&jmD2p)}GO>Fe(oZM|)WBRv-S zs%Y2rSXQ&QUQl>SR3x=O-nZ=mrbV|lre%$P~K4aDt|3J1Rf5Wlcoe{sU-^cJVu%`qUw2C zpDE;2R8)wQ;uq_u0yHO15F++)q2|ngc?5wGyMQ)Ad#OO?0*8oPVr!P#<2}u1F$ccC zoKU{0z9#YMVNE@EZxu3o`0L1~ZVK=`V)WcthbJ6_H~v0^%hFWb^0npr!v(NB`!GTX z+c`+dy8q$Gk6V~)Ixn;2&Pf$%w~S22+}&*QqvmKHp8^p6_qow9>wuA&xuNU^FMW`| zKNl_Uh&yHTMwhkP84saDHc6!}%|`S)!6jUV$;CTYNE6mof$1{qt)*$cbIFUHKg9D} z{S&+XSifBHfqldZ+_m$WR{{ z%PWZRE?Y5-twrE-S@4Cux7|73Z4Vusddqohdd?bz6*Vw3o6DJGj~dN zZAt@uyDlW6brTl@Y#q;@#qlEwrZeAZYgm=f>q z!HIrHpQRpei!fy|rOC3)yzj}{@RRg_f$PROXfYa5Y*wLMNWATO9Q-;G#q{~wq zhk<*s!;hM3#u;Vv`da9l*HVVRP9U`1n!`J!%uTJoe#_{|8(=ehizp>*aXHXmKx4=P z6ZLb)bVPJ3?^=#hFAV<<;V)mXSoSj1V3qy0x67(gW0W<*eZ%Qe?~|0=&re#Lo5geU zCP)4hsqJ_za))8L=-apYW2WCJ7ZApn7r@dwHFALG(<=wb@RCV{-42+?N|jLg()6{ZUe~Kb>LbzNg|IA<~+6{CLnxtK99*^@s>-bwp6fd~Af1 zTAZ`e-#KUsmnkTQ zAq`xYAKZI}-T;ryIl2B?kvmVf-9Rkzz0*2(>NPjY@9Q&TvJ_Ij)MsV;q)RgLy#PJO zsUO;m6y>Jlt=3#M(k|b`t%@!zU9u=d;iL81lxTSo-p6VBF7oyA`!4tK* z2X3N2Rt8Rb4N zl^m4=q~PSYFlc06D(P9sCr@FFM$76uvJ(4jb7V>93`lmMLw9Y&%l>=+HR5Dw^(^2v zIq0I}`1;Ok5QJ(10%h(Xzr?`2uacqEd}nKiF7ePM7$zqP>3biqb&RXN{)KYh9HLs( z26U9b7O_7z^NIE9+C7IjXAQl@@su*xEiToi#d*pE;dR=*y*6p-(`Q_N|4yObv(2!C zY*_gLd3M`ilprP|zWv>MSZ@2hUd3L#&hO67_<#VpIy^5N-Iaya79Zd)z*JDtwb3ft zgKA+wDNZG)9*8$9-C9h=q|n%}FnIB(L8vs^HY0xEarf$94CO^Z&GKc|Fc|?V4w!`0 zYf~bTX>?j|K2LP2puWeOqn8jHnAt35p+Fyj!;fxc982-GIYKFK%FhBqKXnS{rH8(QCl1?FhGiK~mt4oG{( z_(DtIAU&n2iuQ!%Gal#Xi9Akh_?m5g2x&>uUeU5Pb~Uq1@84W6Fw4TRy5qI2x39Iv zxSUiNQ~mS{h1VWT*Bnqi>^fF0gPmNBPvm5?sA)6#&MqPq)nEO($u%V}-W=BeS>1~DRJ zu5HcDy*4eVztoIr#eGl)?32RurmOzl9=G5s8y*;ldgl1JHM08G z)LCwr6H_I)P~*4NZD2!w{qw-=EXQ#1THli8hyD4Z$IV1+ z)@L0|I+=bT{oA+hB%b8P*v7ZMfzIwf3;*o&zbV1lY2{>NKA`%X82pw-OvSNGay6mp#SUOZP>yai}XJd9{JVLgksCnDV=wMo&X`D-ZxY5khJE*g~8T{Wb- zCb81DfuT56Y_4u?O=;qmup}i%02V4Dkolif6VUF#*Z&71*Z;TX-ae$3?F&DKhhwQy zTI!+1!}8BtLtUsq*q5XxrrG*Xwq`T{z%-qmn%bxf_8{#OKx#U}(+?%>Z&4AEqz0%u zT(Y639|Y_nDvrbI3*(Cahy73O`y*NPxTYS{lmk=iR$XtWx~MC zY5-@<+3fiEnlbji!NG2reO4yH0aneO2CAx`I6weE{g9o18~OhI*!<~@9Zt#{?(( zkEiAIvt|KovHSt(%={!Bd*xrn4ZIx0Ixl8c&60M~iAv{zf@;fNbzoCE5BBx>kh;AJ zo8HKfTEHn-|Mhy+yNth32^Kei3_srnda>}G^VQns6I52$>;HwXdd~RHxQ5JRWhAFo zd>_aM8!SO-I0INx_ug$70M#^PG6e;kGfd0ND@?zO&tG#}`RU_#8o|uSSfrh+%YHsj ztDvAAx?%jJ7kwulz~l+-ubAQkXh!ol@3^(no})bnM@B3y`vShCduHcz9Xb0Vlt3;R z*XK(g+t}UN(8)~W2}f&`L*}AeYBv{FM%8d+@sDw+)q53kf)F~h%*4-_z4nB};7%hM zW0;X*aidy;*iR*$JG(BAzk}<#a~i0d@S={q_DtWra%aq4dcD?JC1?2aJ(^oW3E8g_ z%Yd3kj_aJBpEozs9lLUeco12%5v&a?&sdEVqQ*xXE35Xsyw_$v3|VVZtn%&k&S}Na znW4lf)Seg)3JDHDgZGKe#br<`!=zN;oHT$?1ZU z1`(9MEMD`It`SBxwgdwLf*u*^uj@EyEsp}C$?wp_IrpGxQv6#^$#omeZ zP4mLda{q$Sy0qn?(7~z1RyWkGCl${v#P**~WzjWiZ)A{plSog`6ZLBPyj9< zpMis&-9`nAs_)s71GUh!we2;zIF}{C8h37>F-uS71WfUeq?^) z2vf$w@xt|=M0Vf!@$qSj`Z`D zooSTm)~fiG$@G#nZo?q;*GGmF7D^}ubmK194Bv8;nj2`6o^g5w9Xyg-kexob@CsKa zGMEbQEQ+Ce-@)tAdFuZ_%CA>yv;};!x2GZ^ir$SE-Z1p(LI*}wZO^N}FbsX%wxg$M zlB;O4hIJJF6}9sNfBa9K8PDTaZBIc2rMMM1C6-JxFUoevdj=fhv`qXd^nT*?#>$VJGrr;ib@6P89T9oY2_=XP&C_+WZ5l1C#fjR*E=Hc7v*pF)J4 z<-V<0U2yVXfN{Y1+wmad04~=13G@Ol8_4-%+cvYZ&wAdQc8V)%{FF^3&&H-NS`@Mpk$ZtjS5#sSf75wYK!rN#js41r|ij(|r)AfF+W_fN0aY0tboy*|2puI}_2 zaO+~gf4}hTI)v)sTs^&hKJ$P58@y{{sGF4W*<0IL*7vaY zIofmb_a8rfdodYU9na&?_vEpi9d{KK6+1gSdHKebm6dQd(a_|U!`)R&OG_4JW>!{K zFE6i=b?XH<9KO7~+!$6>P*6}+#m>kJhxgiuii*a>{M-Mx@b~ZEqobq9yJ}ls4&BG9 zy|bj#4g0#gC&8lv&ZjMr@cIlq+@XmAMpb1#`_5=I)^992Iyyc+{>qgrI=Ker?#n|3 z78!qYzI$vhBF$fV_-JWq#Tn%2!(IOVMk0}@2JqDC>gvu;)fxB3*E5#4jm!g`|hoA_o2d-3n$a7gv~k>xn>x2_WM~?u}WlyRP#aGh6;0SNkl66o(*+fw4{TB zgZB0p7RPqCw?&QMDwiEjOsIrE(6>nXHR;%UoIcM-T-8KFBI=-|q$Hs>S!DCeS2Ih? ztZ+fF+*-P1u!2&D@e;PT!e*wXa*_H|gi6PTzjOr$2m$-i3L=SQK@}4dla-YPc2!hF zgehNALPE|5>6H*4U+Mp^CK!jqS-q>MsCexgbzk@iKKfgBKO>h+pU;NUsY-o2b3jN1-|tuRrti-zP`Vqudu1~{oicr0|NuAtEHE7$^Chf z;9VztQYgcEOgtKf@U`X1s7B4(TGTp?QW;v_=;TjfVc91+*N4W(^Mu4CBv|ht_;kuw z)~B24B11j3ZQ@Jch)oN=s?`Vob5|^gf*{5(BS3*M)~as&{*hGHdfZ+Sdwg! zX7;uze!DarIi|fr^od%uGq!$wfcP-fq+^5pTXHf`h^*y}XOa*Tvog0t*C&>iapYNE<#vh4 z9Sdd}9UjK+C5ny&=M-C`a?NL_cxB;4^d{299jv5I{0a5dj0Q}4tF5Ouw(vT}c4uSe zdHCS?IM>PsA!6yoBy!;?ECy+n_f+n=Mdi)iS&9smIe3#(sV0U5VxD@mZeycPo>8N( zK_inQUU*}15hHmU^Rm|O7!_;mol;)DpWl6tma8-izARLEG3EJ%Q-2n-JXgoVfr02R zBQo0B73nP8^;ZW528dW4!JmIBfuu z=0%XiaVP$J&dbdAShcp+)S8GxKsWy$i6Y5C!4a6#v^|#TFSkt^l zDZaKl-I_u*vnfY7as9<2nT&+mpRa9fl&OUA7P;Hqe^I&hSvRsXiE*y3u2#2&%cYUJ z=v>`gshn8!-{DdReHtAY7B&F)wBZnXinfR2#I2k{TjLD4tl5Mu@py}j(jiBcjP9j^ z2ACpz#74N-G<2Oc_azQX6{qd3aK)x9_xgMurq|P(saK{TJ{wOcXT}?*+VwH#e9LXe zO``mVjrgQ@a}cNIDeAPa(3{1;Bd_clYN=cl`zf7?GMan&n_*D#o`_C{jCpxg#NUp# z%b87oq*~{dP!IT&(Al*?m3V#&?PkM@=(r~=T_rS8p~&VM@7>%Pq$V8PRhhB+QMwt& zw^!3UEwQRea*AE@Jvdc&k6d|@7Hw>^N^R#Z6NjdU1>^k9G=bCKhf-XvySwA_>hL`H zDe9KQPs+6%HC%XjcvDl8@SoHD$#_q9KR_)}gJpj08u*ci{{O*v=zl+?n=GVSTdwjw zbocc11lWtaxzb~mL?UhVpKeW7Al?1T8z;wyL?Ur?bhOk+PfrhtB!XZAJa-FqbG{dZ zlH6Q=k0u$vqikxvd-r%I7VM0T#|h~E5uIX?g~i1^cUMN+V$_yl-$iU(T#D+>#qb;U zOci6>+S=?wSW-p~+XQL{$g8#gj3Ubr*3F9^g#bqt6{Y*?8XDrcmUT{ko*K?s%KKka zx+G;~^Xjez^Qi0P=9dCg2%at|C`fm8CCtv&Htf?USYV}tqW2RMWaHeoZ_FObAj;mC zl*GO?ZvS1e)ipAL8_i|$R8${3pC_yqzVP)&^2)$~u8+Bixw*VQ2yDR9^9;(T!1L6F zDDNvLn~wed{k~dz6h|Nsa&tT1?o*R|@dw>!ZDO6P=4-5udUDgG&t~ld82Fap~z#0=c6+1&nKOtV7%6 z{zNsV&X@;(4*cole=aWCAlDF%CEPFvzYfhL=RD2q&3$7xhYT$buj1!u8=K;D(1C4j zeXqkEy9_rV^4B)pz&>^>#03)>jw>3_%NHIg#H$j>uWo2TbqLz8k1_rDnmbg);hw#64eRFSWC%bwW}Rc!5f6P*GX>F;N;r%FmpBz%uajh*kQGb!qc(H}Z~ zWt|Mb@k0Gi0Mik79jvU>AuCHuRUWHG18#J6L93OZ40;p>3Hxd;sy2j%UX7^wfQVLr zH_X~Oc`>UNP+_LS3F+SphUxYHvikeXGLsomqTn^?-(-o1Nw`eL^5!s4PtVQeV?ux^b25)bR!4Gj&Uo;-OnX-zQ1aE5R;hLtGA zs!OV{&41$cd#O1~7xXRTS2j`K(2zKMS1n1G8&TkHWyN#CZ&c%BEl|7+sy({cgBqb! z_jD2U!>5mZO-*k|ttv|@|LhUb-t3-zCo*}U6gg7i26r(@+Zzv~jaHgc@`(tRcL-jFguq&{?x=}rGJf^(m zyn$9n4Ly7F^H!O>g*S>Va^FK4`{-V%sln%XdUT~o?a?bZeoWWE=;$(o$)vhKTzvet z=$V&u!}46$0_@6_F|B`wBhimp7ERx3it!=ao96 z^fU|r<-pBl@=Ws3u&kDnZ3j>erURE`LNZ<4G)o1m%sfq5P zA+N%_$2_`2o3_yh2nFm$l<1NODu`>QuFQ3RvVkVUAc$T5Qg~i=rW*8K^hcag*0rXH zR=3dB?cH~)| z9q5gSlb$=&CF!@t9$@#GtX^P|g_gX#ySr$z5dK@*zEuZ-RyjCU8niStOPuIEDIN0uFw2HyppAH~eSrKP2v z4hM2`b3c0u$&I0MiDwgQ*)N`5bUSCOtQc$Ka?e$mJRZu#E` z{5xy59c?YwF{kTKIqyeq4s+gb59_A{0kjNKxU0+aI2K~CXVgk5w>k8}jGkk&NgBp` zuA{wO%-_R2^DH*N^6_*3&2N?t<24*}@bD|l9HnPUX#Ndkaoyw(wM2zi8EWD|6<=yq zymuCXw<(jj*xKzG#E^hC zaO~R1or^-3zN7!(FWY=TUp8;^(IOj#evNH$OS}=bb34LV`b5dof8~Yhh@9Aq@5BDO>?|&G zd1}j-A65N{*k*fgiG5BL3spZNjpd^%8c!mn&ke91@~0LNUy z9I*cTrNRw9w(?nus~NmeLk4|5yJkyG8u0X-C6H$_&U_tyCwyjNii^qM(QgrM2)cy3 z-ON5tT2przq*=;z>nn(1DO|5D)EcVSAc6B9;!aFVOigrLXPWf#_Wrjw9g%N^|66XC zqW{-nLTSD6QpaW)g|7MBmoJwp+jf5oVV|?1{&Wp~AKh63tLRaHC|GM2pQ9LXj+4AJ z!!Ca=Lxw0DErA<~Ep^H4vKMiQCtK6D2>Sh2c~IXQ!6ddxx&W2IBk`LW`}q*u-qw6~ zI)HkW6hy0kw)XY_OR((Q4!2)`o_iv^{YwoYfo}YF_7fA$#Prx-+-i5O@x1cWFD>FV z%;^HE^4<>t+pEN$_%c<2JAxL$u&++8QaM7 z9|c~piw^j60C!;Lt${e<3d^rjyM!a2j5+7Z+I2*sm(s61E(8YvIQ`w(^mvPVI}k7 zsImDhyFV+-YbyDb`v#3;Agy8;u6>OT9lJZ6D<+bh!m+IbkVxGZx$?vOm97m<8|1nE z{vm)xcCYD_FJazo{;|`Z89%#-mRO1bM*R640Abok@B7x**G=s^4A*SspmGg%)gc72 z@J)TG&aExim775b1Ef>gZ%CxhyaL+Wdw4~$qstkpJWcxTnd}z@=l>4BKS!b^z?Zny z0z*qN?}IP+yzxsdjw<6`Z$Zr^;Wi9@OrM-J^+e-SJ&#P5+c!HE7j*hr08pFC@Em>>dYT`fLKIpwo6ZIPM15vnp+{R=KNBxcPj7Ix zNO(Ejo^s)eX?zenQW5%)PGl!L)jh&4RtSV|_W239!n}s<(Q;;e9;b2Z-;lriL7vuenwsDJ-PPUz{ zk2oCmOd?pFX-dm&oHjQ%MNn78MdO&iTaI;qIs_r(@>iF3PK6mC=|SjYc*+(wwzRZ# z&b`a7S`+>5MTl<2yMl@ejDWP%-Di;@0!p&+0!z$*O`4cWyK*n;A+nC?^1X^|n}dy+ zHo#!tu6{w*3QteTc1)|g$V%1xL3Ku(T8F+HC+Dxx8FC|t{= z>@7^r*3gu?nwr{oZC?1NyjWpIuEqjo3jILAAHC*ksi==*gM$S|aZ?qg8~J>t-={Fz z$Qe^_yd5G#qO>uuus2;XVrnvV4pUZ-dvR4c(bFZ<{e38rTN740TH`C}r{rMKAjnu4 zx|!r&^EI`GI!NMhMXsU^zu$bQ2ajzDFF!mwQyX~SfL4#IQB<7VR+#sMMK9D^1y58j zBS=e;XGSmXdQup-5nHWP5Dyrr2pL{zUn^@AG34;zK=mkv7vSQL*n2XDXO+WL*YPa{ zqp6to^w#fha_J(J!<26-pZCH_rF>6?#eRLJMD3M&#?M-9WArKDwS?9dQ> zTZl%*YDO#L=I!t@y*$3(l+l-!tp4GAvS)%1Ew;60bO5!w3H}5Xzfr zgPg9tS#srj&E^wHR5JP$5UGIqz?+`$a&R2 z1nS-2GM=!38G?LIWWf_DS{hjEyOSjpNqj1dfKFv+&<%cC{g@pnJh+f zcqB+W8bKyl*z4)Q#YIGBk8a|ok_SzSOMs_Ly01hee{3#xL=R^lO9=`6_3g7^Nb9kgRTWN zv`g2)RACgshX|xkC*B{UdNn46%I?+Jr@XMQW22QG(4#0$LUrs=I+B&|(E0-^S2ENh zEr+SPxSB3|sQAU%-d0`h_O3T_f-l=bzI_%JwTqeE9CoOksfO;iMBask&K~b<2Q7O0 zZ_cW$D3(+LFFPlxWn3ZC;d34&A!@Ii9|su(EMDNlpx%V9q;=$41EVZY!`$)j`JN~1 z9yh_eM}ZFC<+QY9<+#Hn(;vu(cv`2xoTwb8Vxi0%D(`LLqMdPtk?yQD5FcLo z5e0!&#Zy9hjtaCRYY_7C@>*Jv;H~TS6(Ilr)~~ELrt9UdNTO=c}DDbWTj$Ic@fqo+>F}ZdY0KKM9 zUcGuUX}~>hcn0(%Pe1t{sf_08ZFA58efjdy3@Isz^R_O1&U(~U=-UWkY+H-~T5gw` zDd2p2Dh7k8tQ_VDG5sNe2kFWP8-Qco1})T){Y+d9xHE3!)LI;wvmxm}!otIIEU+)F z7ev*C_oka8Ch%sIgEZD*{5-?$Ns#v$*w$F77+LH?CBSNf#V_8JvFr?GPi_w6Gx$@B4+u4a5 z8+7>maJK(H8-w;7S~+)o4tJK({kel{A3o5k*niNu+1xn1zwhabVQEbUw19{@vSVRk zp|w>RDVF$W0Mh|mhdoeLRRsoW!w^EK)Xwm-x3{;pUI4@ftKAsd%BswHCN#7H75n<} z0%fXLVYoMT0W1bu(7)%&Jm!ykld&rJ(&Z||zf5+ZBbpiK;BEw>EIf+E(` z$k@HFCbkn?p;$F{QQ_0?3_E>wTL%Zp@mX<)QDnEOKiGUy6a-kLgTrz!fUQJ9U>*aY zS(q*IU$byciwSha3*-{tBtzw*IiVl8q+X}2J9sJwuxfExmwlZCnKMe3jg4(H7eiT2 z`JP)KQGw%(^{b%-&zs(fwIaX?K<9yBgi9>=FUZGCx&?=S=8QqknlA|QpAUtl4_R&-5Le`$G;bck5V>z|f{xIHeXrzdc@ zD1YT|TWrnbxR0j}Kdb<7&BD^McD83b4|AfSKGZKuaiFL;o_AtS%i=>ZA$1*W=74NL zP+lSY7pxPMH`8BiQ*K+zi+RWMB7(gN<&`BvdNsEqyreSa3KjBayC%5~9nY_S5wg(z zY`#}@_oKq|{%6rp9jZjj?AKV8-Z-Wjx{Io!gcG_t3FGcKY%s1q4^vs*A}%A{VOwTL z_}JRkh?5OCuM(@wvheVja-P*RP@A*DD%*fb)VBMvn$E4^p&{4Qj%arB$wYjNk(;kz zl^FRy_R;X5-Mgc-s7NbD@4N>9w8r$HAZu962z8NYZdFM-DAoISu=vBy(NX>1RX7~< ztmogg%Q)9Oy7VxA_Tt?N19ujutFjl0dkT8C2NQd&FG=KSR*QPDE&tWL8wb0?vibhy z^O6Is^nDSLOv9`}n>&~gE|F&|!Tv3gX%K^_trZ7Ysv!PYJxSKN_z)=|_sG8)@rh5O za~>8?&@5o%r(=I5Ai<&I#two4QC4VW*xJ^%m~Ks{j7t7|50qHNHMsiAYYGzJ@X0$s zLjvJpXKmg6am4>EGQ4=$3%ZGL>GFJbl;ai$&5e3Kh8}p3_}AK}k~}bPyc-*PEgpI2 z5oc-oEG3hS+qc2q-YcCC0Sbf8Mo)6d`YjzLR#P*x$`mfXv&bmw0bDPUMs8s*zeSME zUVj<#Ly^0rwpP}D6w~%Gw$Sl~4m7%XX&}Q)w|=32pZN*KQ$$!8`kM7fCnf#Ba23~9 zd(i*uWCoC>3qYzuCTop^3q)hY3?_PDpU4riCle(5iC>`Z;yny8^r8UUn7^=(3;q`dGmX|4_$HobxLRoe+tC|*fe*E}xCFL($=6?o<*B_XS-R|En z(5!H;+`&_+%We64ycUU<6dPc!`D>{&sJvG*9#${>YZo?fA)RN_dhqa`lDL$NXAjgk z+`=&IJu|+)f|UCO=xEYuk%rxSp@g{F5>zs5q-jo?HLXL3ndrlmy1m<(C>i$@n^nGm z3XmZN&*wGuEqsJcmOSkk;SByk2p1hGK~)YNw33*u?ci!}Gzb(xsTXR!qvmeJqEv$ai%pQ6Y>Mium`od6Z(lK4J9pSCBQ8uI+WFE6&Z;z0P#yCZc+MB8^gN84S;O z0_5T`?i;f!mIv<)ZQemme;%y)?4G#|_gfvSUK68*G|K^QP%q>d&c?zmc>Dcy(r0UJ zPj4R$*9#4QCZ8o~3taJ4$rjw ze;jPw`Q)+Q~a9H>^7&xMXE)^{4#rbyh6pg(IsCpI;DtF!+}H z(@5G>tyg$vwOON2d@UiZl|##0GkGXzkFsE#^_&%b!_o4A^9rbwii(Q9`BC`LQr`c3 zk^Do#uk`^WAr$mJso!?-CBv#2Ii(|DX9Q6i4 zE>eV^f!zdJhA#_<5HI4~-0W-=xwysYZu?M={Ga`~rhX!EVY{ynF~6Rglk?2{=myIV zU8C!R!TQMUSLXzM_BYH|_;B>}^h1-N-x^y31)Jpkm+_hLI}vw3Kco1r{{AcutbkAr zd2fh-28?t$S?{k-9)b9sE}0bF5A;(hdCsq253tIZx#u+{BqW4!wPt%Rek2*uKM|U2 zZ4IsAI5tji%t^qibUEBS+QTbGIEn_!a9q1a`_EteU+s6hPfdMyBq8$CoG-6KNvkpZ z^22ZauUPw^n|LuOUDwJc@u9z^Lhij=Kvu1Uo`b!8CljArPoAJ3b?^vJnro_ymoFyA zex$4$Na*?srmaij{uN9W7k-}297yTPF3+jY9LTwbqLB-|VbUn%a{F4N{LCYH27Em9 zvRiiFkWKpwJFNy98k*={CMl-K;-r<7q9=_?)DwI2tm9O;OU26Tx0T4fKP6bgtY<+{ zdw_7-1%PKrN94+~=Ca*#@=bCqAcQat=vT|+E|gwW4|_3#@^|icXevH(p)^T5I>-jWV=}3v%E>VGWkq-q=7f`x8uru6+*tu|@@3&WC-#JvSUCn09 z*{yisd~OM`gn!$DT^@FD9M^Yg+ymTO=(p{hlcZKr>K|_*7kELPXs*8K8w zQxu#SiAP}f;jae}yQ|~XeRo{I#FjCLHdaHpw+5>@)TGRlQ1pKuCjtr$P`91&=>+4}ZSU0;r^t!Lw4OrM`X zNPkH$CC|Lfxaa$U&O0;?^#JX(?9*#--fg8#H{_$26Q~95i{b-Fij@*lY|prOA;ahG zakk%#*_`&d|e(q}A>>+Y=t!>Sc}GlRIy){U#TpXG3yXrt_W1>$~* zsE~2J*7z(DZ3^86AQ!C7?D<5LvvVNl1{pV5#-?-hH^-;3l z*m_=+16XF^nqUy6rqdL)0f3iq^~>AbSksCaOCMVxmxVnm_MHnJnkj?x2Ds&&!ts?h!~S znF^S~mvY=<-R=XmX{YhI_jw2#UpePq{tn+_qxRN_*2(g2CF9SUS|djN`#c%0HkI&` zgDK0=tJew~B0HLY>z6xL8eqoi0UTbMVKzN9BpE`7)HYT9v*o&Uz)4`%^i z-K_s3nil{5nI;y7t?H2R{O}ZFcK#Lz}N^Yda$P1bzUw}fUI%9Oceiv|479kmR+oq_&)I4+;U!&yUl zj9@Cqa|aa86nM3Vu0A38Y4gy_-m){Sa?L<{JK^%id}{<7mVXWD#FZ~Us3fKL4~49= z%pEZf-zjdUv#`j1=4yVcv)$954WfEJ@g)T$p!dgbR#~psW@b-s7gBl{y?gQ1gB?nX z=UMdeywCr478+_%?xHnPXh{rS(f|ULGuedjh?3Cc>_KsN81Gs53+?{gB(EX>J^-jKUx4p*t zVB8J(wHmMO*8Q*6ccPeY-n-%PhbKMw+qqO3e_Wo-6Stv}Ky7`}k^jNMT@SJFmmRAO zbY%GNBa3>6>9)Mbkl*sB>`T@&%T3sq!Ro?Jns^|=GoPpujap=qHTzmcp{q4$7n%FM zHA^~UB(s&R!uewwN$`zIWnp1qe*S0cNcMNz?ZX^8N02JhckNf^+VkI+P1@()*qo&0 zjQ^X4GkWoMv%xAIr|0eA;W0^Lx}aqkeNI{guHTfgp_M9NDDPc)QFG=>+ zNRm6SbykgAKb>&R>J(!9r;>Fsb2<%QUtid~-di|BUy5Nj&N`&Dpa{ z%R@tPqnF|?^~*APD|;F^@g-Oby1-{!o>BZv9{yikK&v58I%;1aU;Y`*xT zo5k~vhwM3E_yRV^&&Ow2>CV?XOfrd*Yp5Y|s-H-0(+meg*hT4iPO!3?dsCrR(rk62 zPp<$Ooa3Fv#YN!x_4+euj;|aq%2Led+c+xSQxxF}pVLmE8C|O~_pK6oi^IPo9NeEXN>v2^T6(E)dDkNsTKrRjrBFiP#&*))6we@3X z6P0@NpjW=zilG7oracPKu=1NsBtmM~Pp1bGqHnVOYzjAYnJ%F96mXb(QgGUVa26ZU zj!V60YDh2nFA9bJGcqz_u&0e=$XjqTUBOd^(+F z==>a77euA-dIMt<@9E7=w~+C@68@yVd&K$t2TUz*x{VC;AGy@+{2^z9h45xtM*UFG zG0qkn>FSzLg&F+uFoQqfcGgEy^mIdkt|rRlF6k0RQUOW zFS_H_lQ*dsBDd?KJur?gZhO3(WUPR~-lUy_!(8=V+IX0In>E4q-&g+DfM1uu{6!tD zo?Kh_n+MeKV`F2KrPEE-v%$^8Gs?B3OhilScV$B<(c8IVR6w3@)q0oPIRu6_=qAp- zqhSd7))S|^ZRs5lz8Prq^5yjv`sc{O$w`xpoHBv=&zf|DwNWpqWBgn#q6 zM9rjo&;q6%dh*h2{r&xCyOrZ2c9KX2Br4;(IAe@t$=9(52nHdDBFwL)&~3Xk3x;PhKrWF3+j!Fp~3(sUyyx z{S>5{59nE1JxStCeXKSD!Si=O%gBYdn^02H`R9+M@g!oYAZt4gl+51UqoaoH(iy<) zV4s5zk{;&wygv(jH&A!>j|}o_F>@Pc#D-cZbqu4)aydvur9j1{%#Hl*@@qik$>sR| zFxUc3A)ZM1FqtruTNb>K7WLIT{Q89t@e*(vn@g!dJDr`KA6W#V?h=x`>;hp$?pg+^UK*-a zEb;!+wW`>EY3e*bw^DPWGsiAALiFjl+X^*D26|?=OTu@s_YRrd>yg#6EmuT09U^es zL$>*ekcok6|3BrD;2|FR(zY{dNm5RbP;KUadFMc1^g?1fTy1FilSHkK5HY8`^EYE# z_5tEc1D#W7W$cYqN{nV~BPY>2nqG)2aUSlcE3YbS7Cs%C-uDA>Mf2S(PPjNKmvtJH zTu-j4z}Z=6P!~0&)v=f}xuqfo@i`YV6EcySOWn67Z_dHramQi2uB8z~9;XM*XJnu^ z2}D$}-NLV5@1jqHGEKiS+n_UW8Rd;g#qhig-N%|(jTe%R8F!YOx|T~Q<1Nr1yW*_y za#}9c1Vw&pab9M0temGckm>>BimvI zI=9qNtW-{3yB{j|I=;jl!3QW5336Lc4N2S*(Au5uFJWQ{;>}A zfZ((_c#Bi4lwaESpbs$OOe&LnzkYnD4E|~pNE>|v(e1l6uQA3Z07Wc)z^y+@9MPxk zOKtT(S#`$tXvy&;COXpWC8%|IF8D z#6T4oD3P2k+&EaNT*WzJs2k~{0gompeP>fcR~qj*8T)T%*nydqP#-JV;x_tNwrxb& z`HOM&R~xI~i@hFS4$JP$AUgzmU+eeF6jM%$=Jj}th45|f?qX-zhh8?j5}WuT)+eW~ z>6~#;wjdl}dOF`#*PkvCuR3#{2?~A1^pfc<{vPwVu$KXyMO-wUM_hFDqs>p5=Dce> z`#jn2pjtg$#iVz_9+!PFgQzadPN>gcwRPN8tBeUj)Kgws3f)x1_;zmAs%qZ(qj#!n zM3x3tjsd}Mba{0xx+Z!>(uMaKuCx3%G{&OO9$LJ9=vumK_c|2t)lI0 zK!d@56AnS#Wr=N&79qO!8G@^U>bEs&w7A=hdC=l+^t z80urpHKPe}eaL0SQz%jwWTvL%{-2`u^ojqE9(ViSFYo@p;mqLw`w{ zg@8P?lT$ZET~HcTKal5AK@1!b0Te!V=H}*;QiY(?Nl{nc-_uh_AecAvGZ;V|ouWr8 z+}bGWbX*sDZYMs4ogFW8dIcJUmX?Zna2SjolAr@5)a!w0Uth!KTp(Jeft0C(fyoK9 z7KAhox{QgE+DIf&-c|eT_fXV{e-n4)g{Y~hh(qe+pFqy8A;HM$@yX$8M@I)B&%8k1 z7y=_>ESPLDh**s4MCiEl1ZBX-kB|!!Y+z!-`Q)Sm5Gs=3-=^RmnxK5Fjf{+BFM~{& zazm*0&VnHdD`@obv9Gs@2POEeyeX=F&^5?r0V5RCb<_}+(-L*h3wP{dZl0lf?e3jB zxkl9z5)xkf8(DBCph|H6$N@=!yHE6-hk646klXL6)BnX1@tAUr07f{rx3;qV9*T?a z10fq2X$TGu9_e-mE!z@eGJK)HN! zz?_D^x~l*#^;fDZunpXZp7MS`o|(VD?25bv7_+Ual5!~&14{eeR{c`@?VTN&*K02U z{j3Fgn``l+4xr`U)ZEMg@!_kOx1izo#y0eufIH5wuC6XF7E;lve6KGun zgFd~KP5OAd_cwCsZjvG~`^%-{r2c-e(>Q!e=R@dM<61wwpi-iJt~VGLb%m@Uf6?$) zI%khf3l-)V~K_SXgMM%(nJ4=q!UJ05RU5JIP5&q~Bn~ zR=fJP%&O1aLSHXM9dE&_n1}f%Fw)|N!E7D0sgDeLKpbULEiQ}(5)capgPoXHERY1@ z)2$Y9-S7Z?oND#L#N=dSef^?HrE0<@7!0;0nj4wjH05*UX|zGEVI^sNTvkM6=Troi zZ+jD$mxtCiVSqS>y3%*4*&mgVNQtvaxZ7IPSPk z>~Ws+{$E@`55=izmmD1S*Paj=4A!jx-{3xh($fQyknYbwG%RTh*FGQyKKImZ&h64= zg*YmltAli2N)!V;6dt}52{>Xo)yp$wkgW|sTo^% z{ZiM5nDyvzH@TcriPb*xo_pn^^p4KXlG0Lg0`j>N#PJ5`d6(1hHyUE0!-3BQ?j3;M zsrem78h(zC^?_|1LKU48!1;~e7^P4o+>~%a=z=hL#&)4?$s#HMuf^g^_v|K4rE$RURVlc8Ar1%|D z=LGefIK|9cBo6kn1jd#MzpPT=L!b@~N&Io@l~>ZQ+=I-4WhTNJFmAwV!LW?gd)3=d zVgy+^o50Lj2~xxFXkC%0i?H?FTbq1K!zeV_)1xV+hQ{qpHiQ6p4|*ssV(^f)AlHe5 z$q+8!H|b$DNV*(Y>kS3htvxFpgV6x|n|HLtz0L*@HIxbe& zva(_yn1iqebGbU==3sY$nufPO7_ALLuNPMQuN@0L#JE%W=gv!)^SOZKp=%&LC(X`U zWd5-X-|SVB1wnIO*Aj$6b`B2EYr<^rXHmu6D1KAGQC8+m1Gx**fFs4b9|wEuuM%ZI zRh@m`)KFn{d3m8%UEWFK%P$n$;wmV1T`O`nH4?QK8;=inL1(Ytw-Mqp3WonOd#FcUSoM`4?Y+-L4=IkkGGSa|f+Js|Z#yua)?;B3lQEWwd-J8S3V>2$?8` ziaC~N6FxE+v^IIgFECWB1)hgE-U9K0hq@J;D*fEsyVB+!=)!>tLBgXicIM!qg6>}D zxG)&BpYypi3uej~Mb9>a>>*_0?_ax_6+LqI!o7}v}S5j#IQkU`ZCB6y;|6$rPTEZ%~^Sw6X{w+^2S zf}ii2gik{tArp(&11|sQX$IL0PmxMrz@H9Y2)w{%3AYfdv1Kw83e}d<_)N*WBwhQw&9QatbhOQH z|6{W8G*y3ldsuxwMG2EI92^`-sJVxSPKYuTDdYzdBhZEeIhwFRF927V zw6q9*PX>r8krsT`@vTc-WnlCOJn(7_m@^e~6!+J1S}A%@ky6}!hV>B&Cz7UBw`cai z!sC3%7jUG9-`*rmbuJ!{q*kzu15#B;@-UGRA#>aRUb9T4@zFTLmvr-@UJa2g*Lmb$>B3{yC z#c_gCy&mL`wOHV*IS6Mm3140=PPSFJ&6LOlxD`&)V=U}0U(zBU2)w`nB;NjK@SitE zJ;uc$V$adqN}$g9COq`q)z_~q`N<;Vsars-dW|dulXNp zR)*pYAU{;2$3W+L1_^!SnyIU;otc8IO^v7nCJ~+R1Y?`aw<{_X4Rw;)Zo`dmvm#%+cB&1md0Vx6xYo zS21G%B&#cDL#UJzRj+|Gh@2*pTUwq~cZQ`%I0Ct4C*?Mf24g$VA#Zh;XC~eOFj*JL zRIQ6bv2t(_)`8TrxU4K&ocs%sp3{^js00ncUtx{bUOHO^LWE*QeETDaSQtlHa7YMw zadD$WR|dc_9QH3egTczg&w7wUkrCjek!=a~GTU;KO8|}=JOx$7M5NK5eVLcevZr}d zb)%xP(v35x6u9=JNqwq_sbwTUkj3A6B(|%-bi5G{3Wz(&L7n%OpK1}WCaMe}Bx-#? zyEb+XG9mU!%5ecWnKpB$_e9Sl!*a=uIwFwxQN>6zjqd*k@xduB_R9v43CjNzxLF{$ zL;Vu+RC(M6`m7}Mj;14Iw><3ZmO*p+KuP{;BMKyv3z~T#P76Eh>&L@xf&A_!pYJk-=(D`5Fmlv6WEVxW?dlMbpgCIj1^7*SOMxXp}7Urbu07p>YSbI zWn10V-n&bl^$^Fvt~0C<)!$xmv`f4-f$Ng~B~x2}GKBE6uTY!~&`PgZ z0m9V8RkkMk`x;%xfbs*5&36IYJ_VVOY5~YHA0{ABN~JSNE4bkC$K|9fW;e+UBE|@8 ztIk@JL(DY~qxFc)A|b=V4#t_F8Q?&OU0&q^kS+lfLZ3lKgX3Pb_BzcVdzV8v%IwE$ zmXD$(B^{wjEi-N4Jen9V?+Q(IJ5%J|C*dA+`1wB#AXOdjc}A9Q>twes3`&_`S^cQl zb1#4A4;V}*gZ_gs*`fBB)YaA1-TnB&Sx1FXaYx`hK|6oSX>vnD>B3W3fZqa)bQ+0` zK#b`3iaZz3H^G_g_F!r*g0y{;{GBCKkbR#jBBrGBV$^x~U@C(CPA|FnMp7c{p z=%-R%fEY)(v4bpj49;IdY5*GtM|cSR;uK*26+tS-9qz491IU5RHmLSO_F`m?Vrq{v z>I#8A9K?E%sf1_}qc^_ahTPqp@4o5bIDx_AiRAEbdX7pEJ3aR|Wx^*H0$kGP#%VW29R<5W!#A$sGnqyw;~@pK7an);$2)E>-na%5w?Tgo*YYn zCtB-427*zel>+0*La9yvz@EER433N}K_<=*1j76LTzVZ8%*|(xj;eAGc&SdgS`YV5$92nrR2d|Y~13*T&=jAcTkWNn1QEP4Ic=P1ZZE?2!rf@b1GZj_Uo`=+RqK`pg>bL@q zS6GO0vbAmBubjoDPJkSBqrk}3&5ifM|54s|g*DkmTLu-effZ2%8=@2el@0QA@trXfWL|eN-q(RmJoXA5CuUx2nHl{5JC;Tm(0$8=3LJ+^UTFLGr5eCuf5-X z_g;Igwe@Q(rjg2Ay(gtb+cz5zS<#LRWf_(_)w1JxddQb}mOIiLbE|W++o5OK62v2? zZ#!iGwH3X8SQr_@FmxU2u>fa9))S0mES4`pn(s*PuvEd!#KfEOdPs9qS|#w!$=z61 z+t)okx;up9Ghv|RPq-a&@>@*)-mr{SXeE!ZB*trLYMSA{gnW1?mG28zTk71-D#Mv( zROw~D=E$+}*90NK06TTOwA4lc)5XBpe>Ad}6o%SUv91a!AG1!jK!I4P_5A4u z7}r!+|JC09ZLcJQk0?HIiPdm&dU^{oI~TS5npkW(1iqMQM_4IlW@hfxKF8gX^wHiN z;x3lo%TaNE7wgNt0YZ?d2OPc=aV%k*w^K}9{3r{HUOSSC%AelV%O$lze zuePr4*^ZnicSP^ko=u1wmr45VNddDf*u+UDJe+$aUd%6xR<>V)v_fA|fFe|9{(;X; zVeV}@8#qV+92Dt?lrb_k?lx9Qer@n1MbZT{2cQ&a2P$xU0jOqZFvjYCq{%9_~{7!kd?4ROwW8a2YnIk&)!qtHHs+_=Th=msyi$ z6Dvt1WXP9~&7WQ~#EWvap9*EQQdCs5tB3vKgV~l58lA&}l9JMGIyHS!k2C~ZkYRP_ zAiyYG%}PtdBb))%po>5V-hiC?xvJ&sE*jQ`s8qeGeszEOtMZx8kpgl=&!q>tYY-;k zMU%4qy#e)LWrBH7b`fHTzr>l+3UM0VqYw9B*Xw$q7d1Rip&$>@O(-A-(8#l?7u#)a zjz4}#^FOhG_aF2c>!c7iB8KDB6BF%WD%#5|)28f*BM=h=D;5`toXxI+8Fz4OMA~lo z{??Wj-oBt;)hdo%EzQs&OhC_Ob+$wa8BaCedjF-E1yOaH^YiBy#uXkWZK_VcLe8z8 zD!!4%q6vG0Gvq;>-`_hq+1cS`So$ZtFxf{sY()YLARFbmoAe}ESRuj_V(pBeG(}Zp zg-qtaDrEuWA%S^q7d|#-jtsYkC}7)he6oD5>M+w~&OpdLA<-IiuxpLYn}|Mbk8rCZ zyfY+fZgg*mb#&d)O9@mwx%ZBmMZzKjLa75ROI+i%P3}|PU}Jcur+%nk3p;%HFl=y8 zFeb#rcmh)kSpKpEn}3XO2Rb`CPPE=<7~($=s^U~7vOkD$?}0 zwmiVA_`L$G`heW-1tS@+#lH=Zwq&_O`!p4>H*lQ7YG^yK8yQtsFsfr zjYjh;15jD9#1ZgX9irQ7IvU}HMd0F-wX&A#Yin!i23BuHs{x{EZ$CDRVTfl+y84_+ z(6FdL{?i`(2gAlM)OFLZz8}Q(Y+C?QirNjN-(+~W4+_+W z4@nFP2DbBA6Syl`;ctvG-kQp^{|RwJI6Tp5=w8fyD=zg6ibdWLzV@QR!eneXYSZc>^qDIPjmhW>1M{mMNnu41??wkK)rq{2+V52 zt)Cr0P<}wM>Uv3N`IHQ}-HhrC@Vf(T5tcY)<4(K7kHj3DoEBGSds*&;Lnd@*YUau! z?1K}Nla9NQLjZbr2n9If;5mFXrg(3`#@NYS0bA#q=eg5ke<2L-kQ6E(sG3ACAO1RX zg7e2-#7lsv6XOpKz0^RUM0HHo!95XJ%g|-r0lYQdKboHTg`~X9l{w6LCl-!~*uB5W z=-^0as;sPJmeD(hWo7J%V`=TvKIR@)R&P=`K1z7Kpxe7VDh0os2v633kjGHwI;HM5 zHrUeA7(wcbACB+Xpna$Wxl%vi2swx5LFW|V2l*doKp~y6fl!MDx5dBPnx7|D_6Go- zjzE0*>^LYC5b*z=ZRY=W@8tiW4LDj|Ua+t_d?$6UgP7D*B&})pDid&*7LErv4&%!j zY1J~Wh}JrPs-Fuy7ViJ$0R*KHwPZJHLix9@{?gU1QbQ($r=Zs?vx z&|jX<;o7YN_tCDt>Eu!`idO=^v+=WmQlhhcbE*GeDaw63E#aJ#pe#SuA-k@$-0$6! zJKp2k7lvrU?&s~K)(V`361KcsediDcKJWrSjG`k5)6hwPK2Bh$h>y2^Fqb=|rVx|X zQ-lei(vp@g&&ykYRQHpBmaWmhd4`&Yh&dUE+U|3^6;H7O0zyJL0Pg1G9NjBgP0uQ1 z+IxB?!CCGw{dAdR6LyF9lFTSniM4@J|Cv7j*}kl;%ZD)OTO7YvUgBJuO6RtNc=ivv zQ2)EEBfYe8}7W{cP$8d$)A!+S1IITq5f_ zI#e#-cRBBN%UdUJPtx4%+c@=KVOIOFQtKm#&nyu#5ib&Q_J@A>P*K}N;b)D`1A0pF4Z5ZS`t?O} z#=%@P%kJHtwzDPvHynzGnhzfq_KZVgY-Oe5D*hrqUnWULAONX@V!q~%xmhi-Il8v* z81A@;9(5kjeqHD3_iIK2x?>5U7UAwC&24SaqL~VuDx=P`9)8y;W>J^-Fw5|`DyyE7 zEse*Jl5Jw~i4f#CI&}l1ro-}FWkdmi;8;U&F}#yCp3LGd^oet&mikLmf@%C!rniUs z4iCC*X6ED@xl`rlPwt{+i;!Dc=gv7%F;d#obKSmw`{d^0cqpyIr)D`%h196j%Wd8; z81BByJwiNxgxWAhMgR$V8HcC7j*90w`#6KTb*H*HG9?klUGr~-!AOW$>^Fr7SW%c!-}1&Z@l-=y9};YyY*;%b;n2FdYx6QIKAfz?Ay(YW78&~xS??16N%th=g z_QO$p2DPM;W)=?`=&lVe{3>qwIio@=s|g;M-c=|du6XqgmKjjRO*+tX?z?p}CPc}j z+gG>c{P=gdh~9&nzeF}vf}O|bkS&knsho(1LH$fPVdlUFkhTRP8J{vf8GDSBW?d5b z&_VwF`!^&{r+lGsVWoUtbCQnDU)}=T5Ww|Ed_$9gL69;{C;+LiBjJHs2M&wX_^bIB zr8F}&rSY_DJ3%$^P-QI28dXC$Fs91=O!+G~e#I7nVXbqgkBA=pX#KF#9J?_tT|lD| z`G9#rr^#z)lhBdbWgK@JWu%;^l@*J>`pYjmA)(T3Hufd1x?sNzitN^+$^WI)`pfA} zaO71m_5JFbs%y=7DqXafRn<15LDWE6^Ihjv{-d_7bke$3Eb&%umoe_Eil(W%EZ=l( zS6Aft*+KLXF!(trqC`?0oN=QJt&qRZ5Bl)gF6HE`za1-UYrxBLqt?U6jdxmVUdihC zQMjnwgQ=)_9A-5zH0qpR{rxJ*A6-6IkI)3xCD;dA+4>^g)wllcyh8n8m9-Gxz!%Iv zKev@)%-qDkwjKMUN8#`$Q|rz-CSLN!it*Yf38JOccCVX1>Dy|D<^?sHD>2J|L@_sR z!hW-Ruiy4#0iylmcMfwRVC<6$zJz7Du0bCOIqbD;A9-Y)B5nu~15@b_%d zczph8&K!-PelBp$*hD54>lLpi-6{?0mHOi(nszyR_0tEU3Uz)m;1UV}NLU+@1dr9|BmXxrKnPYam zSO)wqs3867X{ZFIS-+NOp#yTRiW#e-mP^g0v|90jfhE@=iwjD|8~M)91@-c5vWR&y??6U>;<-ovl^8kw2R+vmFmKFLoOXTyDNQip#(u351TrAt@xa>)H=OqEbKG zdXmP4rKGU8a;E;odfE%mRq$I+$LQKo(EE#AQGZd}sG!iC>sHS~EIwJuP3Q$2s&8!W z8aRj0c))muJEyjI#f9Z-$zB4Hs|=`H4GM3G${JV zd#85Rs$w_MTSa{fWh#EM!>ixl9lDSn>~nJBOJ7tLW#^j-21BcTRS_#GS@81l_JL#4 z3^FskR|BOB^1N2U_kMg2)-L7>3O^Sdr7uc1J=fRwcgufzP{6quTl>2tlxLb<$)A-M z1ZG;l3ErkP>9NiR4_bQ($Ty#pmhk*c_wEq9LjfLjUJ8gD^9hq|rU26#HHbO}S}(_7 zwG%~Jn8}$bWh)ebSz5X}<^$_#V@G;13%*T=kJtCy(0FS2N{`>TyjEm+cteam+GI`1 zy!!9YSGvrK!n*F%xxVhF=S)|6iA((RLib5}Tgzo_kx7}o)pwtY(OzPVTy9>S2QTJ55BVVDSOq*PVqv+Cqkg+)9ZT|j~xGf z*^y_SoxcwIgT2fPwX2mzIu?Nha|d~o&T+E><{bf|KTVeUN-8QuPFVOPN?yI|Rzc%0 z)F^Y0j)HawdTm0=n7PZARE+E!`w@E5~>=%t@VboFx!7X{wJQIDUz4JEVUtd|RXM}zyZgdw^HSQ$% z?+`cfyA5lauC%8tTU7&F=uM4znv28QqPLt=1Z^%H)8JLeBIt$CsQo_R?L0r3OZ-{V zr>Zj-7nQT6ynprgm;&ab-J(0DZK?lz<&RFkpHBXr-8%_j0N^a*;<*dsHraFmM*F3w>|x}&@mD_i4WZ+E7|N72(dKH zy+xcF&{GY3roYtOsN;!xNK|7 z#p@6v$momXutMb>;-Vq<+*Uo-V3Ry}vwUXGYiEmT)-`8?JgQnHKN{ut*S|)w{KC%P zR>eNCK{@9T8oEAo6?fDFw;QYeuB~~@kEK&~Zbw+Qb*wchfZT@u(GtB{c@w?)Y&~V} z!!&07h183!?(L2Q<6=i*NS9`JhH9L@Wc2K7p=|8-me7XRikytqYQ^NW3oEn9##_w9 zVa4-ncU|h4ehuz8<*Wxw+HHunk(dhXmhNt1Htn_>+NLlvJT%ni%630x)!UEqc|&bS zbtk`T`;hA9aoP3wzA<-lv~dV=$t-*Q(Uk8er+BXAJt3dePiX$tyrOwebK_>82F@pA zUq-XVca0bAe1+`E53M;lDXl6sqAwSd!V~L?f3jN_u_rU~vQGOv@Mm59j6s@`E{hmo z_)hcf*yzTpdFpiXG27e|P{#13@jbU42mIU%XMkV8&zW;S9y4M7wceHf8UN3N=(xB} zpCXw+OY`To7OB3zUrFuJjRWi%U$KExfpy&n$gF_9#1_cJKa#fWF5pZ?a7Z z;S*6v2K^M>;b>LJSWcl4Z(9&RnQ$sH4AH=9DfY~l1kc6}4exYeIg%ttYs zN;972l8z#;vJMgFoM)VY@G$RyPJ$B!Y4oI zSgWiwo2E6+{**Y<@Z+}mFVpd}(x_!-<>Pq&g`4H`S$#KaUq}|PkT3o0--}RNJn-$# z$dpg#7_CtpoxPS|HoMMrodF?uh4nSwy4c9)kpeqH=>)@>hkbIR1{i1IU7|0oEEK$o z4Ak7@2{I`jPEO#yoY0D4xL9W`81R5LC=Z0lAt)%Nq@_6#L&xaSDL`rdOO7fTt+sVQ z(+;nt&;g6SML8!G7P`Y(`SS9lCLkAZ+L}0+P;KH*~j}2a3a{wQvfl9M(QT+n;TGI-PsG(1f+y>!50Yhe)F#( zrUO1wf26``tTC_p*+6O>^!pSv-N}FsLn{#4hlRUhUvqNiKpjqfFXK-eutQ(i%&Tf? ziXyaRCy(^-`W7hWgs*-Z8A+#oO{w`8h->8rGr#u5#r&5&ZZF<1mqNoANGReWA|*lv zFn{FMt?;vd;J1OvP<;~mY~vQ)Afv}e@RQ*`R#)GvPkzyXhNdXg9ny9H4{6;GU2_I} z(IOtONbV(|S1PARD-d$h()-89!?f}U0;L2VcmtvTR)&YH3GN=xPhrNj!a zaWvz!SFN}zQdgtrdbUw(b2E?d+UjbV&=)6MwGl_aJ1rzKbusfq%GRw|RS$fK%=GfK z&z7BcSns;z^A9$o@CadQO}?~D8y`Pruwug!VQ=NpHE>?mH~UCdbry+42tp6shD7K3 z)VH?AMg!%hbnezRO(puTzGvG z8BjAlK0aPC%Cky@o>HxiJNvyxB%{QV`kNn(tLbrmwDI}5HC07swm-YSFRbC<=7S{s?;mMs& zxi&kX7u`YF7 z%e{6~H`5u~c#xlHM@)adLHw#amzK^7Oa%>=HhsK|Aij6}?qGdstsTiPJuPh}ZK7|y z@tWqBou$(Gg;7jSsEx^PpRZT^LVA1_c|C%^?Zc)M#&*27mkfS-jhriA=@S#;nf5)s zB5>-)tBSk!KqB_?k`h+CNr%j@O_(d`dU<$wEG?z4XIzQf;u#u!i+=jyN@>Glm8|L4KwL0n0rVr@uZYPFu4C zUbvZhlW=Qw*WM9HJ;j4%E+i;x`ED2j~sY|WKP$VgAWM&(g{;RXbN^4bl1oX9`n z{?Nr6DO-9$OiAHNd{jSQW^?RiUU-q<=^rxBt!X9lRy12xauNR;FlDC*s3AZ#XVs%x zuPqti*xY@_wrpC3W3eUVZaHmd({V7`0Yl8H^I*8qL zvU@zHVZ#%RK%MC-EEBu|Y#wwYWIpuH9mn*Q zEL^Afp)Co6;%M30{(%9@kP!cK83|@>8eQq&XR`DuQRuovZ?=AMBImYVB9M#T?)lfG zZ<`CjB>-tgBj+C=+aMyn*K)$K;W{3;yF!$Z$Hgw>14m#Z$6V!LwMZQ$uR)5FS`>6_$%D;MKo$>-#ymQ8m+O}s<5^HHA zAB#ud=#K?v_>471RLsT@I<+2l9}c7e;QZ$fDTK9jcID^#YQHJ)pC)^VWhGx|g{_Vp zmA+d&CJBC%TgXj2n?vA|q{t&PH?^tXj`Y=WUiu{09bI*McQS1-PMpR;TO3mzC3+0Q z19GT39uRh#^;$#=Y;2?DCTwhM5j!wUhWD1BQ{7~@uaQv8Ybla|K8v-DBX5`-ap)Nmf5)54HnBksF-B%Om=A^gYo?}n6Ty!%KWSRXdn!od$ zOiuJQI^yp(*62K2e0U%b;`f~f`ZJ)vf`w^bnQudeTXkwBagTsZqJ4gOqI|(KrK?22 zK80+HdXE7O=pB9lsvob_`b)WwIv&(4WQ%02^o z2HBEVvBaj*m-EFh=m}cDQV8PxeO_DWFvAc99%_j0O^7l&XLUAKAzHv3m=Q$$qv-Nl z88b0qa@1`FUU^bK{?+-=CAVjs`R?PINCQRRs3%>St`>ZTS994)1N+ur)Wn2}v>gkT zFLzAA9T_H^jw)wz>Q2(p&XHZRT{PhR9ggK2Ff(hL>+^K`bLCP_5rK7ri>%8axl8q! zI1U>@Eg0yQX?4s{seTI^_&B#Xd*HzKAtU~gM_)y-b-GB}yVyOsiTK&(+{pY*&!v7N zCbAi7%py}@Hnh#U@KGs}=XZe<{xL)Nc(?DsuH?Xj+R2X{gskee2U4Z0tE%D%eV1+} z!Rr!(uDa)k^>LY=kvW;GmLrI5#!BaZ(*Y^M5_;%z6AmJvr7Gp1|?e^}x@} zTv1jTB`H??-PuD7omO#{X~-QT{<7t>)X`53F)<33T}A6m!$A_UYYY!Bo!3SpbuTl= z$n6_ymVXZemIjXV`~g1=-P)RO3#nCJqc?uzwP9FQesyvGRA-5% zY08^3dfRoO8Lk+%=cCo@Ht*v?Qw?ijBqV1&K0`pg(_{|1}+fes@ ztllXrG6X&mows5B>)5C~x~=FcxT&!eADVUwFluXQ0ksMFW64fIk^Qr0KQ_a}7WRJp z>_@jG|Cl;z(dMa{jgo)YdfONK(SvPl`R3~5xMPDyZr_eCJ-Z&$b z&Jcx+Mh{336EF`@2o|#lb!zn7i2ZRBA0-u&x zf5W_xcF9ju$qSmE;LF??;-~{&2BBt_X%ZYnsKc4Z%v1Jdi>e=Vx3a4Dee#ay>RwxN zZjAPl3`XpC48bL}6Eq$mjc6)P;>>q@=jxf7D+L1xXV?>e;4%x}G&D5Ke;GZd#%cwL z&$4xt4c@4)zfh1HrVc*v2btURhX&V-b#&ff-ew7qsJ<{~!ptmXHJx9h&qrqPQ$`}G ztIWeb$5^iI)V8$Lt`B|)`;peE+Qzg$k?4^WCf7gR`)lF)=P+q$8i%KSYsd%LwWPJu zs*$iTxn2b!0hz%+9)9Z}k%0Au@;GvWzOXMI{0|SbjzyTJLNY`?)d~?`(1=&l$= z=(sOM@QYed3tl3_T6;V5?Na!P9hb}^^pX87w|gJGD~QEURRYiLq!deIA|DH#dl(}r zE-Gq$l~3j8m?P3$US%CdIxK90p68kf`(RHGkRHv~AC6I>TnOs=tv5XAPwQ6@-;2AN zl_Rg&!;T%uPW@Xn(z^b8K!>mZjFYEw{M!R1!^?&SrHj{NYtpZ_pa)7ybP22JC@|!* z+-;>$l$8a^!I)ps?o=PK;y}iz4Rvd6iZ5Td(#{%HM%qRnZq2;t@c{Lgz;D)mYZZX~ zqJu=5C$RFbjLD|)07+WoV)Iu`iYxc)Cluap>gJ#iQXM?aaE2$cF&&GX%1O>iw$|N#`e#)02#NvatU0C0s%5Tf zeosNMQg%ggpY>h6_!HJ8m6AoHyf!+R+ zz4<2P^XLlKsyn)4##s(`_sTR;?0EOJ$QrEH5~@&I|{xlhOqh|toO%-wItSlSGc@QaAxBpB`a zKM5m{nVApO)jcq8g)tC;Cg!6+d;O2_5|TC}1>UC*OM%|}h;AAN2#bxQqa%q0C6aK6)LL&V(-??iprRaVqGxs742SqCUYk%o9bOr7$JG}(fEhINx zH%!vP*j(KlR7%qF)W3w<5Su$8x(&)6AjfxKE_5Mz0Ku@(D$tS{3iJ;zmjM%)n3wkYv+9M(F-;uDETH*eeA!JjwLu)pI%7<`|u5=tPBS%)EPGN@pCRp zLZHlz5MaG!=i{CGu_LGdNtQ?EA^cMDS?D_J+>~rKwl0E^SVEr-Vd7CH+oo0l>G3I* zB|&vbT_LR~;AGyuUTzZ(wnboeL(#uGorBVN*RHi`iyQVyaQ%Mxh}F}dV5FJ?x2ba>#>*d%<2xv+o%`f?!4^h?e7&W)Nt08DxYd; zq&~K{wbitiau33utP8sxr9J*18-C+j}v{KkqTH1 zDEYK*RRSfKa2oj3%#|W^g2O)k41j}Pmqg*Xd%lg6!3MveOhpqjaGLE8xG zv?78koD%4JVN6tnk(H16F906OUlnFXlmGlVGlf%%J8e*N%=WtPXkAEiGU0m2*?ZPJ z+j?#KrOH4oiiwW4g6e{wABJK4(zg}GbA$AYZ<+6s?KVgNR6GFQ10W4&0Mp*me}Pgs z4Uu&tYdAJ;JhPVfd?y>dDpy0mFi97l3w#=Ufqai4EM&A}FTMTpHVpI-3n~>@0^c(} zDVxWPW@8U(e{XJjrK&I*nk!e%QmI9~z7Ya=zw%)qt$=78_WrteT^bJ*6>V0Q~mn}2C}VZFFi`O7X9E#NM-Hxve_H) zVoe7B6(w4LqTeyo|Hw9$c`W(FjY^%kUG&Yf%Cky7V@0+=^v>^tePZkW*W#*V&k4M; z%``W^u&ThXGQ1eOqHSuU!|iqy5)|w*OzY2V8R5sizPj4}@V5!4E~}M-!EmXj-UzoU z<3Jy&exG5;r>*8;59Fv(KK-a1L5L$9->Lkqvq|*9<=<-W3ANw31QG!Vu8^hzOTYW@ zzbm^sdC!qk;2;3eaF+gu20rx452C?;NobzF%>P?&hyNdpV*kIxDW%D6xn86{?Tht; zULTt-Bc9%+b8~f$J%#U1?*r?(07eGDo&O*I`ui=>3b^2eNB8e){a3wWFgv5sShF2T gaH9L^C%PBm!iY@94Xwn%H;6|HPwr>md;0Q!04Nv4N&o-= diff --git a/e2e/snapshots/webgpu/shape.spec.ts-snapshots/cache-as-bitmap-webgpu-darwin.png b/e2e/snapshots/webgpu/shape.spec.ts-snapshots/cache-as-bitmap-webgpu-darwin.png index f729c9800b8f1565dd3f25e675b4ef8843bccd0c..cd88dacf4149da94d15a9352a40cfdd2fb2acc13 100644 GIT binary patch literal 13791 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 literal 13595 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 diff --git a/e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgpu-darwin.png b/e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgpu-darwin.png index 6f7556d9284949e8d13481a68b86fc05b4a0010f..8ea829db52515a40ca3c640873f38d88be831ff4 100644 GIT binary patch literal 34136 zcmeFZSl2Svr zba%tq{I2W$a{hw%T<`hx3^7maeeZj(d);e2VQ*9w$VnMUArJ`p>sK!|AP^Gpk@)X5 zVsMaVQY3;vZbDwae4*u?v@&MvVCYyou+OQLdT@{rCMO2?H@Af`bq-%$J=)!^ z-d?x7ysV_8w8_cL%nVzZo15$DNj;N7Y|nL!j*d2jQtrkbdyLlx6mw^)cq|jRFQ&zh zMR9m!gwZ984K9f|+F9x^M4?beKJoGK&JGUBs;WjfI0}zH)BUEgrPW?oP@r)I-XbI* zklh{o6V0bUVW6&_&U}xFX<}$-ab-ozN#~P}zxFhg(4Q(e7|W?c^Xcy0ySt5t4+@Aq zlRitm@9&3l_fJn}U{G<5AT%MV*At{?G=_$U85kHe{^$tk2g_%_lqgWle}-(0ZApmH zu>Y9Mns!j|F^@IurxOur9cd+|U2#mvdHmkJdwmhq)YPOu1(gjA4Rv(lvyIgSbl*)N z3`C>(`1mHl*#o=CRnjgl1Id>rxgFnnaDe5g85r+xvHl+Hbl-n16`0(M4 zy#U9bkV-egX932V*wde_wFDQK94tO`&hi_1Tb%C7o8a-m!E3xBswD&hAtyJNy3Ls^ zzr{PBU$Oe=(5R1XPqA}~TG9%o!1cDVW4>xxW;uuq1?T0`Z5*-sz z`Va9O5}lo$<2AlI#az6+_JPC0!`^RW_yk^X$Ij)=gDdrVxD|bA?x)uOelf^lZtFV6 z2y|6denl}o`o{M5HX|cr!k6*>ug;E+++17>sG_1G5L#1Hrl@?vas)5Fgga9`CQrai zVjSx#YoA5Le=E}ec+Zi$YSz&)DJO?T=bruz6qca%n1Y;qb!ElI-LTew>|$`~FVUJ9 z+=mMw4UZB}i=bT4XHXYtyQU~wYISU&*tqa4|A8nUUmCx#pdg*R7Pn5-+OH2+Nxy}^ z)qaGDLesDdRGXz_WEyVTYin!U+1bSia-Xr^VWE>Jf`(P*<|??~kh98MTsBlu+4ea* zsp4o4ClKb#<*yg!Ebi~`i>3Yn3kXHYt^Zighx@ysI5Jr4UjGp+mw@@lApcG;)BaV5 zoU?5iK{H+kEmQ0nk8;#F;ZCc#4Nu>tRa2v>eO(J40sH1!9afyiU|09xOd1M>Dtal} zgyX_L7HEj3UWXb$H_Gqj85D#&-EJaIr^<^I9zur_DT z!fMoL;UZiA2&U)LP7xKFytuF6x8rGp{U5__7z|bs#vk5V>mzfu`{F)rY=|S<8eq4; zB+}lN|E2tTy(3YGgFdtk!7ZXs0;Lj?AeGaJ$d8i~pr;LL_`F#Y#c#>HB-eE~_qQ|t zi|mN)e0c_Gcd=1T&r2=g$l~07_Zs`ISeek!P}GWp3f7&ja%T3%r6)z*{izS-_IG#L z{pW8~nsT8M=Inn|dAD{c?!plLNsQoDTzCEMZUwog-+V&oXyN;q_3eX^2HJPDxFzwdmR-jFz=hH%+XVQ^J|n$;j}qhKi@BClqQhTTxc_tV#Ve|8sRk z1^=^W8GUxK?N}OzslR6xN`BC4=o-}1%iDWU)URhdBqXF!poX9UlfCcLS(%3UTUgmD zk?8M7&C&rQv#n7J9nW%D!`KPN@9uh%=Ui`gW{xAAeIlD-kQ;fSwEQeydSp5MMv+d1 z3Ki;?d4fsCtD^EN90i3~$HeD{HH(Xo?~&ZD@(kR^PJ)TJ9?DlAN?5b`E=wnrzR$7<5HK%voQ!Jr}uE zC8M?oY)U&Koka7~z%psGs92gS+LAxRXyDFei2~8&a)qak)VPnsuta;pFbzxr^~G+n zwcW?C&3U=7%6cO90u3EJDH8WT86=ATLX|xIqsJj*hAmjy@)gcP=OH(~9h$GE$1~6U z*1aq!e~P;DndnzK<>x(AOQkNA{Y}bCSocZ|_XXl%OrS>T(xBJcXlb>g9ROB!v$DK@ zu@zghgII)H#<$$Rw>-Y^=YKOyw*RuLEHmfYTf8kH7uuIRNbD1_Kn6y?EgW)Vl6B>* zdG8vyCj&)~_au|72s2c1v5=r(kq-spl`uzboKpo~!>iz|a3Gj8`NI zN$p%)TkGioTX2eO-2VDRaZ%Ay_eIXJen-luaRIt4EiHY0eFr$%+4J-A@(K#DW9EJnb?Y+kp6uOcXxMtd!><*vN8?h;pmfKa3k1z z*Xmx81doFRMAEqtP8>T+dR(5LpI=nuCW0acvC` zUrhjbT)G=NBs5M)M?Vk<}a^2-l`w%(fnxWX(Qb!)N*i%*KB?F)H_X#j?6Pe}+* znyQlIjaNL%GOG4YUyVDB!&j=MN+LEUU*Ml#teM~1U`Eyg+m<7(yP!{TAg>V1vTYmH z_&^0JmIsQ(QrVJt%~SRRNacRDwkkq5v|KPcTH`0*v&CnPvfs>a*Dj4K=B};SeACv^ zv8UrGSyn^YIy)ElAhrJzV8O;D!(Qv-Lrpa>D)RiM-zia&^pS%AKWS^nNjwg2VDdkB zv3T^DW>tzd84FNq4|Ve6zX^hZf_9oVy;V0P2KK5O^}$U}I&@#B<0%gg4jvlP^DVj^ zS3I15lJ@uRIP?m9UhVnmlL55dYQ5kWtloiMcfsr@GBht8 zvSd^4yX{A3JC^T9|f`&>ox+!8R%G}FKC~7nQtKAIg z&r6psg=sij}Hja)a-JhWw zh=_<-)BISx)uBh!sUWv9H8s_C`c#A|a5uPJ58`ZVdtGiaIhhd@6oJz>16@VfX$LKf zIzww(TU*VV?Hn9Ds)#qHrqWxp*2iml`6Ha+g%1V%u!92nok~LJe_%<#i5yK^-z6Lj z55qPY6>GN@m6V`5H+TzTVC^gQ%Cy=jdd|B_)Z%({ku9y25STy*r@xMQFsJ!;>D_0J z&d#Aa_10g;@P?NPKNeJyItQ5}u2y)b7Zr)v9Wg9pWA{>0Qj*3#{BSvX&>{2@l*Yn5 zQAsW$P0)_%_3s4*!;_OH28_^^?krGgO}A?r85!y8j|>g{*tWtjSb|#1)bwFzHo*b? z^QY)}dw`ork^Bww{MKB(e-27W${k*g|1LaCfGx*Qn-c1lQXUq312I< ziZw?^N75fh)%+QPnc8ngUcNxMxjh?QSG=qEt4CYAw|gl_aLPw1v!b$+RaKehobj(m zTzot_cBh*S2lw#c;^wwpGku9bBbE4cg6b<5Dqi#c4;P?F;VR*0i2i(ZWeS+EYEa zd2P)pJ~UWX!q&-&V1BKxvob>R8DzvSRx36Y4Tou3)?*c>&DD=0k0;9Ho{>f-xOJ9c z!heR3!iN5CYhtf|o}OM2RozEoho%ZHvYqm|ecZnO7mh&wSN4E{e zq5?rB5pG+^amo^R4qzq;{)$`~Riy;ww}QgL@^3VoKW@!Big~4B#rXLxc+zcR#6fka2G3<8QNpF7G*Vk_Xj8UA4CM&dmS!Y9N|lL-(>!MaF)ctsW)&XKAlW&{AooH^6YslU^uC5p`it@( zf{#p3=9mlH{L^f6%0gAz|Fe z9Vc#oRNuYY?5SNR?DO(Gx4Q#HXg)gohJMuWHEjy?Zh!cQ!UM2z4VW#+)no_v5y*lq zBa80Xn)HvOP0>LF4x`jy45q9og^kW`jX~*ck0hC6!@+ZoEiseTB!S}3N&>DOh7qWD zXw>xHAWQ~9$gT4Fl4E76KSCH@6$>D(!$fz61(_5_nj5QgM@MEHph|6oLGI`YGk;(p z6Q;zk=7(e|dKA@GPuS}pSKN`JVc=^~iC^`{5CoyOl)9PcN&dKUn$2eCs-+IQ%b_=Z zKFUT*uPrWyjkLf$J%igbiQEWoD8Yr3#lVe>l+d~ykC;5c78#ueSB8OSHZfnm+=@?? zKiL~`rezSw(8!>BHg4l%c#Hp~u;#igYDTa}q0CoL-s(s}$FT13TzahAmkA<73sSC>Fp!|X{4Y49n`>n`*Or2ka%4dE?u!WZ z$)oYdLol0T>(pO;5wQ4;)9(EjtXvf&TKjOk5LS~e6>P|o_8#wpmDp|tk{K)MN!u7D zenh65k7E^>NzxY=ZRd-Nl>1C*H@IvTpIX1gR8|Ug>M*dhi#lC*5IQUMqI}E~ZVmgC zWT?Rn%K37ek^^EbZaZPIo2}gIYzKWmiz@d9O?$*@F7!pDoBWDU>+tLvOp0Rd6Fj4s_nbSiJqgpFMJfw$p&gSJk!NG{ZH*)}S{ zJ!Q08=t+AUKJKWmVslrb!oL$df<^>jn$llBY5P$eo_^WVl-GdIHu-t5J=YA|?V^wl zo(&E%3GWSQv@!FyBy!{O=UH2^mQCJMC|Kd5-|6lEj}-2W;HDY;aNzJHUuPVTunz## z1#<%D)hSUsIbIZ=G>oo_)`@nT)T0dLc|IM%yHX_>udNu7OIylUJkOzSH&n*zHceM= z+PHGoFpd2#fvB<1g-kp8BiE(H^^zH0aRp9SVnX_Rxc5?5vP-&86%qbg(H+u$L{e>r})iUAQ_cK_f@BdfYtmIK>iDXZ*w5lKiK8@+M^9h9dv4wFW~vv~FUl0V zO=8lNd%VA1i#R6mNDB;0f;0!0)pHW_0(^*1NpJ#Hd8z|xB>yH{+} zi&CpwxJ7j}Equ`IPPqXnUX3W#5*HEXHBS8fB22wUO5%-`w%H1+h}0)SBOh>&Gb z=T|hKXV4AnPkxn@Osf3SmEX0)9lKw=)J$HLx5mH%C{KellcILQ*w|?ky)m0&Fg+R} zub{Avax>iw3!Axe96h6ZaT}};qM(8U#JUlipX;Kts7V3mM!K4=jl3N&y*ve$z(u{q z!^&LonOf^Ar{pEh=Rw+;@UDmR&RV_}H9TvTLrx9f+^457vMrYMe_80bUfBP)+q~w8 zhk0p+trb&gMomPLRlj@W}ZalW%Jd=#XoPO3mxFv^@qoTg74DbGb8n)^tI5gWQ$xNissP#Kc6ngx&kO!Z!?+@||z@GaX!i8N3t0 zsphGe3cV%Os)ow(TTD{#kY!gjl831gME5OX=N~cdi`{I~vnTJE*+xI_5@F0XPBvax zS_+rSk#;60CucUgclT~DYld45{<-KKnj^v9fe|N7Fms<(K?uWIZ$ISICF`lLG3Dmr z;a~+ovdzsZ$z-NI?FO}3V(>kv0_`&a+|8+&gqYXUzfoa@k8*F)?vof!;&CsMil-$r z0M63NrzroMr8TNq-ObU$XrpK6XlKVE9uF$8zol_3mZ%(RnmnqZ{OW3PhpCqn15L<; zr72ejRS#*#VsE@Yc~!e*QNkENr{r{3I-_z(a(=j%%=llO4h#BsuRKv*#8?e3_m`OM z)NUHkG((IY`o(<=54s=5shT1I-jb!vV=% z0*Jj8J!|XSuSOZW`623Ww1m}7u0_6^d70~0_`cAnJ4+@&y)*PtFgdeYzf9~yfz7|D zQmg&d%87!Vg3_JQ={IU>YPWURIirfhm?@o`N)?Ip-#yRjWmZdvm5&b(ml-8Y|E%09 z>8SiZT|ft&HS;a<)XEmcH5Hfjo*wVd49IptU4!+0@WqI_&nY|Jq%D-q((+$kU5#|^ zNz~nJj6_aX=SCJ=>6@5G|2`oOeGp%@1a66P9opaQC^tT6FKncjy)B|}4Q3e^Xm8h2JUo%KjrrD~(n00U@?RBdI51E6dI` zoF9;2dq2y9a?)<0{0cm>vTg}2qwnaYX%JsuO^u}#m~a_Go@vR~EZLfnbh|__a%b7K zwLZs~gy+Nhur38tyScONf2B%{)K#^^`a`9h?pNS71*W)fv=TL~xn6-GyzdOS}oP~CNP()YR@A1VIc ze914Z>+i^nxCtP^CxVtH$eg_pABil@n(Q!6J3B5FP5WHZfI$z~a(FmtVvbSY&md_h zrcg{3;$F)Uh0UY%V^@iRM($<1hoD|gt__Bs2(&3E0r*J6GcS4aMm#H6Uths-9-x$Y z9A~dg$alv?euL}9w1C+>Bs+i$Q%f%|FPC<9%T-!WcBZu)GvdQ54Z0`a>-L zo~7tfqJR-9YdXt+}~*rkD^W zueZ0?XLNATM2adRe!S`7Xm8hr!|D0tH6zV6WAirhb0n)xQ2K*Xk&!R~t`S zvL35SEcsQpX$3}D>$j7Z?420snzm^r<*U`}d zAqw3rr&o}dUmL^ah8uUP9~~Xhd7vHsTb?q*~9X`?8MH3&tjUR$8V|Mfn4N==!U?vo+ zMLX(@!y-!hX>&k1E}LGNzMQM zJ=sSStA??FegaaHbJ?ShVU13b+Ih@)NOx$Z+H;gu(-hu7>DxXq$SY~_sio|#Wo=>y zPfhtEeNo;ZzoUIp&ZApu4^zy=5&HLin+SQ|J+C{&?CtF}E)%2&?xRF1I;KxZxiC)z z5xy0?fhd!yL)g`-;0VK^yB*D~`@!sUGZ^Mi`i54uRB9bQPmZoJ{+o2u+*M;|`)9QJ zs@M<*pO)V}QY$Sl&pZ$0xSxW*UV4^kJG8^}0_UVsQg?1}dbRJIOhVhcN!j2MXh$xd zXNGhgN(s%cI3NoO3LfuAHu3tsV`t+9-wAs%-UBD-_xJZ>ZVyi;#NS7ov~YCWw^H60 z|6R%BT5B^uwqwYN1I=oY;VpZTRkOUu19=D0MjvcB^%~p~@%Ri8h3>dM9E1EwZk_1| z&*nQnZ|vtfdbf@vdhsS8 z@0qJJerJ`qE)!7HPfVLAW7}3__R}&mALGc1&}ej}^F(v7K$8fGH+fRIv3x6mz;I*7&%`7RgVFBF&B>`pp+MizV;cMW4tpH^D@^)OIsS&}`>qs;HYWweRCzu=W%1>|qsJ58a{7VZ{Zdyk z{z#E2TlK;4o%Y7L{?E?i)uk(pIr)^L_zyMm21Z7N+{|-7?ksGrEl`_3I zbX1lvn)0FN#+~Q%<^A2CVU}0ZY%WUYy$>|!DYWi96aL?4CioJgC|=aNiwT#^8P(L( zbe`w1gbYyXe)sj0AI(WxAABV>VrpQ{{DyJ)Nl3+6II^|_G|shuVT>}7iYB4-aEX-D zoZ-=##~8E0V(hM~+1QT-mnX^{aijN9pKUy^h`ld${OWAz#xZIc!FYKD=_ug1xnf>6 zzxQ)2ze$@gj?Vmn$4kc7S~DEGtkU36F)A(EQj!NHywZNs=KH;6uB+Ha3M14x^zOJM z&E4jlI|0EC+e^0x4jVjlMq3;k2788e`i^|dmx^l4^!q8kuE>T>pqZmbo@!7xyDpw5 zpSiOS4#q1+i%JieA~w-4Pisrg3pzSFj*K&}FX2$t=?bShJ?D+)u`#<+^?P|ol-Uv? z`0;Srqb<%kTRT@Z!=nJ}Q*Yaxqtxv4Y8X`_dhg%RQ=@Jz^_R~1cDc!kIhp4#hFk2; zH>R7&PI=n~`oGCoWqkP(XW{LAaCUke_E5kJ7w{FHIGni@P!kLl6LSF_@%n6YM?c3u zF4U8{kKT#ZYW7q+2vP9{S_*pEnHp`5YaH`98jaZBar8s1?L9#jO9~?%1$QBADvKp7 z{CZwi$cOb0tfWXH3RLKsC}|G6?fFi>HVfeiR(`un&;w1*jN%`0ns|A)Wu4bUGqY{+VK676aJ4vpw76(| z7PV+A>r{RC=VFJ}bb9;b?4HlzLq2A#TRTqEA*(>VAuB7Zs34l3e2tlY09%^FU%Uh64TXiT~4 z#~>8SIhn-M5|(UmZPq5+7*VsEi!X#(iXGd^s1o>(Yxv>>(`wvU=GgZ>w}XV-yscSYTH$VZ;oD0m8HF}e+X9o!IPJg!1NbmU4b=1!!&AF(di(A+B7;-KGv zDM@CiG*>*{Bouq=UeOoigxz2Enfz|}#a^LqbG-&t)-MS4cm1K5)A3u9SS+nncDsHz zZ>249_1ZW^vZJNrrQsIG(3gU|cY8?3v=z|gX2mU)Ug1Is2qNUWz>Wa|NXjGk9glUJ z#CZBf+zTrPT`>q-==JWIZ>(xNHZ9XkQ}=bSJ29j*8%V^v?PIkXw||UnI|0AW64`DK zM%6EzXKEzXe>%19#;fB9kN4-p9SK`<>1ZVWdmoV>uSIcQrFE! zTIZF3AN~6)a=7KmgPWyN+ofkO=!fz5n|uz2{{UhHI}fpX74$-DI~y)B935489Y88Ve)b-{rt zTc(Kab+OI6-S8^}4ccD6wBg<&Nt#l$#BOXa?-vBL0y0IGKcA4f8X9rF;5cK08$ui; zom<&mEljmS5MU;b%r#VaZ_y9b=RJ$=1J>3PrsQdZ4(E^g{e@Up%^EfX$kj_6 zXIYei)9OT^7_gSNeTW}_p>Ps*BTl8|h;X3!=gAjm)PYqOtsoV*r@MwvX+HD&${x8_ z0}ls+wdgofS)OFPEYwosYUK-)%&s5^vP)Qb3C*4iwPP<`R@fbW4O`ABy=QkN9ikR_@V0i1#y{!YdcW&f0%Dg9A`|0Eyx2= z3rFi@$!o?>P=@0BF2>X5h@G{D$U8i&1WZq(W5iIEy2OCsCn;h2F-t?^biN_J7m*q6 zSXZmg!!d}9b{=Ggs*E^IM1?YAFVjXUX;0HmPVhKCqK#M^S^hyBXTiTFI>__8eITx# z!Kq2AYcHI3dDhq>tuHdH*3CUDiI`B+FfYxkQ}0 z?%9P59X~xa5(i9qtz?32Cxa(^CcfmxUYnT=8H>GWOth7jvLmKWm6yu`-r4tlsLQ6> zgf*?hl9g+x{JZn(3OyfRUsPFv#lgv;`~3dTus5(zd=V?$nZ~-uM4sKZ>Psb$g0PmJ zCbb?3X9LCot6RMg!ZkLo63O?CUMzk>ssR+l(t`ihD@vjX1A5=-d5v@j-;{J;Pxdnu zTD&P`@imD^*L>0Q?%eOz+-Csu_UVeq>g?%t%Xc;RvvVgqw|WJ?((O#@h(;4yM1Rpg zQ%ic@z_sFGBq=x7@NJBl$4{ArYVu2k1_4!*e$H;LuODuU1WWQip$< zl`T(|`)-ipj-KbY%^G0%$WAAuq7l0a&q0}x{5`|Lz_>#pOx;&i-|Ms!Xq%GH&bqFr zZ(AdtRMu>XILYj)5yeFxxTlmqYrAW0)2R(^4Lr! zA$vngKBBEJeYoB3>~NRZWt2bdAo}dp)CaK+TQ3OHRiKl5GkUFqLUScP|9kl&VY-c? zWW^^1=sn{Xvj!9+2&c||GvO-{jb+Pqm>Y+!9!($D<&uiYW67zRUIMOvo}aUqkz7zlQD#oVNjfhmHe(2ZtD?f%=&=OnFh)~TbNK5pIi21+&{^$;+0#2NsxLc9 z$l%JU@}u$giVLGm8nt1!TOe?h3Kq6#?H=v%jP{!)+%AQ z5B~j^6sb$$zb!$t2ZyV7EJdEi(r$v~-rz2#Z-AZX#?G0ba?3(1jT;}0e3-S68WBVk zE;sKIiD{Rty+?(n#hx(UyXW!4{0J-KJE~%M(&9)?9Z04mXF|(O)+iIqw9KRX zVewq2`f6-HR=Km{&{3Ey!mGR7XE%3UmuLPu z=MjD%sgij;)c$<`_HE%8QbrEO2So?FcLhwr<+Bvg8|_Of2Pi4{BZ(A05x^{5E;xb<1G+HvKlp*Bp_AaFUMr7Muol#C|kV; zz#}Nb-s_JNwa5TLy0(HsXxqunVU;&1HtT(zk7}wTyrG8KR_0AB@m<4QdP5zmcSnd) zK`C`;?pxu7LL_@m**6Ss9~j_O9^%Z$4=VZm2b^f_ufB(uH-gfiPetr^d6ewRWW#+& z@8frsmLEqthxSUj-(V}hd%vFkFm|v}dEm)j=ii5#xu^f65IdZy(%32ne`0sFaj#pq zd)y}+6Fs81Mqn+EdNsPaoGmzUu!i5eHPYgE`n;t@ZEtk$zSp67;;s;#l7e&Rou9VEf7W|)~o!ZyN-XrSvp+E9cRMgZ) zz5$_~+0whC37D}l(~|En_rf{esPCM@xAa%Jmu!`8tUh<3d|&EO>RqZ}ZI$yeev;Fx zTEqi9a_N2JJpn&}R!$rD;8d!|{kZlUmdxuF`_Q>i1E;H|^{JpOU3xIa-{+zXO0F_w zN?ezpI1FNEg=yOB+?+Eq`4zst0+Zszv=1A=CX+PX&puG>s;wr(_@k52YEj$O@aTku zRqx#wHrx02-`gPqDy~KZbjy5>x@*d~pBb(5GSzdBH^={LjB^9;%GX>dp(VFVHFG&^ zwOQrhO8Jpe`!s^KQ%48I`9sY=UCK(feI}~8*FY)7 z_Ir_E(=SJvEYrb!9<}qtl}hFMOX|vwcdJG%|DQ z9~DQh={PSMd?!z65} zI=RB$iI3aBv%U7>y0KyMq=bWskUp=$3?xB^Nx>%3CBOg8tSDhgig;npH}WeI^66BQHXriLzpx@$73uQ^|(h4w@yY8unCHZC2K0 zGsb4-4UM5H@GE0JrBQ0E`mSAbP9wjX0Zr}>~)KgPV;tz~hl&HR^Rh)4>EDTJm)*~hA- zi2tcFF-Gk6drctXpF^8NErzAo5KSL!OLxVnw$w}~{I*s0xcGyCVAQ%R#gel4v^-8q zy;|Vt2bR**~$LFvB7r8mKT)fyFM-vX|JM7oyn#s0ra%r&}q;`X4O1eHolVu1t~X$ z?)=NiUcDN@y-`EuY-x2|q+El4!XGt(Jlz?XG>Lk&c&XpIW_7W=ciYc69iTXPG4{v# zzv_ZcV*6J8lwF@#-9Td2u$}(0-dU2I;_lsA*>e?sJymUDR%w#k`R3LiS3S_lQQUs7 zZo!zVDBH}mGk;6+J%euz`m2x`>-#>#Xu-#lRiq}0(|`azn(<`lv*6knp?>9DEHQF> zZLJ{+PsS9bj}VsZ#PB8}?x_CF%opoyK|E`kV7m+9$$3Mh4aNKx6$$jnxY!zjU;yyU z8uQrQ!A6POANm0ITo10<6BS9rIn^q{;|fD&rLGK!OS+bpR)u%i!1nG05#%x;jUaw; zD-?eyAmQ?mD}kv%H+?M{{1R(%tqvUi@B07#S4rUi|GBt`#sBl3)c-VW))4aJUhoy5NDHo-N?X9PuihAr9=)c@3PHmja^~ zT4OmyKWg2;yUsMPtgH|SaVhb!u_6#`9_aqhFb)h4C+r!5b}lJ|v+)ZF?eFX?bOGNeXlh8P z%Qb;J$zs$wVHM+n9QXl+fS=jp$B#RWjVo(w*CCTK;a#JzEiJR}PgK8ch$4eX@`4na zS_kCwP%QxRSHcXhuRta?7v4P*~khz&9M1TFuX!LG}(8LXL^2n$tN#G2eoP2P97HFG9qdXuRi-S>0>_RIzhn8&C zVXxrCq^rZOB9OY^7mzC(UshIEn`(Yn1FI)m@)fKuUnECynw6MGZtL?DrCMLZXl|m5z`-4*xDOqZ)rIT z+ys~zpRIpka06riLrXu261c%Ykzw!0Vb?{Pl#pmI10U6^S0Nkuf3MR8M$d1~v`ke| zJc_mQ^bmgbtY>1P*j%A$EHaXukqdVI^hgs*!-!Z?BQLM2(lauW6c)D01lmpQOZ$6! z1>;mqUif{Fg+<8Zr$M~Z9bIMWw;TV%1w@CVMJ3_~U4tL)`hj9rtjK$3g)tr;7{(Yc zQMOJTxGhg@14#{`I9nU9DP&c|fQ}_-y~1ziOF0H!2s@fRsrq%3oZshtRYD+6>+0$b z4h||O{0r;L$V)JooY%qZkh(iPBEs|ghMnvA_~SJ(33c|t*;o(XukqIMso2bR{4A+QtPNcQ;|)yb#!6^ zw4?jKWE&RbQbiVUQb0OYs%Ofk9S1d;&u&z-G2Fj@wp3UglL`Xb&P888anG=|VFe4B z%mlI_ESnaeI^;tA`xQmOWN&Z3K3bLep$Qvq2GkUwiC<(~>(oRyPbqYKe4HxbtF5m7 zR7z^%LhLhnV%w6<7`2Lxao9ZoEuKNR!KMkYi(zJ9Mn<4tOW*|EZWp{Za;K0neye29 zFujx?GMNN0l)(d0m(lTYux^r;6Ynu-&N73N%I5|0i zH3}`OOslLVr@m}a#bH9doax_Wx~;^Jarh4i_(xlrfsZtFd~>bD*){$wCe3)TM) zsdJ^RnmO8P1lm-o!&9V-*;@8g+N$k+U`K;lQ}p6QHI=B&(bu{_w z5~E~XY-~QCnW6mp%F4pn8#!0?FO9IfOPh_;ljQAgVKpqp?yDKlzv-pdeV)7*9`s z7^?}y0-PKiG$BBSwLXTU_i5HZ>B?Rx2L5W2#LdlF)#ULxWSbPQK_nB1!HIoqNF~P0 z!K7^034lbYLpaUXDB`FC??fOYBNNpCYFQxgfd>BrCK|!Po}RDa+&(&2zwj(U`?MXmj*{xchNg@A7prNIV@ovSd z*ouL&OOSa)7uUEu@NuXB2-kAO%!$=HP%-YHduphzw z{r!P1L*N^5Zq;NfDw#E|SOpbS*o*=mobiqS@oFW(oE(xATC!fo7{4tkM8Ua5)L7Lu!JfQ5PAjSr19brKqSl~g04zg_lZ0NDEu}O~CB0~nct-(G* ztL7nIAp5%C2qX)(T!^sF#VRzf!Q$~o?NLg1yv@yJ8!kqgc#xYIo5aD%IU@yfrso42 zp)Re{e?XW9BnTqDU%Jz?C`3Ic^H{ad`m>O|31MN^PltMY%PTACdH!4qEdM&_N&;_j z^G(?fH(o8)l0Dmu4!F0D1uvzb4RM@k0+8Vb82GFvz&=3@yT5gGsQ6~rjlG;!3IJih zlPCKJNTe7Q0R!K2WKR;fO}CC+X-^(o=Vl#o)$7+0Tw*&+Pkjwnh*VP3kSS)-MCf*M8YyyL0IIq<%U_XVb1Jr~r)KZsg@bdC9 zuJuO%SzodJDAVE~2_0qQAhnMo3OG>KGa(!wpCqfX2kMW%fj_X=wk^mB6-0 zmcNjmCcDR?mp@U`nZwa8x4r%S&kBq2J=+Nh3{Vq~n71x2Y2DD9Ma9LHR{bDVag^{2 z3W`pAi@X&0+!M(-(Ae0xwN(Z;0BgIoefU4T7YLd#=U)rh2% zwLi-KLg{u;B9;W^^s^NJX$S1FiXL1Cd#n14yi2cv%d|%Fzn2qavjWZtEN$Q_EFbVZ z$a@I=c3~!cVeRErNx?J$UdS>fP}Ir`^l@ObitHR^FS~dZM$4?9@qWRT5j>~w1rDi? zQtEP?$YuxF$rp8D1DGLp2?OA9@)B689I?O#*wxtyvwqHzzK~W@T+C~H6;+T_;ayOv ztfB${-Qu;a5$9U8Ol!QcKiKfahoTCBgF>w6f^VyxalmJE2A6vo6x_eSN?i?6ZF6_6 zenpYh&3`22gPgofo%dtn8(P0GAzyt9V4rAIUn(%dOj*^Z3z{i>16rQ)xE|4+YM{JM zVS^2T!4He#T9wx9zs;yJtSbsu-j-$9JY-ThYSeX6U5tm5Y#w}&ORf@gYY_miN@0v2 zqZvD78~~~cV97-$lYtbEr36>jLLd-RBZo_Y8aNg5oDWT)(YpYqYZur}eU#*GH^CUS zm#%4af#DapKsFptL|wEzG8u%-{ZY5}tgN~k`9X;@ zy^*}2VQMiXm(PJ0xK8uJ!9qgL{07oBfNj~kaj|m=gWtK@mA>cy~v7E$j^WrRMdm-?RAAEe_h1_ zxT&w9VRm}1_t|QKWU{;gY+5c>S_8l_{%#x5Fi65e`V&Bh3q30z@>#^Cz=$WMGd-OJ z6%~DOgJ1>atO-W7p@TYih|5F3%LkTciMlKpK|=!xydHjB!XVvoC|;Nt3}ei|i~<<&cr@kCT104Imfy)ZKz;$Hyj0CqRqE^R0@TjD!9lYr+m2)zNJCjuhx zPF-x(M~Y+Q2km|h4vA$F(awg$VJYAXDMq3629N-SAY^45z!-o01?hbKRICWpV1Lg< zd|QBeJcnqR(80RaO+vgDkz zM2S~&#wO>SbL#Hd{eAW3do%T_W@>8wcr*3*i|RtV`*+USXYaMwTANLD@r9BhF5np? zsgfchC19xH&S`9FIuB)6A>SXi+3*ythn%JZA+2qZ?_RJ(8yZdlJMfe$Q!&PhPGV4Q zfNWej4q!$0AV=29va&J=g-96;W)B2yI(b7>6dPGW+)I+q>)+vFB9Y*e!>qP5!H%DQ zLOxqmSt%hP0DSBO&iT~fp<864;lLS(1gaq<+ zu49l*;nW}w*CqdptS03lpo2kjXH zZ50L`b@dmV>N#|!rSezJ_ire~%-ceNgCnbf=*ZRUS5;LDyDs9m^Ln&orh%s_e~yDb9OtQssqT~kO1J-MEb)|_~A z_wA(S23HLDVbK;&$#U(QGm)E7AWUwI1bYM1}K2dXz+s*=g9H#@$zzJI_d9-6YtsS zf>wC0!w~^`C~VT?e2g|&usCI{{ru_O=LjtCtmCC)N7nY(F9qH@Xp%DVnsoNHfqjZ>3+rKyJzc_H?c?PxZ- z+&Dlr*wTuT&UHcANL^puWFGRz75W(y3%O`3@F;*UJ*8=U%4-YYZ3GW1>M5NmWS0)h zseG(D1D|JL15fhggG$k{-pq(WLM==EB&pu*I+d&C3R&1I5qZR1v(uya!8N#!TZZqX z{rEAcd7owFbD@db_PpXQox@{=@-!$Mh8$U+jJhx(*#4v&ti@w-|p!ImY- zi`KBsCf$c?YiptOguZR`*qRw}cLX!G@KI;@v$vzqIAYuZBH;SP7aWf$u+4>#eZh^y z;1J@5T6V1BXN|XU|ClP4+nuweHWm^V_I^9#Vsjnbg7U^J7oxp8?B#=I z&fOnKKIGshPqL`)Hx%cVovfiI4q}&TAE1*?8C~{`F0-$?-2!k%5d{kq(~DE892)n3 z)R3%6z*!^x=hJG$=0xr(grq=v(p~rcPG(w7|DV8rUd*+DXaRQ#PY>Y^m#n$mzhMEZ z1ms)w2E9Hdu;)@ys)DPW-&S<;W16;Q4~16`7dJP`OzpO)Nvu1PDF6KV&6&ov!O`d9 ze^q8bp-#9V*2cIh#cCaDjh~482joFcN?NvP!<^P!#N1Wd>JL?^@i`H{em!e2M4>}N zLt~_{tM0V(kTj4m8##agG^ZZ2JwiSh5^{q~kNMt}aPc!IMQsM-I4^N^1_lJ+xTdD2 z27^{y;C39;Fk5@ct(^JB6!iu`D@RI&AYPq{(_gzCL|rJp^JPQG@AyW;>R6a^^}!Mr z1;S>1H?H$<(6dPKB%_uN0F{*bfjI$pWxrxVAn(%R;_^)ahaz$W`%lR>s+9VTkN;$5WH>~6(Xl~|_mOb;dp;X6-NT9@_SFlO zl}>pcZl{e%{gP(1{*4~4{WK`&0cjQpHH#?(nt#Fd?j-Jzu;n2p@HD3jkxzQS7bqPs zuyLlh4)ylFRKwlDeove0pM8NckWl$fxxrhJJgSx2EpUSrIrm_+Sb;#)AhZMwl)~rXlQD>?yaIr zSL+V*&avIsGBopf$9wSyvoSH z0QwFUu6_hWU_fX|5%c`ph}@M@hrxCyIm*I84CmNlMU4EASx4wRLFOSORY*PyX9+Cf z_yBf`W4?`n!Go#U3Km+gU_h_n|Nk8K!Gb!5-Xr8K>OvPSN->jyxx5oT_i3&J%}c5q zkIOSRHteLe$p_&a)YlA#cbk- z#+AzKrrw3P(EFnPR5emB!$5XaRu;~)>K#0F0c=eW+VW77Le`)}z7HveOf{f1H1~eM zIRL`?9dy~NhJ*5(T}#vm$S@FKMB3Qn*7&0}x$McGMPN$Wly ze3|!k3Iuc$t|F!iN+z3$xo*pdsGzZx7*Xi%%Kk51T_@5{zQ~lD8!hkn9Z}xt6g}jG zu#IuTU?6Q8X~-=p!PqgJLMXiktBS94?QKu6i^uXw_Kxj4chn~3joQm}9ju4Th&5%Z znAK4+kx6ZL6_r`-B74j+bF^1+D@L-0D33XBCAt@2-w}0W)GqM~A&G?(4N5}_3JO`fb!4TFAVcf#>azPs zE$aieOK?=y`$4h?HzEBKFdmfDJS>vFfKl=*YVH6n?h~4{BG6P0tZQOsMp;Cxd;9y- zH;=)(0@758yq_{MGQpmpe6ITdhjHWG2}Bgh{UDr$kk>qiN~*#(lwrqG+OU)I(qyH$ z=p>`LWJ;ROaYOyIpl$9doR#@uIvrxFX^l0ryf-V}4v--TJc}L3tMBzyjsh%d z0o4QmMbrkANc=dt=q2}piG$1~S0b1m1Od_-&r`sdR0Ga?mP`D!1 zGIKwSDgPUlfsog!7Ee4>NBzTM`eQA3(xQ609=)pKfmQ~@57+n~*)7$2`c9fn^?VGz zMVym)4`j`(*prr)Ii6?chw@o#vnA6~ws>i$ip?8|#gfrP`!@u1)~3`Jc96f~)v878Cu~9N2*xuQ+hVhO zh3F^ObTFUMeEjX)E(=YKQ6j$$7E1&qj|A#_6`c+L9zPE$`#Wv8i>tNJN& zM@3>3hl6O`xU)3aCKiy|WigodIvF|NZRa&aM zU%4IyUjQYwQ110?&h4EY4IQEM!$rre53}*tAlb^+V;@CbQ_bvCN3Lt;4a~$d=U>X2 z&cK=d8fAQ?_K#I3w^lv` zIqkILDaStI4vL$I~mG?(!B%iSx3)@R@jH4j#wnE2$}YH$?>d1u2&fz zp-1oA@v2hsvpZSrRktlDHypDpB>i$s2yF*xAtr?3ohx@I#A9nRJk;6jSLZxrR3a*8 z#-cYecDe9=w~mTDziBL0uMEjbZJ*gOh8M_>G{9LGYY&&jG?MS|u z5I6R25ATk_Yjzw~r8(Tg&Cr(SujXuZm9#Z2QxlIv&s7fKzZzo~cHUMyl=bG+vQ$^; z9#R!N!7sW`s7(;JEH<=`XGeD4m~BVWFt+k$e2cj|#D)08?8KHCC`fmmk&%&JmEMb9 zG(-TkqSD&KCu0F*Yrqu!s^Dh!vu}38vB_y1;-Q~IzZiTC%ncvqK+?4E4E!;=qffo) z&PYvtF|9nUDzm(}sHE4w9jy>US;?$srkZ2H&>SoCcj!|{n+2^w&z+pSU`n!2{!|w- zDpbLsV38P{@sW8lZ*~Wi#YxKC)vDaLDrODR)P=MH;#c?t1V|0#@$~gfMj_6u^5it~ z&fe{K<3p0&%Y`LwD=s-1s_&v%zj)j<9|U9%6x|jQ&&uD-O?D>|JOYvyxK3Ie6g|}x z;&7Tv$d7@BvV70S9FbFAz42!Rc`_c3Ejw{-xeIL_?n#Ce9n1~5H@s?_n^ujz(Bl4V ztBEi(-W_R{u#dj((Dh#N<3rnJcN@da`%43ESP!<7g8_mpqF|AcsnO>T*t~;&!lS$Xr%`mR0KW>;bEbmQ~D3=hcSp`)Z;LtVARR zKKacaP`-i?F5uy+}TQT@4M%! zmRTbM7&5zq=;uura*nQzU8pqW+81_BTCS5#d)Tw#I7V|fTczY6f8`4=X3o$A_rRht z;`x`@U&m&@8Og$;l%o9N{gS~;AZ`bj%_!1_a#m*O`(#Ja75e@mdpjydVr^|rEzS|# z_Nr!86T)2E_k@x@F!4T1T|(*)qMAY->2+7Qxh9Yg2lXhYU3+iU(g{}h{m@j=LP~V; zX`XF(Wz_4eBU2WtW+euVYUEaWV3x0m$X0vVr{XBz1}B1no|dt+b%Tx3{(kH0Q~i3v ze#LLv$y2>DTP;^A$u74+;?Qle!R;2!TPVgp5gMQU?bg}3igWKBO^Sk-vLtlBSbZRP zynX0rQKhr!&SjugQN56nR^4AR6BxGA_Wr81aZK=msKjLCJ&L5NmlAWjtUCt>x*gMf z^*_VuUruUy{U$T5D*r~e+S<6-=TCFb9L%JUTu3qb=f7!i8phCXO^Lpx{yec2S@6za~z|G>Ha|F5*{+a=}%I}AhoTamxa$w8fcgl+?Kl3|OIdWh9L#2Ed+8;wA z0)o7A{;m3ABBcYOz13L!qI*#KTm`P`L@x+P~3Y0`+t z%<2Q*`R_&52h5u;`_$OpNww^JQ4qJ_ptx+XHB9gVZ#4C zP0dWwQ{2Enisi-CYjh{{KYVBKeds-)ep{8Jzq7AkGlf4(M^U2)l;L#3R${`q&aq8^ zvxg}hg;_jZUBzy_i_bJN4oS#rWXaF-%Vr+U?sBYR&YVlP$;i(5S-V6|SIV~W8LxNa zV^ENVmd{`IRnHa@--O;NH#A2)z2f9k`ZDvQEffBrcR-j6FO<4@mAHRPz^gfKB^+V7 zH+5!8ljis*qjw2EcL1Zu%APltFyP*yFm@37Ywwj@oR?p!oum8_`|L0Dn13Jd{+#Zk zoS*}y<4UaLRK~FG;T$nVcPYn13WX6+{=Ky`s=Ffl6Dw}NvsuMnT1zaSyphkq{l?$g zOC=QNn!~>qcxdra7XQJq%H<$J%194`)>%J;dyFMc2#%R#b9gR^EFZNBh84IDom#(G zARjVy%{cnOdnwdc6}dj6h>Kif$a~QD;fRkoRZtZT-4N*F3*Nbt7H@a@7v+x#e`)Q8 zE`V^B*k!M%hN~*Nn4130lTdWbs#F;ByYjV)IO;sA0IWp%=Vd}Pjgzb;%=7y-)LOH7 zV#34od`(@pO{vo83k737{A@l>ceA)d|HbMTEtTPn21g%Q;A-X8Q%BW)Dwe2=g`RT% zL_dJk^6aMEnbIYftSBSWVtN)h5lpV$jV8Q{fw}$=eX`n!U(O@kFE11ZF?QsE5v~PpH*9AHVv@?<{RN+YP7?EKyE3 zhcFjz+`;0xeS3`OFiCfV#dG`&`=clBf8u|3>_t?E_lS?pmhlVVgxM2`Wx7o{_(Vd; z^MjIM-nnyh@{_bJxn?`>PAWn9 z$IqUBR`~q{aAB3+<%)0jVwv=mr%ZE4hm7YUadJSabjLtPUi13y1Wyzo3v^`NHQ1<&S$cEf|>58Q(Qw;>GJ+ z<{*|-aSq$1>|PMPMUVW;Y)dKMOB^ceEjQkKkRaOcdiuI zbSYKQSBGkRw8&Q9ptyfXu5_)7Ew4LbF}&ep$ViZ#GspR@U}^tw<7M?Okyh!#_jh${zk_~na;aRDPnl_3c~U?D=!@ctYSzNh#Y??n=L$i8ubxbly=1t%qx_#pMfv8x#?pXY%zx%@@V{vsRSW)0 zDa8P!0%loKGN+Weia&})>#UBBI>EOq{Y26t4uT^Nzb^^m05W3`@D0?mwe3vq3yic= zO3~BP1E5IcU;=n^I?R zn|=!rCta)X*2_do=*~g&3z`qMDFrZV209S+{$PLxR5za|;6vEbo(;IBCQ?n1UTud!8ZUUIktc##bpvEjWWRVf+1;Pp564PF*P;f zOn*NyBhPMa_zMV5+SysyDz&!5z`Yg``{LK<_p>!7Dd5)9rI@DhO#KSCr>vdIeYHF* zFwL$=`UG=WEU*4heh~t|5X!L&zPYo0U{q$KxwSPw%pb#4lT>CNs}%zD0bdE&u?^$v z@vqhZBI-y(X6*E(n;8!F_qhDQeebT9{KE?8jR#BMY7s~8VDd7%;@GCR@YpeH`DhPQ zT)f=iYca6h+#w|7p@y#1)Et~sxH0r%d@p z87lmR?;x8p7%4TU$b4LN5R6{e=E^W#Y1c@-W74#oE0!L>&dRDS(Q`VT=~!y4;$V%N z8y8E&JgkddnLoXTWFo@2A^)Fyu)YF0I{3hH%w9(vO@*q`X7#~uqSoAO>PdiYwQa#5 zx8=E=;Fwmr77Y!}^;0i6loi{e<)1zC75_{d#5B~b@`yvX3zbI%9vrBgFeXy$kEkym@}IYWwa!r!e7Bpi=WGVB<(K^@Aw%L zORCw7cT%O@^VNEHS@S?X;5f zh*6QaGW@+ac>bh>)`@K1$lWu)nM*XSpl$DjTJT> zuOzEvmSjgMh<@ZW# zW}9@z4cD^VF@Y>Km6 zCbKK#r|vr_r!pR_z11$NCb}gjd$x5ep$a_+p0H)+7n@y-)_65^iRhl5poA>Em7H9Q zVo3}Mi@)&%@z^zul^Jt!#=TGQeBZJkE~J&s8X#4{8ff7A{DQ_Hc>^>NCL{NeQ&QAP;990?GUanA2s|eqy*sT+KKKcO}TPFZj-Ib~8A2~d2$z+A<>&{}x zI-6sgt&-_;SFYr@Od?jWV0u$(q;x}YKrSR>Z;!a0q2^bP{hqTn$IP-K-7rYp!3oT9 z)nRSj?^a>EZDL}Hc+YU6rQ4BitGkwGRraa^^|`O!5PNtePw;im07n$ZTU=Op)5}N~ zpOKh;je($|*c+2NnD%#yK^i_yFU?Y0wH|E?{CdX0v*NKV4|%oIxtCye)z75Xi<+W0 zm|tmiQqNd(Tg|**@fMl{653>*(tNZy#?rX~&8@FKzU=S!a5V6$wGU(bW|76ft74P> z$I4gG3UmY@L+JCSTW9`l)j{eT*a=o($Eu6^ev}Utmmd|hwAdX@OZ5&NVKyBOcVb&x zZ{i8;a&lMj&89{(*nz$s3inQw$G#d5FENU}5*lP^h$8@dYX)}Em+1{wB z;Z*aK+I6!Nwy90#N+|Z?K}SivX2DD8;bqK2JJCd4+LXgbR|a@C99^uv=&~QT2e!rA zQ-pfjI8Qi6+vRphZ;7p$s`o>Zy1S*uMhSZ?%(Gijk*2tzqiDiaf*UopnpEph zN5ZR(@&5jfgB#U`#*I7|2wPTGZtM(oO*v>~U!YP>E)SC$g=6Z%V~t>cL%97D8!j2A2}{(4L0Wk2e4vwo^EwfADE`MW3Q zh5ii-=+foNP)7J#vidcqd)~NU_FMFhcxm&1__p{MS0B()qI5mzN2f4Cd%{L7m6ef- z9z{>3fVQLt1f{(4BVD5i9sk82TnBF)bd6UCN2z%Z#Y3LC>2KbQ#iSin|1I`r{Fplo z2pSOESjm_;lQMyb1{#M8dySb)cY&#OHq+hB+4==jS`A3~F4ns6nSNYT`Ug{%BvPzhtaxu|Pu;TBRIG=E3;-m0HuIj)uS z3scE`m?u%X4^NdhL=e)iSWoALWOVY-@GvU|Wi;6tdAg=S5FN;Zjubdsvw@GwKbdRH z_R2G!V++XC+`j?HE)b9QJ8}usl@b*_?JIWPZsh$+?3ANQ6b&bO3+Zl?NPASk##a5( z1D_qVO5AO3wuy)(W>hWZTV6p(Wn~kdltq{~xh5bgJpD$?;9;$wiNWt zMSUps^?6439va}a_HCV~A{I$CH0=nd4 zNN;@c3gYTY{!&t$O-lWe-D|l-+_KL1e51MU0?HcAZzrg~py|9|Ij&i7X)QF^;Lw39 zE>3B}V}HoCkH(=Lr1RRkq9Zpf1_)0T6!y1*f&x{i2YZ#nEpoBKixJ{+)>v#O<5#r= zT8b=08z>kH$?Qv}2a|2L-lEA|kOo$~~(?ql6K{uJoCy zaKy2x;nY}b)cvRXLB=LTS*g`Wg*>{_0|6%GuB|Mh5cf!`ktu%hO`ZJaJREgQU8M`{ zOq(d8nwWduGQKxl;V+Zx#?k77lQwTzCQaK}F8esc#6*2x!Iy+Ty=X;l!%V>sHe?}B z%fw?^hBwuQ_#}P*^s7@9866s;xW`8V?7;T6)GinQc}E;xJS8P(CTBG40;vJcgM=1eL2cN`;z+5o zLBacp_n&}H8*=DXCN zYIlAV>wp@7L|8HE`g7``O-J!H7}Rz)jB?4rD+YMSq`?VXc1I>zBY^Z}v76D3A)YPl zHTW}Lk!@;=u}{|KerFtAdZa>TN163&(a^Foak@H?=M;f9Aps*1C)VTJwjP-VY~P3g5nc9@8@IPF_@9E#=Uj_`}p)Ev8d9_?026Dv$>!e}~_sX~Kd* z@7v9M#S;TxQs0X6^G`}9YS4Z47P3}vOV{2@p>{NGLoYbEyH^0gYtzc~eP){hT8A?e zWH_AV@5DFEZ?#PVPCoIt>B@IxC!}o)BhUbL<5;!UOt}C+SW*)p@opU1UpqQFnlRG+ z5$0QxSVBc!p5jms{f(+MYlUoaMGrOQEt7;ayiXYK8dlwr__f5^XAX6Agmrb^kM+Rs z5Jd&I2Vx}g{=B-ytE+@Gt&&=~AOf(~nf8I|O^D08O;s5HUFfcXoQLJ}Sd87q#-XeC zk{>MjdYUZ@>A`@%aBw|2&-g1CE~+wu;%I|0l=WKaI_c}F1_?4@cICQ?aiF#l>ALfE zF*e;?^E|3YB~Gbf6a;O2(6CW}X;Bgb<;WrPNxK4*5EP2M?`4*ZHZ*1UEpJjSp}7+q zyKkm9H(pKW&vea1Hr{djo$uKDqrwv}c1c=#JVVD>u}GEcyao66F_uXtEPvZzWQ#uwEwB>y|4HuJ3Hxz@6xh@L(LE4$}@jv zWsM1oA8F0b5w{i=hU_Q?C@88u?G*c_s)vRvus)J;q$Bl-3+Q%EH8cETVn4JWom3&0 zcb-{vya2oycF^-aJQR{CXx`e@-a`EWMR7n= zw?6Xp`P#V#KAF3 zMg|sXe;@LtzP@Vw>c~F5QCIcmvPR!~_A%c6-s9|__uV9Oi*}lxJSms7e1ta<6C-p7 zuuy26+06(Dkh;y^O;=FKF%1{iH|X9=5?G-H%KI}dwpzwmU;y*!mC zB!D0xJ}2JnnS~qhCtIG6($_gi2sf=g>}}}zLszS(p;5(woYYw z)0Zz7BtXB0^9KOLwF2OUx+Evi-!#FnGl}-LHk?0@TwRyf)-do1OB4T*vHcIG4j*N~ zp1UDNooWtHgD|skB<$k}fQ7jw+B-Uiw4h3Ht#H{&z69Z!Rgy%}mVoeFx&@>cdI=bp zI==zofmGry{5u!*Fb#mOoG<+-PS43X$Ez7|Tje8=qjy9F1g7{!8eocDaY?4}hppnO zs_Z7vJ7Ea)dPlCEa~I(my8graf;1Eu(P*Ul5+{7t2`t7XND?^^8Fop}Og1())~f@3 zbhqBT!HGA?VAgbR95*(G>0twy88 z8Xje`VwvO}7MO=>V52e9BNhxY!#qalan!xc+PotvX^qu9uro2a(VCZ&(=nQ!n%X%9 z%XJCFOgql_Lr1rk#UvoG=;w=A59i)cE#1?-( zNqw~C{)@OGDi15N%n~cE<9Aj?pz3G>%)nXB%DMcmsKW^O2w%Ex%C7yJ*-mwgW5G&+U{CV$+yX*{#! zoD|q#Y)(IQaB#4nl}m;;ws=ops4=wlNJLHbLWvAiU5IWHIUasUq`gmcx_c}*AM2`fj&u{S)}^l2%&c zKU!%JFiyD4|KhX`wX}LORnB2lu$p;6!Z3XKywz)-o?Cb3)NvOA0y>@<=m(OPXuLgI zoX5U}W9e~=kyvtmIe-^CWs|>yS1!pn+`2c+KRgR#R=t9+l5A)GIkDk5ct-=ak=aK!WeczI)$@UNNkUQ}OQ47p#FA^nwok zA*Iz_IGJIN`M({{Eg9k8TUlHGXf-}-81V69X{%GQe7av5<7c_f#{6GD=4X|73hvjk zeaTd~Wr)J(-nMsgN4=aNjXRFz{nwW-ZM{&k{>zx-e)+SY=g*&)K3BY5F2BT&Sq%qq z>l9F^rWmbsCaRu*e45&@AxSo@>?z5{*a#hI?$f!?KN!WB(}+=f#U3gaq>balM-|O1 z%SP8R+uS{Db0UL#Faie0<+@A`LrwcK7U1(420*jai# z()YZFd_i_f{_j=kX?~%{T%Tq$S#G=?Vl2BG&D}t+x0LYm7zx2kddUnLMaL&gYD=)Op*S%JEK0K z2biTeyMI~d8LAPlTQhH`(G{$!$`m~*ms8weS^QfwTdq@DjhFH2Tv0TvOx!Z*T38wYaWM?%~6S;^GFz z#-qc-OPX3Jb0pHv*7m3M41qvsZf^Ec*U|YTR+W~PM%*_zmgD5&N=Zuc-&>8BbxG-} z^4xHH>Glq3Y|II6<*dj&UgJxqpn6iYJ=Y#b#+y6Y+4ZG(+S)E192^V{4MnhvJ5E%4 zV-@+lVe<$$oR_ky|J&EEJ*%qYBVU<#1o`>^!ihtTyg8ScJ}t9z~!f~X%|GmC*(Rb7m|_s;lqdAzR}*^OLm#r7|hb| z-$IAj>3maTP7aQ@$UL2=Vt&ztko>vDjE+s%39``^YCC?vRM6OdsI18<6t;74;B)1w z)OVz*X;MOhs(qb9Wg7h7O$9w)Q>gu#Ogv-_S~PH9f_2mTrytI{B4G>@%5n^SEnr;GG}u4 z$Wnwd0kQ~9M}n!Ioxp+0dqQ$0GZyNDtFq_(a%fJG)mcc5!+W!7A95doP&uW01Crs) zOI)^I|3i7Y!^ip3_|{=Jv8qdc+;BAFd$&uf3kFtc!9x31^{YMuf#nD63zDl>H752l z;wya#FXQNCIKvE28U%n#Fy{?q3|5E=V*y)3g z$deA$G}3!Q*`$ZQw{e9*yAHPeMHX~P?~dI!GYsO>=s_qswyIiGOl3*0-fAzaimo@Hl>%gPqGpuNV(pGy$hcQ*SnwA~0_?Tp|koktStSL;?( z)b!KMekVFHtodD)EKTx|<;Pw;+FLUgcoN*d?~SBi(PP)AS=-nsFD?xTQvEub9d;^z&x52oTF?z8mKC z>+=b4FPxFDZC z4V%dgK&Y6Rm~dRVqHRY|wKAb8bGE4)xm63Bfc?Q%j!sTGamv~f%gV$y)$xRAQgmqY zwm`1H&T@uscPv-k+oyRXCFDkbnmC_v)mj41(!oLFi|wmT6EXF{+bMB0TcH760?zkxV(incRn6(AoT6q zH@ztnQ&U72Jhy+c0xAscU`FrS>gecbXhamJ^x7a)qH7x1pbDmYy)Hozr z4VMsMd&rLdN_!TZVn6CaTQ{;eOLj%C?JxBQ9&T0*nu67|Uei%;ZMv3TMw&+w7=rh7 zd^3@i4@z24{~Q7ddmZJvr#K>|iLlU}BAt4Xua_^GuajFUQujPOq*U1h+Bef`5_1qe zS6us9oL}v0g6t+NgUPnB^rk~v!2EidRElDf$C|z!gQ@)JC2g2K=fC5lf3XHnap0o7 zGCOA?Ub~w_MvU_CNO{w6K3aJCUTU>x5eWz z$O{FgRoR0}+N`XsqZHkbD*k+Psx6xCmOC*iA(r;N8l3z&KR-_*#YS3qdsiIW$Zc`a zfsJBu9dZlG>|8gdq7hc)h;y8MLR))lqa&7_hW?pD_(Qxrs_LE;v*-HQ zPm_f3L<_^oIJGsKP^RZ$Fj0AkyPKaBmI??Qk*oLQl3v5rIM-q&VoU9u_irB``Fpt z-QCeKRQP-dJ*YWH9X#G&m83IzQ4bp zW&3jJ#c?(XN1X!~+N!|2oL^#MpyLu=0H-hi0zjUo9^@9rYcl6UH7O+ixRSx`-hp)~ zT%CT(uK#MYDO3evTl&DB#FE20%^{uIHo3LsQvHT?fObi)Br|0Yy z^N(FkYJ!|)fqosuk~B!rnH`Wc<9T` zvXE(MY1j4MrEGN~DP$wVhrqiKd?YH#6|_qoPk>Ae4P6PZmzI`3P0xP;iW1_34!tPo z3atSd-ESV7m*+Zs=J)h;T3aTVY^2q3r=w_AHX~z|RolU0cA1~Q|M<1kP7O(IT60s= z&br4-^mroC(7jnmH*3I8pH&}QXjfx6Y=WE`F1C>&quEnU`zvR(L=C(i>`Ck?WP|4s z;`U>WUmF!I3eP>@{qsm-a56~j-oPM1Nx#SZ2Gtc9nsB~`+7%Vv5aU{}wvcbQs zW%Qy>hfP%U1^1rPH|x8DtPTtFzlD()=;)dzoAV6t_VWKG z#>VnLVm8!-mxb)8-PM@~T3cEg{uQ{TGT(KuCUC52+AtMFiL{Sbc`}J}Z=JCBxPVH* zEqZ5=x2mkP^sbln?T+)V?!&Bhg z@E=9ISpqsux-NQWdUsm0Hm9ejpXj|*v=K15#D5J#|1~iY-ldaSI#q;*sVa!|nw6n9&@q~kb(`0Q#D}V&cIa(>O=vwb>?yk}U73ti$+sg%i59w_r zrDVgdHM_xM0+>cm4t7^K0?&CDUoNK)RU8}`7}5ye?|QdYHZH9_wzjfT@@eGzeh?+# z8>sP*6fDBuD1$DlypET6Q37$NZeb^J_Lc9d|Ll}=CJXD(U4+-G=t?b~|A zgyfGq{bmx;?w+e_%Jj+G{&!al+T#x}bH8lV^E8S*`Tp$f*=>m6N$GPo?F3rYkg!|% zJB|VYxB-s9%%xphFX5707j|+1;XVthu^%UO9#kVogOQ^K*R_&mdaZ0SW&4zt7lRv$ zt;pdbMRprfg(C_mD1Jjh+1SW{GCUYrHLsgox}nf2bR4;L$X@8tg*5n)g(Q*+I!R4Z z`eGZDY3$nT%m!wSi7d}6 zAEqqQ?T6z^z8TrxK-wFWRo#(eW)o`Fh+Fe7BZ(m{srBAjq5O&Fe?FHLch7vJFdcaX zuVO3k5k!sIy&~`m-`KW!olh{yqn8SlzW!mr`MkZVa+sa#T{lsV9JIq5tE*S8IJfqH znDpC@P{^>Nl8ABXPg~WYkfQ7xrAz9v}TXF(q5L57M!7R__HC8tt2qg@D)xp*_`N zmy8~_R->a-X%qkz(IsaZpZgs)Wz^Hxoj*4WAcBOsZ9?HP@lIjQeyi-m4@BN%Tj64l zr-ahd%*OPeGt}Ay%=EfEud+2%%DK3~_6pwFeP)`lh~@=e!CKHbnGr0XxH)USLRVA zk@(f7faC2#F0`ifAM7^rmp&2E(cb&BZ11QyLs=}pF)OKW-d6c;2g>k{s)O;oLf4pr zM90_RKtm2z;W)h38jHMNht{t6#e4eY=Pt9S?&C5n2yX9>tbH$)ivA?^v9Ys9eN&pu zXq(bkC?`>?Zb>FH0<5BP;fxTCcDh&Y>xtoH3VqoNVLG%rdnN0^C7K-9wmK>W=h9`& zo0;_;DABdW2pdU&(j@0Q?>-nEjj}Ha^Jv~I(!Kal5W-)<#}&f!WwP2kBPgdccgrNe z?V_U>R))VCux?^9uVf@7auYeI8yFeu<6Wq`@`aO^ou;0Qto-(|6Zl2UA-MKvgoE_G zq9OI?#`5Z_-`_3E+-1~(uy`<-Y~CwppD|}^+qvp&kCzltE8{+77aIc_=(36x6X{~m zcA$WgbIb^{_u}^A9JZUhMn#mNO6 zN_ZMietgOtak)GAoc?on>&wqX0f9N*ulyzMZf-I@ zJ1RP`4+EQ5IXGk-<6Pf6sV9mJ1#;vZuuJIpQO>|ET^)i5R~RoGKB-I-5);erxn|cK zzG)qB?HUA9t6~p=5@n*oX4p zh6>NFEdf|Ra~oq~H?DD~vQG7`Nr!I3L{Rht_nLnWODxA4@Ms zO~JvU(9EsRRz7Z5#UO5&n4Aa7G8P+QTndi za{kqKJ)8#Lvj4b8~aUF${S<>F_bV>R!QCP8%aT93J0Y6FW0A(^C}7Vu{OUV$Quh zj43S@;h26hN&dTZnf^>1-H7ksIv`Z*L=WA1|%XR*dE!kL_g(jU~Xm z=e^e~)cygd&{HW~Ovf$(1U51<61{U7rzINkI=vz6%wrom4j!o(W2lL!(tDaCMM z`T%rW)`&E_JIz@bJIOX99I$Q8V)_2?EUA)ad~yF59<7lSL&@5Rc^ymjM+U*lca*X&WlPxfob4kNZPB+ z&+mvmP5BpptgZCiFUQY<*B7a>7+}(fBwCs}{QB|Z-8Q~3{G=l?0|&q(ZImll?Y-o_ z2HaXk2nr#LYrz$iz&=HzX9}^aT59Nr>@2-&JzoqqL z{e)+C9jaI;O_M^PR?G}c-H<#9%-Ji+dWcQQl4FdNg~@A%(kLV8+6SsWrvd4=p3lHB zw_TLJZRu)`s9nwaZRERo-JQN;SgIZQ@>O;A7e@CV@zZn$32-45NPVT|$gc9qfN#4x zzpT5HF}w-8qhwa>#t~2i;<<*WPeX^q*Mq)Y+ZkgUP$TuS8+9;v$?Dm?$REm!D2>Fw zzE``ia8-6iS*iTmRv;T-v{E4#gFfAa9bhsm7WkBqclr}9E__vsSAC?W2kU#Ao2|&# z3X?XfSLta$yew(So13U059izSlU4>5J=5~9=Sbub7JJsbMwB@#XCv!d(>e=h_7vmp zZ=bNAr`YdGlBQ`%sJ9zu-LVD%DoF1JgtEROjIz+K`LrC3yO8wkKcb@UD+s`B04PwM z5rQPL`wBSAoc_Fh_RFprC4@>TO}u)<&fPtR?iAS#|GxGNvadZlI(qBYEkNK**C?}n zO-V7h24I{Z4CVsc&Q$?;pVxWiH}NFk(oKuG^n=Q)=c|pbG)AD?UE{=g*&=of>mm5kP+grE*)~ z4VnUgZXZ8>oB;F(Qd08PT2oWg)YP-5PoK_X<6>ihNT|0ySM!5Qz}3&sVqs5J{#M&s&+fo$o;Z8_rY;A~((?))b1a5yyd)vCNH zFKl)OJYK%+V9=Jet7}OF9O3HXQe0XJhiWbem)Vu5R;zxPot*_77_tGqR)D}H$HyBP z8WICfYPrAd+M!TRW5l%d0x@ngosTx!rW44Tx&P$?bam6c0ac$#Dvk2MFE z0cy0U##gg{(&FMdP+K2!3w~C@;(LU~{amts&s-*RC-L!}j@TWc?4|X`0`=Y0^}q!keP5 zJ?z`@e0WD*0}~};_Vf48FFb82AW0gDBVL(M^o0-zBUS<)0Rd+GeP<9OgW)c60r|K= zWd2b}aY0{paREy@1Ru<+!jEiSBI{@FN}2N*EE2p4vosyZI3?Jk-8ZOB%mm?$MPsQs z6bc1QV_*1XX-8k&ppoY=%Ti3Z!9z}0=`JY-z|37gI-_zU>6tr|_8(qAflf<8>Mxx> z;9j)T&D}e^Klt|fM)(T(%E0qm#_i3Y%D~dBi>lANIliq_;~!F!>79(htnuGa`gQ46{xzQ9W*8Mt3_1m+7l zk@bE|US0{!Z)LADiu`??gZW&;lVQ6}6p$2-ZXi|Xu8#~2<-Y0&e1nK6ZS{d}!(Dsa zpB`e|6QJwSY9!yG`=-BaeSDXP7R~=)Fnq@FoASwJ8S@;>vhFd%!^2k#9sm#qosXGL zk@a6b{8A&S&n8>E{FWsvuP->5YLHrCX`eXE1or$IBr{6E9@Kebm56dyCd=us>o8c< zaE02>!k~Wqwm=Ac8z_t2n_~M>^47cmCKTfWcky9I?La^o?(OXb0f5Lp2}7VIT5*U( z(x1t|?u_yJc)($6?>PA(5HZJTgSbWQ_|j~fJulq$+%OI&peTFv!k&|ve|Krp_6Ym2 znn=uji}`5JY8@sHt;mS2*;8L#A>*MZRx+l4mt2&|Qk9!^Wo6lODAg4fl&`P!_A5i< zNQcat7kn$>&#^~}s_20LI_SgM8@I2($ zz){-jF`x8PnYgRsDrXB8MM0E(o1LDkn|PemGg%SC={A^kC0h%6%UvV&S#}=N5iM?7 zRO^Ozymmk8MtHR9=kV7g+gNuB+`C-qF<=APYr-2wwI(Ri7DZ;{r(y$F2~q-&ZpEn0 z`c5P9XRrzxECrj$;&b+@%|8ln&i&q>dFtsW8t;DpCXx{1jyGkh^Z%zWjIB&xf}#k1 zYXn&YF#_)^qEv;yi=E?DSXrp?{Uh>TOiQi52I7z9?A6l1w6h`_93qg!8oH;(?59zm zfv_8cd)U_}5@*%BZpg?xBExwR2QTNLshX$E{5Y;fSBJ!Jc~t&TWg1oM)&2a+U7?Iy zI4$FQL&ler#l=O=d)M%R{-x~)am|iV%T5^;74bZ;qdQ!%EpTKeD~h z&8huvRZ9Jpz-BitJ^%QqklH4ubLPE}X>9X7Ua3zkS((=Tty;hA{vxB(Q4DB`i8ZQtmKZ`St0Ev>FPr_c-M-vQervZKX0c1; zlw(+8<+aDI1}|_YYWYj;CjDJDF+!FPxn*crV;P+k>^*P0ztKA3wRt7!xV99VyPmjg z9Us@-Ad0q`|3b)4a(5RqP*iLVTWC2HmuZCfE2_ z-n;KJc-MW#YMV|BP27gfrSp8MNLQ{sFJ^t5NL2nCxSOD^{4@M-nIB3qMlvfhQo!AN%>9t>OYfbnm$ZyXNr{iwDd-n$ zO%g(BEj(P9WCy|#ZaOS1tY`MIzkf~JzITC2k&JRNeH3;%>c-vQgLww=QH3NDX*uYg zpdj~wes$o@A(V^+149TKrE+JT(^juOo1-^vT6u1Iqe-g1 zU&}OKTtwt{cx>e&8CrMdu@$pUWshi$?4U`x(_}$jp5F4%R;i)D2^@_yQc=^h*R~nx zgInmW4xGVJ-cbBhXn`bt{xFm7l6o_=tdk~83Soy5wM;L=Yy#1)&6eg`_|p+h&w=5z z=b2}m{X8O4C`e~VGWS_BcbA?e(YBQS1{OSjEYWv`B*4jU{Eh(=Sr*wHw%aj1Z811a zDA{wj$-Ap+dtv087OW}b()abLLSHtaWjbert zMk&h+6@Gg1J{N;tmG;QRx+|;HLAn$k}7Yyo|A#V0n ztDdL1O~TabDklqkA<*xQVZuXIxk=ZIW+Yt&k^4zkwcyWN`vGk&QeIROC^*WfcQ=j#*3+3Z6h`O#VTbS$)V^w_Ng3X+@> zP-$KMS5};LC{0aGEo?_14p9v5UgDo+bs=RGYZ$So=H-!v$Cq8i4@u>iH<)`Onab_m z2b3j5cI-}qdKJs@_3e{ooR(_Dh4+0{E55yV%TOa$rkW^eLJ3Vh8=lJUpJ*IIzP_EZ zv7D%qd6vKQLApSZ@n0mWoTiTZd?!o(t@{g)U21abM84%!RT=lA=$2R|1bkjnK_Cl{ z$$u&Z8S7vs!?Z3gdK20eeSb}u;~u;3;)OWUiyt`1;sbOh3Oy@$%o^G2KJ%RY(6cAH zS&g>AvC%jx#ptR%f?WxDkYBS)4nYu0w4*Vg;LOQU1~So)31r;RIbkue`NN2yea);g zp4;M_eH{iX&H0D99LZNBcBuG7o7dcvqI6YrJcu*+djaI5VM9Iy@R21f z5hz3(po}=kX7&{2KAg6#*^Hp{-z1%Z%VJcFl_9&E!xCJ4{dio6&5ZDJz!b z_%l^Xx^_8X%fcip%6x>i?(DJsYV%fjVA#e@ZDvOESI2PX3fzsL;yH{@M|jF(5?Y^^ zK2T+(%4Nc4@u;_Uq#Na;B4G~?|RHQYE??6;bzG-a7z~8NBCOS~h<+Q#g4?t=1v-3zHB zOK`v*Z34eXI>Yu=J!+NlIr#){ZHO6 zqjesuz12>U8#G8sm2#L@$rC{R{U05QyKw%G#hYE|?PIHk$(GQ`H;|8Er-V<}mx6i3 z%@S+NBt|h%{j}8^aq$k`3HWi_h_Z0LxRu-VT8x&jT197!sGB8=BF_5kH{f};b#xT$ z<(51@8c&ce*r)RPdp@40E8YT%ZX8$lo^KSc7W=7a^f2YzLTj(4QORCpWaQYrroNm? z{!2dg`eh%~<`xvpE5r9Zl(~=s5Ilj#GQ@3Kw9#c(~p05WKKqB9h z-JPA>RBL+K7JU3Mgs4_^5zXpW24p5)$-rq$Z{;2x>)t~fb#d@$2xy`iB21ZSoi z2To-^5e{Qcin(srz#@n9_2~Yf!@2{e;Cq7IttAwG3L-^-2w+XGtgHlnt3#{_^EEn# z8F2`w0X;+5T+@8UW}nMWD*J>%zUat5gP8Z%WV}9BnOr5+R#y6$Y(KNaGA>n!+XqG_ zH#U3=7v$j_1<4=xjQlGA zKB%|^A6_DAaI&-0-W!p8bMXv9Ku&NDE6-L*UuExR=w=UnfN1F4ex&bDm5^6kdkhrM zY5pGBWT*ItW5ySh_u=M8Va$b%N0)rbI^4e7LU*1|eYthI2(+0uo!5gdO;lq?tVm|> z6nuUzEVX@fE-md)NMCkp%SA>umuTBOEGXkJ@R_x3Xpemk+*&XOz=u_qgd5zKl1eZV zC>0-`@2A>n&Eb$15=u1%%um2PWrx(~a6;I*mV?S&_rn1yLH`#%be{(P&TiOaT8!EJy$IJwlJI6v>ljn>@Gh)k9K5R>ezMt*!0vASe+Wxi<{&uBRzS~2#{%&k;P7HOy;$@5K^J`L@ zo!FDVe!arRM)SqC2u9o8*@+q9V0)3WCJ|ccGZb)y_+9yI3ch%j7SK?i2zn2T2zJ1( zs}NZ;C;H{GDR6!Ol5HER*?i5{7$2XIQ2YwK-G0e@I1EPTmB_%K=vOgHBDy!4lt?~% zn9rF6L;X2@_SCDpc`)}IIyPkH(bBgdi@;u$gD!~Z5#=_;Q^Uf8isG6O0ncVpb=Iut z4&SpD>SF^&lG=Ia1xf*lYF6uqk-@AD6jEWEU@qs?G)uGfOKDNvMHYWOJ#chZqd+sK z@2V78s9V%NfiLSF45A$!93r;;Uc3OJ2tCD);^Ob*q4kH40_7^BE>1dMcqFwRy;5rK zUw!MKK-g)K*OY%UgtYWN*~7>@rB=GLfIX`8zp$&@ZLBqcBSW z0f7Q3J0Dy#&F7I1ej~5=MVEv^9Lu3YrH01AWlxNu2?;9(zH^`VG* zH_LNfb&AJNMO#0pUcxTQdT-rcc@swi+fyQWWeTq2&Ms&^zWTz=O{CW-`gJ)=%sbtY zTq zaCVruxOk|K`b@AD-F}?k|LjCD*Vt;!6*#!3oIJH;GKb0TdvH$uxWS+pCA78Sl zCqR0wUlF&!AGl@REYDA6-UFj~dcK|}>!6_gp}!|v*Cvl!2ws+pv{*k;6zxmd)k4#b zTn!6GL*h?UFN8QU15y9-3?kWc;nXA#dV~J+&yXPabr6stQ+BkRXh8KsoD)|VPJdp! zs#qn)QI|;rd7wi9@2Lc#;phG@Z6*AFQ~lrhW|27|ekLFcF+d8KlZZ-6 zN*s`>@C?va@`4`*qal9+ej)~lU=85lA|mLeJ<&5~g7Wef*T<_EAhPKwa>3F~-TV8% z^erqb3|a%(1o`;T5N9JGC$;hv3=SfassOeImsACfltED%6ozsDe!8z2>g$&k7XAiJ zU(}E|!^l527PBC{QZ_t7X3LE99Yk-ip5sWan8~{3idwP0+R7@EZr6@kW z+GJU`yyHFwt_X+};HLGMp;-{rGy#tc0-+xr*cp{HZnYLA5jsr>izXjk(aC^+YoMD0 zKwkoM0FkY*1zc@zRG9Wo#Kz`qE7%p``%RXf>+n^LMB?q0u)N${mF?rLdnXT0C;JpW z*SBix=%B$kfa?d`5Lj%1W2(6L!HO;-3(B87M5`!!a_IWa(#HqM8Vwc&7%q2rcPrNi zszH+qHV5`w{lb0R^K2LRQJHdROta9Gc`9SH>dA%Wtbxr zdk1p@21S6>oRyUYnEAqCz&neg4GiLz1vcFmq_yIb86aI@NmbFWcJ+$%%NH*56bNO7 z08z+oCfvGP2EBCls|0#jtIHJP+zSjJQ~f>7lc!W*>Pz5Ut|~5m&id(Kkeio}ueY11 zna=ipTjL_a1?&VXCnwOA_~Xs%Q;jNr|hM*HCf>JLuq!k4M)sB!0~T zl*^KV973TNF@Jm7-oEKo~e{$?^dbb6^+&?}J znjiuJC(K_a5m+(Y-@m;)m{(j3gw_i1LQq3kwZQP5SDUh4=B3CTPBX4GLA5|1jWtgJ zn0KH-M0f)xXq)gn%RGq~9qPVL6Oly$VSNjso4VW^JPQmUpwWD4Y6_@K`4|f{y6B+~ z4`A|OUl+HEK(8zi7nQKTE+bVZYkUn@GZM^(4OycP5acAbNUm~Pk>Q;kH_+HzU_@Y{ zhFJGaOeA~H=#CMPMM*@FXT6>;UfjCxD}i1zVb(NR&+T-H=-}k{v6G0LFS~U7f@8QwxYX zT^}?HAtoL`w8PTnk#s=G31r;+mCzfpDqt6acr?46tUkIrNATN0Z@1QL@d@W$A94Hkw-dQ#Mf5?MUw z7$FAlK0BZ8mo~s!<+!u~?y0@4jX4Aa94@&44xbiXoS`1zvKIUFsA56uTLk<{nXV9m z8N#Iz$rQI;V-|dELlR9s(>1?K^PtI$mhoIG3k!?L3nwT1c3g}M z58c$v;S{x8VHwP0Mt7aS6xW6H*oy!9^*oQ@!WUQ497tpc%V4`CqMPb3`lpbOQ0OyA z@by~WfjiH@X%N7F`s>%PF+k{ae6ZsN{NRB~Ol;GK_|EeP+umxR168IT&VO^tZ)JlJ z&rgq8f{U87<%9WXo@feF%ej>lU0fDe2EIQv?3aMl-zQ3+PlaH1HupDQpyX{{ z#1M+Z6B8tit|C-p5;Re+kZ_>wGJ;$UYC~-9Vg@RiGRW0E1WYTVe-lqZLIPV7kvXKQ zva$he@z8MLNkuwHa0GV`q_5fTR^{0b_gG%+l{$a*a&%k=rF@SwfE_TQ$~F)JdRAfQ zL5o6v0s_RTyn0;&g9Se=F_6dxF`6cT2L?M%3JsDnpYY1qDb6_qERTmMY`WpfB3OK2 zssnW@X#N9hW}Ex^X<%R=z^ZLbzIM z<3JF7ySFu`mP7=bbv2C9w^0(rQd>~Y2AQYnh80kOP2_Qf`0u*Tiz^BaMo5<`6X9lwB2-u#>f3l zsBzdt5g%+h=u#aR7+9p)I_<7iXaj&^9x`=vI4lp?=6DKDo5KTVTxwVDW=-aoEH-5+ zpe-%46t(~u1z>}cSX#ORkZn?x1e$^uUg|>e(F3~^84-UdLC|V zPH<*`zQL=5QViy)!CTnpMP$Dq6HxWBu<&zWRYF2T9S#7X2fe7MpOr#`FR?|7G`h_e zD{qyupuUiMLn9-)OWgM_Ln2@B6ny{weR+8~Um!M08qK!?lMH2wllCcco)1$Soc;Op zDLwd(6VHFm#2S?Mjs|QYmGT8YE}{4ov$_CNR&P*KR7MWq!S0^6Y)BJ(Ztw+I_R3=W zDtCaoUVDxihqAL1N#$3o>VbE0qu&EW`z(Hycofq!vR2<4@klb^16=x)zP@yrYG7bp!7b3%%K}= zw}(>S0E&vV#+v3ZYO@CuU`dstRf;&>RACeckZ^ihi;#aMI_?xgfRIzAg~w zX+W9=fFpmvRsc{z^5onl&mAd^s$%t?;0(b#qH#4}e}UZq0zIfBy5gP#NAN_AuQKgp z2rDd+xJq!~Nd~V@*(!I^Ite@qX!@0B;-?$U5qlpnqDH2e(n~u-2jPlx=coaowyG)* zrmC#m3Qo~!8CnT`vp<*+>ki&Bq=|_=G$uTprZey0H)Fu>HykS?0BBBZG)=YuUIyq9fMxpbj*i^x_T#hcNkq^#@22Yl`Mm_H zw5b<*uLrxq5depI_*&g_j`sHI>e@y|w%-k-eIExRD|hT|ZFP*ifTK0*S;Wsx=On9v zol}@7D9+Bt#sTqzNTY*CoU9Hscm)JNd>=HGvz&6OJ1Wc0ZZZ2RA|wPX(*-c}Jcbb> zdzugY*syH@o6W587d+5tG{DH<;2w}>K`IyIii?Yb20%9M~48gOWrY<&K%u_-rLrrke( z@`mzW=Do|4$=}ISYXKtzEP$!y$cH`N@&7?6*rxxlz3+}{s{PiCBJv3$iXuw2qJV%R zO+cb3qEsmf5K083_uh;80n$YU1f=(b-ir_v0Y!S18j8}ZbO_08zB|8Jb7$SP=C7GG zch>#Sh3ABD&in3n?`J>Hvw>Z^yHve=CVY{P3yz|Ey&8Dkd4kG1K)~P3Sq8;zuhT&* zfHY5XU~413Ee-_ccCK3A_O=+G8p4y&@g+4PS_xRu{O7^S;G>Uu8~=6YvuDo)3U`c- z=H0ZUkIU8IQ?#^M=y^QP%bT=++c(%5EDkJ6Ql496CUwXs^3>DTS3Pi9n^I&=C0XWX z-v%gxfq}t7??(&5_7ue7y7W}qz{6HrS}}R-O#bz*x>~T58g-wub8yTBbvNH&LFa0o z1{az6OvPvU>tORVY4s7o|33cp!%`x|3zzk&G)Q+WsI43yb;T^MwIkbgUM|hd3T^Ce zSa^g@eidb)sylzM2)a5O5^E6LJqqvb;{^@}@S}2hS^S>sn!@@ry?09E`+!vGW;;d+ z*!EUdd;-6YtaJ*wwP`q}XJ$^rA=mljyOITN35`NsU21|OAUq^wxZ0(CXDy-4MnLaV z2YXG$WvY4F&yYSJ$RwDO$NR2(u}2-{28N-f^Yp}{hPIgyFaYy#1Y@>gy|7F^m4dFA9AJnW#sl}26fuLbp47? z#Z8OvjZxBpR8|$LJvvqECF8((GA~*tP{>0zgu8hk4JJ`9R670F>Bgw;dhPb;P4^89 z?2vZWCSy}(`r_z+#!F1I79Xf+ObTEz8$Cn4WlD3%F@+QgT+L!b;65>(KJXh$#WVvI zBA8;|KlT`5A^ia>t6`PB>54u4Ndu-=D0z@Os6N>_;FZ^n2i?xFFXZDyNa;Ya zAy*yrS!?f?+K!8(qdK=Jxu?xo{FK=zZ*vy<84;{2#nx4;Y9qh~d=Jp);bRaxUp;Yr|1T^6&=v525T}D4oEEFuh<{vsJc_#_H+K=l^>Piki$m+6 z3K6&9W}QEh_1!TpzqAx(A~Mn`kr!-+xCw`C#h;*H%Eot{{+F~REZeZzh3uh-9v4wK z%Mny;ZM;MzEPGm*=vvphZ^&sx=^x&oLyQ@AK6!F}8(!Qd`3m#h3VjJ$|gu5sTmx3N*4PKk(!2n#y_;L|&&r7klb zozGB*YxACquUPM7@Y#M4zodQ-eSC_C`5qUbUB2oak?ANE#}`@hPo^!n&)NxR!2S~N zP%UgYRCm>Oaj+Ds9yrzN+ynG9HM4LyIID(#{`{-mOB1=7eA(3EM(!KSV$EdD^f9Q5 zjAmHnEZ@UzCyxPoL8-5{U#^x_LwFGibv`G0GV^mkiH#1AlzV9lCiV$M?=E9Q>_267;?7u|!xP1vTd{MMnz#~{~u zbIC~dj9u>M;*|Wi1#*{Io-#GKCl%5xBF5epyv_fu&t&7+%yRxbV3SE!2ghEH+CVU~ z01!tPjlS@(4GGjC2buVqfO1g;Aj`n0J8_Q(I(fb&#lhbIL`f+w z?%t7o35Pat2KjTLLVe>Mzr1JzR)ys&ef|9-dsdq7-?=zDud9h}%t`v`cL}^P?w`59 zfj9}6?dp^QLgW460^DcXda$dpvbG)_8QF(!X}nL2Er4^1sV@s^=-BUxAM_4Mv|R!K zzXPNWl(*jaot+)1rC0bx{q&}J>tUmP{O$WbmDv!bW5Jph*k z>x`KRj>mU6R)M2+Pal_;M~0FObqHFykzF=mN=S7e@IJUV1ImCql(SdeYYsj8r~P>S zx#Skzg@4+c2*khB!@=eMOaJq~F8H@T@V|c!d7nHis>Ao>*nNXLF*Vy79`j(HS=TFz zXpAghkfVfnEFpA4kQ23z@Ia~mXTjlr_<+Z&sNUo&PLEddTK4LiSGQRKBK{a;R{$j} z+5a3Gs+MG>LU7*)(cG_62Zzr5DuUXQPN>638&f5N;QX+Fgy_Vu&zR1;YTI{h^kU3d zrbsc3*+%=vJ9&6D`5 zD%RoMlK55(@l?ZDK-*pCyf2mB)82!bUvHakp4#o|Dk~L3Q2zpO1x}j&Jl(j&#O38> zB9X|zIa1|>2Xw%$q_T1+H61&2#m*#M6Hk)7ecNH;{op*bSIpfPtL>Ko4TX#Y zD=aqgsAUn9P3c4^8~o1$-DZM{8c;9rK=?33Ht}o6%XtwRD}9KWtv*XquMgY4l2Dd! zQ~dQYsjuma0p~2Mm+rdDoy_?(N`EE0i+QQ{s62jQCYb7}nEIyrQRi9~NpQ6EQ3s^s zc#iFiR7zKuYHm+_FVqc1^SirM26KU@d8zQD^I@vxL5)x`L)^h3@gZy(DvP3d$HK$H z7$NCP0WHik!8q~$ZR2`}^X<_~&pnb{)@D*)nf}V72+UJ%u%4&;UiEh|ciVQa>V$eD zORiLkZOw1iWrlfB=>CbNdiq*@TK%w&o?gPokH;@Z zhB8qY)uz_&<#2U8kC_A@-J~`5^kz*?aB~$K-<5gSnE3cyJigm^u4-wUo(|tF#g$R` zIOoBsb|#G6{PuNS6Kj2g=u$ts$+?1$Pc=_i%1@vW2+$#2rk?Vu?Lx_$?uaZ76aEm| zwpR(A8XFRo$0rrSirltV1w}v#)+TTvV zH9@xrQqfu``An_M^TO4w+8{XK!CzJ8m9V%Y7mmrNEl3{A$q^G11N0#~b_H5Q@^aqm zf4^3p0Kqs{-1(zKngyZg1UcGI)YgJWxrP?w$&l=%1t-4f45E-epqh zGdH}vb=zE*xIH^B7}{4VOIX@HnXvNZ+KL<}Iv{?JI*~EbSbIOS z+q>Vpm>6-D(BB@mxy!k|+12j(g*13=T%2$ulD&3iet(qnP5Zu_aBKT^$DX#=HEl`{ zXZ+mPzjDUFvY3t_q0G{J!7~wY&-|xDK~>k&4?3EfV2_7DkEyq* zm#W=T?!KN2{TWGs1Uk#1YPVBz6DUsQmM0{7ZN^B)zfC)wa8#%^KVO*&fD1X7*n3a% zb8-|Slq0mc3u|h0UugYsk#gf%H`hzo3hn24kBof77XemYrlZg#;Zyw4`5fZp#Q@$K z3wNu8&Bxpt=Y_u-=FUT}LN0UAdlB=~HMc>v(7Y^Grb*x3eHYXXw0X4cEZH(<#8L$- z->8fT%q%F#&IE2mghifBr+@mf&mdaTD%rT-m0clMsX9k8=PIa_9y*xqG}jG!TfKEw z3>?|X%6S33e!kn@qvsH+!CW2J)dOk=7W+C^nme~`ym%}&>qR{q?0ctwaV0X!dp=nU zAdioGbKXF%o!W|BR^@dapZ1!a{2j}AJId-v^|PpNCE*{#E97vPYX8MdltpN+9SGImfpf42<6d59 zIc@H~xm9H8oa;Nrh4W@s{$2ThKJ`{P9gqAjJX-d-56{hH=fw6t3w5>6uDc*Z{b7GT zZr6MQ(#x+RRQF;#&mE=C9yLLx9@*Q9-)kc0*K}X&tvG$g1Lr(G(?2oeL+hc#C-frN zLq%_WAb)M9zdf}wD13*ATg)2CKfg*nKD~70ckPs=cccU5o9O81s?*p}PC4vek%J}I zV1YeSBMF~Qn1APKDAeU_E6Aeuhn(7zJi8rhL&Kqgi}M(dhLMY=n2S zAK$rsJ2fTm?rB!3-nWy2_I-J|(rL%zZAQcx5998796+NIvL*b}!7ciCfa6-eTlR7W z#1azzZwu0QADU=zN(Z|`PTu^5XRVQe@7mARkVNgHzCD)^m+5mXo?8622>;+OCFznH zacnb3{gMI)2cP*(%r~3^VL|HX&?!8TTtxKc491mi&hjPlJy0vJOs%u@ZfPg4EsIuJ zueWb4?S0*&84d5gC2^tKTdvrl}_rY$Tjq05fjFw(xb z`S5A**W@~joO5_ai9#!U8hNO&sI%k6LJMSoJyyYn@Ac5Y~Sx@h5}x7dY#}P}c=H z%dd|(c8HibE6^jjzh$)}$rjZU?)?WB7n7Zx@;l&Va# zf4i~4yOD+X$+MRzSWwR2zW29fF~P$pW;rb3YQj3r=18Ytw4nRYQCmN)%w{{rVx{Ez zeB2XU##~tJd0ryVJ!tq`ONr$wu8kk9^{NK70Gziec7J`Rt-=J$=rnZh?6}Qy%3nms zpj9e?xxkTHN@7zjIl7>G9~X~%jY9#O$ig{Y&Z=0o+}pgcPfMdEfgcd$GCxy~^==VW z2~#u4Q?DX-P+NV@JXeOIu8A}@7t7%Q*Dli!WES(|UUc=x0xdVUKKH;VRH{QyGuV2+ z^pOGdXE;wJbL^9syS@0Kp}xV6ioe|L$J0|#U%J@et5rTsOt7U9^P6QS6hK%d}n zXYB4ke0W7`tkO%TE#>(=c{crUiu3}q2z=d&$-X0~#&@j16JYoN&c8?}1-^0KW7{rd~`wYux9 zgvs*#uc=b6Pi$YU8|u5gRYIwM3Z_sjJt3HADU<^0XWtiSU#;WqOEb!Aqa$xd1_(du)=pF{yBhlp zfU6$^>t_-o3z^~~H#Z_g9;oqnSfsthp>c;4^N%@RkByBjdG2xN-HEm`6PkyI#2nw} zMuOY;prjSdZFEj)x`&}r4j0|VY2 zx=@;T*T>sHRI)C;uVBi%Y`Mgzkw!KmXFZLZxKo0jl#gB>6&M0>zUYa~2C1)86&P{rCL1=ZM*8Ud?O<@$ z;C?$3rfH_Q(c(GPywD^T^5mBTQ(-aU+X zy7E`uy=K&2#t-CToM``cTVWZAE(%zQZ)v~um1t9Oszx03eF;#Z>XHUzw!`l!Hth^s zw#Eow-e!)hGM6*q;o;>aB^^7r5nn4>sGm+#JQmu>HeY|L;yh|6yVO zzpecLk6tj@jCZQ`Y4n-}ECOA=O_w(c&Nez;|I!?Q51}Jqe(=TrvkzJU&+e7aU~H_R zp#g@eIzhdkJhmp~<=KaZ4jB80Sn^vyPwdm3=RrfFHTGQ1=C2DBu&$Y&W~q; zhd21kZG`|e2Qs6;2vO_djy7w&kHs9Rm;vA?Tow<`VQd$q%^vGTg3;pA3JJ0)+&jh@1gn|oQ*8tml|I#Pf) zBmwz^y13O)P&ESrgm$@gFQ~T|iUC^@5_*3Q8YrH?^Blen)BqFXexSrzMgEAREdVdD zi@`_b2tb~Mq-E(7v;cTgnwl!1v3;u*MsU1^Z}_`(?MDhvA({EzyTttbM6uuHk&!;d z*|QWJeyF$0kIW#gt z&&izz(91x`5>;07Ah4gRW`JM#>DD!Vv;$Ci5BROxcvYNz>JM!gxd4p}fE*2M&t(sL zKte=^Yc}^OwGjW{yza<(pY^12($L8RU+)8(a$Sj;w(pi^v+Y76WZlJuYL~6@GRt2x zp{y)x#Ty#}JiHn~!PWcpenTD}1T(jtCbWi6UYYaWZiD2i-C}GM$-&8qpzk*Dh-9GH zT7F&>&`*Tzd6+u#xyo+RziaH1O(e^8CgMt~8Y&8Gt-|rq(2(G%hviYtkYtY+OlB;V zNC$puq;llBk$_J7F9pu%&OzORrw(l)1NtuNtVpQcz4K0~-`e)6b(kh<>y%(A%Y%c7 z=4QtddCpNPKi?1j94;zCZXPQ4fmZxJT^3dTS*Cp(sZ9!bKx?kV*}2z9%CrqoU#Vc%HYvuVeI7F4plXnOM2! z$}K?R*<}+}O|_$Tapcm$)Sl3gghWTzL`gS$crDNhb2(&RZq89@H)$iophBslBO7la z(E-}nrddi%4pvlSlZR=x(F#t%%b5^5gGab~+} z1A>P|2?1&k8FAok3b>VTP!E`M}1?s`hkYX673qFkf&K+;Z}7nh|(r z&0ffT2$rom@ALX;IBF_HegI=Qakn1+qTK^8AD8#(_avZRsh{vHN5#r~CxBkxZe*v9 zI5@~6aOJt{i11&ZHJ@mU(oJxbIeHG?awIuju_8u#08>mjhrYsLT1mbqeQiKe8%tqV zY0~gn-q#SKW)Kziv9~QSGMP4EVWps)HfnNniFc4#%F>7pMvq)d3({RX3Gv=!&GFek#l{kPi2r!lR*?Rxdweh`%Onig`s#=up1T@PP)}}22<+kz#Fbe#UQgW-&ye5sMw?nPZ&ZU_IfZGry7XB{6@0_lP5 z?eVNt41~L`IcMB~cO9o^r=fjLL~Jd2;!XMFYwan&EYHct(455>rw;YU{k~r)fOA`= z%{2xMi+H-a&M2jBj#MY)=4OnHOpT0iVlhEO*C?}jc|NWRnOMsA^^&e-iVBvqEB|;@ zG{t3c(Wi?>w>Q9Nt*fhjcfh9BV@uY{%`+uuA#-{7Sog9Hfy_KOMA`fi{m)U#dg$D2 z%0ybx031ssp#>7@Q&A$Wf=8xNrcG?WYHL0{@M&h1(c~>YOT)lehmWkNMfG#!Hwd=5 zLX3LDUaoVzGNJE=)+y1o?x$=gX8XTpOR0P-mGvCCC2)^c`Lao)ac2#cJfXbirk)Gi zf=6eS(q0r-uki*}(zviK(v!Z+}fTTeRx3I`3B$i`ke^1b8Ya{1r z$YbjzcdwB8W8=0)1^mOo>P~&wzM=Xg4P-m65GUj{G@{dmd*xA<$CVp-+O!lwN}>_i z_9(B0m?dl-5jR88`o4Mm(DtV)cfs*PV>$Wkw5t{TJWl?(n3eYCxwH2!u?D0R;)sC{ zSxzYRoyZHDXLqmPk^{2z5@`v)!r_A}73I}lS{wP2~tLc7D!YY`%A*0~PbN-0K9KI)%hqp~GQWws!4PyrtkO}~SUmno|=@M2q( zGAFzfCRZWtx~OvPQl1B4-{DhuQ1``j3*Aa$sG~8mx9bnSua9GQhRhlj?bALyOx)Cs zHljVW4cSh+i3N^*KIrhuAkBIkn34D@&kW}gUf{3K2iuCKL^m}~bi~c1a3_+3ZhJWG zt}s)MCUbxnSxlwuMGoF3E|K1Z1}C!2*TBm6p?p70q(s{@xs+(hgXl(k5pj7@a6UcRCiCmI z*k`UTOJjkjF@g@WZq(1L6vB>v=cla;R7Slmjpy5%2w&i968WfwyKDV|%VN2M-#z7L z-wc1tw@_A}vf|sMUFM@SKZ@Ar0#i*2d?e>$C6bMOC@%!K@*j$xh!w=peEs8f^`8So z99{x{@kWu+?Ou=5g(BAVAHU`892sl*?(8s?Mf&Y}=6EEx}n z&-@%E*Q=>fT9&_=_84t(#>Q6Jd2ZBxI8YSZYC%Kon?2GQuRN&~7<$RTbd{Bm(pQ5m z2n|5Bgm5OcMi|=E5Lvswe35>g1Qb2_~;)rhY6(fLzz>TbEt&bPl&h zTr%p~s8r;GkJ5O--E-Q!JiGa+0WaV{T_Nu=P!ae9D1NCPYxQN6FcV#zi>`iJT*UW%+C^A>%%VFqHVSZf;suh#1PlJFtBMuI1kZht35Ir(24M zi?g`=_~l7m3bQ#J+tR8`N$7+Q?0b7dHncVdGx0O#`iUk2aKill6sa8UkIMQR{2|6h zGf}_Zy%4tf*&1ZIoJsFwoU1$Q`22Yq5WD_ZnFi$48|dnB=WK3la98__uA1}b8hCn2 zewqsCzheBHa|D#D5>i` zv9;uE>~Xp`7>7#?irVZ47VsbM-hBZWXgD6UzfTw@QUnOQb3P06dlCXza;T_Htg{P6 z)xyGcwi9bN z6~13=VGvUlUM?2ojDg|mHR|Ik4UZ!EC!Gl?(ngSBq&RT(B2m31mvB+p(&<4pP$}p` zuci$IBCLIxJVWN=cEu)WoD`M+H=lYGJhvcKYSuMOc^Z6!d`_XET-0+(?ShA zJp>)ulEwb8nQo7*CBLpbXIXrKJ|9erxsjJ;fh`f=H?Pia!0F_a&is|dpUYFCBpYb& zj_&uIoTiD3qbz=V?0Q+{$C5QnB7w3~&Tdt|}{wXd{)u3TVQkAW>mdb^oCVsh2{^25u>|52Y0D%-zL~tFCi0StC(GE zy=+$`n9r);CCk#&@Ac1|h6x{IjFp=Q(_7C(0bx8{k|l{plP6irq(ROrCZC%YNasLt z+9djvsX2ff|MiOjx0<~{grF6j#Qnfm-5n<2X@K5#b3#3K)bShtI}_- z8Sf-20B4DAAH%H|79KuSFk$qV@W>49HtJuhC({993s?S7AXCy^#GXO(iOhF!Q<~`G zoan~DLdZEusJ6OVqHuMyr8ug9>@C|2qRqjAhkGlNByC<^pV^rgzZCL$czq_u>s>>N zW{B<^xQYGzWf(6>27!;raCPYhMBE{w&BJ{S490tRbMaNkk-di9UF8$Udt~{Bt~EE4 zw`)8ovpqdFt3FOXdwsct?w{LS+71Wo4o?gi;9+s8;KVVTnUbGkrm}AE^Y+u@=pA)= zwK-k^0gVB*n5Ug8_0!~_3%QT+YL`NJ>pw?j*5&XWJDv3yZ(iVS{`}Hc^E6y3ON?+n zjyCtalgUMwSX3@#vU~WAIajW#WbzgDZ4|1~+s3Bg6UoYLXLpsDFZWXyW>!FbHRgp< zx#d3D_`(5MRy7ETkwa-Io^vw*&L!XA2M0yjfy2Om4*)IThNuMYzEQBf&q_#lcyZXqb5;Tf@FmUO|T8w6OsxE)et_a*~TYE5V_W6`{ zs2Ol|`S^4-H9=5zVsf!kYhOXZ$JRC}>*KlX?=W;mxL~=`&{UAO(SP=IqH&%IVQ|jU zUGb)v*oVsJq5b|wVeP}}@PUY?WN?gj+27rOodI$XqcNB=WYh*q4L^OON%#)`7Qz%W z7R14T_HiYs?X*CSC0vw~W9jYfZEQRRNojKyJQo%>;KumPxiW1=z=SFshlH?j`mKNC ztvO5mHr7pz%89^4<7#6I)6->UHR1|ON=j;Ka!XCqaGzr}4(WB93Y6&4?Zmnm7=VKl zhBYU!6pSwpUW`oDBbR&01s5<-PtSZfmwzRDChK^%5dI8s`tb_Yg@wu3MYHi2FAn{F zlHT3e_$X9IOH1V>O10EYSRMpU89)hFKg+kpXrXbvHxu)kT&{W@HHvfK0Nw+KJYtA~@G zCmA+^6auYq;)g~o7yxzp7R|>?xCkXFmum7o>=c@3iF_xVROJYYJG%i?F(|gdZRie> z7K1E}!1zsdDdTt1XfzMgf!jCY8l;L$!1w>yp10YNGm&MVN ziyw*qj}M361s@(|45qIM?@&|pUTS`;eY5WmTN%r0oyF>UhiRvJkAk9t<2OnMM6vY= z;$db*@Y1gumn^UoRu5)z-;=t|mb{W(RSh$V-p)b=lT*3^;@X*XH#<9()0+34%*}be zj@St}7PE=#qc5TERsyxEy_B>3(Qi$wNRip%b}2Q%{sLk9u-}1z6z!)V1M;I2|56$RAXA820mnin?u-Gggk{UuE zfc~>EYCEmRjvZRNGCC8L4wy+{W$MwE*2hP572oZ&jk~H%>J`Ct2(|#k@sD*0ylo4~ z2j8NuS6ZOs9zP%z@OQ#X0lqnl?ZI_ Date: Mon, 30 Mar 2026 07:34:42 +0900 Subject: [PATCH 23/26] =?UTF-8?q?#269=20e2e=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- e2e/pages/sprite/cache-as-bitmap-hit.html | 104 ++++++++++++++++++ e2e/pages/sprite/cache-as-bitmap.html | 92 ++++++++++++++++ ...prite-cache-as-bitmap-hit-webgl-darwin.png | Bin 0 -> 8180 bytes .../sprite-cache-as-bitmap-webgl-darwin.png | Bin 0 -> 8435 bytes ...rite-cache-as-bitmap-hit-webgpu-darwin.png | Bin 0 -> 8180 bytes .../sprite-cache-as-bitmap-webgpu-darwin.png | Bin 0 -> 9046 bytes e2e/tests/sprite.spec.ts | 14 +++ 7 files changed, 210 insertions(+) create mode 100644 e2e/pages/sprite/cache-as-bitmap-hit.html create mode 100644 e2e/pages/sprite/cache-as-bitmap.html create mode 100644 e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-hit-webgl-darwin.png create mode 100644 e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-webgl-darwin.png create mode 100644 e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-hit-webgpu-darwin.png create mode 100644 e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-webgpu-darwin.png diff --git a/e2e/pages/sprite/cache-as-bitmap-hit.html b/e2e/pages/sprite/cache-as-bitmap-hit.html new file mode 100644 index 00000000..737268ae --- /dev/null +++ b/e2e/pages/sprite/cache-as-bitmap-hit.html @@ -0,0 +1,104 @@ + + + + + + Next2D E2E - Sprite cacheAsBitmap cache HIT test + + + + + + + diff --git a/e2e/pages/sprite/cache-as-bitmap.html b/e2e/pages/sprite/cache-as-bitmap.html new file mode 100644 index 00000000..f4b52be2 --- /dev/null +++ b/e2e/pages/sprite/cache-as-bitmap.html @@ -0,0 +1,92 @@ + + + + + + Next2D E2E - Sprite cacheAsBitmap + + + + + + + diff --git a/e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-hit-webgl-darwin.png b/e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-hit-webgl-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..39859048275adc980a16eb8b734acde61cb00261 GIT binary patch literal 8180 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 Z0AlV%pF9*#v*8Xn@cW^?g}Vut{sl>fSTz6u literal 0 HcmV?d00001 diff --git a/e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-webgl-darwin.png b/e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-webgl-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..4fc4cab77d59ac89247d3e0bc909da17b64d9bcb GIT binary patch literal 8435 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 literal 0 HcmV?d00001 diff --git a/e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-hit-webgpu-darwin.png b/e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-hit-webgpu-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..39859048275adc980a16eb8b734acde61cb00261 GIT binary patch literal 8180 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 Z0AlV%pF9*#v*8Xn@cW^?g}Vut{sl>fSTz6u literal 0 HcmV?d00001 diff --git a/e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-webgpu-darwin.png b/e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-webgpu-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..0cb0b828e842140b812f1a7813c446e74fe52af0 GIT binary patch literal 9046 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 literal 0 HcmV?d00001 diff --git a/e2e/tests/sprite.spec.ts b/e2e/tests/sprite.spec.ts index 52637afe..dc6230f4 100644 --- a/e2e/tests/sprite.spec.ts +++ b/e2e/tests/sprite.spec.ts @@ -25,4 +25,18 @@ test.describe("Sprite テスト", () => { maxDiffPixels: 15000 }); }); + + test("Sprite cacheAsBitmap(コンテナビットマップキャッシュ)", async ({ page }) => { + await page.goto("/e2e/pages/sprite/cache-as-bitmap.html"); + await waitForCanvas(page); + + await expect(page).toHaveScreenshot("sprite-cache-as-bitmap.png"); + }); + + test("Sprite cacheAsBitmap cache hit(キャッシュ再利用テスト)", async ({ page }) => { + await page.goto("/e2e/pages/sprite/cache-as-bitmap-hit.html"); + await waitForCanvas(page); + + await expect(page).toHaveScreenshot("sprite-cache-as-bitmap-hit.png"); + }); }); From 3d1e266f2863a2d1099963f600fb17ed782b493f Mon Sep 17 00:00:00 2001 From: ienaga Date: Mon, 30 Mar 2026 07:35:00 +0900 Subject: [PATCH 24/26] =?UTF-8?q?#269=20=E3=83=89=E3=82=AD=E3=83=A5?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=88=E3=82=92=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/cn/display-object.md | 30 ++++++++++++++++++++++++++++++ specs/en/display-object.md | 30 ++++++++++++++++++++++++++++++ specs/ja/display-object.md | 30 ++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/specs/cn/display-object.md b/specs/cn/display-object.md index ce5fe982..86ed2f7b 100644 --- a/specs/cn/display-object.md +++ b/specs/cn/display-object.md @@ -192,6 +192,36 @@ const bounds = shape.getBounds(shape); // 返回矢量边界 shape.cacheAsBitmap = null; ``` +### 在 DisplayObjectContainer 上使用 cacheAsBitmap + +您也可以在 `Sprite` 和 `MovieClip` 等 `DisplayObjectContainer` 子类上设置 `cacheAsBitmap`。 +容器的所有子元素将被渲染到单个纹理中并缓存,在后续帧中重复使用。 + +```typescript +const { Shape, Sprite } = next2d.display; +const { Matrix } = next2d.geom; + +// 包含多个子元素的 Sprite +const sprite = new Sprite(); +const rect = new Shape(); +rect.graphics.beginFill(0xFF0000).drawRect(0, 0, 100, 80).endFill(); +sprite.addChild(rect); +const circle = new Shape(); +circle.graphics.beginFill(0x00FF00).drawCircle(50, 40, 30).endFill(); +sprite.addChild(circle); + +// 将整个容器缓存为位图 +sprite.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// 禁用缓存(下一帧恢复正常渲染) +sprite.cacheAsBitmap = null; +``` + +**注意事项:** +- 缓存期间,子元素的更改(添加/删除/属性更改)不会反映在屏幕上 +- 当 `stage.rendererScale` 更改时,缓存会自动失效 +- 同时设置 `filter` 和 `cacheAsBitmap` 时,`cacheAsBitmap` 优先 + ## 相关 - [MovieClip](/cn/reference/player/movie-clip) diff --git a/specs/en/display-object.md b/specs/en/display-object.md index 3bf37f5c..5c2d8c40 100644 --- a/specs/en/display-object.md +++ b/specs/en/display-object.md @@ -192,6 +192,36 @@ const bounds = shape.getBounds(shape); // Returns vector bounds shape.cacheAsBitmap = null; ``` +### cacheAsBitmap on DisplayObjectContainer + +You can also set `cacheAsBitmap` on `DisplayObjectContainer` subclasses such as `Sprite` and `MovieClip`. +All child elements are rendered into a single texture and cached, reused on subsequent frames. + +```typescript +const { Shape, Sprite } = next2d.display; +const { Matrix } = next2d.geom; + +// Sprite with multiple children +const sprite = new Sprite(); +const rect = new Shape(); +rect.graphics.beginFill(0xFF0000).drawRect(0, 0, 100, 80).endFill(); +sprite.addChild(rect); +const circle = new Shape(); +circle.graphics.beginFill(0x00FF00).drawCircle(50, 40, 30).endFill(); +sprite.addChild(circle); + +// Cache the entire container as a bitmap +sprite.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// Disable caching (resumes normal rendering next frame) +sprite.cacheAsBitmap = null; +``` + +**Notes:** +- While cached, changes to children (add/remove/property changes) are not reflected on screen +- Cache is automatically invalidated when `stage.rendererScale` changes +- When both `filter` and `cacheAsBitmap` are set, `cacheAsBitmap` takes priority + ## Related - [MovieClip](/en/reference/player/movie-clip) diff --git a/specs/ja/display-object.md b/specs/ja/display-object.md index 4abf057b..55ec7fbe 100644 --- a/specs/ja/display-object.md +++ b/specs/ja/display-object.md @@ -192,6 +192,36 @@ const bounds = shape.getBounds(shape); // ベクターの境界を返す shape.cacheAsBitmap = null; ``` +### DisplayObjectContainerでのcacheAsBitmap + +`Sprite`や`MovieClip`などの`DisplayObjectContainer`に対しても`cacheAsBitmap`を設定できます。 +コンテナの全子要素をまとめてテクスチャにキャッシュし、以降のフレームではキャッシュされたテクスチャを再利用します。 + +```typescript +const { Shape, Sprite } = next2d.display; +const { Matrix } = next2d.geom; + +// 複数の子要素を持つSprite +const sprite = new Sprite(); +const rect = new Shape(); +rect.graphics.beginFill(0xFF0000).drawRect(0, 0, 100, 80).endFill(); +sprite.addChild(rect); +const circle = new Shape(); +circle.graphics.beginFill(0x00FF00).drawCircle(50, 40, 30).endFill(); +sprite.addChild(circle); + +// コンテナ全体をビットマップキャッシュ +sprite.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// キャッシュを解除(次フレームから通常描画に戻る) +sprite.cacheAsBitmap = null; +``` + +**注意事項:** +- キャッシュ中は子要素の変更(追加・削除・プロパティ変更)が画面に反映されません +- `stage.rendererScale`が変更されるとキャッシュが自動的に無効化されます +- `filter`と`cacheAsBitmap`を同時に設定した場合、`cacheAsBitmap`が優先されます + ## 関連項目 - [MovieClip](/ja/reference/player/movie-clip) From 077aad3331adf4fe28a02e6dcbb374f08ddc590d Mon Sep 17 00:00:00 2001 From: ienaga Date: Mon, 30 Mar 2026 07:35:37 +0900 Subject: [PATCH 25/26] =?UTF-8?q?#269=20DisplayObjectContainer=E3=81=AEcac?= =?UTF-8?q?heAsBitmap=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...yObjectContainerCalcBoundsMatrixUseCase.ts | 30 +- ...ontainerGenerateRenderQueueUseCase.test.ts | 300 +++++++++++ ...jectContainerGenerateRenderQueueUseCase.ts | 473 ++++++++++++------ ...isplayObjectContainerRenderUseCase.test.ts | 89 ++++ .../DisplayObjectContainerRenderUseCase.ts | 70 ++- 5 files changed, 812 insertions(+), 150 deletions(-) diff --git a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerCalcBoundsMatrixUseCase.ts b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerCalcBoundsMatrixUseCase.ts index b645f749..ce0f0982 100644 --- a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerCalcBoundsMatrixUseCase.ts +++ b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerCalcBoundsMatrixUseCase.ts @@ -10,7 +10,9 @@ import { execute as videoCalcBoundsMatrixUseCase } from "../../Video/usecase/Vid import { execute as textFieldCalcBoundsMatrixUseCase } from "../../TextField/usecase/TextFieldCalcBoundsMatrixUseCase"; import { $getBoundsArray, - $poolBoundsArray + $poolBoundsArray, + $getFloat32Array6, + $poolFloat32Array6 } from "../../DisplayObjectUtil"; /** @@ -32,7 +34,27 @@ export const execute = ( return $getBoundsArray(0, 0, 0, 0); } - const rawMatrix = displayObjectGetRawMatrixUseCase(display_object_container); + let rawMatrix = displayObjectGetRawMatrixUseCase(display_object_container); + + // cacheAsBitmap倍率をrawMatrixに適用(ShapeCalcBoundsMatrixUseCaseと同様) + const cacheMatrix = display_object_container.cacheAsBitmap; + let scaledMatrix: Float32Array | null = null; + if (cacheMatrix) { + const m = cacheMatrix.rawData; + const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + if (rawMatrix) { + scaledMatrix = $getFloat32Array6( + rawMatrix[0] * csx, rawMatrix[1] * csx, + rawMatrix[2] * csy, rawMatrix[3] * csy, + rawMatrix[4], rawMatrix[5] + ); + } else { + scaledMatrix = $getFloat32Array6(csx, 0, 0, csy, 0, 0); + } + rawMatrix = scaledMatrix; + } + const tMatrix = rawMatrix ? matrix ? Matrix.multiply(matrix, rawMatrix) @@ -84,5 +106,9 @@ export const execute = ( $poolBoundsArray(bounds); } + if (scaledMatrix) { + $poolFloat32Array6(scaledMatrix); + } + return $getBoundsArray(xMin, yMin, xMax, yMax); }; \ No newline at end of file diff --git a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.test.ts b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.test.ts index cdaa8ed7..1f2a6ff0 100644 --- a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.test.ts +++ b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.test.ts @@ -1,8 +1,12 @@ import { execute } from "./DisplayObjectContainerGenerateRenderQueueUseCase"; import { describe, expect, it } from "vitest"; import { renderQueue } from "@next2d/render-queue"; +import { $cacheStore } from "@next2d/cache"; import { MovieClip } from "../../MovieClip"; +import { Shape } from "../../Shape"; import { $RENDERER_CONTAINER_TYPE } from "../../DisplayObjectUtil"; +import { Matrix } from "@next2d/geom"; +import { stage } from "../../Stage"; describe("DisplayObjectContainerGenerateRenderQueueUseCase.js test", () => { @@ -38,4 +42,300 @@ describe("DisplayObjectContainerGenerateRenderQueueUseCase.js test", () => renderQueue.buffer.fill(0); renderQueue.offset = 0; }); + + it("cacheAsBitmap null の場合は通常パスを使用", () => + { + const movieClip = new MovieClip(); + movieClip.addChild(new MovieClip()); + movieClip.cacheAsBitmap = null; + + const matrix = new Float32Array([1, 0, 0, 1, 0, 0]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + renderQueue.offset = 0; + renderQueue.buffer.fill(0); + + execute(movieClip, [], matrix, colorTransform, 0, 0); + + // 通常パス: visible=1, CONTAINER_TYPE, blendMode(11=normal), useLayer=0 + expect(renderQueue.buffer[0]).toBe(1); + expect(renderQueue.buffer[1]).toBe($RENDERER_CONTAINER_TYPE); + expect(renderQueue.buffer[2]).toBe(11); + expect(renderQueue.buffer[3]).toBe(0); // useLayer=0(no filter, normal blend) + + renderQueue.buffer.fill(0); + renderQueue.offset = 0; + }); + + it("cacheAsBitmap 設定時に cache miss で専用形式を生成", () => + { + const movieClip = new MovieClip(); + const childShape = new Shape(); + movieClip.addChild(childShape); + + movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + stage.rendererScale = 1; + + const matrix = new Float32Array([1, 0, 0, 1, 0, 0]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + // キャッシュをクリア + $cacheStore.removeById(`${movieClip.instanceId}`); + + renderQueue.offset = 0; + renderQueue.buffer.fill(0); + + execute(movieClip, [], matrix, colorTransform, 800, 600); + + // visible=1, CONTAINER_TYPE + expect(renderQueue.buffer[0]).toBe(1); + expect(renderQueue.buffer[1]).toBe($RENDERER_CONTAINER_TYPE); + // blendMode (normal=11) + expect(renderQueue.buffer[2]).toBe(11); + // useLayer=1 + expect(renderQueue.buffer[3]).toBe(1); + + // layerType=2 (cacheAsBitmap), cacheHit=0 + expect(renderQueue.buffer[6]).toBe(2); + expect(renderQueue.buffer[7]).toBe(0); + + // instanceId + expect(renderQueue.buffer[8]).toBe(movieClip.instanceId); + + // filterBounds (フィルターなし: すべて0) + expect(renderQueue.buffer[10]).toBe(0); + expect(renderQueue.buffer[11]).toBe(0); + expect(renderQueue.buffer[12]).toBe(0); + expect(renderQueue.buffer[13]).toBe(0); + + // tMatrix (a,b,c,d) — filterBoundsの後 + expect(renderQueue.buffer[14]).toBe(1); + expect(renderQueue.buffer[15]).toBe(0); + expect(renderQueue.buffer[16]).toBe(0); + expect(renderQueue.buffer[17]).toBe(1); + + // メインスレッドのキャッシュストアにキャッシュキーが設定される + const cacheKey = $cacheStore.generateKeys(1, 1, colorTransform[7]); + const cached = $cacheStore.get( + `${movieClip.instanceId}`, + "bitmapKey" + ); + expect(cached).toBe(cacheKey); + + // cleanup + $cacheStore.removeById(`${movieClip.instanceId}`); + movieClip.cacheAsBitmap = null; + renderQueue.buffer.fill(0); + renderQueue.offset = 0; + }); + + it("cacheAsBitmap cache hit で子要素をスキップ", () => + { + const movieClip = new MovieClip(); + const childShape = new Shape(); + movieClip.addChild(childShape); + + movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + stage.rendererScale = 1; + + const matrix = new Float32Array([1, 0, 0, 1, 0, 0]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + // キャッシュストアにキャッシュキーを事前設定(cache hitを模擬) + const cacheKey = $cacheStore.generateKeys(1, 1, colorTransform[7]); + $cacheStore.set( + `${movieClip.instanceId}`, + "bitmapKey", cacheKey + ); + + renderQueue.offset = 0; + renderQueue.buffer.fill(0); + + const offsetBefore = renderQueue.offset; + execute(movieClip, [], matrix, colorTransform, 800, 600); + const offsetAfter = renderQueue.offset; + + // visible=1, CONTAINER_TYPE + expect(renderQueue.buffer[0]).toBe(1); + expect(renderQueue.buffer[1]).toBe($RENDERER_CONTAINER_TYPE); + // blendMode (normal=11) + expect(renderQueue.buffer[2]).toBe(11); + // useLayer=1 + expect(renderQueue.buffer[3]).toBe(1); + + // layerType=2 (cacheAsBitmap), cacheHit=1 + expect(renderQueue.buffer[6]).toBe(2); + expect(renderQueue.buffer[7]).toBe(1); + + // instanceId + expect(renderQueue.buffer[8]).toBe(movieClip.instanceId); + + // cache hit の場合、子要素のデータは含まれない(offsetが小さい) + // cache miss時よりもoffsetが小さいことを確認 + expect(offsetAfter - offsetBefore).toBeLessThan(30); + + // cleanup + $cacheStore.removeById(`${movieClip.instanceId}`); + movieClip.cacheAsBitmap = null; + renderQueue.buffer.fill(0); + renderQueue.offset = 0; + }); + + it("cacheAsBitmap: 子要素変更後もキャッシュが維持される", () => + { + const movieClip = new MovieClip(); + const childShape = new Shape(); + movieClip.addChild(childShape); + + movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + stage.rendererScale = 1; + + const matrix = new Float32Array([1, 0, 0, 1, 0, 0]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + const cacheKey = $cacheStore.generateKeys(1, 1, colorTransform[7]); + + // キャッシュヒット状態を作る + $cacheStore.set( + `${movieClip.instanceId}`, + "bitmapKey", cacheKey + ); + + // 子要素を変更 + movieClip.addChild(new Shape()); + + renderQueue.offset = 0; + renderQueue.buffer.fill(0); + + execute(movieClip, [], matrix, colorTransform, 800, 600); + + // layerType=2 (cacheAsBitmap), cacheHit=1 → キャッシュが維持されている + expect(renderQueue.buffer[6]).toBe(2); + expect(renderQueue.buffer[7]).toBe(1); + + // cleanup + $cacheStore.removeById(`${movieClip.instanceId}`); + movieClip.cacheAsBitmap = null; + renderQueue.buffer.fill(0); + renderQueue.offset = 0; + }); + + it("cacheAsBitmap: cache miss時にmatrix a/b/c/dがrenderScaleを使用(parentScaleを含む)", () => + { + const movieClip = new MovieClip(); + const childShape = new Shape(); + movieClip.addChild(childShape); + + movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + stage.rendererScale = 1; + + // 親matrixにscale=2を設定 + const matrix = new Float32Array([2, 0, 0, 2, 100, 50]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + $cacheStore.removeById(`${movieClip.instanceId}`); + + renderQueue.offset = 0; + renderQueue.buffer.fill(0); + + execute(movieClip, [], matrix, colorTransform, 800, 600); + + // matrix a/b/c/d はrenderScale = cacheScale * ownScale * parentScale + // ※ parentScaleにrendererScaleが含まれている($renderMatrix起点) + // cacheScale=1, ownScale=1, parentScale=2(rs=1含む) → renderScale=2 + expect(renderQueue.buffer[14]).toBe(2); // renderScaleX(parentScale=2を含む) + expect(renderQueue.buffer[15]).toBe(0); + expect(renderQueue.buffer[16]).toBe(0); + expect(renderQueue.buffer[17]).toBe(2); // renderScaleY(parentScale=2を含む) + + // cleanup + $cacheStore.removeById(`${movieClip.instanceId}`); + movieClip.cacheAsBitmap = null; + renderQueue.buffer.fill(0); + renderQueue.offset = 0; + }); + + it("cacheAsBitmap: cache hit時にもrenderScaleを使用", () => + { + const movieClip = new MovieClip(); + const childShape = new Shape(); + movieClip.addChild(childShape); + + movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + stage.rendererScale = 1; + + // 親matrixにscale=2を設定 + const matrix = new Float32Array([2, 0, 0, 2, 100, 50]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + // renderScale = cacheScale(1) * ownScale(1) * parentScale(2) * stageScale(1) = 2 + const cacheKey = $cacheStore.generateKeys(2, 2, colorTransform[7]); + $cacheStore.set( + `${movieClip.instanceId}`, + "bitmapKey", cacheKey + ); + + renderQueue.offset = 0; + renderQueue.buffer.fill(0); + + execute(movieClip, [], matrix, colorTransform, 800, 600); + + // cache hit + expect(renderQueue.buffer[7]).toBe(1); + + // matrix a/b/c/d はrenderScale=2(parentScale=2を含む) + expect(renderQueue.buffer[14]).toBe(2); + expect(renderQueue.buffer[15]).toBe(0); + expect(renderQueue.buffer[16]).toBe(0); + expect(renderQueue.buffer[17]).toBe(2); + + // cleanup + $cacheStore.removeById(`${movieClip.instanceId}`); + movieClip.cacheAsBitmap = null; + renderQueue.buffer.fill(0); + renderQueue.offset = 0; + }); + + it("cacheAsBitmap: rendererScale変更でキャッシュキーが変わる", () => + { + const movieClip = new MovieClip(); + movieClip.addChild(new Shape()); + + movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + // 実パイプラインでは$renderMatrixにrendererScaleが含まれるため + // matrixパラメータもrendererScaleを反映する必要がある + + // rendererScale=1 でmatrix=[1,0,0,1]($renderMatrixシミュレート) + stage.rendererScale = 1; + const matrix1 = new Float32Array([1, 0, 0, 1, 0, 0]); + const cacheKey1 = $cacheStore.generateKeys(1, 1, colorTransform[7]); + $cacheStore.set( + `${movieClip.instanceId}`, + "bitmapKey", cacheKey1 + ); + + // rendererScale=2 でmatrix=[2,0,0,2]($renderMatrixがscale=2に変更) + stage.rendererScale = 2; + const matrix2 = new Float32Array([2, 0, 0, 2, 0, 0]); + const cacheKey2 = $cacheStore.generateKeys(2, 2, colorTransform[7]); + expect(cacheKey1).not.toBe(cacheKey2); + + renderQueue.offset = 0; + renderQueue.buffer.fill(0); + + execute(movieClip, [], matrix2, colorTransform, 800, 600); + + // cache miss(cacheHit=0): parentScaleX=2(rendererScale含む)→ キャッシュキー変更 + expect(renderQueue.buffer[7]).toBe(0); + + // cleanup + stage.rendererScale = 1; + $cacheStore.removeById(`${movieClip.instanceId}`); + movieClip.cacheAsBitmap = null; + renderQueue.buffer.fill(0); + renderQueue.offset = 0; + }); }); \ No newline at end of file diff --git a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts index 8d536d70..4b967d03 100644 --- a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts +++ b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts @@ -22,7 +22,8 @@ import { $poolBoundsArray, $RENDERER_CONTAINER_TYPE, $getFloat32Array8, - $getFloat32Array6 + $getFloat32Array6, + $poolFloat32Array6 } from "../../DisplayObjectUtil"; import { ColorTransform, @@ -114,157 +115,388 @@ export const execute =

    ( const blendMode = display_object_container.blendMode; renderQueue.push(displayObjectBlendToNumberService(blendMode)); - // filters - const filters = display_object_container.filters; - if (filters) { - const filterKey = $cacheStore.generateFilterKeys( - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3] + // cacheAsBitmap: フィルターキャッシュ形式でコンテナ全体をビットマップキャッシュ + // キャッシュテクスチャは親のスケールを含むサイズで描画し、 + // コンポジットは setTransform(1,0,0,1,x,y) で1:1描画されるため正しい画面サイズになる + // 親の移動はキャッシュヒット(位置だけ更新)、スケール変更はキャッシュミス(再描画) + const cacheMatrix = display_object_container.cacheAsBitmap; + if (cacheMatrix) { + + const m = cacheMatrix.rawData; + const cacheScaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const cacheScaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + + const ownScaleX = rawMatrix + ? Math.sqrt(rawMatrix[0] * rawMatrix[0] + rawMatrix[1] * rawMatrix[1]) + : 1; + const ownScaleY = rawMatrix + ? Math.sqrt(rawMatrix[2] * rawMatrix[2] + rawMatrix[3] * rawMatrix[3]) + : 1; + + // 親matrixからスケール成分を抽出 + // matrixは$renderMatrix(rendererScale含む)を起点とした蓄積行列なので + // parentScaleXには既にrendererScaleが含まれている + // 親の移動(tx,ty)はキャッシュに影響しないが、スケール変更はテクスチャ再生成が必要 + const parentScaleX = matrix + ? Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1]) + : 1; + const parentScaleY = matrix + ? Math.sqrt(matrix[2] * matrix[2] + matrix[3] * matrix[3]) + : 1; + + // コンポジットスケール = cacheScale × ownScale × parentScale + // ※ parentScaleに既にrendererScaleが含まれるため追加乗算不要 + // ※ フィルターパスのtMatrix[0..3]と同等の座標系(キャンバスピクセル座標) + const renderScaleX = cacheScaleX * ownScaleX * parentScaleX; + const renderScaleY = cacheScaleY * ownScaleY * parentScaleY; + const xRounded = Math.round(renderScaleX * 100) / 100; + const yRounded = Math.round(renderScaleY * 100) / 100; + + const bitmapCacheKey = $cacheStore.generateKeys( + xRounded, yRounded, tColorTransform[7] ); - const filterCache = $cacheStore.get( + + // 固定プロパティ名で最新のキーのみ保存(古いキーが蓄積されないようにする) + // レンダラーも最新のfKeyのみ保持するため、キーの一致を保証 + const bitmapCache = $cacheStore.get( `${display_object_container.instanceId}`, - `${filterKey}` + "bitmapKey" + ) === bitmapCacheKey; + + // コンテナ自身のフィルター境界を収集 + const cacheFilters = display_object_container.filters; + const cacheFilterBounds = $getBoundsArray(0, 0, 0, 0); + if (cacheFilters) { + for (let idx = 0; idx < cacheFilters.length; idx++) { + const filter = cacheFilters[idx]; + if (!filter || !filter.canApplyFilter()) { + continue; + } + filter.getBounds(cacheFilterBounds); + } + } + + // スクリーン座標でのレイヤー位置(描画位置として使用) + const screenLayerBounds = displayObjectContainerGetLayerBoundsUseCase( + display_object_container, matrix ); - let updated = false; - const params = []; - const bounds = $getBoundsArray(0, 0, 0, 0); - for (let idx = 0; idx < filters.length; idx++) { + if (bitmapCache) { - const filter = filters[idx]; - if (!filter || !filter.canApplyFilter()) { - continue; + // cacheAsBitmap cache hit: キャッシュされたテクスチャを描画して早期return + // 子要素データは不要(レンダラーがキャッシュテクスチャを再利用) + renderQueue.push(1, + Math.ceil(Math.abs(screenLayerBounds[2] - screenLayerBounds[0])), + Math.ceil(Math.abs(screenLayerBounds[3] - screenLayerBounds[1])), + 2, 1, display_object_container.instanceId, bitmapCacheKey, + cacheFilterBounds[0], cacheFilterBounds[1], cacheFilterBounds[2], cacheFilterBounds[3], + renderScaleX, 0, 0, renderScaleY, screenLayerBounds[0], screenLayerBounds[1], + tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], + tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7] + ); + + $poolBoundsArray(screenLayerBounds); + $poolBoundsArray(cacheFilterBounds); + + if (tColorTransform !== color_transform) { + ColorTransform.release(tColorTransform); } + if (tMatrix !== matrix) { + Matrix.release(tMatrix); + } + return ; + } - // フィルターが更新されたかをチェック - if (filter.$updated) { - updated = true; + // cacheAsBitmap cache miss: 初回描画 + // 親matrixのスケール成分のみを含むローカル空間でキャッシュを生成する + // (親の位置・回転は含まず、スケールのみ反映してテクスチャサイズを画面と一致させる) + + // フィルターパラメータを収集 + const cacheFilterParams: number[] = []; + if (cacheFilters) { + for (let idx = 0; idx < cacheFilters.length; idx++) { + const filter = cacheFilters[idx]; + if (!filter || !filter.canApplyFilter()) { + continue; + } + const buffer = filter.toNumberArray(); + for (let idx = 0; idx < buffer.length; idx += 4096) { + cacheFilterParams.push(...buffer.subarray(idx, idx + 4096)); + } } - filter.$updated = false; + } - filter.getBounds(bounds); + // キャッシュ描画用の親matrix: cacheScale × parentScale(対角行列) + // parentScaleに既にrendererScaleが含まれるため追加乗算不要 + // フィルターパスのlayerBoundsと同じキャンバスピクセル座標系 + const cacheParentMatrix = $getFloat32Array6( + cacheScaleX * parentScaleX, 0, + 0, cacheScaleY * parentScaleY, + 0, 0 + ); + const localLayerBounds = displayObjectContainerGetLayerBoundsUseCase( + display_object_container, cacheParentMatrix + ); - const buffer = filter.toNumberArray(); + const localLayerWidth = Math.ceil(Math.abs(localLayerBounds[2] - localLayerBounds[0])); + const localLayerHeight = Math.ceil(Math.abs(localLayerBounds[3] - localLayerBounds[1])); + + renderQueue.push( + 1, + localLayerWidth, localLayerHeight, + 2, 0, display_object_container.instanceId, bitmapCacheKey, + cacheFilterBounds[0], cacheFilterBounds[1], cacheFilterBounds[2], cacheFilterBounds[3], + renderScaleX, 0, 0, renderScaleY, screenLayerBounds[0], screenLayerBounds[1], + tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], + tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7], + cacheFilterParams.length + ); + if (cacheFilterParams.length > 0) { + renderQueue.set(new Float32Array(cacheFilterParams)); + } - for (let idx = 0; idx < buffer.length; idx += 4096) { - params.push(...buffer.subarray(idx, idx + 4096)); - } + // 子要素の描画マトリクスをローカル空間に切り替え(親matrixの位置・回転を除外) + // cacheParentMatrixにrawMatrixを乗算し、layerBoundsオフセットで原点を調整 + let localMatrix: Float32Array; + if (rawMatrix) { + localMatrix = Matrix.multiply(cacheParentMatrix, rawMatrix); + } else { + localMatrix = $getFloat32Array6( + cacheParentMatrix[0], 0, + 0, cacheParentMatrix[3], + 0, 0 + ); } - const useFilfer = params.length > 0; - if (useFilfer) { + const localTx = localMatrix[4] - localLayerBounds[0]; + const localTy = localMatrix[5] - localLayerBounds[1]; - // 子の変更があった場合は親のフラグが立っているので更新 - if (!updated) { - updated = display_object_container.changed; - } + if (tMatrix !== matrix) { + Matrix.release(tMatrix); + } + tMatrix = $getFloat32Array6( + localMatrix[0], localMatrix[1], + localMatrix[2], localMatrix[3], + localTx, localTy + ); - const layerBounds = displayObjectContainerGetLayerBoundsUseCase( - display_object_container, matrix + if (rawMatrix) { + Matrix.release(localMatrix); + } else { + $poolFloat32Array6(localMatrix); + } + Matrix.release(cacheParentMatrix); + + if (tColorTransform !== color_transform) { + ColorTransform.release(tColorTransform); + } + tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0); + + $poolBoundsArray(localLayerBounds); + $poolBoundsArray(screenLayerBounds); + $poolBoundsArray(cacheFilterBounds); + + $cacheStore.set( + `${display_object_container.instanceId}`, + "bitmapKey", bitmapCacheKey + ); + + } else { + + // filters + const filters = display_object_container.filters; + if (filters) { + const filterKey = $cacheStore.generateFilterKeys( + tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3] + ); + const filterCache = $cacheStore.get( + `${display_object_container.instanceId}`, + `${filterKey}` ); - if (filterCache) { + let updated = false; + const params = []; + const bounds = $getBoundsArray(0, 0, 0, 0); + for (let idx = 0; idx < filters.length; idx++) { + + const filter = filters[idx]; + if (!filter || !filter.canApplyFilter()) { + continue; + } + + // フィルターが更新されたかをチェック + if (filter.$updated) { + updated = true; + } + filter.$updated = false; - // キャッシュがあって、変更がなければキャッシュを使用 + filter.getBounds(bounds); + + const buffer = filter.toNumberArray(); + + for (let idx = 0; idx < buffer.length; idx += 4096) { + params.push(...buffer.subarray(idx, idx + 4096)); + } + } + + const useFilfer = params.length > 0; + if (useFilfer) { + + // 子の変更があった場合は親のフラグが立っているので更新 if (!updated) { - renderQueue.push(1, - Math.ceil(Math.abs(layerBounds[2] - layerBounds[0])), - Math.ceil(Math.abs(layerBounds[3] - layerBounds[1])), - 1, 1, display_object_container.instanceId, filterKey, - bounds[0], bounds[1], bounds[2], bounds[3], - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerBounds[0], layerBounds[1], - tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], - tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7] - ); + updated = display_object_container.changed; + } - $poolBoundsArray(layerBounds); - $poolBoundsArray(bounds); + const layerBounds = displayObjectContainerGetLayerBoundsUseCase( + display_object_container, matrix + ); - if (tColorTransform !== color_transform) { - ColorTransform.release(tColorTransform); + if (filterCache) { + + // キャッシュがあって、変更がなければキャッシュを使用 + if (!updated) { + renderQueue.push(1, + Math.ceil(Math.abs(layerBounds[2] - layerBounds[0])), + Math.ceil(Math.abs(layerBounds[3] - layerBounds[1])), + 1, 1, display_object_container.instanceId, filterKey, + bounds[0], bounds[1], bounds[2], bounds[3], + tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerBounds[0], layerBounds[1], + tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], + tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7] + ); + + $poolBoundsArray(layerBounds); + $poolBoundsArray(bounds); + + if (tColorTransform !== color_transform) { + ColorTransform.release(tColorTransform); + } + if (tMatrix !== matrix) { + Matrix.release(tMatrix); + } + return ; } - if (tMatrix !== matrix) { - Matrix.release(tMatrix); - } - return ; + + // どこかで変更があったので、キャッシュを削除 + $cacheStore.removeById(`${display_object_container.instanceId}`); } - // どこかで変更があったので、キャッシュを削除 - $cacheStore.removeById(`${display_object_container.instanceId}`); - } + renderQueue.push( + 1, + Math.ceil(Math.abs(layerBounds[2] - layerBounds[0])), + Math.ceil(Math.abs(layerBounds[3] - layerBounds[1])), + 1, 0, display_object_container.instanceId, filterKey, + bounds[0], bounds[1], bounds[2], bounds[3], + tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerBounds[0], layerBounds[1], + tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], + tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7], + params.length + ); + renderQueue.set(new Float32Array(params)); - renderQueue.push( - 1, - Math.ceil(Math.abs(layerBounds[2] - layerBounds[0])), - Math.ceil(Math.abs(layerBounds[3] - layerBounds[1])), - 1, 0, display_object_container.instanceId, filterKey, - bounds[0], bounds[1], bounds[2], bounds[3], - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerBounds[0], layerBounds[1], - tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], - tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7], - params.length - ); - renderQueue.set(new Float32Array(params)); + const fa0 = tMatrix[0]; + const fa1 = tMatrix[1]; + const fa2 = tMatrix[2]; + const fa3 = tMatrix[3]; + const faTx = tMatrix[4] - layerBounds[0]; + const faTy = tMatrix[5] - layerBounds[1]; - const fa0 = tMatrix[0]; - const fa1 = tMatrix[1]; - const fa2 = tMatrix[2]; - const fa3 = tMatrix[3]; - const faTx = tMatrix[4] - layerBounds[0]; - const faTy = tMatrix[5] - layerBounds[1]; + if (tMatrix !== matrix) { + Matrix.release(tMatrix); + } + tMatrix = $getFloat32Array6(fa0, fa1, fa2, fa3, faTx, faTy); - if (tMatrix !== matrix) { - Matrix.release(tMatrix); - } - tMatrix = $getFloat32Array6(fa0, fa1, fa2, fa3, faTx, faTy); + if (tColorTransform !== color_transform) { + ColorTransform.release(tColorTransform); + } - if (tColorTransform !== color_transform) { - ColorTransform.release(tColorTransform); - } + tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0); - tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0); + $poolBoundsArray(layerBounds); - $poolBoundsArray(layerBounds); + $cacheStore.set( + `${display_object_container.instanceId}`, + `${filterKey}`, true + ); - $cacheStore.set( - `${display_object_container.instanceId}`, - `${filterKey}`, true - ); + } else { + if (blendMode === "normal") { + renderQueue.push(0); + } else { + + // ブレンドモードのみのLayerモード + const layerBounds = displayObjectContainerCalcBoundsMatrixUseCase( + display_object_container, + matrix + ); + + const layerXMin = layerBounds[0]; + const layerYMin = layerBounds[1]; + + renderQueue.push( + 1, + Math.ceil(Math.abs(layerBounds[2] - layerXMin)), + Math.ceil(Math.abs(layerBounds[3] - layerYMin)), + 0, // not use filter, + tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerXMin, layerYMin, + tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], + tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7] + ); + const a0 = tMatrix[0]; + const a1 = tMatrix[1]; + const a2 = tMatrix[2]; + const a3 = tMatrix[3]; + const adjustedTx1 = tMatrix[4] - layerXMin; + const adjustedTy1 = tMatrix[5] - layerYMin; + + if (tMatrix !== matrix) { + Matrix.release(tMatrix); + } + tMatrix = $getFloat32Array6(a0, a1, a2, a3, adjustedTx1, adjustedTy1); + $poolBoundsArray(layerBounds); + + if (tColorTransform !== color_transform) { + ColorTransform.release(tColorTransform); + } + tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0); + } + } + + $poolBoundsArray(bounds); } else { if (blendMode === "normal") { renderQueue.push(0); } else { - - // ブレンドモードのみのLayerモード const layerBounds = displayObjectContainerCalcBoundsMatrixUseCase( display_object_container, matrix ); - const layerXMin = layerBounds[0]; - const layerYMin = layerBounds[1]; + const layerXMin2 = layerBounds[0]; + const layerYMin2 = layerBounds[1]; renderQueue.push( 1, - Math.ceil(Math.abs(layerBounds[2] - layerXMin)), - Math.ceil(Math.abs(layerBounds[3] - layerYMin)), + Math.ceil(Math.abs(layerBounds[2] - layerXMin2)), + Math.ceil(Math.abs(layerBounds[3] - layerYMin2)), 0, // not use filter, - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerXMin, layerYMin, + tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerXMin2, layerYMin2, tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7] ); - const a0 = tMatrix[0]; - const a1 = tMatrix[1]; - const a2 = tMatrix[2]; - const a3 = tMatrix[3]; - const adjustedTx1 = tMatrix[4] - layerXMin; - const adjustedTy1 = tMatrix[5] - layerYMin; + const b0 = tMatrix[0]; + const b1 = tMatrix[1]; + const b2 = tMatrix[2]; + const b3 = tMatrix[3]; + const adjustedTx2 = tMatrix[4] - layerXMin2; + const adjustedTy2 = tMatrix[5] - layerYMin2; if (tMatrix !== matrix) { Matrix.release(tMatrix); } - tMatrix = $getFloat32Array6(a0, a1, a2, a3, adjustedTx1, adjustedTy1); + tMatrix = $getFloat32Array6(b0, b1, b2, b3, adjustedTx2, adjustedTy2); $poolBoundsArray(layerBounds); if (tColorTransform !== color_transform) { @@ -274,48 +506,7 @@ export const execute =

    ( } } - $poolBoundsArray(bounds); - } else { - if (blendMode === "normal") { - renderQueue.push(0); - } else { - const layerBounds = displayObjectContainerCalcBoundsMatrixUseCase( - display_object_container, - matrix - ); - - const layerXMin2 = layerBounds[0]; - const layerYMin2 = layerBounds[1]; - - renderQueue.push( - 1, - Math.ceil(Math.abs(layerBounds[2] - layerXMin2)), - Math.ceil(Math.abs(layerBounds[3] - layerYMin2)), - 0, // not use filter, - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerXMin2, layerYMin2, - tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], - tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7] - ); - - const b0 = tMatrix[0]; - const b1 = tMatrix[1]; - const b2 = tMatrix[2]; - const b3 = tMatrix[3]; - const adjustedTx2 = tMatrix[4] - layerXMin2; - const adjustedTy2 = tMatrix[5] - layerYMin2; - - if (tMatrix !== matrix) { - Matrix.release(tMatrix); - } - tMatrix = $getFloat32Array6(b0, b1, b2, b3, adjustedTx2, adjustedTy2); - $poolBoundsArray(layerBounds); - - if (tColorTransform !== color_transform) { - ColorTransform.release(tColorTransform); - } - tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0); - } - } + } // end cacheAsBitmap else // mask const maskDisplayObject = display_object_container.mask; diff --git a/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.test.ts b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.test.ts index c379b666..6f7ac6c1 100644 --- a/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.test.ts +++ b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.test.ts @@ -134,4 +134,93 @@ describe("DisplayObjectContainerRenderUseCase.js test", () => { expect($context.containerDrawCachedFilter).toHaveBeenCalledTimes(1); expect(result).toBe(data.length); }); + + it("execute test case4 - cacheAsBitmap cache hit returns early", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.containerDrawCachedFilter).mockClear(); + vi.mocked($context.containerBeginLayer).mockClear(); + vi.mocked($context.containerEndLayer).mockClear(); + + const data: number[] = []; + // blendMode + data.push(0); + // useLayer = true + data.push(1); + // layerWidth, layerHeight + data.push(100, 80); + // layerType = 2 (cacheAsBitmap) + data.push(2); + // cacheHit = true + data.push(1); + // uniqueKey (instanceId) + data.push(10); + // cacheKey + data.push(500); + // filterBounds (4) + data.push(0, 0, 0, 0); + // matrix (6) + data.push(1, 0, 0, 1, 30, 40); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + // cache hit → containerDrawCachedFilter で描画し即return + expect($context.containerDrawCachedFilter).toHaveBeenCalledTimes(1); + // containerBeginLayer/EndLayerは呼ばれない(子要素の描画不要) + expect($context.containerBeginLayer).not.toHaveBeenCalled(); + expect($context.containerEndLayer).not.toHaveBeenCalled(); + expect(result).toBe(data.length); + }); + + it("execute test case5 - cacheAsBitmap cache miss processes children and caches", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.containerBeginLayer).mockClear(); + vi.mocked($context.containerEndLayer).mockClear(); + vi.mocked($context.containerDrawCachedFilter).mockClear(); + + const data: number[] = []; + // blendMode + data.push(0); + // useLayer = true + data.push(1); + // layerWidth, layerHeight + data.push(100, 80); + // layerType = 2 (cacheAsBitmap) + data.push(2); + // cacheHit = false + data.push(0); + // uniqueKey (instanceId) + data.push(10); + // cacheKey + data.push(500); + // filterBounds (4) + data.push(0, 0, 0, 0); + // matrix (6) + data.push(1, 0, 0, 1, 30, 40); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // paramsLength = 0 (no filter params) + data.push(0); + // useMaskDisplayObject = false + data.push(0); + // children length = 0 + data.push(0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + // cache miss → containerBeginLayer → 子要素描画 → containerEndLayer + expect($context.containerBeginLayer).toHaveBeenCalledWith(100, 80); + expect($context.containerEndLayer).toHaveBeenCalledTimes(1); + // containerEndLayer は use_filter=true, 空のfilterBounds/Params でキャッシュ + const endLayerCall = vi.mocked($context.containerEndLayer).mock.calls[0]; + expect(endLayerCall[3]).toBe(true); // use_filter + // containerDrawCachedFilterは呼ばれない(初回描画) + expect($context.containerDrawCachedFilter).not.toHaveBeenCalled(); + expect(result).toBe(data.length); + }); }); diff --git a/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.ts b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.ts index 04e29959..3adf25d1 100644 --- a/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.ts +++ b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.ts @@ -6,6 +6,15 @@ import { execute as videoRenderUseCase } from "../../Video/usecase/VideoRenderUs import { execute as displayObjectContainerClipRenderUseCase } from "./DisplayObjectContainerClipRenderUseCase"; import { execute as displayObjectGetBlendModeService } from "../../DisplayObject/service/DisplayObjectGetBlendModeService"; +/** + * @description cacheAsBitmap用の空フィルターパラメータ + * Empty filter params for cacheAsBitmap (no filter processing) + * + * @type {Float32Array} + * @private + */ +const $emptyFilterParams: Float32Array = new Float32Array(0); + /** * @description DisplayObjectContainerの描画を実行します。 * Execute the drawing of DisplayObjectContainer. @@ -35,6 +44,7 @@ export const execute = ( let layerHeight = 0; let useFilter = false; + let useCacheAsBitmap = false; let uniqueKey = ""; let filterKey = ""; let filterBounds: Float32Array | null = null; @@ -46,9 +56,46 @@ export const execute = ( layerWidth = render_queue[index++]; layerHeight = render_queue[index++]; - useFilter = Boolean(render_queue[index++]); + const layerType = render_queue[index++]; // 0=blend, 1=filter, 2=cacheAsBitmap + useFilter = layerType === 1; + useCacheAsBitmap = layerType === 2; + + if (useCacheAsBitmap) { + + // cacheAsBitmapパス + const cacheHit = Boolean(render_queue[index++]); + uniqueKey = `${render_queue[index++]}`; + filterKey = `${render_queue[index++]}`; + + // フィルター境界を読み取り + filterBounds = render_queue.subarray(index, index + 4); + index += 4; + + matrix = render_queue.subarray(index, index + 6); + index += 6; - if (useFilter) { + colorTransform = render_queue.subarray(index, index + 8); + index += 8; + + if (cacheHit) { + // キャッシュ済み: テクスチャを描画して子要素の処理をスキップ + $context.containerDrawCachedFilter( + blendMode, matrix, colorTransform, + filterBounds, uniqueKey, filterKey + ); + return index; + } + + // 初回描画: フィルターパラメータを読み取り + const paramsLength = render_queue[index++]; + filterParams = paramsLength > 0 + ? render_queue.subarray(index, index + paramsLength) + : $emptyFilterParams; + index += paramsLength; + + // containerBeginLayer → 子要素描画 → containerEndLayerでキャッシュ + + } else if (useFilter) { // フィルターパス: filterCache/uniqueKey/filterKey を読む const filterCache = Boolean(render_queue[index++]); uniqueKey = `${render_queue[index++]}`; @@ -245,11 +292,20 @@ export const execute = ( // コンテナのフィルター/ブレンド結果をメインに合成 if (useLayer) { - $context.containerEndLayer( - blendMode, matrix!, colorTransform, - useFilter, filterBounds, filterParams, - uniqueKey, filterKey - ); + if (useCacheAsBitmap) { + // cacheAsBitmap: フィルター適用後のテクスチャをキャッシュ・描画 + $context.containerEndLayer( + blendMode, matrix!, colorTransform, + true, filterBounds, filterParams, + uniqueKey, filterKey + ); + } else { + $context.containerEndLayer( + blendMode, matrix!, colorTransform, + useFilter, filterBounds, filterParams, + uniqueKey, filterKey + ); + } } return index; From 63725ea90df7db8541e664ac3c0ac7877eaf4d90 Mon Sep 17 00:00:00 2001 From: ienaga Date: Mon, 30 Mar 2026 08:08:20 +0900 Subject: [PATCH 26/26] =?UTF-8?q?#269=20=E3=83=89=E3=82=AD=E3=83=A5?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=88=E3=82=92=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/cn/display-object.md | 43 ++++++++++++++++++++++++++++++++++++-- specs/cn/shape.md | 3 ++- specs/en/display-object.md | 43 ++++++++++++++++++++++++++++++++++++-- specs/en/shape.md | 3 ++- specs/ja/display-object.md | 43 ++++++++++++++++++++++++++++++++++++-- specs/ja/shape.md | 3 ++- 6 files changed, 129 insertions(+), 9 deletions(-) diff --git a/specs/cn/display-object.md b/specs/cn/display-object.md index 86ed2f7b..efec3f6f 100644 --- a/specs/cn/display-object.md +++ b/specs/cn/display-object.md @@ -44,7 +44,7 @@ DisplayObject 是 Next2D Player 中所有显示对象的基类。 | `scaleX` | number | 从参考点应用的对象水平缩放值 | | `scaleY` | number | 从参考点应用的对象垂直缩放值 | | `visible` | boolean | 显示对象是否可见(默认:true) | -| `cacheAsBitmap` | Matrix \| null | 位图缓存用的 Matrix。以 1.0 为基准,应用于 displayObject 自身的 scaleX/scaleY。缓存质量 = 指定 Matrix × 自身缩放 × 舞台缩放。缓存时不受祖先 Matrix 影响,但绘制时应用祖先 Matrix。命中测试、宽度和高度基于矢量(默认:null) | +| `cacheAsBitmap` | Matrix \| null | 位图缓存用的 Matrix。**仅可设置缩放值(a, d)**(b, c, tx, ty 将被忽略)。以 1.0 为基准,应用于 displayObject 自身的 scaleX/scaleY。缓存质量 = 指定 Matrix × 自身缩放 × 舞台缩放。缓存时不受祖先 Matrix 影响,但绘制时应用祖先 Matrix。命中测试、宽度和高度基于矢量。**适用对象:Shape、TextField、Sprite、MovieClip**(Video 因图像尺寸固定而不适用)(默认:null) | | `x` | number | 相对于父 DisplayObjectContainer 本地坐标的 X 坐标 | | `y` | number | 相对于父 DisplayObjectContainer 本地坐标的 Y 坐标 | @@ -161,6 +161,34 @@ displayObject.clearGlobalVariable(); // 清除全部 ### cacheAsBitmap 示例 +`cacheAsBitmap` 将矢量绘制或容器缓存为位图,从第二帧开始重复使用缓存纹理,从而提高性能。 + +**适用类:** +- `Shape` — 缓存矢量绘制 +- `TextField` — 缓存文本渲染 +- `Sprite` — 将容器及其所有子元素一起缓存 +- `MovieClip` — 将容器及其所有子元素一起缓存 + +> ⚠️ 不适用于 `Video`。由于 Video 具有固定的图像尺寸,缓存没有效果。 + +**Matrix 限制:** +Matrix 中仅可设置缩放值(a, d)。旋转(b, c)和平移(tx, ty)将被忽略。 + +```typescript +// ✅ 正确用法(仅设置缩放) +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); // 1 倍 +shape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); // 2 倍质量 + +// ❌ 旋转/平移值将被忽略 +shape.cacheAsBitmap = new Matrix(1, 0.5, 0.5, 1, 100, 200); // b, c, tx, ty 被忽略 +``` + +**使用场景:** + +1. **高速动画** — 高速移动的对象视觉清晰度低,缓存导致的画质降低几乎不可察觉,同时能显著提高性能 +2. **静态背景和 UI 元素** — 缓存不变的 UI 元素(面板、装饰、图标等)可消除每帧重绘成本 +3. **复杂矢量绘制** — 缓存路径较多的复杂 Shape 可大幅降低绘制开销 + ```typescript const { Shape, Sprite } = next2d.display; const { Matrix } = next2d.geom; @@ -198,7 +226,7 @@ shape.cacheAsBitmap = null; 容器的所有子元素将被渲染到单个纹理中并缓存,在后续帧中重复使用。 ```typescript -const { Shape, Sprite } = next2d.display; +const { Shape, Sprite, MovieClip } = next2d.display; const { Matrix } = next2d.geom; // 包含多个子元素的 Sprite @@ -213,11 +241,22 @@ sprite.addChild(circle); // 将整个容器缓存为位图 sprite.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); +// 同样适用于 MovieClip +const mc = new MovieClip(); +mc.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + // 禁用缓存(下一帧恢复正常渲染) sprite.cacheAsBitmap = null; ``` +**缓存行为:** +- 当对象或其祖先的缩放发生变化时,缓存将失效并在下一帧重新生成 +- 位置更改(x, y)会保留缓存 — 仅更新绘制位置 +- 在缩放频繁变化的动画期间,缓存每帧都会重新生成,因此缓存在动画完成后的静态状态下最为有效 + **注意事项:** +- **Matrix 中仅可设置缩放值**(旋转和平移将被忽略) +- **不适用于 Video**(固定尺寸的图像数据) - 缓存期间,子元素的更改(添加/删除/属性更改)不会反映在屏幕上 - 当 `stage.rendererScale` 更改时,缓存会自动失效 - 同时设置 `filter` 和 `cacheAsBitmap` 时,`cacheAsBitmap` 优先 diff --git a/specs/cn/shape.md b/specs/cn/shape.md index f9150f79..1d90fc61 100644 --- a/specs/cn/shape.md +++ b/specs/cn/shape.md @@ -231,7 +231,8 @@ stage.addChild(frontShape); ```javascript // 将复杂形状缓存为位图 -shape.cacheAsBitmap = true; +const { Matrix } = next2d.geom; +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); ``` ## Graphics 类 diff --git a/specs/en/display-object.md b/specs/en/display-object.md index 5c2d8c40..6c9157f8 100644 --- a/specs/en/display-object.md +++ b/specs/en/display-object.md @@ -44,7 +44,7 @@ DisplayObject is the base class for all display objects in Next2D Player. | `scaleX` | number | Horizontal scale value of the object applied from the reference point | | `scaleY` | number | Vertical scale value of the object applied from the reference point | | `visible` | boolean | Whether the display object is visible (default: true) | -| `cacheAsBitmap` | Matrix \| null | Matrix for bitmap caching. 1.0-based, applied relative to the displayObject's own scaleX/scaleY. Cache quality = specified Matrix × own scale × stage scale. Independent of ancestor transforms for caching, but ancestor transforms are applied when drawing. Hit tests, width, and height remain vector-based (default: null) | +| `cacheAsBitmap` | Matrix \| null | Matrix for bitmap caching. **Only scale values (a, d) can be set** (b, c, tx, ty are ignored). 1.0-based, applied relative to the displayObject's own scaleX/scaleY. Cache quality = specified Matrix × own scale × stage scale. Independent of ancestor transforms for caching, but ancestor transforms are applied when drawing. Hit tests, width, and height remain vector-based. **Applicable to: Shape, TextField, Sprite, MovieClip** (not applicable to Video as it has fixed image dimensions) (default: null) | | `x` | number | X coordinate relative to the local coordinates of the parent DisplayObjectContainer | | `y` | number | Y coordinate relative to the local coordinates of the parent DisplayObjectContainer | @@ -161,6 +161,34 @@ displayObject.clearGlobalVariable(); // Clear all ### cacheAsBitmap Example +`cacheAsBitmap` caches vector drawings or containers as bitmaps, improving performance by reusing the cached texture from the second frame onward. + +**Applicable classes:** +- `Shape` — Cache vector drawings +- `TextField` — Cache text rendering +- `Sprite` — Cache containers and all their children together +- `MovieClip` — Cache containers and all their children together + +> ⚠️ Not applicable to `Video`. Since Video has fixed image dimensions, caching provides no benefit. + +**Matrix restriction:** +Only scale values (a, d) can be set in the Matrix. Rotation (b, c) and translation (tx, ty) are ignored. + +```typescript +// ✅ Correct usage (scale only) +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); // 1x scale +shape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); // 2x quality + +// ❌ Rotation/translation values are ignored +shape.cacheAsBitmap = new Matrix(1, 0.5, 0.5, 1, 100, 200); // b, c, tx, ty ignored +``` + +**Use cases:** + +1. **Fast-moving animations** — Objects moving at high speed have low visual clarity, so quality loss from caching is barely noticeable while providing significant performance gains +2. **Static backgrounds and UI elements** — Caching unchanging UI elements (panels, decorations, icons) eliminates per-frame redraw costs +3. **Complex vector drawings** — Caching Shapes with many paths dramatically reduces drawing overhead + ```typescript const { Shape, Sprite } = next2d.display; const { Matrix } = next2d.geom; @@ -198,7 +226,7 @@ You can also set `cacheAsBitmap` on `DisplayObjectContainer` subclasses such as All child elements are rendered into a single texture and cached, reused on subsequent frames. ```typescript -const { Shape, Sprite } = next2d.display; +const { Shape, Sprite, MovieClip } = next2d.display; const { Matrix } = next2d.geom; // Sprite with multiple children @@ -213,11 +241,22 @@ sprite.addChild(circle); // Cache the entire container as a bitmap sprite.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); +// Also applicable to MovieClip +const mc = new MovieClip(); +mc.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + // Disable caching (resumes normal rendering next frame) sprite.cacheAsBitmap = null; ``` +**Cache behavior:** +- When the scale of the object or its ancestors changes, the cache is invalidated and regenerated on the next frame +- Position changes (x, y) preserve the cache — only the drawing position is updated +- During animations with frequent scale changes, the cache is regenerated every frame, so caching is most effective after animations complete and the object is static + **Notes:** +- **Only scale values can be set in the Matrix** (rotation and translation are ignored) +- **Not applicable to Video** (fixed-size image data) - While cached, changes to children (add/remove/property changes) are not reflected on screen - Cache is automatically invalidated when `stage.rendererScale` changes - When both `filter` and `cacheAsBitmap` are set, `cacheAsBitmap` takes priority diff --git a/specs/en/shape.md b/specs/en/shape.md index 0e25d3fd..bc33819f 100644 --- a/specs/en/shape.md +++ b/specs/en/shape.md @@ -231,7 +231,8 @@ stage.addChild(frontShape); ```javascript // Cache complex shapes as bitmap -shape.cacheAsBitmap = true; +const { Matrix } = next2d.geom; +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); ``` ## Graphics Class diff --git a/specs/ja/display-object.md b/specs/ja/display-object.md index 55ec7fbe..1d1c38cf 100644 --- a/specs/ja/display-object.md +++ b/specs/ja/display-object.md @@ -44,7 +44,7 @@ DisplayObjectは、Next2D Playerにおける全ての表示オブジェクトの | `scaleX` | number | 基準点から適用されるオブジェクトの水平スケール値 | | `scaleY` | number | 基準点から適用されるオブジェクトの垂直スケール値 | | `visible` | boolean | 表示オブジェクトが可視かどうか(デフォルト: true) | -| `cacheAsBitmap` | Matrix \| null | ビットマップキャッシュ用のMatrix。1.0基準でdisplayObjectのscaleX/scaleYに適用される。キャッシュ品質 = 指定Matrix × 自身のスケール × stageスケール。先祖のMatrixの影響は受けないが、描画時には先祖のMatrixが適用される。ヒットテスト・幅・高さはベクター基準(デフォルト: null) | +| `cacheAsBitmap` | Matrix \| null | ビットマップキャッシュ用のMatrix。**スケール値(a, d)のみ設定可能**(b, c, tx, tyは無視されます)。1.0基準でdisplayObjectのscaleX/scaleYに適用される。キャッシュ品質 = 指定Matrix × 自身のスケール × stageスケール。先祖のMatrixの影響は受けないが、描画時には先祖のMatrixが適用される。ヒットテスト・幅・高さはベクター基準。**適用対象: Shape, TextField, Sprite, MovieClip**(Videoは固定サイズのため適用不可)(デフォルト: null) | | `x` | number | 親DisplayObjectContainerのローカル座標を基準にしたX座標 | | `y` | number | 親DisplayObjectContainerのローカル座標を基準にしたY座標 | @@ -161,6 +161,34 @@ displayObject.clearGlobalVariable(); // 全てクリア ### cacheAsBitmapの例 +`cacheAsBitmap`はベクター描画やコンテナをビットマップとしてキャッシュし、2回目以降の描画でキャッシュを再利用することでパフォーマンスを向上させるプロパティです。 + +**適用対象クラス:** +- `Shape` — ベクター描画のキャッシュ +- `TextField` — テキスト描画のキャッシュ +- `Sprite` — コンテナとその子要素をまとめてキャッシュ +- `MovieClip` — コンテナとその子要素をまとめてキャッシュ + +> ⚠️ `Video`には適用できません。Videoは画像サイズが固定されているため、キャッシュの効果がありません。 + +**Matrixの制限:** +Matrixにはスケール値(a, d)のみ設定できます。回転(b, c)や移動(tx, ty)は無視されます。 + +```typescript +// ✅ 正しい使い方(スケールのみ設定) +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); // 等倍 +shape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); // 2倍品質 + +// ❌ 回転・移動の値は無視される +shape.cacheAsBitmap = new Matrix(1, 0.5, 0.5, 1, 100, 200); // b, c, tx, tyは無視 +``` + +**利用用途の例:** + +1. **速度の速いアニメーション** — 高速で動くオブジェクトは視認性が低いため、キャッシュによる画質低下が目立たず、パフォーマンス向上が期待できます +2. **静的な背景やUI部品** — 変化しないUI要素(パネル、装飾、アイコンなど)をキャッシュすることで毎フレームの再描画コストを削減できます +3. **複雑なベクター描画** — パスが多い複雑なShapeをキャッシュすることで描画負荷を大幅に軽減できます + ```typescript const { Shape, Sprite } = next2d.display; const { Matrix } = next2d.geom; @@ -198,7 +226,7 @@ shape.cacheAsBitmap = null; コンテナの全子要素をまとめてテクスチャにキャッシュし、以降のフレームではキャッシュされたテクスチャを再利用します。 ```typescript -const { Shape, Sprite } = next2d.display; +const { Shape, Sprite, MovieClip } = next2d.display; const { Matrix } = next2d.geom; // 複数の子要素を持つSprite @@ -213,11 +241,22 @@ sprite.addChild(circle); // コンテナ全体をビットマップキャッシュ sprite.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); +// MovieClipにも同様に適用可能 +const mc = new MovieClip(); +mc.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + // キャッシュを解除(次フレームから通常描画に戻る) sprite.cacheAsBitmap = null; ``` +**キャッシュの動作:** +- 先祖や自身のスケールが変更されるとキャッシュが無効化され、次フレームで再生成されます +- 位置(x, y)の変更ではキャッシュは維持され、描画位置のみ更新されます +- スケールが頻繁に変化するアニメーション中はキャッシュが毎フレーム再生成されるため、アニメーション完了後の静的な状態で最も効果を発揮します + **注意事項:** +- **Matrixにはスケール値のみ設定可能**です(回転・移動は無視されます) +- **Videoには適用できません**(固定サイズの画像データのため) - キャッシュ中は子要素の変更(追加・削除・プロパティ変更)が画面に反映されません - `stage.rendererScale`が変更されるとキャッシュが自動的に無効化されます - `filter`と`cacheAsBitmap`を同時に設定した場合、`cacheAsBitmap`が優先されます diff --git a/specs/ja/shape.md b/specs/ja/shape.md index d3d46a17..68aa12c5 100644 --- a/specs/ja/shape.md +++ b/specs/ja/shape.md @@ -231,7 +231,8 @@ stage.addChild(frontShape); ```typescript // 複雑な図形をビットマップとしてキャッシュ -shape.cacheAsBitmap = true; +const { Matrix } = next2d.geom; +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); ``` ## Graphics クラス