spine资源引入

This commit is contained in:
tianmo committed 2026-08-29 03:10:16 +08:00
1 parent 166b5015c2
commit 5f4667f736
25 files changed
+31151 -9082

No files matched your search

+35 -19
View File
@@ -12,7 +12,7 @@
<button class="scene-add" title="添加粒子系统" @click.stop="addSys">+粒子</button>
<button class="scene-add" title="添加碰撞体" @click.stop="addCollider">+碰撞</button>
<button class="scene-add" title="添加路径" @click.stop="addPath">+路径</button>
<button class="scene-add placeholder-action" title="添加 Spine 骨架(待开发)" @click.stop="showScenePlaceholder('Spine 骨架')">+骨架</button>
<button class="scene-add" title="添加 Spine 骨架" @click.stop="addSpine">+骨架</button>
</div>
</div>
<div class="group-body" v-show="!collapsed.scene">
@@ -53,7 +53,19 @@
<span class="scene-object-kind">路径</span>
<button class="scene-object-delete" title="删除路径" @click.stop="store.removePath(path.id)">✕</button>
</div>
<div v-if="!store.systems.length && !store.colliders.length && !store.paths.length" class="empty">场景中暂无对象</div>
<div
v-for="spine in store.spines"
:key="`spine-${spine.id}`"
class="tree-item scene-object-item"
:class="{ on: activeObjectType === 'spine' && spine.id === store.activeObjectId }"
@click="store.selectSceneObject('spine', spine.id)"
>
<span class="tdot spine-dot">◆</span>
<span class="tlabel">{{ spine.name }}</span>
<span class="scene-object-kind">骨架</span>
<button class="scene-object-delete" title="删除骨架" @click.stop="store.removeSpine(spine.id)">✕</button>
</div>
<div v-if="!store.systems.length && !store.colliders.length && !store.paths.length && !store.spines.length" class="empty">场景中暂无对象</div>
</div>
<template v-if="activeObjectType === 'particle' && sys">
<label class="row"><span>名称</span><input v-model="sys.config.name" class="inp" /></label>
@@ -92,6 +104,10 @@
<label class="compact-field"><span>Y 位置</span><input v-model.number="activeCollider.y" type="number" class="inp" /></label>
</div>
<NumSlider label="旋转" :min="-360" :max="360" :step="1" v-model="activeCollider.rotation" />
<div class="field-grid">
<label class="compact-field"><span>缩放 X</span><input v-model.number="activeCollider.scaleX" type="number" min="0.05" step="0.05" class="inp" /></label>
<label class="compact-field"><span>缩放 Y</span><input v-model.number="activeCollider.scaleY" type="number" min="0.05" step="0.05" class="inp" /></label>
</div>
<NumSlider v-if="activeCollider.shape === 'circle'" label="半径" :min="1" :max="1000" :step="1" v-model="activeCollider.radius" />
<div v-else-if="activeCollider.shape === 'ellipse'" class="field-grid">
<label class="compact-field"><span>X 半径</span><input v-model.number="activeCollider.radiusX" type="number" min="1" class="inp" /></label>
@@ -114,6 +130,15 @@
<template v-else-if="activePath">
<div class="object-editor-title">路径属性</div>
<label class="field-block"><span>轨道名称</span><input v-model="activePath.name" class="inp" /></label>
<div class="field-grid">
<label class="compact-field"><span>X 位置</span><input v-model.number="activePath.x" type="number" class="inp" /></label>
<label class="compact-field"><span>Y 位置</span><input v-model.number="activePath.y" type="number" class="inp" /></label>
</div>
<NumSlider label="旋转" :min="-360" :max="360" :step="1" v-model="activePath.rotation" />
<div class="field-grid">
<label class="compact-field"><span>缩放 X</span><input v-model.number="activePath.scaleX" type="number" min="0.05" step="0.05" class="inp" /></label>
<label class="compact-field"><span>缩放 Y</span><input v-model.number="activePath.scaleY" type="number" min="0.05" step="0.05" class="inp" /></label>
</div>
<label class="setting-line"><span>闭合轨道</span><input v-model="activePath.closed" type="checkbox" /></label>
<label class="setting-line"><span>轨道颜色</span><input v-model="activePath.color" type="color" class="path-color" /></label>
<div class="path-help"><span>操作说明:</span><ul>
@@ -127,8 +152,8 @@
<button class="path-action primary" @click="applyPathPreset">应用预设</button>
<button class="path-action" @click="clearPathAnchors">清空锚点</button>
</template>
<SpineObjectPanel v-else-if="activeSpine" :spine-object="activeSpine" />
<div v-else class="panel-hint">使用右上角按钮创建场景对象</div>
<div v-if="sceneActionHint" class="scene-action-hint">{{ sceneActionHint }}</div>
</div>
</div>
@@ -681,7 +706,7 @@
</template>
<script setup lang="ts">
import { computed, markRaw, onUnmounted, reactive, ref, watchEffect } from 'vue'
import { computed, markRaw, reactive, ref, watchEffect } from 'vue'
import { Texture } from 'pixi.js'
import { useParticleStore } from '../store/particleStore'
import { defaultConfig, ensureEmitterConfig } from '../core/particleEmitter'
@@ -690,6 +715,7 @@ 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'
const store = useParticleStore()
// 默认场景始终有一个可编辑的粒子系统;热更新保留现有系统时不重复创建。
@@ -707,6 +733,9 @@ const activeCollider = computed(() => store.activeObjectType === 'collision'
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)
// 开发热更新会保留旧的 Pinia 对象;渲染属性控件前先补齐新增字段。
watchEffect(() => {
@@ -800,6 +829,7 @@ 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 addColliderTag() {
const collider = activeCollider.value
if (!collider) return
@@ -840,16 +870,6 @@ function applyPathPreset() {
function clearPathAnchors() {
if (activePath.value) activePath.value.points = []
}
const sceneActionHint = ref('')
let sceneActionHintTimer: ReturnType<typeof setTimeout> | null = null
function showScenePlaceholder(type: string) {
sceneActionHint.value = `${type}对象将在后续阶段开发`
if (sceneActionHintTimer) clearTimeout(sceneActionHintTimer)
sceneActionHintTimer = setTimeout(() => { sceneActionHint.value = '' }, 2400)
}
onUnmounted(() => {
if (sceneActionHintTimer) clearTimeout(sceneActionHintTimer)
})
function randomizeSeed() {
if (sys.value) sys.value.config.seed = Math.floor(Math.random() * 999999) + 1
}
@@ -1283,11 +1303,6 @@ function toggleResourceLock(resourceId: number) {
background: #2e3a55; color: #cdf; font-size: 10px; white-space: nowrap; cursor: pointer;
}
.scene-add:hover { border-color: #7774ff; background: #45426f; color: #fff; }
.scene-add.placeholder-action { background: #232938; color: #9da7be; }
.scene-action-hint {
margin-top: 8px; padding: 6px 8px; border: 1px dashed #3a4963; border-radius: 5px;
background: #181e2b; color: #8f9ab3; font-size: 11px;
}
.group-body { padding: 8px 10px 12px; }
.caret { display: inline-block; width: 12px; color: #667; font-size: 11px; }
.divider { height: 1px; background: #2a2a3c; margin: 8px 0; }
@@ -1298,6 +1313,7 @@ function toggleResourceLock(resourceId: number) {
.tree-item .tdot.dim { color: #556; }
.tree-item .tdot.collision-dot { color: #ff7070; }
.tree-item .tdot.path-dot { color: #ff933e; font-size: 14px; }
.tree-item .tdot.spine-dot { color: #c08cff; font-size: 11px; }
.tree-item .tlabel { flex: 1; }
.scene-tree { padding-bottom: 8px; border-bottom: 1px solid #2a2a3c; }
.scene-object-item { min-height: 28px; cursor: pointer; }