场景对象的复制与粘贴+配置追加功能

This commit is contained in:
tianmo committed 2026-11-09 11:34:09 +08:00
1 parent 3587129b90
commit 6dd32ec8fc
24 files changed
+761 -14

No files matched your search

@@ -20,6 +20,7 @@
: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)"
@contextmenu.prevent.stop="openSceneObjectMenu($event, 'particle', entry.system.id)"
>
<span v-if="entry.depth > 0" class="sub-branch">└</span>
<span v-else class="tdot">●</span>
@@ -36,6 +37,7 @@
class="tree-item scene-object-item"
:class="{ on: activeObjectType === 'collision' && collider.id === store.activeObjectId }"
@click="store.selectSceneObject('collision', collider.id)"
@contextmenu.prevent.stop="openSceneObjectMenu($event, 'collision', collider.id)"
>
<span class="tdot collision-dot">■</span>
<span class="tlabel">{{ collider.name }}</span>
@@ -51,6 +53,7 @@
class="tree-item scene-object-item"
:class="{ on: activeObjectType === 'path' && path.id === store.activeObjectId }"
@click="store.selectSceneObject('path', path.id)"
@contextmenu.prevent.stop="openSceneObjectMenu($event, 'path', path.id)"
>
<span class="tdot path-dot">◇</span>
<span class="tlabel">{{ path.name }}</span>
@@ -66,6 +69,7 @@
class="tree-item scene-object-item"
:class="{ on: activeObjectType === 'spine' && spine.id === store.activeObjectId }"
@click="store.selectSceneObject('spine', spine.id)"
@contextmenu.prevent.stop="openSceneObjectMenu($event, 'spine', spine.id)"
>
<span class="tdot spine-dot">◆</span>
<span class="tlabel">{{ spine.name }}</span>
@@ -199,10 +203,24 @@
<div v-else class="panel-hint">{{ $t('scene.panelHint') }}</div>
</div>
</div>
<Teleport to="body">
<div
v-if="sceneContextMenu.open"
ref="sceneContextMenuElement"
class="scene-context-menu"
role="menu"
:style="{ left: sceneContextMenu.x + 'px', top: sceneContextMenu.y + 'px' }"
@pointerdown.stop
@contextmenu.prevent
>
<button type="button" role="menuitem" @click="copyFromSceneContextMenu">{{ $t('scene.contextCopy') }}</button>
<button type="button" role="menuitem" @click="pasteFromSceneContextMenu">{{ $t('scene.contextPaste') }}</button>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import NumSlider from '../NumSlider.vue'
import CurveEditor from '../CurveEditor.vue'
import ParticleAttributeControl from '../ParticleAttributeControl.vue'
@@ -210,11 +228,80 @@ import ColorGradientEditor from '../ColorGradientEditor.vue'
import ImageResourceCard from '../ImageResourceCard.vue'
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
import { collectSubEmitterIds } from '../../core/subEmitterIds'
import { copySceneObjectToClipboard, pasteSceneObjectFromClipboard } from '../../editor/sceneObjectClipboard'
import type { SceneObjectType } from '../../store/particleStore'
import { useParticlePanelContext } from './particlePanelContext'
const { store, resolvedCurveEditorMode, sys, activeObjectType, activeCollider, activePath, activeSpine, canCopyObject, collapsed, toggle, sectionOpen, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, LINEAR_CURVE } = useParticlePanelContext()
type DisplaySystem = { system: (typeof store.systems)[number]; depth: number }
const sceneContextMenuElement = ref<HTMLElement | null>(null)
const sceneContextMenu = reactive({
open: false,
x: 0,
y: 0,
type: 'particle' as SceneObjectType,
id: 0,
})
function closeSceneContextMenu() {
sceneContextMenu.open = false
}
async function openSceneObjectMenu(event: MouseEvent, type: SceneObjectType, id: number) {
store.selectSceneObject(type, id)
Object.assign(sceneContextMenu, { open: true, x: event.clientX, y: event.clientY, type, id })
await nextTick()
const menu = sceneContextMenuElement.value
if (!menu) return
const bounds = menu.getBoundingClientRect()
sceneContextMenu.x = Math.max(8, Math.min(event.clientX, window.innerWidth - bounds.width - 8))
sceneContextMenu.y = Math.max(8, Math.min(event.clientY, window.innerHeight - bounds.height - 8))
}
async function runSceneContextAction(action: 'copy' | 'paste') {
const { type, id } = sceneContextMenu
closeSceneContextMenu()
try {
if (action === 'copy') await copySceneObjectToClipboard(store, type, id)
else await pasteSceneObjectFromClipboard(store, type, id)
} catch (error) {
window.alert(error instanceof Error ? error.message : String(error))
}
}
function copyFromSceneContextMenu() {
void runSceneContextAction('copy')
}
function pasteFromSceneContextMenu() {
void runSceneContextAction('paste')
}
function onSceneMenuPointerDown(event: PointerEvent) {
if (sceneContextMenu.open && !sceneContextMenuElement.value?.contains(event.target as Node)) {
closeSceneContextMenu()
}
}
function onSceneMenuKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') closeSceneContextMenu()
}
onMounted(() => {
window.addEventListener('pointerdown', onSceneMenuPointerDown, true)
window.addEventListener('keydown', onSceneMenuKeyDown, true)
window.addEventListener('scroll', closeSceneContextMenu, true)
window.addEventListener('blur', closeSceneContextMenu)
})
onBeforeUnmount(() => {
window.removeEventListener('pointerdown', onSceneMenuPointerDown, true)
window.removeEventListener('keydown', onSceneMenuKeyDown, true)
window.removeEventListener('scroll', closeSceneContextMenu, true)
window.removeEventListener('blur', closeSceneContextMenu)
})
const warmupDisabled = computed(() => !!sys.value && (
collectSubEmitterIds(store.systems).has(sys.value.id)
|| (sys.value.config.mode === 'stream' && sys.value.config.streamBehavior === 'loop')
@@ -270,6 +357,18 @@ const displaySystems = computed<DisplaySystem[]>(() => {
</script>
<style scoped>
.scene-context-menu {
position: fixed; z-index: 10000; min-width: 142px; padding: 4px;
border: 1px solid #3d4962; border-radius: 7px; background: #171d2a;
box-shadow: 0 10px 28px rgba(0, 0, 0, .48);
}
.scene-context-menu button {
display: block; width: 100%; height: 30px; padding: 0 10px; border: 0; border-radius: 5px;
background: transparent; color: #d5dced; font: inherit; font-size: 12px; text-align: left; cursor: pointer;
}
.scene-context-menu button:hover, .scene-context-menu button:focus-visible {
outline: none; background: #2d3953; color: #fff;
}
.panel { display: flex; width: 100%; height: 100%; flex-direction: column; overflow: hidden; background: #14141f; border-left: 1px solid #2a2a3c; padding: 12px; box-sizing: border-box; }
.empty, .panel-hint { color: #667; font-size: 12px; padding: 8px 0; }
.group { border: 1px solid #2a2a3c; border-radius: 8px; margin-bottom: 10px; overflow: hidden; }