场景对象的复制与粘贴+配置追加功能
This commit is contained in:
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; }
|
||||
|
||||
Reference in new issue
Block a user