场景对象的复制与粘贴+配置追加功能
This commit is contained in:
1 parent
3587129b90
commit
6dd32ec8fc
24 files changed
+761
-14
No files matched your search
@@ -0,0 +1,134 @@
|
||||
import type { CollisionBody, ParticleSystem, ScenePath, useParticleStore } from '../store/particleStore'
|
||||
import type { SpineSceneObject } from '../spine/spineTypes'
|
||||
import { ensureEmitterConfig } from '../core/particleEmitter'
|
||||
import { registerSpineAsset } from '../spine/spineAssetRegistry'
|
||||
import { translate } from '../i18n'
|
||||
import { cloneEditorValue } from './editorClone'
|
||||
import {
|
||||
hydrateParticleResources,
|
||||
parseEditorConfig,
|
||||
parseEmbeddedSpines,
|
||||
withoutLegacyColliderTags,
|
||||
} from './editorConfig'
|
||||
|
||||
type EditorStore = ReturnType<typeof useParticleStore>
|
||||
|
||||
function nextId(items: readonly { id: number }[]) {
|
||||
return Math.max(0, ...items.map((item) => Number(item.id) || 0)) + 1
|
||||
}
|
||||
|
||||
function remapId(map: ReadonlyMap<number, number>, id: number) {
|
||||
return map.get(id) || 0
|
||||
}
|
||||
|
||||
export async function appendEditorConfigText(store: EditorStore, text: string) {
|
||||
const config = parseEditorConfig(text)
|
||||
const systems = cloneEditorValue(config.scene.systems) as unknown as ParticleSystem[]
|
||||
const colliders = config.scene.colliders.map(withoutLegacyColliderTags)
|
||||
const paths = cloneEditorValue(config.scene.paths) as unknown as ScenePath[]
|
||||
const spines = cloneEditorValue(config.scene.spines) as unknown as SpineSceneObject[]
|
||||
|
||||
let systemId = nextId(store.systems)
|
||||
let colliderId = nextId(store.colliders)
|
||||
let pathId = nextId(store.paths)
|
||||
let spineId = nextId(store.spines)
|
||||
const systemIds = new Map<number, number>()
|
||||
const colliderIds = new Map<number, number>()
|
||||
const pathIds = new Map<number, number>()
|
||||
const spineIds = new Map<number, number>()
|
||||
|
||||
for (const system of systems) {
|
||||
systemIds.set(system.id, systemId)
|
||||
system.id = systemId++
|
||||
}
|
||||
for (const collider of colliders) {
|
||||
colliderIds.set(collider.id, colliderId)
|
||||
collider.id = colliderId++
|
||||
}
|
||||
for (const path of paths) {
|
||||
pathIds.set(path.id, pathId)
|
||||
path.id = pathId++
|
||||
}
|
||||
for (const spine of spines) {
|
||||
if (spine.sourceBundle == null) spine.sourceBundle = null
|
||||
if (spine.dataVersion == null) spine.dataVersion = ''
|
||||
if (!Array.isArray(spine.slots)) spine.slots = []
|
||||
if (typeof spine.selectedBone !== 'string') spine.selectedBone = ''
|
||||
if (typeof spine.selectedSlot !== 'string') spine.selectedSlot = ''
|
||||
spineIds.set(spine.id, spineId)
|
||||
spine.id = spineId++
|
||||
}
|
||||
|
||||
for (const system of systems) {
|
||||
const config = ensureEmitterConfig(system.config as any)
|
||||
system.frames = undefined
|
||||
config.collisionColliderIds = config.collisionColliderIds
|
||||
.map((id) => remapId(colliderIds, id))
|
||||
.filter((id) => id > 0)
|
||||
config.spawnPathId = remapId(pathIds, config.spawnPathId)
|
||||
if (!config.spawnPathId && (config.emissionAngleMode === 'path-normal' || config.emissionAngleMode === 'path-tangent')) {
|
||||
config.emissionAngleMode = 'fixed'
|
||||
}
|
||||
config.emitterFollowPathId = remapId(pathIds, config.emitterFollowPathId)
|
||||
config.emitterFollowSpineId = remapId(spineIds, config.emitterFollowSpineId)
|
||||
config.renderLayerSpineId = remapId(spineIds, config.renderLayerSpineId)
|
||||
for (const rule of config.events || []) {
|
||||
rule.targetSystemId = remapId(systemIds, rule.targetSystemId)
|
||||
}
|
||||
}
|
||||
for (const collider of colliders) {
|
||||
collider.followPathId = remapId(pathIds, collider.followPathId)
|
||||
if (!collider.followPathId) collider.followPath = false
|
||||
}
|
||||
|
||||
await hydrateParticleResources(systems)
|
||||
const parsedSpines = await parseEmbeddedSpines(spines)
|
||||
|
||||
const animations = [...store.timeline.animations]
|
||||
for (const animation of config.timeline?.animations || []) {
|
||||
const name = typeof animation === 'string' ? animation.trim() : ''
|
||||
if (name && !animations.includes(name)) animations.push(name)
|
||||
}
|
||||
for (const system of systems) {
|
||||
if (!animations.includes(system.animation)) {
|
||||
system.animation = store.timeline.animation || store.timeline.animations[0] || 'animation'
|
||||
}
|
||||
}
|
||||
|
||||
for (const spine of spines) {
|
||||
const parsed = parsedSpines.get(spine.id)
|
||||
spine.assetVersion = Math.max(1, Number(spine.assetVersion) || 0) + 1
|
||||
if (!parsed) {
|
||||
spine.status = 'empty'
|
||||
spine.error = spine.skeletonFileName ? translate('errors.missingEmbeddedSpine') : ''
|
||||
spine.animations = []
|
||||
spine.bones = []
|
||||
spine.slots = []
|
||||
spine.selectedAnimation = ''
|
||||
continue
|
||||
}
|
||||
registerSpineAsset(spine.id, parsed)
|
||||
spine.dataVersion = parsed.dataVersion
|
||||
spine.animations = parsed.animations
|
||||
spine.bones = parsed.bones
|
||||
spine.slots = parsed.slots
|
||||
if (!parsed.animations.some((animation) => animation.name === spine.selectedAnimation)) {
|
||||
spine.selectedAnimation = parsed.animations[0]?.name || ''
|
||||
}
|
||||
spine.status = 'ready'
|
||||
spine.error = ''
|
||||
}
|
||||
|
||||
store.systems.push(...systems)
|
||||
store.colliders.push(...colliders)
|
||||
store.paths.push(...paths)
|
||||
store.spines.push(...spines)
|
||||
store.timeline.animations = animations
|
||||
store.syncSceneSequences()
|
||||
|
||||
if (systems[0]) store.selectSceneObject('particle', systems[0].id)
|
||||
else if (colliders[0]) store.selectSceneObject('collision', colliders[0].id)
|
||||
else if (paths[0]) store.selectSceneObject('path', paths[0].id)
|
||||
else if (spines[0]) store.selectSceneObject('spine', spines[0].id)
|
||||
store.recalcTotalFrames()
|
||||
}
|
||||
@@ -83,7 +83,7 @@ function jsonClone<T>(value: T): T {
|
||||
})) as T
|
||||
}
|
||||
|
||||
function withoutLegacyColliderTags(collider: CollisionBody): CollisionBody {
|
||||
export function withoutLegacyColliderTags(collider: CollisionBody): CollisionBody {
|
||||
const copy = cloneEditorValue(collider) as CollisionBody & { tags?: unknown; selectedTag?: unknown }
|
||||
delete copy.tags
|
||||
delete copy.selectedTag
|
||||
@@ -192,7 +192,7 @@ export function parseEditorConfig(text: string): EditorConfigFile {
|
||||
return candidate as EditorConfigFile
|
||||
}
|
||||
|
||||
async function hydrateParticleResources(systems: ParticleSystem[]) {
|
||||
export async function hydrateParticleResources(systems: ParticleSystem[]) {
|
||||
const textureCache = new Map<string, Promise<Texture>>()
|
||||
const loadTexture = (url: string) => {
|
||||
const source = url === '/particles/star.png'
|
||||
@@ -225,7 +225,7 @@ async function hydrateParticleResources(systems: ParticleSystem[]) {
|
||||
}))
|
||||
}
|
||||
|
||||
async function parseEmbeddedSpines(spines: SpineSceneObject[]) {
|
||||
export async function parseEmbeddedSpines(spines: SpineSceneObject[]) {
|
||||
const parsed = new Map<number, ParsedSpineBundle>()
|
||||
try {
|
||||
for (const spine of spines) {
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import type {
|
||||
CollisionBody,
|
||||
ParticleSystem,
|
||||
SceneObjectType,
|
||||
ScenePath,
|
||||
useParticleStore,
|
||||
} from '../store/particleStore'
|
||||
import type { SpineSceneObject } from '../spine/spineTypes'
|
||||
import { registerSpineAsset, releaseSpineAsset } from '../spine/spineAssetRegistry'
|
||||
import { ensureEmitterConfig } from '../core/particleEmitter'
|
||||
import { collectEmitterAncestors } from '../core/subEmitterIds'
|
||||
import { translate } from '../i18n'
|
||||
import { cloneEditorValue } from './editorClone'
|
||||
import { hydrateParticleResources, parseEmbeddedSpines, withoutLegacyColliderTags } from './editorConfig'
|
||||
|
||||
type EditorStore = ReturnType<typeof useParticleStore>
|
||||
|
||||
const CLIPBOARD_FORMAT = 'SpineParticle.SceneObject'
|
||||
const CLIPBOARD_VERSION = 1
|
||||
type ClipboardObject = ParticleSystem | CollisionBody | ScenePath | SpineSceneObject
|
||||
|
||||
interface SceneObjectClipboardPayload {
|
||||
format: typeof CLIPBOARD_FORMAT
|
||||
version: typeof CLIPBOARD_VERSION
|
||||
type: SceneObjectType
|
||||
object: ClipboardObject
|
||||
}
|
||||
|
||||
let pasteBusy = false
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function jsonClone<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value, (key, item) => {
|
||||
if (key === 'frames' || key === 'texture' || key === 'trailTexture') return undefined
|
||||
return item
|
||||
})) as T
|
||||
}
|
||||
|
||||
function objectForCopy(store: EditorStore, type: SceneObjectType, id: number): ClipboardObject | null {
|
||||
if (type === 'particle') {
|
||||
const system = store.systems.find((item) => item.id === id)
|
||||
return system ? jsonClone(system) as unknown as ParticleSystem : null
|
||||
}
|
||||
if (type === 'collision') {
|
||||
const collider = store.colliders.find((item) => item.id === id)
|
||||
return collider ? withoutLegacyColliderTags(collider) : null
|
||||
}
|
||||
if (type === 'path') {
|
||||
const path = store.paths.find((item) => item.id === id)
|
||||
return path ? cloneEditorValue(path) : null
|
||||
}
|
||||
const spine = store.spines.find((item) => item.id === id)
|
||||
if (!spine) return null
|
||||
const copy = jsonClone(spine)
|
||||
copy.selectedBone = ''
|
||||
copy.selectedSlot = ''
|
||||
copy.status = copy.sourceBundle ? 'ready' : 'empty'
|
||||
copy.error = ''
|
||||
return copy
|
||||
}
|
||||
|
||||
function parsePayload(text: string): SceneObjectClipboardPayload {
|
||||
let value: unknown
|
||||
try {
|
||||
value = JSON.parse(text, (key, item) => (
|
||||
key === '__proto__' || key === 'prototype' || key === 'constructor' ? undefined : item
|
||||
))
|
||||
} catch {
|
||||
throw new Error(translate('scene.clipboardInvalid'))
|
||||
}
|
||||
if (!isRecord(value)
|
||||
|| value.format !== CLIPBOARD_FORMAT
|
||||
|| value.version !== CLIPBOARD_VERSION
|
||||
|| !['particle', 'collision', 'path', 'spine'].includes(String(value.type))
|
||||
|| !isRecord(value.object)) {
|
||||
throw new Error(translate('scene.clipboardInvalid'))
|
||||
}
|
||||
return value as unknown as SceneObjectClipboardPayload
|
||||
}
|
||||
|
||||
function validObjectForType(payload: SceneObjectClipboardPayload) {
|
||||
const object = payload.object as unknown as Record<string, unknown>
|
||||
if (payload.type === 'particle') return isRecord(object.config)
|
||||
if (payload.type === 'collision') return typeof object.name === 'string'
|
||||
if (payload.type === 'path') return typeof object.name === 'string' && Array.isArray(object.points)
|
||||
return typeof object.name === 'string'
|
||||
&& (object.sourceBundle == null || isRecord(object.sourceBundle))
|
||||
}
|
||||
|
||||
function typeLabel(type: SceneObjectType) {
|
||||
return type === 'spine' ? 'Spine' : translate(`scene.${type}`)
|
||||
}
|
||||
|
||||
function resetParticleReferences(store: EditorStore, system: ParticleSystem) {
|
||||
const config = ensureEmitterConfig(system.config as any)
|
||||
const systems = store.systems.map((item) => (item.id === system.id ? system : item))
|
||||
const systemIds = new Set(systems.map((item) => item.id))
|
||||
const colliderIds = new Set(store.colliders.map((item) => item.id))
|
||||
const pathIds = new Set(store.paths.map((item) => item.id))
|
||||
const spineIds = new Set(store.spines.map((item) => item.id))
|
||||
const ancestors = collectEmitterAncestors(systems, system.id)
|
||||
|
||||
config.collisionColliderIds = config.collisionColliderIds.filter((id) => colliderIds.has(id))
|
||||
if (!pathIds.has(config.spawnPathId)) {
|
||||
config.spawnPathId = 0
|
||||
if (config.emissionAngleMode === 'path-normal' || config.emissionAngleMode === 'path-tangent') {
|
||||
config.emissionAngleMode = 'fixed'
|
||||
}
|
||||
}
|
||||
for (const rule of config.events || []) {
|
||||
if (!systemIds.has(rule.targetSystemId) || rule.targetSystemId === system.id || ancestors.has(rule.targetSystemId)) {
|
||||
rule.targetSystemId = 0
|
||||
}
|
||||
}
|
||||
if (config.emitterFollowMode === 'path' && !pathIds.has(config.emitterFollowPathId)) {
|
||||
config.emitterFollow = false
|
||||
config.emitterFollowMode = 'none'
|
||||
config.emitterFollowPathId = 0
|
||||
}
|
||||
if (!spineIds.has(config.emitterFollowSpineId)) {
|
||||
config.emitterFollowSpineId = 0
|
||||
config.emitterFollowBoneName = ''
|
||||
if (config.emitterFollowMode === 'bone') {
|
||||
config.emitterFollow = false
|
||||
config.emitterFollowMode = 'none'
|
||||
}
|
||||
}
|
||||
if (config.emitterFollowMode === 'bone') {
|
||||
const spine = store.spines.find((item) => item.id === config.emitterFollowSpineId)
|
||||
if (!spine?.bones.some((bone) => bone.name === config.emitterFollowBoneName)) {
|
||||
config.emitterFollow = false
|
||||
config.emitterFollowMode = 'none'
|
||||
config.emitterFollowSpineId = 0
|
||||
config.emitterFollowBoneName = ''
|
||||
}
|
||||
}
|
||||
if (!spineIds.has(config.renderLayerSpineId)) {
|
||||
config.renderLayerSpineId = 0
|
||||
config.renderLayerSlotName = ''
|
||||
if (config.renderLayerMode === 'slot') config.renderLayerMode = 'top'
|
||||
}
|
||||
}
|
||||
|
||||
export async function copySceneObjectToClipboard(store: EditorStore, type: SceneObjectType, id: number) {
|
||||
const object = objectForCopy(store, type, id)
|
||||
if (!object) throw new Error(translate('scene.clipboardInvalid'))
|
||||
if (!navigator.clipboard?.writeText) throw new Error(translate('scene.clipboardUnavailable'))
|
||||
const payload: SceneObjectClipboardPayload = {
|
||||
format: CLIPBOARD_FORMAT,
|
||||
version: CLIPBOARD_VERSION,
|
||||
type,
|
||||
object,
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(JSON.stringify(payload))
|
||||
} catch {
|
||||
throw new Error(translate('scene.clipboardCopyFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
export async function pasteSceneObjectFromClipboard(store: EditorStore, type: SceneObjectType, id: number) {
|
||||
if (pasteBusy) return
|
||||
pasteBusy = true
|
||||
try {
|
||||
if (!navigator.clipboard?.readText) throw new Error(translate('scene.clipboardUnavailable'))
|
||||
let text: string
|
||||
try {
|
||||
text = await navigator.clipboard.readText()
|
||||
} catch {
|
||||
throw new Error(translate('scene.clipboardReadFailed'))
|
||||
}
|
||||
const payload = parsePayload(text)
|
||||
if (!validObjectForType(payload)) throw new Error(translate('scene.clipboardInvalid'))
|
||||
if (payload.type !== type) {
|
||||
throw new Error(translate('scene.clipboardTypeMismatch', {
|
||||
source: typeLabel(payload.type),
|
||||
target: typeLabel(type),
|
||||
}))
|
||||
}
|
||||
|
||||
if (type === 'particle') {
|
||||
const index = store.systems.findIndex((item) => item.id === id)
|
||||
if (index < 0) throw new Error(translate('scene.clipboardInvalid'))
|
||||
const target = store.systems[index]
|
||||
const source = payload.object as ParticleSystem
|
||||
if (!isRecord(source.config)) throw new Error(translate('scene.clipboardInvalid'))
|
||||
const system = cloneEditorValue(source)
|
||||
system.id = target.id
|
||||
system.config = ensureEmitterConfig(system.config as any)
|
||||
system.config.name = target.config.name
|
||||
system.frames = undefined
|
||||
system.animation = store.timeline.animations.includes(system.animation)
|
||||
? system.animation
|
||||
: store.timeline.animation
|
||||
resetParticleReferences(store, system)
|
||||
await hydrateParticleResources([system])
|
||||
store.systems.splice(index, 1, system)
|
||||
store.selectSceneObject('particle', system.id)
|
||||
store.recalcTotalFrames()
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'collision') {
|
||||
const index = store.colliders.findIndex((item) => item.id === id)
|
||||
if (index < 0) throw new Error(translate('scene.clipboardInvalid'))
|
||||
const target = store.colliders[index]
|
||||
const collider = withoutLegacyColliderTags(payload.object as CollisionBody)
|
||||
collider.id = target.id
|
||||
collider.name = target.name
|
||||
if (!store.paths.some((item) => item.id === collider.followPathId)) {
|
||||
collider.followPath = false
|
||||
collider.followPathId = 0
|
||||
}
|
||||
store.colliders.splice(index, 1, collider)
|
||||
store.selectSceneObject('collision', collider.id)
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'path') {
|
||||
const index = store.paths.findIndex((item) => item.id === id)
|
||||
if (index < 0) throw new Error(translate('scene.clipboardInvalid'))
|
||||
const target = store.paths[index]
|
||||
const path = cloneEditorValue(payload.object as ScenePath)
|
||||
path.id = target.id
|
||||
path.name = target.name
|
||||
store.paths.splice(index, 1, path)
|
||||
store.selectSceneObject('path', path.id)
|
||||
return
|
||||
}
|
||||
|
||||
const index = store.spines.findIndex((item) => item.id === id)
|
||||
if (index < 0) throw new Error(translate('scene.clipboardInvalid'))
|
||||
const target = store.spines[index]
|
||||
const spine = cloneEditorValue(payload.object as SpineSceneObject)
|
||||
spine.id = target.id
|
||||
spine.name = target.name
|
||||
spine.selectedBone = ''
|
||||
spine.selectedSlot = ''
|
||||
spine.assetVersion = target.assetVersion + 1
|
||||
spine.status = 'loading'
|
||||
spine.error = ''
|
||||
const parsed = (await parseEmbeddedSpines([spine])).get(spine.id)
|
||||
if (parsed) {
|
||||
releaseSpineAsset(target.id)
|
||||
registerSpineAsset(spine.id, parsed)
|
||||
spine.dataVersion = parsed.dataVersion
|
||||
spine.animations = parsed.animations
|
||||
spine.bones = parsed.bones
|
||||
spine.slots = parsed.slots
|
||||
if (!parsed.animations.some((animation) => animation.name === spine.selectedAnimation)) {
|
||||
spine.selectedAnimation = parsed.animations[0]?.name || ''
|
||||
}
|
||||
spine.status = 'ready'
|
||||
} else {
|
||||
releaseSpineAsset(target.id)
|
||||
spine.dataVersion = ''
|
||||
spine.animations = []
|
||||
spine.bones = []
|
||||
spine.slots = []
|
||||
spine.selectedAnimation = ''
|
||||
spine.status = 'empty'
|
||||
}
|
||||
store.spines.splice(index, 1, spine)
|
||||
store.selectSceneObject('spine', spine.id)
|
||||
store.recalcTotalFrames()
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message) throw error
|
||||
throw new Error(translate('scene.clipboardPasteFailed'))
|
||||
} finally {
|
||||
pasteBusy = false
|
||||
}
|
||||
}
|
||||
@@ -164,6 +164,9 @@
|
||||
"storageUnavailable": "Der lokale Browserspeicher ist nicht verfügbar",
|
||||
"saved": "Im Browser gespeichert: {name}",
|
||||
"loaded": "Aus dem Browser geladen: {name}",
|
||||
"appendToScene": "Diese Konfiguration an die Szene anhängen",
|
||||
"appending": "Konfiguration wird angehängt: {name}",
|
||||
"appended": "An die Szene angehängt: {name}",
|
||||
"deleted": "Browserkonfiguration gelöscht: {name}",
|
||||
"downloadEmpty": "Keine Konfigurationen zum Herunterladen",
|
||||
"downloaded": "Heruntergeladen: {name}"
|
||||
@@ -501,6 +504,14 @@
|
||||
"addPathShort": "+ Pfad",
|
||||
"addSpine": "Fügen Sie Spine hinzu",
|
||||
"copyTitle": "Kopieren Sie das aktuell ausgewählte Szenenobjekt",
|
||||
"contextCopy": "In die Zwischenablage kopieren",
|
||||
"contextPaste": "Einfügen und überschreiben",
|
||||
"clipboardUnavailable": "Dieser Browser erlaubt keinen Zugriff auf die Zwischenablage",
|
||||
"clipboardInvalid": "Die Zwischenablage enthält kein gültiges Szenenobjekt",
|
||||
"clipboardCopyFailed": "Das Szenenobjekt konnte nicht in die Zwischenablage kopiert werden",
|
||||
"clipboardReadFailed": "Die Zwischenablage konnte nicht gelesen werden. Prüfen Sie die Browserberechtigungen.",
|
||||
"clipboardTypeMismatch": "Nur Objekte desselben Typs können überschrieben werden (Zwischenablage: {source}, aktuell: {target})",
|
||||
"clipboardPasteFailed": "Das Szenenobjekt konnte nicht wiederhergestellt werden",
|
||||
"showParticle": "Partikelsystem anzeigen",
|
||||
"hideParticle": "Partikelsystem ausblenden",
|
||||
"showCollider": "Collider anzeigen",
|
||||
@@ -802,6 +813,8 @@
|
||||
"eye": "kleine Augen",
|
||||
"eyeDesc": "Objekte einzeln ein-/ausblenden; Verborgene Partikelsysteme werden nicht exportiert.",
|
||||
"copyDesc": "Kopiert das aktuell ausgewählte Objekt vollständig, um den Vergleich verschiedener Parameter zu erleichtern.",
|
||||
"sceneClipboard": "Szenen-Zwischenablage",
|
||||
"sceneClipboardDesc": "Rechtsklick auf ein Szenenobjekt, um es zu kopieren oder durch Einfügen zu ersetzen.",
|
||||
"backgroundDesc": "Laden Sie das Referenzbild hoch und passen Sie Position und Transparenz nur für die Leinwandvorschau an.",
|
||||
"settingsDesc": "Das Layout der Verwaltungsoberfläche, die Zusatzanzeige, der Maßstab usw. werden automatisch im Browser gespeichert.",
|
||||
"undoDesc": "Ctrl+Z fährt einen Schritt zurück; Die maximale Schrittanzahl kann in den Systemeinstellungen angepasst werden.",
|
||||
|
||||
@@ -176,6 +176,9 @@
|
||||
"storageUnavailable": "Browser local storage is unavailable",
|
||||
"saved": "Saved to browser: {name}",
|
||||
"loaded": "Loaded from browser: {name}",
|
||||
"appendToScene": "Append this configuration to the scene",
|
||||
"appending": "Appending configuration: {name}",
|
||||
"appended": "Appended to scene: {name}",
|
||||
"deleted": "Deleted browser configuration: {name}",
|
||||
"downloadEmpty": "No configurations to download",
|
||||
"downloaded": "Downloaded: {name}"
|
||||
@@ -513,6 +516,14 @@
|
||||
"addPathShort": "+ Path",
|
||||
"addSpine": "Add Spine",
|
||||
"copyTitle": "Duplicate selected scene object",
|
||||
"contextCopy": "Copy to clipboard",
|
||||
"contextPaste": "Paste and overwrite",
|
||||
"clipboardUnavailable": "This browser does not allow clipboard access",
|
||||
"clipboardInvalid": "The clipboard does not contain a valid scene object",
|
||||
"clipboardCopyFailed": "Could not copy the scene object to the clipboard",
|
||||
"clipboardReadFailed": "Could not read the clipboard. Check browser permissions.",
|
||||
"clipboardTypeMismatch": "Only objects of the same type can be overwritten (clipboard: {source}, current: {target})",
|
||||
"clipboardPasteFailed": "Could not restore the scene object",
|
||||
"showParticle": "Show particle system",
|
||||
"hideParticle": "Hide particle system",
|
||||
"showCollider": "Show collider",
|
||||
@@ -825,6 +836,8 @@
|
||||
"eye": "Visibility",
|
||||
"eyeDesc": "Show or hide an object independently. Hidden particle systems are not exported.",
|
||||
"copyDesc": "Duplicate the selected object for parameter comparisons.",
|
||||
"sceneClipboard": "Scene clipboard",
|
||||
"sceneClipboardDesc": "Right-click a scene object to copy it to the clipboard or overwrite it by pasting.",
|
||||
"backgroundDesc": "Upload a reference image and adjust its position and opacity for canvas preview only.",
|
||||
"settingsDesc": "Manage layout, canvas guides, tick display, and other editor settings.",
|
||||
"undoDesc": "Ctrl+Z undoes one step. The maximum history size is configurable.",
|
||||
|
||||
@@ -164,6 +164,9 @@
|
||||
"storageUnavailable": "El almacenamiento local del navegador no está disponible",
|
||||
"saved": "Guardado en el navegador: {name}",
|
||||
"loaded": "Cargado desde el navegador: {name}",
|
||||
"appendToScene": "Añadir esta configuración a la escena",
|
||||
"appending": "Añadiendo configuración: {name}",
|
||||
"appended": "Añadido a la escena: {name}",
|
||||
"deleted": "Configuración eliminada del navegador: {name}",
|
||||
"downloadEmpty": "No hay configuraciones para descargar",
|
||||
"downloaded": "Descargado: {name}"
|
||||
@@ -501,6 +504,14 @@
|
||||
"addPathShort": "+ camino",
|
||||
"addSpine": "Añadir Spine",
|
||||
"copyTitle": "Copiar el objeto de escena seleccionado actualmente",
|
||||
"contextCopy": "Copiar al portapapeles",
|
||||
"contextPaste": "Pegar y sobrescribir",
|
||||
"clipboardUnavailable": "Este navegador no permite acceder al portapapeles",
|
||||
"clipboardInvalid": "El portapapeles no contiene un objeto de escena válido",
|
||||
"clipboardCopyFailed": "No se pudo copiar el objeto de escena al portapapeles",
|
||||
"clipboardReadFailed": "No se pudo leer el portapapeles. Comprueba los permisos del navegador.",
|
||||
"clipboardTypeMismatch": "Solo se pueden sobrescribir objetos del mismo tipo (portapapeles: {source}, actual: {target})",
|
||||
"clipboardPasteFailed": "No se pudo restaurar el objeto de escena",
|
||||
"showParticle": "Mostrar sistema de partículas",
|
||||
"hideParticle": "Ocultar sistema de partículas",
|
||||
"showCollider": "mostrar colisionadores",
|
||||
@@ -802,6 +813,8 @@
|
||||
"eye": "ojos pequeños",
|
||||
"eyeDesc": "Mostrar/ocultar objetos individualmente; Los sistemas de partículas ocultas no se exportan.",
|
||||
"copyDesc": "Copia completamente el objeto seleccionado actualmente para facilitar la comparación de diferentes parámetros.",
|
||||
"sceneClipboard": "Portapapeles de escena",
|
||||
"sceneClipboardDesc": "Haz clic derecho en un objeto de escena para copiarlo o sustituirlo pegando.",
|
||||
"backgroundDesc": "Cargue la imagen de referencia y ajuste la posición y la transparencia solo para la vista previa del lienzo.",
|
||||
"settingsDesc": "El diseño de la interfaz de administración, la pantalla auxiliar, la escala, etc. se guardan automáticamente en el navegador.",
|
||||
"undoDesc": "Ctrl+Z retrocede un paso; el número máximo de pasos se puede ajustar en la configuración del sistema.",
|
||||
|
||||
@@ -164,6 +164,9 @@
|
||||
"storageUnavailable": "Le stockage local du navigateur est indisponible",
|
||||
"saved": "Enregistré dans le navigateur : {name}",
|
||||
"loaded": "Chargé depuis le navigateur : {name}",
|
||||
"appendToScene": "Ajouter cette configuration à la scène",
|
||||
"appending": "Ajout de la configuration : {name}",
|
||||
"appended": "Ajouté à la scène : {name}",
|
||||
"deleted": "Configuration supprimée du navigateur : {name}",
|
||||
"downloadEmpty": "Aucune configuration à télécharger",
|
||||
"downloaded": "Téléchargé : {name}"
|
||||
@@ -501,6 +504,14 @@
|
||||
"addPathShort": "+ chemin",
|
||||
"addSpine": "Ajouter Spine",
|
||||
"copyTitle": "Copier l'objet de scène actuellement sélectionné",
|
||||
"contextCopy": "Copier dans le presse-papiers",
|
||||
"contextPaste": "Coller et remplacer",
|
||||
"clipboardUnavailable": "Ce navigateur n’autorise pas l’accès au presse-papiers",
|
||||
"clipboardInvalid": "Le presse-papiers ne contient aucun objet de scène valide",
|
||||
"clipboardCopyFailed": "Impossible de copier l’objet de scène dans le presse-papiers",
|
||||
"clipboardReadFailed": "Impossible de lire le presse-papiers. Vérifiez les autorisations du navigateur.",
|
||||
"clipboardTypeMismatch": "Seul un objet de même type peut être remplacé (presse-papiers : {source}, actuel : {target})",
|
||||
"clipboardPasteFailed": "Impossible de restaurer l’objet de scène",
|
||||
"showParticle": "Afficher le système de particules",
|
||||
"hideParticle": "Masquer le système de particules",
|
||||
"showCollider": "montrer les collisionneurs",
|
||||
@@ -802,6 +813,8 @@
|
||||
"eye": "petits yeux",
|
||||
"eyeDesc": "Afficher/masquer les objets individuellement ; les systèmes de particules cachées ne sont pas exportés.",
|
||||
"copyDesc": "Copie entièrement l'objet actuellement sélectionné pour faciliter la comparaison des différents paramètres.",
|
||||
"sceneClipboard": "Presse-papiers de scène",
|
||||
"sceneClipboardDesc": "Faites un clic droit sur un objet de scène pour le copier ou le remplacer par collage.",
|
||||
"backgroundDesc": "Téléchargez l'image de référence et ajustez la position et la transparence pour l'aperçu du canevas uniquement.",
|
||||
"settingsDesc": "La disposition de l'interface de gestion, l'affichage auxiliaire, la balance, etc. sont automatiquement enregistrés dans le navigateur.",
|
||||
"undoDesc": "Ctrl+Z se rétracte d'un pas ; le nombre maximum d'étapes peut être ajusté dans les paramètres du système.",
|
||||
|
||||
@@ -164,6 +164,9 @@
|
||||
"storageUnavailable": "ブラウザのローカルストレージを利用できません",
|
||||
"saved": "ブラウザに保存しました:{name}",
|
||||
"loaded": "ブラウザから読み込みました:{name}",
|
||||
"appendToScene": "この設定をシーンに追加",
|
||||
"appending": "設定を追加しています:{name}",
|
||||
"appended": "シーンに追加しました:{name}",
|
||||
"deleted": "ブラウザの設定を削除しました:{name}",
|
||||
"downloadEmpty": "ダウンロードできる設定がありません",
|
||||
"downloaded": "ダウンロードしました:{name}"
|
||||
@@ -501,6 +504,14 @@
|
||||
"addPathShort": "+ パス",
|
||||
"addSpine": "Spineを追加",
|
||||
"copyTitle": "現在選択されているシーン オブジェクトをコピーします",
|
||||
"contextCopy": "クリップボードへコピー",
|
||||
"contextPaste": "貼り付けて上書き",
|
||||
"clipboardUnavailable": "このブラウザではクリップボードにアクセスできません",
|
||||
"clipboardInvalid": "クリップボードに有効なシーンオブジェクトがありません",
|
||||
"clipboardCopyFailed": "シーンオブジェクトをクリップボードへコピーできませんでした",
|
||||
"clipboardReadFailed": "クリップボードを読み取れません。ブラウザの権限を確認してください。",
|
||||
"clipboardTypeMismatch": "同じ種類のシーンオブジェクトだけを上書きできます(クリップボード: {source}、現在: {target})",
|
||||
"clipboardPasteFailed": "シーンオブジェクトを復元できませんでした",
|
||||
"showParticle": "パーティクル システムを表示する",
|
||||
"hideParticle": "パーティクル システムを非表示にする",
|
||||
"showCollider": "コライダーを表示",
|
||||
@@ -802,6 +813,8 @@
|
||||
"eye": "小さな目",
|
||||
"eyeDesc": "オブジェクトを個別に表示/非表示にします。非表示のパーティクル システムはエクスポートされません。",
|
||||
"copyDesc": "現在選択されているオブジェクトを完全にコピーして、さまざまなパラメータの比較を容易にします。",
|
||||
"sceneClipboard": "シーンクリップボード",
|
||||
"sceneClipboardDesc": "シーンオブジェクトを右クリックすると、クリップボードへのコピーや貼り付けによる上書きができます。",
|
||||
"backgroundDesc": "参照画像をアップロードし、キャンバスプレビューのみ位置と透明度を調整します。",
|
||||
"settingsDesc": "管理インターフェイスのレイアウト、補助表示、スケールなどはブラウザに自動的に保存されます。",
|
||||
"undoDesc": "Ctrl+Z は 1 ステップ後退します。最大ステップ数はシステム設定で調整できます。",
|
||||
|
||||
@@ -164,6 +164,9 @@
|
||||
"storageUnavailable": "브라우저 로컬 저장소를 사용할 수 없습니다",
|
||||
"saved": "브라우저에 저장했습니다: {name}",
|
||||
"loaded": "브라우저에서 불러왔습니다: {name}",
|
||||
"appendToScene": "이 구성을 장면에 추가",
|
||||
"appending": "구성을 추가하는 중: {name}",
|
||||
"appended": "장면에 추가했습니다: {name}",
|
||||
"deleted": "브라우저 설정을 삭제했습니다: {name}",
|
||||
"downloadEmpty": "다운로드할 설정이 없습니다",
|
||||
"downloaded": "다운로드했습니다: {name}"
|
||||
@@ -501,6 +504,14 @@
|
||||
"addPathShort": "+ 경로",
|
||||
"addSpine": "Spine 추가",
|
||||
"copyTitle": "현재 선택된 장면 객체를 복사합니다.",
|
||||
"contextCopy": "클립보드로 복사",
|
||||
"contextPaste": "붙여넣어 덮어쓰기",
|
||||
"clipboardUnavailable": "이 브라우저에서는 클립보드에 접근할 수 없습니다",
|
||||
"clipboardInvalid": "클립보드에 사용할 수 있는 장면 객체가 없습니다",
|
||||
"clipboardCopyFailed": "장면 객체를 클립보드로 복사하지 못했습니다",
|
||||
"clipboardReadFailed": "클립보드를 읽을 수 없습니다. 브라우저 권한을 확인하세요.",
|
||||
"clipboardTypeMismatch": "같은 유형의 장면 객체만 덮어쓸 수 있습니다(클립보드: {source}, 현재: {target})",
|
||||
"clipboardPasteFailed": "장면 객체를 복원하지 못했습니다",
|
||||
"showParticle": "입자 시스템 표시",
|
||||
"hideParticle": "입자 시스템 숨기기",
|
||||
"showCollider": "충돌체 표시",
|
||||
@@ -802,6 +813,8 @@
|
||||
"eye": "작은 눈",
|
||||
"eyeDesc": "개체를 개별적으로 표시하거나 숨깁니다. 숨겨진 입자 시스템은 내보내지지 않습니다.",
|
||||
"copyDesc": "다양한 매개변수의 비교를 용이하게 하기 위해 현재 선택된 개체를 완전히 복사합니다.",
|
||||
"sceneClipboard": "장면 클립보드",
|
||||
"sceneClipboardDesc": "장면 객체를 마우스 오른쪽 버튼으로 클릭하여 클립보드로 복사하거나 붙여넣어 덮어쓸 수 있습니다.",
|
||||
"backgroundDesc": "참조 이미지를 업로드하고 캔버스 미리보기 전용 위치와 투명도를 조정하세요.",
|
||||
"settingsDesc": "관리 인터페이스 레이아웃, 보조 디스플레이, 규모 등은 브라우저에 자동으로 저장됩니다.",
|
||||
"undoDesc": "Ctrl+Z는 한 단계 후퇴합니다. 최대 단계 수는 시스템 설정에서 조정할 수 있습니다.",
|
||||
|
||||
@@ -164,6 +164,9 @@
|
||||
"storageUnavailable": "O armazenamento local do navegador não está disponível",
|
||||
"saved": "Salvo no navegador: {name}",
|
||||
"loaded": "Carregado do navegador: {name}",
|
||||
"appendToScene": "Adicionar esta configuração à cena",
|
||||
"appending": "Adicionando configuração: {name}",
|
||||
"appended": "Adicionado à cena: {name}",
|
||||
"deleted": "Configuração excluída do navegador: {name}",
|
||||
"downloadEmpty": "Nenhuma configuração para baixar",
|
||||
"downloaded": "Baixado: {name}"
|
||||
@@ -501,6 +504,14 @@
|
||||
"addPathShort": "+ caminho",
|
||||
"addSpine": "Adicionar Spine",
|
||||
"copyTitle": "Copie o objeto de cena atualmente selecionado",
|
||||
"contextCopy": "Copiar para a área de transferência",
|
||||
"contextPaste": "Colar e substituir",
|
||||
"clipboardUnavailable": "Este navegador não permite acessar a área de transferência",
|
||||
"clipboardInvalid": "A área de transferência não contém um objeto de cena válido",
|
||||
"clipboardCopyFailed": "Não foi possível copiar o objeto de cena para a área de transferência",
|
||||
"clipboardReadFailed": "Não foi possível ler a área de transferência. Verifique as permissões do navegador.",
|
||||
"clipboardTypeMismatch": "Somente objetos do mesmo tipo podem ser substituídos (área de transferência: {source}, atual: {target})",
|
||||
"clipboardPasteFailed": "Não foi possível restaurar o objeto de cena",
|
||||
"showParticle": "Mostrar sistema de partículas",
|
||||
"hideParticle": "Ocultar sistema de partículas",
|
||||
"showCollider": "mostrar colisores",
|
||||
@@ -802,6 +813,8 @@
|
||||
"eye": "olhos pequenos",
|
||||
"eyeDesc": "Mostrar/ocultar objetos individualmente; sistemas de partículas ocultas não são exportados.",
|
||||
"copyDesc": "Copia completamente o objeto atualmente selecionado para facilitar a comparação de diferentes parâmetros.",
|
||||
"sceneClipboard": "Área de transferência da cena",
|
||||
"sceneClipboardDesc": "Clique com o botão direito em um objeto de cena para copiá-lo ou substituí-lo colando.",
|
||||
"backgroundDesc": "Carregue a imagem de referência e ajuste a posição e a transparência apenas para visualização da tela.",
|
||||
"settingsDesc": "Layout da interface de gerenciamento, display auxiliar, balança, etc. são salvos automaticamente no navegador.",
|
||||
"undoDesc": "Ctrl+Z retrai um passo; o número máximo de etapas pode ser ajustado nas configurações do sistema.",
|
||||
|
||||
@@ -176,6 +176,9 @@
|
||||
"storageUnavailable": "Локальное хранилище браузера недоступно",
|
||||
"saved": "Сохранено в браузере: {name}",
|
||||
"loaded": "Загружено из браузера: {name}",
|
||||
"appendToScene": "Добавить эту конфигурацию в сцену",
|
||||
"appending": "Добавление конфигурации: {name}",
|
||||
"appended": "Добавлено в сцену: {name}",
|
||||
"deleted": "Удалено из кэша браузера: {name}",
|
||||
"downloadEmpty": "Нет конфигураций для скачивания",
|
||||
"downloaded": "Скачано: {name}"
|
||||
@@ -513,6 +516,14 @@
|
||||
"addPathShort": "+Путь",
|
||||
"addSpine": "Добавить Spine",
|
||||
"copyTitle": "Дублировать выбранный объект сцены",
|
||||
"contextCopy": "Копировать в буфер обмена",
|
||||
"contextPaste": "Вставить с заменой",
|
||||
"clipboardUnavailable": "Браузер не разрешает доступ к буферу обмена",
|
||||
"clipboardInvalid": "В буфере обмена нет допустимого объекта сцены",
|
||||
"clipboardCopyFailed": "Не удалось скопировать объект сцены в буфер обмена",
|
||||
"clipboardReadFailed": "Не удалось прочитать буфер обмена. Проверьте разрешения браузера.",
|
||||
"clipboardTypeMismatch": "Можно заменять только объект того же типа (в буфере: {source}, текущий: {target})",
|
||||
"clipboardPasteFailed": "Не удалось восстановить объект сцены",
|
||||
"showParticle": "Показать систему частиц",
|
||||
"hideParticle": "Скрыть систему частиц",
|
||||
"showCollider": "Показать коллайдер",
|
||||
@@ -825,6 +836,8 @@
|
||||
"eye": "Видимость",
|
||||
"eyeDesc": "Показывает или скрывает отдельный объект. Скрытые системы частиц не экспортируются.",
|
||||
"copyDesc": "Полностью дублирует выбранный объект для сравнения параметров.",
|
||||
"sceneClipboard": "Буфер объектов сцены",
|
||||
"sceneClipboardDesc": "Щёлкните объект сцены правой кнопкой, чтобы скопировать его в буфер или заменить вставкой.",
|
||||
"backgroundDesc": "Загружает опорное изображение и позволяет настроить его положение и непрозрачность только для предпросмотра на холсте.",
|
||||
"settingsDesc": "Управление расположением интерфейса, вспомогательными элементами, делениями и другими настройками; значения автоматически сохраняются в браузере.",
|
||||
"undoDesc": "Ctrl+Z отменяет одно действие. Максимальное число шагов задаётся в системных настройках.",
|
||||
|
||||
@@ -176,6 +176,9 @@
|
||||
"storageUnavailable": "浏览器本地存储不可用",
|
||||
"saved": "已保存到浏览器:{name}",
|
||||
"loaded": "已从浏览器加载:{name}",
|
||||
"appendToScene": "将当前配置追加到场景中",
|
||||
"appending": "正在追加配置:{name}",
|
||||
"appended": "已追加到场景:{name}",
|
||||
"deleted": "已删除缓存配置:{name}",
|
||||
"downloadEmpty": "没有可下载的配置",
|
||||
"downloaded": "已下载:{name}"
|
||||
@@ -513,6 +516,14 @@
|
||||
"addPathShort": "+路径",
|
||||
"addSpine": "添加 Spine",
|
||||
"copyTitle": "复制当前选中的场景对象",
|
||||
"contextCopy": "复制到剪贴板",
|
||||
"contextPaste": "粘贴覆盖",
|
||||
"clipboardUnavailable": "当前浏览器不允许访问剪贴板",
|
||||
"clipboardInvalid": "剪贴板中没有可用的场景对象",
|
||||
"clipboardCopyFailed": "复制场景对象到剪贴板失败",
|
||||
"clipboardReadFailed": "无法读取剪贴板,请检查浏览器权限",
|
||||
"clipboardTypeMismatch": "只能覆盖同类型场景对象(剪贴板为 {source},当前为 {target})",
|
||||
"clipboardPasteFailed": "场景对象恢复失败",
|
||||
"showParticle": "显示粒子系统",
|
||||
"hideParticle": "隐藏粒子系统",
|
||||
"showCollider": "显示碰撞体",
|
||||
@@ -825,6 +836,8 @@
|
||||
"eye": "小眼睛",
|
||||
"eyeDesc": "单独显示/隐藏对象;隐藏的粒子系统不会被导出。",
|
||||
"copyDesc": "完整复制当前选中对象,方便对比不同参数。",
|
||||
"sceneClipboard": "场景剪贴板",
|
||||
"sceneClipboardDesc": "右键场景对象可复制到剪贴板或覆盖粘贴。",
|
||||
"backgroundDesc": "上传参考图并调整位置和透明度,只用于画布预览。",
|
||||
"settingsDesc": "管理界面布局、辅助显示、刻度等,自动存入浏览器。",
|
||||
"undoDesc": "Ctrl+Z 撤回一步;最大步数可在系统设置中调整。",
|
||||
|
||||
@@ -164,6 +164,9 @@
|
||||
"storageUnavailable": "瀏覽器本機儲存空間無法使用",
|
||||
"saved": "已儲存到瀏覽器:{name}",
|
||||
"loaded": "已從瀏覽器載入:{name}",
|
||||
"appendToScene": "將目前設定追加到場景中",
|
||||
"appending": "正在追加設定:{name}",
|
||||
"appended": "已追加到場景:{name}",
|
||||
"deleted": "已刪除瀏覽器設定:{name}",
|
||||
"downloadEmpty": "沒有可下載的設定",
|
||||
"downloaded": "已下載:{name}"
|
||||
@@ -501,6 +504,14 @@
|
||||
"addPathShort": "+路徑",
|
||||
"addSpine": "加入 Spine",
|
||||
"copyTitle": "複製目前選取的場景對象",
|
||||
"contextCopy": "複製到剪貼簿",
|
||||
"contextPaste": "貼上覆蓋",
|
||||
"clipboardUnavailable": "目前瀏覽器不允許存取剪貼簿",
|
||||
"clipboardInvalid": "剪貼簿中沒有可用的場景物件",
|
||||
"clipboardCopyFailed": "無法複製場景物件到剪貼簿",
|
||||
"clipboardReadFailed": "無法讀取剪貼簿,請檢查瀏覽器權限",
|
||||
"clipboardTypeMismatch": "只能覆蓋同類型場景物件(剪貼簿為 {source},目前為 {target})",
|
||||
"clipboardPasteFailed": "場景物件還原失敗",
|
||||
"showParticle": "顯示粒子系統",
|
||||
"hideParticle": "隱藏粒子系統",
|
||||
"showCollider": "顯示碰撞體",
|
||||
@@ -802,6 +813,8 @@
|
||||
"eye": "小眼睛",
|
||||
"eyeDesc": "單獨顯示/隱藏物件;隱藏的粒子系統不會被匯出。",
|
||||
"copyDesc": "完整複製目前選取對象,方便比較不同參數。",
|
||||
"sceneClipboard": "場景剪貼簿",
|
||||
"sceneClipboardDesc": "在場景物件上按右鍵,即可複製到剪貼簿或貼上覆蓋。",
|
||||
"backgroundDesc": "上傳參考圖並調整位置和透明度,只用於畫布預覽。",
|
||||
"settingsDesc": "管理介面佈局、輔助顯示、刻度等,自動存入瀏覽器。",
|
||||
"undoDesc": "Ctrl+Z 撤回一步;最大步數可在系統設定中調整。",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
<dl class="help-list">
|
||||
<div><dt>{{ $t('help.eye') }}</dt><dd>{{ $t('help.eyeDesc') }}</dd></div>
|
||||
<div><dt>+Copy</dt><dd>{{ $t('help.copyDesc') }}</dd></div>
|
||||
<div><dt>{{ $t('help.sceneClipboard') }}</dt><dd>{{ $t('help.sceneClipboardDesc') }}</dd></div>
|
||||
<div><dt>{{ $t('toolbar.background') }}</dt><dd>{{ $t('help.backgroundDesc') }}</dd></div>
|
||||
<div><dt>{{ $t('toolbar.settings') }}</dt><dd>{{ $t('help.settingsDesc') }}</dd></div>
|
||||
<div><dt>{{ $t('help.undoShortcut') }}</dt><dd>{{ $t('help.undoDesc') }}</dd></div>
|
||||
|
||||
+16
-2
@@ -93,7 +93,7 @@
|
||||
<div class="tl-playhead" :style="{ left: playPx + 'px' }"></div>
|
||||
|
||||
<!-- 每系统一条轨道(粒子条) -->
|
||||
<div class="tl-lanes">
|
||||
<div class="tl-lanes" @wheel.stop="onLaneWheel">
|
||||
<div v-for="(sys, i) in store.systems" :key="sys.id" class="tl-lane">
|
||||
<span class="lane-ico" :class="{ 'sub-lane-ico': isSubEmitter(sys) }" :style="{ left: startMarkerPx(sys) + 'px' }">●</span>
|
||||
<!-- 粒子条:left=delay;红色底条=系统存在粒子的总时长;绿色覆盖层=持续发射时长 -->
|
||||
@@ -405,6 +405,13 @@ function onWheel(e: WheelEvent) {
|
||||
viewFrames.value = Math.round(nv)
|
||||
}
|
||||
|
||||
function onLaneWheel(e: WheelEvent) {
|
||||
const lanes = e.currentTarget as HTMLElement
|
||||
if (lanes.scrollHeight > lanes.clientHeight) return
|
||||
e.preventDefault()
|
||||
onWheel(e)
|
||||
}
|
||||
|
||||
// 拖动时间轴滑动(scrub):只允许进入实际播放范围。
|
||||
function onScrubDown(e: PointerEvent) {
|
||||
const vp = vpRef.value!
|
||||
@@ -580,7 +587,14 @@ function onSpineBarDown(e: PointerEvent, spine: SpineSceneObject) {
|
||||
.tl-tickline { position: absolute; top: 0; bottom: 0; width: 1px; background: #263040; }
|
||||
.tl-ticklabel { position: absolute; top: 2px; transform: translateX(-50%); font-size: 9px; color: #667; }
|
||||
.tl-playhead { position: absolute; top: 0; bottom: 0; width: 2px; background: #ffcc33; z-index: 5; pointer-events: none; }
|
||||
.tl-lanes { position: absolute; top: 22px; left: 0; right: 0; bottom: 0; }
|
||||
.tl-lanes {
|
||||
position: absolute; top: 22px; left: 0; right: 0; bottom: 0;
|
||||
overflow-x: hidden; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #46516b #0d0d14;
|
||||
}
|
||||
.tl-lanes::-webkit-scrollbar { width: 8px; }
|
||||
.tl-lanes::-webkit-scrollbar-track { background: #0d0d14; }
|
||||
.tl-lanes::-webkit-scrollbar-thumb { border-radius: 4px; background: #46516b; }
|
||||
.tl-lanes::-webkit-scrollbar-thumb:hover { background: #5d6a88; }
|
||||
.tl-lane { position: relative; height: 26px; margin-bottom: 4px; }
|
||||
.lane-ico { position: absolute; left: 4px; top: 6px; font-size: 11px; color: #dde; z-index: 2; pointer-events: none; }
|
||||
.lane-ico.sub-lane-ico { color: #7f8999; }
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
@dblclick="loadLocalProject(project)"
|
||||
@keydown.enter.prevent="loadLocalProject(project)"
|
||||
@keydown.space.prevent="selectLocalProject(project)"
|
||||
@contextmenu.prevent.stop="openProjectContextMenu($event, project)"
|
||||
>
|
||||
<span class="project-name">{{ project.name }}</span>
|
||||
<span v-if="store.settings.showProjectSize" class="project-size">{{ formatProjectSize(project.size) }}</span>
|
||||
@@ -95,10 +96,25 @@
|
||||
</form>
|
||||
</div>
|
||||
</Teleport>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="projectContextMenu.open"
|
||||
ref="projectContextMenuElement"
|
||||
class="project-context-menu"
|
||||
role="menu"
|
||||
:style="{ left: projectContextMenu.x + 'px', top: projectContextMenu.y + 'px' }"
|
||||
@pointerdown.stop
|
||||
@contextmenu.prevent
|
||||
>
|
||||
<button type="button" role="menuitem" @click="appendProjectFromContextMenu">{{ $t('config.local.appendToScene') }}</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { LocalProject } from '../../editor/projectLibrary'
|
||||
import NumSlider from '../NumSlider.vue'
|
||||
import CurveEditor from '../CurveEditor.vue'
|
||||
import ParticleAttributeControl from '../ParticleAttributeControl.vue'
|
||||
@@ -111,12 +127,65 @@ const {
|
||||
store,
|
||||
configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError,
|
||||
localProjects, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy,
|
||||
saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject,
|
||||
saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, appendLocalProject, downloadLocalProject,
|
||||
downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, projectFilesInput, loadAllProjectFiles,
|
||||
selectedLocalProjectName, selectLocalProject, onConfigFileChange, refreshPresets,
|
||||
loadSelectedPreset, collapsed, toggle, sectionOpen, select,
|
||||
} = useParticlePanelContext()
|
||||
const { locale } = useI18n()
|
||||
const projectContextMenuElement = ref<HTMLElement | null>(null)
|
||||
const projectContextMenu = reactive({
|
||||
open: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
project: null as LocalProject | null,
|
||||
})
|
||||
|
||||
function closeProjectContextMenu() {
|
||||
projectContextMenu.open = false
|
||||
projectContextMenu.project = null
|
||||
}
|
||||
|
||||
async function openProjectContextMenu(event: MouseEvent, project: LocalProject) {
|
||||
selectLocalProject(project)
|
||||
Object.assign(projectContextMenu, { open: true, x: event.clientX, y: event.clientY, project })
|
||||
await nextTick()
|
||||
const menu = projectContextMenuElement.value
|
||||
if (!menu) return
|
||||
const bounds = menu.getBoundingClientRect()
|
||||
projectContextMenu.x = Math.max(8, Math.min(event.clientX, window.innerWidth - bounds.width - 8))
|
||||
projectContextMenu.y = Math.max(8, Math.min(event.clientY, window.innerHeight - bounds.height - 8))
|
||||
}
|
||||
|
||||
function appendProjectFromContextMenu() {
|
||||
const project = projectContextMenu.project
|
||||
closeProjectContextMenu()
|
||||
if (project) void appendLocalProject(project)
|
||||
}
|
||||
|
||||
function onProjectMenuPointerDown(event: PointerEvent) {
|
||||
if (projectContextMenu.open && !projectContextMenuElement.value?.contains(event.target as Node)) {
|
||||
closeProjectContextMenu()
|
||||
}
|
||||
}
|
||||
|
||||
function onProjectMenuKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') closeProjectContextMenu()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('pointerdown', onProjectMenuPointerDown, true)
|
||||
window.addEventListener('keydown', onProjectMenuKeyDown, true)
|
||||
window.addEventListener('scroll', closeProjectContextMenu, true)
|
||||
window.addEventListener('blur', closeProjectContextMenu)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('pointerdown', onProjectMenuPointerDown, true)
|
||||
window.removeEventListener('keydown', onProjectMenuKeyDown, true)
|
||||
window.removeEventListener('scroll', closeProjectContextMenu, true)
|
||||
window.removeEventListener('blur', closeProjectContextMenu)
|
||||
})
|
||||
function formatProjectSize(bytes: number) {
|
||||
const kb = Math.max(1, bytes / 1024)
|
||||
if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`
|
||||
@@ -129,6 +198,18 @@ function presetDisplayName(preset: PresetOption) {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.project-context-menu {
|
||||
position: fixed; z-index: 10000; min-width: 180px; padding: 4px;
|
||||
border: 1px solid #3d4962; border-radius: 7px; background: #171d2a;
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, .48);
|
||||
}
|
||||
.project-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; white-space: nowrap; cursor: pointer;
|
||||
}
|
||||
.project-context-menu button:hover, .project-context-menu button:focus-visible {
|
||||
outline: none; background: #2d3953; color: #fff;
|
||||
}
|
||||
.config-management-body { padding-bottom: 2px; }
|
||||
.group-download {
|
||||
max-width: 50%; min-height: 24px; margin-left: auto; padding: 3px 8px; border: 1px solid #485673; border-radius: 5px;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -41,7 +41,7 @@ const { exportSettings, exportBusy, exportTarget, exportMessage, exportError, co
|
||||
const {
|
||||
configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError,
|
||||
localProjects, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy,
|
||||
saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject, downloadCurrentProject,
|
||||
saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, appendLocalProject, downloadLocalProject, downloadCurrentProject,
|
||||
removeLocalProject, downloadAllLocalProjects, projectFilesInput, loadAllProjectFiles,
|
||||
selectedLocalProjectName, selectLocalProject, onConfigFileChange, refreshPresets, loadSelectedPreset,
|
||||
} = useProjectPresets(store)
|
||||
@@ -657,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, setImageMode, 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 }
|
||||
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, appendLocalProject, 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, setImageMode, 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 }
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import ColorGradientEditor from '../ColorGradientEditor.vue'
|
||||
import ImageResourceCard from '../ImageResourceCard.vue'
|
||||
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
|
||||
import { createEditorConfig, loadEditorConfigFile, loadEditorConfigText, parseEditorConfig } from '../../editor/editorConfig'
|
||||
import { appendEditorConfigText } from '../../editor/appendSceneConfig'
|
||||
import { beginTransfer, readResponseTextWithProgress } from '../../editor/transferProgress'
|
||||
import { defaultSpineExportSettings, ensureSpineExportSettings } from '../../export/spineExportSettings'
|
||||
import { buildSpineJson, downloadSpineJson } from '../../export/spineJsonExporter'
|
||||
@@ -128,6 +129,18 @@ async function loadLocalProject(project: LocalProject) {
|
||||
}
|
||||
}
|
||||
|
||||
async function appendLocalProject(project: LocalProject) {
|
||||
configError.value = false
|
||||
configMessage.value = translate('config.local.appending', { name: project.name })
|
||||
try {
|
||||
await appendEditorConfigText(store, project.text)
|
||||
configMessage.value = translate('config.local.appended', { name: project.name })
|
||||
} catch (error) {
|
||||
configError.value = true
|
||||
configMessage.value = error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
||||
|
||||
function downloadLocalProject(project: LocalProject) {
|
||||
const fileName = projectJsonFileName(project.name)
|
||||
downloadTextFile(project.text, fileName)
|
||||
@@ -280,7 +293,7 @@ onBeforeUnmount(() => window.removeEventListener(PROJECT_SAVE_EVENT, saveConfig)
|
||||
return {
|
||||
configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError,
|
||||
localProjects, selectedLocalProjectName, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy,
|
||||
saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject,
|
||||
saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, appendLocalProject, downloadLocalProject,
|
||||
downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, projectFilesInput, loadAllProjectFiles,
|
||||
selectLocalProject, onConfigFileChange, refreshPresets, loadSelectedPreset,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user