导出优化
This commit is contained in:
1 parent
72d8daa283
commit
79ce3165c1
17 files changed
+1559
-1291
No files matched your search
+10
-6
@@ -3,6 +3,7 @@ import { constants } from 'node:fs'
|
||||
import { homedir, platform } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { execFile } from 'node:child_process'
|
||||
import { createSpine38ImportOptions } from './spine38-project.mjs'
|
||||
|
||||
const SUPPORTED_VERSIONS = new Set(['3.8', '4.0', '4.1', '4.2', '4.3'])
|
||||
|
||||
@@ -42,9 +43,9 @@ export function normalizeSpineVersion(value) {
|
||||
return match[1]
|
||||
}
|
||||
|
||||
function run(executablePath, args, timeoutMs = 180000) {
|
||||
function run(executablePath, args, { cwd, timeoutMs = 180000 } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(executablePath, args, { timeout: timeoutMs, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
|
||||
execFile(executablePath, args, { cwd, timeout: timeoutMs, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
const details = [stdout, stderr, error.message]
|
||||
.filter(Boolean)
|
||||
@@ -59,15 +60,18 @@ function run(executablePath, args, timeoutMs = 180000) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function importSpineProject({ executablePath, inputPath, outputPath, projectName, spineVersion }) {
|
||||
export async function importSpineProject({ executablePath, inputPath, outputPath, projectName, spineVersion, workspaceRoot }) {
|
||||
const version = normalizeSpineVersion(spineVersion)
|
||||
const editorVersion = `${version}.xx`
|
||||
const importOptions = version === '3.8'
|
||||
? createSpine38ImportOptions({ workspaceRoot, inputPath, outputPath })
|
||||
: { inputPath, outputPath }
|
||||
const result = await run(executablePath, [
|
||||
'--update', editorVersion,
|
||||
'--input', inputPath,
|
||||
'--output', outputPath,
|
||||
'--input', importOptions.inputPath,
|
||||
'--output', importOptions.outputPath,
|
||||
'--import', projectName,
|
||||
])
|
||||
], { cwd: importOptions.cwd })
|
||||
const output = await stat(outputPath).catch(() => null)
|
||||
if (!output?.isFile() || output.size === 0) throw new Error('Spine 未生成有效的工程文件')
|
||||
const detected = /Starting:\s+Spine\s+([0-9.]+)/i.exec(`${result.stdout}\n${result.stderr}`)?.[1] || editorVersion
|
||||
|
||||
@@ -105,6 +105,7 @@ const server = createServer(async (request, response) => {
|
||||
outputPath: workspace.outputPath,
|
||||
projectName: workspace.projectName,
|
||||
spineVersion,
|
||||
workspaceRoot: workspace.root,
|
||||
})
|
||||
const project = await workspace.readOutput()
|
||||
const projectFileName = `${safeProjectName(workspace.projectName)}.spine`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, normalize, sep } from 'node:path'
|
||||
import { prepareSpine38SkeletonJson } from './spine38-project.mjs'
|
||||
|
||||
const MAX_JSON_BYTES = 64 * 1024 * 1024
|
||||
const MAX_IMAGE_BYTES = 32 * 1024 * 1024
|
||||
@@ -49,7 +50,11 @@ export async function createConversionWorkspace(payload) {
|
||||
const projectName = safeProjectName(payload.projectName)
|
||||
const inputPath = join(root, `${projectName}.json`)
|
||||
const outputPath = join(root, `${projectName}.spine`)
|
||||
await writeFile(inputPath, payload.skeletonJson, 'utf8')
|
||||
const importJson = spineVersion === '3.8'
|
||||
? prepareSpine38SkeletonJson(skeleton, imagesDirectory)
|
||||
: payload.skeletonJson
|
||||
await mkdir(join(root, imagesDirectory), { recursive: true })
|
||||
await writeFile(inputPath, importJson, 'utf8')
|
||||
let total = 0
|
||||
const archiveImages = []
|
||||
for (const image of Array.isArray(payload.images) ? payload.images : []) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { relative, sep } from 'node:path'
|
||||
|
||||
function relativeWorkspacePath(workspaceRoot, targetPath) {
|
||||
const path = relative(workspaceRoot, targetPath)
|
||||
if (!path || path === '..' || path.startsWith(`..${sep}`)) {
|
||||
throw new Error('Spine 3.8 工程文件必须位于连接器工作目录内')
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
export function normalizeSpine38ImagesPath(imagesDirectory) {
|
||||
const path = String(imagesDirectory || 'images')
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/^\.\/+/, '')
|
||||
.replace(/\/+$/, '')
|
||||
return path || 'images'
|
||||
}
|
||||
|
||||
export function prepareSpine38SkeletonJson(skeleton, imagesDirectory) {
|
||||
const prepared = JSON.parse(JSON.stringify(skeleton))
|
||||
prepared.skeleton ||= {}
|
||||
// Spine 3.8 的命令行导入器对 "./images/" 的处理并不稳定,某些工程会
|
||||
// 将其固化为导入时的临时绝对路径。使用无前缀、无尾斜杠的工程内路径。
|
||||
prepared.skeleton.images = normalizeSpine38ImagesPath(imagesDirectory)
|
||||
return `${JSON.stringify(prepared, null, 2)}\n`
|
||||
}
|
||||
|
||||
export function createSpine38ImportOptions({ workspaceRoot, inputPath, outputPath }) {
|
||||
return {
|
||||
cwd: workspaceRoot,
|
||||
inputPath: relativeWorkspacePath(workspaceRoot, inputPath),
|
||||
outputPath: relativeWorkspacePath(workspaceRoot, outputPath),
|
||||
}
|
||||
}
|
||||
@@ -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-qzW_me3K.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-qzW_me3K.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-B_G0a-2w.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-B_G0a-2w.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
+1239
File diff suppressed because it is too large.
Load diff
@@ -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-qzW_me3K.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-qzW_me3K.js";import{SkeletonClipping as de,SkeletonPhysicsMovement as Ze,AnimationStateData as Je,AtlasAttachmentLoader as Qe,SkeletonBounds as je}from"./index-CKGEwrB2.js";import{AssetCache as qo,AssetManagerBase as Ko,Downloader as Zo,SkeletonRendererCore as Jo,Triangulator as Qo}from"./index-CKGEwrB2.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-B_G0a-2w.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-B_G0a-2w.js";import{SkeletonClipping as de,SkeletonPhysicsMovement as Ze,AnimationStateData as Je,AtlasAttachmentLoader as Qe,SkeletonBounds as je}from"./index-BIz1xbMt.js";import{AssetCache as qo,AssetManagerBase as Ko,Downloader as Zo,SkeletonRendererCore as Jo,Triangulator as Qo}from"./index-BIz1xbMt.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;
|
||||
|
||||
@@ -1 +1 @@
|
||||
.stage-wrap[data-v-aa9fab85]{position:relative;width:100%;height:100%;overflow:hidden}.stage-holder[data-v-aa9fab85]{width:100%;height:100%;cursor:grab;touch-action:none;background-color:#0e1219;outline:none}.stage-holder.grid[data-v-aa9fab85]{background:conic-gradient(#141a26 90deg,transparent 90deg 180deg,#141a26 180deg 270deg,transparent 270deg);background-size:28px 28px}.stage-holder[data-v-aa9fab85]:active{cursor:grabbing}.topbar[data-v-aa9fab85]{position:absolute;top:10px;left:10px;right:10px;display:flex;align-items:center;gap:10px;padding:8px 12px;background:#141420d9;border-radius:10px;border:1px solid rgba(255,255,255,.08);font-size:12px;color:#aab;z-index:5;flex-wrap:wrap}.topbar input[type=range][data-v-aa9fab85]{background:#1a1a28;border:1px solid #2e2e44;color:#dde;border-radius:5px}.tool-group[data-v-aa9fab85]{display:flex;gap:6px;margin-left:6px;padding-left:12px;border-left:1px solid #2a2a3c}.tool-btn[data-v-aa9fab85]{width:30px;height:26px;background:#1e1e2e;border:1px solid #2e2e44;border-radius:6px;color:#aab;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center}.tool-btn[data-v-aa9fab85]:hover{background:#2b3a5c;color:#fff}.tool-btn.on[data-v-aa9fab85]{background:#3a5a8c;color:#fff;border-color:#5a7ab8}.background-menu[data-v-aa9fab85]{position:relative;flex:0 0 auto}.background-btn[data-v-aa9fab85]{height:28px;padding:0 11px;border:1px solid #3a4963;border-radius:6px;background:#202a3b;color:#cbd7ed;font-size:12px;cursor:pointer}.background-btn[data-v-aa9fab85]:hover,.background-btn.on[data-v-aa9fab85]{border-color:#7774ff;background:#343b63;color:#fff}.background-panel[data-v-aa9fab85]{position:absolute;top:calc(100% + 9px);right:0;width:248px;box-sizing:border-box;padding:10px;border:1px solid #343b50;border-radius:9px;background:#141420fa;color:#aeb8cc;box-shadow:0 8px 24px #0000007a;z-index:12}.background-upload[data-v-aa9fab85]{width:100%;height:30px;border:1px solid #5652b5;border-radius:6px;background:#4540a0;color:#fff;cursor:pointer}.background-upload[data-v-aa9fab85]:hover{border-color:#8982ff;background:#554dc2}.background-clear[data-v-aa9fab85]{width:100%;height:28px;margin-top:6px;border:1px solid #70404a;border-radius:6px;background:#512a33;color:#efb5bd;cursor:pointer}.background-clear[data-v-aa9fab85]:hover:not(:disabled){border-color:#b65b6a;background:#6a303b;color:#fff}.background-clear[data-v-aa9fab85]:disabled{opacity:.42;cursor:not-allowed}.background-file-input[data-v-aa9fab85]{display:none}.background-file-name[data-v-aa9fab85]{margin:7px 2px 9px;overflow:hidden;color:#737f98;font-size:10px;text-overflow:ellipsis;white-space:nowrap}.background-position-row[data-v-aa9fab85]{display:grid;grid-template-columns:1fr 1fr;gap:8px}.background-position-row label[data-v-aa9fab85]{display:flex;align-items:center;gap:5px;color:#8995ad;font-size:11px}.background-position-row input[data-v-aa9fab85]{width:0;min-width:0;flex:1;box-sizing:border-box;height:28px;padding:0 6px;border:1px solid #343e55;border-radius:5px;background:#101522;color:#e1e6f2}.background-opacity-row[data-v-aa9fab85]{display:grid;grid-template-columns:46px 1fr 34px;align-items:center;gap:7px;margin-top:10px;font-size:11px}.background-opacity-row input[data-v-aa9fab85]{min-width:0;accent-color:#675de0}.background-opacity-row b[data-v-aa9fab85]{color:#aeb8cc;font-size:10px;text-align:right;font-variant-numeric:tabular-nums}.help-btn[data-v-aa9fab85]{height:28px;margin-left:auto;padding:0 11px;flex:0 0 auto;border:1px solid #3a4963;border-radius:6px;background:#202a3b;color:#cbd7ed;font-size:12px;font-weight:700;cursor:pointer}.help-btn[data-v-aa9fab85]:hover,.help-btn.on[data-v-aa9fab85]{border-color:#7774ff;background:#343b63;color:#fff}.zoom-pct[data-v-aa9fab85]{margin-left:0;font-variant-numeric:tabular-nums;color:#9aa;min-width:44px;text-align:center}.help-overlay[data-v-aa9fab85]{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-aa9fab85]{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-aa9fab85]{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-aa9fab85]{margin:0;color:#eef2ff;font-size:19px}.help-head p[data-v-aa9fab85]{margin:5px 0 0;color:#7786a3;font-size:11px}.help-close[data-v-aa9fab85]{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-aa9fab85]:hover{border-color:#7774ff;background:#3b4670;color:#fff}.help-body[data-v-aa9fab85]{min-height:0;overflowLine truncated
|
||||
.stage-wrap[data-v-aa9fab85]{position:relative;width:100%;height:100%;overflow:hidden}.stage-holder[data-v-aa9fab85]{width:100%;height:100%;cursor:grab;touch-action:none;background-color:#0e1219;outline:none}.stage-holder.grid[data-v-aa9fab85]{background:conic-gradient(#141a26 90deg,transparent 90deg 180deg,#141a26 180deg 270deg,transparent 270deg);background-size:28px 28px}.stage-holder[data-v-aa9fab85]:active{cursor:grabbing}.topbar[data-v-aa9fab85]{position:absolute;top:10px;left:10px;right:10px;display:flex;align-items:center;gap:10px;padding:8px 12px;background:#141420d9;border-radius:10px;border:1px solid rgba(255,255,255,.08);font-size:12px;color:#aab;z-index:5;flex-wrap:wrap}.topbar input[type=range][data-v-aa9fab85]{background:#1a1a28;border:1px solid #2e2e44;color:#dde;border-radius:5px}.tool-group[data-v-aa9fab85]{display:flex;gap:6px;margin-left:6px;padding-left:12px;border-left:1px solid #2a2a3c}.tool-btn[data-v-aa9fab85]{width:30px;height:26px;background:#1e1e2e;border:1px solid #2e2e44;border-radius:6px;color:#aab;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center}.tool-btn[data-v-aa9fab85]:hover{background:#2b3a5c;color:#fff}.tool-btn.on[data-v-aa9fab85]{background:#3a5a8c;color:#fff;border-color:#5a7ab8}.background-menu[data-v-aa9fab85]{position:relative;flex:0 0 auto}.background-btn[data-v-aa9fab85]{height:28px;padding:0 11px;border:1px solid #3a4963;border-radius:6px;background:#202a3b;color:#cbd7ed;font-size:12px;cursor:pointer}.background-btn[data-v-aa9fab85]:hover,.background-btn.on[data-v-aa9fab85]{border-color:#7774ff;background:#343b63;color:#fff}.background-panel[data-v-aa9fab85]{position:absolute;top:calc(100% + 9px);right:0;width:248px;box-sizing:border-box;padding:10px;border:1px solid #343b50;border-radius:9px;background:#141420fa;color:#aeb8cc;box-shadow:0 8px 24px #0000007a;z-index:12}.background-upload[data-v-aa9fab85]{width:100%;height:30px;border:1px solid #5652b5;border-radius:6px;background:#4540a0;color:#fff;cursor:pointer}.background-upload[data-v-aa9fab85]:hover{border-color:#8982ff;background:#554dc2}.background-clear[data-v-aa9fab85]{width:100%;height:28px;margin-top:6px;border:1px solid #70404a;border-radius:6px;background:#512a33;color:#efb5bd;cursor:pointer}.background-clear[data-v-aa9fab85]:hover:not(:disabled){border-color:#b65b6a;background:#6a303b;color:#fff}.background-clear[data-v-aa9fab85]:disabled{opacity:.42;cursor:not-allowed}.background-file-input[data-v-aa9fab85]{display:none}.background-file-name[data-v-aa9fab85]{margin:7px 2px 9px;overflow:hidden;color:#737f98;font-size:10px;text-overflow:ellipsis;white-space:nowrap}.background-position-row[data-v-aa9fab85]{display:grid;grid-template-columns:1fr 1fr;gap:8px}.background-position-row label[data-v-aa9fab85]{display:flex;align-items:center;gap:5px;color:#8995ad;font-size:11px}.background-position-row input[data-v-aa9fab85]{width:0;min-width:0;flex:1;box-sizing:border-box;height:28px;padding:0 6px;border:1px solid #343e55;border-radius:5px;background:#101522;color:#e1e6f2}.background-opacity-row[data-v-aa9fab85]{display:grid;grid-template-columns:46px 1fr 34px;align-items:center;gap:7px;margin-top:10px;font-size:11px}.background-opacity-row input[data-v-aa9fab85]{min-width:0;accent-color:#675de0}.background-opacity-row b[data-v-aa9fab85]{color:#aeb8cc;font-size:10px;text-align:right;font-variant-numeric:tabular-nums}.help-btn[data-v-aa9fab85]{height:28px;margin-left:auto;padding:0 11px;flex:0 0 auto;border:1px solid #3a4963;border-radius:6px;background:#202a3b;color:#cbd7ed;font-size:12px;font-weight:700;cursor:pointer}.help-btn[data-v-aa9fab85]:hover,.help-btn.on[data-v-aa9fab85]{border-color:#7774ff;background:#343b63;color:#fff}.zoom-pct[data-v-aa9fab85]{margin-left:0;font-variant-numeric:tabular-nums;color:#9aa;min-width:44px;text-align:center}.help-overlay[data-v-aa9fab85]{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-aa9fab85]{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-aa9fab85]{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-aa9fab85]{margin:0;color:#eef2ff;font-size:19px}.help-head p[data-v-aa9fab85]{margin:5px 0 0;color:#7786a3;font-size:11px}.help-close[data-v-aa9fab85]{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-aa9fab85]:hover{border-color:#7774ff;background:#3b4670;color:#fff}.help-body[data-v-aa9fab85]{min-height:0;overflowLine truncated
|
||||
Vendored
-1239
File diff suppressed because it is too large.
Load diff
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-qzW_me3K.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D3l7sHON.css">
|
||||
<script type="module" crossorigin src="/assets/index-B_G0a-2w.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D5HEdVAv.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"test:timeline": "node scripts/verify-timeline.mjs",
|
||||
"test:spine-json": "node scripts/verify-spine-json.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",
|
||||
"test:connector-versions": "node scripts/verify-spine-connector-versions.mjs",
|
||||
"preview": "vite preview"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { access, readFile } from 'node:fs/promises'
|
||||
import { createSpine38ImportOptions, prepareSpine38SkeletonJson } from '../connector/spine38-project.mjs'
|
||||
import { createConversionWorkspace } from '../connector/spine-workspace.mjs'
|
||||
|
||||
const source = {
|
||||
skeleton: { spine: '3.8', images: './images/', fps: 30 },
|
||||
bones: [{ name: 'root' }],
|
||||
slots: [],
|
||||
skins: { default: {} },
|
||||
animations: { animation: {} },
|
||||
}
|
||||
|
||||
const prepared = JSON.parse(prepareSpine38SkeletonJson(source, 'images'))
|
||||
assert.equal(prepared.skeleton.images, 'images')
|
||||
assert.equal(source.skeleton.images, './images/')
|
||||
|
||||
const workspace = await createConversionWorkspace({
|
||||
schemaVersion: 1,
|
||||
projectName: 'Spine38PathTest',
|
||||
spineVersion: '3.8',
|
||||
skeletonJson: JSON.stringify(source),
|
||||
images: [],
|
||||
})
|
||||
|
||||
try {
|
||||
const written = JSON.parse(await readFile(workspace.inputPath, 'utf8'))
|
||||
assert.equal(written.skeleton.images, 'images')
|
||||
await access(`${workspace.root}/images`)
|
||||
|
||||
const invocation = createSpine38ImportOptions({
|
||||
workspaceRoot: workspace.root,
|
||||
inputPath: workspace.inputPath,
|
||||
outputPath: workspace.outputPath,
|
||||
})
|
||||
assert.equal(invocation.cwd, workspace.root)
|
||||
assert.equal(invocation.inputPath, 'Spine38PathTest.json')
|
||||
assert.equal(invocation.outputPath, 'Spine38PathTest.spine')
|
||||
console.log('Spine 3.8 图片路径预处理与相对路径导入检查通过')
|
||||
} finally {
|
||||
await workspace.cleanup()
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TransferProgressOverlay />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -24,6 +25,7 @@ import { installSystemSettingsCache } from './editor/systemSettings'
|
||||
import Stage from './views/Stage.vue'
|
||||
import ParticlePanel from './views/ParticlePanel.vue'
|
||||
import Timeline from './views/Timeline.vue'
|
||||
import TransferProgressOverlay from './views/TransferProgressOverlay.vue'
|
||||
|
||||
const store = useParticleStore()
|
||||
installSystemSettingsCache(store)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export interface TransferProgressState {
|
||||
visible: boolean
|
||||
title: string
|
||||
detail: string
|
||||
progress: number
|
||||
indeterminate: boolean
|
||||
failed: boolean
|
||||
}
|
||||
|
||||
export const transferProgress = reactive<TransferProgressState>({
|
||||
visible: false,
|
||||
title: '',
|
||||
detail: '',
|
||||
progress: 0,
|
||||
indeterminate: false,
|
||||
failed: false,
|
||||
})
|
||||
|
||||
let operationId = 0
|
||||
|
||||
function delay(ms: number) {
|
||||
return new Promise<void>((resolve) => window.setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
/** 管理全屏传输提示;新操作会自动取代尚未结束的旧操作。 */
|
||||
export function beginTransfer(title: string, detail: string) {
|
||||
const id = ++operationId
|
||||
const startedAt = performance.now()
|
||||
Object.assign(transferProgress, {
|
||||
visible: true,
|
||||
title,
|
||||
detail,
|
||||
progress: 0,
|
||||
indeterminate: false,
|
||||
failed: false,
|
||||
})
|
||||
|
||||
const update = (progress: number | null, nextDetail?: string) => {
|
||||
if (id !== operationId) return
|
||||
transferProgress.indeterminate = progress == null
|
||||
if (progress != null) transferProgress.progress = Math.min(100, Math.max(0, progress))
|
||||
if (nextDetail) transferProgress.detail = nextDetail
|
||||
}
|
||||
|
||||
const closeAfter = async (failed: boolean, nextDetail: string) => {
|
||||
if (id !== operationId) return
|
||||
transferProgress.failed = failed
|
||||
transferProgress.indeterminate = false
|
||||
transferProgress.progress = failed ? transferProgress.progress : 100
|
||||
transferProgress.detail = nextDetail
|
||||
const minimumVisible = failed ? 850 : 450
|
||||
await delay(Math.max(180, minimumVisible - (performance.now() - startedAt)))
|
||||
if (id === operationId) transferProgress.visible = false
|
||||
}
|
||||
|
||||
return {
|
||||
update,
|
||||
/** 让初始状态先绘制出来,再执行可能阻塞主线程的序列化。 */
|
||||
paint: () => new Promise<void>((resolve) => requestAnimationFrame(() => resolve())),
|
||||
finish: (nextDetail = '已完成') => closeAfter(false, nextDetail),
|
||||
fail: (nextDetail = '操作失败') => closeAfter(true, nextDetail),
|
||||
}
|
||||
}
|
||||
|
||||
/** 读取 HTTP 响应并按 Content-Length 汇报真实下载进度。 */
|
||||
export async function readResponseTextWithProgress(
|
||||
response: Response,
|
||||
onProgress: (progress: number | null) => void,
|
||||
) {
|
||||
if (!response.body) {
|
||||
onProgress(null)
|
||||
return response.text()
|
||||
}
|
||||
const total = Number(response.headers.get('content-length'))
|
||||
const hasTotal = Number.isFinite(total) && total > 0
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let loaded = 0
|
||||
let text = ''
|
||||
onProgress(hasTotal ? 0 : null)
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
loaded += value.byteLength
|
||||
text += decoder.decode(value, { stream: true })
|
||||
onProgress(hasTotal ? Math.min(1, loaded / total) : null)
|
||||
}
|
||||
text += decoder.decode()
|
||||
if (hasTotal) onProgress(1)
|
||||
return text
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ export interface ConnectorImage {
|
||||
dataBase64: string
|
||||
}
|
||||
|
||||
export interface SpineConnectorProgress {
|
||||
phase: 'images' | 'upload' | 'processing' | 'download'
|
||||
progress: number | null
|
||||
}
|
||||
|
||||
interface ConnectorErrorBody {
|
||||
code?: string
|
||||
message?: string
|
||||
@@ -74,21 +79,26 @@ async function sourceToImage(source: string, folder: string, name: string): Prom
|
||||
}
|
||||
|
||||
/** 收集 Spine JSON 中附件会引用的粒子与拖尾图片。 */
|
||||
export async function collectSpineExportImages(systems: ParticleSystem[]) {
|
||||
export async function collectSpineExportImages(systems: ParticleSystem[], onProgress?: (completed: number, total: number) => void) {
|
||||
const requests: Array<Promise<ConnectorImage | null>> = []
|
||||
let completed = 0
|
||||
const track = (request: Promise<ConnectorImage | null>) => request.finally(() => {
|
||||
completed++
|
||||
onProgress?.(completed, requests.length)
|
||||
})
|
||||
for (const system of systems.filter((item) => item.visible !== false)) {
|
||||
for (const resource of system.config.imageResources) {
|
||||
if (resource.imageMode === 'sequence' && resource.sequenceFrames.length) {
|
||||
for (const frame of resource.sequenceFrames) {
|
||||
requests.push(sourceToImage(frame.previewUrl, resource.textureFolder, frame.name || resource.textureName))
|
||||
requests.push(track(sourceToImage(frame.previewUrl, resource.textureFolder, frame.name || resource.textureName)))
|
||||
}
|
||||
} else {
|
||||
requests.push(sourceToImage(resource.previewUrl, resource.textureFolder, resource.textureName))
|
||||
requests.push(track(sourceToImage(resource.previewUrl, resource.textureFolder, resource.textureName)))
|
||||
}
|
||||
}
|
||||
if (system.config.trail) {
|
||||
for (const resource of system.config.trailResources) {
|
||||
requests.push(sourceToImage(resource.previewUrl, '', resource.textureName))
|
||||
requests.push(track(sourceToImage(resource.previewUrl, '', resource.textureName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,39 +123,53 @@ export async function convertWithSpineConnector(input: {
|
||||
spineVersion: SpineExportVersion
|
||||
skeletonJson: string
|
||||
systems: ParticleSystem[]
|
||||
}) {
|
||||
const images = await collectSpineExportImages(input.systems)
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(`${CONNECTOR_URL}/v1/convert`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
projectName: input.projectName,
|
||||
spineVersion: input.spineVersion,
|
||||
skeletonJson: input.skeletonJson,
|
||||
images,
|
||||
}),
|
||||
})
|
||||
} catch {
|
||||
throw new SpineConnectorError('未检测到本地 Spine 连接器,请先启动连接器', 'CONNECTOR_OFFLINE')
|
||||
}
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => ({} as ConnectorErrorBody)) as ConnectorErrorBody
|
||||
throw new SpineConnectorError(body.message || `Spine 连接器返回错误:HTTP ${response.status}`, body.code, body.pairUrl)
|
||||
}
|
||||
const encodedFileName = response.headers.get('X-Spine-File-Name')
|
||||
let fileName = `${cleanPart(input.projectName, 'SpineParticle')}.zip`
|
||||
if (encodedFileName) {
|
||||
try { fileName = decodeURIComponent(encodedFileName) } catch { /* 保留安全的默认文件名 */ }
|
||||
}
|
||||
return {
|
||||
blob: await response.blob(),
|
||||
fileName,
|
||||
spineEditorVersion: response.headers.get('X-Spine-Editor-Version') || input.spineVersion,
|
||||
imageCount: images.length,
|
||||
}
|
||||
}, onProgress?: (progress: SpineConnectorProgress) => void) {
|
||||
const images = await collectSpineExportImages(input.systems, (completed, total) => {
|
||||
onProgress?.({ phase: 'images', progress: total ? completed / total : 1 })
|
||||
})
|
||||
const body = JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
projectName: input.projectName,
|
||||
spineVersion: input.spineVersion,
|
||||
skeletonJson: input.skeletonJson,
|
||||
images,
|
||||
})
|
||||
|
||||
return new Promise<{ blob: Blob; fileName: string; spineEditorVersion: string; imageCount: number }>((resolve, reject) => {
|
||||
const request = new XMLHttpRequest()
|
||||
request.open('POST', `${CONNECTOR_URL}/v1/convert`)
|
||||
request.responseType = 'blob'
|
||||
request.setRequestHeader('Content-Type', 'application/json')
|
||||
request.upload.onprogress = (event) => {
|
||||
onProgress?.({ phase: 'upload', progress: event.lengthComputable && event.total > 0 ? event.loaded / event.total : null })
|
||||
}
|
||||
request.upload.onloadend = () => onProgress?.({ phase: 'processing', progress: null })
|
||||
request.onprogress = (event) => {
|
||||
onProgress?.({ phase: 'download', progress: event.lengthComputable && event.total > 0 ? event.loaded / event.total : null })
|
||||
}
|
||||
request.onerror = () => reject(new SpineConnectorError('未检测到本地 Spine 连接器,请先启动连接器', 'CONNECTOR_OFFLINE'))
|
||||
request.onload = async () => {
|
||||
const responseBlob = request.response as Blob
|
||||
if (request.status < 200 || request.status >= 300) {
|
||||
let errorBody: ConnectorErrorBody = {}
|
||||
try { errorBody = JSON.parse(await responseBlob.text()) as ConnectorErrorBody } catch { /* 使用 HTTP 回退信息 */ }
|
||||
reject(new SpineConnectorError(errorBody.message || `Spine 连接器返回错误:HTTP ${request.status}`, errorBody.code, errorBody.pairUrl))
|
||||
return
|
||||
}
|
||||
const encodedFileName = request.getResponseHeader('X-Spine-File-Name')
|
||||
let fileName = `${cleanPart(input.projectName, 'SpineParticle')}.zip`
|
||||
if (encodedFileName) {
|
||||
try { fileName = decodeURIComponent(encodedFileName) } catch { /* 保留安全的默认文件名 */ }
|
||||
}
|
||||
resolve({
|
||||
blob: responseBlob,
|
||||
fileName,
|
||||
spineEditorVersion: request.getResponseHeader('X-Spine-Editor-Version') || input.spineVersion,
|
||||
imageCount: images.length,
|
||||
})
|
||||
}
|
||||
request.send(body)
|
||||
})
|
||||
}
|
||||
|
||||
export function downloadSpineProject(blob: Blob, fileName: string) {
|
||||
|
||||
@@ -1002,6 +1002,7 @@ import ColorGradientEditor from './ColorGradientEditor.vue'
|
||||
import ImageResourceCard from './ImageResourceCard.vue'
|
||||
import SpineObjectPanel from '../spine/SpineObjectPanel.vue'
|
||||
import { downloadEditorConfig, loadEditorConfigFile, loadEditorConfigText } from '../editor/editorConfig'
|
||||
import { beginTransfer, readResponseTextWithProgress } from '../editor/transferProgress'
|
||||
import { defaultSpineExportSettings, ensureSpineExportSettings } from '../export/spineExportSettings'
|
||||
import { buildSpineJson, downloadSpineJson } from '../export/spineJsonExporter'
|
||||
import { validateSpineJson } from '../export/spineJsonValidator'
|
||||
@@ -1112,15 +1113,24 @@ async function loadSelectedPreset() {
|
||||
presetLoading.value = true
|
||||
configError.value = false
|
||||
configMessage.value = '正在加载预设…'
|
||||
const transfer = beginTransfer('加载预设', `正在下载 ${file}…`)
|
||||
try {
|
||||
await transfer.paint()
|
||||
const response = await fetch(`/Preset/${encodeURIComponent(file)}`, { cache: 'no-store' })
|
||||
if (!response.ok) throw new Error(`无法读取预设:${file}`)
|
||||
await loadEditorConfigText(store, await response.text())
|
||||
const text = await readResponseTextWithProgress(response, (progress) => {
|
||||
transfer.update(progress == null ? null : progress * 55, `正在下载 ${file}…`)
|
||||
})
|
||||
transfer.update(58, '正在解析预设配置…')
|
||||
transfer.update(70, '正在解析并恢复预设资源…')
|
||||
await loadEditorConfigText(store, text)
|
||||
configMessage.value = `已加载预设:${file.replace(/\.json$/i, '')}`
|
||||
await transfer.finish('预设加载完成')
|
||||
} catch (error) {
|
||||
configError.value = true
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
configMessage.value = `${message},请检查预设文件命名`
|
||||
await transfer.fail(message)
|
||||
} finally {
|
||||
presetLoading.value = false
|
||||
selectedPreset.value = ''
|
||||
@@ -1146,7 +1156,7 @@ function nextPaint() {
|
||||
return new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
||||
}
|
||||
|
||||
async function prepareExportFrames() {
|
||||
async function prepareExportFrames(onProgress?: (progress: number) => void) {
|
||||
store.timeline.playing = false
|
||||
store.timeline.frame = 0
|
||||
store.timeline.recorded = false
|
||||
@@ -1158,8 +1168,10 @@ async function prepareExportFrames() {
|
||||
const deadline = performance.now() + Math.max(8000, expectedMs * 2.5 + 3000)
|
||||
while (!store.timeline.recorded) {
|
||||
if (performance.now() > deadline) throw new Error('重新计算动画超时,请确认页面仍在运行后重试')
|
||||
onProgress?.(Math.min(0.98, store.timeline.frame / Math.max(1, store.timeline.totalFrames - 1)))
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
}
|
||||
onProgress?.(1)
|
||||
}
|
||||
|
||||
async function exportSpineJson() {
|
||||
@@ -1192,8 +1204,11 @@ async function exportSpineProject() {
|
||||
exportError.value = false
|
||||
connectorPairUrl.value = ''
|
||||
exportMessage.value = '正在重新计算粒子动画…'
|
||||
const transfer = beginTransfer('导出 Spine 包', '正在重新计算粒子动画…')
|
||||
try {
|
||||
await prepareExportFrames()
|
||||
await transfer.paint()
|
||||
await prepareExportFrames((progress) => transfer.update(progress * 18, '正在重新计算粒子动画…'))
|
||||
transfer.update(20, '正在生成 Spine JSON…')
|
||||
const result = buildSpineJson({ systems: store.systems as unknown as ParticleSystem[], timeline: store.timeline, settings: exportSettings.value })
|
||||
validateSpineJson(result.text, exportSettings.value.spineVersion)
|
||||
exportMessage.value = '正在整理图片并调用本机 Spine…'
|
||||
@@ -1203,15 +1218,23 @@ async function exportSpineProject() {
|
||||
spineVersion: exportSettings.value.spineVersion,
|
||||
skeletonJson: result.text,
|
||||
systems: store.systems as unknown as ParticleSystem[],
|
||||
}, ({ phase, progress }) => {
|
||||
if (phase === 'images') transfer.update(progress == null ? null : 22 + progress * 18, '正在整理工程图片…')
|
||||
else if (phase === 'upload') transfer.update(progress == null ? null : 40 + progress * 30, '正在上传数据到 Spine 连接器…')
|
||||
else if (phase === 'processing') transfer.update(null, 'Spine 正在转换工程,请稍候…')
|
||||
else transfer.update(progress == null ? null : 82 + progress * 16, '正在下载生成的 Spine 工程包…')
|
||||
})
|
||||
transfer.update(99, '正在交给浏览器下载…')
|
||||
downloadSpineProject(converted.blob, converted.fileName)
|
||||
exportMessage.value = `已生成 ${converted.fileName}:包含 .spine 工程和 ${converted.imageCount} 张图片,Spine ${converted.spineEditorVersion}`
|
||||
await refreshConnectorHealth()
|
||||
await transfer.finish('Spine 工程包已开始下载')
|
||||
} catch (error) {
|
||||
exportError.value = true
|
||||
if (error instanceof SpineConnectorError && error.pairUrl) connectorPairUrl.value = error.pairUrl
|
||||
exportMessage.value = error instanceof Error ? error.message : String(error)
|
||||
await refreshConnectorHealth()
|
||||
await transfer.fail(exportMessage.value)
|
||||
} finally {
|
||||
exportBusy.value = false
|
||||
exportTarget.value = null
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<Transition name="transfer-fade">
|
||||
<div v-if="transferProgress.visible" class="transfer-overlay" role="status" aria-live="polite">
|
||||
<div class="transfer-dialog" :class="{ failed: transferProgress.failed }">
|
||||
<div class="transfer-heading">
|
||||
<span>{{ transferProgress.title }}</span>
|
||||
<strong v-if="!transferProgress.indeterminate">{{ Math.round(transferProgress.progress) }}%</strong>
|
||||
</div>
|
||||
<div class="transfer-track" :class="{ indeterminate: transferProgress.indeterminate }">
|
||||
<div class="transfer-bar" :style="transferProgress.indeterminate ? undefined : { width: `${transferProgress.progress}%` }"></div>
|
||||
</div>
|
||||
<div class="transfer-detail">{{ transferProgress.detail }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { transferProgress } from '../editor/transferProgress'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.transfer-overlay { position: fixed; z-index: 2000; inset: 0; display: grid; place-items: center; pointer-events: none; background: rgba(5, 8, 15, 0.26); backdrop-filter: blur(1px); }
|
||||
.transfer-dialog { width: min(420px, calc(100vw - 40px)); padding: 18px 20px 16px; border: 1px solid #414b66; border-radius: 10px; box-shadow: 0 18px 55px rgba(0, 0, 0, 0.46); background: #171d2b; color: #e7ebf5; }
|
||||
.transfer-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 12px; font-size: 14px; font-weight: 700; }
|
||||
.transfer-heading strong { color: #a8a6ff; font-size: 12px; font-variant-numeric: tabular-nums; }
|
||||
.transfer-track { height: 9px; overflow: hidden; border: 1px solid #35415a; border-radius: 999px; background: #0d1422; }
|
||||
.transfer-bar { height: 100%; border-radius: inherit; background: linear-gradient(90deg, #4d75e8, #7774ff); transition: width .16s ease; }
|
||||
.transfer-track.indeterminate .transfer-bar { width: 42%; animation: transfer-slide 1s ease-in-out infinite; }
|
||||
.transfer-detail { min-height: 17px; margin-top: 9px; color: #8895ae; font-size: 11px; line-height: 1.5; }
|
||||
.transfer-dialog.failed { border-color: #754351; }
|
||||
.transfer-dialog.failed .transfer-heading strong, .transfer-dialog.failed .transfer-detail { color: #e99aa9; }
|
||||
.transfer-dialog.failed .transfer-bar { background: #c85d70; }
|
||||
.transfer-fade-enter-active, .transfer-fade-leave-active { transition: opacity .16s ease; }
|
||||
.transfer-fade-enter-from, .transfer-fade-leave-to { opacity: 0; }
|
||||
@keyframes transfer-slide { from { transform: translateX(-110%); } to { transform: translateX(250%); } }
|
||||
</style>
|
||||
|
||||
Reference in new issue
Block a user