130 lines
5.0 KiB
TypeScript
130 lines
5.0 KiB
TypeScript
import { watch, type WatchStopHandle } from 'vue'
|
|
import type { SettingsState, useParticleStore } from '../store/particleStore'
|
|
import { translate } from '../i18n'
|
|
|
|
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'
|
|
| 'floatingMenuEnabled'
|
|
| 'undoLimit'
|
|
| 'showEmitter'
|
|
| 'showBones'
|
|
| 'bonePointDrawSize'
|
|
| 'boneAxisDrawSize'
|
|
| 'showSkinMesh'
|
|
| 'tickEnabled'
|
|
| 'axisFontSize'
|
|
| 'tickWidth'
|
|
| 'showAxisLabels'
|
|
| 'unsavedChangesPrompt'
|
|
| 'showProjectSize'
|
|
| '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,
|
|
floatingMenuEnabled: settings.floatingMenuEnabled,
|
|
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,
|
|
unsavedChangesPrompt: settings.unsavedChangesPrompt,
|
|
showProjectSize: settings.showProjectSize,
|
|
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'),
|
|
floatingMenuEnabled: booleanValue('floatingMenuEnabled'),
|
|
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'),
|
|
unsavedChangesPrompt: booleanValue('unsavedChangesPrompt'),
|
|
showProjectSize: booleanValue('showProjectSize'),
|
|
axisColor,
|
|
}
|
|
}
|
|
|
|
function parseSystemSettingsCache(text: string): SystemSettingsFile {
|
|
let parsed: unknown
|
|
try {
|
|
parsed = JSON.parse(text)
|
|
} catch {
|
|
throw new Error(translate('errors.settingsInvalidJson'))
|
|
}
|
|
const candidate = parsed as Partial<SystemSettingsFile>
|
|
if (candidate.format !== SYSTEM_SETTINGS_FORMAT) throw new Error(translate('errors.settingsFormat'))
|
|
if (candidate.version !== SYSTEM_SETTINGS_VERSION) throw new Error(translate('errors.settingsVersion', { version: candidate.version ?? translate('export.unknown') }))
|
|
if (!candidate.settings || typeof candidate.settings !== 'object') throw new Error(translate('errors.settingsMissing'))
|
|
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 },
|
|
)
|
|
}
|