渲染顺序功能
This commit is contained in:
1 parent
aabf4ff693
commit
c041503004
22 files changed
+1904
-1291
No files matched your search
@@ -1,2 +1,2 @@
|
||||
var ft=Object.defineProperty;var gt=(W,t,e)=>t in W?ft(W,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):W[t]=e;var T=(W,t,e)=>gt(W,typeof t!="symbol"?t+"":t,e);import{f as ht,R as pt,N as dt,ac as ut,Z as mt,b4 as yt,O as rt,b5 as at,U as D,B as Tt}from"./index-CBAQrWSZ.js";import{$ as Rt,a0 as Yt,a1 as Ft,a2 as Et,y as _t,a3 as Ot,a4 as kt,a5 as Xt,a6 as Bt,a7 as Nt,a8 as Dt,a9 as $t,aa as zt,ab as Wt,ad as qt,H as Gt,ae as Jt,af as Ht,ag as Zt,ah as Kt,ai as Qt,aj as jt,ak as te,al as ee,am as se,an as ne,ao as ie,ap as oe,aq as re,ar as ae,as as le,at as ce,au as he,av as pe,aw as de,ax as ue,ay as fe,az as ge,aA as me,aB as ye,aC as Te,aD as be,aE as we,aF as xe,aG as Ae,aH as Pe,aI as Ie,aJ as Ce,aK as Ve,aL as Le,aM as Se,aN as Me,aO as ve,aP as Ue,aQ as Re,aR as Ye,aS as Fe,aT as Ee,x as _e,aU as Oe,aV as ke,aW as Xe,aX as Be,aY as Ne,aZ as De,a_ as $e,a$ as ze,b0 as We,b1 as qe,b2 as Ge,b3 as Je,b6 as He,b7 as Ze,b8 as Ke,b9 as Qe,ba as je,bb as ts,bc as es,bd as ss,be as ns,bf as is,bg as os,bh as rs,bi as as,bj as ls,bk as cs,bl as hs,bm as ps,bn as ds,bo as us,bp as fs,bq as gs,br as ms,bs as ys,bt as Ts,bu as bs,w as ws,X as xs,J as As,Y as Ps,z as Is,bv as Cs,bw as Vs,bx as Ls,by as Ss,bz as Ms,bA as vs,bB as Us,bC as Rs,bD as Ys,bE as Fs,bF as Es,bG as _s,T as Os,bH as ks,bI as Xs,b as Bs,bJ as Ns,c as Ds,bK as $s,bL as zs,bM as Ws,bN as qs,bO as Gs,bP as Js,bQ as Hs,bR as Zs,bS as Ks,bT as Qs,bU as js,bV as tn,bW as en,bX as sn,bY as nn,bZ as on,b_ as rn,V as an,b$ as ln,c0 as cn,c1 as hn,c2 as pn,c3 as dn}from"./index-CBAQrWSZ.js";class It{constructor(t){T(this,"skeletonData");T(this,"animationToMixTime",{});T(this,"defaultMix",0);if(!t)throw new Error("skeletonData cannot be null.");this.skeletonData=t}setMix(t,e,n){return typeof t=="string"?this.setMix1(t,e,n):this.setMix2(t,e,n)}setMix1(t,e,n){const s=this.skeletonData.findAnimation(t);if(!s)throw new Error(`Animation not found: ${t}`);const i=this.skeletonData.findAnimation(e);if(!i)throw new Error(`Animation not found: ${e}`);this.setMix2(s,i,n)}setMix2(t,e,n){if(!t)throw new Error("from cannot be null.");if(!e)throw new Error("to cannot be null.");const s=`${t.name}.${e.name}`;this.animationToMixTime[s]=n}getMix(t,e){const n=`${t.name}.${e.name}`,s=this.animationToMixTime[n];return s===void 0?this.defaultMix:s}}class Ct{constructor(t,e="",n=new bt,s=new lt){T(this,"textureLoader");T(this,"pathPrefix");T(this,"downloader");T(this,"cache");T(this,"errors",{});T(this,"toLoad",0);T(this,"loaded",0);T(this,"texturePmaInfo",{});this.textureLoader=t,this.pathPrefix=e,this.downloader=n,this.cache=s}start(t){return this.toLoad++,this.pathPrefix+t}success(t,e,n){this.toLoad--,this.loaded++,this.cache.assets[e]=n,this.cache.assetsRefCount[e]=(this.cache.assetsRefCount[e]||0)+1,t&&t(e,n)}error(t,e,n){this.toLoad--,this.loaded++,this.errors[e]=n,t&&t(e,n)}loadAll(){return new Promise((e,n)=>{const s=()=>{if(this.isLoadingComplete()){this.hasErrors()?n(this.errors):e(this);return}requestAnimationFrame(s)};requestAnimationFrame(s)})}setRawDataURI(t,e){this.downloader.rawDataUris[this.pathPrefix+t]=e}loadBinary(t,e=()=>{},n=()=>{}){t=this.start(t),!this.reuseAssets(t,e,n)&&(this.cache.assetsLoaded[t]=new Promise((s,i)=>{this.downloader.downloadBinary(t,o=>{this.success(e,t,o),s(o)},(o,r)=>{const l=`Couldn't load binary ${t}: status ${o}, ${r}`;this.error(n,t,l),i(l)})}))}loadText(t,e=()=>{},n=()=>{}){t=this.start(t),this.downloader.downloadText(t,s=>{this.success(e,t,s)},(s,i)=>{this.error(n,t,`Couldn't load text ${t}: status ${s}, ${i}`)})}loadJson(t,e=()=>{},n=()=>{}){t=this.start(t),!this.reuseAssets(t,e,n)&&(this.cache.assetsLoaded[t]=new Promise((s,i)=>{this.downloader.downloadJson(t,o=>{this.success(e,t,o),s(o)},(o,r)=>{const l=`Couldn't load JSON ${t}: status ${o}, ${r}`;this.error(n,t,l),i(l)})}))}reuseAssets(t,e=()=>{},n=()=>{}){const s=this.cache.getAsset(t),i=s!==void 0;return i&&(this.cache.assetsLoaded[t]=s.then(o=>(o=o instanceof Image||o instanceof ImageBitmap?this.textureLoader(o):o,this.success(e,t,o),o)).catch(o=>{this.error(n,t,o)})),i}loadTexture(t,e=()=>{},n=()=>{}){if(t=this.start(t),this.reuseAssets(t,e,n))return;const s=this.texturePmaInfo[t];this.cache.assetsLoaded[t]=new Promise((i,o)=>{if(!!!(typeof window<"u"&&typeof navigator<"u"&&window.document))fetch(t,{mode:"cors"}).then(c=>{if(c.ok)return c.blob();const a=`Couldn't load image: ${t}`;this.error(n,t,`Couldn't load image: ${t}`),o(a)}).then(c=>c?createImageBitmap(c,{premultiplyAlpha:"none",colorSpaceConversion:"none"}):null).then(c=>{if(c){const a=this.createTexture(t,s,c);this.success(e,t,a),i(a)}});else{const c=new Image;c.crossOrigin="anonymous",c.onload=()=>{const a=this.createTexture(t,s,c);this.success(e,t,a),i(a)},c.onerror=()=>{const a=`Couldn't load image: ${t}`;this.error(n,t,a),o(a)},this.downloader.rawDataUris[t]&&(t=this.downloader.rawDataUris[t]),c.src=t}})}loadTextureAtlas(t,e=()=>{},n=()=>{},s){const i=t.lastIndexOf("/"),o=i>=0?t.substring(0,i+1)Line truncated
|
||||
var ft=Object.defineProperty;var gt=(W,t,e)=>t in W?ft(W,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):W[t]=e;var T=(W,t,e)=>gt(W,typeof t!="symbol"?t+"":t,e);import{f as ht,R as pt,N as dt,ac as ut,Z as mt,b4 as yt,O as rt,b5 as at,U as D,B as Tt}from"./index-CjEJ0XJ5.js";import{$ as Rt,a0 as Yt,a1 as Ft,a2 as Et,y as _t,a3 as Ot,a4 as kt,a5 as Xt,a6 as Bt,a7 as Nt,a8 as Dt,a9 as $t,aa as zt,ab as Wt,ad as qt,H as Gt,ae as Jt,af as Ht,ag as Zt,ah as Kt,ai as Qt,aj as jt,ak as te,al as ee,am as se,an as ne,ao as ie,ap as oe,aq as re,ar as ae,as as le,at as ce,au as he,av as pe,aw as de,ax as ue,ay as fe,az as ge,aA as me,aB as ye,aC as Te,aD as be,aE as we,aF as xe,aG as Ae,aH as Pe,aI as Ie,aJ as Ce,aK as Ve,aL as Le,aM as Se,aN as Me,aO as ve,aP as Ue,aQ as Re,aR as Ye,aS as Fe,aT as Ee,x as _e,aU as Oe,aV as ke,aW as Xe,aX as Be,aY as Ne,aZ as De,a_ as $e,a$ as ze,b0 as We,b1 as qe,b2 as Ge,b3 as Je,b6 as He,b7 as Ze,b8 as Ke,b9 as Qe,ba as je,bb as ts,bc as es,bd as ss,be as ns,bf as is,bg as os,bh as rs,bi as as,bj as ls,bk as cs,bl as hs,bm as ps,bn as ds,bo as us,bp as fs,bq as gs,br as ms,bs as ys,bt as Ts,bu as bs,w as ws,X as xs,J as As,Y as Ps,z as Is,bv as Cs,bw as Vs,bx as Ls,by as Ss,bz as Ms,bA as vs,bB as Us,bC as Rs,bD as Ys,bE as Fs,bF as Es,bG as _s,T as Os,bH as ks,bI as Xs,b as Bs,bJ as Ns,c as Ds,bK as $s,bL as zs,bM as Ws,bN as qs,bO as Gs,bP as Js,bQ as Hs,bR as Zs,bS as Ks,bT as Qs,bU as js,bV as tn,bW as en,bX as sn,bY as nn,bZ as on,b_ as rn,V as an,b$ as ln,c0 as cn,c1 as hn,c2 as pn,c3 as dn}from"./index-CjEJ0XJ5.js";class It{constructor(t){T(this,"skeletonData");T(this,"animationToMixTime",{});T(this,"defaultMix",0);if(!t)throw new Error("skeletonData cannot be null.");this.skeletonData=t}setMix(t,e,n){return typeof t=="string"?this.setMix1(t,e,n):this.setMix2(t,e,n)}setMix1(t,e,n){const s=this.skeletonData.findAnimation(t);if(!s)throw new Error(`Animation not found: ${t}`);const i=this.skeletonData.findAnimation(e);if(!i)throw new Error(`Animation not found: ${e}`);this.setMix2(s,i,n)}setMix2(t,e,n){if(!t)throw new Error("from cannot be null.");if(!e)throw new Error("to cannot be null.");const s=`${t.name}.${e.name}`;this.animationToMixTime[s]=n}getMix(t,e){const n=`${t.name}.${e.name}`,s=this.animationToMixTime[n];return s===void 0?this.defaultMix:s}}class Ct{constructor(t,e="",n=new bt,s=new lt){T(this,"textureLoader");T(this,"pathPrefix");T(this,"downloader");T(this,"cache");T(this,"errors",{});T(this,"toLoad",0);T(this,"loaded",0);T(this,"texturePmaInfo",{});this.textureLoader=t,this.pathPrefix=e,this.downloader=n,this.cache=s}start(t){return this.toLoad++,this.pathPrefix+t}success(t,e,n){this.toLoad--,this.loaded++,this.cache.assets[e]=n,this.cache.assetsRefCount[e]=(this.cache.assetsRefCount[e]||0)+1,t&&t(e,n)}error(t,e,n){this.toLoad--,this.loaded++,this.errors[e]=n,t&&t(e,n)}loadAll(){return new Promise((e,n)=>{const s=()=>{if(this.isLoadingComplete()){this.hasErrors()?n(this.errors):e(this);return}requestAnimationFrame(s)};requestAnimationFrame(s)})}setRawDataURI(t,e){this.downloader.rawDataUris[this.pathPrefix+t]=e}loadBinary(t,e=()=>{},n=()=>{}){t=this.start(t),!this.reuseAssets(t,e,n)&&(this.cache.assetsLoaded[t]=new Promise((s,i)=>{this.downloader.downloadBinary(t,o=>{this.success(e,t,o),s(o)},(o,r)=>{const l=`Couldn't load binary ${t}: status ${o}, ${r}`;this.error(n,t,l),i(l)})}))}loadText(t,e=()=>{},n=()=>{}){t=this.start(t),this.downloader.downloadText(t,s=>{this.success(e,t,s)},(s,i)=>{this.error(n,t,`Couldn't load text ${t}: status ${s}, ${i}`)})}loadJson(t,e=()=>{},n=()=>{}){t=this.start(t),!this.reuseAssets(t,e,n)&&(this.cache.assetsLoaded[t]=new Promise((s,i)=>{this.downloader.downloadJson(t,o=>{this.success(e,t,o),s(o)},(o,r)=>{const l=`Couldn't load JSON ${t}: status ${o}, ${r}`;this.error(n,t,l),i(l)})}))}reuseAssets(t,e=()=>{},n=()=>{}){const s=this.cache.getAsset(t),i=s!==void 0;return i&&(this.cache.assetsLoaded[t]=s.then(o=>(o=o instanceof Image||o instanceof ImageBitmap?this.textureLoader(o):o,this.success(e,t,o),o)).catch(o=>{this.error(n,t,o)})),i}loadTexture(t,e=()=>{},n=()=>{}){if(t=this.start(t),this.reuseAssets(t,e,n))return;const s=this.texturePmaInfo[t];this.cache.assetsLoaded[t]=new Promise((i,o)=>{if(!!!(typeof window<"u"&&typeof navigator<"u"&&window.document))fetch(t,{mode:"cors"}).then(c=>{if(c.ok)return c.blob();const a=`Couldn't load image: ${t}`;this.error(n,t,`Couldn't load image: ${t}`),o(a)}).then(c=>c?createImageBitmap(c,{premultiplyAlpha:"none",colorSpaceConversion:"none"}):null).then(c=>{if(c){const a=this.createTexture(t,s,c);this.success(e,t,a),i(a)}});else{const c=new Image;c.crossOrigin="anonymous",c.onload=()=>{const a=this.createTexture(t,s,c);this.success(e,t,a),i(a)},c.onerror=()=>{const a=`Couldn't load image: ${t}`;this.error(n,t,a),o(a)},this.downloader.rawDataUris[t]&&(t=this.downloader.rawDataUris[t]),c.src=t}})}loadTextureAtlas(t,e=()=>{},n=()=>{},s){const i=t.lastIndexOf("/"),o=i>=0?t.substring(0,i+1)Line truncated
|
||||
${n}`:""}`)}remove(t){t=this.pathPrefix+t;const e=this.cache.assets[t];return e.dispose&&e.dispose(),delete this.cache.assets[t],delete this.cache.assetsRefCount[t],delete this.cache.assetsLoaded[t],e}removeAll(){for(const t in this.cache.assets){const e=this.cache.assets[t];e.dispose&&e.dispose()}this.cache.assets={},this.cache.assetsLoaded={},this.cache.assetsRefCount={}}isLoadingComplete(){return this.toLoad===0}getToLoad(){return this.toLoad}getLoaded(){return this.loaded}dispose(){this.removeAll()}disposeAsset(t){const e=this.cache.assets[t];if(e instanceof ht){e.dispose();return}this.disposeAssetInternal(t)}hasErrors(){return Object.keys(this.errors).length>0}getErrors(){return this.errors}disposeAssetInternal(t){if(this.cache.assetsRefCount[t]>0&&--this.cache.assetsRefCount[t]===0)return this.remove(t)}createTextureAtlas(t,e,n,s){const i=new ht(t);i.dispose=()=>{var o;if(!(this.cache.assetsRefCount[n]<=0)){this.disposeAssetInternal(n);for(const r of i.pages)(o=r.texture)==null||o.dispose()}};for(const o of i.pages){const r=this.texturePath(e,o.name,s);this.texturePmaInfo[this.pathPrefix+r]=o.pma}return i}createTexture(t,e,n){const s=this.textureLoader(n,e),i=s.dispose.bind(s);return s.dispose=()=>{this.disposeAssetInternal(t)&&i()},s}texturePath(t,e,n){return n?n[e]:t+e}}const nt=class nt{constructor(){T(this,"assets",{});T(this,"assetsRefCount",{});T(this,"assetsLoaded",{})}static getCache(t){const e=nt.AVAILABLE_CACHES.get(t);if(e)return e;const n=new nt;return nt.AVAILABLE_CACHES.set(t,n),n}async addAsset(t,e){return this.assetsLoaded[t]=Promise.resolve(e),this.assets[t]=e,e}getAsset(t){return this.assetsLoaded[t]}};T(nt,"AVAILABLE_CACHES",new Map);let lt=nt;class bt{constructor(){T(this,"callbacks",{});T(this,"rawDataUris",{})}dataUriToString(t){if(!t.startsWith("data:"))throw new Error("Not a data URI.");let e=t.indexOf("base64,");return e!==-1?(e+=7,atob(t.substr(e))):t.substr(t.indexOf(",")+1)}base64ToUint8Array(t){var e=window.atob(t),n=e.length,s=new Uint8Array(n);for(let i=0;i<n;i++)s[i]=e.charCodeAt(i);return s}dataUriToUint8Array(t){if(!t.startsWith("data:"))throw new Error("Not a data URI.");let e=t.indexOf("base64,");if(e===-1)throw new Error("Not a binary data URI.");return e+=7,this.base64ToUint8Array(t.substr(e))}downloadText(t,e,n){if(this.start(t,e,n))return;const s=this.rawDataUris[t];if(s&&!s.includes(".")){try{this.finish(t,200,this.dataUriToString(s))}catch(r){this.finish(t,400,JSON.stringify(r))}return}const i=new XMLHttpRequest;i.overrideMimeType("text/html"),i.open("GET",s||t,!0);const o=()=>{this.finish(t,i.status,i.responseText)};i.onload=o,i.onerror=o,i.send()}downloadJson(t,e,n){this.downloadText(t,s=>{e(JSON.parse(s))},n)}downloadBinary(t,e,n){if(this.start(t,e,n))return;const s=this.rawDataUris[t];if(s&&!s.includes(".")){try{this.finish(t,200,this.dataUriToUint8Array(s))}catch(r){this.finish(t,400,JSON.stringify(r))}return}const i=new XMLHttpRequest;i.open("GET",s||t,!0),i.responseType="arraybuffer";const o=()=>{this.finish(t,i.status,i.response)};i.onload=()=>{i.status===200||i.status===0?this.finish(t,200,new Uint8Array(i.response)):o()},i.onerror=o,i.send()}start(t,e,n){let s=this.callbacks[t];try{if(s)return!0;this.callbacks[t]=s=[]}finally{s.push(e,n)}}finish(t,e,n){const s=this.callbacks[t];if(delete this.callbacks[t],e===200||e===0)for(let i=0,o=s.length;i<o;i+=2)s[i](n);else for(let i=1,o=s.length;i<o;i+=2)s[i](e,n)}}class Vt{constructor(t,e=!1){T(this,"atlas");T(this,"allowMissingRegions");this.atlas=t,this.allowMissingRegions=e}findRegions(t,e,n){const s=n.regions;for(let i=0,o=s.length;i<o;i++)s[i]=this.findRegion(t,n.getPath(e,i))}findRegion(t,e){const n=this.atlas.findRegion(e);if(!n&&!this.allowMissingRegions)throw new Error(`Region not found in atlas: ${e} (attachment: ${t})`);return n}newRegionAttachment(t,e,n,s,i){return this.findRegions(n,s,i),new pt(n,i)}newMeshAttachment(t,e,n,s,i){return this.findRegions(n,s,i),new dt(n,i)}newBoundingBoxAttachment(t,e,n){return new ut(n)}newPathAttachment(t,e,n){return new mt(n)}newPointAttachment(t,e,n){return new yt(n)}newClippingAttachment(t,e,n){return new rt(n)}}class Lt{constructor(){T(this,"minX",0);T(this,"minY",0);T(this,"maxX",0);T(this,"maxY",0);T(this,"boundingBoxes",[]);T(this,"polygons",[]);T(this,"polygonPool",new at(()=>D.newFloatArray(16)))}update(t,e){if(!t)throw new Error("skeleton cannot be null.");const n=this.boundingBoxes,s=this.polygons,i=this.polygonPool,o=t.slots,r=o.length;n.length=0,i.freeAll(s),s.length=0;for(let l=0;l<r;l++){const c=o[l];if(!c.bone.active)continue;const a=c.appliedPose.attachment;if(a instanceof ut){n.push(a);let h=i.obtain();h.length!==a.worldVerticesLength&&(h=D.newFloatArray(a.worldVerticesLength)),s.push(h),a.computeWorldVertices(t,c,0,a.worldVerticesLength,h,0,2)}}e?this.aabbCompute():(this.minX=Number.POSITIVE_INFINITY,this.minY=Number.POSITIVE_INFINITY,this.maxX=Number.NEGATIVE_INFINITY,this.maxY=Number.NEGATIVE_INFINITY)}aabbCompute(){let t=Number.POSITIVE_INFLine truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
.help-overlay[data-v-78204b9d]{position:absolute;top:0;right:0;bottom:0;left:0;z-index:40;display:flex;align-items:center;justify-content:center;padding:72px 24px 28px;box-sizing:border-box;background:#05080fb8;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}.help-dialog[data-v-78204b9d]{width:min(820px,100%);max-height:100%;display:flex;flex-direction:column;overflow:hidden;border:1px solid #3a4864;border-radius:14px;background:#141a27;color:#b8c3d8;box-shadow:0 22px 70px #0000009e}.help-head[data-v-78204b9d]{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding:18px 20px 15px;border-bottom:1px solid #2b354a;background:linear-gradient(135deg,#202942,#181e2d)}.help-head h2[data-v-78204b9d]{margin:0;color:#eef2ff;font-size:19px}.help-head p[data-v-78204b9d]{margin:5px 0 0;color:#7786a3;font-size:11px}.help-close[data-v-78204b9d]{width:28px;height:28px;flex:0 0 28px;border:1px solid #43516b;border-radius:6px;background:#273249;color:#ccd7ed;cursor:pointer}.help-close[data-v-78204b9d]:hover{border-color:#7774ff;background:#3b4670;color:#fff}.help-body[data-v-78204b9d]{min-height:0;overflow:auto;padding:18px 20px 21px}.help-section[data-v-78204b9d]{padding:14px 15px;border:1px solid #2c3850;border-radius:10px;background:#181f2e}.help-section h3[data-v-78204b9d]{margin:0 0 11px;color:#9fb2ff;font-size:13px}.help-start ol[data-v-78204b9d]{margin:0;padding-left:21px;color:#aab6cc;font-size:12px;line-height:1.75}.help-start li[data-v-78204b9d]::marker{color:#7774ff;font-weight:700}.help-section b[data-v-78204b9d]{color:#dce4f7}.help-grid[data-v-78204b9d]{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(250px,.85fr);gap:12px;margin-top:12px}.help-list[data-v-78204b9d]{display:grid;gap:0;margin:0}.help-list>div[data-v-78204b9d]{display:grid;grid-template-columns:78px 1fr;gap:9px;padding:7px 0;border-bottom:1px solid #263146}.help-list>div[data-v-78204b9d]:last-child{border-bottom:0}.help-list dt[data-v-78204b9d]{color:#d3dcf0;font-size:11px;font-weight:700}.help-list dd[data-v-78204b9d]{margin:0;color:#8795ae;font-size:11px;line-height:1.5}.shortcut-list[data-v-78204b9d]{display:grid;gap:7px}.shortcut-list>div[data-v-78204b9d]{display:flex;align-items:center;min-height:25px;gap:5px}.shortcut-list kbd[data-v-78204b9d]{min-width:24px;box-sizing:border-box;padding:3px 6px;border:1px solid #465470;border-bottom-width:2px;border-radius:5px;background:#101623;color:#e0e7f6;font:10px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;text-align:center}.shortcut-list span[data-v-78204b9d]{color:#65728b;font-size:10px}.shortcut-list em[data-v-78204b9d]{margin-left:auto;color:#9ca9c1;font-size:11px;font-style:normal}.help-note[data-v-78204b9d]{margin:11px 0 0;padding-top:9px;border-top:1px solid #2a3549;color:#697791;font-size:10px;line-height:1.55}.help-export[data-v-78204b9d]{margin-top:12px}.help-export p[data-v-78204b9d]{margin:0;color:#8997b0;font-size:11px;line-height:1.65}.help-doc-title[data-v-78204b9d]{margin:16px 2px 2px;color:#9fb2ff;font-size:13px;font-weight:700;letter-spacing:.5px}.help-module[data-v-78204b9d]{margin-top:12px}.help-module-head[data-v-78204b9d]{display:flex;align-items:center;gap:8px;width:100%;padding:0;border:0;background:transparent;color:#9fb2ff;font:inherit;font-size:13px;font-weight:700;text-align:left;cursor:pointer}.help-module-head[data-v-78204b9d]:hover{color:#fff}.help-module-head .mtitle[data-v-78204b9d]{flex:1;min-width:0}.help-module-head .caret[data-v-78204b9d]{flex:0 0 auto;color:#7b86a2;font-size:12px}.help-module.open .help-module-head[data-v-78204b9d]{color:#fff}.help-module.open .help-module-head .caret[data-v-78204b9d]{color:#9fb2ff}.help-module .mnum[data-v-78204b9d]{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;background:linear-gradient(135deg,#4a53b8,#6a5fd0);color:#fff;font-size:12px;font-weight:700}.help-module.help-connector[data-v-78204b9d]{border-color:#4a4f8a;background:linear-gradient(135deg,#1d2442,#181d2f)}.help-module.help-connector .help-module-head[data-v-78204b9d]{color:#b8c2ff}.help-module .connector-intro[data-v-78204b9d]{margin:0 0 10px;color:#9aa8c4;font-size:11px;line-height:1.6}.help-module .connector-intro b[data-v-78204b9d]{color:#dce4f7}.help-module .dl-link[data-v-78204b9d]{display:inline-flex;flex-direction:column;align-items:flex-start;gap:2px;margin:0 10px 6px 0;padding:6px 12px;border:1px solid #5652b5;border-radius:6px;background:#4540a0;color:#fff;font-size:12px;font-weight:700;text-decoration:none}.help-module .dl-link[data-v-78204b9d]:hover{border-color:#8982ff;background:#554dc2}.help-module .dl-link small[data-v-78204b9d]{color:#cfe0ff;font-weight:500;font-size:9px}.help-module-body[data-v-78204b9d]{padding-top:10px}@media(max-width:760px){.help-overlay[data-v-78204b9d]{padding:66px 12px 14px}.help-grid[data-v-78204b9d]{grid-template-columns:1fr}}.topbar[data-v-4749834a]{position:absolute;top:10px;left:10px;right:10px;dispLine truncated
|
||||
@@ -1,4 +1,4 @@
|
||||
var Ae=Object.defineProperty;var Be=(m,o,e)=>o in m?Ae(m,o,{enumerable:!0,configurable:!0,writable:!0,value:e}):m[o]=e;var a=(m,o,e)=>Be(m,typeof o!="symbol"?o+"":o,e);import{T as Fe,a as he,b as F,S as ue,M as pe,c as oe,W as ne,B as H,d as z,L as Te,E as O,e as ce,p as q,f as Ee,g as fe,A as Q,h as me,i as De,j as V,s as re,G as we,k as $,l as Y,m as Le,P as Ie,n as Re,o as Ne,C as X,q as be,r as _e,t as Ue,u as Ye,v as Xe,w as ee,x as j,y as ke,z as Ve,D as J,F as ge,V as Ge,H as ae,I as We,J as $e,U as xe,K as E,R as ve,N as ye,O as Pe,Q as Oe,X as He,Y as ze,Z as qe,_ as Ke}from"./index-CBAQrWSZ.js";import{$ as bt,a0 as kt,a1 as vt,a2 as yt,a3 as Pt,a4 as St,a5 as Mt,a6 as At,a7 as Bt,a8 as Ft,a9 as Et,aa as Dt,ab as Lt,ac as It,ad as Rt,ae as Nt,af as _t,ag as Ut,ah as Yt,ai as Xt,aj as Vt,ak as Gt,al as Wt,am as $t,an as Ot,ao as Ht,ap as zt,aq as qt,ar as Kt,as as Zt,at as Jt,au as Qt,av as jt,aw as es,ax as ts,ay as ss,az as os,aA as ns,aB as as,aC as rs,aD as is,aE as ls,aF as hs,aG as cs,aH as ds,aI as us,aJ as ps,aK as fs,aL as ms,aM as gs,aN as xs,aO as Cs,aP as Ts,aQ as ws,aR as bs,aS as ks,aT as vs,aU as ys,aV as Ps,aW as Ss,aX as Ms,aY as As,aZ as Bs,a_ as Fs,a$ as Es,b0 as Ds,b1 as Ls,b2 as Is,b3 as Rs,b4 as Ns,b5 as _s,b6 as Us,b7 as Ys,b8 as Xs,b9 as Vs,ba as Gs,bb as Ws,bc as $s,bd as Os,be as Hs,bf as zs,bg as qs,bh as Ks,bi as Zs,bj as Js,bk as Qs,bl as js,bm as eo,bn as to,bo as so,bp as oo,bq as no,br as ao,bs as ro,bt as io,bu as lo,bv as ho,bw as co,bx as uo,by as po,bz as fo,bA as mo,bB as go,bC as xo,bD as Co,bE as To,bF as wo,bG as bo,bH as ko,bI as vo,bJ as yo,bK as Po,bL as So,bM as Mo,bN as Ao,bO as Bo,bP as Fo,bQ as Eo,bR as Do,bS as Lo,bT as Io,bU as Ro,bV as No,bW as _o,bX as Uo,bY as Yo,bZ as Xo,b_ as Vo,b$ as Go,c0 as Wo,c1 as $o,c2 as Oo,c3 as Ho}from"./index-CBAQrWSZ.js";import{SkeletonClipping as de,SkeletonPhysicsMovement as Ze,AnimationStateData as Je,AtlasAttachmentLoader as Qe,SkeletonBounds as je}from"./index-3evwjaUy.js";import{AssetCache as qo,AssetManagerBase as Ko,Downloader as Zo,SkeletonRendererCore as Jo,Triangulator as Qo}from"./index-3evwjaUy.js";if(typeof window<"u"&&window.PIXI){const m=window.require;window.require=o=>{if(m)return m(o);if(o.startsWith("@pixi/"))return window.PIXI}}const _=class _ extends Fe{constructor(e){super(e.resource.source);a(this,"texture");this.texture=he.from(e)}static from(e){return _.textureMap.has(e)?_.textureMap.get(e):new _(e)}setFilters(e,t){this.texture.baseTexture.scaleMode=_.toPixiTextureFilter(e),this.texture.baseTexture.mipmap=_.toPixiMipMap(e)}setWraps(e,t){this.texture.baseTexture.wrapMode=_.toPixiTextureWrap(e)}dispose(){this.texture.destroy()}static toPixiTextureFilter(e){switch(e){case F.Nearest:case F.MipMapNearestLinear:case F.MipMapNearestNearest:return ue.NEAREST;case F.Linear:case F.MipMapLinearLinear:case F.MipMapLinearNearest:return ue.LINEAR;default:throw new Error(`Unknown texture filter: ${String(e)}`)}}static toPixiMipMap(e){switch(e){case F.Nearest:case F.Linear:return pe.OFF;case F.MipMapNearestLinear:case F.MipMapNearestNearest:case F.MipMapLinearLinear:case F.MipMapLinearNearest:return pe.ON;default:throw new Error(`Unknown texture filter: ${String(e)}`)}}static toPixiTextureWrap(e){switch(e){case oe.ClampToEdge:return ne.CLAMP;case oe.MirroredRepeat:return ne.MIRRORED_REPEAT;case oe.Repeat:return ne.REPEAT;default:throw new Error(`Unknown texture wrap: ${String(e)}`)}}static toPixiBlending(e){switch(e){case H.Normal:return z.NORMAL;case H.Additive:return z.ADD;case H.Multiply:return z.MULTIPLY;case H.Screen:return z.SCREEN;default:throw new Error(`Unknown blendMode: ${String(e)}`)}}};a(_,"textureMap",new Map);let G=_;const K="spineTextureAtlasLoader",et={extension:O.Asset,resolver:{test:m=>V(m,".atlas"),parse:m=>{var e,t;const o=m.split(".");return{resolution:parseFloat(((t=(e=re.RETINA_PREFIX)==null?void 0:e.exec(m))==null?void 0:t[1])??"1"),format:o[o.length-2],src:m}}},loader:{name:K,extension:{type:O.LoadParser,priority:Te.Normal,name:K},test(m){return V(m,".atlas")},async load(m){const o=await re.ADAPTER.fetch(m);if(!o.ok)throw new Error(`[${K}] Failed to fetch ${m}: ${o.status} ${o.statusText}`);return await o.text()},testParse(m,o){const e=V(o.src,".atlas"),t=typeof m=="string",s=o.loadParser===K;return Promise.resolve((e||s)&&t)},unload(m){m.dispose()},async parse(m,o,e){var p;const t=o.data||{};let s=q.dirname(o.src);s&&s.lastIndexOf("/")!==s.length-1&&(s+="/");const n=new Ee(m);if(t.images instanceof fe||typeof t.images=="string"){const h=t.images;t.images={},t.images[n.pages[0].name]=h}const r=[];let l=!0;for(const h of e.parsers)if(h.name==="loadTextures"){l=(p=h.config)==null?void 0:p.preferCreateImageBitmap;break}Q.setPreferences({preferCreateImageBitmap:!1});for(const h of n.pages){const u=h.name,c=t!=null&&t.images?t.images[u]:void 0;if(c instanceof fe)h.setTexture(G.from(c));else{const i=c??q.normalize([...s.split(q.sep),u].join(q.sep)),g={src:De(i,o.src),data:{...t.imageMetadLine truncated
|
||||
var Ae=Object.defineProperty;var Be=(m,o,e)=>o in m?Ae(m,o,{enumerable:!0,configurable:!0,writable:!0,value:e}):m[o]=e;var a=(m,o,e)=>Be(m,typeof o!="symbol"?o+"":o,e);import{T as Fe,a as he,b as F,S as ue,M as pe,c as oe,W as ne,B as H,d as z,L as Te,E as O,e as ce,p as q,f as Ee,g as fe,A as Q,h as me,i as De,j as V,s as re,G as we,k as $,l as Y,m as Le,P as Ie,n as Re,o as Ne,C as X,q as be,r as _e,t as Ue,u as Ye,v as Xe,w as ee,x as j,y as ke,z as Ve,D as J,F as ge,V as Ge,H as ae,I as We,J as $e,U as xe,K as E,R as ve,N as ye,O as Pe,Q as Oe,X as He,Y as ze,Z as qe,_ as Ke}from"./index-CjEJ0XJ5.js";import{$ as bt,a0 as kt,a1 as vt,a2 as yt,a3 as Pt,a4 as St,a5 as Mt,a6 as At,a7 as Bt,a8 as Ft,a9 as Et,aa as Dt,ab as Lt,ac as It,ad as Rt,ae as Nt,af as _t,ag as Ut,ah as Yt,ai as Xt,aj as Vt,ak as Gt,al as Wt,am as $t,an as Ot,ao as Ht,ap as zt,aq as qt,ar as Kt,as as Zt,at as Jt,au as Qt,av as jt,aw as es,ax as ts,ay as ss,az as os,aA as ns,aB as as,aC as rs,aD as is,aE as ls,aF as hs,aG as cs,aH as ds,aI as us,aJ as ps,aK as fs,aL as ms,aM as gs,aN as xs,aO as Cs,aP as Ts,aQ as ws,aR as bs,aS as ks,aT as vs,aU as ys,aV as Ps,aW as Ss,aX as Ms,aY as As,aZ as Bs,a_ as Fs,a$ as Es,b0 as Ds,b1 as Ls,b2 as Is,b3 as Rs,b4 as Ns,b5 as _s,b6 as Us,b7 as Ys,b8 as Xs,b9 as Vs,ba as Gs,bb as Ws,bc as $s,bd as Os,be as Hs,bf as zs,bg as qs,bh as Ks,bi as Zs,bj as Js,bk as Qs,bl as js,bm as eo,bn as to,bo as so,bp as oo,bq as no,br as ao,bs as ro,bt as io,bu as lo,bv as ho,bw as co,bx as uo,by as po,bz as fo,bA as mo,bB as go,bC as xo,bD as Co,bE as To,bF as wo,bG as bo,bH as ko,bI as vo,bJ as yo,bK as Po,bL as So,bM as Mo,bN as Ao,bO as Bo,bP as Fo,bQ as Eo,bR as Do,bS as Lo,bT as Io,bU as Ro,bV as No,bW as _o,bX as Uo,bY as Yo,bZ as Xo,b_ as Vo,b$ as Go,c0 as Wo,c1 as $o,c2 as Oo,c3 as Ho}from"./index-CjEJ0XJ5.js";import{SkeletonClipping as de,SkeletonPhysicsMovement as Ze,AnimationStateData as Je,AtlasAttachmentLoader as Qe,SkeletonBounds as je}from"./index-Bvs6zl88.js";import{AssetCache as qo,AssetManagerBase as Ko,Downloader as Zo,SkeletonRendererCore as Jo,Triangulator as Qo}from"./index-Bvs6zl88.js";if(typeof window<"u"&&window.PIXI){const m=window.require;window.require=o=>{if(m)return m(o);if(o.startsWith("@pixi/"))return window.PIXI}}const _=class _ extends Fe{constructor(e){super(e.resource.source);a(this,"texture");this.texture=he.from(e)}static from(e){return _.textureMap.has(e)?_.textureMap.get(e):new _(e)}setFilters(e,t){this.texture.baseTexture.scaleMode=_.toPixiTextureFilter(e),this.texture.baseTexture.mipmap=_.toPixiMipMap(e)}setWraps(e,t){this.texture.baseTexture.wrapMode=_.toPixiTextureWrap(e)}dispose(){this.texture.destroy()}static toPixiTextureFilter(e){switch(e){case F.Nearest:case F.MipMapNearestLinear:case F.MipMapNearestNearest:return ue.NEAREST;case F.Linear:case F.MipMapLinearLinear:case F.MipMapLinearNearest:return ue.LINEAR;default:throw new Error(`Unknown texture filter: ${String(e)}`)}}static toPixiMipMap(e){switch(e){case F.Nearest:case F.Linear:return pe.OFF;case F.MipMapNearestLinear:case F.MipMapNearestNearest:case F.MipMapLinearLinear:case F.MipMapLinearNearest:return pe.ON;default:throw new Error(`Unknown texture filter: ${String(e)}`)}}static toPixiTextureWrap(e){switch(e){case oe.ClampToEdge:return ne.CLAMP;case oe.MirroredRepeat:return ne.MIRRORED_REPEAT;case oe.Repeat:return ne.REPEAT;default:throw new Error(`Unknown texture wrap: ${String(e)}`)}}static toPixiBlending(e){switch(e){case H.Normal:return z.NORMAL;case H.Additive:return z.ADD;case H.Multiply:return z.MULTIPLY;case H.Screen:return z.SCREEN;default:throw new Error(`Unknown blendMode: ${String(e)}`)}}};a(_,"textureMap",new Map);let G=_;const K="spineTextureAtlasLoader",et={extension:O.Asset,resolver:{test:m=>V(m,".atlas"),parse:m=>{var e,t;const o=m.split(".");return{resolution:parseFloat(((t=(e=re.RETINA_PREFIX)==null?void 0:e.exec(m))==null?void 0:t[1])??"1"),format:o[o.length-2],src:m}}},loader:{name:K,extension:{type:O.LoadParser,priority:Te.Normal,name:K},test(m){return V(m,".atlas")},async load(m){const o=await re.ADAPTER.fetch(m);if(!o.ok)throw new Error(`[${K}] Failed to fetch ${m}: ${o.status} ${o.statusText}`);return await o.text()},testParse(m,o){const e=V(o.src,".atlas"),t=typeof m=="string",s=o.loadParser===K;return Promise.resolve((e||s)&&t)},unload(m){m.dispose()},async parse(m,o,e){var p;const t=o.data||{};let s=q.dirname(o.src);s&&s.lastIndexOf("/")!==s.length-1&&(s+="/");const n=new Ee(m);if(t.images instanceof fe||typeof t.images=="string"){const h=t.images;t.images={},t.images[n.pages[0].name]=h}const r=[];let l=!0;for(const h of e.parsers)if(h.name==="loadTextures"){l=(p=h.config)==null?void 0:p.preferCreateImageBitmap;break}Q.setPreferences({preferCreateImageBitmap:!1});for(const h of n.pages){const u=h.name,c=t!=null&&t.images?t.images[u]:void 0;if(c instanceof fe)h.setTexture(G.from(c));else{const i=c??q.normalize([...s.split(q.sep),u].join(q.sep)),g={src:De(i,o.src),data:{...t.imageMetadLine truncated
|
||||
attribute vec2 aVertexPosition;
|
||||
attribute vec2 aTextureCoord;
|
||||
|
||||
Vendored
-1239
File diff suppressed because it is too large.
Load diff
Vendored
+1239
File diff suppressed because it is too large.
Load diff
Vendored
-1
@@ -1 +0,0 @@
|
||||
.help-overlay[data-v-78204b9d]{position:absolute;top:0;right:0;bottom:0;left:0;z-index:40;display:flex;align-items:center;justify-content:center;padding:72px 24px 28px;box-sizing:border-box;background:#05080fb8;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}.help-dialog[data-v-78204b9d]{width:min(820px,100%);max-height:100%;display:flex;flex-direction:column;overflow:hidden;border:1px solid #3a4864;border-radius:14px;background:#141a27;color:#b8c3d8;box-shadow:0 22px 70px #0000009e}.help-head[data-v-78204b9d]{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding:18px 20px 15px;border-bottom:1px solid #2b354a;background:linear-gradient(135deg,#202942,#181e2d)}.help-head h2[data-v-78204b9d]{margin:0;color:#eef2ff;font-size:19px}.help-head p[data-v-78204b9d]{margin:5px 0 0;color:#7786a3;font-size:11px}.help-close[data-v-78204b9d]{width:28px;height:28px;flex:0 0 28px;border:1px solid #43516b;border-radius:6px;background:#273249;color:#ccd7ed;cursor:pointer}.help-close[data-v-78204b9d]:hover{border-color:#7774ff;background:#3b4670;color:#fff}.help-body[data-v-78204b9d]{min-height:0;overflow:auto;padding:18px 20px 21px}.help-section[data-v-78204b9d]{padding:14px 15px;border:1px solid #2c3850;border-radius:10px;background:#181f2e}.help-section h3[data-v-78204b9d]{margin:0 0 11px;color:#9fb2ff;font-size:13px}.help-start ol[data-v-78204b9d]{margin:0;padding-left:21px;color:#aab6cc;font-size:12px;line-height:1.75}.help-start li[data-v-78204b9d]::marker{color:#7774ff;font-weight:700}.help-section b[data-v-78204b9d]{color:#dce4f7}.help-grid[data-v-78204b9d]{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(250px,.85fr);gap:12px;margin-top:12px}.help-list[data-v-78204b9d]{display:grid;gap:0;margin:0}.help-list>div[data-v-78204b9d]{display:grid;grid-template-columns:78px 1fr;gap:9px;padding:7px 0;border-bottom:1px solid #263146}.help-list>div[data-v-78204b9d]:last-child{border-bottom:0}.help-list dt[data-v-78204b9d]{color:#d3dcf0;font-size:11px;font-weight:700}.help-list dd[data-v-78204b9d]{margin:0;color:#8795ae;font-size:11px;line-height:1.5}.shortcut-list[data-v-78204b9d]{display:grid;gap:7px}.shortcut-list>div[data-v-78204b9d]{display:flex;align-items:center;min-height:25px;gap:5px}.shortcut-list kbd[data-v-78204b9d]{min-width:24px;box-sizing:border-box;padding:3px 6px;border:1px solid #465470;border-bottom-width:2px;border-radius:5px;background:#101623;color:#e0e7f6;font:10px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;text-align:center}.shortcut-list span[data-v-78204b9d]{color:#65728b;font-size:10px}.shortcut-list em[data-v-78204b9d]{margin-left:auto;color:#9ca9c1;font-size:11px;font-style:normal}.help-note[data-v-78204b9d]{margin:11px 0 0;padding-top:9px;border-top:1px solid #2a3549;color:#697791;font-size:10px;line-height:1.55}.help-export[data-v-78204b9d]{margin-top:12px}.help-export p[data-v-78204b9d]{margin:0;color:#8997b0;font-size:11px;line-height:1.65}.help-doc-title[data-v-78204b9d]{margin:16px 2px 2px;color:#9fb2ff;font-size:13px;font-weight:700;letter-spacing:.5px}.help-module[data-v-78204b9d]{margin-top:12px}.help-module-head[data-v-78204b9d]{display:flex;align-items:center;gap:8px;width:100%;padding:0;border:0;background:transparent;color:#9fb2ff;font:inherit;font-size:13px;font-weight:700;text-align:left;cursor:pointer}.help-module-head[data-v-78204b9d]:hover{color:#fff}.help-module-head .mtitle[data-v-78204b9d]{flex:1;min-width:0}.help-module-head .caret[data-v-78204b9d]{flex:0 0 auto;color:#7b86a2;font-size:12px}.help-module.open .help-module-head[data-v-78204b9d]{color:#fff}.help-module.open .help-module-head .caret[data-v-78204b9d]{color:#9fb2ff}.help-module .mnum[data-v-78204b9d]{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;background:linear-gradient(135deg,#4a53b8,#6a5fd0);color:#fff;font-size:12px;font-weight:700}.help-module.help-connector[data-v-78204b9d]{border-color:#4a4f8a;background:linear-gradient(135deg,#1d2442,#181d2f)}.help-module.help-connector .help-module-head[data-v-78204b9d]{color:#b8c2ff}.help-module .connector-intro[data-v-78204b9d]{margin:0 0 10px;color:#9aa8c4;font-size:11px;line-height:1.6}.help-module .connector-intro b[data-v-78204b9d]{color:#dce4f7}.help-module .dl-link[data-v-78204b9d]{display:inline-flex;flex-direction:column;align-items:flex-start;gap:2px;margin:0 10px 6px 0;padding:6px 12px;border:1px solid #5652b5;border-radius:6px;background:#4540a0;color:#fff;font-size:12px;font-weight:700;text-decoration:none}.help-module .dl-link[data-v-78204b9d]:hover{border-color:#8982ff;background:#554dc2}.help-module .dl-link small[data-v-78204b9d]{color:#cfe0ff;font-weight:500;font-size:9px}.help-module-body[data-v-78204b9d]{padding-top:10px}@media(max-width:760px){.help-overlay[data-v-78204b9d]{padding:66px 12px 14px}.help-grid[data-v-78204b9d]{grid-template-columns:1fr}}.topbar[data-v-4749834a]{position:absolute;top:10px;left:10px;right:10px;dispLine truncated
|
||||
Vendored
+2
-2
@@ -8,8 +8,8 @@
|
||||
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; }
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-CBAQrWSZ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D9S6kMFu.css">
|
||||
<script type="module" crossorigin src="/assets/index-CjEJ0XJ5.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BwW3PUa0.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"test:timeline": "node scripts/verify-timeline.mjs",
|
||||
"test:spine-json": "node scripts/verify-spine-json.mjs",
|
||||
"test:spine-naming": "node scripts/verify-spine-naming.mjs",
|
||||
"test:render-layer": "node scripts/verify-render-layer.mjs",
|
||||
"test:connector": "node scripts/verify-spine-connector.mjs",
|
||||
"test:connector:spine38-path": "node scripts/verify-spine38-connector-path.mjs",
|
||||
"test:connector:sea": "node scripts/verify-connector-sea.mjs",
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { build } from 'esbuild'
|
||||
|
||||
const bundled = await build({
|
||||
bundle: true,
|
||||
entryPoints: ['src/views/stage/renderLayer.ts'],
|
||||
format: 'esm',
|
||||
platform: 'node',
|
||||
write: false,
|
||||
})
|
||||
const { resolveRenderTarget } = await import(
|
||||
`data:text/javascript;base64,${Buffer.from(bundled.outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
|
||||
const spines = [{
|
||||
id: 7,
|
||||
enabled: true,
|
||||
status: 'ready',
|
||||
slots: [
|
||||
{ name: 'slot_back', order: 0 },
|
||||
{ name: 'slot_body', order: 1 },
|
||||
{ name: 'slot_hand', order: 2 },
|
||||
],
|
||||
}]
|
||||
const base = {
|
||||
renderLayerMode: 'slot',
|
||||
renderLayerSpineId: 7,
|
||||
renderLayerSlotName: 'slot_hand',
|
||||
renderLayerSlotPosition: 'above',
|
||||
renderLayerFollowAttachment: true,
|
||||
}
|
||||
|
||||
function expect(actual, expected, label) {
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
throw new Error(`${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`)
|
||||
}
|
||||
}
|
||||
|
||||
expect(resolveRenderTarget(spines, { ...base, renderLayerMode: 'top' }), null, '最上层不应挂槽位')
|
||||
expect(
|
||||
resolveRenderTarget(spines, base),
|
||||
{ spineId: 7, slotName: 'slot_hand', targetSlotName: 'slot_hand', followAttachment: true },
|
||||
'图片之上应挂到目标插槽',
|
||||
)
|
||||
expect(
|
||||
resolveRenderTarget(spines, { ...base, renderLayerSlotPosition: 'below' }),
|
||||
{ spineId: 7, slotName: 'slot_body', targetSlotName: 'slot_hand', followAttachment: false },
|
||||
'图片之下应挂到 drawOrder 前一个插槽',
|
||||
)
|
||||
expect(
|
||||
resolveRenderTarget(spines, { ...base, renderLayerSlotPosition: 'below' }, ['slot_back', 'slot_hand', 'slot_body']),
|
||||
{ spineId: 7, slotName: 'slot_back', targetSlotName: 'slot_hand', followAttachment: false },
|
||||
'动画 drawOrder 变化时应按当前顺序取前一个插槽',
|
||||
)
|
||||
expect(
|
||||
resolveRenderTarget(spines, { ...base, renderLayerSlotPosition: 'below' }, ['slot_hand', 'slot_body', 'slot_back']),
|
||||
null,
|
||||
'当前 drawOrder 中目标位于最底层时无法放在其下方',
|
||||
)
|
||||
expect(
|
||||
resolveRenderTarget(spines, { ...base, renderLayerSlotName: 'slot_body', renderLayerSlotPosition: 'below' }, ['slot_hand', 'slot_body', 'slot_back']),
|
||||
{ spineId: 7, slotName: 'slot_hand', targetSlotName: 'slot_body', followAttachment: false },
|
||||
'当前 drawOrder 中应取紧邻的前一个插槽',
|
||||
)
|
||||
expect(
|
||||
resolveRenderTarget(spines, { ...base, renderLayerSlotName: 'slot_back', renderLayerSlotPosition: 'below' }),
|
||||
null,
|
||||
'第一个插槽下方没有可用层级',
|
||||
)
|
||||
expect(resolveRenderTarget(spines, { ...base, renderLayerSlotName: 'missing' }), null, '插槽不存在时应回退')
|
||||
expect(resolveRenderTarget([{ ...spines[0], enabled: false }], base), null, 'Spine 停用时应回退')
|
||||
expect(resolveRenderTarget([{ ...spines[0], status: 'loading' }], base), null, 'Spine 未就绪时应回退')
|
||||
|
||||
console.log('渲染层级解析校验通过:最上层 / 插槽之上 / 插槽之下 / 动画 drawOrder / 失效回退')
|
||||
@@ -65,6 +65,11 @@ export function defaultConfig(): EmitterConfig {
|
||||
rootRotation: 0,
|
||||
rootScaleX: 1,
|
||||
rootScaleY: 1,
|
||||
renderLayerMode: 'top',
|
||||
renderLayerSpineId: 0,
|
||||
renderLayerSlotName: '',
|
||||
renderLayerSlotPosition: 'above',
|
||||
renderLayerFollowAttachment: false,
|
||||
emissionAngleMode: 'random',
|
||||
direction: 0,
|
||||
spread: 360,
|
||||
@@ -296,6 +301,11 @@ export function ensureEmitterConfig(config: EmitterConfig): EmitterConfig {
|
||||
if (config.shape !== 'path' && (config.emissionAngleMode === 'path-normal' || config.emissionAngleMode === 'path-tangent')) config.emissionAngleMode = 'fixed'
|
||||
config.spawnPathId = Math.max(0, Math.round(Number(config.spawnPathId) || 0))
|
||||
if (config.spawnPathDistribution !== 'fixed' && config.spawnPathDistribution !== 'random') config.spawnPathDistribution = 'fixed'
|
||||
if (config.renderLayerMode !== 'top' && config.renderLayerMode !== 'slot') config.renderLayerMode = 'top'
|
||||
config.renderLayerSpineId = Math.max(0, Math.round(Number(config.renderLayerSpineId) || 0))
|
||||
if (typeof config.renderLayerSlotName !== 'string') config.renderLayerSlotName = ''
|
||||
if (config.renderLayerSlotPosition !== 'above' && config.renderLayerSlotPosition !== 'below') config.renderLayerSlotPosition = 'above'
|
||||
if (typeof config.renderLayerFollowAttachment !== 'boolean') config.renderLayerFollowAttachment = false
|
||||
config.innerRadius = Math.max(0, Number(config.innerRadius) || 0)
|
||||
config.outerRadius = Math.max(config.innerRadius, Number(config.outerRadius) || 0)
|
||||
config.innerRectW = Math.max(0, Number(config.innerRectW) || 0)
|
||||
|
||||
@@ -13,6 +13,9 @@ export type AttributeMode = 'fixed' | 'random' | 'curve'
|
||||
export type RotationMode = AttributeMode | 'direction'
|
||||
export type ScaleAxisMode = 'uniform' | 'separate'
|
||||
export type FieldFalloff = 'constant' | 'linear' | 'inverse'
|
||||
/** 粒子系统在 Spine 绘制顺序中的位置(仅影响编辑器预览)。 */
|
||||
export type RenderLayerMode = 'top' | 'slot'
|
||||
export type RenderLayerPosition = 'above' | 'below'
|
||||
export interface CurvePoint { x: number; y: number; inX?: number; inY?: number; outX?: number; outY?: number }
|
||||
export interface ColorGradientStop { position: number; color: string }
|
||||
|
||||
@@ -168,6 +171,12 @@ export interface EmitterConfig {
|
||||
rootRotation: number
|
||||
rootScaleX: number
|
||||
rootScaleY: number
|
||||
// ==== 渲染层级(仅编辑器预览;不写入 Spine 导出数据)====
|
||||
renderLayerMode: RenderLayerMode
|
||||
renderLayerSpineId: number
|
||||
renderLayerSlotName: string
|
||||
renderLayerSlotPosition: RenderLayerPosition
|
||||
renderLayerFollowAttachment: boolean
|
||||
// 方向
|
||||
emissionAngleMode: EmissionAngleMode
|
||||
direction: number
|
||||
|
||||
@@ -223,6 +223,9 @@ export async function applyEditorConfig(store: EditorStore, input: EditorConfigF
|
||||
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 = ''
|
||||
}
|
||||
|
||||
await hydrateParticleResources(systems)
|
||||
@@ -257,6 +260,7 @@ export async function applyEditorConfig(store: EditorStore, input: EditorConfigF
|
||||
spine.error = spine.skeletonFileName ? '配置未包含可复用的 Spine 原始资源' : ''
|
||||
spine.animations = []
|
||||
spine.bones = []
|
||||
spine.slots = []
|
||||
spine.selectedAnimation = ''
|
||||
continue
|
||||
}
|
||||
@@ -264,6 +268,7 @@ export async function applyEditorConfig(store: EditorStore, input: EditorConfigF
|
||||
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 || ''
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
<div class="tree-title-row">
|
||||
<div class="section-title">层级树</div>
|
||||
<button
|
||||
v-if="treeTab === 'bone'"
|
||||
class="selected-bone"
|
||||
:class="{ active: spineObject.selectedBone }"
|
||||
:disabled="!spineObject.selectedBone"
|
||||
@@ -71,36 +72,71 @@
|
||||
<template v-if="spineObject.selectedBone">清除选中骨骼:<strong>{{ spineObject.selectedBone }}</strong></template>
|
||||
<template v-else>当前骨骼:<strong>未选择</strong></template>
|
||||
</button>
|
||||
<button v-if="spineObject.bones.length" class="tree-action" @click="toggleAllBones">{{ allBonesCollapsed ? '全部展开' : '全部折叠' }}</button>
|
||||
<button v-else class="selected-bone" :class="{ active: spineObject.selectedSlot }" :disabled="!spineObject.selectedSlot" :title="spineObject.selectedSlot ? '点击清除插槽选择' : '当前未选择插槽'" @click="spineObject.selectedSlot = ''">
|
||||
<template v-if="spineObject.selectedSlot">清除选中插槽:<strong>{{ spineObject.selectedSlot }}</strong></template>
|
||||
<template v-else>当前插槽:<strong>未选择</strong></template>
|
||||
</button>
|
||||
<button v-if="treeTab === 'bone' && spineObject.bones.length" class="tree-action" @click="toggleAllBones">{{ allBonesCollapsed ? '全部展开' : '全部折叠' }}</button>
|
||||
</div>
|
||||
<div v-if="!spineObject.bones.length" class="empty">未加载 Spine</div>
|
||||
<div v-else class="bone-search">
|
||||
<input v-model.trim="boneSearch" type="search" aria-label="搜索骨骼" placeholder="搜索骨骼名称…" />
|
||||
<button v-if="boneSearch" title="清空搜索" aria-label="清空骨骼搜索" @click="boneSearch = ''">✕</button>
|
||||
<div class="tree-tabs">
|
||||
<button :class="{ on: treeTab === 'bone' }" @click="treeTab = 'bone'">骨骼</button>
|
||||
<button :class="{ on: treeTab === 'slot' }" @click="treeTab = 'slot'">插槽</button>
|
||||
</div>
|
||||
<div v-if="spineObject.bones.length && !visibleBones.length" class="empty">未找到匹配的骨骼</div>
|
||||
<div v-else-if="spineObject.bones.length" class="bone-tree">
|
||||
<div
|
||||
v-for="bone in visibleBones"
|
||||
:key="bone.name"
|
||||
class="bone"
|
||||
:class="{ selected: spineObject.selectedBone === bone.name }"
|
||||
:style="{ paddingLeft: `${6 + bone.depth * 16}px` }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="selectBone(bone.name)"
|
||||
@keydown.enter.prevent="selectBone(bone.name)"
|
||||
>
|
||||
<button
|
||||
v-if="parentBoneNames.has(bone.name)"
|
||||
class="bone-toggle"
|
||||
:title="collapsedBones.has(bone.name) ? '展开子骨骼' : '折叠子骨骼'"
|
||||
@click.stop="toggleBone(bone.name)"
|
||||
>{{ collapsedBones.has(bone.name) ? '▸' : '▾' }}</button>
|
||||
<span v-else class="bone-leaf">•</span>
|
||||
<span class="bone-name">{{ bone.name }}</span>
|
||||
<template v-if="treeTab === 'bone'">
|
||||
<div v-if="!spineObject.bones.length" class="empty">未加载 Spine</div>
|
||||
<div v-else class="bone-search">
|
||||
<input v-model.trim="boneSearch" type="search" aria-label="搜索骨骼" placeholder="搜索骨骼名称…" />
|
||||
<button v-if="boneSearch" title="清空搜索" aria-label="清空骨骼搜索" @click="boneSearch = ''">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="spineObject.bones.length && !visibleBones.length" class="empty">未找到匹配的骨骼</div>
|
||||
<div v-else-if="spineObject.bones.length" class="bone-tree">
|
||||
<div
|
||||
v-for="bone in visibleBones"
|
||||
:key="bone.name"
|
||||
class="bone"
|
||||
:class="{ selected: spineObject.selectedBone === bone.name }"
|
||||
:style="{ paddingLeft: `${6 + bone.depth * 16}px` }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="selectBone(bone.name)"
|
||||
@keydown.enter.prevent="selectBone(bone.name)"
|
||||
>
|
||||
<button
|
||||
v-if="parentBoneNames.has(bone.name)"
|
||||
class="bone-toggle"
|
||||
:title="collapsedBones.has(bone.name) ? '展开子骨骼' : '折叠子骨骼'"
|
||||
@click.stop="toggleBone(bone.name)"
|
||||
>{{ collapsedBones.has(bone.name) ? '▸' : '▾' }}</button>
|
||||
<span v-else class="bone-leaf">•</span>
|
||||
<span class="bone-name">{{ bone.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="!spineObject.slots.length" class="empty">未加载 Spine 插槽</div>
|
||||
<div v-else class="bone-search">
|
||||
<input v-model.trim="slotSearch" type="search" aria-label="搜索插槽" placeholder="搜索插槽或附件名称…" />
|
||||
<button v-if="slotSearch" title="清空搜索" aria-label="清空插槽搜索" @click="slotSearch = ''">✕</button>
|
||||
</div>
|
||||
<div v-if="spineObject.slots.length && !visibleSlots.length" class="empty">未找到匹配的插槽</div>
|
||||
<div v-else-if="spineObject.slots.length" class="bone-tree slot-tree">
|
||||
<div
|
||||
v-for="slot in visibleSlots"
|
||||
:key="slot.name"
|
||||
class="bone slot-row"
|
||||
:class="{ selected: spineObject.selectedSlot === slot.name }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:title="`${slot.name}(骨骼:${slot.bone || '-'},附件:${slot.attachment || '空'})`"
|
||||
@click="selectSlot(slot.name)"
|
||||
@keydown.enter.prevent="selectSlot(slot.name)"
|
||||
>
|
||||
<span class="slot-order">{{ slot.order + 1 }}</span>
|
||||
<span class="bone-name">{{ slot.name }}</span>
|
||||
<span class="slot-attachment">{{ slot.attachment || '空' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -121,12 +157,16 @@ const textureInput = ref<HTMLInputElement | null>(null)
|
||||
const folderInput = ref<HTMLInputElement | null>(null)
|
||||
const collapsedBones = ref(new Set<string>())
|
||||
const boneSearch = ref('')
|
||||
const treeTab = ref<'bone' | 'slot'>('bone')
|
||||
const slotSearch = ref('')
|
||||
const skeletonFile = ref<File | null>(null)
|
||||
const atlasFile = ref<File | null>(null)
|
||||
const textureFiles = ref<File[]>([])
|
||||
|
||||
// 兼容热更新或旧场景数据;骨骼选择默认保持为空。
|
||||
if (props.spineObject.selectedBone == null) props.spineObject.selectedBone = ''
|
||||
if (props.spineObject.selectedSlot == null) props.spineObject.selectedSlot = ''
|
||||
if (!Array.isArray(props.spineObject.slots)) props.spineObject.slots = []
|
||||
if (props.spineObject.sourceBundle == null) props.spineObject.sourceBundle = null
|
||||
if (props.spineObject.dataVersion == null) props.spineObject.dataVersion = ''
|
||||
|
||||
@@ -147,6 +187,14 @@ const visibleBones = computed(() => props.spineObject.bones.filter((bone) => {
|
||||
return true
|
||||
}))
|
||||
const allBonesCollapsed = computed(() => parentBoneNames.value.size > 0 && [...parentBoneNames.value].every((name) => collapsedBones.value.has(name)))
|
||||
const visibleSlots = computed(() => {
|
||||
const query = slotSearch.value.trim().toLocaleLowerCase()
|
||||
const slots = [...props.spineObject.slots].sort((a, b) => a.order - b.order)
|
||||
if (!query) return slots
|
||||
return slots.filter((slot) =>
|
||||
slot.name.toLocaleLowerCase().includes(query) || slot.attachment.toLocaleLowerCase().includes(query),
|
||||
)
|
||||
})
|
||||
|
||||
function selectBone(name: string) {
|
||||
const shouldClear = props.spineObject.selectedBone === name
|
||||
@@ -154,6 +202,12 @@ function selectBone(name: string) {
|
||||
if (!shouldClear) props.spineObject.selectedBone = name
|
||||
}
|
||||
|
||||
function selectSlot(name: string) {
|
||||
const shouldClear = props.spineObject.selectedSlot === name
|
||||
for (const spine of store.spines) spine.selectedSlot = ''
|
||||
if (!shouldClear) props.spineObject.selectedSlot = name
|
||||
}
|
||||
|
||||
function toggleBone(name: string) {
|
||||
if (collapsedBones.value.has(name)) collapsedBones.value.delete(name)
|
||||
else collapsedBones.value.add(name)
|
||||
@@ -228,8 +282,11 @@ async function loadBundle() {
|
||||
props.spineObject.dataVersion = parsed.dataVersion
|
||||
props.spineObject.animations = parsed.animations
|
||||
props.spineObject.bones = parsed.bones
|
||||
props.spineObject.slots = parsed.slots
|
||||
props.spineObject.selectedBone = ''
|
||||
props.spineObject.selectedSlot = ''
|
||||
boneSearch.value = ''
|
||||
slotSearch.value = ''
|
||||
collapsedBones.value = new Set()
|
||||
props.spineObject.selectedAnimation = parsed.animations[0]?.name || ''
|
||||
props.spineObject.assetVersion++
|
||||
@@ -266,8 +323,11 @@ function clearBundle() {
|
||||
props.spineObject.sourceBundle = null
|
||||
props.spineObject.animations = []
|
||||
props.spineObject.bones = []
|
||||
props.spineObject.slots = []
|
||||
props.spineObject.selectedBone = ''
|
||||
props.spineObject.selectedSlot = ''
|
||||
boneSearch.value = ''
|
||||
slotSearch.value = ''
|
||||
collapsedBones.value = new Set()
|
||||
props.spineObject.selectedAnimation = ''
|
||||
props.spineObject.assetVersion++
|
||||
@@ -302,6 +362,10 @@ button:disabled { opacity: .55; cursor: wait; }
|
||||
.empty { color: #667187; font-size: 12px; padding: 5px 0; }
|
||||
.bone-tree { max-height: 240px; overflow: auto; padding: 4px 0; background: #111827; border-radius: 5px; }
|
||||
.tree-title-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.tree-tabs { display: flex; gap: 3px; margin: 6px 0; }
|
||||
.tree-tabs button { height: 24px; padding: 0 8px; border: 1px solid transparent; border-radius: 5px; background: transparent; color: #8d98ae; font-size: 10px; cursor: pointer; }
|
||||
.tree-tabs button:hover { background: #273246; }
|
||||
.tree-tabs button.on { border-color: #7774ff; background: #35336b; color: #fff; }
|
||||
.selected-bone { width: auto; min-width: 0; max-width: 100%; height: 24px; flex: 0 1 auto; overflow: hidden; padding: 0 7px; color: #75819c; background: #182132; border: 1px solid transparent; border-radius: 5px; font-size: 10px; text-align: right; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.selected-bone.active { color: #aeb9d4; background: #242f44; border-color: #35445e; cursor: pointer; }
|
||||
.selected-bone.active:hover { border-color: #675ec4; background: #302b57; }
|
||||
@@ -320,5 +384,8 @@ button:disabled { opacity: .55; cursor: wait; }
|
||||
.bone-toggle:hover { background: #26324a; }
|
||||
.bone-leaf { display: inline-flex; width: 22px; justify-content: center; color: #58647d; }
|
||||
.bone-name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.slot-row { padding-right: 8px; }
|
||||
.slot-order { width: 24px; flex: 0 0 24px; color: #66758e; font-size: 10px; font-variant-numeric: tabular-nums; text-align: right; }
|
||||
.slot-attachment { margin-left: auto; max-width: 45%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #6d7c96; font-size: 10px; }
|
||||
@media (max-width: 430px) { .grid.three { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Container } from 'pixi.js'
|
||||
import { Container, Matrix } from 'pixi.js'
|
||||
import { getSpineAsset } from './spineAssetRegistry'
|
||||
import type { SpineSceneObject } from './spineTypes'
|
||||
|
||||
@@ -22,11 +22,79 @@ export class SpineRuntimeLayer {
|
||||
readonly container = new Container()
|
||||
private entries = new Map<number, RuntimeEntry>()
|
||||
|
||||
/** 供渲染层级使用:取某个场景 Spine 的运行时实例。 */
|
||||
getSpine(objectId: number) {
|
||||
return this.entries.get(objectId)?.spine || null
|
||||
}
|
||||
|
||||
/** 当前帧的绘制顺序(会跟随动画里的 drawOrder timeline)。 */
|
||||
getDrawOrder(objectId: number): string[] | null {
|
||||
const spine = this.entries.get(objectId)?.spine
|
||||
try {
|
||||
const drawOrder = spine?.skeleton?.drawOrder
|
||||
return Array.isArray(drawOrder) ? drawOrder.map((slot: any) => String(slot?.data?.name || '')) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** 插槽 wrapper 相对 Spine 的局部矩阵(与 SpinePixi updateSlotObject 保持一致)。 */
|
||||
getSlotLocalMatrix(objectId: number, slotName: string): Matrix | null {
|
||||
const spine = this.entries.get(objectId)?.spine
|
||||
const slot = spine?.skeleton?.findSlot?.(slotName)
|
||||
const bone = slot?.bone
|
||||
if (!bone?.active) return null
|
||||
return new Matrix(bone.a, bone.c, -bone.b, -bone.d, bone.worldX, bone.worldY)
|
||||
}
|
||||
|
||||
/**
|
||||
* 插槽 wrapper 在编辑器 world 空间中的父矩阵 = 场景对象 root 变换 × 插槽骨骼矩阵。
|
||||
* 粒子发射器挂进插槽后用它做逆变换补偿,保持原有的世界坐标表现。
|
||||
*/
|
||||
getSlotParentMatrix(
|
||||
objectId: number,
|
||||
slotName: string,
|
||||
root: { x: number; y: number; rotation: number; scaleX: number; scaleY: number },
|
||||
): Matrix | null {
|
||||
const local = this.getSlotLocalMatrix(objectId, slotName)
|
||||
if (!local) return null
|
||||
const radians = -root.rotation * Math.PI / 180
|
||||
const cos = Math.cos(radians), sin = Math.sin(radians)
|
||||
const rootMatrix = new Matrix(
|
||||
cos * root.scaleX, sin * root.scaleX,
|
||||
-sin * root.scaleY, cos * root.scaleY,
|
||||
root.x, -root.y,
|
||||
)
|
||||
return rootMatrix.append(local)
|
||||
}
|
||||
|
||||
attachSlotObject(objectId: number, slotName: string, container: Container, followAttachmentTimeline: boolean) {
|
||||
const spine = this.entries.get(objectId)?.spine
|
||||
if (!spine?.addSlotObject) return false
|
||||
try {
|
||||
spine.addSlotObject(slotName, container, { followAttachmentTimeline })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
detachSlotObject(objectId: number, slotName: string, container: Container) {
|
||||
const spine = this.entries.get(objectId)?.spine
|
||||
try { spine?.removeSlotObject?.(slotName, container) } catch { /* 运行时已被释放时忽略 */ }
|
||||
}
|
||||
|
||||
private destroyEntry(entry: RuntimeEntry) {
|
||||
// 先摘掉渲染层级 wrapper,避免随 root 一起销毁编辑器里的粒子发射器。
|
||||
try { entry.spine?.removeSlotObjects?.() } catch { /* ignore */ }
|
||||
this.container.removeChild(entry.root)
|
||||
entry.root.destroy({ children: true })
|
||||
}
|
||||
|
||||
sync(objects: SpineSceneObject[], frame: number, fps: number) {
|
||||
for (const [id, entry] of this.entries) {
|
||||
if (objects.some((object) => object.id === id)) continue
|
||||
this.container.removeChild(entry.root)
|
||||
entry.root.destroy({ children: true })
|
||||
this.destroyEntry(entry)
|
||||
this.entries.delete(id)
|
||||
}
|
||||
|
||||
@@ -34,16 +102,12 @@ export class SpineRuntimeLayer {
|
||||
const asset = getSpineAsset(object.id)
|
||||
let entry = this.entries.get(object.id)
|
||||
if (entry && entry.assetVersion !== object.assetVersion && !asset) {
|
||||
this.container.removeChild(entry.root)
|
||||
entry.root.destroy({ children: true })
|
||||
this.destroyEntry(entry)
|
||||
this.entries.delete(object.id)
|
||||
entry = undefined
|
||||
}
|
||||
if ((!entry || entry.assetVersion !== object.assetVersion) && asset) {
|
||||
if (entry) {
|
||||
this.container.removeChild(entry.root)
|
||||
entry.root.destroy({ children: true })
|
||||
}
|
||||
if (entry) this.destroyEntry(entry)
|
||||
const root = new Container()
|
||||
const spine = new asset.SpineClass({ skeletonData: asset.skeletonData, autoUpdate: false, darkTint: false })
|
||||
if (asset.skeletonData.defaultSkin) spine.skeleton.setSkin(asset.skeletonData.defaultSkin)
|
||||
@@ -115,8 +179,7 @@ export class SpineRuntimeLayer {
|
||||
|
||||
destroy() {
|
||||
for (const entry of this.entries.values()) {
|
||||
entry.root.removeFromParent()
|
||||
entry.root.destroy({ children: true })
|
||||
this.destroyEntry(entry)
|
||||
}
|
||||
this.entries.clear()
|
||||
this.container.removeFromParent()
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Texture } from 'pixi.js'
|
||||
import type { LoadedSpineAsset } from './spineAssetRegistry'
|
||||
import { parseBinary42, parseJson42 } from './runtime/spineRuntime42'
|
||||
import { parseBinary43, parseJson43 } from './runtime/spineRuntime43'
|
||||
import type { SpineAnimationInfo, SpineBoneInfo } from './spineTypes'
|
||||
import type { SpineAnimationInfo, SpineBoneInfo, SpineSlotInfo } from './spineTypes'
|
||||
import { normalizeSpine38JsonFor42 } from './spine38Compatibility'
|
||||
import { normalizeSpineVersion, spineRuntimeVersion, supportedSpineVersionsText, type SupportedSpineVersion } from './spineVersion'
|
||||
|
||||
@@ -11,6 +11,7 @@ type CompatibleSkeletonData = LoadedSpineAsset['skeletonData']
|
||||
export interface ParsedSpineBundle extends LoadedSpineAsset {
|
||||
animations: SpineAnimationInfo[]
|
||||
bones: SpineBoneInfo[]
|
||||
slots: SpineSlotInfo[]
|
||||
dataVersion: SupportedSpineVersion
|
||||
}
|
||||
|
||||
@@ -40,6 +41,16 @@ function boneMetadata(skeletonData: CompatibleSkeletonData): SpineBoneInfo[] {
|
||||
})
|
||||
}
|
||||
|
||||
/** setup 姿态下的插槽顺序(即 Spine 层级面板中的绘制顺序)。 */
|
||||
function slotMetadata(skeletonData: CompatibleSkeletonData): SpineSlotInfo[] {
|
||||
return skeletonData.slots.map((slot) => ({
|
||||
name: slot.name,
|
||||
order: Number(slot.index) || 0,
|
||||
bone: slot.boneData?.name || '',
|
||||
attachment: slot.attachmentName || '',
|
||||
}))
|
||||
}
|
||||
|
||||
export async function parseSpineBundle(skeletonFile: File, atlasFile: File, textureFiles: File[]): Promise<ParsedSpineBundle> {
|
||||
if (!/\.(json|skel)$/i.test(skeletonFile.name)) throw new Error('Spine 数据仅支持 .json 或 .skel')
|
||||
if (!/\.atlas$/i.test(atlasFile.name)) throw new Error('图集文件仅支持 .atlas')
|
||||
@@ -98,6 +109,7 @@ export async function parseSpineBundle(skeletonFile: File, atlasFile: File, text
|
||||
dataVersion,
|
||||
animations: skeletonData.animations.map((animation) => ({ name: animation.name, duration: animation.duration })),
|
||||
bones: boneMetadata(skeletonData),
|
||||
slots: slotMetadata(skeletonData),
|
||||
}
|
||||
} catch (error) {
|
||||
for (const texture of textures) texture.destroy(true)
|
||||
|
||||
@@ -12,6 +12,14 @@ export interface SpineBoneInfo {
|
||||
depth: number
|
||||
}
|
||||
|
||||
/** 插槽信息;order 为 setup 姿态下的绘制顺序(Spine drawOrder 可能被动画改变)。 */
|
||||
export interface SpineSlotInfo {
|
||||
name: string
|
||||
order: number
|
||||
bone: string
|
||||
attachment: string
|
||||
}
|
||||
|
||||
export interface SpineSceneObject {
|
||||
id: number
|
||||
name: string
|
||||
@@ -32,8 +40,10 @@ export interface SpineSceneObject {
|
||||
sourceBundle: SpineSourceBundle | null
|
||||
selectedAnimation: string
|
||||
selectedBone: string
|
||||
selectedSlot: string
|
||||
animations: SpineAnimationInfo[]
|
||||
bones: SpineBoneInfo[]
|
||||
slots: SpineSlotInfo[]
|
||||
assetVersion: number
|
||||
status: 'empty' | 'loading' | 'ready' | 'error'
|
||||
error: string
|
||||
|
||||
@@ -322,8 +322,10 @@ export const useParticleStore = defineStore('particle', {
|
||||
sourceBundle: null,
|
||||
selectedAnimation: '',
|
||||
selectedBone: '',
|
||||
selectedSlot: '',
|
||||
animations: [],
|
||||
bones: [],
|
||||
slots: [],
|
||||
assetVersion: 0,
|
||||
status: 'empty',
|
||||
error: '',
|
||||
@@ -381,6 +383,7 @@ export const useParticleStore = defineStore('particle', {
|
||||
copy.name = copyName(source.name, this.spines.map((item) => item.name))
|
||||
// 骨骼选择是层级树的临时操作状态,不在两个对象之间复用。
|
||||
copy.selectedBone = ''
|
||||
copy.selectedSlot = ''
|
||||
retainSpineAsset(source.id, copy.id)
|
||||
this.spines.push(copy)
|
||||
this.activeObjectId = copy.id
|
||||
|
||||
+5
-3
@@ -189,7 +189,7 @@ useEditorShortcuts({
|
||||
|
||||
function syncEmitters() {
|
||||
if (!world) return
|
||||
emitterSync.sync(store, world, dotTex, trailTex)
|
||||
emitterSync.sync(store, world, dotTex, trailTex, spineRuntime)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,13 +243,15 @@ function loop() {
|
||||
background.applyTransform(store.settings.backgroundX, store.settings.backgroundY, store.settings.backgroundOpacity)
|
||||
|
||||
drawAxis(store, app, axisGfx, axisLabelLayer, axisLabels)
|
||||
syncEmitters()
|
||||
// 根节点(root)变换:位置(centerX/centerY)=位移,rotation/scale 绕 root 中心,与场景对象参数联动
|
||||
const followFps = Math.max(1, store.timeline.fps)
|
||||
const followFrame = store.timeline.frame
|
||||
const followTime = followFrame / followFps
|
||||
// 发射器读取骨骼变换前,先把 Spine 运行时同步到同一时间轴帧。
|
||||
// 发射器读取骨骼变换、插槽 drawOrder 之前,先把 Spine 运行时同步到同一时间轴帧。
|
||||
spineRuntime?.sync(store.spines, followFrame, followFps)
|
||||
syncEmitters()
|
||||
// 同步完成后计算插槽层级补偿,抵消插槽骨骼变换,保持粒子系统原有世界坐标表现。
|
||||
emitterSync.updateRenderCompensation(store, spineRuntime)
|
||||
previewColliders = buildPreviewColliders(store, followTime)
|
||||
for (const sys of store.systems) {
|
||||
const em = emitterSync.emitters.get(sys.id)
|
||||
|
||||
@@ -27,6 +27,59 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 渲染层级(仅编辑器预览,不影响导出数据) -->
|
||||
<div class="look-title">渲染层级(Spine 插槽)</div>
|
||||
<div class="seg">
|
||||
<button class="seg-btn" :class="{ active: sys.config.renderLayerMode === 'top' }" @click="sys.config.renderLayerMode = 'top'">最上层</button>
|
||||
<button class="seg-btn" :class="{ active: sys.config.renderLayerMode === 'slot' }" @click="setRenderLayerSlotMode">Spine 插槽</button>
|
||||
</div>
|
||||
<template v-if="sys.config.renderLayerMode === 'slot'">
|
||||
<label class="row"><span>Spine</span>
|
||||
<select class="inp" :value="sys.config.renderLayerSpineId" @change="selectRenderLayerSpine">
|
||||
<option :value="0">{{ renderLayerReadySpines.length ? '请选择 Spine' : '无可用 Spine' }}</option>
|
||||
<option v-for="spine in renderLayerReadySpines" :key="spine.id" :value="spine.id">{{ spine.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<!-- 没有可用的 Spine 时,插槽选择与层级说明整体隐藏。 -->
|
||||
<template v-if="renderLayerSpineReady">
|
||||
<div class="render-layer-current">
|
||||
<span class="render-layer-text" :title="renderLayerSlotLabel">{{ renderLayerSlotLabel || '尚未选择插槽' }}</span>
|
||||
<button class="mini-btn" @click="renderLayerSlotPickerOpen = !renderLayerSlotPickerOpen">{{ renderLayerSlotLabel ? '重新选择' : '选择插槽' }}</button>
|
||||
<button v-if="sys.config.renderLayerSlotName" class="mini-btn ghost" @click="clearRenderLayerSlot">清除</button>
|
||||
</div>
|
||||
<div v-if="renderLayerSlotPickerOpen" class="slot-picker">
|
||||
<input v-model.trim="renderLayerSlotSearch" class="inp" type="search" placeholder="搜索插槽或附件名称…" aria-label="搜索插槽" />
|
||||
<div class="slot-list">
|
||||
<button
|
||||
v-for="slot in renderLayerFilteredSlots"
|
||||
:key="slot.name"
|
||||
class="slot-item"
|
||||
:class="{ on: sys.config.renderLayerSlotName === slot.name }"
|
||||
@click="pickRenderLayerSlot(slot)"
|
||||
>
|
||||
<span class="slot-order">{{ slot.order + 1 }}</span>
|
||||
<span class="slot-name">{{ slot.name }}</span>
|
||||
<span class="slot-attachment">{{ slot.attachment || '空插槽' }}</span>
|
||||
</button>
|
||||
<div v-if="!renderLayerFilteredSlots.length" class="modifier-hint">没有匹配的插槽</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="sys.config.renderLayerSlotName">
|
||||
<div class="seg">
|
||||
<button class="seg-btn" :class="{ active: sys.config.renderLayerSlotPosition === 'above' }" @click="sys.config.renderLayerSlotPosition = 'above'">在此插槽图片之上</button>
|
||||
<button class="seg-btn" :class="{ active: sys.config.renderLayerSlotPosition === 'below' }" @click="sys.config.renderLayerSlotPosition = 'below'">在此插槽图片之下</button>
|
||||
</div>
|
||||
<label class="switch-row render-layer-switch">
|
||||
<input v-model="sys.config.renderLayerFollowAttachment" type="checkbox" :disabled="sys.config.renderLayerSlotPosition === 'below'" />
|
||||
<span class="switch-ui"></span><span>随插槽附件显隐</span>
|
||||
</label>
|
||||
<div v-if="renderLayerResolvedLabel" class="modifier-hint">{{ renderLayerResolvedLabel }}</div>
|
||||
</template>
|
||||
<div v-if="renderLayerWarning" class="modifier-hint render-layer-warning">{{ renderLayerWarning }}</div>
|
||||
<div class="modifier-hint">调整显示层级只影响编辑器预览,不影响导出数据。</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 图片资源 -->
|
||||
<div class="look-title image-title">
|
||||
<span>图片资源</span>
|
||||
@@ -105,7 +158,7 @@ import ColorGradientEditor from '../ColorGradientEditor.vue'
|
||||
import ImageResourceCard from '../ImageResourceCard.vue'
|
||||
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
|
||||
import { useParticlePanelContext } from './particlePanelContext'
|
||||
const { resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, collapsed, toggle, resetModule, select, ALPHA_CURVE, RESOURCE_ALPHA_CURVE } = useParticlePanelContext()
|
||||
const { resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, collapsed, toggle, resetModule, select, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, ALPHA_CURVE, RESOURCE_ALPHA_CURVE } = useParticlePanelContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -154,4 +207,27 @@ const { resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, addIm
|
||||
.file-input { display: none; }
|
||||
.hex { font-size: 11px; color: #889; margin-left: auto; font-variant-numeric: tabular-nums; }
|
||||
.inp.tiny { width: 46px; flex: 0 0 46px; text-align: center; }
|
||||
.render-layer-current { display: flex; align-items: center; gap: 6px; margin: 6px 0; }
|
||||
.render-layer-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #9aa6c2; font-size: 11px; }
|
||||
.mini-btn { flex: 0 0 auto; height: 24px; padding: 0 7px; border: 1px solid #3d4a66; border-radius: 5px; background: #202a3e; color: #a9b5cf; font-size: 10px; cursor: pointer; }
|
||||
.mini-btn:hover { border-color: #7774ff; color: #fff; }
|
||||
.mini-btn.ghost { background: transparent; }
|
||||
.slot-picker { margin: 6px 0; padding: 6px; border: 1px solid #2e3a52; border-radius: 6px; background: #111a28; }
|
||||
.slot-list { max-height: 180px; overflow: auto; margin-top: 6px; }
|
||||
.slot-item { display: flex; align-items: center; gap: 6px; width: 100%; padding: 4px 6px; border: 1px solid transparent; border-radius: 4px; background: transparent; color: #aab4cc; font-size: 11px; text-align: left; cursor: pointer; }
|
||||
.slot-item:hover { background: #1d2a3f; }
|
||||
.slot-item.on { border-color: #7774ff; background: #2b2b55; color: #fff; }
|
||||
.slot-order { width: 26px; flex: 0 0 26px; color: #66758e; font-variant-numeric: tabular-nums; }
|
||||
.slot-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.slot-attachment { max-width: 45%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #6d7c96; font-size: 10px; }
|
||||
.switch-row { display: flex; align-items: center; gap: 9px; color: #b8c1d5; font-size: 12px; cursor: pointer; }
|
||||
.switch-row input { position: absolute; opacity: 0; pointer-events: none; }
|
||||
.switch-ui { position: relative; width: 32px; height: 18px; border-radius: 10px; background: #354258; transition: background .16s ease; }
|
||||
.switch-ui::after { content: ''; position: absolute; left: 2px; top: 2px; width: 14px; height: 14px; border-radius: 50%; background: #fff; transition: transform .16s ease; }
|
||||
.switch-row input:checked + .switch-ui { background: #625ff1; }
|
||||
.switch-row input:checked + .switch-ui::after { transform: translateX(14px); }
|
||||
.switch-row input:disabled + .switch-ui { opacity: .45; cursor: not-allowed; }
|
||||
.render-layer-switch { margin: 6px 0; }
|
||||
.modifier-hint { margin: 7px 0 0 2px; color: #647086; font-size: 9px; line-height: 1.5; }
|
||||
.render-layer-warning { color: #d8a45e; }
|
||||
</style>
|
||||
@@ -83,6 +83,104 @@ const boundSpineBone = computed(() => {
|
||||
if (!spine || !spine.bones.some((bone) => bone.name === config.emitterFollowBoneName)) return null
|
||||
return { spine, boneName: config.emitterFollowBoneName }
|
||||
})
|
||||
|
||||
// ===== 渲染层级(仅编辑器预览) =====
|
||||
const renderLayerSlotPickerOpen = ref(false)
|
||||
const renderLayerSlotSearch = ref('')
|
||||
const renderLayerReadySpines = computed(() =>
|
||||
store.spines.filter((spine) => spine.enabled && spine.visible !== false && spine.status === 'ready'),
|
||||
)
|
||||
const renderLayerSpine = computed(() =>
|
||||
store.spines.find((spine) => spine.id === sys.value?.config.renderLayerSpineId) || null,
|
||||
)
|
||||
const renderLayerSpineReady = computed(() => {
|
||||
const spine = renderLayerSpine.value
|
||||
return !!spine && spine.enabled && spine.visible !== false && spine.status === 'ready'
|
||||
})
|
||||
const renderLayerSlots = computed(() =>
|
||||
[...(renderLayerSpine.value?.slots || [])].sort((a, b) => a.order - b.order),
|
||||
)
|
||||
const renderLayerFilteredSlots = computed(() => {
|
||||
const query = renderLayerSlotSearch.value.trim().toLocaleLowerCase()
|
||||
if (!query) return renderLayerSlots.value
|
||||
return renderLayerSlots.value.filter((slot) =>
|
||||
slot.name.toLocaleLowerCase().includes(query) || slot.attachment.toLocaleLowerCase().includes(query),
|
||||
)
|
||||
})
|
||||
const renderLayerTargetSlot = computed(() =>
|
||||
renderLayerSlots.value.find((slot) => slot.name === sys.value?.config.renderLayerSlotName) || null,
|
||||
)
|
||||
const renderLayerPreviousSlot = computed(() => {
|
||||
const target = renderLayerTargetSlot.value
|
||||
if (!target) return null
|
||||
return renderLayerSlots.value.filter((slot) => slot.order < target.order).sort((a, b) => b.order - a.order)[0] || null
|
||||
})
|
||||
const renderLayerSlotLabel = computed(() => {
|
||||
const config = sys.value?.config
|
||||
const spine = renderLayerSpine.value
|
||||
const target = renderLayerTargetSlot.value
|
||||
if (!config || !spine || !target) return ''
|
||||
return `${spine.name} / ${target.name} · 图片${config.renderLayerSlotPosition === 'below' ? '之下' : '之上'}`
|
||||
})
|
||||
const renderLayerResolvedLabel = computed(() => {
|
||||
const config = sys.value?.config
|
||||
const target = renderLayerTargetSlot.value
|
||||
if (!config || !target) return ''
|
||||
if (config.renderLayerSlotPosition === 'above') return `渲染在 ${target.name} 图片之上、其后插槽之下`
|
||||
return renderLayerPreviousSlot.value
|
||||
? `渲染在 ${renderLayerPreviousSlot.value.name} 与 ${target.name} 之间`
|
||||
: ''
|
||||
})
|
||||
const renderLayerWarning = computed(() => {
|
||||
const config = sys.value?.config
|
||||
if (!config || config.renderLayerMode !== 'slot') return ''
|
||||
if (!renderLayerReadySpines.value.length) return '没有已加载的 Spine,粒子临时显示在最上层。'
|
||||
if (!config.renderLayerSpineId) return '尚未选择 Spine,粒子临时显示在最上层。'
|
||||
const spine = renderLayerSpine.value
|
||||
if (!spine) return '目标 Spine 不存在,粒子临时显示在最上层。'
|
||||
if (!spine.enabled || spine.status !== 'ready') return '目标 Spine 未就绪,粒子临时显示在最上层。'
|
||||
// 未选择插槽时面板本身已显示“尚未选择插槽”,这里不再重复提示。
|
||||
if (!config.renderLayerSlotName) return ''
|
||||
if (!renderLayerTargetSlot.value) return '目标插槽不存在(资源可能已更换),粒子临时显示在最上层。'
|
||||
if (config.renderLayerSlotPosition === 'below' && !renderLayerPreviousSlot.value) {
|
||||
return '这是第一个插槽,无法显示在其下方,粒子临时显示在最上层。'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
function setRenderLayerSlotMode() {
|
||||
const config = sys.value?.config
|
||||
if (!config) return
|
||||
config.renderLayerMode = 'slot'
|
||||
if (!config.renderLayerSpineId && renderLayerReadySpines.value.length) {
|
||||
config.renderLayerSpineId = renderLayerReadySpines.value[0].id
|
||||
}
|
||||
renderLayerSlotPickerOpen.value = !config.renderLayerSlotName
|
||||
}
|
||||
|
||||
function selectRenderLayerSpine(event: Event) {
|
||||
const config = sys.value?.config
|
||||
if (!config) return
|
||||
config.renderLayerSpineId = Math.max(0, Math.round(Number((event.target as HTMLSelectElement).value) || 0))
|
||||
config.renderLayerSlotName = ''
|
||||
renderLayerSlotPickerOpen.value = true
|
||||
}
|
||||
|
||||
function pickRenderLayerSlot(slot: { name: string }) {
|
||||
const config = sys.value?.config
|
||||
if (!config) return
|
||||
config.renderLayerSlotName = slot.name
|
||||
renderLayerSlotPickerOpen.value = false
|
||||
renderLayerSlotSearch.value = ''
|
||||
}
|
||||
|
||||
function clearRenderLayerSlot() {
|
||||
const config = sys.value?.config
|
||||
if (!config) return
|
||||
config.renderLayerSlotName = ''
|
||||
config.renderLayerSlotPosition = 'above'
|
||||
}
|
||||
|
||||
const collisionPickerOpen = ref(false)
|
||||
const selectedCollisionBodies = computed(() => {
|
||||
const ids = new Set(sys.value?.config.collisionColliderIds || [])
|
||||
@@ -96,6 +194,10 @@ const availableCollisionBodies = computed(() => {
|
||||
watch(() => [sys.value?.id, sys.value?.config.collision], () => {
|
||||
collisionPickerOpen.value = false
|
||||
})
|
||||
watch(() => sys.value?.id, () => {
|
||||
renderLayerSlotPickerOpen.value = false
|
||||
renderLayerSlotSearch.value = ''
|
||||
})
|
||||
|
||||
function addCollisionBody(colliderId: number) {
|
||||
const config = sys.value?.config
|
||||
@@ -156,6 +258,18 @@ watchEffect(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 渲染层级:仅当 Spine 对象被删除时清空绑定;资源尚未就绪时保留绑定,等加载完成后自动恢复。
|
||||
watchEffect(() => {
|
||||
for (const system of store.systems) {
|
||||
const config = ensureEmitterConfig(system.config as any)
|
||||
if (config.renderLayerMode !== 'slot' || !config.renderLayerSpineId) continue
|
||||
if (!store.spines.some((spine) => spine.id === config.renderLayerSpineId)) {
|
||||
config.renderLayerSpineId = 0
|
||||
config.renderLayerSlotName = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// HMR 或旧场景中的碰撞体缺少路径跟随字段时即时补齐。
|
||||
watchEffect(() => {
|
||||
for (const collider of store.colliders) {
|
||||
@@ -320,6 +434,8 @@ const MODULE_RESET_KEYS: Record<Exclude<ResetModule, 'export'>, string[]> = {
|
||||
'alpha', 'alphaEnd', 'colorStart', 'colorEnd', 'colorOverLife', 'blend',
|
||||
'alphaMode', 'curveMode', 'alphaCurve', 'textureName', 'textureFolder',
|
||||
'alphaScale', 'anchorX', 'anchorY', 'independentAlpha',
|
||||
'renderLayerMode', 'renderLayerSpineId', 'renderLayerSlotName',
|
||||
'renderLayerSlotPosition', 'renderLayerFollowAttachment',
|
||||
],
|
||||
mods: [
|
||||
'noise', 'noiseAmt', 'noiseRange', 'noiseSpeed',
|
||||
@@ -455,5 +571,5 @@ function onSpawnPathChange() {
|
||||
// 重置/删除图片 → 回到默认 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, saveConfig, 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, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, automaticLoopDuration, loopDurationFrames, activeObjectType, activeCollider, activePath, activeSpine, canCopyObject, selectedSpineBone, boundSpineBone, 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, saveConfig, 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, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, automaticLoopDuration, loopDurationFrames, activeObjectType, 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 }
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Texture, type Container } from 'pixi.js'
|
||||
import { Container, Texture } from 'pixi.js'
|
||||
import { ParticleEmitter, ensureEmitterConfig } from '../../core/particleEmitter'
|
||||
import { sampleScenePath } from '../../core/scenePathSampler'
|
||||
import type { EmitterConfig } from '../../core/particleTypes'
|
||||
import { followCurveValue } from './stageGeometry'
|
||||
import type { useParticleStore, CollisionBody } from '../../store/particleStore'
|
||||
import type { ScenePath } from '../../store/particleStore'
|
||||
import type { SpineRuntimeLayer } from '../../spine/SpineRuntimeLayer'
|
||||
import { resolveRenderTarget, type RenderTarget } from './renderLayer'
|
||||
|
||||
type EditorStore = ReturnType<typeof useParticleStore>
|
||||
|
||||
@@ -67,17 +69,28 @@ export function buildPreviewColliders(store: EditorStore, timeSeconds: number):
|
||||
export class EmitterSync {
|
||||
readonly emitters = new Map<number, ParticleEmitter>()
|
||||
private configs = new Map<number, object>()
|
||||
/** 渲染层级:一个插槽对应一个 wrapper(同插槽多系统共用),内部 compensator 抵消插槽骨骼变换。 */
|
||||
private slotGroups = new Map<string, {
|
||||
key: string
|
||||
spineId: number
|
||||
slotName: string
|
||||
wrapper: Container
|
||||
compensator: Container
|
||||
followAttachment: boolean
|
||||
attached: boolean
|
||||
}>()
|
||||
|
||||
sync(
|
||||
store: EditorStore,
|
||||
world: Container,
|
||||
dotTex: Texture<any> | null,
|
||||
trailTex: Texture<any> | null,
|
||||
spineRuntime: SpineRuntimeLayer | null = null,
|
||||
) {
|
||||
for (const [id, em] of this.emitters) {
|
||||
const system = store.systems.find((item) => item.id === id)
|
||||
if (!system || this.configs.get(id) !== system.config) {
|
||||
world.removeChild(em)
|
||||
em.removeFromParent()
|
||||
em.destroy({ children: true })
|
||||
this.emitters.delete(id)
|
||||
this.configs.delete(id)
|
||||
@@ -103,15 +116,98 @@ export class EmitterSync {
|
||||
this.emitters.set(sys.id, em)
|
||||
this.configs.set(sys.id, sys.config)
|
||||
}
|
||||
|
||||
const activeGroups = new Set<string>()
|
||||
for (const sys of store.systems) {
|
||||
const em = this.emitters.get(sys.id)
|
||||
if (!em) continue
|
||||
const drawOrder = spineRuntime && sys.config.renderLayerMode === 'slot'
|
||||
? spineRuntime.getDrawOrder(sys.config.renderLayerSpineId)
|
||||
: null
|
||||
const target = resolveRenderTarget(store.spines, sys.config, drawOrder)
|
||||
const group = target && spineRuntime ? this.ensureSlotGroup(target, spineRuntime, sys.config) : null
|
||||
if (group) {
|
||||
activeGroups.add(group.key)
|
||||
if (em.parent !== group.compensator) group.compensator.addChild(em)
|
||||
} else if (em.parent !== world) {
|
||||
world.addChild(em)
|
||||
}
|
||||
}
|
||||
for (const [key, group] of [...this.slotGroups]) {
|
||||
if (activeGroups.has(key)) continue
|
||||
for (const child of [...group.compensator.children]) {
|
||||
if (child instanceof ParticleEmitter && child.parent === group.compensator) world.addChild(child)
|
||||
}
|
||||
spineRuntime?.detachSlotObject(group.spineId, group.slotName, group.wrapper)
|
||||
group.wrapper.removeFromParent()
|
||||
group.wrapper.removeChild(group.compensator)
|
||||
group.compensator.destroy()
|
||||
group.wrapper.destroy()
|
||||
this.slotGroups.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
private ensureSlotGroup(
|
||||
target: RenderTarget,
|
||||
spineRuntime: SpineRuntimeLayer,
|
||||
config: { renderLayerFollowAttachment: boolean; renderLayerSlotPosition: string },
|
||||
) {
|
||||
// 以“用户配置的目标插槽 + 上/下”分组;drawOrder 变化导致实际挂载插槽改变时只重挂,不重建分组。
|
||||
const key = `${target.spineId}:${target.targetSlotName}:${config.renderLayerSlotPosition}`
|
||||
let group = this.slotGroups.get(key)
|
||||
if (!group) {
|
||||
const wrapper = new Container()
|
||||
const compensator = new Container()
|
||||
wrapper.addChild(compensator)
|
||||
group = {
|
||||
key,
|
||||
spineId: target.spineId,
|
||||
slotName: target.slotName,
|
||||
wrapper,
|
||||
compensator,
|
||||
followAttachment: config.renderLayerFollowAttachment,
|
||||
attached: false,
|
||||
}
|
||||
this.slotGroups.set(key, group)
|
||||
}
|
||||
// 同一插槽的多个系统共用一个 wrapper,跟随设置以首个挂载的系统为准,避免逐帧反复重挂。
|
||||
if (!group.attached || !group.wrapper.parent || group.slotName !== target.slotName) {
|
||||
if (group.wrapper.parent) spineRuntime.detachSlotObject(group.spineId, group.slotName, group.wrapper)
|
||||
group.slotName = target.slotName
|
||||
group.attached = spineRuntime.attachSlotObject(group.spineId, group.slotName, group.wrapper, target.followAttachment)
|
||||
}
|
||||
return group.attached ? group : null
|
||||
}
|
||||
|
||||
/** 每帧在 Spine 同步之后调用:把插槽骨骼变换抵消掉,粒子仍保持在编辑器世界坐标中。 */
|
||||
updateRenderCompensation(store: EditorStore, spineRuntime: SpineRuntimeLayer | null) {
|
||||
if (!spineRuntime) return
|
||||
for (const group of this.slotGroups.values()) {
|
||||
if (!group.attached || !group.wrapper.parent) continue
|
||||
const spine = store.spines.find((item) => item.id === group.spineId)
|
||||
if (!spine) continue
|
||||
const parent = spineRuntime.getSlotParentMatrix(group.spineId, group.slotName, spine)
|
||||
if (!parent) continue
|
||||
parent.invert()
|
||||
group.compensator.transform.setFromMatrix(parent)
|
||||
}
|
||||
}
|
||||
|
||||
/** 卸载时销毁全部发射器。 */
|
||||
destroy(world: Container) {
|
||||
for (const group of this.slotGroups.values()) {
|
||||
for (const child of [...group.compensator.children]) group.compensator.removeChild(child)
|
||||
group.wrapper.removeFromParent()
|
||||
group.compensator.destroy()
|
||||
group.wrapper.destroy()
|
||||
}
|
||||
this.slotGroups.clear()
|
||||
for (const [id, em] of this.emitters) {
|
||||
world.removeChild(em)
|
||||
em.removeFromParent()
|
||||
em.destroy({ children: true })
|
||||
this.emitters.delete(id)
|
||||
this.configs.delete(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/** 配置中的渲染层级字段(仅编辑器预览)。 */
|
||||
export interface RenderLayerConfig {
|
||||
renderLayerMode: string
|
||||
renderLayerSpineId: number
|
||||
renderLayerSlotName: string
|
||||
renderLayerSlotPosition: string
|
||||
renderLayerFollowAttachment: boolean
|
||||
}
|
||||
|
||||
export interface RenderLayerSlot {
|
||||
name: string
|
||||
order: number
|
||||
}
|
||||
|
||||
export interface RenderLayerSpine {
|
||||
id: number
|
||||
enabled: boolean
|
||||
status: string
|
||||
slots?: RenderLayerSlot[]
|
||||
}
|
||||
|
||||
/** 实际要挂载的插槽;below 会解析到当前 drawOrder 中的前一个插槽。 */
|
||||
export interface RenderTarget {
|
||||
spineId: number
|
||||
/** 实际挂载的插槽 */
|
||||
slotName: string
|
||||
/** 用户配置的目标插槽(用于分组与重挂判断) */
|
||||
targetSlotName: string
|
||||
followAttachment: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* SpinePixi 的 addSlotObject 渲染在该插槽附件之上,所以「在此插槽图片之下」
|
||||
* 需要挂到当前 drawOrder 中紧邻的前一个插槽。传入 drawOrder 时按动画实时顺序解析,
|
||||
* 否则回退到 setup 顺序(用于没有运行时或旧数据)。
|
||||
*/
|
||||
export function resolveRenderTarget(
|
||||
spines: readonly RenderLayerSpine[],
|
||||
config: RenderLayerConfig,
|
||||
drawOrder?: readonly string[] | null,
|
||||
): RenderTarget | null {
|
||||
if (config.renderLayerMode !== 'slot') return null
|
||||
const spine = spines.find((item) => item.id === config.renderLayerSpineId && item.enabled && item.status === 'ready')
|
||||
if (!spine) return null
|
||||
const slots = spine.slots || []
|
||||
const target = slots.find((slot) => slot.name === config.renderLayerSlotName)
|
||||
if (!target) return null
|
||||
if (config.renderLayerSlotPosition === 'below') {
|
||||
let attachSlotName = ''
|
||||
if (drawOrder?.length) {
|
||||
const index = drawOrder.indexOf(target.name)
|
||||
if (index === 0) return null
|
||||
if (index > 0) attachSlotName = drawOrder[index - 1]
|
||||
}
|
||||
if (!attachSlotName) {
|
||||
const previous = slots
|
||||
.filter((slot) => slot.order < target.order)
|
||||
.sort((a, b) => b.order - a.order)[0]
|
||||
if (!previous) return null
|
||||
attachSlotName = previous.name
|
||||
}
|
||||
return { spineId: spine.id, slotName: attachSlotName, targetSlotName: target.name, followAttachment: false }
|
||||
}
|
||||
return {
|
||||
spineId: spine.id,
|
||||
slotName: target.name,
|
||||
targetSlotName: target.name,
|
||||
followAttachment: config.renderLayerFollowAttachment,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user