悬浮导航
This commit is contained in:
1 parent
cffbd49eff
commit
a417c9119d
31 files changed
+1577
-1326
No files matched your search
+59
-5
@@ -6,14 +6,38 @@
|
||||
:aria-hidden="transferProgress.visible ? 'true' : undefined"
|
||||
>
|
||||
<!-- 参数面板:可在系统设置中切换左右位置,宽度可拖。 -->
|
||||
<div class="parameter-panel" :style="{ width: panelW + 'px' }">
|
||||
<div v-show="showParameterPanel" class="parameter-panel" :style="{ width: panelW + 'px' }">
|
||||
<ParticlePanel />
|
||||
</div>
|
||||
<div class="resizer-col" @pointerdown="onPanelDrag"></div>
|
||||
<div v-show="showParameterPanel" class="resizer-col" @pointerdown="onPanelDrag"></div>
|
||||
|
||||
<!-- 右列:上=画布区,下=时间轴区(高度可拖) -->
|
||||
<div class="right">
|
||||
<div class="stage-area"><Stage /></div>
|
||||
<div class="stage-area">
|
||||
<Stage />
|
||||
<nav
|
||||
v-if="store.settings.floatingMenuEnabled"
|
||||
class="floating-section-nav"
|
||||
role="tablist"
|
||||
:aria-label="$t('settings.floatingMenu')"
|
||||
>
|
||||
<button
|
||||
v-for="(section, index) in PARTICLE_SECTIONS"
|
||||
:key="section.key"
|
||||
type="button"
|
||||
role="tab"
|
||||
class="floating-section-tab"
|
||||
:class="{ selected: activeSection === section.key && canUseSection(section.key) }"
|
||||
:disabled="!canUseSection(section.key)"
|
||||
:aria-selected="activeSection === section.key && canUseSection(section.key)"
|
||||
:title="canUseSection(section.key) ? $t(section.titleKey) : $t('settings.floatingMenuParticleOnly')"
|
||||
@click="activateSection(section.key)"
|
||||
>
|
||||
<span class="floating-section-index">{{ index + 1 }}</span>
|
||||
<span class="floating-section-label">{{ $t(section.titleKey) }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="resizer-row" @pointerdown="onTimelineDrag" :title="$t('app.resizeTimeline')"></div>
|
||||
<div class="timeline-area" :style="{ height: timelineH + 'px' }">
|
||||
<Timeline />
|
||||
@@ -24,7 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useParticleStore } from './store/particleStore'
|
||||
import { installSystemSettingsCache } from './editor/systemSettings'
|
||||
import Stage from './views/Stage.vue'
|
||||
@@ -32,9 +56,13 @@ import ParticlePanel from './views/ParticlePanel.vue'
|
||||
import Timeline from './views/Timeline.vue'
|
||||
import TransferProgressOverlay from './views/TransferProgressOverlay.vue'
|
||||
import { transferProgress } from './editor/transferProgress'
|
||||
import { provideParticlePanel } from './views/particle-panel/particlePanelContext'
|
||||
import { PARTICLE_SECTIONS } from './views/particle-panel/particleSections'
|
||||
|
||||
const store = useParticleStore()
|
||||
installSystemSettingsCache(store)
|
||||
const { activeSection, activateSection, canUseSection } = provideParticlePanel()
|
||||
const showParameterPanel = computed(() => !store.settings.floatingMenuEnabled || activeSection.value !== null)
|
||||
const panelW = ref(360)
|
||||
const timelineH = ref(180)
|
||||
|
||||
@@ -77,7 +105,33 @@ function onTimelineDrag(e: PointerEvent) {
|
||||
.panel-right .right { order: 1; }
|
||||
.panel-right .resizer-col { order: 2; }
|
||||
.panel-right .parameter-panel { order: 3; }
|
||||
.stage-area { flex: 1; min-height: 0; overflow: clip; }
|
||||
.stage-area { position: relative; flex: 1; min-height: 0; overflow: clip; }
|
||||
.floating-section-nav {
|
||||
position: absolute; right: 12px; top: 72px; z-index: 6; display: flex;
|
||||
width: clamp(184px, 15vw, 236px); max-height: calc(100% - 92px);
|
||||
flex-direction: column; gap: 5px; overflow-y: auto; box-sizing: border-box; padding: 8px;
|
||||
border: 1px solid rgba(79, 91, 121, .72); border-radius: 10px;
|
||||
background: rgba(15, 18, 29, .9); box-shadow: 0 12px 30px rgba(0, 0, 0, .36);
|
||||
scrollbar-width: thin; scrollbar-color: #3a455e transparent;
|
||||
}
|
||||
.floating-section-tab {
|
||||
display: flex; width: 100%; min-height: 40px; flex: 0 0 auto; align-items: center; gap: 9px;
|
||||
padding: 0 10px; border: 1px solid #2d3548; border-radius: 8px;
|
||||
background: #191e2c; color: #aeb9cf; font: inherit; font-size: 12px; text-align: left; cursor: pointer;
|
||||
}
|
||||
.floating-section-tab:hover:not(:disabled) { border-color: rgba(112, 121, 167, .45); background: #242b40; color: #f0f3fb; }
|
||||
.floating-section-tab.selected {
|
||||
border-color: #6f69db; background: #37365e; color: #f5f4ff;
|
||||
box-shadow: inset 3px 0 #8580ff;
|
||||
}
|
||||
.floating-section-tab:focus-visible { outline: 2px solid #8b86ff; outline-offset: 1px; }
|
||||
.floating-section-tab:disabled { color: #596476; cursor: not-allowed; opacity: .58; }
|
||||
.floating-section-index {
|
||||
display: grid; width: 20px; height: 20px; flex: 0 0 20px; place-items: center;
|
||||
border-radius: 5px; background: #252d40; color: #8390ad; font-size: 10px; font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.floating-section-tab.selected .floating-section-index { background: #7771e8; color: #fff; }
|
||||
.floating-section-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.resizer-row { height: 4px; cursor: row-resize; background: #1c1c2a; border-top: 1px solid #2a2a3c; border-bottom: 1px solid #2a2a3c; flex-shrink: 0; }
|
||||
.resizer-row:hover { background: #3a5a8c; }
|
||||
.timeline-area { position: relative; z-index: 10; flex-shrink: 0; overflow: visible; }
|
||||
|
||||
@@ -10,6 +10,7 @@ const SYSTEM_SETTINGS_CACHE_KEY = 'SpineParticle.SystemSettings.v1'
|
||||
|
||||
export type SystemSettingsState = Pick<SettingsState,
|
||||
| 'parameterPanelOnRight'
|
||||
| 'floatingMenuEnabled'
|
||||
| 'undoLimit'
|
||||
| 'showEmitter'
|
||||
| 'showBones'
|
||||
@@ -40,6 +41,7 @@ function clamp(value: unknown, min: number, max: number, fallback: number) {
|
||||
export function captureSystemSettings(settings: SettingsState): SystemSettingsState {
|
||||
return {
|
||||
parameterPanelOnRight: settings.parameterPanelOnRight,
|
||||
floatingMenuEnabled: settings.floatingMenuEnabled,
|
||||
undoLimit: settings.undoLimit,
|
||||
showEmitter: settings.showEmitter,
|
||||
showBones: settings.showBones,
|
||||
@@ -65,6 +67,7 @@ function normalizedSystemSettings(value: Partial<SystemSettingsState>, current:
|
||||
: current.axisColor
|
||||
return {
|
||||
parameterPanelOnRight: booleanValue('parameterPanelOnRight'),
|
||||
floatingMenuEnabled: booleanValue('floatingMenuEnabled'),
|
||||
undoLimit: Math.round(clamp(value.undoLimit, 1, 200, current.undoLimit)),
|
||||
showEmitter: booleanValue('showEmitter'),
|
||||
showBones: booleanValue('showBones'),
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"common": {
|
||||
"close": "Schließen",
|
||||
"collapse": "Einklappen",
|
||||
"reset": "Zurücksetzen",
|
||||
"add": "Hinzufügen",
|
||||
"remove": "Entfernen",
|
||||
@@ -60,6 +61,8 @@
|
||||
"settings": {
|
||||
"title": "Systemeinstellungen",
|
||||
"layout": "Schnittstellenlayout",
|
||||
"floatingMenu": "Schwebendes Menü",
|
||||
"floatingMenuParticleOnly": "Wähle zuerst ein Partikelsystem",
|
||||
"parameterPanelPosition": "Position des Parameterfelds",
|
||||
"panelOnRight": "Das Parameterfeld wird rechts angezeigt",
|
||||
"history": "Operationsgeschichte",
|
||||
@@ -387,8 +390,8 @@
|
||||
"warningFirstSlot": "Dies ist der erste Slot und kann nicht darunter angezeigt werden, die Partikel werden vorübergehend oben angezeigt."
|
||||
},
|
||||
"export": {
|
||||
"title": "Exporteinstellungen",
|
||||
"reset": "Exporteinstellungen zurücksetzen",
|
||||
"title": "Exportressourcen",
|
||||
"reset": "Exportressourcen zurücksetzen",
|
||||
"imagesPath": "Bildpfad (Spine-Bildpfad)",
|
||||
"spineVersion": "Spine-Version",
|
||||
"keyframeCurve": "Keyframe-Interpolationskurve",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
},
|
||||
"common": {
|
||||
"close": "Close",
|
||||
"collapse": "Collapse",
|
||||
"reset": "Reset",
|
||||
"add": "Add",
|
||||
"remove": "Remove",
|
||||
@@ -72,6 +73,8 @@
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"layout": "Layout",
|
||||
"floatingMenu": "Floating menu",
|
||||
"floatingMenuParticleOnly": "Select a particle system first",
|
||||
"parameterPanelPosition": "Parameter panel position",
|
||||
"panelOnRight": "Show parameter panel on the right",
|
||||
"history": "History",
|
||||
@@ -399,8 +402,8 @@
|
||||
"warningFirstSlot": "This is the first slot, so particles cannot render below it and are temporarily shown on top."
|
||||
},
|
||||
"export": {
|
||||
"title": "Export Settings",
|
||||
"reset": "Reset export settings",
|
||||
"title": "Export Resources",
|
||||
"reset": "Reset export resources",
|
||||
"imagesPath": "Image path (Spine Images Path)",
|
||||
"spineVersion": "Spine version",
|
||||
"keyframeCurve": "Keyframe interpolation",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"common": {
|
||||
"close": "Cerrar",
|
||||
"collapse": "Contraer",
|
||||
"reset": "Restablecer",
|
||||
"add": "Añadir",
|
||||
"remove": "Quitar",
|
||||
@@ -60,6 +61,8 @@
|
||||
"settings": {
|
||||
"title": "Configuración del sistema",
|
||||
"layout": "Diseño de interfaz",
|
||||
"floatingMenu": "Menú flotante",
|
||||
"floatingMenuParticleOnly": "Selecciona primero un sistema de partículas",
|
||||
"parameterPanelPosition": "Ubicación del panel de parámetros",
|
||||
"panelOnRight": "El panel de parámetros se muestra a la derecha.",
|
||||
"history": "Historial de operaciones",
|
||||
@@ -387,8 +390,8 @@
|
||||
"warningFirstSlot": "Esta es la primera ranura y no se puede mostrar debajo de ella; las partículas se muestran temporalmente en la parte superior."
|
||||
},
|
||||
"export": {
|
||||
"title": "Ajustes de exportación",
|
||||
"reset": "Restablecer la configuración de exportación",
|
||||
"title": "Recursos de exportación",
|
||||
"reset": "Restablecer recursos de exportación",
|
||||
"imagesPath": "Ruta de imagen (Ruta de imágenes Spine)",
|
||||
"spineVersion": "Versión de Spine",
|
||||
"keyframeCurve": "Curva de interpolación de fotogramas clave",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"common": {
|
||||
"close": "Fermer",
|
||||
"collapse": "Réduire",
|
||||
"reset": "Réinitialiser",
|
||||
"add": "Ajouter",
|
||||
"remove": "Retirer",
|
||||
@@ -60,6 +61,8 @@
|
||||
"settings": {
|
||||
"title": "Paramètres système",
|
||||
"layout": "Disposition de l'interface",
|
||||
"floatingMenu": "Menu flottant",
|
||||
"floatingMenuParticleOnly": "Sélectionnez d'abord un système de particules",
|
||||
"parameterPanelPosition": "Emplacement du panneau de paramètres",
|
||||
"panelOnRight": "Le panneau de paramètres est affiché à droite",
|
||||
"history": "Historique des opérations",
|
||||
@@ -387,8 +390,8 @@
|
||||
"warningFirstSlot": "Il s'agit du premier emplacement et ne peut pas être affiché en dessous, les particules sont temporairement affichées en haut."
|
||||
},
|
||||
"export": {
|
||||
"title": "Paramètres d’export",
|
||||
"reset": "Réinitialiser les paramètres d'exportation",
|
||||
"title": "Ressources d’export",
|
||||
"reset": "Réinitialiser les ressources d'exportation",
|
||||
"imagesPath": "Chemin de l'image (Chemin des images Spine)",
|
||||
"spineVersion": "Version de Spine",
|
||||
"keyframeCurve": "Courbe d'interpolation d'images clés",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"common": {
|
||||
"close": "閉じる",
|
||||
"collapse": "折りたたむ",
|
||||
"reset": "リセット",
|
||||
"add": "追加",
|
||||
"remove": "削除",
|
||||
@@ -60,6 +61,8 @@
|
||||
"settings": {
|
||||
"title": "システム設定",
|
||||
"layout": "インターフェースのレイアウト",
|
||||
"floatingMenu": "フローティングメニュー",
|
||||
"floatingMenuParticleOnly": "先にパーティクルシステムを選択してください",
|
||||
"parameterPanelPosition": "パラメータパネルの位置",
|
||||
"panelOnRight": "パラメータパネルは右側に表示されます",
|
||||
"history": "動作履歴",
|
||||
@@ -387,8 +390,8 @@
|
||||
"warningFirstSlot": "これは最初のスロットであり、その下に表示することはできません。パーティクルは一時的に上に表示されます。"
|
||||
},
|
||||
"export": {
|
||||
"title": "エクスポート設定",
|
||||
"reset": "エクスポート設定をリセットする",
|
||||
"title": "エクスポートリソース",
|
||||
"reset": "エクスポートリソースをリセット",
|
||||
"imagesPath": "画像パス (Spine 画像パス)",
|
||||
"spineVersion": "Spine バージョン",
|
||||
"keyframeCurve": "キーフレーム補間カーブ",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"common": {
|
||||
"close": "닫기",
|
||||
"collapse": "접기",
|
||||
"reset": "초기화",
|
||||
"add": "추가",
|
||||
"remove": "제거",
|
||||
@@ -60,6 +61,8 @@
|
||||
"settings": {
|
||||
"title": "시스템 설정",
|
||||
"layout": "인터페이스 레이아웃",
|
||||
"floatingMenu": "플로팅 메뉴",
|
||||
"floatingMenuParticleOnly": "먼저 파티클 시스템을 선택하세요",
|
||||
"parameterPanelPosition": "매개변수 패널 위치",
|
||||
"panelOnRight": "매개변수 패널이 오른쪽에 표시됩니다.",
|
||||
"history": "운영 이력",
|
||||
@@ -387,8 +390,8 @@
|
||||
"warningFirstSlot": "이것은 첫 번째 슬롯으로 그 아래에 표시될 수 없으며 파티클은 일시적으로 위에 표시됩니다."
|
||||
},
|
||||
"export": {
|
||||
"title": "내보내기 설정",
|
||||
"reset": "내보내기 설정 재설정",
|
||||
"title": "내보내기 리소스",
|
||||
"reset": "내보내기 리소스 재설정",
|
||||
"imagesPath": "이미지 경로(Spine 이미지 경로)",
|
||||
"spineVersion": "Spine 버전",
|
||||
"keyframeCurve": "키프레임 보간 곡선",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"common": {
|
||||
"close": "Fechar",
|
||||
"collapse": "Recolher",
|
||||
"reset": "Redefinir",
|
||||
"add": "Adicionar",
|
||||
"remove": "Remover",
|
||||
@@ -60,6 +61,8 @@
|
||||
"settings": {
|
||||
"title": "Configurações do sistema",
|
||||
"layout": "Layout da interface",
|
||||
"floatingMenu": "Menu flutuante",
|
||||
"floatingMenuParticleOnly": "Selecione primeiro um sistema de partículas",
|
||||
"parameterPanelPosition": "Localização do painel de parâmetros",
|
||||
"panelOnRight": "O painel de parâmetros é mostrado à direita",
|
||||
"history": "Histórico de operação",
|
||||
@@ -387,8 +390,8 @@
|
||||
"warningFirstSlot": "Este é o primeiro slot e não pode ser exibido abaixo dele; as partículas são exibidas temporariamente no topo."
|
||||
},
|
||||
"export": {
|
||||
"title": "Configurações de exportação",
|
||||
"reset": "Redefinir configurações de exportação",
|
||||
"title": "Recursos de exportação",
|
||||
"reset": "Redefinir recursos de exportação",
|
||||
"imagesPath": "Caminho da imagem (caminho das imagens Spine)",
|
||||
"spineVersion": "Versão do Spine",
|
||||
"keyframeCurve": "Curva de interpolação de quadro-chave",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
},
|
||||
"common": {
|
||||
"close": "关闭",
|
||||
"collapse": "收起",
|
||||
"reset": "重置",
|
||||
"add": "添加",
|
||||
"remove": "移除",
|
||||
@@ -72,6 +73,8 @@
|
||||
"settings": {
|
||||
"title": "系统设置",
|
||||
"layout": "界面布局",
|
||||
"floatingMenu": "悬浮菜单",
|
||||
"floatingMenuParticleOnly": "请先选择粒子系统",
|
||||
"parameterPanelPosition": "参数面板位置",
|
||||
"panelOnRight": "参数面板显示在右侧",
|
||||
"history": "操作历史",
|
||||
@@ -399,8 +402,8 @@
|
||||
"warningFirstSlot": "这是第一个插槽,无法显示在其下方,粒子临时显示在最上层。"
|
||||
},
|
||||
"export": {
|
||||
"title": "导出设置",
|
||||
"reset": "重置导出设置",
|
||||
"title": "导出资源",
|
||||
"reset": "重置导出资源参数",
|
||||
"imagesPath": "图片路径 (Spine Images Path)",
|
||||
"spineVersion": "Spine 版本",
|
||||
"keyframeCurve": "关键帧插值曲线",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"common": {
|
||||
"close": "關閉",
|
||||
"collapse": "收合",
|
||||
"reset": "重設",
|
||||
"add": "新增",
|
||||
"remove": "移除",
|
||||
@@ -60,6 +61,8 @@
|
||||
"settings": {
|
||||
"title": "系統設定",
|
||||
"layout": "介面佈局",
|
||||
"floatingMenu": "懸浮選單",
|
||||
"floatingMenuParticleOnly": "請先選擇粒子系統",
|
||||
"parameterPanelPosition": "參數面板位置",
|
||||
"panelOnRight": "參數面板顯示在右側",
|
||||
"history": "操作歷史",
|
||||
@@ -387,8 +390,8 @@
|
||||
"warningFirstSlot": "這是第一個插槽,無法顯示在其下方,粒子暫時顯示在最上層。"
|
||||
},
|
||||
"export": {
|
||||
"title": "匯出設定",
|
||||
"reset": "重置導出設定",
|
||||
"title": "匯出資源",
|
||||
"reset": "重置匯出資源參數",
|
||||
"imagesPath": "圖片路徑 (Spine Images Path)",
|
||||
"spineVersion": "Spine 版本",
|
||||
"keyframeCurve": "關鍵幀插值曲線",
|
||||
|
||||
@@ -57,6 +57,8 @@ export interface EditorState {
|
||||
export interface SettingsState {
|
||||
/** 参数面板是否显示在页面右侧;关闭时显示在左侧。 */
|
||||
parameterPanelOnRight: boolean
|
||||
/** 是否用画布左侧悬浮菜单切换参数分区。 */
|
||||
floatingMenuEnabled: boolean
|
||||
/** 是否启用刻度(主开关;启用=显示坐标轴+刻度线+数值,可调整字号/粗细) */
|
||||
tickEnabled: boolean
|
||||
/** 坐标轴刻度文字大小(px),默认 12 */
|
||||
@@ -143,6 +145,7 @@ export const useParticleStore = defineStore('particle', {
|
||||
timeline: { playing: true, loop: true, abLoop: false, frame: 0, totalFrames: 60, playbackStart: 0, playbackEnd: 59, fps: 30, recorded: false, animation: 'animation', animations: ['animation'] } as TimelineState,
|
||||
settings: {
|
||||
parameterPanelOnRight: true,
|
||||
floatingMenuEnabled: false,
|
||||
tickEnabled: false,
|
||||
axisFontSize: 12,
|
||||
tickWidth: 1,
|
||||
|
||||
+72
-15
@@ -2,28 +2,61 @@
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span>{{ $t('app.title') }}</span>
|
||||
<button
|
||||
v-if="store.settings.floatingMenuEnabled && activeSection"
|
||||
type="button"
|
||||
class="panel-collapse"
|
||||
:title="$t('common.collapse')"
|
||||
@click="collapsePanel"
|
||||
>
|
||||
{{ $t('common.collapse') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-scroll">
|
||||
<ProjectConfigSection />
|
||||
<SceneObjectsSection />
|
||||
<div ref="panelScroll" class="panel-scroll">
|
||||
<template v-if="store.settings.floatingMenuEnabled">
|
||||
<div v-show="activeSection === 'config'" class="section-detail" :data-active-section="renderedSection === 'config' ? true : undefined">
|
||||
<ProjectConfigSection />
|
||||
</div>
|
||||
<div v-show="activeSection === 'scene'" class="section-detail" :data-active-section="renderedSection === 'scene' ? true : undefined">
|
||||
<SceneObjectsSection />
|
||||
</div>
|
||||
<div v-show="renderedSection === 'emitMode' || renderedSection === 'shape'" class="section-detail" :data-active-section="renderedSection === 'emitMode' || renderedSection === 'shape' ? true : undefined">
|
||||
<EmissionSection />
|
||||
</div>
|
||||
<div v-show="renderedSection === 'attr'" class="section-detail" :data-active-section="renderedSection === 'attr' ? true : undefined">
|
||||
<ParticleAttributesSection />
|
||||
</div>
|
||||
<div v-show="renderedSection === 'look'" class="section-detail" :data-active-section="renderedSection === 'look' ? true : undefined">
|
||||
<ParticleAppearanceSection />
|
||||
</div>
|
||||
<div v-show="renderedSection === 'mods'" class="section-detail" :data-active-section="renderedSection === 'mods' ? true : undefined">
|
||||
<ParticleModifiersSection />
|
||||
</div>
|
||||
<div v-show="renderedSection === 'export'" class="section-detail" :data-active-section="renderedSection === 'export' ? true : undefined">
|
||||
<ExportSettingsSection />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="activeObjectType === 'particle' && sys">
|
||||
<template v-else>
|
||||
<ProjectConfigSection />
|
||||
<SceneObjectsSection />
|
||||
|
||||
<EmissionSection />
|
||||
<ParticleAttributesSection />
|
||||
<ParticleAppearanceSection />
|
||||
<ParticleModifiersSection />
|
||||
<ExportSettingsSection />
|
||||
</template>
|
||||
<template v-if="activeObjectType === 'particle' && sys">
|
||||
<EmissionSection />
|
||||
<ParticleAttributesSection />
|
||||
<ParticleAppearanceSection />
|
||||
<ParticleModifiersSection />
|
||||
<ExportSettingsSection />
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<ParticleExportFooter />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import ProjectConfigSection from './particle-panel/ProjectConfigSection.vue'
|
||||
import SceneObjectsSection from './particle-panel/SceneObjectsSection.vue'
|
||||
import EmissionSection from './particle-panel/EmissionSection.vue'
|
||||
@@ -31,9 +64,25 @@ import ParticleAttributesSection from './particle-panel/ParticleAttributesSectio
|
||||
import ParticleAppearanceSection from './particle-panel/ParticleAppearanceSection.vue'
|
||||
import ParticleModifiersSection from './particle-panel/ParticleModifiersSection.vue'
|
||||
import ExportSettingsSection from './particle-panel/ExportSettingsSection.vue'
|
||||
import ParticleExportFooter from './particle-panel/ParticleExportFooter.vue'
|
||||
import { provideParticlePanel } from './particle-panel/particlePanelContext'
|
||||
const { activeObjectType, sys } = provideParticlePanel()
|
||||
import { useParticlePanelContext } from './particle-panel/particlePanelContext'
|
||||
import type { ParticleSectionKey } from './particle-panel/particleSections'
|
||||
|
||||
const { store, activeObjectType, activeSection, sys, canUseSection } = useParticlePanelContext()
|
||||
const panelScroll = ref<HTMLElement | null>(null)
|
||||
function collapsePanel() {
|
||||
activeSection.value = null
|
||||
}
|
||||
const renderedSection = computed<ParticleSectionKey | null>(() => {
|
||||
const selected = activeSection.value
|
||||
if (!selected || !canUseSection(selected)) return null
|
||||
if (selected !== 'config' && selected !== 'scene' && (activeObjectType.value !== 'particle' || !sys.value)) return null
|
||||
return selected
|
||||
})
|
||||
|
||||
watch(renderedSection, async () => {
|
||||
await nextTick()
|
||||
panelScroll.value?.querySelector<HTMLElement>('[data-active-section="true"]')?.scrollIntoView({ block: 'start' })
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -44,4 +93,12 @@ const { activeObjectType, sys } = provideParticlePanel()
|
||||
.panel-scroll::-webkit-scrollbar-thumb { background: #2e2e44; border-radius: 3px; }
|
||||
.panel-scroll::-webkit-scrollbar-thumb:hover { background: #3a3a55; }
|
||||
.panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; flex: 0 0 auto; font-weight: 700; color: #ccd; }
|
||||
.panel-collapse {
|
||||
height: 27px; padding: 0 11px; border: 1px solid #3d465b; border-radius: 6px;
|
||||
background: #202533; color: #cbd3e2; font: inherit; font-size: 11px; font-weight: 600; cursor: pointer;
|
||||
}
|
||||
.panel-collapse:hover { border-color: #7774ff; background: #303650; color: #fff; }
|
||||
.panel-collapse:focus-visible { outline: 2px solid #8b86ff; outline-offset: 1px; }
|
||||
.section-detail :deep(.group-head) { cursor: default; }
|
||||
.section-detail :deep(.group-head .caret) { visibility: hidden; }
|
||||
</style>
|
||||
@@ -79,6 +79,7 @@ if (store.settings.showSkinMesh == null) store.settings.showSkinMesh = false
|
||||
if (store.settings.showGrid == null) store.settings.showGrid = true
|
||||
|
||||
let app: Application | null = null
|
||||
let stageResizeObserver: ResizeObserver | null = null
|
||||
let world: Container | null = null
|
||||
let originGfx: Graphics | null = null
|
||||
let boneFollowTargetGfx: Graphics | null = null
|
||||
@@ -522,6 +523,11 @@ onMounted(async () => {
|
||||
const el = holder.value!
|
||||
app = new Application({ width: el.clientWidth || 800, height: el.clientHeight || 600, backgroundAlpha: 0, antialias: true })
|
||||
el.appendChild(app.view as unknown as HTMLCanvasElement)
|
||||
stageResizeObserver = new ResizeObserver(() => {
|
||||
if (!app) return
|
||||
app.renderer.resize(Math.max(1, el.clientWidth), Math.max(1, el.clientHeight))
|
||||
})
|
||||
stageResizeObserver.observe(el)
|
||||
createLayers()
|
||||
watch(() => store.settings.backgroundImageData, (value) => background.syncTexture(value), { immediate: true })
|
||||
const textures = await loadDefaultTextures()
|
||||
@@ -565,6 +571,8 @@ onMounted(async () => {
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('beforeunload', onBeforeUnload)
|
||||
cancelAnimationFrame(raf)
|
||||
stageResizeObserver?.disconnect()
|
||||
stageResizeObserver = null
|
||||
spineRuntime?.destroy()
|
||||
spineRuntime = null
|
||||
background.destroy()
|
||||
|
||||
@@ -31,6 +31,14 @@ function normalizeBoneDebugSizes() {
|
||||
<b>{{ store.settings.parameterPanelOnRight ? t('common.right') : t('common.left') }}</b>
|
||||
</span>
|
||||
</label>
|
||||
<label class="sp-switch-row">
|
||||
<span>{{ t('settings.floatingMenu') }}</span>
|
||||
<span class="sp-switch-control">
|
||||
<input type="checkbox" v-model="store.settings.floatingMenuEnabled" :aria-label="t('settings.floatingMenu')" />
|
||||
<span class="sp-switch-ui" aria-hidden="true"></span>
|
||||
<b>{{ store.settings.floatingMenuEnabled ? t('common.enabled') : t('common.disabled') }}</b>
|
||||
</span>
|
||||
</label>
|
||||
<div class="sp-divider"></div>
|
||||
<div class="sp-section-title">{{ t('settings.history') }}</div>
|
||||
<label class="sp-row"><span>{{ t('settings.maxUndo') }}</span>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<!-- 2. 发射模式 -->
|
||||
<div class="group">
|
||||
<div class="group" v-show="sectionVisible('emitMode')">
|
||||
<div class="group-head" @click="toggle('emitMode')">
|
||||
<span class="caret">{{ collapsed.emitMode ? '▸' : '▾' }}</span><span>{{ $t('emission.title') }}</span>
|
||||
<button class="group-reset" :title="$t('emission.reset')" @click.stop="resetModule('emitMode')">{{ $t('common.reset') }}</button>
|
||||
</div>
|
||||
<div class="group-body" v-show="!collapsed.emitMode">
|
||||
<div class="group-body" v-show="sectionOpen('emitMode')">
|
||||
<div class="segs">
|
||||
<button class="seg" :class="{ on: sys.config.mode === 'stream' }" @click="setEmitMode('stream')">{{ $t('emission.stream') }}</button>
|
||||
<button class="seg" :class="{ on: sys.config.mode === 'burst' }" @click="setEmitMode('burst')">{{ $t('emission.burst') }}</button>
|
||||
@@ -62,12 +62,12 @@
|
||||
</div>
|
||||
|
||||
<!-- 3. 发射器形状 -->
|
||||
<div class="group">
|
||||
<div class="group" v-show="sectionVisible('shape')">
|
||||
<div class="group-head" @click="toggle('shape')">
|
||||
<span class="caret">{{ collapsed.shape ? '▸' : '▾' }}</span><span>{{ $t('emission.shapeTitle') }}</span>
|
||||
<button class="group-reset" :title="$t('emission.shapeReset')" @click.stop="resetModule('shape')">{{ $t('common.reset') }}</button>
|
||||
</div>
|
||||
<div class="group-body" v-show="!collapsed.shape">
|
||||
<div class="group-body" v-show="sectionOpen('shape')">
|
||||
<label class="row"><span>{{ $t('emission.shape') }}</span>
|
||||
<select v-model="sys.config.shape" class="inp" @change="onShapeChange">
|
||||
<option value="point">{{ $t('emission.point') }}</option>
|
||||
@@ -136,7 +136,7 @@ import ColorGradientEditor from '../ColorGradientEditor.vue'
|
||||
import ImageResourceCard from '../ImageResourceCard.vue'
|
||||
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
|
||||
import { useParticlePanelContext } from './particlePanelContext'
|
||||
const { store, sys, automaticLoopDuration, loopDurationFrames, collapsed, toggle, setEmitMode, setLoopDurationFrames, setStreamBehavior, setLoopSection, setLoopSectionDuration, setLoopCustomDuration, resetModule, select, onShapeChange, onSpawnPathChange } = useParticlePanelContext()
|
||||
const { store, sys, automaticLoopDuration, loopDurationFrames, collapsed, toggle, sectionVisible, sectionOpen, setEmitMode, setLoopDurationFrames, setStreamBehavior, setLoopSection, setLoopSectionDuration, setLoopCustomDuration, resetModule, select, onShapeChange, onSpawnPathChange } = useParticlePanelContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<!-- 7. 导出设置:当前阶段只维护参数,后续接入 JSON 生成。 -->
|
||||
<!-- 7. 导出资源:导出参数与资源输出操作。 -->
|
||||
<div class="group">
|
||||
<div class="group-head" @click="toggle('export')">
|
||||
<span class="caret">{{ collapsed.export ? '▸' : '▾' }}</span><span>{{ $t('export.title') }}</span>
|
||||
<button class="group-reset" :title="$t('export.reset')" @click.stop="resetModule('export')">{{ $t('common.reset') }}</button>
|
||||
</div>
|
||||
<div class="group-body export-settings" v-show="!collapsed.export">
|
||||
<div class="group-body export-settings" v-show="sectionOpen('export')">
|
||||
<label class="export-field">
|
||||
<span>{{ $t('export.imagesPath') }}</span>
|
||||
<input v-model.trim="exportSettings.imagesPath" class="inp export-input" type="text" placeholder="./images/" />
|
||||
@@ -55,6 +55,9 @@
|
||||
<span class="switch-ui"></span><span>{{ $t('export.excludeEmpty') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="export-resource-actions">
|
||||
<ParticleExportFooter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -66,8 +69,9 @@ import ParticleAttributeControl from '../ParticleAttributeControl.vue'
|
||||
import ColorGradientEditor from '../ColorGradientEditor.vue'
|
||||
import ImageResourceCard from '../ImageResourceCard.vue'
|
||||
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
|
||||
import ParticleExportFooter from './ParticleExportFooter.vue'
|
||||
import { useParticlePanelContext } from './particlePanelContext'
|
||||
const { exportSettings, collapsed, toggle, resetModule, normalizeExportFps, select } = useParticlePanelContext()
|
||||
const { exportSettings, collapsed, toggle, sectionOpen, resetModule, normalizeExportFps, select } = useParticlePanelContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -99,6 +103,7 @@ const { exportSettings, collapsed, toggle, resetModule, normalizeExportFps, sele
|
||||
.export-switches { display: grid; gap: 13px; margin: 4px 0 7px; padding-top: 2px; }
|
||||
.export-switch { font-weight: 600; }
|
||||
.export-switch-last { margin-top: 13px; }
|
||||
.export-resource-actions { margin-top: 14px; padding-top: 12px; border-top: 1px solid #2d3950; }
|
||||
.inp.tiny { width: 46px; flex: 0 0 46px; text-align: center; }
|
||||
.switch-row { display: flex; align-items: center; gap: 9px; color: #b8c1d5; font-size: 12px; cursor: pointer; }
|
||||
.switch-row input { position: absolute; opacity: 0; pointer-events: none; }
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<span class="caret">{{ collapsed.look ? '▸' : '▾' }}</span><span>{{ $t('appearance.title') }}</span>
|
||||
<button class="group-reset" :title="$t('appearance.reset')" @click.stop="resetModule('look')">{{ $t('common.reset') }}</button>
|
||||
</div>
|
||||
<div class="group-body" v-show="!collapsed.look">
|
||||
<div class="group-body" v-show="sectionOpen('look')">
|
||||
<!-- 透明度 -->
|
||||
<div class="look-title">{{ $t('appearance.opacity') }}</div>
|
||||
<div class="seg">
|
||||
@@ -158,7 +158,7 @@ import ColorGradientEditor from '../ColorGradientEditor.vue'
|
||||
import ImageResourceCard from '../ImageResourceCard.vue'
|
||||
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
|
||||
import { useParticlePanelContext } from './particlePanelContext'
|
||||
const { resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, collapsed, toggle, resetModule, select, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, ALPHA_CURVE, RESOURCE_ALPHA_CURVE } = useParticlePanelContext()
|
||||
const { resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, collapsed, toggle, sectionOpen, resetModule, select, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, ALPHA_CURVE, RESOURCE_ALPHA_CURVE } = useParticlePanelContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<span class="caret">{{ collapsed.attr ? '▸' : '▾' }}</span><span>{{ $t('attributes.title') }}</span>
|
||||
<button class="group-reset" :title="$t('attributes.reset')" @click.stop="resetModule('attr')">{{ $t('common.reset') }}</button>
|
||||
</div>
|
||||
<div class="group-body attr-body" v-show="!collapsed.attr">
|
||||
<div class="group-body attr-body" v-show="sectionOpen('attr')">
|
||||
<div class="attr-section-title">{{ $t('attributes.initialParams') }}</div>
|
||||
|
||||
<ParticleAttributeControl
|
||||
@@ -158,7 +158,7 @@ import ColorGradientEditor from '../ColorGradientEditor.vue'
|
||||
import ImageResourceCard from '../ImageResourceCard.vue'
|
||||
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
|
||||
import { useParticlePanelContext } from './particlePanelContext'
|
||||
const { sys, collapsed, toggle, resetModule, setSpeedOverLifeMode, prepareScaleOverLifeCurve, setScaleOverLifeMode, LINEAR_CURVE, FLAT_CURVE } = useParticlePanelContext()
|
||||
const { sys, collapsed, toggle, sectionOpen, resetModule, setSpeedOverLifeMode, prepareScaleOverLifeCurve, setScaleOverLifeMode, LINEAR_CURVE, FLAT_CURVE } = useParticlePanelContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<span class="caret">{{ collapsed.mods ? '▸' : '▾' }}</span><span>{{ $t('modifiers.title') }}</span>
|
||||
<button class="group-reset" :title="$t('modifiers.reset')" @click.stop="resetModule('mods')">{{ $t('common.reset') }}</button>
|
||||
</div>
|
||||
<div class="group-body" v-show="!collapsed.mods">
|
||||
<div class="group-body" v-show="sectionOpen('mods')">
|
||||
<div class="modifier-item damping-item">
|
||||
<NumSlider :label="$t('modifiers.damping')" :min="0" :max="2" :step="0.05" v-model="sys.config.damping" />
|
||||
</div>
|
||||
@@ -434,7 +434,7 @@ import ImageResourceCard from '../ImageResourceCard.vue'
|
||||
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
|
||||
import { useParticlePanelContext } from './particlePanelContext'
|
||||
import { collectEmitterAncestors } from '../../core/subEmitterIds'
|
||||
const { store, sys, trailTextureFileInput, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, selectedSpineBone, boundSpineBone, collisionPickerOpen, selectedCollisionBodies, availableCollisionBodies, addCollisionBody, removeCollisionBody, collapsed, toggle, onEmitterFollowModeChange, bindSelectedBone, resetModule, select, LINEAR_CURVE, FLAT_CURVE, TRAIL_ALPHA_CURVE, TRAIL_SHAPE_CURVE } = useParticlePanelContext()
|
||||
const { store, sys, trailTextureFileInput, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, selectedSpineBone, boundSpineBone, collisionPickerOpen, selectedCollisionBodies, availableCollisionBodies, addCollisionBody, removeCollisionBody, collapsed, toggle, sectionOpen, onEmitterFollowModeChange, bindSelectedBone, resetModule, select, LINEAR_CURVE, FLAT_CURVE, TRAIL_ALPHA_CURVE, TRAIL_SHAPE_CURVE } = useParticlePanelContext()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 不能指向自身或任何上游父系统,避免循环依赖。
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<span class="caret">{{ collapsed.config ? '▸' : '▾' }}</span><span>{{ $t('config.title') }}</span>
|
||||
<button type="button" class="group-download" @click.stop="downloadCurrentProject">{{ $t('config.local.downloadCurrent') }}</button>
|
||||
</div>
|
||||
<div class="group-body config-management-body" v-show="!collapsed.config">
|
||||
<div class="group-body config-management-body" v-show="sectionOpen('config')">
|
||||
<div class="config-toolbar">
|
||||
<button :title="$t('config.saveTitle')" @click="saveConfig">{{ $t('config.save') }}</button>
|
||||
<button :title="$t('config.loadTitle')" @click="configFileInput?.click()">{{ $t('config.load') }}</button>
|
||||
@@ -96,7 +96,7 @@ const {
|
||||
saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject,
|
||||
downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, projectFilesInput, loadAllProjectFiles,
|
||||
selectedLocalProjectName, selectLocalProject, onConfigFileChange, refreshPresets,
|
||||
loadSelectedPreset, collapsed, toggle, select,
|
||||
loadSelectedPreset, collapsed, toggle, sectionOpen, select,
|
||||
} = useParticlePanelContext()
|
||||
const { locale } = useI18n()
|
||||
function formatProjectSize(bytes: number) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<button class="scene-add" :disabled="!canCopyObject" :title="$t('scene.copyTitle')" @click.stop="copySelectedObject">+Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group-body" v-show="!collapsed.scene">
|
||||
<div class="group-body" v-show="sectionOpen('scene')">
|
||||
<div class="tree scene-tree">
|
||||
<div
|
||||
v-for="entry in displaySystems"
|
||||
@@ -192,7 +192,7 @@ 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, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, LINEAR_CURVE } = useParticlePanelContext()
|
||||
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 }
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export type ParticleSectionKey =
|
||||
| 'config'
|
||||
| 'scene'
|
||||
| 'emitMode'
|
||||
| 'shape'
|
||||
| 'attr'
|
||||
| 'look'
|
||||
| 'mods'
|
||||
| 'export'
|
||||
|
||||
export const PARTICLE_SECTIONS: ReadonlyArray<{
|
||||
key: ParticleSectionKey
|
||||
titleKey: string
|
||||
requiresParticle: boolean
|
||||
}> = [
|
||||
{ key: 'config', titleKey: 'config.title', requiresParticle: false },
|
||||
{ key: 'scene', titleKey: 'scene.title', requiresParticle: false },
|
||||
{ key: 'emitMode', titleKey: 'emission.title', requiresParticle: true },
|
||||
{ key: 'shape', titleKey: 'emission.shapeTitle', requiresParticle: true },
|
||||
{ key: 'attr', titleKey: 'attributes.title', requiresParticle: true },
|
||||
{ key: 'look', titleKey: 'appearance.title', requiresParticle: true },
|
||||
{ key: 'mods', titleKey: 'modifiers.title', requiresParticle: true },
|
||||
{ key: 'export', titleKey: 'export.title', requiresParticle: true },
|
||||
]
|
||||
@@ -31,6 +31,7 @@ import { LINEAR_CURVE, FLAT_CURVE, ALPHA_CURVE, RESOURCE_ALPHA_CURVE, TRAIL_ALPH
|
||||
import { useParticleExport } from './useParticleExport'
|
||||
import { useProjectPresets } from './useProjectPresets'
|
||||
import { useParticleResources } from './useParticleResources'
|
||||
import { PARTICLE_SECTIONS, type ParticleSectionKey } from './particleSections'
|
||||
|
||||
/** 每个参数面板创建一次状态,子分组共享选择、折叠状态与操作。 */
|
||||
export function useParticlePanel() {
|
||||
@@ -66,6 +67,7 @@ const loopDurationFrames = computed(() => {
|
||||
return Math.max(1, Math.ceil(Number(config.loopDurationFrames) || 1))
|
||||
})
|
||||
const activeObjectType = computed(() => store.activeObjectType)
|
||||
const activeSection = ref<ParticleSectionKey | null>(null)
|
||||
const activeCollider = computed(() => store.activeObjectType === 'collision'
|
||||
? store.colliders.find((item) => item.id === store.activeObjectId) || null
|
||||
: null)
|
||||
@@ -306,7 +308,7 @@ watchEffect(() => {
|
||||
|
||||
const collapsed = reactive<Record<string, boolean>>({
|
||||
config: true,
|
||||
scene: false,
|
||||
scene: !store.settings.floatingMenuEnabled,
|
||||
emitMode: true,
|
||||
shape: true,
|
||||
attr: true,
|
||||
@@ -314,7 +316,46 @@ const collapsed = reactive<Record<string, boolean>>({
|
||||
mods: true,
|
||||
export: false,
|
||||
})
|
||||
function toggle(key: string) { collapsed[key] = !collapsed[key] }
|
||||
|
||||
function canUseSection(key: ParticleSectionKey) {
|
||||
const section = PARTICLE_SECTIONS.find((item) => item.key === key)
|
||||
return !!section && (!section.requiresParticle || (activeObjectType.value === 'particle' && !!sys.value))
|
||||
}
|
||||
|
||||
function activateSection(key: ParticleSectionKey) {
|
||||
if (canUseSection(key)) activeSection.value = key
|
||||
}
|
||||
|
||||
function sectionVisible(key: ParticleSectionKey) {
|
||||
return !store.settings.floatingMenuEnabled || activeSection.value === key
|
||||
}
|
||||
|
||||
function sectionOpen(key: ParticleSectionKey) {
|
||||
return store.settings.floatingMenuEnabled ? activeSection.value === key : !collapsed[key]
|
||||
}
|
||||
|
||||
function toggle(key: string) {
|
||||
if (store.settings.floatingMenuEnabled) return
|
||||
collapsed[key] = !collapsed[key]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => store.settings.floatingMenuEnabled,
|
||||
(enabled) => {
|
||||
if (enabled) {
|
||||
activeSection.value = null
|
||||
collapsed.scene = true
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(() => [store.activeObjectType, store.activeObjectId], ([type]) => {
|
||||
if (store.settings.floatingMenuEnabled && type !== 'particle') {
|
||||
collapsed.scene = false
|
||||
activeSection.value = 'scene'
|
||||
}
|
||||
})
|
||||
|
||||
function setEmitMode(mode: 'stream' | 'burst') {
|
||||
const config = sys.value?.config
|
||||
@@ -501,11 +542,32 @@ function normalizeExportFps() {
|
||||
}
|
||||
|
||||
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 expandSceneForObjectCreation() {
|
||||
if (store.settings.floatingMenuEnabled) {
|
||||
collapsed.scene = false
|
||||
activeSection.value = 'scene'
|
||||
}
|
||||
}
|
||||
function addSys() {
|
||||
expandSceneForObjectCreation()
|
||||
store.addSystem()
|
||||
}
|
||||
function addCollider() {
|
||||
expandSceneForObjectCreation()
|
||||
store.addCollider()
|
||||
}
|
||||
function addPath() {
|
||||
expandSceneForObjectCreation()
|
||||
store.addPath()
|
||||
}
|
||||
function addSpine() {
|
||||
expandSceneForObjectCreation()
|
||||
store.addSpine()
|
||||
}
|
||||
function copySelectedObject() {
|
||||
expandSceneForObjectCreation()
|
||||
store.duplicateSelectedObject()
|
||||
}
|
||||
function pathAnchor(x: number, y: number, inX = 0, inY = 0, outX = 0, outY = 0) {
|
||||
return { x, y, inX, inY, outX, outY }
|
||||
}
|
||||
@@ -595,5 +657,5 @@ function onSpawnPathChange() {
|
||||
// 重置/删除图片 → 回到默认 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, localProjects, selectedLocalProjectName, selectLocalProject, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy, projectFilesInput, saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject, downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, loadAllProjectFiles, 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, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerSlots, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, 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 }
|
||||
return { store, exportSettings, exportBusy, exportTarget, exportMessage, exportError, connectorPairUrl, connectorStatusText, connectorStatusClass, connectorReady, connectorAuthorizationUrl, connectorDisabledReason, connectorButtonTitle, refreshConnectorHealth, exportSpineJson, exportSpineProject, exportSpineImages, configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, localProjects, selectedLocalProjectName, selectLocalProject, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy, projectFilesInput, saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject, downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, loadAllProjectFiles, 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, activeSection, canUseSection, activateSection, sectionVisible, sectionOpen, activeCollider, activePath, activeSpine, canCopyObject, selectedSpineBone, boundSpineBone, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerSlots, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, 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 }
|
||||
}
|
||||
Reference in new issue
Block a user