From 3d217aba7b4c31e6e0d191b886b085644faef60f Mon Sep 17 00:00:00 2001 From: longyi-xw <2691049525@qq.com> Date: Tue, 16 Jun 2026 11:49:32 +0800 Subject: [PATCH 01/14] =?UTF-8?q?docs(spec):=20v1.0=20B3b=20=E2=80=94=20Ba?= =?UTF-8?q?bylon=20gizmo=20+=20snap=20design?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- design/screenshots/img_1.png | Bin 0 -> 116697 bytes ...2026-06-16-v1.0b3b-babylon-gizmo-design.md | 137 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 design/screenshots/img_1.png create mode 100644 docs/superpowers/specs/2026-06-16-v1.0b3b-babylon-gizmo-design.md diff --git a/design/screenshots/img_1.png b/design/screenshots/img_1.png new file mode 100644 index 0000000000000000000000000000000000000000..13c5024649dee9017607afd2555e0a2c5d6d63aa GIT binary patch literal 116697 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%0D|iH6?lc5VK%ttY*X`mnF@j+A0a)nD>bd~p34 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

cKm z<7Xz%EajZ^@%PW;SMEpdg+iyH-#9tMB|le`EOxg%Vzi~dDjVp5F!jnjeODfzVE@vJ zJ3m2Sk?wLMxG<|i{(Q8@_sW%Tg;~wd^JaGZ|9Inu^cI$tb*1sy4oysiv|LQLjGi|i zVs{>5RNhQ;;tV|wf-&n}+_JR96*9MOx#Fd1E0^CNzGVSbbb-#uxuuTn(Dn&WiQwpKQOiE=;f6|GVk4L{cLW|U+@;eYV75)xaMFeydyBAYy z7z;q|H$)i>2hL@fr@OXP{`nM?H{teElpibhzBh;|6A@=P>o9etF{$X~|?}(?mu5KxP{x`GVal?Feaq;as z$N91I<_koN2@sTj`#ac{Ujjl|WS^O{U4~NBcdj?dZo@)^Y<(sKtB*@P>Le-WVMHkwUB`*-XJ7507;n2Ibe?}g>DAe>5i;y-5s6*8vH8(klc0}|?x z`@3f(Qh2*RcLNcR^6c6o6j_~o4+q>|aoMvzT&qdnv)3uSd~=sTv7=vx>ZL>hIiPoR zDza~bvms^uU5cX!p}~LKTh%~r={FzSY1%ken_e4myRasK*cihA9w54+35J;THM_hq z@FV-2epkO0akPk-nvu-WC$>Zz7&D(27FU-Bi&&la>2FO{X2?sLDR;n@qcL&5gDN6zI zFAcb8`m|kt{<-{30ZtW|BS@L-8kt{y4yIlI!kNUWzHK1t;qyBzHd>JQvoCSj+;Y;l z1LA1z6kMt59nY-<1P8Xd0Kq4u6=#)JUDao5PArqyTba^G2`YES*<(2zw0e@%swg$F zoF+$eW!cup!B!IoHx;xDT9;)CVy1?Q#MafSiDoSrHA6pE-IA?lXolwzeAOxb!B_j0 z-8HhO=4^O{_mM&zoN_dzzfjuDtKLZzvQpXu6kxQ}yWL_NI@>_YXY?1KVWqnx?}m+E zx{5S#7Cnl0)?xJ)bH(;`rx_GTpx!f}x%7MW2V25~`UFg^&uL4ht&i$O-t66Ax0-wh zsCTS8%ejHYjQ`f8b_Mq54QlayZc-ivzNTX!y;}-C6IE)c6trRj`W9DRJ6XkE%7|D=`_ zr{7EnyrsDClBdre~=5i=;=L*PbF(Tr)3?@u$h2h4rLO zNkXrRA@X8Xy0e3vqejOBGD50Nd7q1@LT`1-42qJK)>nt|$s{`*=f>Zgy;n>Jqi-dH zDZ*ToB-h5`OF3n_{(h}ptHXhagl|Ee%T_I4IzBh>no|%zqfI}~lsxPwPje$l<#*Y= zNptpb`hi5DOl9IIXXme13ln}nuRdI@wGp!a-k&IJz|aEEXI_`?S2ijiI1ypcRy4oe zn?B^d!l9Ht9TYTKT*HY_dY9OR#}j3`vx|9Bl9B7XElnEsAfXb>;DC9ZzTjvhXZ?99 z`aAY32|Zq)<1nqc>tO5hZ_C$ZmSq^Pb9_gmK5WzY@v>8$#5pUdSp1^yeI`)2etD&{ zGlAg^^7wRzsC6LWbcrOM7fR+fO1Q7No!-;X9H+zj?xN2BT!16=0%*(M8(r=tpLB5g zefzgUAf|v|a8HL)%J{fC{s+LVK$e9of!DIWi;USQ5sDPXlUjzA;5Gi9q~+ZJeKC>i z)f5PM6gvRTxFyN;)WXPniR2ax;;zuRm_$>uWQmPs5dZLQ%MJ!a{z^=CF$yE$WtE4?pE|sIxxm z+By{dof5hMGA7@@c57xssQ#+VEOYhc*zcgTIe*uRrL#YuNQ0QMdv+wapsx+8Pf+&R z4F0~0KjQVrt+AQ=DSQQ#WRKC+8nu9nPjr^}b2>N- zWVwz$OH^z-{El-G6owv>J4-|OQaAsu-Y&SSYP|*^SL@`t@o|Wxf*F%NOA}zB5{uuA zs7;pWA!cmU9O=hxNfMbx3_cWTaSB)c;FG!qR5k;-KSov19p20vXZe81Yv|>wVxa*M zC?)Ny3EdDz7ek_q5RUVK9E&jpayQh>$#BUCE1~oc#Ft+Wqa@BB*3vb;{Zh@ELL3{F z&qOmZ-5YdRc;V$h&P`xba||0ju(-oR%lSl*azCxk7znb@-unU7 zBM?QkukSjAsH8;zq4b-jNXNx9%=}Bw7p@7($+`9Ql(#7$?Kl^R@tr?AatD23|B*LkDv0(0 zNh?!ofYOK?!*(URlO@YGb8%z15-ZmY1HF1ub68pe8!Czs>{3`Z->@rbYeoR1fliWz zRak%MkT3!-wEPAFi9@c$GpXnUV#LAKU!x)mycVjW2XC~2e}LX-qP$`G6ykyI&`)6=x~^fG~=9B zoL{!j<$Z1?-1{_^?wRNhpA6${FaI|h%(CZ#5UHDDXQ|wZhQZOQyz*!aYKv#U-JjU) z$a0qK+%I-dzjmh!=Ya)pATU`iXLEnvryxbID@-WlpZ5=1zZPO7qEtv#kQ%KwqV&4K{qMzn{Nj>x2{)Z zA$I64GyBB+KYCxjJT@425m~z!bKMADaq-q|X*tKE#9ON5HVQ_HhB>5HCuDcic}P1%Wf7$UE9cR+2N zSj81o&q;ozcnqHP{Vfu=rM3|ggkAv2(xOrYHn`jWdiPt(4Qm!MvvWK(-L?8=(K7s4 z-L!HletrOILG|D4Aw7SRXcbEts=MJQUab4gS6XEvSogJn zrhd(SPsVkfO%NQuxPtpC3uy-^o4pDA=qEFqzWRIgI2w{%@b8jMVxuqed;*?pQpjj? zh*fB@cIs0mVB_vM;8@HwF7Fp!$)I|2S;*s{7}QMuQ|3twmdxxUPSqU544}uqn`Gdw z1>V8IXaB*T;jYEJ`>*nBaE+MzC2qsKAOj!&jeA_&HL07p5{mzz8gSRTpZ^zM0#UW=eASg1;2EOBdh=UrIle& zLePyD*ttp%sk&N0X&y+t=-zIgWO~rfh`Tu)6!_20^#Wfi8TYiUvt1_{e8{nV-JSe@ z-@wW+?rZPd1dsMl>y7`H_xk_m3-uQ@rVwP?%dP`OVd4K%hW^j`?VWx=gw(x_UBG)_ z%q9BcB%9jAAjtExZJA@A@VOT#nnDum|8~Dg$smn%ZdHZ!K+Vnl3du(nL$yRzKG+pFd=1F+^?`YgupB>!|Wf>C4K5^ zq75gO5{=op_sylA<$rItgf0rVX(GethCOiJe(R3QOsWZ&t%n2iXMfQk3d)h6$xfMg z`sv2nq`1V{3C~q#z6OTC*^L^AK%TVF8t(42eh}_e{~06taIK7TDEf_5_lV-pLcDfH z3VQxL`7-aho&SC)5S=ov2IN+j8@qE3i2tE=qiYIS+`RGRiKEm?wYeYwk2HAMB|*uP z+4A`7i4-eFizL(UM`VKDOJd5CqXRaQ(_^{oO=tYIG(Pu1X+uul5^-GBQxd^ecS;WW zK)gM=zv6z{>&;}3i|x4UkGou+%?(ojM&^Yyxd~i$!<&Z1Efp2+FmR` ztShsvd1+kpBQ$*MVFU!}np zg@&Cmg@k+`^>h19b+TEdrO1(qFGDWwqSoY&z%Q9k7?%7uMb3#+7RT zJV0&N{nzsCwEn+44Kd6MarJyi;dZ43wLBo|G1Y0f_+prB&ZS0+c`*elbpPoY^pCxu zg-t+HZ8c1;+|Bg5f~z-jqpJ^7_Ey1^htkX@KXa9>3Ts%gJ+kSU`Z9D+c?8jT#_vYk z95^diW^z$#s65sa?GWpZ=&W4aJOz7*nA7a+09mPsk66DC-t~_o7&N>)qO|~dl)P~i|6uWJ&sG2MqWQZ&pJu6n$&B#Iuw3k*_luo=FWOE8D=3(+eV)M8U}B#A-C?3F=7&T4F3n@} z6KY|}f4Zam>x_rAI>3#H8oqlEK58hu(l>Sd(NEd*TBpJsu1%uum}@0Eg@v=-AFYyi$ zZ#)*@z5aVq;KnSVzKF~3PH@Wj=gRS~!@fNf_IG!~D!ZK`bscJXHe>vBc z-9XsakT;23ZJpAX!?&@k*tcML9?)9+etCBoQ&|d0c0FsG7<2g}URQN2!v4c|mTQcQ zGJ1_IWNYb^0Xdwz^}^^Hrv6X%@Ob-I&sh)q#$ST>Jt0|#J_j{CwXQ~kJ}qM7hc!ln z#|y^M1;=8`ep0kJ!stJu15mZv0Aej-lB{U!{pku|-G_TV^iIT>1$M0%J9pTXOnDGf z#@i4JirxVMaZCm*o6$V+@i&hWBzwIBdtZ^XokFb3JVPiT3|>ILUyVqO`FO4@&nc;9 zAw&QG0zrSmPnIR{u-4@*g?__YsADvdvJBu5?w8C2fl?+{3CV2M2jus54yi$RMkL9S z38*r9nnQY9$=C{CS{t7U{VJL2cgX%CirI#P7CscAg~oRyumBRuNDU z6AKPw1x3f1mS2GY>OaFaoE4Adrf0Y&g5q45vNh_;R_DawdO9stx2oC!}dY?YiPnWaH+!4hG5>d{ucnZV~$)VKqzgul_ zRp7O*2)XS9SxW-!D=>7C`w1;9YV9y(rT#NdCZ!X{ znkE`|N-FFsA!PJIY)=+$C%ZA!O7{yvT0-NdK3JuU2d&nezuBvIk1+LBiO$VidIU@e zN9u!;J-eMyj9258D@%tLcOrX+r@2@^oMrYng|W|?iOPA>3U%5x+NjqxkFRC=8N&PQy;*S4s0cg<)gCTG#et?PunjqUwey@mot5hE+?~ zqcWQ88*971kJs+s`(#;qpqCsYhl@@B(-K&m!b21+1!n;l{$@D%|V^)A{3~_ zt(bQr+t;#_ad`FEIiJ_0znxKe0W0c53F`ABQ^N&bpZ%_3!sXTlpIpV^Au;7?_+n8hB&6)Zxr&lPAIbodP2|HnWMAz(&hv5y=oy0ngXJHTG zmnK}7E17THV*3avD#9nm`FZ)B6?rRK*^S2dYR+sLDXv+b_&oPN3m9TdkgLk0$C7F; zdtNmT!Vx3mEjiA1yPptg1~a|~p9lupEDbvssyJ;?Dl*Fx4@z|A^Kz68&-Vo+zABlv z01yP6lWgXbRugC9hlohR2CqZESf7R6Q=O&?s$zaxDGi^@-QQ&&?J=*GToqjNN%?A6 zLQ!wItY)1beD7RZCg)qX6`AxHB;)C$)st21e*Ql_CjXTc1eH@!MxpXY|4(~o{+49g z_Hi0pr%7|1QOl)Cr^Om`$ZcH7a!GT^Y%vop$c)@l6a%-?%1V^ntpsXRsLX}jQUo#= zT(QK&70V3?l$4NAgy){;oq3-3c>ja{j9QUg_qyal|+UObO zug*ouLcvS-P9F`Qw3s1EaG=wLY25^W*TfjB@GCFFf#So?A`_yL833?YryC=IttZayr7!wamObj6HdBC|H_lIxmf<)9=TSw~ZV- zTSiL?Y^OVWxxZeCZlwdmW~00TJOh$omlY&vfJx}@Ksa5jBmW$9!U}`3HgnIJ%WbH_@02m zgWM5lYK8WQ@a>O+hi8p9-j@L6`LO=F|C_%z*{C1r)A0P!fB(Qd`$ykQm;rL?#4;xc z;M#c!!KdE32G$Ssq}EiwEE4DBy(TauH@KQWhc3Nrr8mT?toXxs-?d$sU+meJlWb^O zqvbDdSRIU`OKAVBw^J`cb8v=QQfYf{QKU^ z?Qc9U=Hv-bx{l32n?3A`Lv!J28uza=EMddbl~tOtm-~)8P5|c`+XPpS?3(D@0&Q^L z>aF<`K><7w^*sZ;JnZTR{^h(d_$YPbWKF>>4Xr@VMQG@iOt)7rhn=+HjV?e=eeSPo zZ_y_7k-Fb;`@?lj#um@E+fdb$z5W*(F9mNU2Kr z+&MSdOTS%Z$z=nK(n(cY|M}5jZ?29}FtkciFyEE;^n{ySSr^OmI(-_QoT}mNcz@SW zkj8O`Lyiuoy)C?R685YAGh5=Fh|ehNyd$@GE%# zDciR`8e}gB#P-4rl)J)}lGH8j3xg!LgDVKk&G_-jgf@wPeWI7&ds&44IDRFAEGEj0 z0+k`q{d;!~Is~n?0fF`cXCn^`SW>5#T+x{6do?!5;=AuoA~o&v$mto{ab6Fpm!X)C zvsA0BEe#1#0fITY6OKG2J~PeF2OFDycN|N4$gVebPHgS(*7i1sb&v67MWSLah74!> zTrd^K@DN+~ra!9%>zO)Vbh?>#C|WxDGkO`Zmf&k>>^#zXviFWFc=)&E6KjiHV~tz}hror3#x!N&Hj=WKr+x$cF5=mtb#BI;`s)t|I$Dzh2 zhYMpx>s28?J(;NegSE&+m24Rabk(w#SrDad_=*_;ri|aHE`v=CHyE4DT&-KY8{9`4 zOSR!3BexIO8556T19VJ{1~i1MkoS0*%o4p8>OPtN%l6Ewh)Qb07p$%fwlC5@#rIex zRV;qhA)S_PGyDa9bMmGQk9;oGt0U6bS(fm*OZh-0-2F-Vvu5?LDB9JR;(96YXl9J^ zIicsUH3ysw)~`vjRrvT|vK|PO)4)1!`D)MaPJ8c-IUBk= zeI81hf@NG({{&lmCRF#GP6uXSzs`ejdf4H_c(F@j;Dd81vm4!zekLFOjl& zNnhiJ<=vUOiK;|}nFl?@LI0iRjbQ#mJ@oJ;cQ$s*_pnb0EO)zI;%UDIj0^V%gXF+b z>EeRgqUuX^VmTqUu*<83s{7Wu*D7i`ihFb?2~Y2{I$emo*-&Ty)!NH6eR2xU?Z*>F z7p`#AYf3|o>+5OxAmBIs9UHWv`Ia58oUc*QvwG;93k1c=w6dLj8-8&jD|e(^PiQR- zL(g1Jq!nafcy?)vJPL{}Mtp**mCW_h2;^Dds`!Ps^it}MQx!8zK#0>^$?W5duA z!rVgbIU{X6LHz|z)0g2PlPE_lqMTwsPGN}_25;@RD53J~vdGPC?HwDd1huwAa=r7R zc*W;i(esm#8Kb^4E-Nt#O@qcZ44QJ*)qliS#l%_OiiwTvnZ!ueQ%i{>=H)8#txc-? z3sa!P05kF`PJk^)y&@V9T?Rbnd<1&5GkN!=q^0hP;UO zC%-#4w8o_wF(@kG0l{G(o|ZE2a_)|ugtz!&pQj-z)mvel_u3xX>O#MYA#nqIac`CF zspw{;UC0Nh;fdCj?Uukim9KoFkM!nQ*+Zr$McvELuWfrGczbxq^DvpUOJy3O!HR^+ z(7O?{&>3bm*hbNF6adn~0K zsSlnoAq}_PVT3@{X1yv?Iya&~pyQvMQ^s!Ee%j}Za5|c1&O6+aw-Mc|XG=^uu{Q5q zII(~*s&H$NQ+)ZuGjK0f$teeNIg(|!H`^H z7?JRh{HOC^+S8wS>T5pHodk=lVs)m4TzFi6RkNt)O%tnLZ_d_)Nxm`0rx=?|W%`4S zEeD3uyw9zUpR=ET?QM0aExKL?}OQ=GU^2urs5+n=5`Dl+aQB%^Nepb9Ks9<>x`x3hwCkqD^p9oc5~bg6W@^ zuol=5&7!3V@tt?+;LdqG3r_0xFC@k_B3D%KQVmLEbFb=*`FHS!(6ujLK?c@g-(}b= z(K5rg9o)%mFCggJh-M@bYffXV%g9!r%wSfW87{BzeLX#*P#Ls5flKnrXDD#d%r6@- zBpQkvh<%gR7o*1JvqM`1zxo~}kTOIBpq4_P9s657Pe;mu~CkNU-U?qNC z`6ApOjBFgL7dfh0Z{ESZZ|Ql*e0;_PO0-{DReCbVW&7|9d6@NT6Nf+NMfV7XEH}SL zWJyiV5-s!ZT>FWA8D5o2+rXN}yW{XHwl2lGIqR|Abzx(1zu~^#Q$H7Z05A_WO2;NB z03+&Rlpw0|rzJxygXEoKyEec4?U zFIcS*TgRz4xgGIod8??CXff|0(p z#Yx{f#;PnnD&nC<2m-@nGe}K%=}*jfs(5s#A&K+^4`&8{9h_N}VcL$q3Gq_@~o@{4gM&>1ORLJYcV8rLo&(Oa@vDi;~?=&e! zD6RCK?FQY(zI5=GIRasQ^gNMvKFm+n&jo_YxvtfWz0qgqfO(n`34kF&E7oHQLO6xy zKhb{1L5GKCY-R5wv;4CxDFsy@DAl2J8FI|RptnXa@A}U^{_v>*j}*p^w-Xdf_Lb1A zw=0cKxX_MAzqMGIoTb@~+r#ADw%?UW(UIeW9nunt&-}5XJv|+5?hhZ4$J^qtVv*8M zX>=xeY?Os|?!Sj%QX-w)OQ2Pef%N%iCO2Dh`^{H>SPm4s6&tf;(Ahc%_V@#po-VLi zX?ibXsJG3S+0f8`NsbpC}%9P?-*r*B{dcHIyOIOk5)N2DZRbX{UaV@cFQl;ai|VX`VLw+ zzJ?4V!FN8~feiAyBAQH^wJqoFFqAJha@Azqkd{d6-k!R^ZMhtAfyjBbbwz;RzHFC} zAyN8`OS39s3I(|%7?zAG(3eh2)=rwHna`E`Kf1Q#OvN0eks&Hi5JgrbUZRy_e;4BC zhgYm+bbBJ2KG(T`VNG`O5~iy@tps2y=H4^E(3c3X>m&*DW@o zj&EyH1%cc)p$X(44Y}$<5a@Bv`3=>s z|73T!N|oVX|0z6o4Xz3M@!Qw@U3z@GY2Q5UTikq0Hs4a`xBceZ?)k0c_*OuDt2+Px z)X?s#s7W#@vxpgk1vF@4|0`B3&j`>2x`3+CXbSJ_c0%;oZC(SqPTV?<-@~PSjTZn6VKx??Kr8+c|ipw3~?Ca;4Yd|1~ z0o_z>yT#c`m4}9alrR_^vyQ)__^wZB4+4F#S-{H%J`Mmfe;C;_b;Z>5WZ~;ynBpsF@YzwdKU8fPQUrplQ;)>xqWbVaXsQgn8qT~ z1xwunv5596$aNkAQ<2I-)|@H(b(6;8 zRyfUHU{T^|%4tT4<+v;c6UtV|SF&vaT*aD@%#;gCF`)R80J(QL7x2(}!g(zi=F?4t zn0=2KOU}+u%pM3I&99wLForHq1hQYqO8Z*9lv(;`>uhj0)1JLN;>>-Z+9>lLUhO-_ z|Jqv7H+lv3HUzh}hEH$#fksj9`saRFVucOTdB z`OQPMtW`C;msXHkW&Ik^>l%4~)Nc@{N)^9dfYHnM54LLMT*zkMhHmJ)p|OL6kRI?b zy=3AQk?=VN!xJ+|0NnvNqU=gTLO<3vEas@R+IBenvh<4?qAeL<%)Q8Ho0X3u_EK5uB$@$UaM+ANHU4p7}sUAi) zfgsQ;13FPv2N@eMG3}i1`?uHq+k#sp&wzC;Y-YG4HD7p`=*5}f8@`O_4&27kZ0zDb z_*Bs6Cu~^nMJ_83Jb`KR- z4A9Q0N@<5kVTTv$DqG?Ci~YlP6~F|-Kx;hb%}TGlYcB|-q_s%9=Oazo57g+c!HEiw zPx=tS?MtD5$3eGbEwwSSWsu5VJw|9>5WbmaOuecFxO;Vsh*DZK1F*ZK7>@?+%sRMo z;mhmAE&O(VmsLAWv1HPhQmm2%Z89#{D)AdIeGrA$x1^ip_PT zB|i%Yo=|9t&226Geh}!UuAr^gjXbWjoBzAgtQrS_F0hoSvdSd{tg?u0TpkiAc=Rv}-We zvlE}?UY(HCG2BzV60$@%#3ss-&`^KM<>`ShEd%*|T1oOa>uZaXU$`cUJo2c1f*=aB zT*5|tzUl*81Nw@fkQNK>BH8EE1p!vs`Pc4>@KgdM)03v8nxPg)se+>hcz)+zdUS(f3}5KrQ!1{j;{BCLqBU;gwIQ z$n)nU{Z-FOLwvbwY5~4)0+HHRS_3}#NM3jcC5UazvY59nAPl2_DH~sk3gCo)yyqeF zJ>t@HG$Kh(Zd@NARE-mkYRME*hP 删除 `isEngineEditingCapable`(无遗留调用点后)。 + +## 4. GizmoManager 接线(BabylonRenderHost) + +`mount` 时建 `this.gizmoManager = new GizmoManager(scene)`,`usePointerToAttachGizmos = false`。 + +- `setGizmoMode(mode)`:`positionGizmoEnabled = mode==="translate"`,rotation/scale 同理(互斥)。 +- `setGizmoTarget(node_id, locked)`:`obj = adapter.getRuntimeObject(id)`;`gizmoManager.attachToNode((obj && !locked) ? obj : null)`。 +- **拖拽生命周期**:gizmo lazy 创建,需在三 gizmo 都 enable 一次以实例化并挂 observable(plan 期细化稳妥写法),挂: + - `onDragStartObservable`(三 gizmo):`dragStart = captureTransform(attachedNode)`;若 translate 且修饰键,经 `snapProvider()` 缓存其它节点的 `featureSnapPoints` + `socketPoints`(屏幕投影,相机此刻冻结)。 + - `onDragObservable`(仅 positionGizmo):修饰键按下 → 对 `attachedNode.position` 应用 `computeSnapOffset`(socket→node→grid 链,与 Three 同)。 + - `onDragEndObservable`(三 gizmo):`end = captureTransform`;`transformsEqual(start,end)` 则跳过;否则 `commitCb(nodeId, start, end)`(nodeId 取 `node.metadata.nodeId`)。 +- 修饰键追踪:window `pointermove`/`pointerdown` capture 读 `ctrlKey||metaKey`(同 Three,避免 keydown/keyup 卡死)。 +- 拖拽中 `highlight.removeAllMeshes()`,松手按当前选中恢复(对齐 Three 拖拽期隐藏描边)。 +- dispose:`gizmoManager.dispose()` + 注销 window 监听。 + +## 5. Babylon snap-features(新文件,喂共享 computeSnapOffset) + +`src/runtime/babylon/snap-features.ts`,镜像 `runtime/three/snap-features.ts` 的签名,产同样的 `SnapPoint`/`SocketPoint`(来自 `@/core/snap/*`): + +- `bboxFeatures(node): Vector3[]`:OBB 15 点(中心+8角+6面心,顺序与 Three 一致——node-align 跨引擎对等需顺序一致)。用 `getBoundingInfo().boundingBox` 的 local min/max 算 15 个局部点,经 node world matrix 变换到世界(跟随旋转 = OBB)。无几何 → `[]`。 +- `toScreen(v, scene, w, h)`:`Vector3.Project(v, Matrix.IdentityReadOnly, scene.getTransformMatrix(), {x:0,y:0,width:w,height:h})` 取 x/y。 +- `featureSnapPoints(node, scene, w, h): SnapPoint[]` / `socketPoints(node, sockets, scene, w, h): SocketPoint[]`:同 Three 语义(socket 世界点经 node world matrix)。 + +> 注意:Three 版签名带 `camera`,Babylon 版用 `scene`(Babylon 投影走 `scene.getTransformMatrix()`,相机是 `scene.activeCamera`)。两版各自引擎专有,喂同一个 `computeSnapOffset(args)`(args 是引擎中立的 `SnapPoint[]`/`SocketPoint[]`/`Vec3`)。 + +## 6. Babylon transform-util(新文件) + +`src/runtime/babylon/transform-util.ts`: + +- `captureTransform(node): Transform`:读 `node.position` / `node.rotationQuaternion ?? Quaternion.FromEulerVector(node.rotation)` / `node.scaling`,rotation 统一成四元数 `[x,y,z,w]`(gizmo 旋转写 `rotationQuaternion`,但 Euler 节点首次旋转前 `rotationQuaternion` 可能为 null,须从 `rotation` 转)。 +- 复用 B3a 的引擎中立 `transformsEqual`(`@/runtime/three/transform-util`)?——`transformsEqual` 是纯数组比较、与引擎无关,但住在 `runtime/three`。**搬到中立位置** `src/core/...` 或就近复用?决定:`transformsEqual` 移到 `src/runtime/transform-util.ts`(引擎中立,Three/Babylon 都引),`captureTransform` 各引擎一份(读各自 node)。最小改动:B3b 把 `transformsEqual` 从 `three/transform-util` 提到 `runtime/transform-util`,更新两处 import。 + +## 7. BabylonViewport gizmo 接线 + +mount effect 内(host.mount 之后)镜像 ThreeViewport 薄壳的 gizmo 部分: + +- `host.onTransformCommit((id, prev, next) => executeCommand(new SetNodeTransformCommand({ node_id: id, transform: next, prev_transform: prev })))`。 +- `host.setSnapProvider(() => Object.values(nodes).map(n => ({ id, sockets: n.sockets ?? [], visible, type })))`(同 Three)。 +- 订阅 `useUIStore`:`selectedNodeId` 变 → `host.setGizmoTarget(id, node ? isEffectivelyLocked(node) : false)`(host.setSelection 已在 B2 接);`gizmoMode` 变 → `host.setGizmoMode`。 +- 初始:mount 后 `host.setGizmoMode(useUIStore.getState().gizmoMode)` + `host.setGizmoTarget(selectedNodeId, locked)`。 +- **不接** socket markers / play / focus / asset drop(B4)。 + +## 8. 测试与验证 + +- 单测(headless): + - `babylon/snap-features.test.ts`:bboxFeatures 15 点 / OBB 旋转面心 / 空 node [];toScreen 投影(origin→视口中心);featureSnapPoints/socketPoints。仿 `three/snap-features.test.ts`。 + - `babylon/transform-util.test.ts`:rotationQuaternion 与 Euler 两路捕获。 + - `render-host.test.ts`:`engineCapabilities` 表(gizmo 两引擎 true,play/focus/assetDrop three-only)。 + - GizmoManager 接线轻测(headless):setGizmoTarget 后 attachedNode 正确、setGizmoMode 切 enabled、locked/null detach。 +- 共享 `computeSnapOffset` 已在 B3a 测(三分支)。 +- nice-to-have:Three vs Babylon snap-features 对等断言(等价场景+相机产等价 offset)。 +- **visual smoke(`pnpm tauri dev`,Babylon 模式)**:① 选中后 gizmo 出现;② T/R/S 三模式拖动;③ Ctrl/Cmd 三层吸附(grid/node/socket);④ 一次拖拽=单次 undo;⑤ locked 只描边不挂 gizmo;⑥ 拖拽中描边隐藏、松手恢复;⑦ gizmo pill 在 Babylon 不再灰、play/F/drop 仍灰;⑧ Three 模式零回归;⑨ console 零错。 + +## 9. 风险 + +- GizmoManager 三 gizmo lazy 创建——observable 须在各 gizmo 实例化后挂全(plan 期给稳妥写法:enable 三次以实例化或用 gizmoManager 的 gizmo getter)。 +- Euler 节点首次旋转 `rotationQuaternion` 为 null → captureTransform 须 fallback 到 `rotation`。 +- 屏幕投影依赖相机矩阵已更新(真机拖拽时已 render;测试手动 `updateTransformMatrix`)。 +- node-align 跨引擎对等要求 bboxFeatures 15 点**顺序与 Three 完全一致**(computeSnapOffset 按最近点选,顺序不一致会选到不同特征但结果应等价;仍保持一致以防偏差)。 + +## 10. 交付物 + +- `src/runtime/render-host.ts`:`EngineCapabilities` + `engineCapabilities`,删 `isEngineEditingCapable`。 +- `src/runtime/babylon/render-host.ts`:4 个 gizmo 方法实现 + GizmoManager 字段 + 修饰键监听 + dispose。 +- `src/runtime/babylon/snap-features.ts`(+ 测试)。 +- `src/runtime/babylon/transform-util.ts`(+ 测试)。 +- `src/runtime/transform-util.ts`:`transformsEqual` 中立化(从 `three/transform-util` 提出),更新 import。 +- `src/ui/viewport/BabylonViewport.tsx`:gizmo 接线。 +- 5 处能力门调用点 + `render-host.test.ts`。 +- `docs/roadmap.md`:B3b 勾选 + B3 收口。 From a94d0fa2742ebc2edf53cca7b3faf46e52684f95 Mon Sep 17 00:00:00 2001 From: longyi-xw <2691049525@qq.com> Date: Tue, 16 Jun 2026 16:38:33 +0800 Subject: [PATCH 02/14] =?UTF-8?q?docs(plan):=20v1.0=20B3b=20=E2=80=94=20Ba?= =?UTF-8?q?bylon=20gizmo=20+=20snap=20implementation=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../plans/2026-06-16-v1.0b3b-babylon-gizmo.md | 997 ++++++++++++++++++ 1 file changed, 997 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-16-v1.0b3b-babylon-gizmo.md diff --git a/docs/superpowers/plans/2026-06-16-v1.0b3b-babylon-gizmo.md b/docs/superpowers/plans/2026-06-16-v1.0b3b-babylon-gizmo.md new file mode 100644 index 0000000..9d1837b --- /dev/null +++ b/docs/superpowers/plans/2026-06-16-v1.0b3b-babylon-gizmo.md @@ -0,0 +1,997 @@ +# B3b Babylon gizmo + snap 实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 填实 `BabylonRenderHost` 的 gizmo+snap(B3a 定义的 IRenderHost 契约),让 Babylon 模式可拖动(T/R/S)+ 三层吸附,与 Three 跨引擎对等。 + +**架构:** Babylon 用 `GizmoManager`(`usePointerToAttachGizmos=false`),拖拽生命周期映射到契约(onDragStart=缓存 snap 目标、onDrag=应用 snap、onDragEnd=commit)。snap 喂引擎中立 `computeSnapOffset`(移到 `core/snap/offset.ts`),Babylon 自写特征提取(OBB+投影)。能力门从单一布尔细分为 `engineCapabilities{gizmo,play,focus,assetDrop}`,B3b 放开 gizmo(两引擎)。 + +**技术栈:** TypeScript / `@babylonjs/core` 9.11(`GizmoManager`/`PositionGizmo`/`Vector3.Project`/`getBoundingInfo`)/ React / Zustand / Vitest(NullEngine headless)。 + +**规格:** `docs/superpowers/specs/2026-06-16-v1.0b3b-babylon-gizmo-design.md` + +**已 de-risk(计划期 NullEngine 实验):** GizmoManager headless 实例化/attach/dispose OK;`positionGizmo.onDragStart/onDrag/onDragEnd` observable 齐全;OBB(`getBoundingInfo().boundingBox`)+ `Vector3.Project`(先 `cam.getViewMatrix()`+`scene.updateTransformMatrix()`)headless 可用。 + +**测试约束:** GizmoManager 接线(mode→enabled / target→attached / dispose)+ snap-features + transform-util + engineCapabilities **都 headless 可测**;拖拽→snap→commit 整链 + 描边隐藏/恢复 = **visual smoke**(任务 9)。 + +--- + +## 文件结构 + +| 文件 | 职责 | 操作 | +| ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------ | +| `src/core/snap/offset.ts` | `computeSnapOffset`(引擎中立,从 three 移来) | 创建(移动) | +| `src/runtime/three/snap-features.ts` | 删 computeSnapOffset,改从 core 引(如内部还需) | 修改 | +| `src/runtime/transform-util.ts` | `transformsEqual`(引擎中立,从 three 移来) | 创建(移动) | +| `src/runtime/three/transform-util.ts` | 删 transformsEqual,保留 captureTransform | 修改 | +| `src/runtime/three/render-host.ts` | transformsEqual 改从 runtime 引 | 修改 | +| `src/runtime/render-host.ts` | `engineCapabilities` 替换 `isEngineEditingCapable` | 修改 | +| `src/ui/viewport/{PlayButton,use-editor-shortcuts}.tsx/.ts` + `EditorView.tsx` + `services/library/asset-drag.ts` | 改用 engineCapabilities | 修改 | +| `src/runtime/babylon/snap-features.ts` | Babylon OBB 特征提取 + 投影 | 创建 | +| `src/runtime/babylon/transform-util.ts` | Babylon captureTransform(四元数归一) | 创建 | +| `src/runtime/babylon/render-host.ts` | 4 个 gizmo 方法实现 + GizmoManager | 修改 | +| `src/ui/viewport/BabylonViewport.tsx` | gizmo 接线 | 修改 | +| `docs/roadmap.md` | B3b 勾选 + B3 收口 | 修改 | + +--- + +## 任务 1:computeSnapOffset 移到 core/snap/offset.ts + +**文件:** + +- 创建:`src/core/snap/offset.ts` +- 修改:`src/runtime/three/snap-features.ts`(删 computeSnapOffset) +- 修改:`src/runtime/three/snap-features.test.ts`(import 改路径) +- 修改:`src/runtime/three/render-host.ts`(import computeSnapOffset 改路径) + +`computeSnapOffset` 引擎中立(只用 `@/core/snap/*`),住 three/ 会让 babylon→three 依赖。移到 core。 + +- [ ] **步骤 1:建 offset.ts(逐字搬 + 改 import 为同目录)** + +```ts +// src/core/snap/offset.ts +import { snapTranslation } from "./grid"; +import { SNAP_PIXELS, snapToNodes, type SnapPoint } from "./nodes"; +import { snapToSockets, type SocketPoint } from "./sockets"; + +type Vec3 = [number, number, number]; + +/** + * Pure snap priority chain: socket-align → node-align (only when the dragged + * node has no tagged socket) → grid fallback. Returns the world-space offset to + * apply to the dragged object's position. Engine-neutral — both ThreeRenderHost + * and BabylonRenderHost feed it engine-specific SnapPoint[]/SocketPoint[]. The + * caller gates on translate-mode + modifier before calling. + */ +export function computeSnapOffset(args: { + currentPos: Vec3; + draggedFeatures: SnapPoint[]; + draggedSockets: SocketPoint[]; + hasSockets: boolean; + targetFeatures: SnapPoint[]; + targetSockets: SocketPoint[]; +}): Vec3 | null { + const { currentPos, draggedFeatures, draggedSockets, hasSockets } = args; + const socketOffset = snapToSockets(draggedSockets, args.targetSockets, SNAP_PIXELS); + if (socketOffset) return socketOffset; + if (!hasSockets) { + const offset = snapToNodes(draggedFeatures, args.targetFeatures, SNAP_PIXELS); + if (offset) return offset; + } + const [gx, gy, gz] = snapTranslation(currentPos); + return [gx - currentPos[0], gy - currentPos[1], gz - currentPos[2]]; +} +``` + +- [ ] **步骤 2:从 three/snap-features.ts 删 computeSnapOffset** + +删掉 `three/snap-features.ts` 末尾的 `type Vec3` + `computeSnapOffset`(约 106-134 行)及其不再用的 import(`snapTranslation`/`snapToNodes`/`snapToSockets`/`SNAP_PIXELS` 若仅被 computeSnapOffset 用——核对:`SnapPoint`/`SocketPoint` 仍被 featureSnapPoints/socketPoints 用,保留它们的 type import;`snapTranslation`/`snapToNodes`/`snapToSockets`/`SNAP_PIXELS` 删)。 + +- [ ] **步骤 3:改引用** + +`three/snap-features.test.ts`:`import { computeSnapOffset } from "@/core/snap/offset";`(与 bboxFeatures 等分开 import)。 +`three/render-host.ts`:把 `computeSnapOffset` 从 `./snap-features` 的 import 拆出,改 `import { computeSnapOffset } from "@/core/snap/offset";`(`featureSnapPoints`/`socketPoints` 仍从 `./snap-features`)。 + +- [ ] **步骤 4:验证** + +运行:`pnpm typecheck && pnpm test` +预期:typecheck PASS;snap-features.test + 全量绿。 + +- [ ] **步骤 5:Commit** + +```bash +git add src/core/snap/offset.ts src/runtime/three/snap-features.ts src/runtime/three/snap-features.test.ts src/runtime/three/render-host.ts +git commit -m "refactor(snap): move computeSnapOffset to core/snap (engine-neutral home)" +``` + +--- + +## 任务 2:transformsEqual 移到 runtime/transform-util.ts + +**文件:** + +- 创建:`src/runtime/transform-util.ts` +- 创建:`src/runtime/transform-util.test.ts` +- 修改:`src/runtime/three/transform-util.ts`(删 transformsEqual,留 captureTransform) +- 修改:`src/runtime/three/transform-util.test.ts`(删 transformsEqual 测试) +- 修改:`src/runtime/three/render-host.ts`(transformsEqual 改 import 路径) + +- [ ] **步骤 1:建 runtime/transform-util.ts + 测试** + +```ts +// src/runtime/transform-util.ts +import type { Transform } from "@/core/scene/types"; + +/** Engine-neutral exact transform equality (component-wise). Used by both + * render hosts to skip a no-op gizmo drag commit. */ +export function transformsEqual(a: Transform, b: Transform): boolean { + return ( + a.position[0] === b.position[0] && + a.position[1] === b.position[1] && + a.position[2] === b.position[2] && + a.rotation[0] === b.rotation[0] && + a.rotation[1] === b.rotation[1] && + a.rotation[2] === b.rotation[2] && + a.rotation[3] === b.rotation[3] && + a.scale[0] === b.scale[0] && + a.scale[1] === b.scale[1] && + a.scale[2] === b.scale[2] + ); +} +``` + +```ts +// src/runtime/transform-util.test.ts +import { describe, expect, it } from "vitest"; +import { transformsEqual } from "./transform-util"; +import type { Transform } from "@/core/scene/types"; + +describe("transformsEqual", () => { + it("true for identical, false for any differing component", () => { + const a: Transform = { + position: [0, 0, 0], + rotation: [0, 0, 0, 1], + scale: [1, 1, 1], + }; + expect(transformsEqual(a, { ...a })).toBe(true); + expect(transformsEqual(a, { ...a, position: [0, 1, 0] })).toBe(false); + expect(transformsEqual(a, { ...a, rotation: [0, 0, 1, 0] })).toBe(false); + }); +}); +``` + +- [ ] **步骤 2:从 three/transform-util.ts 删 transformsEqual** + +只留 `captureTransform`(Three 专有,读 Object3D)。删 transformsEqual。`three/transform-util.test.ts` 删掉 transformsEqual 的 describe 块(captureTransform 测试保留)。 + +- [ ] **步骤 3:改 three/render-host.ts 引用** + +`three/render-host.ts` 现在 `import { captureTransform, transformsEqual } from "./transform-util";` → 改成 `import { captureTransform } from "./transform-util";` + `import { transformsEqual } from "@/runtime/transform-util";`。 + +- [ ] **步骤 4:验证** + +运行:`pnpm typecheck && pnpm test` +预期:全绿。 + +- [ ] **步骤 5:Commit** + +```bash +git add src/runtime/transform-util.ts src/runtime/transform-util.test.ts src/runtime/three/transform-util.ts src/runtime/three/transform-util.test.ts src/runtime/three/render-host.ts +git commit -m "refactor(transform): move transformsEqual to runtime (engine-neutral, shared by hosts)" +``` + +--- + +## 任务 3:engineCapabilities 替换 isEngineEditingCapable + +**文件:** + +- 修改:`src/runtime/render-host.ts` +- 修改:`src/ui/views/EditorView.tsx`、`src/ui/viewport/PlayButton.tsx`、`src/ui/viewport/use-editor-shortcuts.ts`、`src/services/library/asset-drag.ts` +- 修改:`src/runtime/render-host.test.ts` + +- [ ] **步骤 1:render-host.ts 加 engineCapabilities,删 isEngineEditingCapable** + +替换 `isEngineEditingCapable` 函数(及其 doc comment)为: + +```ts +/** + * Per-engine editing capability flags. B1/B2 gated everything behind a single + * "three-only" boolean; B3b splits it so Babylon can enable the gizmo while + * play / focus / asset-drop stay Three-only until B4 implements them. B4 flips + * the remaining flags here in one place. + */ +export interface EngineCapabilities { + /** Transform gizmo + snap (B3b: both engines). */ + gizmo: boolean; + /** Play/pause behavior preview (B4: Three-only). */ + play: boolean; + /** F-to-focus camera (B4: Three-only). */ + focus: boolean; + /** Drag library assets into the viewport (B4: Three-only). */ + assetDrop: boolean; +} + +export function engineCapabilities(engine: ViewportEngine): EngineCapabilities { + const three = engine === "three.js"; + return { gizmo: true, play: three, focus: three, assetDrop: three }; +} +``` + +- [ ] **步骤 2:改 5 处调用点** + +`src/ui/views/EditorView.tsx`(GizmoModeToolbar disabled,约 :121): + +```tsx +disabled={selectedNodeId === null || !engineCapabilities(viewportEngine).gizmo} +``` + +import 改 `import { engineCapabilities } from "@/runtime/render-host";`。 + +`src/ui/viewport/PlayButton.tsx`(:12): + +```ts +const editingCapable = engineCapabilities(viewportEngine).play; +``` + +(变量名可保留 editingCapable 或改 canPlay;import 换 engineCapabilities。) + +`src/ui/viewport/use-editor-shortcuts.ts`::115(F focus)→ `if (!engineCapabilities(ui.viewportEngine).focus) return;`;:125(Space/play)→ `if (!engineCapabilities(ui.viewportEngine).play) return;`。import 换。 + +`src/services/library/asset-drag.ts`(:22)→ `if (!engineCapabilities(useUIStore.getState().viewportEngine).assetDrop) return;`。import 换。 + +- [ ] **步骤 3:改 render-host.test.ts** + +把 `isEngineEditingCapable` 的 describe 换成: + +```ts +import { engineCapabilities } from "./render-host"; + +describe("engineCapabilities", () => { + it("enables gizmo on both engines (B3b)", () => { + expect(engineCapabilities("three.js").gizmo).toBe(true); + expect(engineCapabilities("babylon.js").gizmo).toBe(true); + }); + it("keeps play/focus/assetDrop Three-only (B4)", () => { + const b = engineCapabilities("babylon.js"); + expect(b.play).toBe(false); + expect(b.focus).toBe(false); + expect(b.assetDrop).toBe(false); + const t = engineCapabilities("three.js"); + expect(t.play && t.focus && t.assetDrop).toBe(true); + }); +}); +``` + +- [ ] **步骤 4:验证(含无遗留引用)** + +运行:`grep -rn "isEngineEditingCapable" src/`(应为空)→ `pnpm typecheck && pnpm lint && pnpm test` +预期:grep 空;全绿。 + +- [ ] **步骤 5:Commit** + +```bash +git add -A && git commit -m "feat(render-host): split editing capability into per-feature flags (enable Babylon gizmo)" +``` + +--- + +## 任务 4:Babylon snap-features(OBB + 投影)— TDD + +**文件:** + +- 创建:`src/runtime/babylon/snap-features.ts` +- 测试:`src/runtime/babylon/snap-features.test.ts` + +镜像 `src/runtime/three/snap-features.ts` 的语义(15 点顺序必须一致),喂同一 `computeSnapOffset`。Babylon 投影走 `scene`。 + +- [ ] **步骤 1:编写失败的测试** + +```ts +// src/runtime/babylon/snap-features.test.ts +import { describe, expect, it } from "vitest"; +import { + NullEngine, + Scene, + ArcRotateCamera, + Vector3, + MeshBuilder, + TransformNode, +} from "@babylonjs/core"; +import { bboxFeatures, toScreen, socketPoints } from "./snap-features"; + +function makeScene() { + const engine = new NullEngine(); + const scene = new Scene(engine); + const cam = new ArcRotateCamera("c", 0, 1, 8, Vector3.Zero(), scene); + cam.setPosition(new Vector3(4, 3, 4)); + scene.activeCamera = cam; + cam.getViewMatrix(); + scene.updateTransformMatrix(); + return { engine, scene }; +} + +describe("babylon bboxFeatures (OBB)", () => { + it("returns 15 world features for a unit box, center first at origin", () => { + const { scene, engine } = makeScene(); + const box = MeshBuilder.CreateBox("b", { size: 2 }, scene); + box.computeWorldMatrix(true); + const pts = bboxFeatures(box); + expect(pts).toHaveLength(15); + expect(pts[0]!.x).toBeCloseTo(0); + expect(pts[0]!.y).toBeCloseTo(0); + expect(pts[0]!.z).toBeCloseTo(0); + engine.dispose(); + }); + + it("rotates +X face center with the object (OBB not AABB)", () => { + const { scene, engine } = makeScene(); + const box = MeshBuilder.CreateBox("b", { size: 2 }, scene); + box.rotation.y = Math.PI / 4; + box.computeWorldMatrix(true); + const pts = bboxFeatures(box); + const fc = pts[9]!; // +X face center (same index as Three) + expect(Math.hypot(fc.x, fc.z)).toBeCloseTo(1, 4); + engine.dispose(); + }); + + it("returns [] for a transform node with no mesh", () => { + const { scene, engine } = makeScene(); + const tn = new TransformNode("t", scene); + expect(bboxFeatures(tn)).toEqual([]); + engine.dispose(); + }); +}); + +describe("babylon toScreen", () => { + it("projects world origin to viewport center", () => { + const { scene, engine } = makeScene(); + const [x, y] = toScreen(Vector3.Zero(), scene, 800, 600); + expect(x).toBeCloseTo(400, 0); + expect(y).toBeCloseTo(300, 0); + engine.dispose(); + }); +}); + +describe("babylon socketPoints", () => { + it("maps socket local position through the node world matrix", () => { + const { scene, engine } = makeScene(); + const box = MeshBuilder.CreateBox("b", { size: 2 }, scene); + box.position.set(5, 0, 0); + box.computeWorldMatrix(true); + const pts = socketPoints( + box, + [{ id: "s", name: "s", position: [0, 1, 0] }], + scene, + 800, + 600, + ); + expect(pts).toHaveLength(1); + expect(pts[0]!.world[0]).toBeCloseTo(5); + expect(pts[0]!.world[1]).toBeCloseTo(1); + engine.dispose(); + }); +}); +``` + +- [ ] **步骤 2:运行验证失败** + +运行:`pnpm test src/runtime/babylon/snap-features.test.ts` +预期:FAIL,模块不存在。 + +- [ ] **步骤 3:实现 snap-features.ts** + +```ts +// src/runtime/babylon/snap-features.ts +import { + Matrix, + Vector3, + type AbstractMesh, + type Node as BabylonNode, + type Scene, +} from "@babylonjs/core"; + +import type { SnapPoint } from "@/core/snap/nodes"; +import type { SocketPoint } from "@/core/snap/sockets"; +import type { Socket } from "@/core/scene/types"; + +/** Descendant meshes of a node, including the node itself when it is a mesh. */ +function meshesOf(node: BabylonNode): AbstractMesh[] { + const children = node.getChildMeshes(false); + const self = node as Partial; + // A Mesh has getBoundingInfo + is not already in getChildMeshes(false) of itself. + if (typeof self.getBoundingInfo === "function") { + return [self as AbstractMesh, ...children.filter((m) => m !== self)]; + } + return children; +} + +/** 15 OBB features (center + 8 corners + 6 face centers) in WORLD space; [] + * when the node has no mesh geometry. Mirrors three/snap-features.bboxFeatures: + * accumulate each descendant mesh's local box into the dragged node's local + * space, then transform the 15 local points by the node world matrix so they + * follow rotation (OBB, not world AABB). Index order MUST match the Three + * version for cross-engine node-align parity. */ +export function bboxFeatures(node: BabylonNode): Vector3[] { + const tn = node as BabylonNode & { computeWorldMatrix?: (force: boolean) => Matrix }; + if (typeof tn.computeWorldMatrix !== "function") return []; + tn.computeWorldMatrix(true); + const world = tn.getWorldMatrix(); + const invWorld = Matrix.Invert(world); + let min: Vector3 | null = null; + let max: Vector3 | null = null; + const expand = (p: Vector3) => { + if (!min || !max) { + min = p.clone(); + max = p.clone(); + } else { + min = Vector3.Minimize(min, p); + max = Vector3.Maximize(max, p); + } + }; + for (const mesh of meshesOf(node)) { + const bb = mesh.getBoundingInfo().boundingBox; + const lo = bb.minimum; + const hi = bb.maximum; + mesh.computeWorldMatrix(true); + // mesh-local → dragged-node-local = meshWorld · invNodeWorld (row-vector) + const rel = mesh.getWorldMatrix().multiply(invWorld); + for (const x of [lo.x, hi.x]) + for (const y of [lo.y, hi.y]) + for (const z of [lo.z, hi.z]) + expand(Vector3.TransformCoordinates(new Vector3(x, y, z), rel)); + } + if (!min || !max) return []; + const lmin: Vector3 = min; + const lmax: Vector3 = max; + const c = Vector3.Center(lmin, lmax); + const local = [ + c, + new Vector3(lmin.x, lmin.y, lmin.z), + new Vector3(lmin.x, lmin.y, lmax.z), + new Vector3(lmin.x, lmax.y, lmin.z), + new Vector3(lmin.x, lmax.y, lmax.z), + new Vector3(lmax.x, lmin.y, lmin.z), + new Vector3(lmax.x, lmin.y, lmax.z), + new Vector3(lmax.x, lmax.y, lmin.z), + new Vector3(lmax.x, lmax.y, lmax.z), + new Vector3(lmax.x, c.y, c.z), + new Vector3(lmin.x, c.y, c.z), + new Vector3(c.x, lmax.y, c.z), + new Vector3(c.x, lmin.y, c.z), + new Vector3(c.x, c.y, lmax.z), + new Vector3(c.x, c.y, lmin.z), + ]; + return local.map((p) => Vector3.TransformCoordinates(p, world)); +} + +/** World point → screen pixels. Caller ensures scene transform matrix is + * current (render loop updates it; tests call scene.updateTransformMatrix). */ +export function toScreen( + v: Vector3, + scene: Scene, + w: number, + h: number, +): [number, number] { + const p = Vector3.Project(v, Matrix.IdentityReadOnly, scene.getTransformMatrix(), { + x: 0, + y: 0, + width: w, + height: h, + }); + return [p.x, p.y]; +} + +export function featureSnapPoints( + node: BabylonNode, + scene: Scene, + w: number, + h: number, +): SnapPoint[] { + return bboxFeatures(node).map((v) => ({ + screen: toScreen(v, scene, w, h), + world: [v.x, v.y, v.z] as [number, number, number], + })); +} + +export function socketPoints( + node: BabylonNode, + sockets: readonly Socket[], + scene: Scene, + w: number, + h: number, +): SocketPoint[] { + if (sockets.length === 0) return []; + const tn = node as BabylonNode & { computeWorldMatrix?: (force: boolean) => Matrix }; + if (typeof tn.computeWorldMatrix === "function") tn.computeWorldMatrix(true); + const world = tn.getWorldMatrix(); + return sockets.map((s) => { + const wp = Vector3.TransformCoordinates( + new Vector3(s.position[0], s.position[1], s.position[2]), + world, + ); + return { + screen: toScreen(wp, scene, w, h), + world: [wp.x, wp.y, wp.z] as [number, number, number], + tag: s.tag, + }; + }); +} +``` + +> 实现后若 OBB 旋转测试不过(面心距离 ≠ 1),多半是 Babylon 矩阵乘序:改用 `invWorld.multiply(...)` 的顺序前先确认 `mesh.getWorldMatrix().multiply(invWorld)` 是「先 meshWorld 后 invNodeWorld」(Babylon 行向量约定)。测试是安全网。 + +- [ ] **步骤 4:运行验证通过** + +运行:`pnpm test src/runtime/babylon/snap-features.test.ts` +预期:PASS(4 个 describe 全过)。 + +- [ ] **步骤 5:Commit** + +```bash +git add src/runtime/babylon/snap-features.ts src/runtime/babylon/snap-features.test.ts +git commit -m "feat(babylon): snap feature projection (OBB + Vector3.Project)" +``` + +--- + +## 任务 5:Babylon transform-util(四元数归一)— TDD + +**文件:** + +- 创建:`src/runtime/babylon/transform-util.ts` +- 测试:`src/runtime/babylon/transform-util.test.ts` + +- [ ] **步骤 1:编写失败的测试** + +```ts +// src/runtime/babylon/transform-util.test.ts +import { describe, expect, it } from "vitest"; +import { NullEngine, Scene, TransformNode, Vector3, Quaternion } from "@babylonjs/core"; +import { captureTransform } from "./transform-util"; + +function scene() { + return new Scene(new NullEngine()); +} + +describe("babylon captureTransform", () => { + it("reads position/scaling and rotationQuaternion when present", () => { + const s = scene(); + const n = new TransformNode("n", s); + n.position.set(1, 2, 3); + n.scaling.set(2, 2, 2); + n.rotationQuaternion = new Quaternion(0, 0, 0, 1); + const t = captureTransform(n); + expect(t.position).toEqual([1, 2, 3]); + expect(t.scale).toEqual([2, 2, 2]); + expect(t.rotation).toEqual([0, 0, 0, 1]); + s.getEngine().dispose(); + }); + + it("falls back to Euler rotation when rotationQuaternion is null", () => { + const s = scene(); + const n = new TransformNode("n", s); + n.rotationQuaternion = null; + n.rotation.set(0, Math.PI / 2, 0); + const t = captureTransform(n); + const expected = Quaternion.FromEulerVector(new Vector3(0, Math.PI / 2, 0)); + expect(t.rotation[1]).toBeCloseTo(expected.y, 5); + expect(t.rotation[3]).toBeCloseTo(expected.w, 5); + s.getEngine().dispose(); + }); +}); +``` + +- [ ] **步骤 2:运行验证失败** + +运行:`pnpm test src/runtime/babylon/transform-util.test.ts` +预期:FAIL,模块不存在。 + +- [ ] **步骤 3:实现 transform-util.ts** + +```ts +// src/runtime/babylon/transform-util.ts +import { Quaternion, Vector3, type Node as BabylonNode } from "@babylonjs/core"; + +import type { Transform } from "@/core/scene/types"; + +/** Snapshot a Babylon node's transform. Gizmo rotation writes + * rotationQuaternion, but a node that has never been rotated may still carry a + * null rotationQuaternion + Euler rotation — fall back to converting that so a + * rotate-drag commit captures the real start/end. */ +export function captureTransform(node: BabylonNode): Transform { + const t = node as BabylonNode & { + position?: Vector3; + rotationQuaternion?: Quaternion | null; + rotation?: Vector3; + scaling?: Vector3; + }; + const pos = t.position ?? Vector3.Zero(); + const q = + t.rotationQuaternion ?? + (t.rotation ? Quaternion.FromEulerVector(t.rotation) : null); + const scl = t.scaling ?? new Vector3(1, 1, 1); + return { + position: [pos.x, pos.y, pos.z], + rotation: q ? [q.x, q.y, q.z, q.w] : [0, 0, 0, 1], + scale: [scl.x, scl.y, scl.z], + }; +} +``` + +- [ ] **步骤 4:运行验证通过** + +运行:`pnpm test src/runtime/babylon/transform-util.test.ts` +预期:PASS。 + +- [ ] **步骤 5:Commit** + +```bash +git add src/runtime/babylon/transform-util.ts src/runtime/babylon/transform-util.test.ts +git commit -m "feat(babylon): captureTransform with quaternion normalization" +``` + +--- + +## 任务 6:BabylonRenderHost 实现 gizmo + snap + +**文件:** + +- 修改:`src/runtime/babylon/render-host.ts`(替换 4 个 no-op stub + 加 GizmoManager 接线) +- 测试:`src/runtime/babylon/render-host.test.ts`(加 gizmo 接线断言) + +**关键 Babylon 行为**:`GizmoManager` 的 `positionGizmoEnabled=false` 会**销毁**该 gizmo(连同其 observable)。故每次 `setGizmoMode` 切模式后要**重新挂**活动 gizmo 的 observable(重建后 observable 为空,挂一次不会重复累积)。`attachedNode` 由 GizmoManager 跨模式切换保留。 + +- [ ] **步骤 1:加 import + 字段** + +`render-host.ts` 顶部 import 增补: + +```ts +import { + GizmoManager, + Quaternion, // (若 transform-util 已封装则不需要) + // …已有 ArcRotateCamera/Color3/Color4/Engine/HighlightLayer/Mesh/Vector3/AbstractEngine/Node +} from "@babylonjs/core"; +import { computeSnapOffset } from "@/core/snap/offset"; +import { transformsEqual } from "@/runtime/transform-util"; +import { captureTransform } from "./transform-util"; +import { featureSnapPoints, socketPoints } from "./snap-features"; +import type { GizmoMode } from "@/core/editor-types"; +import type { SnapNode, Transform } from ...; // (已有 GizmoMode/Transform/SnapNode import) +``` + +类字段(加在 highlight 附近): + +```ts + private gizmoManager: GizmoManager | null = null; + private gizmoMode: GizmoMode = "translate"; + private attachedNodeId: string | null = null; + private commitCb: ((id: string, prev: Transform, next: Transform) => void) | null = null; + private snapProvider: (() => SnapNode[]) | null = null; + private dragStart: Transform | null = null; + private cachedTargets: ReturnType = []; + private cachedSocketTargets: ReturnType = []; + private snapModifierDown = false; + private readonly onSnapPointer = (e: PointerEvent) => { + this.snapModifierDown = e.ctrlKey || e.metaKey; + }; +``` + +- [ ] **步骤 2:mount 里建 GizmoManager** + +在 `mount()` 末尾(highlight 之后)加: + +```ts +const gm = new GizmoManager(scene); +gm.usePointerToAttachGizmos = false; // selection drives attach, not clicks +gm.positionGizmoEnabled = false; +gm.rotationGizmoEnabled = false; +gm.scaleGizmoEnabled = false; +this.gizmoManager = gm; +window.addEventListener("pointermove", this.onSnapPointer, true); +window.addEventListener("pointerdown", this.onSnapPointer, true); +``` + +- [ ] **步骤 3:替换 4 个 no-op stub** + +```ts + setGizmoMode(mode: GizmoMode): void { + this.gizmoMode = mode; + const gm = this.gizmoManager; + if (!gm) return; + gm.positionGizmoEnabled = mode === "translate"; + gm.rotationGizmoEnabled = mode === "rotate"; + gm.scaleGizmoEnabled = mode === "scale"; + this.wireActiveGizmo(); + } + + setGizmoTarget(node_id: string | null, locked: boolean): void { + this.attachedNodeId = node_id && !locked ? node_id : null; + const gm = this.gizmoManager; + const adapter = this.adapterInstance; + if (!gm || !adapter) return; + const obj = this.attachedNodeId + ? (adapter.getRuntimeObject(this.attachedNodeId) as BabylonNode | undefined) + : undefined; + gm.attachToNode(obj ?? null); + } + + onTransformCommit(cb: (id: string, prev: Transform, next: Transform) => void): void { + this.commitCb = cb; + } + + setSnapProvider(provider: () => SnapNode[]): void { + this.snapProvider = provider; + } +``` + +- [ ] **步骤 4:加私有 helper(wireActiveGizmo + 三个 drag handler)** + +```ts + /** Re-attach drag observers to the currently-enabled gizmo. Called after + * setGizmoMode because GizmoManager disposes a gizmo (and its observers) + * when its *Enabled flag goes false; the freshly-recreated gizmo starts + * with empty observables, so adding once here never double-fires. */ + private wireActiveGizmo(): void { + const gm = this.gizmoManager; + if (!gm) return; + const g = + this.gizmoMode === "translate" + ? gm.gizmos.positionGizmo + : this.gizmoMode === "rotate" + ? gm.gizmos.rotationGizmo + : gm.gizmos.scaleGizmo; + if (!g) return; + g.onDragStartObservable.add(() => this.onGizmoDragStart()); + g.onDragEndObservable.add(() => this.onGizmoDragEnd()); + if (this.gizmoMode === "translate") { + gm.gizmos.positionGizmo?.onDragObservable.add(() => this.onGizmoDrag()); + } + } + + private draggedNode(): BabylonNode | undefined { + if (!this.attachedNodeId) return undefined; + return this.adapterInstance?.getRuntimeObject(this.attachedNodeId) as + | BabylonNode + | undefined; + } + + private viewportSize(): [number, number] { + const eng = this.babylonEngine; + return [eng?.getRenderWidth() ?? 0, eng?.getRenderHeight() ?? 0]; + } + + private onGizmoDragStart(): void { + const node = this.draggedNode(); + if (!node) return; + this.dragStart = captureTransform(node); + this.highlight?.removeAllMeshes(); // hide selection outline during drag + this.cachedTargets = []; + this.cachedSocketTargets = []; + if (this.gizmoMode !== "translate") return; // only translate snaps + const scene = this.adapterInstance?.scene; + const adapter = this.adapterInstance; + if (!scene || !adapter) return; + const [w, h] = this.viewportSize(); + for (const n of this.snapProvider?.() ?? []) { + if (n.id === this.attachedNodeId || !n.visible || n.type === "helper") continue; + const tobj = adapter.getRuntimeObject(n.id) as BabylonNode | undefined; + if (!tobj) continue; + this.cachedTargets.push(...featureSnapPoints(tobj, scene, w, h)); + this.cachedSocketTargets.push(...socketPoints(tobj, n.sockets, scene, w, h)); + } + } + + private onGizmoDrag(): void { + const node = this.draggedNode() as (BabylonNode & { position: Vector3 }) | undefined; + if (!node || this.gizmoMode !== "translate" || !this.snapModifierDown) return; + const scene = this.adapterInstance?.scene; + if (!scene) return; + const [w, h] = this.viewportSize(); + const draggedNode = this.snapProvider?.().find((n) => n.id === this.attachedNodeId); + const hasSockets = (draggedNode?.sockets ?? []).some((s) => s.tag); + const offset = computeSnapOffset({ + currentPos: [node.position.x, node.position.y, node.position.z], + draggedFeatures: featureSnapPoints(node, scene, w, h), + draggedSockets: socketPoints(node, draggedNode?.sockets ?? [], scene, w, h), + hasSockets, + targetFeatures: this.cachedTargets, + targetSockets: this.cachedSocketTargets, + }); + if (offset) { + node.position.set( + node.position.x + offset[0], + node.position.y + offset[1], + node.position.z + offset[2], + ); + } + } + + private onGizmoDragEnd(): void { + const node = this.draggedNode(); + const start = this.dragStart; + this.dragStart = null; + // Restore the selection outline that onGizmoDragStart hid. + this.setSelection(this.attachedNodeId); + if (!node || !start) return; + const nodeId = this.attachedNodeId; + if (!nodeId) return; + const end = captureTransform(node); + if (transformsEqual(start, end)) return; + this.commitCb?.(nodeId, start, end); + } +``` + +- [ ] **步骤 5:dispose 释放 gizmo + 监听** + +`dispose()` 里(stop 之后、highlight 之前或附近)加: + +```ts +window.removeEventListener("pointermove", this.onSnapPointer, true); +window.removeEventListener("pointerdown", this.onSnapPointer, true); +this.gizmoManager?.dispose(); +this.gizmoManager = null; +this.commitCb = null; +this.snapProvider = null; +``` + +- [ ] **步骤 6:加接线测试(headless)** + +在 `src/runtime/babylon/render-host.test.ts` 加(用现有 NullEngine host fixture 范式): + +```ts +it("setGizmoMode enables exactly the matching gizmo", () => { + // mount host with NullEngine (see existing tests for the createEngine seam) + host.setGizmoMode("rotate"); + // access gizmoManager via a test getter (add one) or assert via behavior +}); +it("setGizmoTarget attaches the node; locked/null detaches", () => { + /* … */ +}); +``` + +> 实现注意:若 render-host.test.ts 现有 fixture 没暴露 gizmoManager,加一个 test-only getter `get gizmoManagerForTest()`(仿现有 `selectionLayer` test getter),断言 `positionGizmoEnabled`/`attachedNode`。保持与既有测试风格一致。 + +- [ ] **步骤 7:验证** + +运行:`pnpm typecheck && pnpm lint && pnpm test src/runtime/babylon/` +预期:typecheck/lint 净;babylon 测试(adapter/render-host/snap-features/transform-util/headless)全绿。 + +- [ ] **步骤 8:Commit** + +```bash +git add src/runtime/babylon/render-host.ts src/runtime/babylon/render-host.test.ts +git commit -m "feat(babylon): GizmoManager gizmo + snap (translate/rotate/scale, 3-layer snap)" +``` + +--- + +## 任务 7:BabylonViewport gizmo 接线 + +**文件:** + +- 修改:`src/ui/viewport/BabylonViewport.tsx` + +镜像 ThreeViewport 薄壳的 gizmo 部分。socket markers / play / focus / asset drop **不接**(B4)。 + +- [ ] **步骤 1:加 import** + +```ts +import { SetNodeTransformCommand } from "@/core/command/commands/set-node-transform"; +import { isEffectivelyLocked } from "@/core/scene/policy"; +import { executeCommand } from "@/services/command-history"; +import type { SnapNode } from "@/runtime/render-host"; +``` + +(已有:useSceneStore / useUIStore / BabylonRenderHost / diffSceneNodes 等。) + +- [ ] **步骤 2:mount effect 内接线(host.mount 之后、host.start 之前)** + +```ts +host.onTransformCommit((id, prev, next) => + executeCommand( + new SetNodeTransformCommand({ node_id: id, transform: next, prev_transform: prev }), + ), +); +host.setSnapProvider(() => { + const nodes = useSceneStore.getState().project?.scene.nodes ?? {}; + return Object.values(nodes).map((n) => ({ + id: n.id, + sockets: n.sockets ?? [], + visible: n.visible, + type: n.type, + })) satisfies SnapNode[]; +}); +const syncGizmoTarget = (id: string | null) => { + const node = id ? useSceneStore.getState().project?.scene.nodes[id] : undefined; + host.setGizmoTarget(id, node ? isEffectivelyLocked(node) : false); +}; +// initial gizmo state +host.setGizmoMode(useUIStore.getState().gizmoMode); +syncGizmoTarget(useUIStore.getState().selectedNodeId); +``` + +- [ ] **步骤 3:扩 UI 订阅** + +现有 BabylonViewport 的 `useUIStore.subscribe` 里 selectedNodeId 变化已调 `host.setSelection`;加: + +```ts +const unsubscribeUI = useUIStore.subscribe((state, prev) => { + if (state.selectedNodeId !== prev.selectedNodeId) { + host.setSelection(state.selectedNodeId); + syncGizmoTarget(state.selectedNodeId); + } + if (state.gizmoMode !== prev.gizmoMode) { + host.setGizmoMode(state.gizmoMode); + } +}); +``` + +(若原来是只订 selectedNodeId 的写法,替换成上面这个含 gizmoMode 的。) + +- [ ] **步骤 4:seedScene/applyDiff 后的 gizmo 重放(与 B2 高亮重放一致)** + +B2 在 applyDiff 后重放 `syncSelection`(host.setSelection)。同理,diff 重建后 gizmo target 也要重挂——在 applyDiff 的 `syncSelection()` 调用处旁补 `syncGizmoTarget(useUIStore.getState().selectedNodeId)`(同 id 重建后重新 attach gizmo 到新节点实例)。 + +- [ ] **步骤 5:验证** + +运行:`pnpm typecheck && pnpm lint && pnpm test` +预期:全绿(含既有 BabylonViewport 测试;若测试断言旧订阅结构,最小适配并说明)。注意 eslint react-hooks(勿在 effect body setState)。 + +- [ ] **步骤 6:Commit** + +```bash +git add src/ui/viewport/BabylonViewport.tsx +git commit -m "feat(viewport): wire Babylon gizmo (target/mode/commit/snap-provider)" +``` + +--- + +## 任务 8:更新 roadmap + +**文件:** + +- 修改:`docs/roadmap.md` + +- [ ] **步骤 1:勾选 B3a/B3b + B3 收口** + +把 B3a 行 `- [ ]` 改 `- [x]` 并补 sha;B3b 行 `- [ ]` 改 `- [x]`;B3 父行标完成。(B3a 已本地合并 sha 56c0b05;B3b 完成后补。) + +- [ ] **步骤 2:Commit** + +```bash +git add docs/roadmap.md +git commit -m "docs(roadmap): B3 gizmo+snap cross-engine complete (B3a + B3b)" +``` + +--- + +## 任务 9:全量验证 + visual smoke + +- [ ] **步骤 1:自动化全绿** + +运行:`pnpm typecheck && pnpm lint && pnpm test` +预期:全 PASS。`grep -rn "isEngineEditingCapable" src/` 为空。 + +- [ ] **步骤 2:visual smoke(`pnpm tauri dev`,切到 Babylon 模式)** + +1. 选中节点 → gizmo 出现(之前 Babylon 拖不动,现在能拖) +2. T/R/S 三模式切换 + 拖动改 transform +3. 按 Ctrl/Cmd 拖动:grid + node-align + socket 三层吸附(socket 无视觉标记是预期,B4 补) +4. 一次连续拖拽 = 一条 undo(Cmd+Z 一步回退) +5. locked 节点:只描边、gizmo 不挂 +6. 拖拽中描边消失、松手恢复 +7. gizmo pill 在 Babylon 模式**不再灰**;play / F focus / 资源拖入在 Babylon **仍灰/无效**(B4) +8. 切回 Three 模式:gizmo/snap/描边/play/focus/drop 全部零回归 +9. 来回切引擎选中态正确;全程 console 零 error/warn + +- [ ] **步骤 3:偏差则修复重跑**;全过后进入收尾(finishing-a-development-branch)。 + +--- + +## 自检记录 + +- **规格覆盖**:§2 能力门→任务 3;§3 同;§4 GizmoManager→任务 6;§5 snap-features→任务 4;§6 transform-util→任务 5;§7 viewport→任务 7;§8 测试→任务 4/5/6 单测 + 任务 9 smoke;§10 交付物全覆盖。**新增**:computeSnapOffset 中立化(任务 1)——规格 §5 隐含「喂同一 computeSnapOffset」但它原住 three/,移到 core/snap 才能让 babylon 不依赖 three;transformsEqual 中立化(任务 2)规格 §6 已列。 +- **占位符**:snap-features/render-host 给完整代码;Babylon 矩阵乘序有不确定性,靠任务 4 的 OBB 旋转单测兜底(计划已标注)。 +- **类型一致**:`computeSnapOffset` 参数对象形状(core/snap/offset 定义)在任务 4/6 一致;`SnapNode` 在任务 7 produce、任务 6 consume 一致;`captureTransform(node)`/`transformsEqual` 在任务 5/2 定义、任务 6 用一致;`engineCapabilities` 返回字段在任务 3 定义、5 处调用一致。 +- **顺序依赖**:1(offset)→4/6;2(transformsEqual)→6;3 独立→7 需要;4/5→6→7。任务编号即执行序。 From 94943903bb01c31a91765b2666bce45a6448d1ef Mon Sep 17 00:00:00 2001 From: longyi-xw <2691049525@qq.com> Date: Wed, 17 Jun 2026 10:28:26 +0800 Subject: [PATCH 03/14] refactor(snap): move computeSnapOffset to core/snap (engine-neutral home) --- src/core/snap/offset.ts | 31 ++++++++++++++++++++++ src/runtime/three/render-host.ts | 3 ++- src/runtime/three/snap-features.test.ts | 3 ++- src/runtime/three/snap-features.ts | 35 ++----------------------- 4 files changed, 37 insertions(+), 35 deletions(-) create mode 100644 src/core/snap/offset.ts diff --git a/src/core/snap/offset.ts b/src/core/snap/offset.ts new file mode 100644 index 0000000..2537ff1 --- /dev/null +++ b/src/core/snap/offset.ts @@ -0,0 +1,31 @@ +import { snapTranslation } from "./grid"; +import { SNAP_PIXELS, snapToNodes, type SnapPoint } from "./nodes"; +import { snapToSockets, type SocketPoint } from "./sockets"; + +type Vec3 = [number, number, number]; + +/** + * Pure snap priority chain: socket-align → node-align (only when the dragged + * node has no tagged socket) → grid fallback. Returns the world-space offset to + * apply to the dragged object's position. Engine-neutral — both ThreeRenderHost + * and BabylonRenderHost feed it engine-specific SnapPoint[]/SocketPoint[]. The + * caller gates on translate-mode + modifier before calling. + */ +export function computeSnapOffset(args: { + currentPos: Vec3; + draggedFeatures: SnapPoint[]; + draggedSockets: SocketPoint[]; + hasSockets: boolean; + targetFeatures: SnapPoint[]; + targetSockets: SocketPoint[]; +}): Vec3 | null { + const { currentPos, draggedFeatures, draggedSockets, hasSockets } = args; + const socketOffset = snapToSockets(draggedSockets, args.targetSockets, SNAP_PIXELS); + if (socketOffset) return socketOffset; + if (!hasSockets) { + const offset = snapToNodes(draggedFeatures, args.targetFeatures, SNAP_PIXELS); + if (offset) return offset; + } + const [gx, gy, gz] = snapTranslation(currentPos); + return [gx - currentPos[0], gy - currentPos[1], gz - currentPos[2]]; +} diff --git a/src/runtime/three/render-host.ts b/src/runtime/three/render-host.ts index 20df187..7079357 100644 --- a/src/runtime/three/render-host.ts +++ b/src/runtime/three/render-host.ts @@ -11,7 +11,8 @@ import type { Transform } from "@/core/scene/types"; import type { IRenderHost, SnapNode } from "@/runtime/render-host"; import { ThreeAdapter } from "@/runtime/three/adapter"; -import { computeSnapOffset, featureSnapPoints, socketPoints } from "./snap-features"; +import { computeSnapOffset } from "@/core/snap/offset"; +import { featureSnapPoints, socketPoints } from "./snap-features"; import { captureTransform, transformsEqual } from "./transform-util"; /** diff --git a/src/runtime/three/snap-features.test.ts b/src/runtime/three/snap-features.test.ts index 5f49d86..fd2ac38 100644 --- a/src/runtime/three/snap-features.test.ts +++ b/src/runtime/three/snap-features.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import * as THREE from "three"; -import { bboxFeatures, computeSnapOffset } from "./snap-features"; +import { bboxFeatures } from "./snap-features"; +import { computeSnapOffset } from "@/core/snap/offset"; import type { SnapPoint } from "@/core/snap/nodes"; describe("bboxFeatures (OBB)", () => { diff --git a/src/runtime/three/snap-features.ts b/src/runtime/three/snap-features.ts index 9090696..c5a7de5 100644 --- a/src/runtime/three/snap-features.ts +++ b/src/runtime/three/snap-features.ts @@ -1,8 +1,7 @@ import * as THREE from "three"; -import { snapTranslation } from "@/core/snap/grid"; -import { SNAP_PIXELS, snapToNodes, type SnapPoint } from "@/core/snap/nodes"; -import { snapToSockets, type SocketPoint } from "@/core/snap/sockets"; +import type { SnapPoint } from "@/core/snap/nodes"; +import type { SocketPoint } from "@/core/snap/sockets"; import type { Socket } from "@/core/scene/types"; /** 15 个 bbox 特征(中心 + 8 角 + 6 面中心)的世界坐标;无几何返回 []。 @@ -102,33 +101,3 @@ export function socketPoints( }; }); } - -type Vec3 = [number, number, number]; - -/** - * Pure snap priority chain extracted from ThreeViewport.snapDraggedObject: - * socket-align → node-align (only when the dragged node has no tagged socket) - * → grid fallback. Returns the world-space offset to apply to the dragged - * object's position, or null only when there is genuinely nothing to do - * (never happens on the grid path — grid always returns an offset, possibly - * zero). Behaviour-equivalent to the in-component version; the caller still - * gates on translate-mode + modifier before calling. - */ -export function computeSnapOffset(args: { - currentPos: Vec3; - draggedFeatures: SnapPoint[]; - draggedSockets: SocketPoint[]; - hasSockets: boolean; - targetFeatures: SnapPoint[]; - targetSockets: SocketPoint[]; -}): Vec3 | null { - const { currentPos, draggedFeatures, draggedSockets, hasSockets } = args; - const socketOffset = snapToSockets(draggedSockets, args.targetSockets, SNAP_PIXELS); - if (socketOffset) return socketOffset; - if (!hasSockets) { - const offset = snapToNodes(draggedFeatures, args.targetFeatures, SNAP_PIXELS); - if (offset) return offset; - } - const [gx, gy, gz] = snapTranslation(currentPos); - return [gx - currentPos[0], gy - currentPos[1], gz - currentPos[2]]; -} From f3bd2e7834dd0adc045fbbf67618c638a0e036b0 Mon Sep 17 00:00:00 2001 From: longyi-xw <2691049525@qq.com> Date: Wed, 17 Jun 2026 10:30:38 +0800 Subject: [PATCH 04/14] refactor(transform): move transformsEqual to runtime (engine-neutral, shared by hosts) --- src/runtime/three/render-host.ts | 3 ++- src/runtime/three/transform-util.test.ts | 15 +-------------- src/runtime/three/transform-util.ts | 15 --------------- src/runtime/transform-util.test.ts | 16 ++++++++++++++++ src/runtime/transform-util.ts | 18 ++++++++++++++++++ 5 files changed, 37 insertions(+), 30 deletions(-) create mode 100644 src/runtime/transform-util.test.ts create mode 100644 src/runtime/transform-util.ts diff --git a/src/runtime/three/render-host.ts b/src/runtime/three/render-host.ts index 7079357..037398e 100644 --- a/src/runtime/three/render-host.ts +++ b/src/runtime/three/render-host.ts @@ -13,7 +13,8 @@ import { ThreeAdapter } from "@/runtime/three/adapter"; import { computeSnapOffset } from "@/core/snap/offset"; import { featureSnapPoints, socketPoints } from "./snap-features"; -import { captureTransform, transformsEqual } from "./transform-util"; +import { captureTransform } from "./transform-util"; +import { transformsEqual } from "@/runtime/transform-util"; /** * Three.js render host (v1.0 B3a) — owns the WebGLRenderer, post-processing diff --git a/src/runtime/three/transform-util.test.ts b/src/runtime/three/transform-util.test.ts index 23661a1..a2f140c 100644 --- a/src/runtime/three/transform-util.test.ts +++ b/src/runtime/three/transform-util.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from "vitest"; import * as THREE from "three"; -import type { Transform } from "@/core/scene/types"; -import { captureTransform, transformsEqual } from "./transform-util"; +import { captureTransform } from "./transform-util"; describe("captureTransform", () => { it("snapshots position/quaternion/scale arrays", () => { @@ -14,15 +13,3 @@ describe("captureTransform", () => { expect(t.rotation).toHaveLength(4); }); }); - -describe("transformsEqual", () => { - it("true for identical, false for any differing component", () => { - const a: Transform = { - position: [0, 0, 0], - rotation: [0, 0, 0, 1], - scale: [1, 1, 1], - }; - expect(transformsEqual(a, { ...a })).toBe(true); - expect(transformsEqual(a, { ...a, position: [0, 1, 0] })).toBe(false); - }); -}); diff --git a/src/runtime/three/transform-util.ts b/src/runtime/three/transform-util.ts index 4a62e35..7d99c7a 100644 --- a/src/runtime/three/transform-util.ts +++ b/src/runtime/three/transform-util.ts @@ -9,18 +9,3 @@ export function captureTransform(obj: THREE.Object3D): Transform { scale: [obj.scale.x, obj.scale.y, obj.scale.z], }; } - -export function transformsEqual(a: Transform, b: Transform): boolean { - return ( - a.position[0] === b.position[0] && - a.position[1] === b.position[1] && - a.position[2] === b.position[2] && - a.rotation[0] === b.rotation[0] && - a.rotation[1] === b.rotation[1] && - a.rotation[2] === b.rotation[2] && - a.rotation[3] === b.rotation[3] && - a.scale[0] === b.scale[0] && - a.scale[1] === b.scale[1] && - a.scale[2] === b.scale[2] - ); -} diff --git a/src/runtime/transform-util.test.ts b/src/runtime/transform-util.test.ts new file mode 100644 index 0000000..2ddbe17 --- /dev/null +++ b/src/runtime/transform-util.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { transformsEqual } from "./transform-util"; +import type { Transform } from "@/core/scene/types"; + +describe("transformsEqual", () => { + it("true for identical, false for any differing component", () => { + const a: Transform = { + position: [0, 0, 0], + rotation: [0, 0, 0, 1], + scale: [1, 1, 1], + }; + expect(transformsEqual(a, { ...a })).toBe(true); + expect(transformsEqual(a, { ...a, position: [0, 1, 0] })).toBe(false); + expect(transformsEqual(a, { ...a, rotation: [0, 0, 1, 0] })).toBe(false); + }); +}); diff --git a/src/runtime/transform-util.ts b/src/runtime/transform-util.ts new file mode 100644 index 0000000..398eadf --- /dev/null +++ b/src/runtime/transform-util.ts @@ -0,0 +1,18 @@ +import type { Transform } from "@/core/scene/types"; + +/** Engine-neutral exact transform equality (component-wise). Used by both + * render hosts to skip a no-op gizmo drag commit. */ +export function transformsEqual(a: Transform, b: Transform): boolean { + return ( + a.position[0] === b.position[0] && + a.position[1] === b.position[1] && + a.position[2] === b.position[2] && + a.rotation[0] === b.rotation[0] && + a.rotation[1] === b.rotation[1] && + a.rotation[2] === b.rotation[2] && + a.rotation[3] === b.rotation[3] && + a.scale[0] === b.scale[0] && + a.scale[1] === b.scale[1] && + a.scale[2] === b.scale[2] + ); +} From 6cf5200cbd614aa906dc5fae89aa360bbff3a2e7 Mon Sep 17 00:00:00 2001 From: longyi-xw <2691049525@qq.com> Date: Wed, 17 Jun 2026 10:33:40 +0800 Subject: [PATCH 05/14] feat(render-host): split editing capability into per-feature flags (enable Babylon gizmo) --- src/runtime/render-host.test.ts | 19 ++++++++++++------- src/runtime/render-host.ts | 25 ++++++++++++++++++------- src/services/library/asset-drag.ts | 4 ++-- src/ui/viewport/BabylonViewport.tsx | 6 +++--- src/ui/viewport/PlayButton.tsx | 4 ++-- src/ui/viewport/use-editor-shortcuts.ts | 6 +++--- src/ui/views/EditorView.tsx | 4 ++-- 7 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/runtime/render-host.test.ts b/src/runtime/render-host.test.ts index 988b268..5f481e4 100644 --- a/src/runtime/render-host.test.ts +++ b/src/runtime/render-host.test.ts @@ -1,13 +1,18 @@ import { describe, expect, it } from "vitest"; -import { isEngineEditingCapable } from "./render-host"; +import { engineCapabilities } from "./render-host"; -describe("isEngineEditingCapable", () => { - it("three.js viewport supports editing interactions", () => { - expect(isEngineEditingCapable("three.js")).toBe(true); +describe("engineCapabilities", () => { + it("enables gizmo on both engines (B3b)", () => { + expect(engineCapabilities("three.js").gizmo).toBe(true); + expect(engineCapabilities("babylon.js").gizmo).toBe(true); }); - - it("babylon.js viewport is view-only in B1", () => { - expect(isEngineEditingCapable("babylon.js")).toBe(false); + it("keeps play/focus/assetDrop Three-only (B4)", () => { + const b = engineCapabilities("babylon.js"); + expect(b.play).toBe(false); + expect(b.focus).toBe(false); + expect(b.assetDrop).toBe(false); + const t = engineCapabilities("three.js"); + expect(t.play && t.focus && t.assetDrop).toBe(true); }); }); diff --git a/src/runtime/render-host.ts b/src/runtime/render-host.ts index 90a5c8c..1ebdeb4 100644 --- a/src/runtime/render-host.ts +++ b/src/runtime/render-host.ts @@ -54,12 +54,23 @@ export interface IRenderHost { } /** - * Capability gate: only the Three viewport supports editing interactions - * (gizmo / play / drop / focus). Picking + selection highlight are cross-engine - * since B2 (viewport-internal, not gated here). Every UI surface that disables - * itself in Babylon mode reads this single helper, so B3/B4 flip capabilities - * in one place instead of hunting scattered engine === "..." checks. + * Per-engine editing capability flags. B1/B2 gated everything behind a single + * "three-only" boolean; B3b splits it so Babylon can enable the gizmo while + * play / focus / asset-drop stay Three-only until B4 implements them. B4 flips + * the remaining flags here in one place. */ -export function isEngineEditingCapable(engine: ViewportEngine): boolean { - return engine === "three.js"; +export interface EngineCapabilities { + /** Transform gizmo + snap (B3b: both engines). */ + gizmo: boolean; + /** Play/pause behavior preview (B4: Three-only). */ + play: boolean; + /** F-to-focus camera (B4: Three-only). */ + focus: boolean; + /** Drag library assets into the viewport (B4: Three-only). */ + assetDrop: boolean; +} + +export function engineCapabilities(engine: ViewportEngine): EngineCapabilities { + const three = engine === "three.js"; + return { gizmo: true, play: three, focus: three, assetDrop: three }; } diff --git a/src/services/library/asset-drag.ts b/src/services/library/asset-drag.ts index 9ce1e01..2b427b0 100644 --- a/src/services/library/asset-drag.ts +++ b/src/services/library/asset-drag.ts @@ -1,4 +1,4 @@ -import { isEngineEditingCapable } from "@/runtime/render-host"; +import { engineCapabilities } from "@/runtime/render-host"; import { useUIStore } from "@/services/ui/store"; /** @@ -19,7 +19,7 @@ let onUp: (() => void) | null = null; export function beginAssetDrag(id: string, clientX: number, clientY: number): void { // B1: the Babylon viewport has no drop handler (raycastGroundPoint lands in // B4) — without a window-pointerup consumer the drag flag would leak. - if (!isEngineEditingCapable(useUIStore.getState().viewportEngine)) return; + if (!engineCapabilities(useUIStore.getState().viewportEngine).assetDrop) return; teardown(); // drop any stale candidate before starting a new one candidate = { id, x: clientX, y: clientY }; onMove = (e) => { diff --git a/src/ui/viewport/BabylonViewport.tsx b/src/ui/viewport/BabylonViewport.tsx index c9e7775..1bf5588 100644 --- a/src/ui/viewport/BabylonViewport.tsx +++ b/src/ui/viewport/BabylonViewport.tsx @@ -14,9 +14,9 @@ import { diffSceneNodes, EMPTY_SCENE_GRAPH, type SceneDiff } from "./scene-diff" * - Mount effect re-runs only when the active project id changes; node * edits flow through a useSceneStore subscription → diffSceneNodes → * adapter.syncNode, so the canvas / camera state survive edits. - * - No gizmo / play / drop / focus — those are B3/B4; picking + selection - * highlight landed in B2. The surrounding UI disables itself via - * isEngineEditingCapable. + * - No play / drop / focus — those are B4; gizmo landed in B3b; picking + + * selection highlight landed in B2. The surrounding UI disables itself via + * engineCapabilities. * Per-node sync failures warn + skip (one unbuildable node — e.g. a helper, * which has no Babylon builder yet — must not kill the whole viewport). */ diff --git a/src/ui/viewport/PlayButton.tsx b/src/ui/viewport/PlayButton.tsx index 4536809..16fbe6a 100644 --- a/src/ui/viewport/PlayButton.tsx +++ b/src/ui/viewport/PlayButton.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; -import { isEngineEditingCapable } from "@/runtime/render-host"; +import { engineCapabilities } from "@/runtime/render-host"; import { useUIStore } from "@/services/ui/store"; export function PlayButton() { @@ -9,7 +9,7 @@ export function PlayButton() { const setPlayState = useUIStore((s) => s.setPlayState); const viewportEngine = useUIStore((s) => s.viewportEngine); const isPlay = playState === "play"; - const editingCapable = isEngineEditingCapable(viewportEngine); + const editingCapable = engineCapabilities(viewportEngine).play; return (