阶段11开发
This commit is contained in:
1 parent
8e9b83f95b
commit
8802bbe25e
21 files changed
+5285
-81
No files matched your search
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
BoundingBoxAttachment,
|
||||
ClippingAttachment,
|
||||
MeshAttachment,
|
||||
PathAttachment,
|
||||
PointAttachment,
|
||||
RegionAttachment,
|
||||
SkeletonJson,
|
||||
type AttachmentLoader,
|
||||
type Skin,
|
||||
} from '@esotericsoftware/spine-core'
|
||||
|
||||
class ExportValidationAttachmentLoader implements AttachmentLoader {
|
||||
newRegionAttachment(_skin: Skin, name: string, path: string, _sequence: any) { return new RegionAttachment(name, path) }
|
||||
newMeshAttachment(_skin: Skin, name: string, path: string, _sequence: any) { return new MeshAttachment(name, path) }
|
||||
newBoundingBoxAttachment(_skin: Skin, name: string) { return new BoundingBoxAttachment(name) }
|
||||
newPathAttachment(_skin: Skin, name: string) { return new PathAttachment(name) }
|
||||
newPointAttachment(_skin: Skin, name: string) { return new PointAttachment(name) }
|
||||
newClippingAttachment(_skin: Skin, name: string) { return new ClippingAttachment(name) }
|
||||
}
|
||||
|
||||
const VALID_INHERIT = new Set(['normal', 'onlyTranslation', 'noRotationOrReflection', 'noScale', 'noScaleOrReflection'])
|
||||
|
||||
function validateMesh(mesh: Record<string, any>, attachmentName: string, boneCount: number) {
|
||||
const uvs = mesh.uvs
|
||||
const vertices = mesh.vertices
|
||||
const triangles = mesh.triangles
|
||||
if (!Array.isArray(uvs) || !uvs.length || uvs.length % 2) throw new Error(`拖尾网格“${attachmentName}”的 UV 数据无效`)
|
||||
if (!Array.isArray(vertices) || !vertices.length) throw new Error(`拖尾网格“${attachmentName}”缺少顶点数据`)
|
||||
const vertexCount = uvs.length / 2
|
||||
if (!Array.isArray(triangles) || triangles.some((index) => !Number.isInteger(index) || index < 0 || index >= vertexCount)) {
|
||||
throw new Error(`拖尾网格“${attachmentName}”的三角形索引越界`)
|
||||
}
|
||||
const hull = Number(mesh.hull) || 0
|
||||
if (!Number.isInteger(hull) || hull < 0 || hull > vertexCount) throw new Error(`拖尾网格“${attachmentName}”的外轮廓数量无效`)
|
||||
if (Array.isArray(mesh.edges) && mesh.edges.some((index) => !Number.isInteger(index) || index < 0 || index >= vertexCount * 2 || index % 2)) {
|
||||
throw new Error(`拖尾网格“${attachmentName}”的外轮廓边索引越界`)
|
||||
}
|
||||
// 未加权 Mesh 每个顶点只有 x/y;拖尾必须是逐顶点的骨骼权重编码。
|
||||
if (vertices.length === uvs.length) return
|
||||
let offset = 0
|
||||
let decodedVertices = 0
|
||||
while (offset < vertices.length) {
|
||||
const influences = vertices[offset++]
|
||||
if (!Number.isInteger(influences) || influences < 1) throw new Error(`拖尾网格“${attachmentName}”包含无效骨骼权重数量`)
|
||||
let weightSum = 0
|
||||
for (let influence = 0; influence < influences; influence++) {
|
||||
const boneIndex = vertices[offset++]
|
||||
const x = vertices[offset++]
|
||||
const y = vertices[offset++]
|
||||
const weight = vertices[offset++]
|
||||
if (!Number.isInteger(boneIndex) || boneIndex < 0 || boneIndex >= boneCount) throw new Error(`拖尾网格“${attachmentName}”引用了不存在的骨骼`)
|
||||
if (![x, y, weight].every(Number.isFinite) || weight < 0) throw new Error(`拖尾网格“${attachmentName}”包含无效权重数据`)
|
||||
weightSum += weight
|
||||
}
|
||||
if (Math.abs(weightSum - 1) > 0.001) throw new Error(`拖尾网格“${attachmentName}”的顶点权重之和不为 1`)
|
||||
decodedVertices++
|
||||
}
|
||||
if (offset !== vertices.length || decodedVertices !== vertexCount) throw new Error(`拖尾网格“${attachmentName}”的顶点数量与 UV 不一致`)
|
||||
}
|
||||
|
||||
function validateEditorImportStructure(root: Record<string, any>) {
|
||||
const bones = Array.isArray(root.bones) ? root.bones : []
|
||||
const boneByName = new Map(bones.map((bone) => [String(bone.name || ''), bone]))
|
||||
const weightedMeshSlots = new Set<string>()
|
||||
for (const bone of bones) {
|
||||
if (bone.inherit != null && !VALID_INHERIT.has(bone.inherit)) throw new Error(`骨骼“${bone.name || ''}”的 inherit 值“${bone.inherit}”不符合 Spine 4.2 JSON 格式`)
|
||||
if (/_particle_\d+_trail_/.test(String(bone.name || ''))) {
|
||||
const parent = boneByName.get(String(bone.parent || ''))
|
||||
if (!parent || /_particle_\d+_trail_/.test(String(parent.name || ''))) {
|
||||
throw new Error(`拖尾骨骼“${bone.name || ''}”必须直接挂在粒子系统骨骼下,不能组成骨骼链`)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const skin of Array.isArray(root.skins) ? root.skins : []) {
|
||||
const attachments = skin?.attachments || {}
|
||||
for (const slotName of Object.keys(attachments)) {
|
||||
for (const attachmentName of Object.keys(attachments[slotName] || {})) {
|
||||
const attachment = attachments[slotName][attachmentName]
|
||||
if (attachment?.type === 'mesh') {
|
||||
validateMesh(attachment, attachmentName, bones.length)
|
||||
if (attachment.vertices?.length !== attachment.uvs?.length) weightedMeshSlots.add(slotName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const animation of Object.values(root.animations || {}) as Array<Record<string, any>>) {
|
||||
for (const slotName of weightedMeshSlots) {
|
||||
const attachmentKeys = animation?.slots?.[slotName]?.attachment
|
||||
if (Array.isArray(attachmentKeys) && attachmentKeys.some((key) => !key?.name)) {
|
||||
throw new Error(`加权拖尾插槽“${slotName}”不能使用空附件,未出现或死亡阶段应将对应骨骼缩放设为 0`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 下载前使用项目内置 Spine 4.2 Runtime 做一次结构解析,提前拦截无效 JSON。 */
|
||||
export function validateSpineJson(text: string) {
|
||||
const root = JSON.parse(text)
|
||||
validateEditorImportStructure(root)
|
||||
const reader = new SkeletonJson(new ExportValidationAttachmentLoader())
|
||||
const data = reader.readSkeletonData(root)
|
||||
if (!data.bones.length) throw new Error('导出结果缺少骨骼')
|
||||
return { bones: data.bones.length, slots: data.slots.length, animations: data.animations.length }
|
||||
}
|
||||
Reference in new issue
Block a user