支持spine进行绘制

This commit is contained in:
tianmo committed 2026-11-10 15:50:16 +08:00
1 parent 266c353fd2
commit e89eeaa502
19 files changed
+310 -18

No files matched your search

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