支持spine4.3
This commit is contained in:
@@ -1,25 +1,58 @@
|
||||
import {
|
||||
BoundingBoxAttachment,
|
||||
ClippingAttachment,
|
||||
MeshAttachment,
|
||||
PathAttachment,
|
||||
PointAttachment,
|
||||
RegionAttachment,
|
||||
SkeletonJson,
|
||||
type AttachmentLoader,
|
||||
type Skin,
|
||||
BoundingBoxAttachment as BoundingBoxAttachment43,
|
||||
ClippingAttachment as ClippingAttachment43,
|
||||
MeshAttachment as MeshAttachment43,
|
||||
PathAttachment as PathAttachment43,
|
||||
PointAttachment as PointAttachment43,
|
||||
RegionAttachment as RegionAttachment43,
|
||||
SkeletonJson as SkeletonJson43,
|
||||
TextureRegion as TextureRegion43,
|
||||
type AttachmentLoader as AttachmentLoader43,
|
||||
type Sequence as Sequence43,
|
||||
type Skin as Skin43,
|
||||
} from '@spine-particle/spine-core-4.3'
|
||||
import {
|
||||
BoundingBoxAttachment as BoundingBoxAttachment42,
|
||||
ClippingAttachment as ClippingAttachment42,
|
||||
MeshAttachment as MeshAttachment42,
|
||||
PathAttachment as PathAttachment42,
|
||||
PointAttachment as PointAttachment42,
|
||||
RegionAttachment as RegionAttachment42,
|
||||
SkeletonJson as SkeletonJson42,
|
||||
type AttachmentLoader as AttachmentLoader42,
|
||||
type Skin as Skin42,
|
||||
} from '@esotericsoftware/spine-core'
|
||||
import { normalizeSpine38JsonFor42 } from '../spine/spine38Compatibility'
|
||||
import { normalizeSpineVersion, supportedSpineVersionsText, type SupportedSpineVersion } from '../spine/spineVersion'
|
||||
import type { SpineJsonMap } from './spineJsonTypes'
|
||||
|
||||
class ExportValidationAttachmentLoader implements AttachmentLoader {
|
||||
newRegionAttachment(_skin: Skin, name: string, path: string, _sequence: any) { return new RegionAttachment(name, path) }
|
||||
newMeshAttachment(_skin: Skin, name: string, path: string, _sequence: any) { return new MeshAttachment(name, path) }
|
||||
newBoundingBoxAttachment(_skin: Skin, name: string) { return new BoundingBoxAttachment(name) }
|
||||
newPathAttachment(_skin: Skin, name: string) { return new PathAttachment(name) }
|
||||
newPointAttachment(_skin: Skin, name: string) { return new PointAttachment(name) }
|
||||
newClippingAttachment(_skin: Skin, name: string) { return new ClippingAttachment(name) }
|
||||
class ExportValidationAttachmentLoader42 implements AttachmentLoader42 {
|
||||
newRegionAttachment(_skin: Skin42, name: string, path: string, _sequence: any) { return new RegionAttachment42(name, path) }
|
||||
newMeshAttachment(_skin: Skin42, name: string, path: string, _sequence: any) { return new MeshAttachment42(name, path) }
|
||||
newBoundingBoxAttachment(_skin: Skin42, name: string) { return new BoundingBoxAttachment42(name) }
|
||||
newPathAttachment(_skin: Skin42, name: string) { return new PathAttachment42(name) }
|
||||
newPointAttachment(_skin: Skin42, name: string) { return new PointAttachment42(name) }
|
||||
newClippingAttachment(_skin: Skin42, name: string) { return new ClippingAttachment42(name) }
|
||||
}
|
||||
|
||||
class ExportValidationAttachmentLoader43 implements AttachmentLoader43 {
|
||||
private sequence(sequence: Sequence43) {
|
||||
for (let index = 0; index < sequence.regions.length; index++) {
|
||||
if (sequence.regions[index]) continue
|
||||
const region = new TextureRegion43()
|
||||
region.u = 0; region.v = 0; region.u2 = 1; region.v2 = 1
|
||||
region.width = 1; region.height = 1; region.originalWidth = 1; region.originalHeight = 1
|
||||
region.offsetX = 0; region.offsetY = 0; region.degrees = 0
|
||||
sequence.regions[index] = region
|
||||
}
|
||||
return sequence
|
||||
}
|
||||
newRegionAttachment(_skin: Skin43, _placeholder: string, name: string, _path: string, sequence: Sequence43) { return new RegionAttachment43(name, this.sequence(sequence)) }
|
||||
newMeshAttachment(_skin: Skin43, _placeholder: string, name: string, _path: string, sequence: Sequence43) { return new MeshAttachment43(name, this.sequence(sequence)) }
|
||||
newBoundingBoxAttachment(_skin: Skin43, _placeholder: string, name: string) { return new BoundingBoxAttachment43(name) }
|
||||
newPathAttachment(_skin: Skin43, _placeholder: string, name: string) { return new PathAttachment43(name) }
|
||||
newPointAttachment(_skin: Skin43, _placeholder: string, name: string) { return new PointAttachment43(name) }
|
||||
newClippingAttachment(_skin: Skin43, _placeholder: string, name: string) { return new ClippingAttachment43(name) }
|
||||
}
|
||||
|
||||
const VALID_INHERIT = new Set(['normal', 'onlyTranslation', 'noRotationOrReflection', 'noScale', 'noScaleOrReflection'])
|
||||
@@ -136,8 +169,9 @@ export function validateSpineJson(text: string, expectedVersion?: SupportedSpine
|
||||
validateEditorImportStructure(root, version)
|
||||
if (version === '3.8') validateSpine38Structure(root)
|
||||
const runtimeRoot = version === '3.8' ? normalizeSpine38JsonFor42(JSON.parse(JSON.stringify(root))) : root
|
||||
const reader = new SkeletonJson(new ExportValidationAttachmentLoader())
|
||||
const data = reader.readSkeletonData(runtimeRoot)
|
||||
const data = version === '4.3'
|
||||
? new SkeletonJson43(new ExportValidationAttachmentLoader43()).readSkeletonData(runtimeRoot)
|
||||
: new SkeletonJson42(new ExportValidationAttachmentLoader42()).readSkeletonData(runtimeRoot)
|
||||
if (!data.bones.length) throw new Error('导出结果缺少骨骼')
|
||||
return { version, bones: data.bones.length, slots: data.slots.length, animations: data.animations.length }
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<button class="clear" @click="clearBundle">清除</button>
|
||||
</div>
|
||||
<div class="runtime-note">
|
||||
支持 Spine 3.8 / 4.0 / 4.1 / 4.2 JSON · 4.2.119 预览运行时 · 本地离线
|
||||
支持 Spine 3.8 / 4.0 / 4.1 / 4.2 / 4.3 JSON · 对应版本运行时按需加载 · 本地离线
|
||||
<span v-if="spineObject.dataVersion"> · 当前资源 {{ spineObject.dataVersion }}</span>
|
||||
</div>
|
||||
<div v-if="spineObject.error" class="error">{{ spineObject.error }}</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Container } from 'pixi.js'
|
||||
import { Spine } from '@esotericsoftware/spine-pixi-v7'
|
||||
import { getSpineAsset } from './spineAssetRegistry'
|
||||
import type { SpineSceneObject } from './spineTypes'
|
||||
|
||||
interface RuntimeEntry {
|
||||
root: Container
|
||||
spine: Spine
|
||||
spine: any
|
||||
runtimeVersion: '4.2' | '4.3'
|
||||
assetVersion: number
|
||||
animation: string
|
||||
}
|
||||
@@ -45,12 +45,13 @@ export class SpineRuntimeLayer {
|
||||
entry.root.destroy({ children: true })
|
||||
}
|
||||
const root = new Container()
|
||||
const spine = new Spine({ skeletonData: asset.skeletonData, autoUpdate: false, darkTint: false })
|
||||
const spine = new asset.SpineClass({ skeletonData: asset.skeletonData, autoUpdate: false, darkTint: false })
|
||||
if (asset.skeletonData.defaultSkin) spine.skeleton.setSkin(asset.skeletonData.defaultSkin)
|
||||
spine.skeleton.setSlotsToSetupPose()
|
||||
if (asset.runtimeVersion === '4.3') spine.skeleton.setupPoseSlots()
|
||||
else spine.skeleton.setSlotsToSetupPose()
|
||||
root.addChild(spine)
|
||||
this.container.addChild(root)
|
||||
entry = { root, spine, assetVersion: object.assetVersion, animation: '' }
|
||||
entry = { root, spine, runtimeVersion: asset.runtimeVersion, assetVersion: object.assetVersion, animation: '' }
|
||||
this.entries.set(object.id, entry)
|
||||
}
|
||||
if (!entry) continue
|
||||
@@ -66,12 +67,13 @@ export class SpineRuntimeLayer {
|
||||
|
||||
if (entry.animation !== object.selectedAnimation) {
|
||||
entry.animation = object.selectedAnimation
|
||||
spine.skeleton.setToSetupPose()
|
||||
if (entry.runtimeVersion === '4.3') spine.skeleton.setupPose()
|
||||
else spine.skeleton.setToSetupPose()
|
||||
spine.state.clearTracks()
|
||||
// 编辑器时间轴负责整体循环;单个 Spine 动画自身只播放一次并停在末帧。
|
||||
if (entry.animation) spine.state.setAnimation(0, entry.animation, false)
|
||||
}
|
||||
const track = spine.state.getCurrent(0)
|
||||
const track = entry.runtimeVersion === '4.3' ? spine.state.getTrack(0) : spine.state.getCurrent(0)
|
||||
if (track) {
|
||||
const duration = object.animations.find((animation) => animation.name === object.selectedAnimation)?.duration || 0
|
||||
const localTime = Math.max(0, frame / Math.max(1, fps) - object.timeOffset)
|
||||
@@ -85,6 +87,7 @@ export class SpineRuntimeLayer {
|
||||
const entry = this.entries.get(objectId)
|
||||
const bone = entry?.spine.skeleton.findBone(boneName)
|
||||
if (!entry || !bone || !bone.active) return null
|
||||
const pose = entry.runtimeVersion === '4.3' ? bone.appliedPose : bone
|
||||
|
||||
// Spine 骨骼矩阵位于 Spine 对象局部空间;与外层 Pixi 容器矩阵组合后,
|
||||
// 再把屏幕 Y 向下坐标转换为编辑器使用的数学坐标(Y 向上)。
|
||||
@@ -95,12 +98,12 @@ export class SpineRuntimeLayer {
|
||||
const rb = -sin * root.scale.y
|
||||
const rc = sin * root.scale.x
|
||||
const rd = cos * root.scale.y
|
||||
const a = ra * bone.a + rb * bone.c
|
||||
const b = ra * bone.b + rb * bone.d
|
||||
const c = rc * bone.a + rd * bone.c
|
||||
const d = rc * bone.b + rd * bone.d
|
||||
const screenX = root.position.x + ra * bone.worldX + rb * bone.worldY
|
||||
const screenY = root.position.y + rc * bone.worldX + rd * bone.worldY
|
||||
const a = ra * pose.a + rb * pose.c
|
||||
const b = ra * pose.b + rb * pose.d
|
||||
const c = rc * pose.a + rd * pose.c
|
||||
const d = rc * pose.b + rd * pose.d
|
||||
const screenX = root.position.x + ra * pose.worldX + rb * pose.worldY
|
||||
const screenY = root.position.y + rc * pose.worldX + rd * pose.worldY
|
||||
return {
|
||||
x: screenX,
|
||||
y: -screenY,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
AtlasAttachmentLoader,
|
||||
SkeletonBinary,
|
||||
SkeletonJson,
|
||||
TextureAtlas,
|
||||
} from '@esotericsoftware/spine-core'
|
||||
import { Spine, SpineTexture } from '@esotericsoftware/spine-pixi-v7'
|
||||
import { bindAtlasTextures } from './spineRuntimeTextures'
|
||||
import type { RuntimeParseResult, SpineTextureMap } from './spineRuntimeTypes'
|
||||
|
||||
function atlasLoader(atlasText: string, textures: SpineTextureMap) {
|
||||
const atlas = new TextureAtlas(atlasText)
|
||||
bindAtlasTextures(atlas, textures, SpineTexture)
|
||||
return new AtlasAttachmentLoader(atlas)
|
||||
}
|
||||
|
||||
export function parseJson42(source: any, atlasText: string, textures: SpineTextureMap): RuntimeParseResult {
|
||||
return {
|
||||
skeletonData: new SkeletonJson(atlasLoader(atlasText, textures)).readSkeletonData(source),
|
||||
SpineClass: Spine,
|
||||
}
|
||||
}
|
||||
|
||||
export function parseBinary42(source: ArrayBuffer, atlasText: string, textures: SpineTextureMap): RuntimeParseResult {
|
||||
return {
|
||||
skeletonData: new SkeletonBinary(atlasLoader(atlasText, textures)).readSkeletonData(source),
|
||||
SpineClass: Spine,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { bindAtlasTextures } from './spineRuntimeTextures'
|
||||
import type { RuntimeParseResult, SpineTextureMap } from './spineRuntimeTypes'
|
||||
|
||||
async function modules() {
|
||||
const [core, pixi] = await Promise.all([
|
||||
import('@spine-particle/spine-core-4.3'),
|
||||
import('@spine-particle/spine-pixi-v7-4.3'),
|
||||
])
|
||||
return { core, pixi }
|
||||
}
|
||||
|
||||
export async function parseJson43(source: any, atlasText: string, textures: SpineTextureMap): Promise<RuntimeParseResult> {
|
||||
const { core, pixi } = await modules()
|
||||
const atlas = new core.TextureAtlas(atlasText)
|
||||
bindAtlasTextures(atlas, textures, pixi.SpineTexture)
|
||||
return {
|
||||
skeletonData: new core.SkeletonJson(new core.AtlasAttachmentLoader(atlas)).readSkeletonData(source),
|
||||
SpineClass: pixi.Spine,
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseBinary43(source: ArrayBuffer, atlasText: string, textures: SpineTextureMap): Promise<RuntimeParseResult> {
|
||||
const { core, pixi } = await modules()
|
||||
const atlas = new core.TextureAtlas(atlasText)
|
||||
bindAtlasTextures(atlas, textures, pixi.SpineTexture)
|
||||
return {
|
||||
skeletonData: new core.SkeletonBinary(new core.AtlasAttachmentLoader(atlas)).readSkeletonData(source),
|
||||
SpineClass: pixi.Spine,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ALPHA_MODES } from 'pixi.js'
|
||||
import type { SpineTextureMap } from './spineRuntimeTypes'
|
||||
|
||||
function basename(path: string) {
|
||||
return path.replace(/\\/g, '/').split('/').pop() || path
|
||||
}
|
||||
|
||||
export function bindAtlasTextures(atlas: any, textures: SpineTextureMap, SpineTexture: { from(baseTexture: any): any }) {
|
||||
for (const page of atlas.pages) {
|
||||
const texture = textures.get(basename(page.name).toLowerCase())
|
||||
if (!texture) throw new Error(`Atlas 引用的纹理未选择:${page.name}`)
|
||||
texture.baseTexture.alphaMode = page.pma ? ALPHA_MODES.PMA : ALPHA_MODES.UNPACK
|
||||
page.setTexture(SpineTexture.from(texture.baseTexture))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Texture } from 'pixi.js'
|
||||
|
||||
export type SpineRuntimeVersion = '4.2' | '4.3'
|
||||
export type SpineClass = new (options: any) => any
|
||||
export type SpineTextureMap = Map<string, Texture>
|
||||
|
||||
export interface RuntimeParseResult {
|
||||
skeletonData: any
|
||||
SpineClass: SpineClass
|
||||
}
|
||||
@@ -1,16 +1,12 @@
|
||||
import {
|
||||
AtlasAttachmentLoader,
|
||||
SkeletonBinary,
|
||||
SkeletonJson,
|
||||
TextureAtlas,
|
||||
type SkeletonData,
|
||||
} from '@esotericsoftware/spine-core'
|
||||
import { SpineTexture } from '@esotericsoftware/spine-pixi-v7'
|
||||
import { ALPHA_MODES, Texture } from 'pixi.js'
|
||||
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 { normalizeSpine38JsonFor42 } from './spine38Compatibility'
|
||||
import { normalizeSpineVersion, supportedSpineVersionsText, type SupportedSpineVersion } from './spineVersion'
|
||||
import { normalizeSpineVersion, spineRuntimeVersion, supportedSpineVersionsText, type SupportedSpineVersion } from './spineVersion'
|
||||
|
||||
type CompatibleSkeletonData = LoadedSpineAsset['skeletonData']
|
||||
|
||||
export interface ParsedSpineBundle extends LoadedSpineAsset {
|
||||
animations: SpineAnimationInfo[]
|
||||
@@ -22,12 +18,19 @@ function basename(path: string) {
|
||||
return path.replace(/\\/g, '/').split('/').pop() || path
|
||||
}
|
||||
|
||||
async function readTexture(file: File) {
|
||||
const url = URL.createObjectURL(file)
|
||||
return await Texture.fromURL(url)
|
||||
async function readTextures(files: File[]) {
|
||||
const entries = await Promise.all(files.map(async (file) => {
|
||||
const url = URL.createObjectURL(file)
|
||||
try {
|
||||
return [basename(file.name).toLowerCase(), await Texture.fromURL(url)] as const
|
||||
} finally {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
}))
|
||||
return new Map(entries)
|
||||
}
|
||||
|
||||
function boneMetadata(skeletonData: SkeletonData): SpineBoneInfo[] {
|
||||
function boneMetadata(skeletonData: CompatibleSkeletonData): SpineBoneInfo[] {
|
||||
const depths = new Map<string, number>()
|
||||
return skeletonData.bones.map((bone) => {
|
||||
const parent = bone.parent?.name || null
|
||||
@@ -42,27 +45,31 @@ export async function parseSpineBundle(skeletonFile: File, atlasFile: File, text
|
||||
if (!/\.atlas$/i.test(atlasFile.name)) throw new Error('图集文件仅支持 .atlas')
|
||||
if (!textureFiles.length) throw new Error('请至少选择一张纹理图片')
|
||||
|
||||
const atlas = new TextureAtlas(await atlasFile.text())
|
||||
const fileMap = new Map(textureFiles.map((file) => [basename(file.name).toLowerCase(), file]))
|
||||
const textures: Texture[] = []
|
||||
const atlasText = await atlasFile.text()
|
||||
const texturesByName = await readTextures(textureFiles)
|
||||
const textures = [...texturesByName.values()]
|
||||
try {
|
||||
for (const page of atlas.pages) {
|
||||
const file = fileMap.get(basename(page.name).toLowerCase())
|
||||
if (!file) throw new Error(`Atlas 引用的纹理未选择:${page.name}`)
|
||||
const texture = await readTexture(file)
|
||||
// 与 Spine 官方 PixiJS Atlas 加载器保持一致。Spine 导出的 Atlas
|
||||
// 常使用 pma:true;忽略该标记会让骨架数据正常但附件纹理无法正确合成。
|
||||
texture.baseTexture.alphaMode = page.pma ? ALPHA_MODES.PMA : ALPHA_MODES.UNPACK
|
||||
textures.push(texture)
|
||||
page.setTexture(SpineTexture.from(texture.baseTexture))
|
||||
}
|
||||
|
||||
const attachmentLoader = new AtlasAttachmentLoader(atlas)
|
||||
let skeletonData: SkeletonData
|
||||
let skeletonData: CompatibleSkeletonData
|
||||
let SpineClass: new (options: any) => any
|
||||
let dataVersion: SupportedSpineVersion | null = null
|
||||
let runtimeVersion: '4.2' | '4.3'
|
||||
|
||||
if (/\.skel$/i.test(skeletonFile.name)) {
|
||||
const reader = new SkeletonBinary(attachmentLoader)
|
||||
skeletonData = reader.readSkeletonData(await skeletonFile.arrayBuffer())
|
||||
const source = await skeletonFile.arrayBuffer()
|
||||
// 二进制版本只能从数据内部读取:先按 4.3 尝试,再回退到完全保留的旧版链路。
|
||||
try {
|
||||
;({ skeletonData, SpineClass } = await parseBinary43(source, atlasText, texturesByName))
|
||||
runtimeVersion = '4.3'
|
||||
} catch (error43) {
|
||||
try {
|
||||
;({ skeletonData, SpineClass } = parseBinary42(source, atlasText, texturesByName))
|
||||
runtimeVersion = '4.2'
|
||||
} catch (error42) {
|
||||
const message43 = error43 instanceof Error ? error43.message : String(error43)
|
||||
const message42 = error42 instanceof Error ? error42.message : String(error42)
|
||||
throw new Error(`Spine 二进制解析失败(4.3:${message43};旧版:${message42})`)
|
||||
}
|
||||
}
|
||||
dataVersion = normalizeSpineVersion(skeletonData.version)
|
||||
} else {
|
||||
let source: any
|
||||
@@ -73,15 +80,21 @@ export async function parseSpineBundle(skeletonFile: File, atlasFile: File, text
|
||||
}
|
||||
dataVersion = normalizeSpineVersion(source?.skeleton?.spine)
|
||||
if (!dataVersion) throw new Error(`Spine JSON 版本不受支持,仅支持 ${supportedSpineVersionsText()}`)
|
||||
runtimeVersion = spineRuntimeVersion(dataVersion)
|
||||
if (dataVersion === '3.8') normalizeSpine38JsonFor42(source)
|
||||
const reader = new SkeletonJson(attachmentLoader)
|
||||
skeletonData = reader.readSkeletonData(source)
|
||||
const parsed = runtimeVersion === '4.3'
|
||||
? await parseJson43(source, atlasText, texturesByName)
|
||||
: parseJson42(source, atlasText, texturesByName)
|
||||
skeletonData = parsed.skeletonData
|
||||
SpineClass = parsed.SpineClass
|
||||
}
|
||||
if (!dataVersion) throw new Error(`Spine 数据版本不受支持,仅支持 ${supportedSpineVersionsText()}`)
|
||||
|
||||
return {
|
||||
skeletonData,
|
||||
textures,
|
||||
runtimeVersion,
|
||||
SpineClass,
|
||||
dataVersion,
|
||||
animations: skeletonData.animations.map((animation) => ({ name: animation.name, duration: animation.duration })),
|
||||
bones: boneMetadata(skeletonData),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { SkeletonData } from '@esotericsoftware/spine-core'
|
||||
import type { SkeletonData as SkeletonData42 } from '@esotericsoftware/spine-core'
|
||||
import type { SkeletonData as SkeletonData43 } from '@spine-particle/spine-core-4.3'
|
||||
import type { Texture } from 'pixi.js'
|
||||
|
||||
export interface LoadedSpineAsset {
|
||||
skeletonData: SkeletonData
|
||||
skeletonData: SkeletonData42 | SkeletonData43
|
||||
runtimeVersion: '4.2' | '4.3'
|
||||
SpineClass: new (options: any) => any
|
||||
textures: Texture[]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const SUPPORTED_SPINE_VERSIONS = ['3.8', '4.0', '4.1', '4.2'] as const
|
||||
export const SUPPORTED_SPINE_VERSIONS = ['3.8', '4.0', '4.1', '4.2', '4.3'] as const
|
||||
|
||||
export type SupportedSpineVersion = typeof SUPPORTED_SPINE_VERSIONS[number]
|
||||
|
||||
@@ -14,3 +14,8 @@ export function isSupportedSpineVersion(value: unknown): value is SupportedSpine
|
||||
export function supportedSpineVersionsText() {
|
||||
return SUPPORTED_SPINE_VERSIONS.join(' / ')
|
||||
}
|
||||
|
||||
/** 4.3 的约束和姿势模型与旧版不兼容,预览时必须分流到对应的官方运行时。 */
|
||||
export function spineRuntimeVersion(version: SupportedSpineVersion) {
|
||||
return version === '4.3' ? '4.3' as const : '4.2' as const
|
||||
}
|
||||
|
||||
@@ -832,6 +832,7 @@
|
||||
<option value="4.0">Spine 4.0</option>
|
||||
<option value="4.1">Spine 4.1</option>
|
||||
<option value="4.2">Spine 4.2</option>
|
||||
<option value="4.3">Spine 4.3</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@
|
||||
<li><b>创建对象:</b>在左侧“场景对象”中添加粒子、碰撞、路径或 Spine。</li>
|
||||
<li><b>编辑粒子:</b>选中粒子系统,依次调整发射、形状、属性、外观资源和修改器。</li>
|
||||
<li><b>查看效果:</b>使用底部时间轴播放、暂停或拖动到指定帧;修改参数后会自动重新计算。</li>
|
||||
<li><b>保存成果:</b>配置文件用于继续编辑;“导出 Spine Json”支持 Spine 3.8、4.0、4.1 和 4.2。</li>
|
||||
<li><b>保存成果:</b>配置文件用于继续编辑;“导出 Spine Json”支持 Spine 3.8、4.0、4.1、4.2 和 4.3。</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user