修改标题,独立系统设置,可缓存
This commit is contained in:
Vendored
-4
@@ -2,9 +2,5 @@
|
||||
{
|
||||
"name": "粒子-序列帧-拖尾-碰撞-路径-spine",
|
||||
"file": "粒子-序列帧-拖尾-碰撞-路径-spine.json"
|
||||
},
|
||||
{
|
||||
"name": "粒子序列帧拖尾",
|
||||
"file": "粒子序列帧拖尾.json"
|
||||
}
|
||||
]
|
||||
|
||||
+5
-16
@@ -1,29 +1,18 @@
|
||||
{
|
||||
"format": "SpineParticle.EditorConfig",
|
||||
"version": 1,
|
||||
"savedAt": "2026-09-01T09:14:16.879Z",
|
||||
"savedAt": "2026-09-01T09:56:02.078Z",
|
||||
"editor": {
|
||||
"viewScale": 0.3606100249815796,
|
||||
"viewX": -86.72420252825225,
|
||||
"viewY": 43.3444575964476
|
||||
"viewScale": 0.25667509294538904,
|
||||
"viewX": -108.03375430502649,
|
||||
"viewY": 58.10811303956338
|
||||
},
|
||||
"settings": {
|
||||
"tickEnabled": false,
|
||||
"axisFontSize": 12,
|
||||
"tickWidth": 1,
|
||||
"showAxisLabels": true,
|
||||
"axisColor": "#7a86ad",
|
||||
"undoLimit": 20,
|
||||
"backgroundX": 0,
|
||||
"backgroundY": 0,
|
||||
"backgroundOpacity": 1,
|
||||
"backgroundImageName": "",
|
||||
"backgroundImageData": "",
|
||||
"showEmitter": true,
|
||||
"showBones": false,
|
||||
"bonePointDrawSize": 1,
|
||||
"boneAxisDrawSize": 2,
|
||||
"showSkinMesh": false,
|
||||
"showGrid": true,
|
||||
"exportSettings": {
|
||||
"imagesPath": "./images/",
|
||||
@@ -2387,7 +2376,7 @@
|
||||
"depth": 7
|
||||
}
|
||||
],
|
||||
"assetVersion": 3,
|
||||
"assetVersion": 4,
|
||||
"status": "ready",
|
||||
"error": ""
|
||||
}
|
||||
|
||||
Vendored
-1440
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
+70
-70
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+3
-3
@@ -3,13 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Spine Particle · 阶段1 Spike</title>
|
||||
<title>Spine粒子编辑器</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; background: #111; color: #eee; font-family: system-ui, -apple-system, sans-serif; }
|
||||
#app { display: flex; flex-direction: column; height: 100vh; }
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-Dw7eU6re.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BPQ62DCC.css">
|
||||
<script type="module" crossorigin src="/assets/index-DujVfZi9.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-nUMfzhFe.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Spine Particle · 阶段1 Spike</title>
|
||||
<title>Spine粒子编辑器</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; background: #111; color: #eee; font-family: system-ui, -apple-system, sans-serif; }
|
||||
#app { display: flex; flex-direction: column; height: 100vh; }
|
||||
|
||||
@@ -2,9 +2,5 @@
|
||||
{
|
||||
"name": "粒子-序列帧-拖尾-碰撞-路径-spine",
|
||||
"file": "粒子-序列帧-拖尾-碰撞-路径-spine.json"
|
||||
},
|
||||
{
|
||||
"name": "粒子序列帧拖尾",
|
||||
"file": "粒子序列帧拖尾.json"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
{
|
||||
"format": "SpineParticle.EditorConfig",
|
||||
"version": 1,
|
||||
"savedAt": "2026-09-01T09:14:16.879Z",
|
||||
"savedAt": "2026-09-01T09:56:02.078Z",
|
||||
"editor": {
|
||||
"viewScale": 0.3606100249815796,
|
||||
"viewX": -86.72420252825225,
|
||||
"viewY": 43.3444575964476
|
||||
"viewScale": 0.25667509294538904,
|
||||
"viewX": -108.03375430502649,
|
||||
"viewY": 58.10811303956338
|
||||
},
|
||||
"settings": {
|
||||
"tickEnabled": false,
|
||||
"axisFontSize": 12,
|
||||
"tickWidth": 1,
|
||||
"showAxisLabels": true,
|
||||
"axisColor": "#7a86ad",
|
||||
"undoLimit": 20,
|
||||
"backgroundX": 0,
|
||||
"backgroundY": 0,
|
||||
"backgroundOpacity": 1,
|
||||
"backgroundImageName": "",
|
||||
"backgroundImageData": "",
|
||||
"showEmitter": true,
|
||||
"showBones": false,
|
||||
"bonePointDrawSize": 1,
|
||||
"boneAxisDrawSize": 2,
|
||||
"showSkinMesh": false,
|
||||
"showGrid": true,
|
||||
"exportSettings": {
|
||||
"imagesPath": "./images/",
|
||||
@@ -2387,7 +2376,7 @@
|
||||
"depth": 7
|
||||
}
|
||||
],
|
||||
"assetVersion": 3,
|
||||
"assetVersion": 4,
|
||||
"status": "ready",
|
||||
"error": ""
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+16
-6
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<!-- 左上:参数面板(宽度可拖) -->
|
||||
<div class="left" :style="{ width: panelW + 'px' }">
|
||||
<div class="layout" :class="{ 'panel-right': store.settings.parameterPanelOnRight }">
|
||||
<!-- 参数面板:可在系统设置中切换左右位置,宽度可拖。 -->
|
||||
<div class="parameter-panel" :style="{ width: panelW + 'px' }">
|
||||
<ParticlePanel />
|
||||
</div>
|
||||
<div class="resizer-col" @pointerdown="onPanelDrag"></div>
|
||||
@@ -19,18 +19,25 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useParticleStore } from './store/particleStore'
|
||||
import { installSystemSettingsCache } from './editor/systemSettings'
|
||||
import Stage from './views/Stage.vue'
|
||||
import ParticlePanel from './views/ParticlePanel.vue'
|
||||
import Timeline from './views/Timeline.vue'
|
||||
|
||||
const store = useParticleStore()
|
||||
installSystemSettingsCache(store)
|
||||
const panelW = ref(360)
|
||||
const timelineH = ref(180)
|
||||
|
||||
// 左面板宽度拖拽
|
||||
// 参数面板宽度拖拽:面板在右侧时,从视口右边缘反向计算宽度。
|
||||
function onPanelDrag(e: PointerEvent) {
|
||||
e.preventDefault()
|
||||
const move = (ev: PointerEvent) => {
|
||||
const w = Math.max(240, Math.min(ev.clientX, window.innerWidth - 200))
|
||||
const pointerWidth = store.settings.parameterPanelOnRight
|
||||
? window.innerWidth - ev.clientX
|
||||
: ev.clientX
|
||||
const w = Math.max(240, Math.min(pointerWidth, window.innerWidth - 200))
|
||||
panelW.value = w
|
||||
}
|
||||
const up = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up) }
|
||||
@@ -55,10 +62,13 @@ function onTimelineDrag(e: PointerEvent) {
|
||||
|
||||
<style scoped>
|
||||
.layout { display: flex; width: 100%; height: 100%; min-height: 0; overflow: clip; }
|
||||
.left { flex-shrink: 0; height: 100%; min-height: 0; overflow: clip; }
|
||||
.parameter-panel { flex-shrink: 0; height: 100%; min-height: 0; overflow: clip; }
|
||||
.resizer-col { width: 5px; cursor: col-resize; background: #1c1c2a; border-left: 1px solid #2a2a3c; border-right: 1px solid #2a2a3c; flex-shrink: 0; }
|
||||
.resizer-col:hover { background: #3a5a8c; }
|
||||
.right { flex: 1; min-width: 0; height: 100%; min-height: 0; overflow: clip; display: flex; flex-direction: column; }
|
||||
.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; }
|
||||
.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; }
|
||||
|
||||
@@ -22,12 +22,22 @@ type EditorStore = ReturnType<typeof useParticleStore>
|
||||
export const EDITOR_CONFIG_FORMAT = 'SpineParticle.EditorConfig'
|
||||
export const EDITOR_CONFIG_VERSION = 1
|
||||
|
||||
export type ProjectSettingsState = Pick<SettingsState,
|
||||
| 'backgroundX'
|
||||
| 'backgroundY'
|
||||
| 'backgroundOpacity'
|
||||
| 'backgroundImageName'
|
||||
| 'backgroundImageData'
|
||||
| 'showGrid'
|
||||
| 'exportSettings'
|
||||
>
|
||||
|
||||
export interface EditorConfigFile {
|
||||
format: typeof EDITOR_CONFIG_FORMAT
|
||||
version: number
|
||||
savedAt: string
|
||||
editor: EditorState
|
||||
settings: SettingsState
|
||||
settings: ProjectSettingsState
|
||||
timeline: {
|
||||
fps: number
|
||||
loop: boolean
|
||||
@@ -45,6 +55,18 @@ export interface EditorConfigFile {
|
||||
}
|
||||
}
|
||||
|
||||
export function captureProjectSettings(settings: SettingsState): ProjectSettingsState {
|
||||
return {
|
||||
backgroundX: settings.backgroundX,
|
||||
backgroundY: settings.backgroundY,
|
||||
backgroundOpacity: settings.backgroundOpacity,
|
||||
backgroundImageName: settings.backgroundImageName,
|
||||
backgroundImageData: settings.backgroundImageData,
|
||||
showGrid: settings.showGrid,
|
||||
exportSettings: cloneEditorValue(settings.exportSettings),
|
||||
}
|
||||
}
|
||||
|
||||
function jsonClone<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value, (key, item) => {
|
||||
if (key === 'texture' || key === 'trailTexture' || key === 'frames') return undefined
|
||||
@@ -53,12 +75,13 @@ function jsonClone<T>(value: T): T {
|
||||
}
|
||||
|
||||
export function createEditorConfig(store: EditorStore): EditorConfigFile {
|
||||
const settings = captureProjectSettings(store.settings)
|
||||
return jsonClone({
|
||||
format: EDITOR_CONFIG_FORMAT,
|
||||
version: EDITOR_CONFIG_VERSION,
|
||||
savedAt: new Date().toISOString(),
|
||||
editor: cloneEditorValue(store.editor),
|
||||
settings: cloneEditorValue(store.settings),
|
||||
settings,
|
||||
timeline: {
|
||||
fps: store.timeline.fps,
|
||||
loop: store.timeline.loop,
|
||||
@@ -201,11 +224,14 @@ export async function applyEditorConfig(store: EditorStore, input: EditorConfigF
|
||||
store.paths = paths
|
||||
store.spines = spines
|
||||
Object.assign(store.editor, cloneEditorValue(config.editor || {}))
|
||||
Object.assign(store.settings, cloneEditorValue(config.settings || {}))
|
||||
const projectSettings = config.settings as Partial<ProjectSettingsState> | undefined
|
||||
if (projectSettings) {
|
||||
for (const key of ['backgroundX', 'backgroundY', 'backgroundOpacity', 'backgroundImageName', 'backgroundImageData', 'showGrid'] as const) {
|
||||
if (Object.prototype.hasOwnProperty.call(projectSettings, key)) (store.settings[key] as any) = projectSettings[key]
|
||||
}
|
||||
}
|
||||
store.settings.exportSettings = ensureSpineExportSettings(config.settings?.exportSettings)
|
||||
store.settings.undoLimit = Math.min(200, Math.max(1, Math.round(Number(store.settings.undoLimit) || 20)))
|
||||
store.settings.bonePointDrawSize = Math.min(10, Math.max(0.1, Number(config.settings?.bonePointDrawSize) || 1))
|
||||
store.settings.boneAxisDrawSize = Math.min(10, Math.max(0.1, Number(config.settings?.boneAxisDrawSize) || 2))
|
||||
store.timeline.fps = Math.max(1, Math.round(Number(config.timeline?.fps) || 30))
|
||||
store.timeline.loop = config.timeline?.loop !== false
|
||||
store.timeline.animations = Array.isArray(config.timeline?.animations) && config.timeline.animations.length
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { reactive, watch, type WatchStopHandle } from 'vue'
|
||||
import type { useParticleStore, ParticleSystem, CollisionBody, ScenePath, SettingsState } from '../store/particleStore'
|
||||
import type { useParticleStore, ParticleSystem, CollisionBody, ScenePath } from '../store/particleStore'
|
||||
import type { SpineSceneObject } from '../spine/spineTypes'
|
||||
import type { LoadedSpineAsset } from '../spine/spineAssetRegistry'
|
||||
import {
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
retainSpineAssetForHistory,
|
||||
} from '../spine/spineAssetRegistry'
|
||||
import { cloneEditorValue } from './editorClone'
|
||||
import { captureProjectSettings, type ProjectSettingsState } from './editorConfig'
|
||||
|
||||
type EditorStore = ReturnType<typeof useParticleStore>
|
||||
type UndoableSettings = Omit<SettingsState, 'undoLimit'>
|
||||
type UndoableSettings = ProjectSettingsState
|
||||
|
||||
interface EditorSnapshotData {
|
||||
systems: ParticleSystem[]
|
||||
@@ -43,14 +44,13 @@ function captureData(store: EditorStore): EditorSnapshotData {
|
||||
copy.frames = undefined
|
||||
return copy
|
||||
})
|
||||
const { undoLimit: _undoLimit, ...settings } = cloneEditorValue(store.settings)
|
||||
return {
|
||||
systems,
|
||||
colliders: cloneEditorValue(store.colliders) as unknown as CollisionBody[],
|
||||
paths: cloneEditorValue(store.paths) as unknown as ScenePath[],
|
||||
spines: cloneEditorValue(store.spines) as unknown as SpineSceneObject[],
|
||||
animations: [...store.timeline.animations],
|
||||
settings,
|
||||
settings: captureProjectSettings(store.settings),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { watch, type WatchStopHandle } from 'vue'
|
||||
import type { SettingsState, useParticleStore } from '../store/particleStore'
|
||||
|
||||
type EditorStore = ReturnType<typeof useParticleStore>
|
||||
|
||||
export const SYSTEM_SETTINGS_FORMAT = 'SpineParticle.SystemSettings'
|
||||
export const SYSTEM_SETTINGS_VERSION = 1
|
||||
const SYSTEM_SETTINGS_CACHE_KEY = 'SpineParticle.SystemSettings.v1'
|
||||
|
||||
export type SystemSettingsState = Pick<SettingsState,
|
||||
| 'parameterPanelOnRight'
|
||||
| 'undoLimit'
|
||||
| 'showEmitter'
|
||||
| 'showBones'
|
||||
| 'bonePointDrawSize'
|
||||
| 'boneAxisDrawSize'
|
||||
| 'showSkinMesh'
|
||||
| 'tickEnabled'
|
||||
| 'axisFontSize'
|
||||
| 'tickWidth'
|
||||
| 'showAxisLabels'
|
||||
| 'axisColor'
|
||||
>
|
||||
|
||||
export interface SystemSettingsFile {
|
||||
format: typeof SYSTEM_SETTINGS_FORMAT
|
||||
version: typeof SYSTEM_SETTINGS_VERSION
|
||||
savedAt: string
|
||||
settings: SystemSettingsState
|
||||
}
|
||||
|
||||
function clamp(value: unknown, min: number, max: number, fallback: number) {
|
||||
const number = Number(value)
|
||||
return Math.min(max, Math.max(min, Number.isFinite(number) ? number : fallback))
|
||||
}
|
||||
|
||||
export function captureSystemSettings(settings: SettingsState): SystemSettingsState {
|
||||
return {
|
||||
parameterPanelOnRight: settings.parameterPanelOnRight,
|
||||
undoLimit: settings.undoLimit,
|
||||
showEmitter: settings.showEmitter,
|
||||
showBones: settings.showBones,
|
||||
bonePointDrawSize: settings.bonePointDrawSize,
|
||||
boneAxisDrawSize: settings.boneAxisDrawSize,
|
||||
showSkinMesh: settings.showSkinMesh,
|
||||
tickEnabled: settings.tickEnabled,
|
||||
axisFontSize: settings.axisFontSize,
|
||||
tickWidth: settings.tickWidth,
|
||||
showAxisLabels: settings.showAxisLabels,
|
||||
axisColor: settings.axisColor,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizedSystemSettings(value: Partial<SystemSettingsState>, current: SettingsState): SystemSettingsState {
|
||||
const booleanValue = <K extends keyof SystemSettingsState>(key: K) => value[key] == null
|
||||
? Boolean(current[key])
|
||||
: value[key] === true
|
||||
const axisColor = typeof value.axisColor === 'string' && /^#[0-9a-f]{6}$/i.test(value.axisColor)
|
||||
? value.axisColor
|
||||
: current.axisColor
|
||||
return {
|
||||
parameterPanelOnRight: booleanValue('parameterPanelOnRight'),
|
||||
undoLimit: Math.round(clamp(value.undoLimit, 1, 200, current.undoLimit)),
|
||||
showEmitter: booleanValue('showEmitter'),
|
||||
showBones: booleanValue('showBones'),
|
||||
bonePointDrawSize: clamp(value.bonePointDrawSize, 0.1, 10, current.bonePointDrawSize),
|
||||
boneAxisDrawSize: clamp(value.boneAxisDrawSize, 0.1, 10, current.boneAxisDrawSize),
|
||||
showSkinMesh: booleanValue('showSkinMesh'),
|
||||
tickEnabled: booleanValue('tickEnabled'),
|
||||
axisFontSize: Math.round(clamp(value.axisFontSize, 8, 40, current.axisFontSize)),
|
||||
tickWidth: clamp(value.tickWidth, 1, 5, current.tickWidth),
|
||||
showAxisLabels: booleanValue('showAxisLabels'),
|
||||
axisColor,
|
||||
}
|
||||
}
|
||||
|
||||
function parseSystemSettingsCache(text: string): SystemSettingsFile {
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(text)
|
||||
} catch {
|
||||
throw new Error('系统设置缓存不是有效的 JSON')
|
||||
}
|
||||
const candidate = parsed as Partial<SystemSettingsFile>
|
||||
if (candidate.format !== SYSTEM_SETTINGS_FORMAT) throw new Error('不是 Spine粒子编辑器系统设置缓存')
|
||||
if (candidate.version !== SYSTEM_SETTINGS_VERSION) throw new Error(`暂不支持系统配置版本:${candidate.version ?? '未知'}`)
|
||||
if (!candidate.settings || typeof candidate.settings !== 'object') throw new Error('系统设置缓存缺少 settings')
|
||||
return candidate as SystemSettingsFile
|
||||
}
|
||||
|
||||
export function applySystemSettings(store: EditorStore, settings: Partial<SystemSettingsState>) {
|
||||
Object.assign(store.settings, normalizedSystemSettings(settings, store.settings))
|
||||
}
|
||||
|
||||
let stopCacheSync: WatchStopHandle | null = null
|
||||
|
||||
/** 启动时读取浏览器缓存,之后将系统设置的每次修改自动同步回缓存。 */
|
||||
export function installSystemSettingsCache(store: EditorStore) {
|
||||
stopCacheSync?.()
|
||||
try {
|
||||
const cached = localStorage.getItem(SYSTEM_SETTINGS_CACHE_KEY)
|
||||
if (cached) applySystemSettings(store, parseSystemSettingsCache(cached).settings)
|
||||
} catch {
|
||||
try { localStorage.removeItem(SYSTEM_SETTINGS_CACHE_KEY) } catch { /* 浏览器禁用本地存储时使用当前会话默认值。 */ }
|
||||
}
|
||||
stopCacheSync = watch(
|
||||
() => captureSystemSettings(store.settings),
|
||||
(settings) => {
|
||||
const file: SystemSettingsFile = {
|
||||
format: SYSTEM_SETTINGS_FORMAT,
|
||||
version: SYSTEM_SETTINGS_VERSION,
|
||||
savedAt: new Date().toISOString(),
|
||||
settings,
|
||||
}
|
||||
try { localStorage.setItem(SYSTEM_SETTINGS_CACHE_KEY, JSON.stringify(file)) } catch { /* 缓存不可用不影响编辑器运行。 */ }
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
}
|
||||
@@ -54,6 +54,8 @@ export interface EditorState {
|
||||
|
||||
/** 系统设置(可通过右上角设置面板调整) */
|
||||
export interface SettingsState {
|
||||
/** 参数面板是否显示在页面右侧;关闭时显示在左侧。 */
|
||||
parameterPanelOnRight: boolean
|
||||
/** 是否启用刻度(主开关;启用=显示坐标轴+刻度线+数值,可调整字号/粗细) */
|
||||
tickEnabled: boolean
|
||||
/** 坐标轴刻度文字大小(px),默认 12 */
|
||||
@@ -130,6 +132,7 @@ export const useParticleStore = defineStore('particle', {
|
||||
// 与 Spine 编辑器默认时间基准对齐:1 秒 = 30 帧。
|
||||
timeline: { playing: true, loop: true, frame: 0, totalFrames: 60, fps: 30, recorded: false, animation: 'animation', animations: ['animation'] } as TimelineState,
|
||||
settings: {
|
||||
parameterPanelOnRight: false,
|
||||
tickEnabled: false,
|
||||
axisFontSize: 12,
|
||||
tickWidth: 1,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span>粒子系统 · 编辑器</span>
|
||||
<span>Spine粒子编辑器</span>
|
||||
</div>
|
||||
|
||||
<div class="panel-scroll">
|
||||
|
||||
+24
-2
@@ -64,6 +64,16 @@
|
||||
<!-- 系统设置面板(左上角弹出) -->
|
||||
<div class="settings-panel" v-if="showSettings">
|
||||
<div class="sp-head">系统设置 <button class="sp-close" @click="showSettings = false">✕</button></div>
|
||||
<div class="sp-section-title">界面布局</div>
|
||||
<label class="sp-switch-row">
|
||||
<span>参数面板位置</span>
|
||||
<span class="sp-switch-control">
|
||||
<input type="checkbox" v-model="store.settings.parameterPanelOnRight" aria-label="参数面板显示在右侧" />
|
||||
<span class="sp-switch-ui" aria-hidden="true"></span>
|
||||
<b>{{ store.settings.parameterPanelOnRight ? '右侧' : '左侧' }}</b>
|
||||
</span>
|
||||
</label>
|
||||
<div class="sp-divider"></div>
|
||||
<div class="sp-section-title">操作历史</div>
|
||||
<label class="sp-row"><span>最大撤回步数</span>
|
||||
<input
|
||||
@@ -106,6 +116,7 @@
|
||||
</label>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="store.settings.showAxisLabels" />显示刻度数值</label>
|
||||
</template>
|
||||
<div class="sp-cache-note">整个系统设置会自动保存到当前浏览器</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showHelp" class="help-overlay" role="presentation" @click.self="showHelp = false">
|
||||
@@ -136,8 +147,8 @@
|
||||
<div><dt>小眼睛</dt><dd>单独显示或隐藏场景对象;隐藏的粒子系统不会导出。</dd></div>
|
||||
<div><dt>+Copy</dt><dd>完整复制当前选中对象,方便对比不同参数效果。</dd></div>
|
||||
<div><dt>背景图</dt><dd>上传参考图并调整位置和透明度,只用于画布预览。</dd></div>
|
||||
<div><dt>系统设置</dt><dd>管理撤回步数、发射点、骨骼、蒙皮网格和刻度显示。</dd></div>
|
||||
<div><dt>配置管理</dt><dd>保存当前编辑数据、加载配置,或直接使用预设文件。</dd></div>
|
||||
<div><dt>系统设置</dt><dd>管理界面与辅助显示,并自动保存在当前浏览器中。</dd></div>
|
||||
<div><dt>配置管理</dt><dd>保存当前项目数据、加载配置,或直接使用预设文件;不包含系统设置。</dd></div>
|
||||
<div><dt>Spine 骨骼</dt><dd>在层级树搜索并选择骨骼,可用于粒子发射器跟随。</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
@@ -198,6 +209,7 @@ const canvasFocused = ref(false)
|
||||
function toggleTool(t: '' | 'translate' | 'rotate' | 'scale') { activeTool.value = activeTool.value === t ? '' : t }
|
||||
|
||||
if (!(store.settings.undoLimit > 0)) store.settings.undoLimit = 20
|
||||
if (store.settings.parameterPanelOnRight == null) store.settings.parameterPanelOnRight = false
|
||||
if (!Number.isFinite(store.settings.backgroundX)) store.settings.backgroundX = 0
|
||||
if (!Number.isFinite(store.settings.backgroundY)) store.settings.backgroundY = 0
|
||||
if (!Number.isFinite(store.settings.backgroundOpacity)) store.settings.backgroundOpacity = 1
|
||||
@@ -1932,6 +1944,16 @@ b { color: #ffcc33; }
|
||||
}
|
||||
.sp-head { display: flex; justify-content: space-between; align-items: center; font-weight: 700; color: #cdf; margin-bottom: 10px; }
|
||||
.sp-section-title { margin: 2px 0 8px; color: #8a93bb; font-size: 11px; }
|
||||
.sp-switch-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; color: #aab; }
|
||||
.sp-switch-control { display: inline-flex; align-items: center; gap: 7px; }
|
||||
.sp-switch-control input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
|
||||
.sp-switch-ui { position: relative; width: 34px; height: 18px; border-radius: 999px; background: #39445a; cursor: pointer; transition: background .16s ease; }
|
||||
.sp-switch-ui::after { content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: #f4f6fb; transition: transform .16s ease; }
|
||||
.sp-switch-control input:checked + .sp-switch-ui { background: #655df0; }
|
||||
.sp-switch-control input:checked + .sp-switch-ui::after { transform: translateX(16px); }
|
||||
.sp-switch-control input:focus-visible + .sp-switch-ui { outline: 2px solid #8b86ff; outline-offset: 2px; }
|
||||
.sp-switch-control b { min-width: 24px; color: #8a93bb; font-size: 11px; font-weight: 500; }
|
||||
.sp-cache-note { margin-top: 10px; padding-top: 9px; border-top: 1px solid #2a2a3c; color: #647089; font-size: 10px; text-align: center; }
|
||||
.sp-divider { height: 1px; margin: 10px 0; background: #2a2a3c; }
|
||||
.sp-close { background: #2e3a55; border: none; color: #cdf; border-radius: 4px; width: 18px; height: 18px; cursor: pointer; font-size: 11px; }
|
||||
.sp-row { display: flex; align-items: center; gap: 8px; margin: 8px 0; }
|
||||
|
||||
Reference in New Issue
Block a user