支持spine进行绘制
This commit is contained in:
1 parent
266c353fd2
commit
e89eeaa502
19 files changed
+310
-18
No files matched your search
+10
-10
@@ -2,15 +2,15 @@
|
||||
"enabled": true,
|
||||
"scrollDurationSeconds": 24,
|
||||
"messages": {
|
||||
"zh-CN": "2026-11-04更新:修正路径跟随导出效果。",
|
||||
"zh-TW": "2026-11-04更新:修正路徑跟隨匯出效果。",
|
||||
"en": "Updated 2026-11-04: fixed path-following in exports.",
|
||||
"ja": "2026-11-04 更新:パス追従のエクスポート結果を修正しました。",
|
||||
"ko": "2026-11-04 업데이트: 경로 추적 내보내기 결과를 수정했습니다.",
|
||||
"ru": "Обновление от 04.11.2026: исправлен экспорт со следованием по пути.",
|
||||
"fr": "Mise à jour du 04/11/2026 : correction du suivi de trajectoire lors de l’exportation.",
|
||||
"de": "Update vom 04.11.2026: Der Export mit Pfadverfolgung wurde korrigiert.",
|
||||
"pt-BR": "Atualização de 04/11/2026: corrigida a exportação com acompanhamento de trajetória.",
|
||||
"es": "Actualización del 04/11/2026: corregida la exportación con seguimiento de trayectoria."
|
||||
"zh-CN": "2026-11-10更新:Spine 显示支持骨架、蒙皮区域和裁切绘制",
|
||||
"zh-TW": "2026-11-10更新:Spine 顯示支援骨架、蒙皮區域與裁切繪製",
|
||||
"en": "Updated 2026-11-10: Spine display supports skeleton, skinned regions, and clipping rendering.",
|
||||
"ja": "2026-11-10 更新:Spine 表示でスケルトン、スキン領域、クリッピング描画に対応しました。",
|
||||
"ko": "2026-11-10 업데이트: Spine 표시에서 스켈레톤, 스킨 영역 및 클리핑 렌더링을 지원합니다.",
|
||||
"ru": "Обновление от 10.11.2026: отображение Spine поддерживает скелет, области скина и отрисовку обрезки.",
|
||||
"fr": "Mise à jour du 10/11/2026 : l’affichage Spine prend en charge le squelette, les zones de peau et le rendu des masques.",
|
||||
"de": "Update vom 10.11.2026: Die Spine-Anzeige unterstützt Skelett, Skin-Bereiche und Clipping-Darstellung.",
|
||||
"pt-BR": "Atualização de 10/11/2026: a exibição do Spine agora oferece suporte a esqueleto, áreas de pele e renderização de recorte.",
|
||||
"es": "Actualización del 10/11/2026: la visualización de Spine admite esqueleto, áreas de piel y representación de recortes."
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CollisionBody, ParticleSystem, ScenePath, useParticleStore } from '../store/particleStore'
|
||||
import type { SpineSceneObject } from '../spine/spineTypes'
|
||||
import { ensureSpineDisplaySettings, type SpineSceneObject } from '../spine/spineTypes'
|
||||
import { ensureEmitterConfig } from '../core/particleEmitter'
|
||||
import { registerSpineAsset } from '../spine/spineAssetRegistry'
|
||||
import { translate } from '../i18n'
|
||||
@@ -52,6 +52,7 @@ export async function appendEditorConfigText(store: EditorStore, text: string) {
|
||||
for (const spine of spines) {
|
||||
if (spine.sourceBundle == null) spine.sourceBundle = null
|
||||
if (spine.dataVersion == null) spine.dataVersion = ''
|
||||
ensureSpineDisplaySettings(spine)
|
||||
if (!Array.isArray(spine.slots)) spine.slots = []
|
||||
if (typeof spine.selectedBone !== 'string') spine.selectedBone = ''
|
||||
if (typeof spine.selectedSlot !== 'string') spine.selectedSlot = ''
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
SettingsState,
|
||||
useParticleStore,
|
||||
} from '../store/particleStore'
|
||||
import type { SpineSceneObject } from '../spine/spineTypes'
|
||||
import { ensureSpineDisplaySettings, type SpineSceneObject } from '../spine/spineTypes'
|
||||
import { parseSpineBundle, type ParsedSpineBundle } from '../spine/spineAssetLoader'
|
||||
import { restoreSpineSource } from '../spine/spineSourceBundle'
|
||||
import { registerSpineAsset, releaseSpineAsset } from '../spine/spineAssetRegistry'
|
||||
@@ -268,6 +268,7 @@ export async function applyEditorConfig(store: EditorStore, input: EditorConfigF
|
||||
for (const spine of spines) {
|
||||
if (spine.sourceBundle == null) spine.sourceBundle = null
|
||||
if (spine.dataVersion == null) spine.dataVersion = ''
|
||||
ensureSpineDisplaySettings(spine)
|
||||
if (!Array.isArray(spine.slots)) spine.slots = []
|
||||
if (typeof spine.selectedBone !== 'string') spine.selectedBone = ''
|
||||
if (typeof spine.selectedSlot !== 'string') spine.selectedSlot = ''
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
ScenePath,
|
||||
useParticleStore,
|
||||
} from '../store/particleStore'
|
||||
import type { SpineSceneObject } from '../spine/spineTypes'
|
||||
import { ensureSpineDisplaySettings, type SpineSceneObject } from '../spine/spineTypes'
|
||||
import { registerSpineAsset, releaseSpineAsset } from '../spine/spineAssetRegistry'
|
||||
import { ensureEmitterConfig } from '../core/particleEmitter'
|
||||
import { collectEmitterAncestors } from '../core/subEmitterIds'
|
||||
@@ -237,6 +237,7 @@ export async function pasteSceneObjectFromClipboard(store: EditorStore, type: Sc
|
||||
const spine = cloneEditorValue(payload.object as SpineSceneObject)
|
||||
spine.id = target.id
|
||||
spine.name = target.name
|
||||
ensureSpineDisplaySettings(spine)
|
||||
spine.selectedBone = ''
|
||||
spine.selectedSlot = ''
|
||||
spine.assetVersion = target.assetVersion + 1
|
||||
|
||||
@@ -735,6 +735,10 @@
|
||||
},
|
||||
"spine": {
|
||||
"title": "Spine-Eigenschaften",
|
||||
"skeleton": "Bone",
|
||||
"onlySkinMesh": "Mesh",
|
||||
"allRegions": "Region",
|
||||
"allClipping": "Clipping",
|
||||
"name": "Spine-Name",
|
||||
"xPosition": "X-Position",
|
||||
"yPosition": "Y-Position",
|
||||
@@ -796,6 +800,9 @@
|
||||
"tabsLabel": "Hilfebereiche",
|
||||
"updates": "Versionshinweise",
|
||||
"updatesSubtitle": "Aktuelle Funktionsupdates anzeigen",
|
||||
"releaseSummaryV11110": "V1.11.10 aktualisiert",
|
||||
"releaseHeadingV11110": "Update vom 10.11.2026:",
|
||||
"releaseSpineHelpers": "Die Spine-Anzeige unterstützt Skelett, Skin-Bereiche und Clipping-Darstellung.",
|
||||
"releaseSummaryV11109": "V1.11.09 aktualisiert",
|
||||
"releaseHeadingV11109": "Update vom 09.11.2026:",
|
||||
"releaseSceneClipboard": "Kopieren und Einfügen einzelner Szenenobjekte zur Wiederverwendung hinzugefügt.",
|
||||
|
||||
@@ -758,6 +758,10 @@
|
||||
},
|
||||
"spine": {
|
||||
"title": "Spine Properties",
|
||||
"skeleton": "Bone",
|
||||
"onlySkinMesh": "Mesh",
|
||||
"allRegions": "Region",
|
||||
"allClipping": "Clipping",
|
||||
"name": "Spine name",
|
||||
"xPosition": "X position",
|
||||
"yPosition": "Y position",
|
||||
@@ -819,6 +823,9 @@
|
||||
"tabsLabel": "Help sections",
|
||||
"updates": "Release Notes",
|
||||
"updatesSubtitle": "View recent feature updates",
|
||||
"releaseSummaryV11110": "Updated to V1.11.10",
|
||||
"releaseHeadingV11110": "Updated 2026-11-10:",
|
||||
"releaseSpineHelpers": "Spine display supports skeleton, skinned regions, and clipping rendering.",
|
||||
"releaseSummaryV11109": "Updated to V1.11.09",
|
||||
"releaseHeadingV11109": "Updated 2026-11-09:",
|
||||
"releaseSceneClipboard": "Added copy and paste for individual scene objects.",
|
||||
|
||||
@@ -735,6 +735,10 @@
|
||||
},
|
||||
"spine": {
|
||||
"title": "Propiedades de Spine",
|
||||
"skeleton": "Bone",
|
||||
"onlySkinMesh": "Mesh",
|
||||
"allRegions": "Region",
|
||||
"allClipping": "Clipping",
|
||||
"name": "Nombre Spine",
|
||||
"xPosition": "Posición X",
|
||||
"yPosition": "Posición Y",
|
||||
@@ -796,6 +800,9 @@
|
||||
"tabsLabel": "Secciones de ayuda",
|
||||
"updates": "Actualizaciones de versión",
|
||||
"updatesSubtitle": "Ver actualizaciones recientes de funciones",
|
||||
"releaseSummaryV11110": "Actualizado a V1.11.10",
|
||||
"releaseHeadingV11110": "Actualización del 10/11/2026:",
|
||||
"releaseSpineHelpers": "La visualización de Spine admite esqueleto, áreas de piel y representación de recortes.",
|
||||
"releaseSummaryV11109": "Actualizado a V1.11.09",
|
||||
"releaseHeadingV11109": "Actualización del 09/11/2026:",
|
||||
"releaseSceneClipboard": "Añadida la copia y el pegado de objetos de escena individuales para reutilizarlos.",
|
||||
|
||||
@@ -735,6 +735,10 @@
|
||||
},
|
||||
"spine": {
|
||||
"title": "Propriétés Spine",
|
||||
"skeleton": "Bone",
|
||||
"onlySkinMesh": "Mesh",
|
||||
"allRegions": "Region",
|
||||
"allClipping": "Clipping",
|
||||
"name": "Nom Spine",
|
||||
"xPosition": "Position X",
|
||||
"yPosition": "Position Y",
|
||||
@@ -796,6 +800,9 @@
|
||||
"tabsLabel": "Sections de l’aide",
|
||||
"updates": "Mises à jour",
|
||||
"updatesSubtitle": "Voir les mises à jour récentes des fonctionnalités",
|
||||
"releaseSummaryV11110": "Mis à jour vers V1.11.10",
|
||||
"releaseHeadingV11110": "Mise à jour du 10/11/2026 :",
|
||||
"releaseSpineHelpers": "L’affichage Spine prend en charge le squelette, les zones de peau et le rendu des masques.",
|
||||
"releaseSummaryV11109": "Mis à jour vers V1.11.09",
|
||||
"releaseHeadingV11109": "Mise à jour du 09/11/2026 :",
|
||||
"releaseSceneClipboard": "Ajout de la copie et du collage d’objets de scène individuels pour les réutiliser.",
|
||||
|
||||
@@ -735,6 +735,10 @@
|
||||
},
|
||||
"spine": {
|
||||
"title": "Spine プロパティ",
|
||||
"skeleton": "Bone",
|
||||
"onlySkinMesh": "Mesh",
|
||||
"allRegions": "Region",
|
||||
"allClipping": "Clipping",
|
||||
"name": "Spine 名前",
|
||||
"xPosition": "X 位置",
|
||||
"yPosition": "Y 位置",
|
||||
@@ -796,6 +800,9 @@
|
||||
"tabsLabel": "ヘルプのセクション",
|
||||
"updates": "バージョン更新",
|
||||
"updatesSubtitle": "最近の機能更新を見る",
|
||||
"releaseSummaryV11110": "V1.11.10 更新済み",
|
||||
"releaseHeadingV11110": "2026-11-10 更新:",
|
||||
"releaseSpineHelpers": "Spine 表示でスケルトン、スキン領域、クリッピング描画に対応しました。",
|
||||
"releaseSummaryV11109": "V1.11.09 更新済み",
|
||||
"releaseHeadingV11109": "2026-11-09 更新:",
|
||||
"releaseSceneClipboard": "個別のシーンオブジェクトを再利用できるコピー&ペーストを追加しました。",
|
||||
|
||||
@@ -735,6 +735,10 @@
|
||||
},
|
||||
"spine": {
|
||||
"title": "Spine 속성",
|
||||
"skeleton": "Bone",
|
||||
"onlySkinMesh": "Mesh",
|
||||
"allRegions": "Region",
|
||||
"allClipping": "Clipping",
|
||||
"name": "Spine 이름",
|
||||
"xPosition": "X 위치",
|
||||
"yPosition": "Y 위치",
|
||||
@@ -796,6 +800,9 @@
|
||||
"tabsLabel": "도움말 섹션",
|
||||
"updates": "버전 업데이트",
|
||||
"updatesSubtitle": "최근 기능 업데이트 보기",
|
||||
"releaseSummaryV11110": "V1.11.10 업데이트됨",
|
||||
"releaseHeadingV11110": "2026-11-10 업데이트:",
|
||||
"releaseSpineHelpers": "Spine 표시에서 스켈레톤, 스킨 영역 및 클리핑 렌더링을 지원합니다.",
|
||||
"releaseSummaryV11109": "V1.11.09 업데이트됨",
|
||||
"releaseHeadingV11109": "2026-11-09 업데이트:",
|
||||
"releaseSceneClipboard": "개별 장면 객체를 재사용할 수 있는 복사 및 붙여넣기를 추가했습니다.",
|
||||
|
||||
@@ -735,6 +735,10 @@
|
||||
},
|
||||
"spine": {
|
||||
"title": "Propriedades do Spine",
|
||||
"skeleton": "Bone",
|
||||
"onlySkinMesh": "Mesh",
|
||||
"allRegions": "Region",
|
||||
"allClipping": "Clipping",
|
||||
"name": "Nome Spine",
|
||||
"xPosition": "Posição X",
|
||||
"yPosition": "Posição Y",
|
||||
@@ -796,6 +800,9 @@
|
||||
"tabsLabel": "Seções da ajuda",
|
||||
"updates": "Atualizações da versão",
|
||||
"updatesSubtitle": "Ver atualizações recentes de recursos",
|
||||
"releaseSummaryV11110": "Atualizado para V1.11.10",
|
||||
"releaseHeadingV11110": "Atualização de 10/11/2026:",
|
||||
"releaseSpineHelpers": "A exibição do Spine agora oferece suporte a esqueleto, áreas de pele e renderização de recorte.",
|
||||
"releaseSummaryV11109": "Atualizado para V1.11.09",
|
||||
"releaseHeadingV11109": "Atualização de 09/11/2026:",
|
||||
"releaseSceneClipboard": "Adicionada cópia e colagem de objetos de cena individuais para reutilização.",
|
||||
|
||||
@@ -758,6 +758,10 @@
|
||||
},
|
||||
"spine": {
|
||||
"title": "Свойства Spine",
|
||||
"skeleton": "Bone",
|
||||
"onlySkinMesh": "Mesh",
|
||||
"allRegions": "Region",
|
||||
"allClipping": "Clipping",
|
||||
"name": "Имя Spine",
|
||||
"xPosition": "Позиция X",
|
||||
"yPosition": "Позиция Y",
|
||||
@@ -819,6 +823,9 @@
|
||||
"tabsLabel": "Разделы справки",
|
||||
"updates": "Обновления версии",
|
||||
"updatesSubtitle": "Последние обновления функций",
|
||||
"releaseSummaryV11110": "Обновлено до V1.11.10",
|
||||
"releaseHeadingV11110": "Обновление от 10.11.2026:",
|
||||
"releaseSpineHelpers": "Отображение Spine поддерживает скелет, области скина и отрисовку обрезки.",
|
||||
"releaseSummaryV11109": "Обновлено до V1.11.09",
|
||||
"releaseHeadingV11109": "Обновление от 09.11.2026:",
|
||||
"releaseSceneClipboard": "Добавлены копирование и вставка отдельных объектов сцены для их повторного использования.",
|
||||
|
||||
@@ -758,6 +758,10 @@
|
||||
},
|
||||
"spine": {
|
||||
"title": "Spine 属性",
|
||||
"skeleton": "Bone",
|
||||
"onlySkinMesh": "Mesh",
|
||||
"allRegions": "Region",
|
||||
"allClipping": "Clipping",
|
||||
"name": "Spine 名称",
|
||||
"xPosition": "X 位置",
|
||||
"yPosition": "Y 位置",
|
||||
@@ -819,6 +823,9 @@
|
||||
"tabsLabel": "使用说明栏目",
|
||||
"updates": "版本更新",
|
||||
"updatesSubtitle": "查看近期功能更新记录",
|
||||
"releaseSummaryV11110": "V1.11.10 已更新",
|
||||
"releaseHeadingV11110": "2026-11-10更新:",
|
||||
"releaseSpineHelpers": "Spine 显示支持骨架、蒙皮区域和裁切绘制",
|
||||
"releaseSummaryV11109": "V1.11.09 已更新",
|
||||
"releaseHeadingV11109": "2026-11-09更新:",
|
||||
"releaseSceneClipboard": "新增场景对象的复制与粘贴,方便单独对场景对象进行复用",
|
||||
|
||||
@@ -735,6 +735,10 @@
|
||||
},
|
||||
"spine": {
|
||||
"title": "Spine 屬性",
|
||||
"skeleton": "Bone",
|
||||
"onlySkinMesh": "Mesh",
|
||||
"allRegions": "Region",
|
||||
"allClipping": "Clipping",
|
||||
"name": "Spine 名稱",
|
||||
"xPosition": "X 位置",
|
||||
"yPosition": "Y 位置",
|
||||
@@ -796,6 +800,9 @@
|
||||
"tabsLabel": "使用說明欄目",
|
||||
"updates": "版本更新",
|
||||
"updatesSubtitle": "查看近期功能更新記錄",
|
||||
"releaseSummaryV11110": "V1.11.10 已更新",
|
||||
"releaseHeadingV11110": "2026-11-10更新:",
|
||||
"releaseSpineHelpers": "Spine 顯示支援骨架、蒙皮區域與裁切繪製",
|
||||
"releaseSummaryV11109": "V1.11.09 已更新",
|
||||
"releaseHeadingV11109": "2026-11-09更新:",
|
||||
"releaseSceneClipboard": "新增場景物件的複製與貼上,方便單獨重複使用場景物件",
|
||||
|
||||
@@ -1,6 +1,78 @@
|
||||
<template>
|
||||
<div class="spine-editor" data-matomo-section="spine">
|
||||
<div class="title-row">
|
||||
<div class="editor-title">{{ $t('spine.title') }}</div>
|
||||
<div class="debug-switches">
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
class="debug-toggle"
|
||||
:class="{ on: spineObject.showBones }"
|
||||
:aria-checked="spineObject.showBones"
|
||||
:aria-label="$t('spine.skeleton')"
|
||||
:title="$t('spine.skeleton')"
|
||||
@click="spineObject.showBones = !spineObject.showBones"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" />
|
||||
<circle cx="12" cy="12" r="2.8" />
|
||||
<path v-if="!spineObject.showBones" class="eye-slash" d="M4 4l16 16" />
|
||||
</svg>
|
||||
<span>{{ $t('spine.skeleton') }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
class="debug-toggle"
|
||||
:class="{ on: spineObject.showOnlySkinMesh }"
|
||||
:aria-checked="spineObject.showOnlySkinMesh"
|
||||
:aria-label="$t('spine.onlySkinMesh')"
|
||||
:title="$t('spine.onlySkinMesh')"
|
||||
@click="spineObject.showOnlySkinMesh = !spineObject.showOnlySkinMesh"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" />
|
||||
<circle cx="12" cy="12" r="2.8" />
|
||||
<path v-if="!spineObject.showOnlySkinMesh" class="eye-slash" d="M4 4l16 16" />
|
||||
</svg>
|
||||
<span>{{ $t('spine.onlySkinMesh') }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
class="debug-toggle"
|
||||
:class="{ on: spineObject.showRegionAttachments }"
|
||||
:aria-checked="spineObject.showRegionAttachments"
|
||||
:aria-label="$t('spine.allRegions')"
|
||||
:title="$t('spine.allRegions')"
|
||||
@click="spineObject.showRegionAttachments = !spineObject.showRegionAttachments"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" />
|
||||
<circle cx="12" cy="12" r="2.8" />
|
||||
<path v-if="!spineObject.showRegionAttachments" class="eye-slash" d="M4 4l16 16" />
|
||||
</svg>
|
||||
<span>{{ $t('spine.allRegions') }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
class="debug-toggle"
|
||||
:class="{ on: spineObject.showClippingAttachments }"
|
||||
:aria-checked="spineObject.showClippingAttachments"
|
||||
:aria-label="$t('spine.allClipping')"
|
||||
:title="$t('spine.allClipping')"
|
||||
@click="spineObject.showClippingAttachments = !spineObject.showClippingAttachments"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" />
|
||||
<circle cx="12" cy="12" r="2.8" />
|
||||
<path v-if="!spineObject.showClippingAttachments" class="eye-slash" d="M4 4l16 16" />
|
||||
</svg>
|
||||
<span>{{ $t('spine.allClipping') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="field wide"><span>{{ $t('spine.name') }}</span><input v-model="spineObject.name" /></label>
|
||||
<div class="grid three">
|
||||
<label class="field"><span>{{ $t('spine.xPosition') }}</span><input v-model.number="spineObject.x" type="number" /></label>
|
||||
@@ -147,7 +219,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useParticleStore } from '../store/particleStore'
|
||||
import { parseSpineBundle } from './spineAssetLoader'
|
||||
import { registerSpineAsset, releaseSpineAsset } from './spineAssetRegistry'
|
||||
import type { SpineSceneObject } from './spineTypes'
|
||||
import { ensureSpineDisplaySettings, type SpineSceneObject } from './spineTypes'
|
||||
import { embedSpineSource } from './spineSourceBundle'
|
||||
|
||||
const props = defineProps<{ spineObject: SpineSceneObject }>()
|
||||
@@ -168,6 +240,7 @@ const textureFiles = ref<File[]>([])
|
||||
// 兼容热更新或旧场景数据;骨骼选择默认保持为空。
|
||||
if (props.spineObject.selectedBone == null) props.spineObject.selectedBone = ''
|
||||
if (props.spineObject.selectedSlot == null) props.spineObject.selectedSlot = ''
|
||||
ensureSpineDisplaySettings(props.spineObject)
|
||||
if (!Array.isArray(props.spineObject.slots)) props.spineObject.slots = []
|
||||
if (props.spineObject.sourceBundle == null) props.spineObject.sourceBundle = null
|
||||
if (props.spineObject.dataVersion == null) props.spineObject.dataVersion = ''
|
||||
@@ -341,8 +414,13 @@ function clearBundle() {
|
||||
|
||||
<style scoped>
|
||||
.spine-editor { display: grid; gap: 10px; color: var(--ui-text-body); }
|
||||
.title-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding-bottom: 9px; border-bottom: 1px solid var(--ui-border-strong); flex-wrap: wrap; }
|
||||
.editor-title,.section-title { color: var(--ui-text-strong); font-weight: 700; }
|
||||
.editor-title { padding-bottom: 9px; border-bottom: 1px solid var(--ui-border-strong); }
|
||||
.debug-switches { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px; }
|
||||
.debug-toggle { display: inline-flex; height: 27px; align-items: center; gap: 4px; padding: 0 7px; color: var(--ui-text-faint); background: transparent; border-color: var(--ui-border-strong); font-size: 10px; }
|
||||
.debug-toggle.on { color: var(--ui-accent-text-soft); background: var(--ui-accent-soft); border-color: var(--ui-accent-strong); }
|
||||
.debug-toggle svg { display: block; width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.debug-toggle .eye-slash { stroke: #d47b88; stroke-width: 2; }
|
||||
.grid { display: grid; gap: 8px; }
|
||||
.grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.field { display: grid; gap: 5px; min-width: 0; color: var(--ui-text-secondary); font-size: 12px; }
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
import { Container, Matrix } from 'pixi.js'
|
||||
import { Container, Graphics, Matrix } from 'pixi.js'
|
||||
import { getSpineAsset } from './spineAssetRegistry'
|
||||
import type { SpineSceneObject } from './spineTypes'
|
||||
|
||||
interface RuntimeEntry {
|
||||
root: Container
|
||||
spine: any
|
||||
skeletonDebug: Graphics
|
||||
skinMeshDebug: Graphics
|
||||
runtimeVersion: '4.2' | '4.3'
|
||||
assetVersion: number
|
||||
animation: string
|
||||
}
|
||||
|
||||
function attachmentDebugColor(key: string) {
|
||||
let hash = 2166136261
|
||||
for (let i = 0; i < key.length; i++) hash = Math.imul(hash ^ key.charCodeAt(i), 16777619)
|
||||
const hue = (hash >>> 0) % 360
|
||||
const segment = hue / 60
|
||||
const chroma = 0.78
|
||||
const x = chroma * (1 - Math.abs(segment % 2 - 1))
|
||||
const offset = 1 - chroma
|
||||
const [red, green, blue] = segment < 1 ? [chroma, x, 0]
|
||||
: segment < 2 ? [x, chroma, 0]
|
||||
: segment < 3 ? [0, chroma, x]
|
||||
: segment < 4 ? [0, x, chroma]
|
||||
: segment < 5 ? [x, 0, chroma]
|
||||
: [chroma, 0, x]
|
||||
return (Math.round((red + offset) * 255) << 16)
|
||||
| (Math.round((green + offset) * 255) << 8)
|
||||
| Math.round((blue + offset) * 255)
|
||||
}
|
||||
|
||||
export interface SpineBoneWorldTransform {
|
||||
x: number
|
||||
y: number
|
||||
@@ -91,6 +112,90 @@ export class SpineRuntimeLayer {
|
||||
entry.root.destroy({ children: true })
|
||||
}
|
||||
|
||||
private drawDebug(entry: RuntimeEntry, object: SpineSceneObject) {
|
||||
const { skeletonDebug, skinMeshDebug, root, spine } = entry
|
||||
skeletonDebug.clear()
|
||||
skinMeshDebug.clear()
|
||||
skeletonDebug.visible = object.showBones
|
||||
skinMeshDebug.visible = object.showOnlySkinMesh
|
||||
|| object.showRegionAttachments
|
||||
|| object.showClippingAttachments
|
||||
if (!object.showBones && !skinMeshDebug.visible) return
|
||||
|
||||
const screenScale = Math.max(
|
||||
0.0001,
|
||||
Math.abs(root.scale.x * (this.container.parent?.scale.x ?? 1)),
|
||||
Math.abs(root.scale.y * (this.container.parent?.scale.y ?? 1)),
|
||||
)
|
||||
const lineWidth = 1 / screenScale
|
||||
const skeleton = spine.skeleton
|
||||
const offsetX = Number(skeleton.x) || 0
|
||||
const offsetY = Number(skeleton.y) || 0
|
||||
|
||||
if (object.showBones) {
|
||||
skeletonDebug.lineStyle(lineWidth, 0x55d6ff, 0.95)
|
||||
for (const bone of skeleton.bones) {
|
||||
if (!bone.active || !bone.parent?.active) continue
|
||||
skeletonDebug.moveTo(offsetX + bone.parent.worldX, offsetY + bone.parent.worldY)
|
||||
skeletonDebug.lineTo(offsetX + bone.worldX, offsetY + bone.worldY)
|
||||
}
|
||||
skeletonDebug.beginFill(0xb8efff, 0.95)
|
||||
for (const bone of skeleton.bones) {
|
||||
if (bone.active) skeletonDebug.drawCircle(offsetX + bone.worldX, offsetY + bone.worldY, 2 / screenScale)
|
||||
}
|
||||
skeletonDebug.endFill()
|
||||
}
|
||||
|
||||
if (!object.showOnlySkinMesh && !object.showRegionAttachments && !object.showClippingAttachments) return
|
||||
const activeSlots = skeleton.slots.filter((slot: any) => slot.bone?.active)
|
||||
for (const slot of activeSlots) {
|
||||
const attachment = slot.getAttachment()
|
||||
if (!attachment) continue
|
||||
const color = attachmentDebugColor(`${slot.data.index}:${attachment.name}`)
|
||||
const isMesh = Array.isArray(attachment.triangles) && attachment.worldVerticesLength > 0
|
||||
if (isMesh) {
|
||||
if (!object.showOnlySkinMesh) continue
|
||||
skinMeshDebug.lineStyle(lineWidth, color, 0.9)
|
||||
skinMeshDebug.beginFill(color, 0.18)
|
||||
const vertices = new Float32Array(attachment.worldVerticesLength)
|
||||
attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, vertices, 0, 2)
|
||||
for (let i = 0; i < attachment.triangles.length; i += 3) {
|
||||
const a = attachment.triangles[i] * 2
|
||||
const b = attachment.triangles[i + 1] * 2
|
||||
const c = attachment.triangles[i + 2] * 2
|
||||
skinMeshDebug.drawPolygon([
|
||||
vertices[a], vertices[a + 1],
|
||||
vertices[b], vertices[b + 1],
|
||||
vertices[c], vertices[c + 1],
|
||||
])
|
||||
}
|
||||
skinMeshDebug.endFill()
|
||||
continue
|
||||
}
|
||||
|
||||
if (!object.showRegionAttachments
|
||||
|| !attachment.offset
|
||||
|| !Number.isFinite(attachment.width)
|
||||
|| !Number.isFinite(attachment.height)
|
||||
|| typeof attachment.computeWorldVertices !== 'function') {
|
||||
if (object.showClippingAttachments
|
||||
&& attachment.endSlot !== undefined
|
||||
&& attachment.worldVerticesLength > 0
|
||||
&& typeof attachment.computeWorldVertices === 'function') {
|
||||
const clippingVertices = new Float32Array(attachment.worldVerticesLength)
|
||||
attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, clippingVertices, 0, 2)
|
||||
skinMeshDebug.lineStyle(lineWidth, color, 0.9)
|
||||
skinMeshDebug.drawPolygon(Array.from(clippingVertices))
|
||||
}
|
||||
continue
|
||||
}
|
||||
const regionVertices = new Float32Array(8)
|
||||
attachment.computeWorldVertices(slot, regionVertices, 0, 2)
|
||||
skinMeshDebug.lineStyle(lineWidth, color, 0.9)
|
||||
skinMeshDebug.drawPolygon(Array.from(regionVertices))
|
||||
}
|
||||
}
|
||||
|
||||
sync(objects: SpineSceneObject[], frame: number, fps: number) {
|
||||
for (const [id, entry] of this.entries) {
|
||||
if (objects.some((object) => object.id === id)) continue
|
||||
@@ -110,12 +215,18 @@ export class SpineRuntimeLayer {
|
||||
if (entry) this.destroyEntry(entry)
|
||||
const root = new Container()
|
||||
const spine = new asset.SpineClass({ skeletonData: asset.skeletonData, autoUpdate: false, darkTint: false })
|
||||
const skeletonDebug = new Graphics()
|
||||
const skinMeshDebug = new Graphics()
|
||||
skeletonDebug.eventMode = 'none'
|
||||
skinMeshDebug.eventMode = 'none'
|
||||
skeletonDebug.visible = false
|
||||
skinMeshDebug.visible = false
|
||||
if (asset.skeletonData.defaultSkin) spine.skeleton.setSkin(asset.skeletonData.defaultSkin)
|
||||
if (asset.runtimeVersion === '4.3') spine.skeleton.setupPoseSlots()
|
||||
else spine.skeleton.setSlotsToSetupPose()
|
||||
root.addChild(spine)
|
||||
root.addChild(spine, skeletonDebug, skinMeshDebug)
|
||||
this.container.addChild(root)
|
||||
entry = { root, spine, runtimeVersion: asset.runtimeVersion, assetVersion: object.assetVersion, animation: '' }
|
||||
entry = { root, spine, skeletonDebug, skinMeshDebug, runtimeVersion: asset.runtimeVersion, assetVersion: object.assetVersion, animation: '' }
|
||||
this.entries.set(object.id, entry)
|
||||
}
|
||||
if (!entry) continue
|
||||
@@ -144,6 +255,7 @@ export class SpineRuntimeLayer {
|
||||
track.trackTime = duration > 0 ? Math.min(localTime, duration) : localTime
|
||||
}
|
||||
spine.update(0)
|
||||
this.drawDebug(entry, object)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,14 @@ export interface SpineSceneObject {
|
||||
name: string
|
||||
enabled: boolean
|
||||
visible: boolean
|
||||
/** 仅用于画布辅助显示,不参与导出。 */
|
||||
showBones: boolean
|
||||
/** 仅显示蒙皮网格,不显示普通附件边界。 */
|
||||
showOnlySkinMesh: boolean
|
||||
/** 仅显示没有蒙皮网格的 RegionAttachment。 */
|
||||
showRegionAttachments: boolean
|
||||
/** 仅显示裁切附件轮廓。 */
|
||||
showClippingAttachments: boolean
|
||||
x: number
|
||||
y: number
|
||||
rotation: number
|
||||
@@ -48,3 +56,14 @@ export interface SpineSceneObject {
|
||||
status: 'empty' | 'loading' | 'ready' | 'error'
|
||||
error: string
|
||||
}
|
||||
|
||||
type LegacySpineSceneObject = SpineSceneObject & { showSkinMesh?: boolean }
|
||||
|
||||
export function ensureSpineDisplaySettings(spine: SpineSceneObject) {
|
||||
const legacyShowSkinMesh = (spine as LegacySpineSceneObject).showSkinMesh === true
|
||||
if (spine.showBones == null) spine.showBones = false
|
||||
if (spine.showOnlySkinMesh == null) spine.showOnlySkinMesh = false
|
||||
if (spine.showRegionAttachments == null) spine.showRegionAttachments = legacyShowSkinMesh
|
||||
if (spine.showClippingAttachments == null) spine.showClippingAttachments = false
|
||||
delete (spine as LegacySpineSceneObject).showSkinMesh
|
||||
}
|
||||
@@ -394,6 +394,10 @@ export const useParticleStore = defineStore('particle', {
|
||||
name: `SpineSkeleton${id}`,
|
||||
enabled: true,
|
||||
visible: true,
|
||||
showBones: false,
|
||||
showOnlySkinMesh: false,
|
||||
showRegionAttachments: false,
|
||||
showClippingAttachments: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
|
||||
@@ -273,6 +273,12 @@ const embedCode = `<iframe style="width: 75%;height: 80%;border: 10px solid #ccc
|
||||
></iframe>`
|
||||
|
||||
const releases = [
|
||||
{
|
||||
date: '2026-11-10',
|
||||
title: 'help.releaseSummaryV11110',
|
||||
heading: 'help.releaseHeadingV11110',
|
||||
updates: ['help.releaseSpineHelpers'],
|
||||
},
|
||||
{
|
||||
date: '2026-11-09',
|
||||
title: 'help.releaseSummaryV11109',
|
||||
|
||||
Reference in new issue
Block a user