资源拆分

This commit is contained in:
tianmo committed 2026-09-11 10:00:38 +08:00
1 parent f810e712ec
commit 83890faaea
38 files changed
+4586 -4144

No files matched your search

@@ -0,0 +1,458 @@
import { computed, markRaw, onBeforeUnmount, onMounted, reactive, ref, watch, watchEffect } from 'vue'
import { Texture } from 'pixi.js'
import { useParticleStore } from '../../store/particleStore'
import type { ParticleSystem } from '../../store/particleStore'
import { defaultConfig, ensureEmitterConfig } from '../../core/particleEmitter'
import NumSlider from '../NumSlider.vue'
import CurveEditor from '../CurveEditor.vue'
import ParticleAttributeControl from '../ParticleAttributeControl.vue'
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'
import { buildSpineImagesArchive, downloadSpineImagesArchive } from '../../export/spineImagesExporter'
import {
SpineConnectorError,
checkSpineConnector,
convertWithSpineConnector,
downloadSpineProject,
type SpineConnectorHealth,
} from '../../export/spineConnectorClient'
import { automaticLoopTransitionFrames } from '../../core/loopSegmentTiming'
import { LINEAR_CURVE, FLAT_CURVE, ALPHA_CURVE, RESOURCE_ALPHA_CURVE, TRAIL_ALPHA_CURVE, TRAIL_SHAPE_CURVE } from './particlePanelCurves'
import { useParticleExport } from './useParticleExport'
import { useProjectPresets } from './useProjectPresets'
import { useParticleResources } from './useParticleResources'
/** 每个参数面板创建一次状态,子分组共享选择、折叠状态与操作。 */
export function useParticlePanel() {
const store = useParticleStore()
const { exportSettings, exportBusy, exportTarget, exportMessage, exportError, connectorHealth, connectorPairUrl, connectorExportErrorCode, connectorStatusText, connectorStatusClass, connectorReady, connectorAuthorizationUrl, connectorDisabledReason, connectorButtonTitle, connectorHealthTimer, connectorHealthRequestId, refreshConnectorHealth, nextPaint, prepareExportFrames, exportSpineJson, exportSpineProject, exportSpineImages } = useParticleExport(store)
const { configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, saveConfig, onConfigFileChange, refreshPresets, loadSelectedPreset } = useProjectPresets(store)
function resolvedCurveEditorMode(mode: '跟随导出' | '线性' | '贝塞尔') {
if (mode === '跟随导出') return exportSettings.value.keyframeCurve === 'bezier' ? 'bezier' : 'linear'
return mode === '贝塞尔' ? 'bezier' : 'linear'
}
// 默认场景始终有一个可编辑的粒子系统;热更新保留现有系统时不重复创建。
if (!store.systems.length) store.addSystem()
const activeIdx = computed(() => {
const i = store.systems.findIndex((s) => s.id === store.activeId)
return i >= 0 ? i : 0
})
const sys = computed(() => store.systems[activeIdx.value] || null)
const { textureFileInput, sequenceFileInput, pendingResourceId, trailTextureFileInput, pendingTrailResourceId, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, rebalanceTrailWeights, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, readFileAsDataUrl, resetTexture, normalizeHex, setResourceFixedColor, commitResourceFixedColor, removeImageResource, roundWeight, distributeImageWeights, rebalanceImageWeights, setResourceWeight, toggleResourceLock } = useParticleResources(sys)
const automaticLoopDuration = computed(() => sys.value
? automaticLoopTransitionFrames(sys.value.config, store.timeline.fps)
: 30)
const loopDurationFrames = computed(() => {
const config = sys.value?.config
if (!config) return 1
return Math.max(1, Math.ceil(Number(config.loopDurationFrames) || 1))
})
const activeObjectType = computed(() => store.activeObjectType)
const activeCollider = computed(() => store.activeObjectType === 'collision'
? store.colliders.find((item) => item.id === store.activeObjectId) || null
: null)
const activePath = computed(() => store.activeObjectType === 'path'
? store.paths.find((item) => item.id === store.activeObjectId) || null
: null)
const activeSpine = computed(() => store.activeObjectType === 'spine'
? store.spines.find((item) => item.id === store.activeObjectId) || null
: null)
const canCopyObject = computed(() => {
if (store.activeObjectType === 'particle') return store.systems.some((item) => item.id === store.activeObjectId)
if (store.activeObjectType === 'collision') return store.colliders.some((item) => item.id === store.activeObjectId)
if (store.activeObjectType === 'path') return store.paths.some((item) => item.id === store.activeObjectId)
return store.spines.some((item) => item.id === store.activeObjectId)
})
const selectedSpineBone = computed(() => {
const spine = store.spines.find((item) => item.selectedBone && item.bones.some((bone) => bone.name === item.selectedBone))
return spine ? { spine, boneName: spine.selectedBone } : null
})
const boundSpineBone = computed(() => {
const config = sys.value?.config
if (!config || config.emitterFollowMode !== 'bone') return null
const spine = store.spines.find((item) => item.id === config.emitterFollowSpineId)
if (!spine || !spine.bones.some((bone) => bone.name === config.emitterFollowBoneName)) return null
return { spine, boneName: config.emitterFollowBoneName }
})
const collisionPickerOpen = ref(false)
const selectedCollisionBodies = computed(() => {
const ids = new Set(sys.value?.config.collisionColliderIds || [])
return store.colliders.filter((collider) => ids.has(collider.id))
})
const availableCollisionBodies = computed(() => {
const ids = new Set(sys.value?.config.collisionColliderIds || [])
return store.colliders.filter((collider) => !ids.has(collider.id))
})
watch(() => [sys.value?.id, sys.value?.config.collision], () => {
collisionPickerOpen.value = false
})
function addCollisionBody(colliderId: number) {
const config = sys.value?.config
if (!config || config.collisionColliderIds.includes(colliderId)) return
config.collisionColliderIds.push(colliderId)
collisionPickerOpen.value = false
}
function removeCollisionBody(colliderId: number) {
const config = sys.value?.config
if (!config) return
config.collisionColliderIds = config.collisionColliderIds.filter((id) => id !== colliderId)
}
// 兼容开发热更新和旧场景:缺少显隐字段的对象按“显示”处理。
watchEffect(() => {
for (const system of store.systems) if (system.visible == null) system.visible = true
for (const collider of store.colliders) if (collider.visible == null) collider.visible = true
for (const path of store.paths) if (path.visible == null) path.visible = true
for (const spine of store.spines) if (spine.visible == null) spine.visible = true
})
// 开发热更新会保留旧的 Pinia 对象;渲染属性控件前先补齐新增字段。
watchEffect(() => {
if (!sys.value) return
const config = ensureEmitterConfig(sys.value.config as any)
// 初始属性不再提供曲线模式;旧配置自动回退为随机。
if (config.lifeMode === 'curve') config.lifeMode = 'random'
if (config.speedMode === 'curve') config.speedMode = 'random'
if (config.scaleMode === 'curve') config.scaleMode = 'random'
if (config.initialRotationMode === 'curve') config.initialRotationMode = 'random'
if (config.mode === 'stream' && (!(config.duration > 0))) config.duration = 1
if (config.streamBehavior === 'loop') {
if (config.emitterFollow || config.emitterFollowMode !== 'none') resetParticleFollow(config)
}
})
// 每个粒子系统永久保存自己的 Spine ID 与骨骼名;层级树当前选择不会覆盖既有绑定。
// 仅当已绑定的 Spine 或骨骼确实不存在时,才回到“无”模式。
watchEffect(() => {
const colliderIds = new Set(store.colliders.map((collider) => collider.id))
for (const system of store.systems) {
const config = ensureEmitterConfig(system.config as any)
const validCollisionIds = config.collisionColliderIds.filter((id) => colliderIds.has(id))
if (validCollisionIds.length !== config.collisionColliderIds.length) config.collisionColliderIds = validCollisionIds
if (config.spawnPathId > 0 && !store.paths.some((path) => path.id === config.spawnPathId)) {
config.spawnPathId = 0
if (config.emissionAngleMode === 'path-normal' || config.emissionAngleMode === 'path-tangent') config.emissionAngleMode = 'fixed'
}
if (config.emitterFollowMode !== 'bone') continue
const spine = store.spines.find((item) => item.id === config.emitterFollowSpineId)
const targetExists = !!spine?.bones.some((bone) => bone.name === config.emitterFollowBoneName)
if (!targetExists) {
config.emitterFollowMode = 'none'
config.emitterFollowSpineId = 0
config.emitterFollowBoneName = ''
}
}
})
// HMR 或旧场景中的碰撞体缺少路径跟随字段时即时补齐。
watchEffect(() => {
for (const collider of store.colliders) {
if (collider.followPathId == null) collider.followPathId = 0
if (!(collider.followDuration > 0)) collider.followDuration = 2
if (collider.followPathRotation == null) collider.followPathRotation = false
if (!['跟随导出', '线性', '贝塞尔'].includes(collider.followCurveMode)) collider.followCurveMode = '跟随导出'
if (!Array.isArray(collider.followCurve) || collider.followCurve.length < 2) collider.followCurve = [{ x: 0, y: 0 }, { x: 1, y: 1 }]
}
})
const collapsed = reactive<Record<string, boolean>>({
config: true,
scene: false,
emitMode: true,
shape: true,
attr: true,
look: true,
mods: true,
export: false,
})
function toggle(key: string) { collapsed[key] = !collapsed[key] }
function setEmitMode(mode: 'stream' | 'burst') {
const config = sys.value?.config
if (!config) return
config.mode = mode
if (mode === 'stream' && (!(config.duration > 0))) config.duration = 1
}
function setLoopDurationFrames(value: number) {
const config = sys.value?.config
if (!config) return
const frames = Math.min(1800, Math.max(1, Math.ceil(Number(value) || 1)))
config.loopDurationFrames = frames
}
function resetParticleFollow(config: any) {
const defaults = defaultConfig()
config.emitterFollow = false
config.emitterFollowMode = 'none'
config.emitterFollowPathId = defaults.emitterFollowPathId
config.emitterFollowSpineId = defaults.emitterFollowSpineId
config.emitterFollowBoneName = defaults.emitterFollowBoneName
config.emitterFollowBoneOffsetX = defaults.emitterFollowBoneOffsetX
config.emitterFollowBoneOffsetY = defaults.emitterFollowBoneOffsetY
config.emitterFollowBoneRotation = defaults.emitterFollowBoneRotation
config.emitterFollowBoneScaleX = defaults.emitterFollowBoneScaleX
config.emitterFollowBoneScaleY = defaults.emitterFollowBoneScaleY
config.emitterAngleMode = defaults.emitterAngleMode
config.emitterFollowCurve = defaults.emitterFollowCurve.map((point) => ({ ...point }))
config.emitterFollowDuration = defaults.emitterFollowDuration
config.emitterFollowSpace = defaults.emitterFollowSpace
config.emitterFollowDirection = defaults.emitterFollowDirection
config.emitterFollowOffset = defaults.emitterFollowOffset
}
function setStreamBehavior(behavior: 'normal' | 'loop') {
const config = sys.value?.config
if (!config || config.streamBehavior === behavior) return
const entering = behavior === 'loop'
const message = entering
? '进入循环持续模式后,粒子跟随设置会失效并恢复默认值。是否继续?'
: '退出循环持续模式后,粒子跟随设置会失效并恢复默认值。是否继续?'
if (!window.confirm(message)) return
resetParticleFollow(config)
config.streamBehavior = behavior
if (!entering) {
config.generateLoopStartAnimation = false
config.generateLoopEndAnimation = false
}
}
function setLoopSection(section: 'start' | 'end', event: Event) {
const config = sys.value?.config
if (!config) return
const checked = (event.target as HTMLInputElement).checked
if (section === 'start') {
config.generateLoopStartAnimation = checked
} else {
config.generateLoopEndAnimation = checked
}
}
function setLoopSectionDuration(section: 'start' | 'end', event: Event) {
const config = sys.value?.config
if (!config) return
const frames = Math.min(1800, Math.max(1, Math.ceil(Number((event.target as HTMLInputElement).value) || 30)))
if (section === 'start') config.loopStartDurationFrames = frames
else config.loopEndDurationFrames = frames
}
function setLoopCustomDuration(section: 'start' | 'end', event: Event) {
const config = sys.value?.config
if (!config) return
const checked = (event.target as HTMLInputElement).checked
if (section === 'start') config.loopStartUseCustomDuration = checked
else config.loopEndUseCustomDuration = checked
}
function onEmitterFollowModeChange(event: Event, config: {
emitterFollowMode: 'none' | 'path' | 'bone'
emitterFollowSpineId: number
emitterFollowBoneName: string
}) {
const mode = (event.target as HTMLSelectElement).value as 'none' | 'path' | 'bone'
config.emitterFollowMode = mode
if (mode !== 'bone') {
config.emitterFollowSpineId = 0
config.emitterFollowBoneName = ''
return
}
bindSelectedBone(config)
}
function bindSelectedBone(config: {
emitterFollowMode: 'none' | 'path' | 'bone'
emitterFollowSpineId: number
emitterFollowBoneName: string
}) {
const target = selectedSpineBone.value
if (!target) {
config.emitterFollowMode = 'none'
config.emitterFollowSpineId = 0
config.emitterFollowBoneName = ''
return
}
config.emitterFollowMode = 'bone'
config.emitterFollowSpineId = target.spine.id
config.emitterFollowBoneName = target.boneName
}
type ResetModule = 'emitMode' | 'shape' | 'attr' | 'look' | 'mods' | 'export'
const MODULE_RESET_KEYS: Record<Exclude<ResetModule, 'export'>, string[]> = {
emitMode: [
'mode', 'rate', 'streamBehavior', 'loopDurationFrames',
'generateLoopStartAnimation', 'loopStartUseCustomDuration', 'loopStartDurationFrames',
'generateLoopEndAnimation', 'loopEndUseCustomDuration', 'loopEndDurationFrames',
'burstCount', 'burstLoop', 'delay', 'duration',
],
shape: [
'shape', 'shapeRangeVersion',
'innerRadius', 'outerRadius', 'innerRectW', 'innerRectH', 'outerRectW', 'outerRectH', 'spawnPathId', 'spawnPathDistribution',
'radius', 'rectW', 'rectH', 'emissionAngleMode', 'direction', 'spread', 'coneDirection', 'coneAngle',
],
attr: [
'lifeMin', 'lifeMax', 'lifeMode', 'lifeCurve',
'speedMin', 'speedMax', 'speedMode', 'speedCurve',
'scaleMin', 'scaleMax', 'scaleMode', 'scaleAxisMode', 'scaleYMin', 'scaleYMax',
'scaleCurve', 'scaleYCurve', 'scaleEndRatio',
'initialRotationMode', 'initialRotationMin', 'initialRotationMax', 'initialRotationCurve',
'rotationSpeedMin', 'rotationSpeedMax',
'speedOverLifeEnabled', 'speedOverLifeMode', 'speedOverLifeMin', 'speedOverLifeMax', 'speedOverLifeCurve',
'scaleOverLifeEnabled', 'scaleOverLifeMode', 'scaleOverLifeAxisMode',
'scaleOverLifeMin', 'scaleOverLifeMax', 'scaleYOverLifeMin', 'scaleYOverLifeMax',
'scaleOverLifeCurve', 'scaleYOverLifeCurve',
'rotationOverLifeEnabled', 'rotationOverLifeMode', 'rotationOverLifeCurve',
],
look: [
'texture', 'imageResources', 'imageWeightVersion',
'alpha', 'alphaEnd', 'colorStart', 'colorEnd', 'colorOverLife', 'blend',
'alphaMode', 'curveMode', 'alphaCurve', 'textureName', 'textureFolder',
'alphaScale', 'anchorX', 'anchorY', 'independentAlpha',
],
mods: [
'noise', 'noiseAmt', 'noiseRange', 'noiseSpeed',
'useGravity', 'gravity', 'gravityMode', 'gravityMin', 'gravityMax', 'gravityDirection', 'gravityCurve',
'wind', 'windX', 'windY', 'windMode', 'windMin', 'windMax', 'windDirection', 'windCurve',
'forceField', 'forceStrength', 'forceRadius', 'forceCenterX', 'forceCenterY', 'forceFalloff',
'attraction', 'attractionCenterX', 'attractionCenterY', 'attractionRadius',
'attractionDuration', 'attractionDelay', 'attractionDelayMode', 'attractionPath',
'trail', 'trailConfigVersion', 'trailResources', 'trailResourceVersion',
'trailTexture', 'trailTextureName', 'trailPreviewUrl', 'trailColorMode', 'trailColor',
'trailColorGradient', 'trailAlphaMode', 'trailAlpha', 'trailAlphaCurve',
'trailBoneCount', 'trailLength', 'trailWidth', 'trailWidthMode',
'trailLifeLengthEnabled', 'trailLifeLengthCurve', 'trailShapeEnabled', 'trailShapeCurve',
'trailGridRows', 'trailGridCols', 'collision', 'collisionColliderIds', 'pathFollow',
'emitterFollow', 'emitterFollowMode', 'emitterFollowPathId',
'emitterFollowSpineId', 'emitterFollowBoneName',
'emitterFollowBoneOffsetX', 'emitterFollowBoneOffsetY', 'emitterFollowBoneRotation',
'emitterFollowBoneScaleX', 'emitterFollowBoneScaleY', 'emitterAngleMode',
'emitterFollowCurve', 'emitterFollowDuration', 'emitterFollowSpace',
'emitterFollowDirection', 'emitterFollowOffset', 'damping',
],
}
function resetModule(module: ResetModule) {
const config = sys.value?.config as any
if (module === 'export') {
store.settings.exportSettings = defaultSpineExportSettings()
return
}
if (!config) return
const defaults = defaultConfig() as any
for (const key of MODULE_RESET_KEYS[module]) config[key] = defaults[key]
// 重置资源模块后,Stage 会在下一帧重新挂载默认 star/trail 纹理。
}
function normalizeExportFps() {
exportSettings.value.fps = Math.min(240, Math.max(1, Math.round(Number(exportSettings.value.fps) || 30)))
}
function select(i: number) { store.selectSystem(store.systems[i]?.id ?? 0) }
function addSys() { store.addSystem() }
function addCollider() { store.addCollider() }
function addPath() { store.addPath() }
function addSpine() { store.addSpine() }
function copySelectedObject() { store.duplicateSelectedObject() }
function pathAnchor(x: number, y: number, inX = 0, inY = 0, outX = 0, outY = 0) {
return { x, y, inX, inY, outX, outY }
}
function applyPathPreset() {
const path = activePath.value
if (!path || !path.preset) return
if (path.preset === 'circle') {
const radius = 110, handle = radius * 0.5522848
path.points = [
pathAnchor(radius, 0, 0, -handle, 0, handle),
pathAnchor(0, radius, handle, 0, -handle, 0),
pathAnchor(-radius, 0, 0, handle, 0, -handle),
pathAnchor(0, -radius, -handle, 0, handle, 0),
]
path.closed = true
} else if (path.preset === 'square') {
path.points = [pathAnchor(-100, -100), pathAnchor(100, -100), pathAnchor(100, 100), pathAnchor(-100, 100)]
path.closed = true
} else {
const sides = path.preset === 'star' ? 10 : 6
path.points = Array.from({ length: sides }, (_, index) => {
const radius = path.preset === 'star' && index % 2 ? 50 : 110
const angle = -Math.PI * 0.5 + index / sides * Math.PI * 2
return pathAnchor(Math.cos(angle) * radius, Math.sin(angle) * radius)
})
path.closed = true
}
}
function clearPathAnchors() {
if (activePath.value) activePath.value.points = []
}
function randomizeSeed() {
if (sys.value) sys.value.config.seed = Math.floor(Math.random() * 999999) + 1
}
function setSpeedOverLifeMode(mode: 'fixed' | 'random' | 'curve' | 'direction') {
const config = sys.value?.config
if (!config || mode === 'direction') return
config.speedOverLifeMode = mode
// 旧默认是 1~1,曲线映射后永远为 1;切换曲线时改为有效倍率范围。
if (mode === 'curve' && config.speedOverLifeMin === config.speedOverLifeMax) {
config.speedOverLifeMin = 0
config.speedOverLifeMax = 1
}
}
function prepareScaleOverLifeCurve() {
const config = sys.value?.config
if (!config || config.scaleOverLifeMode !== 'curve') return
if (config.scaleOverLifeMin === config.scaleOverLifeMax) {
config.scaleOverLifeMin = 0
config.scaleOverLifeMax = 1
}
if (config.scaleYOverLifeMin === config.scaleYOverLifeMax) {
config.scaleYOverLifeMin = 0
config.scaleYOverLifeMax = 1
}
}
function setScaleOverLifeMode(mode: 'fixed' | 'random' | 'curve' | 'direction') {
const config = sys.value?.config
if (!config || mode === 'direction') return
config.scaleOverLifeMode = mode
prepareScaleOverLifeCurve()
}
// 各发射器形状的默认参数:切换形状时应用(只影响形状相关字段,不牵连其他)
const SHAPE_DEFAULTS: Record<string, Partial<any>> = {
point: { emissionAngleMode: 'random', direction: 0, spread: 360 },
circle: { innerRadius: 0, outerRadius: 100, emissionAngleMode: 'fixed', direction: -90, spread: 0 },
rect: { innerRectW: 0, innerRectH: 0, outerRectW: 200, outerRectH: 100, emissionAngleMode: 'fixed', direction: -90, spread: 0 },
cone: { innerRadius: 0, outerRadius: 100, coneAngle: 90, coneDirection: 90, emissionAngleMode: 'random', direction: 90, spread: 90 },
path: { spawnPathId: 0, spawnPathDistribution: 'fixed', emissionAngleMode: 'fixed', direction: 0, spread: 0 },
}
function onShapeChange() {
const cfg = sys.value?.config
if (!cfg) return
const d = SHAPE_DEFAULTS[cfg.shape]
if (!d) return
Object.assign(cfg, d)
}
function onSpawnPathChange() {
const cfg = sys.value?.config
if (!cfg || cfg.spawnPathId > 0) return
if (cfg.emissionAngleMode === 'path-normal' || cfg.emissionAngleMode === 'path-tangent') cfg.emissionAngleMode = 'fixed'
}
// 重置/删除图片 → 回到默认 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 }
}