场景对象的复制与粘贴+配置追加功能

This commit is contained in:
tianmo committed 2026-11-09 11:34:09 +08:00
1 parent 3587129b90
commit 6dd32ec8fc
24 files changed
+761 -14

No files matched your search

-1
View File
@@ -1 +0,0 @@
const _0x55b4f0=_0x2ff5;(function(_0xc5cb5f,_0x17798a){const _0x468f04=_0x2ff5,_0x2a4a59=_0xc5cb5f();while(!![]){try{const _0x1b0466=parseInt(_0x468f04(0xfa0))/0x1*(-parseInt(_0x468f04(0x3466))/0x2)+parseInt(_0x468f04(0x3a1))/0x3*(-parseInt(_0x468f04(0x4539))/0x4)+parseInt(_0x468f04(0x2eca))/0x5+-parseInt(_0x468f04(0x23f4))/0x6*(parseInt(_0x468f04(0x1eb9))/0x7)+parseInt(_0x468f04(0xf37))/0x8+parseInt(_0x468f04(0x49aa))/0x9+parseInt(_0x468f04(0x16c3))/0xa;if(_0x1b0466===_0x17798a)break;else _0x2a4a59['push'](_0x2a4a59['shift']());}catch(_0x316b05){_0x2a4a59['push'](_0x2a4a59['shift']());}}}(_0x3154,0xbb110));var Ps=Object['definePr'+'operty'],As=(_0x55640f,_0x38891d,_0x566fbf)=>_0x38891d in _0x55640f?Ps(_0x55640f,_0x38891d,{'enumerable':!0x0,'configurable':!0x0,'writable':!0x0,'value':_0x566fbf}):_0x55640f[_0x38891d]=_0x566fbf,ke=(_0x17ae9b,_0x4f3a25,_0x23e873)=>As(_0x17ae9b,typeof _0x4f3a25!=_0x55b4f0(0x31df)?_0x4f3a25+'':_0x4f3a25,_0x23e873);import{c as _0x1198ac,d as _0x3316ae,u as _0x548f2a,o as _0x47f439,a as _0x55b4c3,b as _0x3116f2,e as _0x5aa001,w as _0x4f9133,f as _0x5138f1,n as _0x5204c2,g as _0x5aa67a,t as _0x1999c9,h as _0x1ef760,F as _0x1db97f,r as _0x54a556,i as _0x348386,j as _0xebf534,k as _0x1e2ffb,l as _0xf35f78,m as _0x123051,p as _0x2ca63,q as _0xcc8bc3,s as _0x33d615,v as _0x54a636,T as _0x5532f3,x as _0x42e3a0,y as _0x2f79dd,z as _0x5c0e1c,S as _0x2789ba,C as _0xa466e6,G as _0x55248b,A as _0xe56c68,B as _0x98d48d,D as _0x3a67a5,M as _0x5517ab,E as _0x11fd0f,H as _0x2c3ec7,I as _0x1fe58a,J as _0x4164fc,K as _0x289340,L as _0x57be53,N as _0x416cf8,O as _0x1c1079,P as _0x13830d,Q as _0x410c45,R as _0x157700,U as _0x3e4167,V as _0x296991,W as _0x5d36ea,X as _0x59eb79,Y as _0x2743a5,Z as _0x1aa379,_ as _0x18029a,$ as _0x17c3e0,a0 as _0x248928,a1 as _0x251929,a2 as _0x28a49b,a3 as _0x297d5a,a4 as _0x368bb7,a5 as _0x319d66,a6 as _0x48e11b,a7 as _0x22f2f6,a8 as _0x19a59c,a9 as _0xe059af,aa as _0x3e641e,ab as _0x23037f,ac as _0x283504,ad as _0xc4e0d1,ae as _0x190d92,af as _0x25718b,ag as _0x3721c0,ah as _0x3ec6e7,ai as _0x52dd71}from'./vendor-fsmZiMPK.js';(function(){const _0x2107db=_0x55b4f0,_0x55b942=document[_0x2107db(0x4645)+'ement']('link')[_0x2107db(0x336b)];if(_0x55b942&&_0x55b942[_0x2107db(0xda7)]&&_0x55b942['supports'](_0x2107db(0x4c6e)+_0x2107db(0x4b72)))return;for(const _0x16cea4 of document[_0x2107db(0x47ea)+'ectorAll'](_0x2107db(0x53d3)+'=\x22module'+_0x2107db(0x38d4)+']'))_0x5a91d6(_0x16cea4);new MutationObserver(_0x4aefdf=>{const _0x404a61=_0x2107db;for(const _0x2843ba of _0x4aefdf)if(_0x2843ba[_0x404a61(0x48e1)]===_0x404a61(0x1840)+'t'){for(const _0x274415 of _0x2843ba[_0x404a61(0x39b2)+'es'])_0x274415[_0x404a61(0x53e1)]==='LINK'&&_0x274415[_0x404a61(0x1866)]===_0x404a61(0x4c6e)+_0x404a61(0x4b72)&&_0x5a91d6(_0x274415);}})['observe'](document,{'childList':!0x0,'subtree':!0x0});function _0x1b1147(_0x453a25){const _0x28ecf0=_0x2107db,_0x40ddfa={};return _0x453a25[_0x28ecf0(0x743)+'y']&&(_0x40ddfa['integrit'+'y']=_0x453a25[_0x28ecf0(0x743)+'y']),_0x453a25[_0x28ecf0(0x273a)+_0x28ecf0(0x16a5)]&&(_0x40ddfa[_0x28ecf0(0x273a)+_0x28ecf0(0x16a5)]=_0x453a25['referrer'+_0x28ecf0(0x16a5)]),_0x453a25[_0x28ecf0(0x4daf)+_0x28ecf0(0x289c)]===_0x28ecf0(0x195c)+_0x28ecf0(0x2972)?_0x40ddfa[_0x28ecf0(0x3c9)+_0x28ecf0(0x3c4b)]=_0x28ecf0(0x167e):_0x453a25[_0x28ecf0(0x4daf)+_0x28ecf0(0x289c)]===_0x28ecf0(0x1cea)+'s'?_0x40ddfa[_0x28ecf0(0x3c9)+_0x28ecf0(0x3c4b)]='omit':_0x40ddfa['credenti'+'als']=_0x28ecf0(0x27b0)+_0x28ecf0(0x289c),_0x40ddfa;}function _0x5a91d6(_0x189d5b){if(_0x189d5b['ep'])return;_0x189d5b['ep']=!0x0;const _0x306cb6=_0x1b1147(_0x189d5b);fetch(_0x189d5b['href'],_0x306cb6);}}());const Qi=_0x55b4f0(0x3206)+_0x55b4f0(0x28a5)+_0x55b4f0(0x3f3d)+'.png',ir='/favicon'+_0x55b4f0(0x37d0),Qo=_0x55b4f0(0x3206)+_0x55b4f0(0x1f09)+_0x55b4f0(0x28e6),ar={'resizeTimeline':_0x55b4f0(0x3271)+_0x55b4f0(0x3205)+'ie\x20Höhe\x20'+_0x55b4f0(0x1944)+_0x55b4f0(0x2d8a)+_0x55b4f0(0x2fb8)+'n','systems':_0x55b4f0(0x1853),'particlesBones':_0x55b4f0(0x59e)+_0x55b4f0(0x3316)+'n)','rootBone':_0x55b4f0(0x20d3)+_0x55b4f0(0x3bcf),'hudTip':_0x55b4f0(0x1a4)+'·\x20Klicke'+'n\x20Sie\x20mi'+_0x55b4f0(0x35ca)+_0x55b4f0(0x17c3)+'staste,\x20'+_0x55b4f0(0x577)+_0x55b4f0(0xa0c)+'zu\x20ziehe'+'n','title':'Spine-Pa'+'rtikeled'+_0x55b4f0(0xbf9)},sr={'documentTitle':_0x55b4f0(0x1da5)+_0x55b4f0(0x532)+_0x55b4f0(0xbf9),'titleLead':_0x55b4f0(0x53ce)+_0x55b4f0(0x10f5)+_0x55b4f0(0x514f),'titleAccent':_0x55b4f0(0x34dd)+_0x55b4f0(0x2da4)+_0x55b4f0(0x253c),'backToTools':_0x55b4f0(0x129a)+'ur\x20Werkz'+_0x55b4f0(0x7cd)+'hl','editor':{'badge':_0x55b4f0(0x2c2a)+_0x55b4f0(0x47f5),'title':'Partikel'+'editor','description':_0x55b4f0(0x7e0)+_0x55b4f0(0x2ea8)+_0x55b4f0(0x4aa7)+'und\x20expo'+_0x55b4f0(0x1c6)+_0x55b4f0(0x33fb)+_0x55b4f0(0x2ea8)+_0x55b4f0(0x3954)+'teien\x20fr'+_0x55b4f0(0x4a39),'tagPreview':_0x55b4f0(0x4a65)+_0x55b4f0(0x550),'tagSequence':_0x55b4f0(0x32fe)+_0x55b4f0(0x2e5b)+_0x55b4f0(0x4e29),'tagExport':_0x55b4f0(0x2a4a)+_0x55b4f0(0x4e29)Line truncated
+1
View File
@@ -0,0 +1 @@
.language-picker[data-v-811a66dc]{display:flex;height:28px;align-items:center;gap:6px;margin-left:auto;color:#9aa8c1;font-size:11px;white-space:nowrap}.language-dropdown[data-v-811a66dc]{position:relative}.language-trigger[data-v-811a66dc]{display:flex;height:28px;min-width:88px;align-items:center;gap:7px;padding:0 8px;border:1px solid #3a4963;border-radius:6px;outline:none;background:#202a3b;color:#dbe4f5;font:inherit;font-size:11px;cursor:pointer}.language-trigger[data-v-811a66dc]:hover,.language-trigger.open[data-v-811a66dc]{border-color:#5b608c;background:#2b3a5c}.language-trigger[data-v-811a66dc]:focus-visible{border-color:#7774ff}.language-flag-icon[data-v-811a66dc],.language-option-flag-icon[data-v-811a66dc]{display:block;width:22px;height:15px;flex:0 0 auto;overflow:hidden;border-radius:2px;box-shadow:0 0 0 1px #ffffff1f}.language-short[data-v-811a66dc]{min-width:20px;color:#dbe4f5;font-size:11px;font-weight:600}.language-chevron[data-v-811a66dc]{width:10px;height:6px;margin-left:auto;fill:none;stroke:#aab;stroke-width:1.7;transition:transform .15s ease}.language-trigger.open .language-chevron[data-v-811a66dc]{transform:rotate(180deg)}.language-menu[data-v-811a66dc]{position:absolute;right:0;bottom:calc(100% + 6px);z-index:30;width:190px;max-height:min(360px,calc(100vh - 120px));overflow-y:auto;padding:5px;border:1px solid #343b50;border-radius:9px;background:#141420fa;box-shadow:0 8px 24px #0000007a}.menu-down .language-menu[data-v-811a66dc]{top:calc(100% + 6px);bottom:auto}.language-option[data-v-811a66dc]{display:grid;width:100%;min-height:38px;grid-template-columns:24px 1fr 16px;align-items:center;gap:8px;padding:0 7px;border:0;border-radius:6px;background:transparent;color:#aeb8cc;font:inherit;font-size:12px;text-align:left;white-space:nowrap;cursor:pointer}.language-option[data-v-811a66dc]:hover,.language-option[data-v-811a66dc]:focus-visible{outline:none;background:#2b3a5c;color:#fff}.language-option.selected[data-v-811a66dc]{background:#343b63;color:#fff}.language-check[data-v-811a66dc]{justify-self:end;color:#fff;font-size:18px;font-weight:500}.welcome-shell[data-v-d3581500]{--u: max(1px, calc(100vw / 1920));position:fixed;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;overflow:hidden;box-sizing:border-box;padding:clamp(22px,3vw,calc(42 * var(--u))) clamp(22px,4vw,calc(64 * var(--u))) calc(28 * var(--u));color:#eef0f5;background:radial-gradient(circle at 17% 0%,rgba(218,45,57,.16),transparent 32%),radial-gradient(circle at 85% 8%,rgba(72,100,170,.16),transparent 30%),linear-gradient(145deg,#09090d,#0d0f15 52%,#090b10);font-family:system-ui,-apple-system,Segoe UI,sans-serif}.welcome-shell[data-v-d3581500]:before{position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;content:"";opacity:.22;background-image:linear-gradient(rgba(255,255,255,.025) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.025) 1px,transparent 1px);background-size:calc(42 * var(--u)) calc(42 * var(--u));-webkit-mask-image:linear-gradient(to bottom,#000,transparent 70%);mask-image:linear-gradient(to bottom,#000,transparent 70%)}.welcome-header[data-v-d3581500],.welcome-intro[data-v-d3581500],.hub-grid[data-v-d3581500]{position:relative}.welcome-header[data-v-d3581500]{display:flex;min-height:calc(42 * var(--u));align-items:center;gap:calc(18 * var(--u))}.welcome-header[data-v-d3581500]{z-index:40}.brand-link[data-v-d3581500]{display:inline-flex;border-radius:6px;transition:opacity .16s ease}.brand-link[data-v-d3581500]:hover{opacity:.78}.brand-link[data-v-d3581500]:focus-visible{outline:2px solid #ef5147;outline-offset:5px}.brand-logo[data-v-d3581500]{display:block;width:clamp(128px,10vw,calc(156 * var(--u)));height:auto}.welcome-intro[data-v-d3581500]{padding:clamp(34px,6vh,calc(64 * var(--u))) 0 clamp(22px,3.5vh,calc(34 * var(--u)));text-align:center}.welcome-intro h1[data-v-d3581500]{max-width:none;margin:0 auto;color:#f3f3f5;font-size:clamp(20px,2.4vw,calc(40 * var(--u)));line-height:1.1;letter-spacing:-.025em;white-space:nowrap}.welcome-intro h1 span[data-v-d3581500]{color:#ff4e47}.welcome-intro p[data-v-d3581500]{max-width:390px;margin:0 0 3px;color:#8c93a3;font-size:13px;line-height:1.75}.hub-grid[data-v-d3581500]{display:grid;min-height:0;flex:0 1 auto;grid-template-columns:repeat(3,minmax(0,1fr));gap:clamp(14px,1.5vw,calc(22 * var(--u)));width:min(100%,calc(1540 * var(--u)));margin:0 auto}.hub-card[data-v-d3581500]{--hub-border: #242731;--hub-divider: #242731;position:relative;display:flex;min-width:0;min-height:0;flex-direction:column;padding:0;overflow:hidden;border:1px solid var(--hub-border);border-radius:calc(18 * var(--u));background:linear-gradient(180deg,#1a1d26f0,#0f1117fa);color:inherit;font:inherit;text-align:left;text-decoration:none;cursor:pointer;box-shadow:0 20px 52px #00000040;transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease}.hub-card[data-v-d3581500]:hover{transform:translateY(-3px);border-color:var(--hub-border);box-shadow:0 25px 6Line truncated
+1
View File
@@ -0,0 +1 @@
const _0x560eaf=_0x4e21;(function(_0x55cc66,_0x33005a){const _0x40853f=_0x4e21,_0x2dfaa5=_0x55cc66();while(!![]){try{const _0x3f48d1=-parseInt(_0x40853f(0x465f))/0x1+-parseInt(_0x40853f(0x5232))/0x2*(-parseInt(_0x40853f(0x3ed6))/0x3)+-parseInt(_0x40853f(0x3e4d))/0x4+parseInt(_0x40853f(0x4945))/0x5*(-parseInt(_0x40853f(0x4f3a))/0x6)+parseInt(_0x40853f(0x30e))/0x7*(parseInt(_0x40853f(0x2a7e))/0x8)+-parseInt(_0x40853f(0x329e))/0x9*(-parseInt(_0x40853f(0x35f))/0xa)+parseInt(_0x40853f(0x41be))/0xb;if(_0x3f48d1===_0x33005a)break;else _0x2dfaa5['push'](_0x2dfaa5['shift']());}catch(_0x3f0677){_0x2dfaa5['push'](_0x2dfaa5['shift']());}}}(_0x5740,0xe9638));var zs=Object['definePr'+_0x560eaf(0x19c0)],Vs=(_0x2726dd,_0x4f63ff,_0x467d95)=>_0x4f63ff in _0x2726dd?zs(_0x2726dd,_0x4f63ff,{'enumerable':!0x0,'configurable':!0x0,'writable':!0x0,'value':_0x467d95}):_0x2726dd[_0x4f63ff]=_0x467d95,Pe=(_0x1acc84,_0x213c67,_0x5f5bb5)=>Vs(_0x1acc84,typeof _0x213c67!=_0x560eaf(0xf5c)?_0x213c67+'':_0x213c67,_0x5f5bb5);import{c as _0x13dbfd,d as _0x7cfca,u as _0x5f4907,o as _0x33d9c6,a as _0xe8791b,b as _0x32ccf0,e as _0x147fea,w as _0x26f7df,f as _0x13aad1,n as _0x4b466b,g as _0x3cf543,t as _0x179372,h as _0x4dfb77,F as _0x3e631e,r as _0x4686b3,i as _0x5ebb84,j as _0x363061,k as _0x12366e,l as _0x241e59,m as _0x5e5dce,p as _0x209c25,q as _0x22f219,s as _0x2fb989,v as _0x551fb8,T as _0x40090d,x as _0x771149,y as _0x3278d8,z as _0x3b632a,S as _0x40b053,C as _0x100622,G as _0x14deae,A as _0x2c9e4e,B as _0x556eff,D as _0x476add,M as _0x57a4c0,E as _0x5364a0,H as _0x36bde6,I as _0x201861,J as _0x1b53b3,K as _0x12f3f2,L as _0x1f006f,N as _0x344cf2,O as _0xdf7d8e,P as _0xf4074b,Q as _0x5812f2,R as _0x352898,U as _0x28f0fc,V as _0x1da179,W as _0x42c62d,X as _0x13e008,Y as _0x3e6ef4,Z as _0x46026e,_ as _0x1795c7,$ as _0x543d86,a0 as _0x630b8a,a1 as _0x97c3dd,a2 as _0x2764ab,a3 as _0x55634f,a4 as _0x4cb046,a5 as _0x21f39a,a6 as _0x196a5c,a7 as _0x4432b0,a8 as _0x3314fb,a9 as _0x206a62,aa as _0x3812cb,ab as _0x44285e,ac as _0x46ab79,ad as _0x8da8d3,ae as _0x25ef69,af as _0x9ec9f4,ag as _0x381441,ah as _0x321acc,ai as _0x4eb4df}from'./vendor-fsmZiMPK.js';(function(){const _0xdc32d=_0x560eaf,_0x5c2155=document[_0xdc32d(0x1c42)+'ement'](_0xdc32d(0x9d6))['relList'];if(_0x5c2155&&_0x5c2155[_0xdc32d(0x2fa5)]&&_0x5c2155['supports'](_0xdc32d(0x3dfb)+'eload'))return;for(const _0x506b10 of document[_0xdc32d(0x552)+_0xdc32d(0xf22)](_0xdc32d(0x3da8)+_0xdc32d(0x437d)+_0xdc32d(0x17ce)+']'))_0x4eb118(_0x506b10);new MutationObserver(_0x17a330=>{const _0x56513f=_0xdc32d;for(const _0x34818f of _0x17a330)if(_0x34818f[_0x56513f(0x1da)]===_0x56513f(0x130b)+'t'){for(const _0x21f2e4 of _0x34818f[_0x56513f(0x3f76)+'es'])_0x21f2e4['tagName']===_0x56513f(0x2eab)&&_0x21f2e4[_0x56513f(0x1471)]===_0x56513f(0x3dfb)+_0x56513f(0x164e)&&_0x4eb118(_0x21f2e4);}})['observe'](document,{'childList':!0x0,'subtree':!0x0});function _0x3c409a(_0x4829d3){const _0x40f2eb=_0xdc32d,_0x23163a={};return _0x4829d3['integrit'+'y']&&(_0x23163a[_0x40f2eb(0x3dd)+'y']=_0x4829d3[_0x40f2eb(0x3dd)+'y']),_0x4829d3[_0x40f2eb(0x2f7a)+'Policy']&&(_0x23163a[_0x40f2eb(0x2f7a)+'Policy']=_0x4829d3[_0x40f2eb(0x2f7a)+'Policy']),_0x4829d3[_0x40f2eb(0x1466)+'gin']===_0x40f2eb(0x11a2)+'entials'?_0x23163a[_0x40f2eb(0x31c3)+_0x40f2eb(0xd62)]=_0x40f2eb(0x7a7):_0x4829d3[_0x40f2eb(0x1466)+_0x40f2eb(0x1fdb)]===_0x40f2eb(0x60c)+'s'?_0x23163a['credenti'+_0x40f2eb(0xd62)]=_0x40f2eb(0x5618):_0x23163a[_0x40f2eb(0x31c3)+'als']='same-ori'+_0x40f2eb(0x1fdb),_0x23163a;}function _0x4eb118(_0x4707b7){const _0x350c9b=_0xdc32d;if(_0x4707b7['ep'])return;_0x4707b7['ep']=!0x0;const _0x38f4be=_0x3c409a(_0x4707b7);fetch(_0x4707b7[_0x350c9b(0x341)],_0x38f4be);}}());const ca=_0x560eaf(0x98a)+_0x560eaf(0x396)+_0x560eaf(0x1a03)+_0x560eaf(0x4342),hr=_0x560eaf(0xbac)+_0x560eaf(0x4342),ri=_0x560eaf(0x98a)+'eagle-lo'+_0x560eaf(0x2f7f),gr={'resizeTimeline':_0x560eaf(0x156b)+_0x560eaf(0x4cee)+'ie\x20Höhe\x20'+_0x560eaf(0x4c5b)+_0x560eaf(0x3a9a)+_0x560eaf(0xb23)+'n','systems':'System','particlesBones':_0x560eaf(0x1aae)+_0x560eaf(0x4a83)+'n)','rootBone':'Wurzelkn'+_0x560eaf(0x1e17),'hudTip':'Radzoom\x20'+_0x560eaf(0x2bd5)+_0x560eaf(0x34e2)+_0x560eaf(0x304)+_0x560eaf(0x5479)+_0x560eaf(0x392)+'um\x20die\x20L'+_0x560eaf(0xc4c)+_0x560eaf(0x4d34)+'n','title':_0x560eaf(0x3219)+'rtikeled'+_0x560eaf(0x10a)},vr={'documentTitle':_0x560eaf(0x3219)+_0x560eaf(0x16d8)+_0x560eaf(0x10a),'titleLead':_0x560eaf(0x1c6)+_0x560eaf(0x2d6c)+_0x560eaf(0x27c7),'titleAccent':_0x560eaf(0x2358)+_0x560eaf(0x4af7)+'olchain','backToTools':_0x560eaf(0x1de8)+_0x560eaf(0x4ff)+_0x560eaf(0x2e74)+'hl','editor':{'badge':_0x560eaf(0x4216)+_0x560eaf(0x671),'title':_0x560eaf(0x1aae)+_0x560eaf(0x42aa),'description':_0x560eaf(0x36dc)+_0x560eaf(0x3115)+_0x560eaf(0x4e49)+_0x560eaf(0x455d)+_0x560eaf(0x203f)+_0x560eaf(0x402a)+_0x560eaf(0x3115)+_0x560eaf(0x292)+_0x560eaf(0x293)+_0x560eaf(0x8be),'tagPreview':_0x560eaf(0x930)+_0x560eaf(0x4c54),'tagSequence':'Spine-Pr'+_0x560eaf(0x2a7d)+_0x560eaf(0x7Line truncated
-1
View File
@@ -1 +0,0 @@
.language-picker[data-v-811a66dc]{display:flex;height:28px;align-items:center;gap:6px;margin-left:auto;color:#9aa8c1;font-size:11px;white-space:nowrap}.language-dropdown[data-v-811a66dc]{position:relative}.language-trigger[data-v-811a66dc]{display:flex;height:28px;min-width:88px;align-items:center;gap:7px;padding:0 8px;border:1px solid #3a4963;border-radius:6px;outline:none;background:#202a3b;color:#dbe4f5;font:inherit;font-size:11px;cursor:pointer}.language-trigger[data-v-811a66dc]:hover,.language-trigger.open[data-v-811a66dc]{border-color:#5b608c;background:#2b3a5c}.language-trigger[data-v-811a66dc]:focus-visible{border-color:#7774ff}.language-flag-icon[data-v-811a66dc],.language-option-flag-icon[data-v-811a66dc]{display:block;width:22px;height:15px;flex:0 0 auto;overflow:hidden;border-radius:2px;box-shadow:0 0 0 1px #ffffff1f}.language-short[data-v-811a66dc]{min-width:20px;color:#dbe4f5;font-size:11px;font-weight:600}.language-chevron[data-v-811a66dc]{width:10px;height:6px;margin-left:auto;fill:none;stroke:#aab;stroke-width:1.7;transition:transform .15s ease}.language-trigger.open .language-chevron[data-v-811a66dc]{transform:rotate(180deg)}.language-menu[data-v-811a66dc]{position:absolute;right:0;bottom:calc(100% + 6px);z-index:30;width:190px;max-height:min(360px,calc(100vh - 120px));overflow-y:auto;padding:5px;border:1px solid #343b50;border-radius:9px;background:#141420fa;box-shadow:0 8px 24px #0000007a}.menu-down .language-menu[data-v-811a66dc]{top:calc(100% + 6px);bottom:auto}.language-option[data-v-811a66dc]{display:grid;width:100%;min-height:38px;grid-template-columns:24px 1fr 16px;align-items:center;gap:8px;padding:0 7px;border:0;border-radius:6px;background:transparent;color:#aeb8cc;font:inherit;font-size:12px;text-align:left;white-space:nowrap;cursor:pointer}.language-option[data-v-811a66dc]:hover,.language-option[data-v-811a66dc]:focus-visible{outline:none;background:#2b3a5c;color:#fff}.language-option.selected[data-v-811a66dc]{background:#343b63;color:#fff}.language-check[data-v-811a66dc]{justify-self:end;color:#fff;font-size:18px;font-weight:500}.welcome-shell[data-v-d3581500]{--u: max(1px, calc(100vw / 1920));position:fixed;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;overflow:hidden;box-sizing:border-box;padding:clamp(22px,3vw,calc(42 * var(--u))) clamp(22px,4vw,calc(64 * var(--u))) calc(28 * var(--u));color:#eef0f5;background:radial-gradient(circle at 17% 0%,rgba(218,45,57,.16),transparent 32%),radial-gradient(circle at 85% 8%,rgba(72,100,170,.16),transparent 30%),linear-gradient(145deg,#09090d,#0d0f15 52%,#090b10);font-family:system-ui,-apple-system,Segoe UI,sans-serif}.welcome-shell[data-v-d3581500]:before{position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;content:"";opacity:.22;background-image:linear-gradient(rgba(255,255,255,.025) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.025) 1px,transparent 1px);background-size:calc(42 * var(--u)) calc(42 * var(--u));-webkit-mask-image:linear-gradient(to bottom,#000,transparent 70%);mask-image:linear-gradient(to bottom,#000,transparent 70%)}.welcome-header[data-v-d3581500],.welcome-intro[data-v-d3581500],.hub-grid[data-v-d3581500]{position:relative}.welcome-header[data-v-d3581500]{display:flex;min-height:calc(42 * var(--u));align-items:center;gap:calc(18 * var(--u))}.welcome-header[data-v-d3581500]{z-index:40}.brand-link[data-v-d3581500]{display:inline-flex;border-radius:6px;transition:opacity .16s ease}.brand-link[data-v-d3581500]:hover{opacity:.78}.brand-link[data-v-d3581500]:focus-visible{outline:2px solid #ef5147;outline-offset:5px}.brand-logo[data-v-d3581500]{display:block;width:clamp(128px,10vw,calc(156 * var(--u)));height:auto}.welcome-intro[data-v-d3581500]{padding:clamp(34px,6vh,calc(64 * var(--u))) 0 clamp(22px,3.5vh,calc(34 * var(--u)));text-align:center}.welcome-intro h1[data-v-d3581500]{max-width:none;margin:0 auto;color:#f3f3f5;font-size:clamp(20px,2.4vw,calc(40 * var(--u)));line-height:1.1;letter-spacing:-.025em;white-space:nowrap}.welcome-intro h1 span[data-v-d3581500]{color:#ff4e47}.welcome-intro p[data-v-d3581500]{max-width:390px;margin:0 0 3px;color:#8c93a3;font-size:13px;line-height:1.75}.hub-grid[data-v-d3581500]{display:grid;min-height:0;flex:0 1 auto;grid-template-columns:repeat(3,minmax(0,1fr));gap:clamp(14px,1.5vw,calc(22 * var(--u)));width:min(100%,calc(1540 * var(--u)));margin:0 auto}.hub-card[data-v-d3581500]{--hub-border: #242731;--hub-divider: #242731;position:relative;display:flex;min-width:0;min-height:0;flex-direction:column;padding:0;overflow:hidden;border:1px solid var(--hub-border);border-radius:calc(18 * var(--u));background:linear-gradient(180deg,#1a1d26f0,#0f1117fa);color:inherit;font:inherit;text-align:left;text-decoration:none;cursor:pointer;box-shadow:0 20px 52px #00000040;transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease}.hub-card[data-v-d3581500]:hover{transform:translateY(-3px);border-color:var(--hub-border);box-shadow:0 25px 6Line truncated
+2 -2
View File
@@ -9,9 +9,9 @@
html, body { margin: 0; padding: 0; background: #111; color: #eee; font-family: system-ui, -apple-system, sans-serif; } html, body { margin: 0; padding: 0; background: #111; color: #eee; font-family: system-ui, -apple-system, sans-serif; }
#app { display: flex; flex-direction: column; height: 100vh; } #app { display: flex; flex-direction: column; height: 100vh; }
</style> </style>
<script type="module" crossorigin src="/assets/index-6a1Sdk_L.js"></script> <script type="module" crossorigin src="/assets/index-LPRIE0BV.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-fsmZiMPK.js"> <link rel="modulepreload" crossorigin href="/assets/vendor-fsmZiMPK.js">
<link rel="stylesheet" crossorigin href="/assets/index-uyvk6dvP.css"> <link rel="stylesheet" crossorigin href="/assets/index-BOPqyKhf.css">
<script> <script>
var _paq = window._paq = window._paq || []; var _paq = window._paq = window._paq || [];
_paq.push(['enableLinkTracking']); _paq.push(['enableLinkTracking']);
+134
View File
@@ -0,0 +1,134 @@
import type { CollisionBody, ParticleSystem, ScenePath, useParticleStore } from '../store/particleStore'
import type { SpineSceneObject } from '../spine/spineTypes'
import { ensureEmitterConfig } from '../core/particleEmitter'
import { registerSpineAsset } from '../spine/spineAssetRegistry'
import { translate } from '../i18n'
import { cloneEditorValue } from './editorClone'
import {
hydrateParticleResources,
parseEditorConfig,
parseEmbeddedSpines,
withoutLegacyColliderTags,
} from './editorConfig'
type EditorStore = ReturnType<typeof useParticleStore>
function nextId(items: readonly { id: number }[]) {
return Math.max(0, ...items.map((item) => Number(item.id) || 0)) + 1
}
function remapId(map: ReadonlyMap<number, number>, id: number) {
return map.get(id) || 0
}
export async function appendEditorConfigText(store: EditorStore, text: string) {
const config = parseEditorConfig(text)
const systems = cloneEditorValue(config.scene.systems) as unknown as ParticleSystem[]
const colliders = config.scene.colliders.map(withoutLegacyColliderTags)
const paths = cloneEditorValue(config.scene.paths) as unknown as ScenePath[]
const spines = cloneEditorValue(config.scene.spines) as unknown as SpineSceneObject[]
let systemId = nextId(store.systems)
let colliderId = nextId(store.colliders)
let pathId = nextId(store.paths)
let spineId = nextId(store.spines)
const systemIds = new Map<number, number>()
const colliderIds = new Map<number, number>()
const pathIds = new Map<number, number>()
const spineIds = new Map<number, number>()
for (const system of systems) {
systemIds.set(system.id, systemId)
system.id = systemId++
}
for (const collider of colliders) {
colliderIds.set(collider.id, colliderId)
collider.id = colliderId++
}
for (const path of paths) {
pathIds.set(path.id, pathId)
path.id = pathId++
}
for (const spine of spines) {
if (spine.sourceBundle == null) spine.sourceBundle = null
if (spine.dataVersion == null) spine.dataVersion = ''
if (!Array.isArray(spine.slots)) spine.slots = []
if (typeof spine.selectedBone !== 'string') spine.selectedBone = ''
if (typeof spine.selectedSlot !== 'string') spine.selectedSlot = ''
spineIds.set(spine.id, spineId)
spine.id = spineId++
}
for (const system of systems) {
const config = ensureEmitterConfig(system.config as any)
system.frames = undefined
config.collisionColliderIds = config.collisionColliderIds
.map((id) => remapId(colliderIds, id))
.filter((id) => id > 0)
config.spawnPathId = remapId(pathIds, config.spawnPathId)
if (!config.spawnPathId && (config.emissionAngleMode === 'path-normal' || config.emissionAngleMode === 'path-tangent')) {
config.emissionAngleMode = 'fixed'
}
config.emitterFollowPathId = remapId(pathIds, config.emitterFollowPathId)
config.emitterFollowSpineId = remapId(spineIds, config.emitterFollowSpineId)
config.renderLayerSpineId = remapId(spineIds, config.renderLayerSpineId)
for (const rule of config.events || []) {
rule.targetSystemId = remapId(systemIds, rule.targetSystemId)
}
}
for (const collider of colliders) {
collider.followPathId = remapId(pathIds, collider.followPathId)
if (!collider.followPathId) collider.followPath = false
}
await hydrateParticleResources(systems)
const parsedSpines = await parseEmbeddedSpines(spines)
const animations = [...store.timeline.animations]
for (const animation of config.timeline?.animations || []) {
const name = typeof animation === 'string' ? animation.trim() : ''
if (name && !animations.includes(name)) animations.push(name)
}
for (const system of systems) {
if (!animations.includes(system.animation)) {
system.animation = store.timeline.animation || store.timeline.animations[0] || 'animation'
}
}
for (const spine of spines) {
const parsed = parsedSpines.get(spine.id)
spine.assetVersion = Math.max(1, Number(spine.assetVersion) || 0) + 1
if (!parsed) {
spine.status = 'empty'
spine.error = spine.skeletonFileName ? translate('errors.missingEmbeddedSpine') : ''
spine.animations = []
spine.bones = []
spine.slots = []
spine.selectedAnimation = ''
continue
}
registerSpineAsset(spine.id, parsed)
spine.dataVersion = parsed.dataVersion
spine.animations = parsed.animations
spine.bones = parsed.bones
spine.slots = parsed.slots
if (!parsed.animations.some((animation) => animation.name === spine.selectedAnimation)) {
spine.selectedAnimation = parsed.animations[0]?.name || ''
}
spine.status = 'ready'
spine.error = ''
}
store.systems.push(...systems)
store.colliders.push(...colliders)
store.paths.push(...paths)
store.spines.push(...spines)
store.timeline.animations = animations
store.syncSceneSequences()
if (systems[0]) store.selectSceneObject('particle', systems[0].id)
else if (colliders[0]) store.selectSceneObject('collision', colliders[0].id)
else if (paths[0]) store.selectSceneObject('path', paths[0].id)
else if (spines[0]) store.selectSceneObject('spine', spines[0].id)
store.recalcTotalFrames()
}
+3 -3
View File
@@ -83,7 +83,7 @@ function jsonClone<T>(value: T): T {
})) as T })) as T
} }
function withoutLegacyColliderTags(collider: CollisionBody): CollisionBody { export function withoutLegacyColliderTags(collider: CollisionBody): CollisionBody {
const copy = cloneEditorValue(collider) as CollisionBody & { tags?: unknown; selectedTag?: unknown } const copy = cloneEditorValue(collider) as CollisionBody & { tags?: unknown; selectedTag?: unknown }
delete copy.tags delete copy.tags
delete copy.selectedTag delete copy.selectedTag
@@ -192,7 +192,7 @@ export function parseEditorConfig(text: string): EditorConfigFile {
return candidate as EditorConfigFile return candidate as EditorConfigFile
} }
async function hydrateParticleResources(systems: ParticleSystem[]) { export async function hydrateParticleResources(systems: ParticleSystem[]) {
const textureCache = new Map<string, Promise<Texture>>() const textureCache = new Map<string, Promise<Texture>>()
const loadTexture = (url: string) => { const loadTexture = (url: string) => {
const source = url === '/particles/star.png' const source = url === '/particles/star.png'
@@ -225,7 +225,7 @@ async function hydrateParticleResources(systems: ParticleSystem[]) {
})) }))
} }
async function parseEmbeddedSpines(spines: SpineSceneObject[]) { export async function parseEmbeddedSpines(spines: SpineSceneObject[]) {
const parsed = new Map<number, ParsedSpineBundle>() const parsed = new Map<number, ParsedSpineBundle>()
try { try {
for (const spine of spines) { for (const spine of spines) {
+275
View File
@@ -0,0 +1,275 @@
import type {
CollisionBody,
ParticleSystem,
SceneObjectType,
ScenePath,
useParticleStore,
} from '../store/particleStore'
import type { SpineSceneObject } from '../spine/spineTypes'
import { registerSpineAsset, releaseSpineAsset } from '../spine/spineAssetRegistry'
import { ensureEmitterConfig } from '../core/particleEmitter'
import { collectEmitterAncestors } from '../core/subEmitterIds'
import { translate } from '../i18n'
import { cloneEditorValue } from './editorClone'
import { hydrateParticleResources, parseEmbeddedSpines, withoutLegacyColliderTags } from './editorConfig'
type EditorStore = ReturnType<typeof useParticleStore>
const CLIPBOARD_FORMAT = 'SpineParticle.SceneObject'
const CLIPBOARD_VERSION = 1
type ClipboardObject = ParticleSystem | CollisionBody | ScenePath | SpineSceneObject
interface SceneObjectClipboardPayload {
format: typeof CLIPBOARD_FORMAT
version: typeof CLIPBOARD_VERSION
type: SceneObjectType
object: ClipboardObject
}
let pasteBusy = false
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value)
}
function jsonClone<T>(value: T): T {
return JSON.parse(JSON.stringify(value, (key, item) => {
if (key === 'frames' || key === 'texture' || key === 'trailTexture') return undefined
return item
})) as T
}
function objectForCopy(store: EditorStore, type: SceneObjectType, id: number): ClipboardObject | null {
if (type === 'particle') {
const system = store.systems.find((item) => item.id === id)
return system ? jsonClone(system) as unknown as ParticleSystem : null
}
if (type === 'collision') {
const collider = store.colliders.find((item) => item.id === id)
return collider ? withoutLegacyColliderTags(collider) : null
}
if (type === 'path') {
const path = store.paths.find((item) => item.id === id)
return path ? cloneEditorValue(path) : null
}
const spine = store.spines.find((item) => item.id === id)
if (!spine) return null
const copy = jsonClone(spine)
copy.selectedBone = ''
copy.selectedSlot = ''
copy.status = copy.sourceBundle ? 'ready' : 'empty'
copy.error = ''
return copy
}
function parsePayload(text: string): SceneObjectClipboardPayload {
let value: unknown
try {
value = JSON.parse(text, (key, item) => (
key === '__proto__' || key === 'prototype' || key === 'constructor' ? undefined : item
))
} catch {
throw new Error(translate('scene.clipboardInvalid'))
}
if (!isRecord(value)
|| value.format !== CLIPBOARD_FORMAT
|| value.version !== CLIPBOARD_VERSION
|| !['particle', 'collision', 'path', 'spine'].includes(String(value.type))
|| !isRecord(value.object)) {
throw new Error(translate('scene.clipboardInvalid'))
}
return value as unknown as SceneObjectClipboardPayload
}
function validObjectForType(payload: SceneObjectClipboardPayload) {
const object = payload.object as unknown as Record<string, unknown>
if (payload.type === 'particle') return isRecord(object.config)
if (payload.type === 'collision') return typeof object.name === 'string'
if (payload.type === 'path') return typeof object.name === 'string' && Array.isArray(object.points)
return typeof object.name === 'string'
&& (object.sourceBundle == null || isRecord(object.sourceBundle))
}
function typeLabel(type: SceneObjectType) {
return type === 'spine' ? 'Spine' : translate(`scene.${type}`)
}
function resetParticleReferences(store: EditorStore, system: ParticleSystem) {
const config = ensureEmitterConfig(system.config as any)
const systems = store.systems.map((item) => (item.id === system.id ? system : item))
const systemIds = new Set(systems.map((item) => item.id))
const colliderIds = new Set(store.colliders.map((item) => item.id))
const pathIds = new Set(store.paths.map((item) => item.id))
const spineIds = new Set(store.spines.map((item) => item.id))
const ancestors = collectEmitterAncestors(systems, system.id)
config.collisionColliderIds = config.collisionColliderIds.filter((id) => colliderIds.has(id))
if (!pathIds.has(config.spawnPathId)) {
config.spawnPathId = 0
if (config.emissionAngleMode === 'path-normal' || config.emissionAngleMode === 'path-tangent') {
config.emissionAngleMode = 'fixed'
}
}
for (const rule of config.events || []) {
if (!systemIds.has(rule.targetSystemId) || rule.targetSystemId === system.id || ancestors.has(rule.targetSystemId)) {
rule.targetSystemId = 0
}
}
if (config.emitterFollowMode === 'path' && !pathIds.has(config.emitterFollowPathId)) {
config.emitterFollow = false
config.emitterFollowMode = 'none'
config.emitterFollowPathId = 0
}
if (!spineIds.has(config.emitterFollowSpineId)) {
config.emitterFollowSpineId = 0
config.emitterFollowBoneName = ''
if (config.emitterFollowMode === 'bone') {
config.emitterFollow = false
config.emitterFollowMode = 'none'
}
}
if (config.emitterFollowMode === 'bone') {
const spine = store.spines.find((item) => item.id === config.emitterFollowSpineId)
if (!spine?.bones.some((bone) => bone.name === config.emitterFollowBoneName)) {
config.emitterFollow = false
config.emitterFollowMode = 'none'
config.emitterFollowSpineId = 0
config.emitterFollowBoneName = ''
}
}
if (!spineIds.has(config.renderLayerSpineId)) {
config.renderLayerSpineId = 0
config.renderLayerSlotName = ''
if (config.renderLayerMode === 'slot') config.renderLayerMode = 'top'
}
}
export async function copySceneObjectToClipboard(store: EditorStore, type: SceneObjectType, id: number) {
const object = objectForCopy(store, type, id)
if (!object) throw new Error(translate('scene.clipboardInvalid'))
if (!navigator.clipboard?.writeText) throw new Error(translate('scene.clipboardUnavailable'))
const payload: SceneObjectClipboardPayload = {
format: CLIPBOARD_FORMAT,
version: CLIPBOARD_VERSION,
type,
object,
}
try {
await navigator.clipboard.writeText(JSON.stringify(payload))
} catch {
throw new Error(translate('scene.clipboardCopyFailed'))
}
}
export async function pasteSceneObjectFromClipboard(store: EditorStore, type: SceneObjectType, id: number) {
if (pasteBusy) return
pasteBusy = true
try {
if (!navigator.clipboard?.readText) throw new Error(translate('scene.clipboardUnavailable'))
let text: string
try {
text = await navigator.clipboard.readText()
} catch {
throw new Error(translate('scene.clipboardReadFailed'))
}
const payload = parsePayload(text)
if (!validObjectForType(payload)) throw new Error(translate('scene.clipboardInvalid'))
if (payload.type !== type) {
throw new Error(translate('scene.clipboardTypeMismatch', {
source: typeLabel(payload.type),
target: typeLabel(type),
}))
}
if (type === 'particle') {
const index = store.systems.findIndex((item) => item.id === id)
if (index < 0) throw new Error(translate('scene.clipboardInvalid'))
const target = store.systems[index]
const source = payload.object as ParticleSystem
if (!isRecord(source.config)) throw new Error(translate('scene.clipboardInvalid'))
const system = cloneEditorValue(source)
system.id = target.id
system.config = ensureEmitterConfig(system.config as any)
system.config.name = target.config.name
system.frames = undefined
system.animation = store.timeline.animations.includes(system.animation)
? system.animation
: store.timeline.animation
resetParticleReferences(store, system)
await hydrateParticleResources([system])
store.systems.splice(index, 1, system)
store.selectSceneObject('particle', system.id)
store.recalcTotalFrames()
return
}
if (type === 'collision') {
const index = store.colliders.findIndex((item) => item.id === id)
if (index < 0) throw new Error(translate('scene.clipboardInvalid'))
const target = store.colliders[index]
const collider = withoutLegacyColliderTags(payload.object as CollisionBody)
collider.id = target.id
collider.name = target.name
if (!store.paths.some((item) => item.id === collider.followPathId)) {
collider.followPath = false
collider.followPathId = 0
}
store.colliders.splice(index, 1, collider)
store.selectSceneObject('collision', collider.id)
return
}
if (type === 'path') {
const index = store.paths.findIndex((item) => item.id === id)
if (index < 0) throw new Error(translate('scene.clipboardInvalid'))
const target = store.paths[index]
const path = cloneEditorValue(payload.object as ScenePath)
path.id = target.id
path.name = target.name
store.paths.splice(index, 1, path)
store.selectSceneObject('path', path.id)
return
}
const index = store.spines.findIndex((item) => item.id === id)
if (index < 0) throw new Error(translate('scene.clipboardInvalid'))
const target = store.spines[index]
const spine = cloneEditorValue(payload.object as SpineSceneObject)
spine.id = target.id
spine.name = target.name
spine.selectedBone = ''
spine.selectedSlot = ''
spine.assetVersion = target.assetVersion + 1
spine.status = 'loading'
spine.error = ''
const parsed = (await parseEmbeddedSpines([spine])).get(spine.id)
if (parsed) {
releaseSpineAsset(target.id)
registerSpineAsset(spine.id, parsed)
spine.dataVersion = parsed.dataVersion
spine.animations = parsed.animations
spine.bones = parsed.bones
spine.slots = parsed.slots
if (!parsed.animations.some((animation) => animation.name === spine.selectedAnimation)) {
spine.selectedAnimation = parsed.animations[0]?.name || ''
}
spine.status = 'ready'
} else {
releaseSpineAsset(target.id)
spine.dataVersion = ''
spine.animations = []
spine.bones = []
spine.slots = []
spine.selectedAnimation = ''
spine.status = 'empty'
}
store.spines.splice(index, 1, spine)
store.selectSceneObject('spine', spine.id)
store.recalcTotalFrames()
} catch (error) {
if (error instanceof Error && error.message) throw error
throw new Error(translate('scene.clipboardPasteFailed'))
} finally {
pasteBusy = false
}
}
+13
View File
@@ -164,6 +164,9 @@
"storageUnavailable": "Der lokale Browserspeicher ist nicht verfügbar", "storageUnavailable": "Der lokale Browserspeicher ist nicht verfügbar",
"saved": "Im Browser gespeichert: {name}", "saved": "Im Browser gespeichert: {name}",
"loaded": "Aus dem Browser geladen: {name}", "loaded": "Aus dem Browser geladen: {name}",
"appendToScene": "Diese Konfiguration an die Szene anhängen",
"appending": "Konfiguration wird angehängt: {name}",
"appended": "An die Szene angehängt: {name}",
"deleted": "Browserkonfiguration gelöscht: {name}", "deleted": "Browserkonfiguration gelöscht: {name}",
"downloadEmpty": "Keine Konfigurationen zum Herunterladen", "downloadEmpty": "Keine Konfigurationen zum Herunterladen",
"downloaded": "Heruntergeladen: {name}" "downloaded": "Heruntergeladen: {name}"
@@ -501,6 +504,14 @@
"addPathShort": "+ Pfad", "addPathShort": "+ Pfad",
"addSpine": "Fügen Sie Spine hinzu", "addSpine": "Fügen Sie Spine hinzu",
"copyTitle": "Kopieren Sie das aktuell ausgewählte Szenenobjekt", "copyTitle": "Kopieren Sie das aktuell ausgewählte Szenenobjekt",
"contextCopy": "In die Zwischenablage kopieren",
"contextPaste": "Einfügen und überschreiben",
"clipboardUnavailable": "Dieser Browser erlaubt keinen Zugriff auf die Zwischenablage",
"clipboardInvalid": "Die Zwischenablage enthält kein gültiges Szenenobjekt",
"clipboardCopyFailed": "Das Szenenobjekt konnte nicht in die Zwischenablage kopiert werden",
"clipboardReadFailed": "Die Zwischenablage konnte nicht gelesen werden. Prüfen Sie die Browserberechtigungen.",
"clipboardTypeMismatch": "Nur Objekte desselben Typs können überschrieben werden (Zwischenablage: {source}, aktuell: {target})",
"clipboardPasteFailed": "Das Szenenobjekt konnte nicht wiederhergestellt werden",
"showParticle": "Partikelsystem anzeigen", "showParticle": "Partikelsystem anzeigen",
"hideParticle": "Partikelsystem ausblenden", "hideParticle": "Partikelsystem ausblenden",
"showCollider": "Collider anzeigen", "showCollider": "Collider anzeigen",
@@ -802,6 +813,8 @@
"eye": "kleine Augen", "eye": "kleine Augen",
"eyeDesc": "Objekte einzeln ein-/ausblenden; Verborgene Partikelsysteme werden nicht exportiert.", "eyeDesc": "Objekte einzeln ein-/ausblenden; Verborgene Partikelsysteme werden nicht exportiert.",
"copyDesc": "Kopiert das aktuell ausgewählte Objekt vollständig, um den Vergleich verschiedener Parameter zu erleichtern.", "copyDesc": "Kopiert das aktuell ausgewählte Objekt vollständig, um den Vergleich verschiedener Parameter zu erleichtern.",
"sceneClipboard": "Szenen-Zwischenablage",
"sceneClipboardDesc": "Rechtsklick auf ein Szenenobjekt, um es zu kopieren oder durch Einfügen zu ersetzen.",
"backgroundDesc": "Laden Sie das Referenzbild hoch und passen Sie Position und Transparenz nur für die Leinwandvorschau an.", "backgroundDesc": "Laden Sie das Referenzbild hoch und passen Sie Position und Transparenz nur für die Leinwandvorschau an.",
"settingsDesc": "Das Layout der Verwaltungsoberfläche, die Zusatzanzeige, der Maßstab usw. werden automatisch im Browser gespeichert.", "settingsDesc": "Das Layout der Verwaltungsoberfläche, die Zusatzanzeige, der Maßstab usw. werden automatisch im Browser gespeichert.",
"undoDesc": "Ctrl+Z fährt einen Schritt zurück; Die maximale Schrittanzahl kann in den Systemeinstellungen angepasst werden.", "undoDesc": "Ctrl+Z fährt einen Schritt zurück; Die maximale Schrittanzahl kann in den Systemeinstellungen angepasst werden.",
+13
View File
@@ -176,6 +176,9 @@
"storageUnavailable": "Browser local storage is unavailable", "storageUnavailable": "Browser local storage is unavailable",
"saved": "Saved to browser: {name}", "saved": "Saved to browser: {name}",
"loaded": "Loaded from browser: {name}", "loaded": "Loaded from browser: {name}",
"appendToScene": "Append this configuration to the scene",
"appending": "Appending configuration: {name}",
"appended": "Appended to scene: {name}",
"deleted": "Deleted browser configuration: {name}", "deleted": "Deleted browser configuration: {name}",
"downloadEmpty": "No configurations to download", "downloadEmpty": "No configurations to download",
"downloaded": "Downloaded: {name}" "downloaded": "Downloaded: {name}"
@@ -513,6 +516,14 @@
"addPathShort": "+ Path", "addPathShort": "+ Path",
"addSpine": "Add Spine", "addSpine": "Add Spine",
"copyTitle": "Duplicate selected scene object", "copyTitle": "Duplicate selected scene object",
"contextCopy": "Copy to clipboard",
"contextPaste": "Paste and overwrite",
"clipboardUnavailable": "This browser does not allow clipboard access",
"clipboardInvalid": "The clipboard does not contain a valid scene object",
"clipboardCopyFailed": "Could not copy the scene object to the clipboard",
"clipboardReadFailed": "Could not read the clipboard. Check browser permissions.",
"clipboardTypeMismatch": "Only objects of the same type can be overwritten (clipboard: {source}, current: {target})",
"clipboardPasteFailed": "Could not restore the scene object",
"showParticle": "Show particle system", "showParticle": "Show particle system",
"hideParticle": "Hide particle system", "hideParticle": "Hide particle system",
"showCollider": "Show collider", "showCollider": "Show collider",
@@ -825,6 +836,8 @@
"eye": "Visibility", "eye": "Visibility",
"eyeDesc": "Show or hide an object independently. Hidden particle systems are not exported.", "eyeDesc": "Show or hide an object independently. Hidden particle systems are not exported.",
"copyDesc": "Duplicate the selected object for parameter comparisons.", "copyDesc": "Duplicate the selected object for parameter comparisons.",
"sceneClipboard": "Scene clipboard",
"sceneClipboardDesc": "Right-click a scene object to copy it to the clipboard or overwrite it by pasting.",
"backgroundDesc": "Upload a reference image and adjust its position and opacity for canvas preview only.", "backgroundDesc": "Upload a reference image and adjust its position and opacity for canvas preview only.",
"settingsDesc": "Manage layout, canvas guides, tick display, and other editor settings.", "settingsDesc": "Manage layout, canvas guides, tick display, and other editor settings.",
"undoDesc": "Ctrl+Z undoes one step. The maximum history size is configurable.", "undoDesc": "Ctrl+Z undoes one step. The maximum history size is configurable.",
+13
View File
@@ -164,6 +164,9 @@
"storageUnavailable": "El almacenamiento local del navegador no está disponible", "storageUnavailable": "El almacenamiento local del navegador no está disponible",
"saved": "Guardado en el navegador: {name}", "saved": "Guardado en el navegador: {name}",
"loaded": "Cargado desde el navegador: {name}", "loaded": "Cargado desde el navegador: {name}",
"appendToScene": "Añadir esta configuración a la escena",
"appending": "Añadiendo configuración: {name}",
"appended": "Añadido a la escena: {name}",
"deleted": "Configuración eliminada del navegador: {name}", "deleted": "Configuración eliminada del navegador: {name}",
"downloadEmpty": "No hay configuraciones para descargar", "downloadEmpty": "No hay configuraciones para descargar",
"downloaded": "Descargado: {name}" "downloaded": "Descargado: {name}"
@@ -501,6 +504,14 @@
"addPathShort": "+ camino", "addPathShort": "+ camino",
"addSpine": "Añadir Spine", "addSpine": "Añadir Spine",
"copyTitle": "Copiar el objeto de escena seleccionado actualmente", "copyTitle": "Copiar el objeto de escena seleccionado actualmente",
"contextCopy": "Copiar al portapapeles",
"contextPaste": "Pegar y sobrescribir",
"clipboardUnavailable": "Este navegador no permite acceder al portapapeles",
"clipboardInvalid": "El portapapeles no contiene un objeto de escena válido",
"clipboardCopyFailed": "No se pudo copiar el objeto de escena al portapapeles",
"clipboardReadFailed": "No se pudo leer el portapapeles. Comprueba los permisos del navegador.",
"clipboardTypeMismatch": "Solo se pueden sobrescribir objetos del mismo tipo (portapapeles: {source}, actual: {target})",
"clipboardPasteFailed": "No se pudo restaurar el objeto de escena",
"showParticle": "Mostrar sistema de partículas", "showParticle": "Mostrar sistema de partículas",
"hideParticle": "Ocultar sistema de partículas", "hideParticle": "Ocultar sistema de partículas",
"showCollider": "mostrar colisionadores", "showCollider": "mostrar colisionadores",
@@ -802,6 +813,8 @@
"eye": "ojos pequeños", "eye": "ojos pequeños",
"eyeDesc": "Mostrar/ocultar objetos individualmente; Los sistemas de partículas ocultas no se exportan.", "eyeDesc": "Mostrar/ocultar objetos individualmente; Los sistemas de partículas ocultas no se exportan.",
"copyDesc": "Copia completamente el objeto seleccionado actualmente para facilitar la comparación de diferentes parámetros.", "copyDesc": "Copia completamente el objeto seleccionado actualmente para facilitar la comparación de diferentes parámetros.",
"sceneClipboard": "Portapapeles de escena",
"sceneClipboardDesc": "Haz clic derecho en un objeto de escena para copiarlo o sustituirlo pegando.",
"backgroundDesc": "Cargue la imagen de referencia y ajuste la posición y la transparencia solo para la vista previa del lienzo.", "backgroundDesc": "Cargue la imagen de referencia y ajuste la posición y la transparencia solo para la vista previa del lienzo.",
"settingsDesc": "El diseño de la interfaz de administración, la pantalla auxiliar, la escala, etc. se guardan automáticamente en el navegador.", "settingsDesc": "El diseño de la interfaz de administración, la pantalla auxiliar, la escala, etc. se guardan automáticamente en el navegador.",
"undoDesc": "Ctrl+Z retrocede un paso; el número máximo de pasos se puede ajustar en la configuración del sistema.", "undoDesc": "Ctrl+Z retrocede un paso; el número máximo de pasos se puede ajustar en la configuración del sistema.",
+13
View File
@@ -164,6 +164,9 @@
"storageUnavailable": "Le stockage local du navigateur est indisponible", "storageUnavailable": "Le stockage local du navigateur est indisponible",
"saved": "Enregistré dans le navigateur : {name}", "saved": "Enregistré dans le navigateur : {name}",
"loaded": "Chargé depuis le navigateur : {name}", "loaded": "Chargé depuis le navigateur : {name}",
"appendToScene": "Ajouter cette configuration à la scène",
"appending": "Ajout de la configuration : {name}",
"appended": "Ajouté à la scène : {name}",
"deleted": "Configuration supprimée du navigateur : {name}", "deleted": "Configuration supprimée du navigateur : {name}",
"downloadEmpty": "Aucune configuration à télécharger", "downloadEmpty": "Aucune configuration à télécharger",
"downloaded": "Téléchargé : {name}" "downloaded": "Téléchargé : {name}"
@@ -501,6 +504,14 @@
"addPathShort": "+ chemin", "addPathShort": "+ chemin",
"addSpine": "Ajouter Spine", "addSpine": "Ajouter Spine",
"copyTitle": "Copier l'objet de scène actuellement sélectionné", "copyTitle": "Copier l'objet de scène actuellement sélectionné",
"contextCopy": "Copier dans le presse-papiers",
"contextPaste": "Coller et remplacer",
"clipboardUnavailable": "Ce navigateur n’autorise pas l’accès au presse-papiers",
"clipboardInvalid": "Le presse-papiers ne contient aucun objet de scène valide",
"clipboardCopyFailed": "Impossible de copier l’objet de scène dans le presse-papiers",
"clipboardReadFailed": "Impossible de lire le presse-papiers. Vérifiez les autorisations du navigateur.",
"clipboardTypeMismatch": "Seul un objet de même type peut être remplacé (presse-papiers : {source}, actuel : {target})",
"clipboardPasteFailed": "Impossible de restaurer l’objet de scène",
"showParticle": "Afficher le système de particules", "showParticle": "Afficher le système de particules",
"hideParticle": "Masquer le système de particules", "hideParticle": "Masquer le système de particules",
"showCollider": "montrer les collisionneurs", "showCollider": "montrer les collisionneurs",
@@ -802,6 +813,8 @@
"eye": "petits yeux", "eye": "petits yeux",
"eyeDesc": "Afficher/masquer les objets individuellement ; les systèmes de particules cachées ne sont pas exportés.", "eyeDesc": "Afficher/masquer les objets individuellement ; les systèmes de particules cachées ne sont pas exportés.",
"copyDesc": "Copie entièrement l'objet actuellement sélectionné pour faciliter la comparaison des différents paramètres.", "copyDesc": "Copie entièrement l'objet actuellement sélectionné pour faciliter la comparaison des différents paramètres.",
"sceneClipboard": "Presse-papiers de scène",
"sceneClipboardDesc": "Faites un clic droit sur un objet de scène pour le copier ou le remplacer par collage.",
"backgroundDesc": "Téléchargez l'image de référence et ajustez la position et la transparence pour l'aperçu du canevas uniquement.", "backgroundDesc": "Téléchargez l'image de référence et ajustez la position et la transparence pour l'aperçu du canevas uniquement.",
"settingsDesc": "La disposition de l'interface de gestion, l'affichage auxiliaire, la balance, etc. sont automatiquement enregistrés dans le navigateur.", "settingsDesc": "La disposition de l'interface de gestion, l'affichage auxiliaire, la balance, etc. sont automatiquement enregistrés dans le navigateur.",
"undoDesc": "Ctrl+Z se rétracte d'un pas ; le nombre maximum d'étapes peut être ajusté dans les paramètres du système.", "undoDesc": "Ctrl+Z se rétracte d'un pas ; le nombre maximum d'étapes peut être ajusté dans les paramètres du système.",
+13
View File
@@ -164,6 +164,9 @@
"storageUnavailable": "ブラウザのローカルストレージを利用できません", "storageUnavailable": "ブラウザのローカルストレージを利用できません",
"saved": "ブラウザに保存しました:{name}", "saved": "ブラウザに保存しました:{name}",
"loaded": "ブラウザから読み込みました:{name}", "loaded": "ブラウザから読み込みました:{name}",
"appendToScene": "この設定をシーンに追加",
"appending": "設定を追加しています:{name}",
"appended": "シーンに追加しました:{name}",
"deleted": "ブラウザの設定を削除しました:{name}", "deleted": "ブラウザの設定を削除しました:{name}",
"downloadEmpty": "ダウンロードできる設定がありません", "downloadEmpty": "ダウンロードできる設定がありません",
"downloaded": "ダウンロードしました:{name}" "downloaded": "ダウンロードしました:{name}"
@@ -501,6 +504,14 @@
"addPathShort": "+ パス", "addPathShort": "+ パス",
"addSpine": "Spineを追加", "addSpine": "Spineを追加",
"copyTitle": "現在選択されているシーン オブジェクトをコピーします", "copyTitle": "現在選択されているシーン オブジェクトをコピーします",
"contextCopy": "クリップボードへコピー",
"contextPaste": "貼り付けて上書き",
"clipboardUnavailable": "このブラウザではクリップボードにアクセスできません",
"clipboardInvalid": "クリップボードに有効なシーンオブジェクトがありません",
"clipboardCopyFailed": "シーンオブジェクトをクリップボードへコピーできませんでした",
"clipboardReadFailed": "クリップボードを読み取れません。ブラウザの権限を確認してください。",
"clipboardTypeMismatch": "同じ種類のシーンオブジェクトだけを上書きできます(クリップボード: {source}、現在: {target})",
"clipboardPasteFailed": "シーンオブジェクトを復元できませんでした",
"showParticle": "パーティクル システムを表示する", "showParticle": "パーティクル システムを表示する",
"hideParticle": "パーティクル システムを非表示にする", "hideParticle": "パーティクル システムを非表示にする",
"showCollider": "コライダーを表示", "showCollider": "コライダーを表示",
@@ -802,6 +813,8 @@
"eye": "小さな目", "eye": "小さな目",
"eyeDesc": "オブジェクトを個別に表示/非表示にします。非表示のパーティクル システムはエクスポートされません。", "eyeDesc": "オブジェクトを個別に表示/非表示にします。非表示のパーティクル システムはエクスポートされません。",
"copyDesc": "現在選択されているオブジェクトを完全にコピーして、さまざまなパラメータの比較を容易にします。", "copyDesc": "現在選択されているオブジェクトを完全にコピーして、さまざまなパラメータの比較を容易にします。",
"sceneClipboard": "シーンクリップボード",
"sceneClipboardDesc": "シーンオブジェクトを右クリックすると、クリップボードへのコピーや貼り付けによる上書きができます。",
"backgroundDesc": "参照画像をアップロードし、キャンバスプレビューのみ位置と透明度を調整します。", "backgroundDesc": "参照画像をアップロードし、キャンバスプレビューのみ位置と透明度を調整します。",
"settingsDesc": "管理インターフェイスのレイアウト、補助表示、スケールなどはブラウザに自動的に保存されます。", "settingsDesc": "管理インターフェイスのレイアウト、補助表示、スケールなどはブラウザに自動的に保存されます。",
"undoDesc": "Ctrl+Z は 1 ステップ後退します。最大ステップ数はシステム設定で調整できます。", "undoDesc": "Ctrl+Z は 1 ステップ後退します。最大ステップ数はシステム設定で調整できます。",
+13
View File
@@ -164,6 +164,9 @@
"storageUnavailable": "브라우저 로컬 저장소를 사용할 수 없습니다", "storageUnavailable": "브라우저 로컬 저장소를 사용할 수 없습니다",
"saved": "브라우저에 저장했습니다: {name}", "saved": "브라우저에 저장했습니다: {name}",
"loaded": "브라우저에서 불러왔습니다: {name}", "loaded": "브라우저에서 불러왔습니다: {name}",
"appendToScene": "이 구성을 장면에 추가",
"appending": "구성을 추가하는 중: {name}",
"appended": "장면에 추가했습니다: {name}",
"deleted": "브라우저 설정을 삭제했습니다: {name}", "deleted": "브라우저 설정을 삭제했습니다: {name}",
"downloadEmpty": "다운로드할 설정이 없습니다", "downloadEmpty": "다운로드할 설정이 없습니다",
"downloaded": "다운로드했습니다: {name}" "downloaded": "다운로드했습니다: {name}"
@@ -501,6 +504,14 @@
"addPathShort": "+ 경로", "addPathShort": "+ 경로",
"addSpine": "Spine 추가", "addSpine": "Spine 추가",
"copyTitle": "현재 선택된 장면 객체를 복사합니다.", "copyTitle": "현재 선택된 장면 객체를 복사합니다.",
"contextCopy": "클립보드로 복사",
"contextPaste": "붙여넣어 덮어쓰기",
"clipboardUnavailable": "이 브라우저에서는 클립보드에 접근할 수 없습니다",
"clipboardInvalid": "클립보드에 사용할 수 있는 장면 객체가 없습니다",
"clipboardCopyFailed": "장면 객체를 클립보드로 복사하지 못했습니다",
"clipboardReadFailed": "클립보드를 읽을 수 없습니다. 브라우저 권한을 확인하세요.",
"clipboardTypeMismatch": "같은 유형의 장면 객체만 덮어쓸 수 있습니다(클립보드: {source}, 현재: {target})",
"clipboardPasteFailed": "장면 객체를 복원하지 못했습니다",
"showParticle": "입자 시스템 표시", "showParticle": "입자 시스템 표시",
"hideParticle": "입자 시스템 숨기기", "hideParticle": "입자 시스템 숨기기",
"showCollider": "충돌체 표시", "showCollider": "충돌체 표시",
@@ -802,6 +813,8 @@
"eye": "작은 눈", "eye": "작은 눈",
"eyeDesc": "개체를 개별적으로 표시하거나 숨깁니다. 숨겨진 입자 시스템은 내보내지지 않습니다.", "eyeDesc": "개체를 개별적으로 표시하거나 숨깁니다. 숨겨진 입자 시스템은 내보내지지 않습니다.",
"copyDesc": "다양한 매개변수의 비교를 용이하게 하기 위해 현재 선택된 개체를 완전히 복사합니다.", "copyDesc": "다양한 매개변수의 비교를 용이하게 하기 위해 현재 선택된 개체를 완전히 복사합니다.",
"sceneClipboard": "장면 클립보드",
"sceneClipboardDesc": "장면 객체를 마우스 오른쪽 버튼으로 클릭하여 클립보드로 복사하거나 붙여넣어 덮어쓸 수 있습니다.",
"backgroundDesc": "참조 이미지를 업로드하고 캔버스 미리보기 전용 위치와 투명도를 조정하세요.", "backgroundDesc": "참조 이미지를 업로드하고 캔버스 미리보기 전용 위치와 투명도를 조정하세요.",
"settingsDesc": "관리 인터페이스 레이아웃, 보조 디스플레이, 규모 등은 브라우저에 자동으로 저장됩니다.", "settingsDesc": "관리 인터페이스 레이아웃, 보조 디스플레이, 규모 등은 브라우저에 자동으로 저장됩니다.",
"undoDesc": "Ctrl+Z는 한 단계 후퇴합니다. 최대 단계 수는 시스템 설정에서 조정할 수 있습니다.", "undoDesc": "Ctrl+Z는 한 단계 후퇴합니다. 최대 단계 수는 시스템 설정에서 조정할 수 있습니다.",
+13
View File
@@ -164,6 +164,9 @@
"storageUnavailable": "O armazenamento local do navegador não está disponível", "storageUnavailable": "O armazenamento local do navegador não está disponível",
"saved": "Salvo no navegador: {name}", "saved": "Salvo no navegador: {name}",
"loaded": "Carregado do navegador: {name}", "loaded": "Carregado do navegador: {name}",
"appendToScene": "Adicionar esta configuração à cena",
"appending": "Adicionando configuração: {name}",
"appended": "Adicionado à cena: {name}",
"deleted": "Configuração excluída do navegador: {name}", "deleted": "Configuração excluída do navegador: {name}",
"downloadEmpty": "Nenhuma configuração para baixar", "downloadEmpty": "Nenhuma configuração para baixar",
"downloaded": "Baixado: {name}" "downloaded": "Baixado: {name}"
@@ -501,6 +504,14 @@
"addPathShort": "+ caminho", "addPathShort": "+ caminho",
"addSpine": "Adicionar Spine", "addSpine": "Adicionar Spine",
"copyTitle": "Copie o objeto de cena atualmente selecionado", "copyTitle": "Copie o objeto de cena atualmente selecionado",
"contextCopy": "Copiar para a área de transferência",
"contextPaste": "Colar e substituir",
"clipboardUnavailable": "Este navegador não permite acessar a área de transferência",
"clipboardInvalid": "A área de transferência não contém um objeto de cena válido",
"clipboardCopyFailed": "Não foi possível copiar o objeto de cena para a área de transferência",
"clipboardReadFailed": "Não foi possível ler a área de transferência. Verifique as permissões do navegador.",
"clipboardTypeMismatch": "Somente objetos do mesmo tipo podem ser substituídos (área de transferência: {source}, atual: {target})",
"clipboardPasteFailed": "Não foi possível restaurar o objeto de cena",
"showParticle": "Mostrar sistema de partículas", "showParticle": "Mostrar sistema de partículas",
"hideParticle": "Ocultar sistema de partículas", "hideParticle": "Ocultar sistema de partículas",
"showCollider": "mostrar colisores", "showCollider": "mostrar colisores",
@@ -802,6 +813,8 @@
"eye": "olhos pequenos", "eye": "olhos pequenos",
"eyeDesc": "Mostrar/ocultar objetos individualmente; sistemas de partículas ocultas não são exportados.", "eyeDesc": "Mostrar/ocultar objetos individualmente; sistemas de partículas ocultas não são exportados.",
"copyDesc": "Copia completamente o objeto atualmente selecionado para facilitar a comparação de diferentes parâmetros.", "copyDesc": "Copia completamente o objeto atualmente selecionado para facilitar a comparação de diferentes parâmetros.",
"sceneClipboard": "Área de transferência da cena",
"sceneClipboardDesc": "Clique com o botão direito em um objeto de cena para copiá-lo ou substituí-lo colando.",
"backgroundDesc": "Carregue a imagem de referência e ajuste a posição e a transparência apenas para visualização da tela.", "backgroundDesc": "Carregue a imagem de referência e ajuste a posição e a transparência apenas para visualização da tela.",
"settingsDesc": "Layout da interface de gerenciamento, display auxiliar, balança, etc. são salvos automaticamente no navegador.", "settingsDesc": "Layout da interface de gerenciamento, display auxiliar, balança, etc. são salvos automaticamente no navegador.",
"undoDesc": "Ctrl+Z retrai um passo; o número máximo de etapas pode ser ajustado nas configurações do sistema.", "undoDesc": "Ctrl+Z retrai um passo; o número máximo de etapas pode ser ajustado nas configurações do sistema.",
+13
View File
@@ -176,6 +176,9 @@
"storageUnavailable": "Локальное хранилище браузера недоступно", "storageUnavailable": "Локальное хранилище браузера недоступно",
"saved": "Сохранено в браузере: {name}", "saved": "Сохранено в браузере: {name}",
"loaded": "Загружено из браузера: {name}", "loaded": "Загружено из браузера: {name}",
"appendToScene": "Добавить эту конфигурацию в сцену",
"appending": "Добавление конфигурации: {name}",
"appended": "Добавлено в сцену: {name}",
"deleted": "Удалено из кэша браузера: {name}", "deleted": "Удалено из кэша браузера: {name}",
"downloadEmpty": "Нет конфигураций для скачивания", "downloadEmpty": "Нет конфигураций для скачивания",
"downloaded": "Скачано: {name}" "downloaded": "Скачано: {name}"
@@ -513,6 +516,14 @@
"addPathShort": "+Путь", "addPathShort": "+Путь",
"addSpine": "Добавить Spine", "addSpine": "Добавить Spine",
"copyTitle": "Дублировать выбранный объект сцены", "copyTitle": "Дублировать выбранный объект сцены",
"contextCopy": "Копировать в буфер обмена",
"contextPaste": "Вставить с заменой",
"clipboardUnavailable": "Браузер не разрешает доступ к буферу обмена",
"clipboardInvalid": "В буфере обмена нет допустимого объекта сцены",
"clipboardCopyFailed": "Не удалось скопировать объект сцены в буфер обмена",
"clipboardReadFailed": "Не удалось прочитать буфер обмена. Проверьте разрешения браузера.",
"clipboardTypeMismatch": "Можно заменять только объект того же типа (в буфере: {source}, текущий: {target})",
"clipboardPasteFailed": "Не удалось восстановить объект сцены",
"showParticle": "Показать систему частиц", "showParticle": "Показать систему частиц",
"hideParticle": "Скрыть систему частиц", "hideParticle": "Скрыть систему частиц",
"showCollider": "Показать коллайдер", "showCollider": "Показать коллайдер",
@@ -825,6 +836,8 @@
"eye": "Видимость", "eye": "Видимость",
"eyeDesc": "Показывает или скрывает отдельный объект. Скрытые системы частиц не экспортируются.", "eyeDesc": "Показывает или скрывает отдельный объект. Скрытые системы частиц не экспортируются.",
"copyDesc": "Полностью дублирует выбранный объект для сравнения параметров.", "copyDesc": "Полностью дублирует выбранный объект для сравнения параметров.",
"sceneClipboard": "Буфер объектов сцены",
"sceneClipboardDesc": "Щёлкните объект сцены правой кнопкой, чтобы скопировать его в буфер или заменить вставкой.",
"backgroundDesc": "Загружает опорное изображение и позволяет настроить его положение и непрозрачность только для предпросмотра на холсте.", "backgroundDesc": "Загружает опорное изображение и позволяет настроить его положение и непрозрачность только для предпросмотра на холсте.",
"settingsDesc": "Управление расположением интерфейса, вспомогательными элементами, делениями и другими настройками; значения автоматически сохраняются в браузере.", "settingsDesc": "Управление расположением интерфейса, вспомогательными элементами, делениями и другими настройками; значения автоматически сохраняются в браузере.",
"undoDesc": "Ctrl+Z отменяет одно действие. Максимальное число шагов задаётся в системных настройках.", "undoDesc": "Ctrl+Z отменяет одно действие. Максимальное число шагов задаётся в системных настройках.",
+13
View File
@@ -176,6 +176,9 @@
"storageUnavailable": "浏览器本地存储不可用", "storageUnavailable": "浏览器本地存储不可用",
"saved": "已保存到浏览器:{name}", "saved": "已保存到浏览器:{name}",
"loaded": "已从浏览器加载:{name}", "loaded": "已从浏览器加载:{name}",
"appendToScene": "将当前配置追加到场景中",
"appending": "正在追加配置:{name}",
"appended": "已追加到场景:{name}",
"deleted": "已删除缓存配置:{name}", "deleted": "已删除缓存配置:{name}",
"downloadEmpty": "没有可下载的配置", "downloadEmpty": "没有可下载的配置",
"downloaded": "已下载:{name}" "downloaded": "已下载:{name}"
@@ -513,6 +516,14 @@
"addPathShort": "+路径", "addPathShort": "+路径",
"addSpine": "添加 Spine", "addSpine": "添加 Spine",
"copyTitle": "复制当前选中的场景对象", "copyTitle": "复制当前选中的场景对象",
"contextCopy": "复制到剪贴板",
"contextPaste": "粘贴覆盖",
"clipboardUnavailable": "当前浏览器不允许访问剪贴板",
"clipboardInvalid": "剪贴板中没有可用的场景对象",
"clipboardCopyFailed": "复制场景对象到剪贴板失败",
"clipboardReadFailed": "无法读取剪贴板,请检查浏览器权限",
"clipboardTypeMismatch": "只能覆盖同类型场景对象(剪贴板为 {source},当前为 {target})",
"clipboardPasteFailed": "场景对象恢复失败",
"showParticle": "显示粒子系统", "showParticle": "显示粒子系统",
"hideParticle": "隐藏粒子系统", "hideParticle": "隐藏粒子系统",
"showCollider": "显示碰撞体", "showCollider": "显示碰撞体",
@@ -825,6 +836,8 @@
"eye": "小眼睛", "eye": "小眼睛",
"eyeDesc": "单独显示/隐藏对象;隐藏的粒子系统不会被导出。", "eyeDesc": "单独显示/隐藏对象;隐藏的粒子系统不会被导出。",
"copyDesc": "完整复制当前选中对象,方便对比不同参数。", "copyDesc": "完整复制当前选中对象,方便对比不同参数。",
"sceneClipboard": "场景剪贴板",
"sceneClipboardDesc": "右键场景对象可复制到剪贴板或覆盖粘贴。",
"backgroundDesc": "上传参考图并调整位置和透明度,只用于画布预览。", "backgroundDesc": "上传参考图并调整位置和透明度,只用于画布预览。",
"settingsDesc": "管理界面布局、辅助显示、刻度等,自动存入浏览器。", "settingsDesc": "管理界面布局、辅助显示、刻度等,自动存入浏览器。",
"undoDesc": "Ctrl+Z 撤回一步;最大步数可在系统设置中调整。", "undoDesc": "Ctrl+Z 撤回一步;最大步数可在系统设置中调整。",
+13
View File
@@ -164,6 +164,9 @@
"storageUnavailable": "瀏覽器本機儲存空間無法使用", "storageUnavailable": "瀏覽器本機儲存空間無法使用",
"saved": "已儲存到瀏覽器:{name}", "saved": "已儲存到瀏覽器:{name}",
"loaded": "已從瀏覽器載入:{name}", "loaded": "已從瀏覽器載入:{name}",
"appendToScene": "將目前設定追加到場景中",
"appending": "正在追加設定:{name}",
"appended": "已追加到場景:{name}",
"deleted": "已刪除瀏覽器設定:{name}", "deleted": "已刪除瀏覽器設定:{name}",
"downloadEmpty": "沒有可下載的設定", "downloadEmpty": "沒有可下載的設定",
"downloaded": "已下載:{name}" "downloaded": "已下載:{name}"
@@ -501,6 +504,14 @@
"addPathShort": "+路徑", "addPathShort": "+路徑",
"addSpine": "加入 Spine", "addSpine": "加入 Spine",
"copyTitle": "複製目前選取的場景對象", "copyTitle": "複製目前選取的場景對象",
"contextCopy": "複製到剪貼簿",
"contextPaste": "貼上覆蓋",
"clipboardUnavailable": "目前瀏覽器不允許存取剪貼簿",
"clipboardInvalid": "剪貼簿中沒有可用的場景物件",
"clipboardCopyFailed": "無法複製場景物件到剪貼簿",
"clipboardReadFailed": "無法讀取剪貼簿,請檢查瀏覽器權限",
"clipboardTypeMismatch": "只能覆蓋同類型場景物件(剪貼簿為 {source},目前為 {target})",
"clipboardPasteFailed": "場景物件還原失敗",
"showParticle": "顯示粒子系統", "showParticle": "顯示粒子系統",
"hideParticle": "隱藏粒子系統", "hideParticle": "隱藏粒子系統",
"showCollider": "顯示碰撞體", "showCollider": "顯示碰撞體",
@@ -802,6 +813,8 @@
"eye": "小眼睛", "eye": "小眼睛",
"eyeDesc": "單獨顯示/隱藏物件;隱藏的粒子系統不會被匯出。", "eyeDesc": "單獨顯示/隱藏物件;隱藏的粒子系統不會被匯出。",
"copyDesc": "完整複製目前選取對象,方便比較不同參數。", "copyDesc": "完整複製目前選取對象,方便比較不同參數。",
"sceneClipboard": "場景剪貼簿",
"sceneClipboardDesc": "在場景物件上按右鍵,即可複製到剪貼簿或貼上覆蓋。",
"backgroundDesc": "上傳參考圖並調整位置和透明度,只用於畫布預覽。", "backgroundDesc": "上傳參考圖並調整位置和透明度,只用於畫布預覽。",
"settingsDesc": "管理介面佈局、輔助顯示、刻度等,自動存入瀏覽器。", "settingsDesc": "管理介面佈局、輔助顯示、刻度等,自動存入瀏覽器。",
"undoDesc": "Ctrl+Z 撤回一步;最大步數可在系統設定中調整。", "undoDesc": "Ctrl+Z 撤回一步;最大步數可在系統設定中調整。",
+1
View File
@@ -49,6 +49,7 @@
<dl class="help-list"> <dl class="help-list">
<div><dt>{{ $t('help.eye') }}</dt><dd>{{ $t('help.eyeDesc') }}</dd></div> <div><dt>{{ $t('help.eye') }}</dt><dd>{{ $t('help.eyeDesc') }}</dd></div>
<div><dt>+Copy</dt><dd>{{ $t('help.copyDesc') }}</dd></div> <div><dt>+Copy</dt><dd>{{ $t('help.copyDesc') }}</dd></div>
<div><dt>{{ $t('help.sceneClipboard') }}</dt><dd>{{ $t('help.sceneClipboardDesc') }}</dd></div>
<div><dt>{{ $t('toolbar.background') }}</dt><dd>{{ $t('help.backgroundDesc') }}</dd></div> <div><dt>{{ $t('toolbar.background') }}</dt><dd>{{ $t('help.backgroundDesc') }}</dd></div>
<div><dt>{{ $t('toolbar.settings') }}</dt><dd>{{ $t('help.settingsDesc') }}</dd></div> <div><dt>{{ $t('toolbar.settings') }}</dt><dd>{{ $t('help.settingsDesc') }}</dd></div>
<div><dt>{{ $t('help.undoShortcut') }}</dt><dd>{{ $t('help.undoDesc') }}</dd></div> <div><dt>{{ $t('help.undoShortcut') }}</dt><dd>{{ $t('help.undoDesc') }}</dd></div>
+16 -2
View File
@@ -93,7 +93,7 @@
<div class="tl-playhead" :style="{ left: playPx + 'px' }"></div> <div class="tl-playhead" :style="{ left: playPx + 'px' }"></div>
<!-- 每系统一条轨道(粒子条) --> <!-- 每系统一条轨道(粒子条) -->
<div class="tl-lanes"> <div class="tl-lanes" @wheel.stop="onLaneWheel">
<div v-for="(sys, i) in store.systems" :key="sys.id" class="tl-lane"> <div v-for="(sys, i) in store.systems" :key="sys.id" class="tl-lane">
<span class="lane-ico" :class="{ 'sub-lane-ico': isSubEmitter(sys) }" :style="{ left: startMarkerPx(sys) + 'px' }">●</span> <span class="lane-ico" :class="{ 'sub-lane-ico': isSubEmitter(sys) }" :style="{ left: startMarkerPx(sys) + 'px' }">●</span>
<!-- 粒子条:left=delay;红色底条=系统存在粒子的总时长;绿色覆盖层=持续发射时长 --> <!-- 粒子条:left=delay;红色底条=系统存在粒子的总时长;绿色覆盖层=持续发射时长 -->
@@ -405,6 +405,13 @@ function onWheel(e: WheelEvent) {
viewFrames.value = Math.round(nv) viewFrames.value = Math.round(nv)
} }
function onLaneWheel(e: WheelEvent) {
const lanes = e.currentTarget as HTMLElement
if (lanes.scrollHeight > lanes.clientHeight) return
e.preventDefault()
onWheel(e)
}
// 拖动时间轴滑动(scrub):只允许进入实际播放范围。 // 拖动时间轴滑动(scrub):只允许进入实际播放范围。
function onScrubDown(e: PointerEvent) { function onScrubDown(e: PointerEvent) {
const vp = vpRef.value! const vp = vpRef.value!
@@ -580,7 +587,14 @@ function onSpineBarDown(e: PointerEvent, spine: SpineSceneObject) {
.tl-tickline { position: absolute; top: 0; bottom: 0; width: 1px; background: #263040; } .tl-tickline { position: absolute; top: 0; bottom: 0; width: 1px; background: #263040; }
.tl-ticklabel { position: absolute; top: 2px; transform: translateX(-50%); font-size: 9px; color: #667; } .tl-ticklabel { position: absolute; top: 2px; transform: translateX(-50%); font-size: 9px; color: #667; }
.tl-playhead { position: absolute; top: 0; bottom: 0; width: 2px; background: #ffcc33; z-index: 5; pointer-events: none; } .tl-playhead { position: absolute; top: 0; bottom: 0; width: 2px; background: #ffcc33; z-index: 5; pointer-events: none; }
.tl-lanes { position: absolute; top: 22px; left: 0; right: 0; bottom: 0; } .tl-lanes {
position: absolute; top: 22px; left: 0; right: 0; bottom: 0;
overflow-x: hidden; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #46516b #0d0d14;
}
.tl-lanes::-webkit-scrollbar { width: 8px; }
.tl-lanes::-webkit-scrollbar-track { background: #0d0d14; }
.tl-lanes::-webkit-scrollbar-thumb { border-radius: 4px; background: #46516b; }
.tl-lanes::-webkit-scrollbar-thumb:hover { background: #5d6a88; }
.tl-lane { position: relative; height: 26px; margin-bottom: 4px; } .tl-lane { position: relative; height: 26px; margin-bottom: 4px; }
.lane-ico { position: absolute; left: 4px; top: 6px; font-size: 11px; color: #dde; z-index: 2; pointer-events: none; } .lane-ico { position: absolute; left: 4px; top: 6px; font-size: 11px; color: #dde; z-index: 2; pointer-events: none; }
.lane-ico.sub-lane-ico { color: #7f8999; } .lane-ico.sub-lane-ico { color: #7f8999; }
@@ -31,6 +31,7 @@
@dblclick="loadLocalProject(project)" @dblclick="loadLocalProject(project)"
@keydown.enter.prevent="loadLocalProject(project)" @keydown.enter.prevent="loadLocalProject(project)"
@keydown.space.prevent="selectLocalProject(project)" @keydown.space.prevent="selectLocalProject(project)"
@contextmenu.prevent.stop="openProjectContextMenu($event, project)"
> >
<span class="project-name">{{ project.name }}</span> <span class="project-name">{{ project.name }}</span>
<span v-if="store.settings.showProjectSize" class="project-size">{{ formatProjectSize(project.size) }}</span> <span v-if="store.settings.showProjectSize" class="project-size">{{ formatProjectSize(project.size) }}</span>
@@ -95,10 +96,25 @@
</form> </form>
</div> </div>
</Teleport> </Teleport>
<Teleport to="body">
<div
v-if="projectContextMenu.open"
ref="projectContextMenuElement"
class="project-context-menu"
role="menu"
:style="{ left: projectContextMenu.x + 'px', top: projectContextMenu.y + 'px' }"
@pointerdown.stop
@contextmenu.prevent
>
<button type="button" role="menuitem" @click="appendProjectFromContextMenu">{{ $t('config.local.appendToScene') }}</button>
</div>
</Teleport>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { LocalProject } from '../../editor/projectLibrary'
import NumSlider from '../NumSlider.vue' import NumSlider from '../NumSlider.vue'
import CurveEditor from '../CurveEditor.vue' import CurveEditor from '../CurveEditor.vue'
import ParticleAttributeControl from '../ParticleAttributeControl.vue' import ParticleAttributeControl from '../ParticleAttributeControl.vue'
@@ -111,12 +127,65 @@ const {
store, store,
configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError,
localProjects, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy, localProjects, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy,
saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject, saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, appendLocalProject, downloadLocalProject,
downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, projectFilesInput, loadAllProjectFiles, downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, projectFilesInput, loadAllProjectFiles,
selectedLocalProjectName, selectLocalProject, onConfigFileChange, refreshPresets, selectedLocalProjectName, selectLocalProject, onConfigFileChange, refreshPresets,
loadSelectedPreset, collapsed, toggle, sectionOpen, select, loadSelectedPreset, collapsed, toggle, sectionOpen, select,
} = useParticlePanelContext() } = useParticlePanelContext()
const { locale } = useI18n() const { locale } = useI18n()
const projectContextMenuElement = ref<HTMLElement | null>(null)
const projectContextMenu = reactive({
open: false,
x: 0,
y: 0,
project: null as LocalProject | null,
})
function closeProjectContextMenu() {
projectContextMenu.open = false
projectContextMenu.project = null
}
async function openProjectContextMenu(event: MouseEvent, project: LocalProject) {
selectLocalProject(project)
Object.assign(projectContextMenu, { open: true, x: event.clientX, y: event.clientY, project })
await nextTick()
const menu = projectContextMenuElement.value
if (!menu) return
const bounds = menu.getBoundingClientRect()
projectContextMenu.x = Math.max(8, Math.min(event.clientX, window.innerWidth - bounds.width - 8))
projectContextMenu.y = Math.max(8, Math.min(event.clientY, window.innerHeight - bounds.height - 8))
}
function appendProjectFromContextMenu() {
const project = projectContextMenu.project
closeProjectContextMenu()
if (project) void appendLocalProject(project)
}
function onProjectMenuPointerDown(event: PointerEvent) {
if (projectContextMenu.open && !projectContextMenuElement.value?.contains(event.target as Node)) {
closeProjectContextMenu()
}
}
function onProjectMenuKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') closeProjectContextMenu()
}
onMounted(() => {
window.addEventListener('pointerdown', onProjectMenuPointerDown, true)
window.addEventListener('keydown', onProjectMenuKeyDown, true)
window.addEventListener('scroll', closeProjectContextMenu, true)
window.addEventListener('blur', closeProjectContextMenu)
})
onBeforeUnmount(() => {
window.removeEventListener('pointerdown', onProjectMenuPointerDown, true)
window.removeEventListener('keydown', onProjectMenuKeyDown, true)
window.removeEventListener('scroll', closeProjectContextMenu, true)
window.removeEventListener('blur', closeProjectContextMenu)
})
function formatProjectSize(bytes: number) { function formatProjectSize(bytes: number) {
const kb = Math.max(1, bytes / 1024) const kb = Math.max(1, bytes / 1024)
if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB` if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`
@@ -129,6 +198,18 @@ function presetDisplayName(preset: PresetOption) {
</script> </script>
<style scoped> <style scoped>
.project-context-menu {
position: fixed; z-index: 10000; min-width: 180px; padding: 4px;
border: 1px solid #3d4962; border-radius: 7px; background: #171d2a;
box-shadow: 0 10px 28px rgba(0, 0, 0, .48);
}
.project-context-menu button {
display: block; width: 100%; height: 30px; padding: 0 10px; border: 0; border-radius: 5px;
background: transparent; color: #d5dced; font: inherit; font-size: 12px; text-align: left; white-space: nowrap; cursor: pointer;
}
.project-context-menu button:hover, .project-context-menu button:focus-visible {
outline: none; background: #2d3953; color: #fff;
}
.config-management-body { padding-bottom: 2px; } .config-management-body { padding-bottom: 2px; }
.group-download { .group-download {
max-width: 50%; min-height: 24px; margin-left: auto; padding: 3px 8px; border: 1px solid #485673; border-radius: 5px; max-width: 50%; min-height: 24px; margin-left: auto; padding: 3px 8px; border: 1px solid #485673; border-radius: 5px;
@@ -20,6 +20,7 @@
:class="{ on: activeObjectType === 'particle' && entry.system.id === store.activeObjectId, 'scene-object-child': entry.depth > 0 }" :class="{ on: activeObjectType === 'particle' && entry.system.id === store.activeObjectId, 'scene-object-child': entry.depth > 0 }"
:style="entry.depth > 0 ? { paddingLeft: `${entry.depth * 18}px` } : undefined" :style="entry.depth > 0 ? { paddingLeft: `${entry.depth * 18}px` } : undefined"
@click="store.selectSystem(entry.system.id)" @click="store.selectSystem(entry.system.id)"
@contextmenu.prevent.stop="openSceneObjectMenu($event, 'particle', entry.system.id)"
> >
<span v-if="entry.depth > 0" class="sub-branch">└</span> <span v-if="entry.depth > 0" class="sub-branch">└</span>
<span v-else class="tdot">●</span> <span v-else class="tdot">●</span>
@@ -36,6 +37,7 @@
class="tree-item scene-object-item" class="tree-item scene-object-item"
:class="{ on: activeObjectType === 'collision' && collider.id === store.activeObjectId }" :class="{ on: activeObjectType === 'collision' && collider.id === store.activeObjectId }"
@click="store.selectSceneObject('collision', collider.id)" @click="store.selectSceneObject('collision', collider.id)"
@contextmenu.prevent.stop="openSceneObjectMenu($event, 'collision', collider.id)"
> >
<span class="tdot collision-dot">■</span> <span class="tdot collision-dot">■</span>
<span class="tlabel">{{ collider.name }}</span> <span class="tlabel">{{ collider.name }}</span>
@@ -51,6 +53,7 @@
class="tree-item scene-object-item" class="tree-item scene-object-item"
:class="{ on: activeObjectType === 'path' && path.id === store.activeObjectId }" :class="{ on: activeObjectType === 'path' && path.id === store.activeObjectId }"
@click="store.selectSceneObject('path', path.id)" @click="store.selectSceneObject('path', path.id)"
@contextmenu.prevent.stop="openSceneObjectMenu($event, 'path', path.id)"
> >
<span class="tdot path-dot">◇</span> <span class="tdot path-dot">◇</span>
<span class="tlabel">{{ path.name }}</span> <span class="tlabel">{{ path.name }}</span>
@@ -66,6 +69,7 @@
class="tree-item scene-object-item" class="tree-item scene-object-item"
:class="{ on: activeObjectType === 'spine' && spine.id === store.activeObjectId }" :class="{ on: activeObjectType === 'spine' && spine.id === store.activeObjectId }"
@click="store.selectSceneObject('spine', spine.id)" @click="store.selectSceneObject('spine', spine.id)"
@contextmenu.prevent.stop="openSceneObjectMenu($event, 'spine', spine.id)"
> >
<span class="tdot spine-dot">◆</span> <span class="tdot spine-dot">◆</span>
<span class="tlabel">{{ spine.name }}</span> <span class="tlabel">{{ spine.name }}</span>
@@ -199,10 +203,24 @@
<div v-else class="panel-hint">{{ $t('scene.panelHint') }}</div> <div v-else class="panel-hint">{{ $t('scene.panelHint') }}</div>
</div> </div>
</div> </div>
<Teleport to="body">
<div
v-if="sceneContextMenu.open"
ref="sceneContextMenuElement"
class="scene-context-menu"
role="menu"
:style="{ left: sceneContextMenu.x + 'px', top: sceneContextMenu.y + 'px' }"
@pointerdown.stop
@contextmenu.prevent
>
<button type="button" role="menuitem" @click="copyFromSceneContextMenu">{{ $t('scene.contextCopy') }}</button>
<button type="button" role="menuitem" @click="pasteFromSceneContextMenu">{{ $t('scene.contextPaste') }}</button>
</div>
</Teleport>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue' import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import NumSlider from '../NumSlider.vue' import NumSlider from '../NumSlider.vue'
import CurveEditor from '../CurveEditor.vue' import CurveEditor from '../CurveEditor.vue'
import ParticleAttributeControl from '../ParticleAttributeControl.vue' import ParticleAttributeControl from '../ParticleAttributeControl.vue'
@@ -210,11 +228,80 @@ import ColorGradientEditor from '../ColorGradientEditor.vue'
import ImageResourceCard from '../ImageResourceCard.vue' import ImageResourceCard from '../ImageResourceCard.vue'
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue' import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
import { collectSubEmitterIds } from '../../core/subEmitterIds' import { collectSubEmitterIds } from '../../core/subEmitterIds'
import { copySceneObjectToClipboard, pasteSceneObjectFromClipboard } from '../../editor/sceneObjectClipboard'
import type { SceneObjectType } from '../../store/particleStore'
import { useParticlePanelContext } from './particlePanelContext' import { useParticlePanelContext } from './particlePanelContext'
const { store, resolvedCurveEditorMode, sys, activeObjectType, activeCollider, activePath, activeSpine, canCopyObject, collapsed, toggle, sectionOpen, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, LINEAR_CURVE } = useParticlePanelContext() const { store, resolvedCurveEditorMode, sys, activeObjectType, activeCollider, activePath, activeSpine, canCopyObject, collapsed, toggle, sectionOpen, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, LINEAR_CURVE } = useParticlePanelContext()
type DisplaySystem = { system: (typeof store.systems)[number]; depth: number } type DisplaySystem = { system: (typeof store.systems)[number]; depth: number }
const sceneContextMenuElement = ref<HTMLElement | null>(null)
const sceneContextMenu = reactive({
open: false,
x: 0,
y: 0,
type: 'particle' as SceneObjectType,
id: 0,
})
function closeSceneContextMenu() {
sceneContextMenu.open = false
}
async function openSceneObjectMenu(event: MouseEvent, type: SceneObjectType, id: number) {
store.selectSceneObject(type, id)
Object.assign(sceneContextMenu, { open: true, x: event.clientX, y: event.clientY, type, id })
await nextTick()
const menu = sceneContextMenuElement.value
if (!menu) return
const bounds = menu.getBoundingClientRect()
sceneContextMenu.x = Math.max(8, Math.min(event.clientX, window.innerWidth - bounds.width - 8))
sceneContextMenu.y = Math.max(8, Math.min(event.clientY, window.innerHeight - bounds.height - 8))
}
async function runSceneContextAction(action: 'copy' | 'paste') {
const { type, id } = sceneContextMenu
closeSceneContextMenu()
try {
if (action === 'copy') await copySceneObjectToClipboard(store, type, id)
else await pasteSceneObjectFromClipboard(store, type, id)
} catch (error) {
window.alert(error instanceof Error ? error.message : String(error))
}
}
function copyFromSceneContextMenu() {
void runSceneContextAction('copy')
}
function pasteFromSceneContextMenu() {
void runSceneContextAction('paste')
}
function onSceneMenuPointerDown(event: PointerEvent) {
if (sceneContextMenu.open && !sceneContextMenuElement.value?.contains(event.target as Node)) {
closeSceneContextMenu()
}
}
function onSceneMenuKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') closeSceneContextMenu()
}
onMounted(() => {
window.addEventListener('pointerdown', onSceneMenuPointerDown, true)
window.addEventListener('keydown', onSceneMenuKeyDown, true)
window.addEventListener('scroll', closeSceneContextMenu, true)
window.addEventListener('blur', closeSceneContextMenu)
})
onBeforeUnmount(() => {
window.removeEventListener('pointerdown', onSceneMenuPointerDown, true)
window.removeEventListener('keydown', onSceneMenuKeyDown, true)
window.removeEventListener('scroll', closeSceneContextMenu, true)
window.removeEventListener('blur', closeSceneContextMenu)
})
const warmupDisabled = computed(() => !!sys.value && ( const warmupDisabled = computed(() => !!sys.value && (
collectSubEmitterIds(store.systems).has(sys.value.id) collectSubEmitterIds(store.systems).has(sys.value.id)
|| (sys.value.config.mode === 'stream' && sys.value.config.streamBehavior === 'loop') || (sys.value.config.mode === 'stream' && sys.value.config.streamBehavior === 'loop')
@@ -270,6 +357,18 @@ const displaySystems = computed<DisplaySystem[]>(() => {
</script> </script>
<style scoped> <style scoped>
.scene-context-menu {
position: fixed; z-index: 10000; min-width: 142px; padding: 4px;
border: 1px solid #3d4962; border-radius: 7px; background: #171d2a;
box-shadow: 0 10px 28px rgba(0, 0, 0, .48);
}
.scene-context-menu button {
display: block; width: 100%; height: 30px; padding: 0 10px; border: 0; border-radius: 5px;
background: transparent; color: #d5dced; font: inherit; font-size: 12px; text-align: left; cursor: pointer;
}
.scene-context-menu button:hover, .scene-context-menu button:focus-visible {
outline: none; background: #2d3953; color: #fff;
}
.panel { display: flex; width: 100%; height: 100%; flex-direction: column; overflow: hidden; background: #14141f; border-left: 1px solid #2a2a3c; padding: 12px; box-sizing: border-box; } .panel { display: flex; width: 100%; height: 100%; flex-direction: column; overflow: hidden; background: #14141f; border-left: 1px solid #2a2a3c; padding: 12px; box-sizing: border-box; }
.empty, .panel-hint { color: #667; font-size: 12px; padding: 8px 0; } .empty, .panel-hint { color: #667; font-size: 12px; padding: 8px 0; }
.group { border: 1px solid #2a2a3c; border-radius: 8px; margin-bottom: 10px; overflow: hidden; } .group { border: 1px solid #2a2a3c; border-radius: 8px; margin-bottom: 10px; overflow: hidden; }
+2 -2
View File
@@ -41,7 +41,7 @@ const { exportSettings, exportBusy, exportTarget, exportMessage, exportError, co
const { const {
configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError,
localProjects, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy, localProjects, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy,
saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject, downloadCurrentProject, saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, appendLocalProject, downloadLocalProject, downloadCurrentProject,
removeLocalProject, downloadAllLocalProjects, projectFilesInput, loadAllProjectFiles, removeLocalProject, downloadAllLocalProjects, projectFilesInput, loadAllProjectFiles,
selectedLocalProjectName, selectLocalProject, onConfigFileChange, refreshPresets, loadSelectedPreset, selectedLocalProjectName, selectLocalProject, onConfigFileChange, refreshPresets, loadSelectedPreset,
} = useProjectPresets(store) } = useProjectPresets(store)
@@ -657,5 +657,5 @@ function onSpawnPathChange() {
// 重置/删除图片 → 回到默认 star.png // 重置/删除图片 → 回到默认 star.png
return { store, exportSettings, exportBusy, exportTarget, exportMessage, exportError, connectorPairUrl, connectorStatusText, connectorStatusClass, connectorReady, connectorAuthorizationUrl, connectorDisabledReason, connectorButtonTitle, refreshConnectorHealth, exportSpineJson, exportSpineProject, exportSpineImages, configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, localProjects, selectedLocalProjectName, selectLocalProject, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy, projectFilesInput, saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject, downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, loadAllProjectFiles, onConfigFileChange, refreshPresets, loadSelectedPreset, resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, trailTextureFileInput, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, setImageMode, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, automaticLoopDuration, loopDurationFrames, activeObjectType, activeSection, canUseSection, activateSection, sectionVisible, sectionOpen, activeCollider, activePath, activeSpine, canCopyObject, selectedSpineBone, boundSpineBone, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerSlots, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, collisionPickerOpen, selectedCollisionBodies, availableCollisionBodies, addCollisionBody, removeCollisionBody, collapsed, toggle, setEmitMode, setLoopDurationFrames, setStreamBehavior, setLoopSection, setLoopSectionDuration, setLoopCustomDuration, onEmitterFollowModeChange, bindSelectedBone, resetModule, normalizeExportFps, select, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, setSpeedOverLifeMode, prepareScaleOverLifeCurve, setScaleOverLifeMode, onShapeChange, onSpawnPathChange, LINEAR_CURVE, FLAT_CURVE, ALPHA_CURVE, RESOURCE_ALPHA_CURVE, TRAIL_ALPHA_CURVE, TRAIL_SHAPE_CURVE } return { store, exportSettings, exportBusy, exportTarget, exportMessage, exportError, connectorPairUrl, connectorStatusText, connectorStatusClass, connectorReady, connectorAuthorizationUrl, connectorDisabledReason, connectorButtonTitle, refreshConnectorHealth, exportSpineJson, exportSpineProject, exportSpineImages, configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, localProjects, selectedLocalProjectName, selectLocalProject, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy, projectFilesInput, saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, appendLocalProject, downloadLocalProject, downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, loadAllProjectFiles, onConfigFileChange, refreshPresets, loadSelectedPreset, resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, trailTextureFileInput, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, setImageMode, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, automaticLoopDuration, loopDurationFrames, activeObjectType, activeSection, canUseSection, activateSection, sectionVisible, sectionOpen, activeCollider, activePath, activeSpine, canCopyObject, selectedSpineBone, boundSpineBone, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerSlots, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, collisionPickerOpen, selectedCollisionBodies, availableCollisionBodies, addCollisionBody, removeCollisionBody, collapsed, toggle, setEmitMode, setLoopDurationFrames, setStreamBehavior, setLoopSection, setLoopSectionDuration, setLoopCustomDuration, onEmitterFollowModeChange, bindSelectedBone, resetModule, normalizeExportFps, select, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, setSpeedOverLifeMode, prepareScaleOverLifeCurve, setScaleOverLifeMode, onShapeChange, onSpawnPathChange, LINEAR_CURVE, FLAT_CURVE, ALPHA_CURVE, RESOURCE_ALPHA_CURVE, TRAIL_ALPHA_CURVE, TRAIL_SHAPE_CURVE }
} }
+14 -1
View File
@@ -11,6 +11,7 @@ import ColorGradientEditor from '../ColorGradientEditor.vue'
import ImageResourceCard from '../ImageResourceCard.vue' import ImageResourceCard from '../ImageResourceCard.vue'
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue' import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
import { createEditorConfig, loadEditorConfigFile, loadEditorConfigText, parseEditorConfig } from '../../editor/editorConfig' import { createEditorConfig, loadEditorConfigFile, loadEditorConfigText, parseEditorConfig } from '../../editor/editorConfig'
import { appendEditorConfigText } from '../../editor/appendSceneConfig'
import { beginTransfer, readResponseTextWithProgress } from '../../editor/transferProgress' import { beginTransfer, readResponseTextWithProgress } from '../../editor/transferProgress'
import { defaultSpineExportSettings, ensureSpineExportSettings } from '../../export/spineExportSettings' import { defaultSpineExportSettings, ensureSpineExportSettings } from '../../export/spineExportSettings'
import { buildSpineJson, downloadSpineJson } from '../../export/spineJsonExporter' import { buildSpineJson, downloadSpineJson } from '../../export/spineJsonExporter'
@@ -128,6 +129,18 @@ async function loadLocalProject(project: LocalProject) {
} }
} }
async function appendLocalProject(project: LocalProject) {
configError.value = false
configMessage.value = translate('config.local.appending', { name: project.name })
try {
await appendEditorConfigText(store, project.text)
configMessage.value = translate('config.local.appended', { name: project.name })
} catch (error) {
configError.value = true
configMessage.value = error instanceof Error ? error.message : String(error)
}
}
function downloadLocalProject(project: LocalProject) { function downloadLocalProject(project: LocalProject) {
const fileName = projectJsonFileName(project.name) const fileName = projectJsonFileName(project.name)
downloadTextFile(project.text, fileName) downloadTextFile(project.text, fileName)
@@ -280,7 +293,7 @@ onBeforeUnmount(() => window.removeEventListener(PROJECT_SAVE_EVENT, saveConfig)
return { return {
configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError,
localProjects, selectedLocalProjectName, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy, localProjects, selectedLocalProjectName, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy,
saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject, saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, appendLocalProject, downloadLocalProject,
downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, projectFilesInput, loadAllProjectFiles, downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, projectFilesInput, loadAllProjectFiles,
selectLocalProject, onConfigFileChange, refreshPresets, loadSelectedPreset, selectLocalProject, onConfigFileChange, refreshPresets, loadSelectedPreset,
} }