子发射器

This commit is contained in:
tianmo committed 2026-10-22 16:49:53 +08:00
1 parent d4ebb18777
commit d224939efd
33 files changed
+5690 -1355

No files matched your search

@@ -14,19 +14,21 @@
<div class="group-body" v-show="!collapsed.scene">
<div class="tree scene-tree">
<div
v-for="(system, i) in store.systems"
:key="`particle-${system.id}`"
v-for="entry in displaySystems"
:key="`particle-${entry.system.id}`"
class="tree-item scene-object-item"
:class="{ on: activeObjectType === 'particle' && system.id === store.activeObjectId }"
@click="select(i)"
:class="{ on: activeObjectType === 'particle' && entry.system.id === store.activeObjectId, 'scene-object-child': entry.depth > 0 }"
:style="entry.depth > 0 ? { paddingLeft: `${entry.depth * 18}px` } : undefined"
@click="store.selectSystem(entry.system.id)"
>
<span class="tdot">●</span>
<span class="tlabel">{{ system.config.name || 'ParticleSystem' + (i + 1) }}</span>
<button class="scene-object-eye" :class="{ off: system.visible === false }" :title="system.visible === false ? '显示粒子系统' : '隐藏粒子系统'" :aria-label="system.visible === false ? '显示粒子系统' : '隐藏粒子系统'" @click.stop="system.visible = system.visible === false">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"/><circle cx="12" cy="12" r="2.8"/><path v-if="system.visible === false" class="eye-slash" d="M4 4l16 16"/></svg>
<span v-if="entry.depth > 0" class="sub-branch">└</span>
<span v-else class="tdot">●</span>
<span class="tlabel">{{ entry.system.config.name || 'ParticleSystem' }}</span>
<button class="scene-object-eye" :class="{ off: entry.system.visible === false }" :title="entry.system.visible === false ? '显示粒子系统' : '隐藏粒子系统'" :aria-label="entry.system.visible === false ? '显示粒子系统' : '隐藏粒子系统'" @click.stop="entry.system.visible = entry.system.visible === false">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"/><circle cx="12" cy="12" r="2.8"/><path v-if="entry.system.visible === false" class="eye-slash" d="M4 4l16 16"/></svg>
</button>
<span class="scene-object-kind">粒子</span>
<button class="scene-object-delete" title="删除粒子系统" @click.stop="store.removeSystem(system.id)">✕</button>
<span class="scene-object-kind" :class="{ 'sub-emitter-kind': entry.depth > 0 }">{{ entry.depth > 0 ? '子发射器' : '粒子' }}</span>
<button class="scene-object-delete" title="删除粒子系统" @click.stop="store.removeSystem(entry.system.id)">✕</button>
</div>
<div
v-for="collider in store.colliders"
@@ -182,6 +184,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import NumSlider from '../NumSlider.vue'
import CurveEditor from '../CurveEditor.vue'
import ParticleAttributeControl from '../ParticleAttributeControl.vue'
@@ -189,7 +192,44 @@ import ColorGradientEditor from '../ColorGradientEditor.vue'
import ImageResourceCard from '../ImageResourceCard.vue'
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
import { useParticlePanelContext } from './particlePanelContext'
const { store, resolvedCurveEditorMode, sys, activeObjectType, activeCollider, activePath, activeSpine, canCopyObject, collapsed, toggle, select, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, LINEAR_CURVE } = useParticlePanelContext()
const { store, resolvedCurveEditorMode, sys, activeObjectType, activeCollider, activePath, activeSpine, canCopyObject, collapsed, toggle, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, LINEAR_CURVE } = useParticlePanelContext()
type DisplaySystem = { system: (typeof store.systems)[number]; depth: number }
/** 子发射器嵌在父系统下方显示,方便直接选中;移除引用后自动回到普通行。 */
const displaySystems = computed<DisplaySystem[]>(() => {
const byId = new Map(store.systems.map((system) => [system.id, system]))
const parentOf = new Map<number, number>()
for (const system of store.systems) {
if (system.config.eventEnabled === false) continue
for (const rule of system.config.events || []) {
if (rule.targetSystemId <= 0 || rule.targetSystemId === system.id) continue
if (!byId.has(rule.targetSystemId) || parentOf.has(rule.targetSystemId)) continue
parentOf.set(rule.targetSystemId, system.id)
}
}
const children = new Map<number, number[]>()
for (const [child, parent] of parentOf) {
const list = children.get(parent)
if (list) list.push(child)
else children.set(parent, [child])
}
const result: DisplaySystem[] = []
const visited = new Set<number>()
const walk = (system: (typeof store.systems)[number], depth: number) => {
if (visited.has(system.id)) return
visited.add(system.id)
result.push({ system, depth })
for (const childId of children.get(system.id) || []) {
const child = byId.get(childId)
if (child) walk(child, depth + 1)
}
}
for (const system of store.systems) if (!parentOf.has(system.id)) walk(system, 0)
// 环形引用兜底:保证所有系统仍然可见、可选中。
for (const system of store.systems) if (!visited.has(system.id)) walk(system, 0)
return result
})
</script>
<style scoped>
@@ -219,11 +259,14 @@ const { store, resolvedCurveEditorMode, sys, activeObjectType, activeCollider, a
.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 .sub-branch { color: #5f7090; font-size: 12px; }
.tree-item .tlabel { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.scene-tree { padding-bottom: 8px; border-bottom: 1px solid #2a2a3c; }
.scene-object-item { min-height: 28px; cursor: pointer; }
.scene-object-item:hover { background: #202b40; }
.scene-object-child { background: rgba(30, 42, 62, .35); }
.scene-object-kind { color: #75829c; font-size: 10px; }
.scene-object-kind.sub-emitter-kind { color: #8f8cff; }
.scene-object-eye {
width: 24px; height: 22px; padding: 3px; flex: 0 0 24px; border: 1px solid transparent; border-radius: 5px;
background: transparent; color: #9aa6bf; cursor: pointer;