From d5bca135364d47f83369958a1ec1d1b750d3e115 Mon Sep 17 00:00:00 2001 From: Milton Date: Fri, 5 Aug 2022 16:35:45 -0300 Subject: [PATCH 1/6] [BROKEN] Added initial code for the debug draw --- packages/base/src/SpineBase.ts | 476 ++++++++++++++++++++++++++++++++- 1 file changed, 475 insertions(+), 1 deletion(-) diff --git a/packages/base/src/SpineBase.ts b/packages/base/src/SpineBase.ts index ee6e30df..db50555e 100644 --- a/packages/base/src/SpineBase.ts +++ b/packages/base/src/SpineBase.ts @@ -26,6 +26,35 @@ import {settings} from "./settings"; let tempRgb = [0, 0, 0]; +type DebugDisplayObjects = { + bones: Container; + skeletonXY: Graphics; + regionAttachmentsShape: Graphics; + meshTrianglesLine: Graphics; + meshHullLine: Graphics; + clippingPolygon: Graphics; + boundingBoxesRect: Graphics; + boundingBoxesCircle: Graphics; + boundingBoxesPolygon: Graphics; + pathsCurve: Graphics; + pathsLine: Graphics; +} + +interface DebugOptions { + lineWidth: number; // 1 + regionAttachmentsColor: number; // 0x0078ff + meshHullColor: number; // 0x0078ff + meshTrianglesColor: number; // 0xffcc00 + clippingPolygonColor: number; // 0xff00ff + boundingBoxesRectColor: number; // 0x00ff00 + boundingBoxesPolygonColor: number; // 0x00ff00 + boundingBoxesCircleColor: number; // 0x00ff00 + pathsCurveColor: number; // 0xff0000 + pathsLineColor: number; // 0xff00ff + skeletonXYColor:number; // 0xff0000 + bonesColor:number; // 0x00eecc +} + /** * @public */ @@ -74,7 +103,6 @@ export abstract class SpineBase extends Container implements GlobalMixins.Spine { - tintRgb: ArrayLike; spineData: SkeletonData; skeleton: Skeleton; @@ -85,6 +113,38 @@ export abstract class SpineBase 0; len--) { + elem.children[len - 1].destroy({ children: true, texture: true, baseTexture: true }); + } + } + } + + const scale = this.scale.x || this.scale.y || 1; + const lineWidth = this.debugOptions.lineWidth / scale; + + if (this.drawBones) { + this.drawBonesFunc(lineWidth, scale); + } + + if (this.drawPaths) { + this.drawPathsFunc(lineWidth); + } + + if (this.drawBoundingBoxes) { + this.drawBoundingBoxesFunc(lineWidth); + } + + if (this.drawClipping) { + this.drawClippingFunc(lineWidth); + } + + if (this.drawMeshHull || this.drawMeshTriangles) { + this.drawMeshHullAndMeshTriangles(lineWidth); + } + + if (this.drawRegionAttachments) { + this.drawRegionAttachmentsFunc(lineWidth); + } + } + + private drawBonesFunc(lineWidth:number, scale:number): void { + const skeleton = this.skeleton; + const skeletonX = skeleton.x; + const skeletonY = skeleton.y; + const bones = skeleton.bones; + + this.debugObjects.skeletonXY.lineStyle(lineWidth, 0xff0000, 1); + + for (let i = 0, len = bones.length; i < len; i++) { + const bone = bones[i], + boneLen = bone.data.length, + starX = skeletonX + bone.worldX, + starY = skeletonY + bone.worldY, + endX = skeletonX + boneLen * bone.matrix.a + bone.worldX, + endY = skeletonY + boneLen * bone.matrix.b + bone.worldY; + + if (bone.data.name === "root" || bone.parent === null) { + continue; + } + + // 三角形计算公式 + // 面积 A=sqrt((a+b+c)*(-a+b+c)*(a-b+c)*(a+b-c))/4 + // 阿尔法 alpha=acos((pow(b, 2)+pow(c, 2)-pow(a, 2))/(2*b*c)) + // 贝塔 beta=acos((pow(a, 2)+pow(c, 2)-pow(b, 2))/(2*a*c)) + // 伽马 gamma=acos((pow(a, 2)+pow(b, 2)-pow(c, 2))/(2*a*b)) + + const w = Math.abs(starX - endX), + h = Math.abs(starY - endY), + // a = w, // 边长a + a2 = Math.pow(w, 2), // 边长a平方根 + b = h, // 边长b + b2 = Math.pow(h, 2), // 边长b平方根 + c = Math.sqrt(a2 + b2), // 边长c + c2 = Math.pow(c, 2), // 边长c平方根 + rad = Math.PI / 180, + // A = Math.acos([a2 + c2 - b2] / [2 * a * c]) || 0, // A角角度 + // C = Math.acos([a2 + b2 - c2] / [2 * a * b]) || 0, // C角角度 + B = Math.acos((c2 + b2 - a2) / (2 * b * c)) || 0; // B角角度 + if (c === 0) { + continue; + } + + const gp = new Graphics(); + this.debugObjects.bones.addChild(gp); + + // 绘制骨骼线条 + const refRation = c / 50 / scale; + gp.beginFill(0x00eecc, 1); + gp.drawPolygon(0, 0, 0 - refRation, c - refRation * 3, 0, c - refRation, 0 + refRation, c - refRation * 3); + gp.endFill(); + gp.x = starX; + gp.y = starY; + gp.pivot.y = c; + + // 计算骨骼旋转角度 + let rotation = 0; + if (starX < endX && starY < endY) { + // 右下 + rotation = -B + 180 * rad; + } else if (starX > endX && starY < endY) { + // 左下 + rotation = 180 * rad + B; + } else if (starX > endX && starY > endY) { + // 左上 + rotation = -B; + } else if (starX < endX && starY > endY) { + // 左下 + rotation = B; + } else if (starY === endY && starX < endX) { + // 向右 + rotation = 90 * rad; + } else if (starY === endY && starX > endX) { + // 向左 + rotation = -90 * rad; + } else if (starX === endX && starY < endY) { + // 向下 + rotation = 180 * rad; + } else if (starX === endX && starY > endY) { + // 向上 + rotation = 0; + } + gp.rotation = rotation; + + // 绘制骨骼起始旋转点 + gp.lineStyle(lineWidth + refRation / 2.4, 0x00eecc, 1); + gp.beginFill(0x000000, 0.6); + gp.drawCircle(0, c, refRation * 1.2); + gp.endFill(); + } + + // 绘制骨架起点『X』形式 + const startDotSize = lineWidth * 3; + this.debugObjects.skeletonXY.moveTo(skeletonX - startDotSize, skeletonY - startDotSize); + this.debugObjects.skeletonXY.lineTo(skeletonX + startDotSize, skeletonY + startDotSize); + this.debugObjects.skeletonXY.moveTo(skeletonX + startDotSize, skeletonY - startDotSize); + this.debugObjects.skeletonXY.lineTo(skeletonX - startDotSize, skeletonY + startDotSize); + } + + private drawRegionAttachmentsFunc(lineWidth:number): void { + const skeleton = this.skeleton; + const slots = skeleton.slots; + + this.debugObjects.regionAttachmentsShape.lineStyle(lineWidth, 0x0078ff, 1); + + for (let i = 0, len = slots.length; i < len; i++) { + const slot = slots[i], + attachment = slot.getAttachment(); + if (!(attachment instanceof RegionAttachment)) { + continue; + } + + const vertices = new Float32Array(8); + attachment.updateOffset(); + attachment.computeWorldVertices(slot.bone, vertices, 0, 2); + this.debugObjects.regionAttachmentsShape.drawPolygon([...vertices.slice(0, 8)]); + // this.debugObjects.regionAttachmentsShape.moveTo(vertices[0],vertices[1]); + // this.debugObjects.regionAttachmentsShape.lineTo(vertices[2],vertices[3]); + // this.debugObjects.regionAttachmentsShape.lineTo(vertices[4],vertices[5]); + // this.debugObjects.regionAttachmentsShape.lineTo(vertices[6],vertices[7]); + // this.debugObjects.regionAttachmentsShape.lineTo(vertices[0],vertices[1]); + } + } + + // 绘制蒙皮 + private drawMeshHullAndMeshTriangles(lineWidth:number): void { + const skeleton = this.skeleton; + const slots = skeleton.slots; + + this.debugObjects.meshHullLine.lineStyle(lineWidth, 0x0078ff, 1); + this.debugObjects.meshTrianglesLine.lineStyle(lineWidth, 0xffcc00, 1); + + for (let i = 0, len = slots.length; i < len; i++) { + const slot = slots[i]; + if (!slot.bone.active) { + continue; + } + const attachment = slot.getAttachment(); + if (!(attachment instanceof MeshAttachment)) { + continue; + } + + const vertices = new Float32Array(attachment.vertices.length), + triangles = attachment.triangles; + let hullLength = attachment.hullLength; + attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, vertices, 0, 2); + // 画蒙皮网格(三角形) + if (this.drawMeshTriangles) { + for (let i = 0, len = triangles.length; i < len; i += 3) { + const v1 = triangles[i] * 2, + v2 = triangles[i + 1] * 2, + v3 = triangles[i + 2] * 2; + this.debugObjects.meshTrianglesLine.moveTo(vertices[v1], vertices[v1 + 1]); + this.debugObjects.meshTrianglesLine.lineTo(vertices[v2], vertices[v2 + 1]); + this.debugObjects.meshTrianglesLine.lineTo(vertices[v3], vertices[v3 + 1]); + } + } + + // 画蒙皮边框 + if (this.drawMeshHull && hullLength > 0) { + hullLength = (hullLength >> 1) * 2; + let lastX = vertices[hullLength - 2], + lastY = vertices[hullLength - 1]; + for (let i = 0, len = hullLength; i < len; i += 2) { + const x = vertices[i], + y = vertices[i + 1]; + this.debugObjects.meshHullLine.moveTo(x, y); + this.debugObjects.meshHullLine.lineTo(lastX, lastY); + lastX = x; + lastY = y; + } + } + } + } + + // 蒙版区域 + private drawClippingFunc(lineWidth:number): void { + const skeleton = this.skeleton; + const slots = skeleton.slots; + + this.debugObjects.clippingPolygon.lineStyle(lineWidth, 0xff00ff, 1); + for (let i = 0, len = slots.length; i < len; i++) { + const slot = slots[i]; + if (!slot.bone.active) { + continue; + } + const attachment = slot.getAttachment(); + if (!(attachment instanceof ClippingAttachment)) { + continue; + } + + const nn = attachment.worldVerticesLength, + world = new Float32Array(nn); + attachment.computeWorldVertices(slot, 0, nn, world, 0, 2); + this.debugObjects.clippingPolygon.drawPolygon([...world]); + } + } + + // 绘制边界框 + private drawBoundingBoxesFunc(lineWidth:number): void { + // 绘制边界框的总外框 + this.debugObjects.boundingBoxesRect.lineStyle(lineWidth, 0x00ff00, 5); + + const bounds = new SkeletonBounds(); + bounds.update(this.skeleton, true); + this.debugObjects.boundingBoxesRect.drawRect(bounds.minX, bounds.minY, bounds.getWidth(), bounds.getHeight()); + + const polygons = bounds.polygons, + drawPolygon = (polygonVertices: ArrayLike, _offset: unknown, count: number): void => { + this.debugObjects.boundingBoxesPolygon.lineStyle(lineWidth, 0x00ff00, 1); + this.debugObjects.boundingBoxesPolygon.beginFill(0x00ff00, 0.1); + + if (count < 3) { + throw new Error("Polygon must contain at least 3 vertices"); + } + const paths = [], + dotSize = lineWidth * 2; + for (let i = 0, len = polygonVertices.length; i < len; i += 2) { + const x1 = polygonVertices[i], + y1 = polygonVertices[i + 1]; + + // 绘制边界框节点 + this.debugObjects.boundingBoxesCircle.lineStyle(0); + this.debugObjects.boundingBoxesCircle.beginFill(0x00dd00); + this.debugObjects.boundingBoxesCircle.drawCircle(x1, y1, dotSize); + this.debugObjects.boundingBoxesCircle.endFill(); + + paths.push(x1, y1); + } + + // 绘制边界框区域 + this.debugObjects.boundingBoxesPolygon.drawPolygon(paths); + this.debugObjects.boundingBoxesPolygon.endFill(); + }; + + for (let i = 0, len = polygons.length; i < len; i++) { + const polygon = polygons[i]; + drawPolygon(polygon, 0, polygon.length); + } + } + + // 绘制路径 + private drawPathsFunc(lineWidth:number): void { + const skeleton = this.skeleton; + const slots = skeleton.slots; + + this.debugObjects.pathsCurve.lineStyle(lineWidth, 0xff0000, 1); + this.debugObjects.pathsLine.lineStyle(lineWidth, 0xff00ff, 1); + + for (let i = 0, len = slots.length; i < len; i++) { + const slot = slots[i]; + if (!slot.bone.active) { + continue; + } + const attachment = slot.getAttachment(); + if (attachment instanceof PathAttachment) { + let nn = attachment.worldVerticesLength; + const world = new Float32Array(nn); + attachment.computeWorldVertices(slot, 0, nn, world, 0, 2); + let x1 = world[2], + y1 = world[3], + x2 = 0, + y2 = 0; + if (attachment.closed) { + const cx1 = world[0], + cy1 = world[1], + cx2 = world[nn - 2], + cy2 = world[nn - 1]; + x2 = world[nn - 4]; + y2 = world[nn - 3]; + + // 曲线 + this.debugObjects.pathsCurve.moveTo(x1, y1); + this.debugObjects.pathsCurve.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2); + + // 句柄 + this.debugObjects.pathsLine.moveTo(x1, y1); + this.debugObjects.pathsLine.lineTo(cx1, cy1); + this.debugObjects.pathsLine.moveTo(x2, y2); + this.debugObjects.pathsLine.lineTo(cx2, cy2); + } + nn -= 4; + for (let ii = 4; ii < nn; ii += 6) { + const cx1 = world[ii], + cy1 = world[ii + 1], + cx2 = world[ii + 2], + cy2 = world[ii + 3]; + x2 = world[ii + 4]; + y2 = world[ii + 5]; + // 曲线 + this.debugObjects.pathsCurve.moveTo(x1, y1); + this.debugObjects.pathsCurve.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2); + + // 句柄 + this.debugObjects.pathsLine.moveTo(x1, y1); + this.debugObjects.pathsLine.lineTo(cx1, cy1); + this.debugObjects.pathsLine.moveTo(x2, y2); + this.debugObjects.pathsLine.lineTo(cx2, cy2); + x1 = x2; + y1 = y2; + } + } + } + } + + destroy(options?: any): void { for (let i = 0, n = this.skeleton.slots.length; i < n; i++) { let slot = this.skeleton.slots[i]; @@ -790,6 +1250,20 @@ export abstract class SpineBase Date: Sat, 6 Aug 2022 18:15:16 -0300 Subject: [PATCH 2/6] I think everything is working! --- packages/base/src/SpineBase.ts | 629 +++++++++--------- packages/base/src/core/ISkeleton.ts | 6 +- packages/base/src/core/SkeletonBoundsBase.ts | 206 ++++++ packages/base/src/index.ts | 1 + packages/runtime-3.7/src/core/Bone.ts | 3 + .../runtime-3.7/src/core/SkeletonBounds.ts | 183 +---- .../src/core/attachments/RegionAttachment.ts | 5 +- .../runtime-3.8/src/core/SkeletonBounds.ts | 184 +---- .../src/core/attachments/RegionAttachment.ts | 5 +- .../runtime-4.0/src/core/SkeletonBounds.ts | 201 +----- .../src/core/attachments/RegionAttachment.ts | 5 +- .../runtime-4.1/src/core/SkeletonBounds.ts | 201 +----- 12 files changed, 559 insertions(+), 1070 deletions(-) create mode 100644 packages/base/src/core/SkeletonBoundsBase.ts diff --git a/packages/base/src/SpineBase.ts b/packages/base/src/SpineBase.ts index db50555e..33d2cf0d 100644 --- a/packages/base/src/SpineBase.ts +++ b/packages/base/src/SpineBase.ts @@ -1,6 +1,7 @@ import {AttachmentType} from './core/AttachmentType'; import {TextureRegion} from './core/TextureRegion'; import {MathUtils} from './core/Utils'; +import {SkeletonBoundsBase} from './core/SkeletonBoundsBase'; import type { IAnimationState, IAnimationStateData @@ -40,7 +41,10 @@ type DebugDisplayObjects = { pathsLine: Graphics; } -interface DebugOptions { +/** + * @public + */ +export interface SpineDebugOptions { lineWidth: number; // 1 regionAttachmentsColor: number; // 0x0078ff meshHullColor: number; // 0x0078ff @@ -115,7 +119,7 @@ export abstract class SpineBase endX && starY < endY) { - // 左下 - rotation = 180 * rad + B; - } else if (starX > endX && starY > endY) { - // 左上 - rotation = -B; - } else if (starX < endX && starY > endY) { - // 左下 - rotation = B; - } else if (starY === endY && starX < endX) { - // 向右 - rotation = 90 * rad; - } else if (starY === endY && starX > endX) { - // 向左 - rotation = -90 * rad; - } else if (starX === endX && starY < endY) { - // 向下 - rotation = 180 * rad; - } else if (starX === endX && starY > endY) { - // 向上 - rotation = 0; - } - gp.rotation = rotation; - - // 绘制骨骼起始旋转点 - gp.lineStyle(lineWidth + refRation / 2.4, 0x00eecc, 1); - gp.beginFill(0x000000, 0.6); - gp.drawCircle(0, c, refRation * 1.2); - gp.endFill(); - } - - // 绘制骨架起点『X』形式 - const startDotSize = lineWidth * 3; - this.debugObjects.skeletonXY.moveTo(skeletonX - startDotSize, skeletonY - startDotSize); - this.debugObjects.skeletonXY.lineTo(skeletonX + startDotSize, skeletonY + startDotSize); - this.debugObjects.skeletonXY.moveTo(skeletonX + startDotSize, skeletonY - startDotSize); - this.debugObjects.skeletonXY.lineTo(skeletonX - startDotSize, skeletonY + startDotSize); - } - - private drawRegionAttachmentsFunc(lineWidth:number): void { - const skeleton = this.skeleton; - const slots = skeleton.slots; - - this.debugObjects.regionAttachmentsShape.lineStyle(lineWidth, 0x0078ff, 1); - - for (let i = 0, len = slots.length; i < len; i++) { - const slot = slots[i], - attachment = slot.getAttachment(); - if (!(attachment instanceof RegionAttachment)) { - continue; - } - - const vertices = new Float32Array(8); - attachment.updateOffset(); - attachment.computeWorldVertices(slot.bone, vertices, 0, 2); - this.debugObjects.regionAttachmentsShape.drawPolygon([...vertices.slice(0, 8)]); - // this.debugObjects.regionAttachmentsShape.moveTo(vertices[0],vertices[1]); - // this.debugObjects.regionAttachmentsShape.lineTo(vertices[2],vertices[3]); - // this.debugObjects.regionAttachmentsShape.lineTo(vertices[4],vertices[5]); - // this.debugObjects.regionAttachmentsShape.lineTo(vertices[6],vertices[7]); - // this.debugObjects.regionAttachmentsShape.lineTo(vertices[0],vertices[1]); - } - } - - // 绘制蒙皮 - private drawMeshHullAndMeshTriangles(lineWidth:number): void { - const skeleton = this.skeleton; - const slots = skeleton.slots; - - this.debugObjects.meshHullLine.lineStyle(lineWidth, 0x0078ff, 1); - this.debugObjects.meshTrianglesLine.lineStyle(lineWidth, 0xffcc00, 1); - - for (let i = 0, len = slots.length; i < len; i++) { - const slot = slots[i]; - if (!slot.bone.active) { - continue; - } - const attachment = slot.getAttachment(); - if (!(attachment instanceof MeshAttachment)) { - continue; - } - - const vertices = new Float32Array(attachment.vertices.length), - triangles = attachment.triangles; - let hullLength = attachment.hullLength; - attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, vertices, 0, 2); - // 画蒙皮网格(三角形) - if (this.drawMeshTriangles) { - for (let i = 0, len = triangles.length; i < len; i += 3) { - const v1 = triangles[i] * 2, - v2 = triangles[i + 1] * 2, - v3 = triangles[i + 2] * 2; - this.debugObjects.meshTrianglesLine.moveTo(vertices[v1], vertices[v1 + 1]); - this.debugObjects.meshTrianglesLine.lineTo(vertices[v2], vertices[v2 + 1]); - this.debugObjects.meshTrianglesLine.lineTo(vertices[v3], vertices[v3 + 1]); - } - } - - // 画蒙皮边框 - if (this.drawMeshHull && hullLength > 0) { - hullLength = (hullLength >> 1) * 2; - let lastX = vertices[hullLength - 2], - lastY = vertices[hullLength - 1]; - for (let i = 0, len = hullLength; i < len; i += 2) { - const x = vertices[i], - y = vertices[i + 1]; - this.debugObjects.meshHullLine.moveTo(x, y); - this.debugObjects.meshHullLine.lineTo(lastX, lastY); - lastX = x; - lastY = y; - } - } - } - } - - // 蒙版区域 - private drawClippingFunc(lineWidth:number): void { - const skeleton = this.skeleton; - const slots = skeleton.slots; - - this.debugObjects.clippingPolygon.lineStyle(lineWidth, 0xff00ff, 1); - for (let i = 0, len = slots.length; i < len; i++) { - const slot = slots[i]; - if (!slot.bone.active) { - continue; - } - const attachment = slot.getAttachment(); - if (!(attachment instanceof ClippingAttachment)) { - continue; - } - - const nn = attachment.worldVerticesLength, - world = new Float32Array(nn); - attachment.computeWorldVertices(slot, 0, nn, world, 0, 2); - this.debugObjects.clippingPolygon.drawPolygon([...world]); - } - } - - // 绘制边界框 - private drawBoundingBoxesFunc(lineWidth:number): void { - // 绘制边界框的总外框 - this.debugObjects.boundingBoxesRect.lineStyle(lineWidth, 0x00ff00, 5); - - const bounds = new SkeletonBounds(); - bounds.update(this.skeleton, true); - this.debugObjects.boundingBoxesRect.drawRect(bounds.minX, bounds.minY, bounds.getWidth(), bounds.getHeight()); - - const polygons = bounds.polygons, - drawPolygon = (polygonVertices: ArrayLike, _offset: unknown, count: number): void => { - this.debugObjects.boundingBoxesPolygon.lineStyle(lineWidth, 0x00ff00, 1); - this.debugObjects.boundingBoxesPolygon.beginFill(0x00ff00, 0.1); - - if (count < 3) { - throw new Error("Polygon must contain at least 3 vertices"); - } - const paths = [], - dotSize = lineWidth * 2; - for (let i = 0, len = polygonVertices.length; i < len; i += 2) { - const x1 = polygonVertices[i], - y1 = polygonVertices[i + 1]; - - // 绘制边界框节点 - this.debugObjects.boundingBoxesCircle.lineStyle(0); - this.debugObjects.boundingBoxesCircle.beginFill(0x00dd00); - this.debugObjects.boundingBoxesCircle.drawCircle(x1, y1, dotSize); - this.debugObjects.boundingBoxesCircle.endFill(); - - paths.push(x1, y1); - } - - // 绘制边界框区域 - this.debugObjects.boundingBoxesPolygon.drawPolygon(paths); - this.debugObjects.boundingBoxesPolygon.endFill(); - }; - - for (let i = 0, len = polygons.length; i < len; i++) { - const polygon = polygons[i]; - drawPolygon(polygon, 0, polygon.length); - } - } - - // 绘制路径 - private drawPathsFunc(lineWidth:number): void { - const skeleton = this.skeleton; - const slots = skeleton.slots; - - this.debugObjects.pathsCurve.lineStyle(lineWidth, 0xff0000, 1); - this.debugObjects.pathsLine.lineStyle(lineWidth, 0xff00ff, 1); - - for (let i = 0, len = slots.length; i < len; i++) { - const slot = slots[i]; - if (!slot.bone.active) { - continue; - } - const attachment = slot.getAttachment(); - if (attachment instanceof PathAttachment) { - let nn = attachment.worldVerticesLength; - const world = new Float32Array(nn); - attachment.computeWorldVertices(slot, 0, nn, world, 0, 2); - let x1 = world[2], - y1 = world[3], - x2 = 0, - y2 = 0; - if (attachment.closed) { - const cx1 = world[0], - cy1 = world[1], - cx2 = world[nn - 2], - cy2 = world[nn - 1]; - x2 = world[nn - 4]; - y2 = world[nn - 3]; - - // 曲线 - this.debugObjects.pathsCurve.moveTo(x1, y1); - this.debugObjects.pathsCurve.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2); - - // 句柄 - this.debugObjects.pathsLine.moveTo(x1, y1); - this.debugObjects.pathsLine.lineTo(cx1, cy1); - this.debugObjects.pathsLine.moveTo(x2, y2); - this.debugObjects.pathsLine.lineTo(cx2, cy2); - } - nn -= 4; - for (let ii = 4; ii < nn; ii += 6) { - const cx1 = world[ii], - cy1 = world[ii + 1], - cx2 = world[ii + 2], - cy2 = world[ii + 3]; - x2 = world[ii + 4]; - y2 = world[ii + 5]; - // 曲线 - this.debugObjects.pathsCurve.moveTo(x1, y1); - this.debugObjects.pathsCurve.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2); - - // 句柄 - this.debugObjects.pathsLine.moveTo(x1, y1); - this.debugObjects.pathsLine.lineTo(cx1, cy1); - this.debugObjects.pathsLine.moveTo(x2, y2); - this.debugObjects.pathsLine.lineTo(cx2, cy2); - x1 = x2; - y1 = y2; - } - } - } - } + const skeleton = this.skeleton; + const skeletonX = skeleton.x; + const skeletonY = skeleton.y; + const bones = skeleton.bones; + + this.debugObjects.skeletonXY.lineStyle(lineWidth, 0xff0000, 1); + + for (let i = 0, len = bones.length; i < len; i++) { + const bone = bones[i], + boneLen = bone.data.length, + starX = skeletonX + bone.matrix.tx, + starY = skeletonY + bone.matrix.ty, + endX = skeletonX + boneLen * bone.matrix.a + bone.matrix.tx, + endY = skeletonY + boneLen * bone.matrix.b + bone.matrix.ty; + + if (bone.data.name === "root" || bone.data.parent === null) { + continue; + } + + // 三角形计算公式 + // 面积 A=sqrt((a+b+c)*(-a+b+c)*(a-b+c)*(a+b-c))/4 + // 阿尔法 alpha=acos((pow(b, 2)+pow(c, 2)-pow(a, 2))/(2*b*c)) + // 贝塔 beta=acos((pow(a, 2)+pow(c, 2)-pow(b, 2))/(2*a*c)) + // 伽马 gamma=acos((pow(a, 2)+pow(b, 2)-pow(c, 2))/(2*a*b)) + + const w = Math.abs(starX - endX), + h = Math.abs(starY - endY), + // a = w, // 边长a + a2 = Math.pow(w, 2), // 边长a平方根 + b = h, // 边长b + b2 = Math.pow(h, 2), // 边长b平方根 + c = Math.sqrt(a2 + b2), // 边长c + c2 = Math.pow(c, 2), // 边长c平方根 + rad = Math.PI / 180, + // A = Math.acos([a2 + c2 - b2] / [2 * a * c]) || 0, // A角角度 + // C = Math.acos([a2 + b2 - c2] / [2 * a * b]) || 0, // C角角度 + B = Math.acos((c2 + b2 - a2) / (2 * b * c)) || 0; // B角角度 + if (c === 0) { + continue; + } + + const gp = new Graphics(); + this.debugObjects.bones.addChild(gp); + + // 绘制骨骼线条 + const refRation = c / 50 / scale; + gp.beginFill(0x00eecc, 1); + gp.drawPolygon(0, 0, 0 - refRation, c - refRation * 3, 0, c - refRation, 0 + refRation, c - refRation * 3); + gp.endFill(); + gp.x = starX; + gp.y = starY; + gp.pivot.y = c; + + // 计算骨骼旋转角度 + let rotation = 0; + if (starX < endX && starY < endY) { + // 右下 + rotation = -B + 180 * rad; + } else if (starX > endX && starY < endY) { + // 左下 + rotation = 180 * rad + B; + } else if (starX > endX && starY > endY) { + // 左上 + rotation = -B; + } else if (starX < endX && starY > endY) { + // 左下 + rotation = B; + } else if (starY === endY && starX < endX) { + // 向右 + rotation = 90 * rad; + } else if (starY === endY && starX > endX) { + // 向左 + rotation = -90 * rad; + } else if (starX === endX && starY < endY) { + // 向下 + rotation = 180 * rad; + } else if (starX === endX && starY > endY) { + // 向上 + rotation = 0; + } + gp.rotation = rotation; + + // 绘制骨骼起始旋转点 + gp.lineStyle(lineWidth + refRation / 2.4, 0x00eecc, 1); + gp.beginFill(0x000000, 0.6); + gp.drawCircle(0, c, refRation * 1.2); + gp.endFill(); + } + + // 绘制骨架起点『X』形式 + const startDotSize = lineWidth * 3; + this.debugObjects.skeletonXY.moveTo(skeletonX - startDotSize, skeletonY - startDotSize); + this.debugObjects.skeletonXY.lineTo(skeletonX + startDotSize, skeletonY + startDotSize); + this.debugObjects.skeletonXY.moveTo(skeletonX + startDotSize, skeletonY - startDotSize); + this.debugObjects.skeletonXY.lineTo(skeletonX - startDotSize, skeletonY + startDotSize); + } + + private drawRegionAttachmentsFunc(lineWidth:number): void { + const skeleton = this.skeleton; + const slots = skeleton.slots; + + this.debugObjects.regionAttachmentsShape.lineStyle(lineWidth, 0x0078ff, 1); + + for (let i = 0, len = slots.length; i < len; i++) { + const slot = slots[i], + attachment = slot.getAttachment(); + if (attachment.type !== AttachmentType.Region) { + continue; + } + + const regionAttachment = attachment as IRegionAttachment & { + computeWorldVertices:(slot: unknown, worldVertices: unknown, offset: unknown, stride: unknown) => void, + updateOffset?:() => void, + }; + + const vertices = new Float32Array(8); + + + regionAttachment?.updateOffset(); // We don't need this on all versions + + regionAttachment.computeWorldVertices(slot, vertices, 0, 2); + this.debugObjects.regionAttachmentsShape.drawPolygon(Array.from(vertices.slice(0, 8))); + + } + } + + // 绘制蒙皮 + private drawMeshHullAndMeshTriangles(lineWidth:number): void { + const skeleton = this.skeleton; + const slots = skeleton.slots; + + this.debugObjects.meshHullLine.lineStyle(lineWidth, 0x0078ff, 1); + this.debugObjects.meshTrianglesLine.lineStyle(lineWidth, 0xffcc00, 1); + + for (let i = 0, len = slots.length; i < len; i++) { + const slot = slots[i]; + if (!slot.bone.active) { + continue; + } + const attachment = slot.getAttachment(); + if ((attachment.type !== AttachmentType.Mesh)) { + continue; + } + + const meshAttachment:IMeshAttachment = attachment as IMeshAttachment; + + const vertices = new Float32Array(meshAttachment.worldVerticesLength), + triangles = meshAttachment.triangles; + let hullLength = meshAttachment.hullLength; + meshAttachment.computeWorldVertices(slot, 0, meshAttachment.worldVerticesLength, vertices, 0, 2); + // 画蒙皮网格(三角形) + if (this.drawMeshTriangles) { + for (let i = 0, len = triangles.length; i < len; i += 3) { + const v1 = triangles[i] * 2, + v2 = triangles[i + 1] * 2, + v3 = triangles[i + 2] * 2; + this.debugObjects.meshTrianglesLine.moveTo(vertices[v1], vertices[v1 + 1]); + this.debugObjects.meshTrianglesLine.lineTo(vertices[v2], vertices[v2 + 1]); + this.debugObjects.meshTrianglesLine.lineTo(vertices[v3], vertices[v3 + 1]); + } + } + + // 画蒙皮边框 + if (this.drawMeshHull && hullLength > 0) { + hullLength = (hullLength >> 1) * 2; + let lastX = vertices[hullLength - 2], + lastY = vertices[hullLength - 1]; + for (let i = 0, len = hullLength; i < len; i += 2) { + const x = vertices[i], + y = vertices[i + 1]; + this.debugObjects.meshHullLine.moveTo(x, y); + this.debugObjects.meshHullLine.lineTo(lastX, lastY); + lastX = x; + lastY = y; + } + } + } + } + + // 蒙版区域 + private drawClippingFunc(lineWidth:number): void { + const skeleton = this.skeleton; + const slots = skeleton.slots; + + this.debugObjects.clippingPolygon.lineStyle(lineWidth, 0xff00ff, 1); + for (let i = 0, len = slots.length; i < len; i++) { + const slot = slots[i]; + if (!slot.bone.active) { + continue; + } + const attachment = slot.getAttachment(); + if (!(attachment.type === AttachmentType.Clipping)) { + continue; + } + + const clippingAttachment: IClippingAttachment = attachment as IClippingAttachment; + + const nn = clippingAttachment.worldVerticesLength, + world = new Float32Array(nn); + clippingAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2); + this.debugObjects.clippingPolygon.drawPolygon(Array.from(world)); + } + } + + // 绘制边界框 + private drawBoundingBoxesFunc(lineWidth:number): void { + // 绘制边界框的总外框 + this.debugObjects.boundingBoxesRect.lineStyle(lineWidth, 0x00ff00, 5); + + const bounds = new SkeletonBoundsBase(); + bounds.update(this.skeleton, true); + this.debugObjects.boundingBoxesRect.drawRect(bounds.minX, bounds.minY, bounds.getWidth(), bounds.getHeight()); + + const polygons = bounds.polygons, + drawPolygon = (polygonVertices: ArrayLike, _offset: unknown, count: number): void => { + this.debugObjects.boundingBoxesPolygon.lineStyle(lineWidth, 0x00ff00, 1); + this.debugObjects.boundingBoxesPolygon.beginFill(0x00ff00, 0.1); + + if (count < 3) { + throw new Error("Polygon must contain at least 3 vertices"); + } + const paths = [], + dotSize = lineWidth * 2; + for (let i = 0, len = polygonVertices.length; i < len; i += 2) { + const x1 = polygonVertices[i], + y1 = polygonVertices[i + 1]; + + // 绘制边界框节点 + this.debugObjects.boundingBoxesCircle.lineStyle(0); + this.debugObjects.boundingBoxesCircle.beginFill(0x00dd00); + this.debugObjects.boundingBoxesCircle.drawCircle(x1, y1, dotSize); + this.debugObjects.boundingBoxesCircle.endFill(); + + paths.push(x1, y1); + } + + // 绘制边界框区域 + this.debugObjects.boundingBoxesPolygon.drawPolygon(paths); + this.debugObjects.boundingBoxesPolygon.endFill(); + }; + + for (let i = 0, len = polygons.length; i < len; i++) { + const polygon = polygons[i]; + drawPolygon(polygon, 0, polygon.length); + } + } + + // 绘制路径 + private drawPathsFunc(lineWidth:number): void { + const skeleton = this.skeleton; + const slots = skeleton.slots; + + this.debugObjects.pathsCurve.lineStyle(lineWidth, 0xff0000, 1); + this.debugObjects.pathsLine.lineStyle(lineWidth, 0xff00ff, 1); + + for (let i = 0, len = slots.length; i < len; i++) { + const slot = slots[i]; + if (!slot.bone.active) { + continue; + } + const attachment = slot.getAttachment(); + if (attachment.type === AttachmentType.Path) { + const pathAttachment = attachment as IVertexAttachment & {closed:boolean}; + let nn = pathAttachment.worldVerticesLength; + const world = new Float32Array(nn); + pathAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2); + let x1 = world[2], + y1 = world[3], + x2 = 0, + y2 = 0; + if (pathAttachment.closed) { + const cx1 = world[0], + cy1 = world[1], + cx2 = world[nn - 2], + cy2 = world[nn - 1]; + x2 = world[nn - 4]; + y2 = world[nn - 3]; + + // 曲线 + this.debugObjects.pathsCurve.moveTo(x1, y1); + this.debugObjects.pathsCurve.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2); + + // 句柄 + this.debugObjects.pathsLine.moveTo(x1, y1); + this.debugObjects.pathsLine.lineTo(cx1, cy1); + this.debugObjects.pathsLine.moveTo(x2, y2); + this.debugObjects.pathsLine.lineTo(cx2, cy2); + } + nn -= 4; + for (let ii = 4; ii < nn; ii += 6) { + const cx1 = world[ii], + cy1 = world[ii + 1], + cx2 = world[ii + 2], + cy2 = world[ii + 3]; + x2 = world[ii + 4]; + y2 = world[ii + 5]; + // 曲线 + this.debugObjects.pathsCurve.moveTo(x1, y1); + this.debugObjects.pathsCurve.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2); + + // 句柄 + this.debugObjects.pathsLine.moveTo(x1, y1); + this.debugObjects.pathsLine.lineTo(cx1, cy1); + this.debugObjects.pathsLine.moveTo(x2, y2); + this.debugObjects.pathsLine.lineTo(cx2, cy2); + x1 = x2; + y1 = y2; + } + } + } + } destroy(options?: any): void { @@ -1252,7 +1265,7 @@ export abstract class SpineBase { + + /** The left edge of the axis aligned bounding box. */ + minX = 0; + + /** The bottom edge of the axis aligned bounding box. */ + minY = 0; + + /** The right edge of the axis aligned bounding box. */ + maxX = 0; + + /** The top edge of the axis aligned bounding box. */ + maxY = 0; + + /** The visible bounding boxes. */ + boundingBoxes = new Array(); + + /** The world vertices for the bounding box polygons. */ + polygons = new Array(); + + private polygonPool = new Pool(() => { + return Utils.newFloatArray(16); + }); + + /** Clears any previous polygons, finds all visible bounding box attachments, and computes the world vertices for each bounding + * box's polygon. + * @param updateAabb If true, the axis aligned bounding box containing all the polygons is computed. If false, the + * SkeletonBounds AABB methods will always return true. */ + update (skeleton: ISkeleton, updateAabb: boolean) { + if (!skeleton) throw new Error("skeleton cannot be null."); + let boundingBoxes = this.boundingBoxes; + let polygons = this.polygons; + let polygonPool = this.polygonPool; + let slots = skeleton.slots; + let slotCount = slots.length; + + boundingBoxes.length = 0; + polygonPool.freeAll(polygons); + polygons.length = 0; + + for (let i = 0; i < slotCount; i++) { + let slot = slots[i]; + if (!slot.bone.active) continue; + let attachment = slot.getAttachment(); + if (attachment.type === AttachmentType.BoundingBox) { + let boundingBox = attachment as BoundingBoxAttachment; + boundingBoxes.push(boundingBox); + + let polygon = polygonPool.obtain() as NumberArrayLike; + if (polygon.length != boundingBox.worldVerticesLength) { + polygon = Utils.newFloatArray(boundingBox.worldVerticesLength); + } + polygons.push(polygon); + boundingBox.computeWorldVertices(slot, 0, boundingBox.worldVerticesLength, polygon, 0, 2); + } + } + + if (updateAabb) { + this.aabbCompute(); + } else { + this.minX = Number.POSITIVE_INFINITY; + this.minY = Number.POSITIVE_INFINITY; + this.maxX = Number.NEGATIVE_INFINITY; + this.maxY = Number.NEGATIVE_INFINITY; + } + } + + aabbCompute () { + let minX = Number.POSITIVE_INFINITY, minY = Number.POSITIVE_INFINITY, maxX = Number.NEGATIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY; + let polygons = this.polygons; + for (let i = 0, n = polygons.length; i < n; i++) { + let polygon = polygons[i]; + let vertices = polygon; + for (let ii = 0, nn = polygon.length; ii < nn; ii += 2) { + let x = vertices[ii]; + let y = vertices[ii + 1]; + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } + } + this.minX = minX; + this.minY = minY; + this.maxX = maxX; + this.maxY = maxY; + } + + /** Returns true if the axis aligned bounding box contains the point. */ + aabbContainsPoint (x: number, y: number) { + return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY; + } + + /** Returns true if the axis aligned bounding box intersects the line segment. */ + aabbIntersectsSegment (x1: number, y1: number, x2: number, y2: number) { + let minX = this.minX; + let minY = this.minY; + let maxX = this.maxX; + let maxY = this.maxY; + if ((x1 <= minX && x2 <= minX) || (y1 <= minY && y2 <= minY) || (x1 >= maxX && x2 >= maxX) || (y1 >= maxY && y2 >= maxY)) + return false; + let m = (y2 - y1) / (x2 - x1); + let y = m * (minX - x1) + y1; + if (y > minY && y < maxY) return true; + y = m * (maxX - x1) + y1; + if (y > minY && y < maxY) return true; + let x = (minY - y1) / m + x1; + if (x > minX && x < maxX) return true; + x = (maxY - y1) / m + x1; + if (x > minX && x < maxX) return true; + return false; + } + + /** Returns true if the axis aligned bounding box intersects the axis aligned bounding box of the specified bounds. */ + aabbIntersectsSkeleton (bounds: SkeletonBoundsBase) { + return this.minX < bounds.maxX && this.maxX > bounds.minX && this.minY < bounds.maxY && this.maxY > bounds.minY; + } + + /** Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more + * efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true. + * Cannot be done here because BoundingBoxAttachment is not a thing yet*/ + containsPoint (x: number, y: number): BoundingBoxAttachment | null { + let polygons = this.polygons; + for (let i = 0, n = polygons.length; i < n; i++) + if (this.containsPointPolygon(polygons[i], x, y)) return this.boundingBoxes[i]; + return null; + } + + /** Returns true if the polygon contains the point. */ + containsPointPolygon (polygon: NumberArrayLike, x: number, y: number) { + let vertices = polygon; + let nn = polygon.length; + + let prevIndex = nn - 2; + let inside = false; + for (let ii = 0; ii < nn; ii += 2) { + let vertexY = vertices[ii + 1]; + let prevY = vertices[prevIndex + 1]; + if ((vertexY < y && prevY >= y) || (prevY < y && vertexY >= y)) { + let vertexX = vertices[ii]; + if (vertexX + (y - vertexY) / (prevY - vertexY) * (vertices[prevIndex] - vertexX) < x) inside = !inside; + } + prevIndex = ii; + } + return inside; + } + + /** Returns the first bounding box attachment that contains any part of the line segment, or null. When doing many checks, it + * is usually more efficient to only call this method if {@link #aabbIntersectsSegment()} returns + * true. */ + intersectsSegment (x1: number, y1: number, x2: number, y2: number) { + let polygons = this.polygons; + for (let i = 0, n = polygons.length; i < n; i++) + if (this.intersectsSegmentPolygon(polygons[i], x1, y1, x2, y2)) return this.boundingBoxes[i]; + return null; + } + + /** Returns true if the polygon contains any part of the line segment. */ + intersectsSegmentPolygon (polygon: NumberArrayLike, x1: number, y1: number, x2: number, y2: number) { + let vertices = polygon; + let nn = polygon.length; + + let width12 = x1 - x2, height12 = y1 - y2; + let det1 = x1 * y2 - y1 * x2; + let x3 = vertices[nn - 2], y3 = vertices[nn - 1]; + for (let ii = 0; ii < nn; ii += 2) { + let x4 = vertices[ii], y4 = vertices[ii + 1]; + let det2 = x3 * y4 - y3 * x4; + let width34 = x3 - x4, height34 = y3 - y4; + let det3 = width12 * height34 - height12 * width34; + let x = (det1 * width34 - width12 * det2) / det3; + if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) { + let y = (det1 * height34 - height12 * det2) / det3; + if (((y >= y3 && y <= y4) || (y >= y4 && y <= y3)) && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) return true; + } + x3 = x4; + y3 = y4; + } + return false; + } + + /** Returns the polygon for the specified bounding box, or null. */ + getPolygon (boundingBox: BoundingBoxAttachment) { + if (!boundingBox) throw new Error("boundingBox cannot be null."); + let index = this.boundingBoxes.indexOf(boundingBox); + return index == -1 ? null : this.polygons[index]; + } + + /** The width of the axis aligned bounding box. */ + getWidth () { + return this.maxX - this.minX; + } + + /** The height of the axis aligned bounding box. */ + getHeight () { + return this.maxY - this.minY; + } +} diff --git a/packages/base/src/index.ts b/packages/base/src/index.ts index 38259601..a292d9b2 100644 --- a/packages/base/src/index.ts +++ b/packages/base/src/index.ts @@ -7,6 +7,7 @@ export * from './core/ISkeleton'; export * from './core/TextureAtlas'; export * from './core/TextureRegion'; export * from './core/Utils'; +export * from './core/SkeletonBoundsBase'; export * from './settings'; export * from './SpineBase'; diff --git a/packages/runtime-3.7/src/core/Bone.ts b/packages/runtime-3.7/src/core/Bone.ts index a1f74eeb..6685f9f5 100644 --- a/packages/runtime-3.7/src/core/Bone.ts +++ b/packages/runtime-3.7/src/core/Bone.ts @@ -51,6 +51,9 @@ export class Bone implements Updatable, IBone { this.setToSetupPose(); } + /** NOT USED IN 3.7. Needed for the debug graph code */ + active: boolean = true; + /** Same as {@link #updateWorldTransform()}. This method exists for Bone to implement {@link Updatable}. */ update() { this.updateWorldTransformWith(this.x, this.y, this.rotation, this.scaleX, this.scaleY, this.shearX, this.shearY); diff --git a/packages/runtime-3.7/src/core/SkeletonBounds.ts b/packages/runtime-3.7/src/core/SkeletonBounds.ts index 37a9562a..cce4ffe0 100644 --- a/packages/runtime-3.7/src/core/SkeletonBounds.ts +++ b/packages/runtime-3.7/src/core/SkeletonBounds.ts @@ -1,181 +1,8 @@ import {BoundingBoxAttachment} from "./attachments"; -import {Pool, Utils} from "@pixi-spine/base"; -import type {Skeleton} from "./Skeleton"; +import {SkeletonBoundsBase} from "@pixi-spine/base"; -/** +/** Collects each visible {@link BoundingBoxAttachment} and computes the world vertices for its polygon. The polygon vertices are + * provided along with convenience methods for doing hit detection. * @public - */ -export class SkeletonBounds { - minX = 0; minY = 0; maxX = 0; maxY = 0; - boundingBoxes = new Array(); - polygons = new Array>(); - private polygonPool = new Pool>(() => { - return Utils.newFloatArray(16); - }); - - update (skeleton: Skeleton, updateAabb: boolean) { - if (skeleton == null) throw new Error("skeleton cannot be null."); - let boundingBoxes = this.boundingBoxes; - let polygons = this.polygons; - let polygonPool = this.polygonPool; - let slots = skeleton.slots; - let slotCount = slots.length; - - boundingBoxes.length = 0; - polygonPool.freeAll(polygons); - polygons.length = 0; - - for (let i = 0; i < slotCount; i++) { - let slot = slots[i]; - let attachment = slot.getAttachment(); - if (attachment instanceof BoundingBoxAttachment) { - let boundingBox = attachment as BoundingBoxAttachment; - boundingBoxes.push(boundingBox); - - let polygon = polygonPool.obtain(); - if (polygon.length != boundingBox.worldVerticesLength) { - polygon = Utils.newFloatArray(boundingBox.worldVerticesLength); - } - polygons.push(polygon); - boundingBox.computeWorldVertices(slot, 0, boundingBox.worldVerticesLength, polygon, 0, 2); - } - } - - if (updateAabb) { - this.aabbCompute(); - } else { - this.minX = Number.POSITIVE_INFINITY; - this.minY = Number.POSITIVE_INFINITY; - this.maxX = Number.NEGATIVE_INFINITY; - this.maxY = Number.NEGATIVE_INFINITY; - } - } - - aabbCompute () { - let minX = Number.POSITIVE_INFINITY, minY = Number.POSITIVE_INFINITY, maxX = Number.NEGATIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY; - let polygons = this.polygons; - for (let i = 0, n = polygons.length; i < n; i++) { - let polygon = polygons[i]; - let vertices = polygon; - for (let ii = 0, nn = polygon.length; ii < nn; ii += 2) { - let x = vertices[ii]; - let y = vertices[ii + 1]; - minX = Math.min(minX, x); - minY = Math.min(minY, y); - maxX = Math.max(maxX, x); - maxY = Math.max(maxY, y); - } - } - this.minX = minX; - this.minY = minY; - this.maxX = maxX; - this.maxY = maxY; - } - - /** Returns true if the axis aligned bounding box contains the point. */ - aabbContainsPoint (x: number, y: number) { - return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY; - } - - /** Returns true if the axis aligned bounding box intersects the line segment. */ - aabbIntersectsSegment (x1: number, y1: number, x2: number, y2: number) { - let minX = this.minX; - let minY = this.minY; - let maxX = this.maxX; - let maxY = this.maxY; - if ((x1 <= minX && x2 <= minX) || (y1 <= minY && y2 <= minY) || (x1 >= maxX && x2 >= maxX) || (y1 >= maxY && y2 >= maxY)) - return false; - let m = (y2 - y1) / (x2 - x1); - let y = m * (minX - x1) + y1; - if (y > minY && y < maxY) return true; - y = m * (maxX - x1) + y1; - if (y > minY && y < maxY) return true; - let x = (minY - y1) / m + x1; - if (x > minX && x < maxX) return true; - x = (maxY - y1) / m + x1; - if (x > minX && x < maxX) return true; - return false; - } - - /** Returns true if the axis aligned bounding box intersects the axis aligned bounding box of the specified bounds. */ - aabbIntersectsSkeleton (bounds: SkeletonBounds) { - return this.minX < bounds.maxX && this.maxX > bounds.minX && this.minY < bounds.maxY && this.maxY > bounds.minY; - } - - /** Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more - * efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true. */ - containsPoint (x: number, y: number): BoundingBoxAttachment { - let polygons = this.polygons; - for (let i = 0, n = polygons.length; i < n; i++) - if (this.containsPointPolygon(polygons[i], x, y)) return this.boundingBoxes[i]; - return null; - } - - /** Returns true if the polygon contains the point. */ - containsPointPolygon (polygon: ArrayLike, x: number, y: number) { - let vertices = polygon; - let nn = polygon.length; - - let prevIndex = nn - 2; - let inside = false; - for (let ii = 0; ii < nn; ii += 2) { - let vertexY = vertices[ii + 1]; - let prevY = vertices[prevIndex + 1]; - if ((vertexY < y && prevY >= y) || (prevY < y && vertexY >= y)) { - let vertexX = vertices[ii]; - if (vertexX + (y - vertexY) / (prevY - vertexY) * (vertices[prevIndex] - vertexX) < x) inside = !inside; - } - prevIndex = ii; - } - return inside; - } - - /** Returns the first bounding box attachment that contains any part of the line segment, or null. When doing many checks, it - * is usually more efficient to only call this method if {@link #aabbIntersectsSegment(float, float, float, float)} returns - * true. */ - intersectsSegment (x1: number, y1: number, x2: number, y2: number) { - let polygons = this.polygons; - for (let i = 0, n = polygons.length; i < n; i++) - if (this.intersectsSegmentPolygon(polygons[i], x1, y1, x2, y2)) return this.boundingBoxes[i]; - return null; - } - - /** Returns true if the polygon contains any part of the line segment. */ - intersectsSegmentPolygon (polygon: ArrayLike, x1: number, y1: number, x2: number, y2: number) { - let vertices = polygon; - let nn = polygon.length; - - let width12 = x1 - x2, height12 = y1 - y2; - let det1 = x1 * y2 - y1 * x2; - let x3 = vertices[nn - 2], y3 = vertices[nn - 1]; - for (let ii = 0; ii < nn; ii += 2) { - let x4 = vertices[ii], y4 = vertices[ii + 1]; - let det2 = x3 * y4 - y3 * x4; - let width34 = x3 - x4, height34 = y3 - y4; - let det3 = width12 * height34 - height12 * width34; - let x = (det1 * width34 - width12 * det2) / det3; - if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) { - let y = (det1 * height34 - height12 * det2) / det3; - if (((y >= y3 && y <= y4) || (y >= y4 && y <= y3)) && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) return true; - } - x3 = x4; - y3 = y4; - } - return false; - } - - /** Returns the polygon for the specified bounding box, or null. */ - getPolygon (boundingBox: BoundingBoxAttachment) { - if (boundingBox == null) throw new Error("boundingBox cannot be null."); - let index = this.boundingBoxes.indexOf(boundingBox); - return index == -1 ? null : this.polygons[index]; - } - - getWidth () { - return this.maxX - this.minX; - } - - getHeight () { - return this.maxY - this.minY; - } -} + * */ + export class SkeletonBounds extends SkeletonBoundsBase{}; \ No newline at end of file diff --git a/packages/runtime-3.7/src/core/attachments/RegionAttachment.ts b/packages/runtime-3.7/src/core/attachments/RegionAttachment.ts index 311eb33c..eb836ed8 100644 --- a/packages/runtime-3.7/src/core/attachments/RegionAttachment.ts +++ b/packages/runtime-3.7/src/core/attachments/RegionAttachment.ts @@ -3,6 +3,7 @@ import {Attachment} from './Attachment'; import {AttachmentType, ArrayLike, Color, TextureRegion, Utils, IRegionAttachment} from "@pixi-spine/base"; import type {Bone} from '../Bone'; +import { Slot } from '../Slot'; /** * @public @@ -130,9 +131,9 @@ export class RegionAttachment extends Attachment implements IRegionAttachment { } } - computeWorldVertices(bone: Bone, worldVertices: ArrayLike, offset: number, stride: number) { + computeWorldVertices(bone: Bone | Slot, worldVertices: ArrayLike, offset: number, stride: number) { let vertexOffset = this.offset; - let mat = bone.matrix; + let mat = bone instanceof Slot? bone.bone.matrix : bone.matrix; let x = mat.tx, y = mat.ty; let a = mat.a, b = mat.c, c = mat.b, d = mat.d; let offsetX = 0, offsetY = 0; diff --git a/packages/runtime-3.8/src/core/SkeletonBounds.ts b/packages/runtime-3.8/src/core/SkeletonBounds.ts index a7795801..cce4ffe0 100644 --- a/packages/runtime-3.8/src/core/SkeletonBounds.ts +++ b/packages/runtime-3.8/src/core/SkeletonBounds.ts @@ -1,182 +1,8 @@ import {BoundingBoxAttachment} from "./attachments"; -import {Pool, Utils} from "@pixi-spine/base"; -import type {Skeleton} from "./Skeleton"; +import {SkeletonBoundsBase} from "@pixi-spine/base"; -/** +/** Collects each visible {@link BoundingBoxAttachment} and computes the world vertices for its polygon. The polygon vertices are + * provided along with convenience methods for doing hit detection. * @public - */ -export class SkeletonBounds { - minX = 0; minY = 0; maxX = 0; maxY = 0; - boundingBoxes = new Array(); - polygons = new Array>(); - private polygonPool = new Pool>(() => { - return Utils.newFloatArray(16); - }); - - update (skeleton: Skeleton, updateAabb: boolean) { - if (skeleton == null) throw new Error("skeleton cannot be null."); - let boundingBoxes = this.boundingBoxes; - let polygons = this.polygons; - let polygonPool = this.polygonPool; - let slots = skeleton.slots; - let slotCount = slots.length; - - boundingBoxes.length = 0; - polygonPool.freeAll(polygons); - polygons.length = 0; - - for (let i = 0; i < slotCount; i++) { - let slot = slots[i]; - if (!slot.bone.active) continue; - let attachment = slot.getAttachment(); - if (attachment instanceof BoundingBoxAttachment) { - let boundingBox = attachment as BoundingBoxAttachment; - boundingBoxes.push(boundingBox); - - let polygon = polygonPool.obtain(); - if (polygon.length != boundingBox.worldVerticesLength) { - polygon = Utils.newFloatArray(boundingBox.worldVerticesLength); - } - polygons.push(polygon); - boundingBox.computeWorldVertices(slot, 0, boundingBox.worldVerticesLength, polygon, 0, 2); - } - } - - if (updateAabb) { - this.aabbCompute(); - } else { - this.minX = Number.POSITIVE_INFINITY; - this.minY = Number.POSITIVE_INFINITY; - this.maxX = Number.NEGATIVE_INFINITY; - this.maxY = Number.NEGATIVE_INFINITY; - } - } - - aabbCompute () { - let minX = Number.POSITIVE_INFINITY, minY = Number.POSITIVE_INFINITY, maxX = Number.NEGATIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY; - let polygons = this.polygons; - for (let i = 0, n = polygons.length; i < n; i++) { - let polygon = polygons[i]; - let vertices = polygon; - for (let ii = 0, nn = polygon.length; ii < nn; ii += 2) { - let x = vertices[ii]; - let y = vertices[ii + 1]; - minX = Math.min(minX, x); - minY = Math.min(minY, y); - maxX = Math.max(maxX, x); - maxY = Math.max(maxY, y); - } - } - this.minX = minX; - this.minY = minY; - this.maxX = maxX; - this.maxY = maxY; - } - - /** Returns true if the axis aligned bounding box contains the point. */ - aabbContainsPoint (x: number, y: number) { - return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY; - } - - /** Returns true if the axis aligned bounding box intersects the line segment. */ - aabbIntersectsSegment (x1: number, y1: number, x2: number, y2: number) { - let minX = this.minX; - let minY = this.minY; - let maxX = this.maxX; - let maxY = this.maxY; - if ((x1 <= minX && x2 <= minX) || (y1 <= minY && y2 <= minY) || (x1 >= maxX && x2 >= maxX) || (y1 >= maxY && y2 >= maxY)) - return false; - let m = (y2 - y1) / (x2 - x1); - let y = m * (minX - x1) + y1; - if (y > minY && y < maxY) return true; - y = m * (maxX - x1) + y1; - if (y > minY && y < maxY) return true; - let x = (minY - y1) / m + x1; - if (x > minX && x < maxX) return true; - x = (maxY - y1) / m + x1; - if (x > minX && x < maxX) return true; - return false; - } - - /** Returns true if the axis aligned bounding box intersects the axis aligned bounding box of the specified bounds. */ - aabbIntersectsSkeleton (bounds: SkeletonBounds) { - return this.minX < bounds.maxX && this.maxX > bounds.minX && this.minY < bounds.maxY && this.maxY > bounds.minY; - } - - /** Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more - * efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true. */ - containsPoint (x: number, y: number): BoundingBoxAttachment { - let polygons = this.polygons; - for (let i = 0, n = polygons.length; i < n; i++) - if (this.containsPointPolygon(polygons[i], x, y)) return this.boundingBoxes[i]; - return null; - } - - /** Returns true if the polygon contains the point. */ - containsPointPolygon (polygon: ArrayLike, x: number, y: number) { - let vertices = polygon; - let nn = polygon.length; - - let prevIndex = nn - 2; - let inside = false; - for (let ii = 0; ii < nn; ii += 2) { - let vertexY = vertices[ii + 1]; - let prevY = vertices[prevIndex + 1]; - if ((vertexY < y && prevY >= y) || (prevY < y && vertexY >= y)) { - let vertexX = vertices[ii]; - if (vertexX + (y - vertexY) / (prevY - vertexY) * (vertices[prevIndex] - vertexX) < x) inside = !inside; - } - prevIndex = ii; - } - return inside; - } - - /** Returns the first bounding box attachment that contains any part of the line segment, or null. When doing many checks, it - * is usually more efficient to only call this method if {@link #aabbIntersectsSegment(float, float, float, float)} returns - * true. */ - intersectsSegment (x1: number, y1: number, x2: number, y2: number) { - let polygons = this.polygons; - for (let i = 0, n = polygons.length; i < n; i++) - if (this.intersectsSegmentPolygon(polygons[i], x1, y1, x2, y2)) return this.boundingBoxes[i]; - return null; - } - - /** Returns true if the polygon contains any part of the line segment. */ - intersectsSegmentPolygon (polygon: ArrayLike, x1: number, y1: number, x2: number, y2: number) { - let vertices = polygon; - let nn = polygon.length; - - let width12 = x1 - x2, height12 = y1 - y2; - let det1 = x1 * y2 - y1 * x2; - let x3 = vertices[nn - 2], y3 = vertices[nn - 1]; - for (let ii = 0; ii < nn; ii += 2) { - let x4 = vertices[ii], y4 = vertices[ii + 1]; - let det2 = x3 * y4 - y3 * x4; - let width34 = x3 - x4, height34 = y3 - y4; - let det3 = width12 * height34 - height12 * width34; - let x = (det1 * width34 - width12 * det2) / det3; - if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) { - let y = (det1 * height34 - height12 * det2) / det3; - if (((y >= y3 && y <= y4) || (y >= y4 && y <= y3)) && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) return true; - } - x3 = x4; - y3 = y4; - } - return false; - } - - /** Returns the polygon for the specified bounding box, or null. */ - getPolygon (boundingBox: BoundingBoxAttachment) { - if (boundingBox == null) throw new Error("boundingBox cannot be null."); - let index = this.boundingBoxes.indexOf(boundingBox); - return index == -1 ? null : this.polygons[index]; - } - - getWidth () { - return this.maxX - this.minX; - } - - getHeight () { - return this.maxY - this.minY; - } -} + * */ + export class SkeletonBounds extends SkeletonBoundsBase{}; \ No newline at end of file diff --git a/packages/runtime-3.8/src/core/attachments/RegionAttachment.ts b/packages/runtime-3.8/src/core/attachments/RegionAttachment.ts index 8fb06e2a..3ac45218 100644 --- a/packages/runtime-3.8/src/core/attachments/RegionAttachment.ts +++ b/packages/runtime-3.8/src/core/attachments/RegionAttachment.ts @@ -2,6 +2,7 @@ import {Attachment} from './Attachment'; import {AttachmentType, ArrayLike, Color, TextureRegion, Utils, IRegionAttachment} from "@pixi-spine/base"; import type {Bone} from '../Bone'; +import { Slot } from '../Slot'; /** * @public @@ -129,9 +130,9 @@ export class RegionAttachment extends Attachment implements IRegionAttachment { } } - computeWorldVertices(bone: Bone, worldVertices: ArrayLike, offset: number, stride: number) { + computeWorldVertices(bone: Bone | Slot, worldVertices: ArrayLike, offset: number, stride: number) { let vertexOffset = this.offset; - let mat = bone.matrix; + let mat = bone instanceof Slot? bone.bone.matrix : bone.matrix; let x = mat.tx, y = mat.ty; let a = mat.a, b = mat.c, c = mat.b, d = mat.d; let offsetX = 0, offsetY = 0; diff --git a/packages/runtime-4.0/src/core/SkeletonBounds.ts b/packages/runtime-4.0/src/core/SkeletonBounds.ts index abf034cf..cce4ffe0 100644 --- a/packages/runtime-4.0/src/core/SkeletonBounds.ts +++ b/packages/runtime-4.0/src/core/SkeletonBounds.ts @@ -1,205 +1,8 @@ import {BoundingBoxAttachment} from "./attachments"; -import {Pool, Utils} from "@pixi-spine/base"; -import type {Skeleton} from "./Skeleton"; +import {SkeletonBoundsBase} from "@pixi-spine/base"; /** Collects each visible {@link BoundingBoxAttachment} and computes the world vertices for its polygon. The polygon vertices are * provided along with convenience methods for doing hit detection. * @public * */ -export class SkeletonBounds { - - /** The left edge of the axis aligned bounding box. */ - minX = 0; - - /** The bottom edge of the axis aligned bounding box. */ - minY = 0; - - /** The right edge of the axis aligned bounding box. */ - maxX = 0; - - /** The top edge of the axis aligned bounding box. */ - maxY = 0; - - /** The visible bounding boxes. */ - boundingBoxes = new Array(); - - /** The world vertices for the bounding box polygons. */ - polygons = new Array>(); - - private polygonPool = new Pool>(() => { - return Utils.newFloatArray(16); - }); - - /** Clears any previous polygons, finds all visible bounding box attachments, and computes the world vertices for each bounding - * box's polygon. - * @param updateAabb If true, the axis aligned bounding box containing all the polygons is computed. If false, the - * SkeletonBounds AABB methods will always return true. */ - update (skeleton: Skeleton, updateAabb: boolean) { - if (skeleton == null) throw new Error("skeleton cannot be null."); - let boundingBoxes = this.boundingBoxes; - let polygons = this.polygons; - let polygonPool = this.polygonPool; - let slots = skeleton.slots; - let slotCount = slots.length; - - boundingBoxes.length = 0; - polygonPool.freeAll(polygons); - polygons.length = 0; - - for (let i = 0; i < slotCount; i++) { - let slot = slots[i]; - if (!slot.bone.active) continue; - let attachment = slot.getAttachment(); - if (attachment instanceof BoundingBoxAttachment) { - let boundingBox = attachment as BoundingBoxAttachment; - boundingBoxes.push(boundingBox); - - let polygon = polygonPool.obtain(); - if (polygon.length != boundingBox.worldVerticesLength) { - polygon = Utils.newFloatArray(boundingBox.worldVerticesLength); - } - polygons.push(polygon); - boundingBox.computeWorldVertices(slot, 0, boundingBox.worldVerticesLength, polygon, 0, 2); - } - } - - if (updateAabb) { - this.aabbCompute(); - } else { - this.minX = Number.POSITIVE_INFINITY; - this.minY = Number.POSITIVE_INFINITY; - this.maxX = Number.NEGATIVE_INFINITY; - this.maxY = Number.NEGATIVE_INFINITY; - } - } - - aabbCompute () { - let minX = Number.POSITIVE_INFINITY, minY = Number.POSITIVE_INFINITY, maxX = Number.NEGATIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY; - let polygons = this.polygons; - for (let i = 0, n = polygons.length; i < n; i++) { - let polygon = polygons[i]; - let vertices = polygon; - for (let ii = 0, nn = polygon.length; ii < nn; ii += 2) { - let x = vertices[ii]; - let y = vertices[ii + 1]; - minX = Math.min(minX, x); - minY = Math.min(minY, y); - maxX = Math.max(maxX, x); - maxY = Math.max(maxY, y); - } - } - this.minX = minX; - this.minY = minY; - this.maxX = maxX; - this.maxY = maxY; - } - - /** Returns true if the axis aligned bounding box contains the point. */ - aabbContainsPoint (x: number, y: number) { - return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY; - } - - /** Returns true if the axis aligned bounding box intersects the line segment. */ - aabbIntersectsSegment (x1: number, y1: number, x2: number, y2: number) { - let minX = this.minX; - let minY = this.minY; - let maxX = this.maxX; - let maxY = this.maxY; - if ((x1 <= minX && x2 <= minX) || (y1 <= minY && y2 <= minY) || (x1 >= maxX && x2 >= maxX) || (y1 >= maxY && y2 >= maxY)) - return false; - let m = (y2 - y1) / (x2 - x1); - let y = m * (minX - x1) + y1; - if (y > minY && y < maxY) return true; - y = m * (maxX - x1) + y1; - if (y > minY && y < maxY) return true; - let x = (minY - y1) / m + x1; - if (x > minX && x < maxX) return true; - x = (maxY - y1) / m + x1; - if (x > minX && x < maxX) return true; - return false; - } - - /** Returns true if the axis aligned bounding box intersects the axis aligned bounding box of the specified bounds. */ - aabbIntersectsSkeleton (bounds: SkeletonBounds) { - return this.minX < bounds.maxX && this.maxX > bounds.minX && this.minY < bounds.maxY && this.maxY > bounds.minY; - } - - /** Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more - * efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true. */ - containsPoint (x: number, y: number): BoundingBoxAttachment { - let polygons = this.polygons; - for (let i = 0, n = polygons.length; i < n; i++) - if (this.containsPointPolygon(polygons[i], x, y)) return this.boundingBoxes[i]; - return null; - } - - /** Returns true if the polygon contains the point. */ - containsPointPolygon (polygon: ArrayLike, x: number, y: number) { - let vertices = polygon; - let nn = polygon.length; - - let prevIndex = nn - 2; - let inside = false; - for (let ii = 0; ii < nn; ii += 2) { - let vertexY = vertices[ii + 1]; - let prevY = vertices[prevIndex + 1]; - if ((vertexY < y && prevY >= y) || (prevY < y && vertexY >= y)) { - let vertexX = vertices[ii]; - if (vertexX + (y - vertexY) / (prevY - vertexY) * (vertices[prevIndex] - vertexX) < x) inside = !inside; - } - prevIndex = ii; - } - return inside; - } - - /** Returns the first bounding box attachment that contains any part of the line segment, or null. When doing many checks, it - * is usually more efficient to only call this method if {@link #aabbIntersectsSegment()} returns - * true. */ - intersectsSegment (x1: number, y1: number, x2: number, y2: number) { - let polygons = this.polygons; - for (let i = 0, n = polygons.length; i < n; i++) - if (this.intersectsSegmentPolygon(polygons[i], x1, y1, x2, y2)) return this.boundingBoxes[i]; - return null; - } - - /** Returns true if the polygon contains any part of the line segment. */ - intersectsSegmentPolygon (polygon: ArrayLike, x1: number, y1: number, x2: number, y2: number) { - let vertices = polygon; - let nn = polygon.length; - - let width12 = x1 - x2, height12 = y1 - y2; - let det1 = x1 * y2 - y1 * x2; - let x3 = vertices[nn - 2], y3 = vertices[nn - 1]; - for (let ii = 0; ii < nn; ii += 2) { - let x4 = vertices[ii], y4 = vertices[ii + 1]; - let det2 = x3 * y4 - y3 * x4; - let width34 = x3 - x4, height34 = y3 - y4; - let det3 = width12 * height34 - height12 * width34; - let x = (det1 * width34 - width12 * det2) / det3; - if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) { - let y = (det1 * height34 - height12 * det2) / det3; - if (((y >= y3 && y <= y4) || (y >= y4 && y <= y3)) && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) return true; - } - x3 = x4; - y3 = y4; - } - return false; - } - - /** Returns the polygon for the specified bounding box, or null. */ - getPolygon (boundingBox: BoundingBoxAttachment) { - if (boundingBox == null) throw new Error("boundingBox cannot be null."); - let index = this.boundingBoxes.indexOf(boundingBox); - return index == -1 ? null : this.polygons[index]; - } - - /** The width of the axis aligned bounding box. */ - getWidth () { - return this.maxX - this.minX; - } - - /** The height of the axis aligned bounding box. */ - getHeight () { - return this.maxY - this.minY; - } -} + export class SkeletonBounds extends SkeletonBoundsBase{}; \ No newline at end of file diff --git a/packages/runtime-4.0/src/core/attachments/RegionAttachment.ts b/packages/runtime-4.0/src/core/attachments/RegionAttachment.ts index 3e99c2c3..2a1795f7 100644 --- a/packages/runtime-4.0/src/core/attachments/RegionAttachment.ts +++ b/packages/runtime-4.0/src/core/attachments/RegionAttachment.ts @@ -2,6 +2,7 @@ import {Attachment} from './Attachment'; import {AttachmentType, ArrayLike, Color, TextureRegion, Utils, IRegionAttachment} from "@pixi-spine/base"; import type {Bone} from '../Bone'; +import { Slot } from '../Slot'; /** * @public @@ -159,9 +160,9 @@ export class RegionAttachment extends Attachment implements IRegionAttachment { * @param worldVertices The output world vertices. Must have a length >= `offset` + 8. * @param offset The `worldVertices` index to begin writing values. * @param stride The number of `worldVertices` entries between the value pairs written. */ - computeWorldVertices (bone: Bone, worldVertices: ArrayLike, offset: number, stride: number) { + computeWorldVertices (bone: Bone | Slot, worldVertices: ArrayLike, offset: number, stride: number) { let vertexOffset = this.offset; - let mat = bone.matrix; + let mat = bone instanceof Slot? bone.bone.matrix : bone.matrix; let x = mat.tx, y = mat.ty; let a = mat.a, b = mat.c, c = mat.b, d = mat.d; let offsetX = 0, offsetY = 0; diff --git a/packages/runtime-4.1/src/core/SkeletonBounds.ts b/packages/runtime-4.1/src/core/SkeletonBounds.ts index 734d9916..5eaea674 100644 --- a/packages/runtime-4.1/src/core/SkeletonBounds.ts +++ b/packages/runtime-4.1/src/core/SkeletonBounds.ts @@ -1,205 +1,8 @@ import {BoundingBoxAttachment} from "./attachments"; -import {Pool, Utils, NumberArrayLike} from "@pixi-spine/base"; -import type {Skeleton} from "./Skeleton"; +import {SkeletonBoundsBase} from "@pixi-spine/base"; /** Collects each visible {@link BoundingBoxAttachment} and computes the world vertices for its polygon. The polygon vertices are * provided along with convenience methods for doing hit detection. * @public * */ -export class SkeletonBounds { - - /** The left edge of the axis aligned bounding box. */ - minX = 0; - - /** The bottom edge of the axis aligned bounding box. */ - minY = 0; - - /** The right edge of the axis aligned bounding box. */ - maxX = 0; - - /** The top edge of the axis aligned bounding box. */ - maxY = 0; - - /** The visible bounding boxes. */ - boundingBoxes = new Array(); - - /** The world vertices for the bounding box polygons. */ - polygons = new Array(); - - private polygonPool = new Pool(() => { - return Utils.newFloatArray(16); - }); - - /** Clears any previous polygons, finds all visible bounding box attachments, and computes the world vertices for each bounding - * box's polygon. - * @param updateAabb If true, the axis aligned bounding box containing all the polygons is computed. If false, the - * SkeletonBounds AABB methods will always return true. */ - update (skeleton: Skeleton, updateAabb: boolean) { - if (!skeleton) throw new Error("skeleton cannot be null."); - let boundingBoxes = this.boundingBoxes; - let polygons = this.polygons; - let polygonPool = this.polygonPool; - let slots = skeleton.slots; - let slotCount = slots.length; - - boundingBoxes.length = 0; - polygonPool.freeAll(polygons); - polygons.length = 0; - - for (let i = 0; i < slotCount; i++) { - let slot = slots[i]; - if (!slot.bone.active) continue; - let attachment = slot.getAttachment(); - if (attachment instanceof BoundingBoxAttachment) { - let boundingBox = attachment as BoundingBoxAttachment; - boundingBoxes.push(boundingBox); - - let polygon = polygonPool.obtain(); - if (polygon.length != boundingBox.worldVerticesLength) { - polygon = Utils.newFloatArray(boundingBox.worldVerticesLength); - } - polygons.push(polygon); - boundingBox.computeWorldVertices(slot, 0, boundingBox.worldVerticesLength, polygon, 0, 2); - } - } - - if (updateAabb) { - this.aabbCompute(); - } else { - this.minX = Number.POSITIVE_INFINITY; - this.minY = Number.POSITIVE_INFINITY; - this.maxX = Number.NEGATIVE_INFINITY; - this.maxY = Number.NEGATIVE_INFINITY; - } - } - - aabbCompute () { - let minX = Number.POSITIVE_INFINITY, minY = Number.POSITIVE_INFINITY, maxX = Number.NEGATIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY; - let polygons = this.polygons; - for (let i = 0, n = polygons.length; i < n; i++) { - let polygon = polygons[i]; - let vertices = polygon; - for (let ii = 0, nn = polygon.length; ii < nn; ii += 2) { - let x = vertices[ii]; - let y = vertices[ii + 1]; - minX = Math.min(minX, x); - minY = Math.min(minY, y); - maxX = Math.max(maxX, x); - maxY = Math.max(maxY, y); - } - } - this.minX = minX; - this.minY = minY; - this.maxX = maxX; - this.maxY = maxY; - } - - /** Returns true if the axis aligned bounding box contains the point. */ - aabbContainsPoint (x: number, y: number) { - return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY; - } - - /** Returns true if the axis aligned bounding box intersects the line segment. */ - aabbIntersectsSegment (x1: number, y1: number, x2: number, y2: number) { - let minX = this.minX; - let minY = this.minY; - let maxX = this.maxX; - let maxY = this.maxY; - if ((x1 <= minX && x2 <= minX) || (y1 <= minY && y2 <= minY) || (x1 >= maxX && x2 >= maxX) || (y1 >= maxY && y2 >= maxY)) - return false; - let m = (y2 - y1) / (x2 - x1); - let y = m * (minX - x1) + y1; - if (y > minY && y < maxY) return true; - y = m * (maxX - x1) + y1; - if (y > minY && y < maxY) return true; - let x = (minY - y1) / m + x1; - if (x > minX && x < maxX) return true; - x = (maxY - y1) / m + x1; - if (x > minX && x < maxX) return true; - return false; - } - - /** Returns true if the axis aligned bounding box intersects the axis aligned bounding box of the specified bounds. */ - aabbIntersectsSkeleton (bounds: SkeletonBounds) { - return this.minX < bounds.maxX && this.maxX > bounds.minX && this.minY < bounds.maxY && this.maxY > bounds.minY; - } - - /** Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more - * efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true. */ - containsPoint (x: number, y: number): BoundingBoxAttachment | null { - let polygons = this.polygons; - for (let i = 0, n = polygons.length; i < n; i++) - if (this.containsPointPolygon(polygons[i], x, y)) return this.boundingBoxes[i]; - return null; - } - - /** Returns true if the polygon contains the point. */ - containsPointPolygon (polygon: NumberArrayLike, x: number, y: number) { - let vertices = polygon; - let nn = polygon.length; - - let prevIndex = nn - 2; - let inside = false; - for (let ii = 0; ii < nn; ii += 2) { - let vertexY = vertices[ii + 1]; - let prevY = vertices[prevIndex + 1]; - if ((vertexY < y && prevY >= y) || (prevY < y && vertexY >= y)) { - let vertexX = vertices[ii]; - if (vertexX + (y - vertexY) / (prevY - vertexY) * (vertices[prevIndex] - vertexX) < x) inside = !inside; - } - prevIndex = ii; - } - return inside; - } - - /** Returns the first bounding box attachment that contains any part of the line segment, or null. When doing many checks, it - * is usually more efficient to only call this method if {@link #aabbIntersectsSegment()} returns - * true. */ - intersectsSegment (x1: number, y1: number, x2: number, y2: number) { - let polygons = this.polygons; - for (let i = 0, n = polygons.length; i < n; i++) - if (this.intersectsSegmentPolygon(polygons[i], x1, y1, x2, y2)) return this.boundingBoxes[i]; - return null; - } - - /** Returns true if the polygon contains any part of the line segment. */ - intersectsSegmentPolygon (polygon: NumberArrayLike, x1: number, y1: number, x2: number, y2: number) { - let vertices = polygon; - let nn = polygon.length; - - let width12 = x1 - x2, height12 = y1 - y2; - let det1 = x1 * y2 - y1 * x2; - let x3 = vertices[nn - 2], y3 = vertices[nn - 1]; - for (let ii = 0; ii < nn; ii += 2) { - let x4 = vertices[ii], y4 = vertices[ii + 1]; - let det2 = x3 * y4 - y3 * x4; - let width34 = x3 - x4, height34 = y3 - y4; - let det3 = width12 * height34 - height12 * width34; - let x = (det1 * width34 - width12 * det2) / det3; - if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) { - let y = (det1 * height34 - height12 * det2) / det3; - if (((y >= y3 && y <= y4) || (y >= y4 && y <= y3)) && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) return true; - } - x3 = x4; - y3 = y4; - } - return false; - } - - /** Returns the polygon for the specified bounding box, or null. */ - getPolygon (boundingBox: BoundingBoxAttachment) { - if (!boundingBox) throw new Error("boundingBox cannot be null."); - let index = this.boundingBoxes.indexOf(boundingBox); - return index == -1 ? null : this.polygons[index]; - } - - /** The width of the axis aligned bounding box. */ - getWidth () { - return this.maxX - this.minX; - } - - /** The height of the axis aligned bounding box. */ - getHeight () { - return this.maxY - this.minY; - } -} +export class SkeletonBounds extends SkeletonBoundsBase{}; \ No newline at end of file From 5e28730d617fb4213ca656bc9b039cc7c45fd90a Mon Sep 17 00:00:00 2001 From: eCode Date: Sat, 6 Aug 2022 19:05:29 -0300 Subject: [PATCH 3/6] Translated the comments and fixed some bugs --- packages/base/src/SpineBase.ts | 170 +++++++++---------- packages/base/src/core/SkeletonBoundsBase.ts | 2 +- 2 files changed, 85 insertions(+), 87 deletions(-) diff --git a/packages/base/src/SpineBase.ts b/packages/base/src/SpineBase.ts index 33d2cf0d..e0d37f72 100644 --- a/packages/base/src/SpineBase.ts +++ b/packages/base/src/SpineBase.ts @@ -345,7 +345,7 @@ export abstract class SpineBase endX && starY < endY) { - // 左下 + // bottom left rotation = 180 * rad + B; } else if (starX > endX && starY > endY) { - // 左上 + // top left rotation = -B; } else if (starX < endX && starY > endY) { - // 左下 + // bottom left rotation = B; } else if (starY === endY && starX < endX) { - // 向右 + // To the right rotation = 90 * rad; } else if (starY === endY && starX > endX) { - // 向左 + // go left rotation = -90 * rad; } else if (starX === endX && starY < endY) { - // 向下 + // down rotation = 180 * rad; } else if (starX === endX && starY > endY) { - // 向上 + // up rotation = 0; } gp.rotation = rotation; - // 绘制骨骼起始旋转点 + // Draw the starting rotation point of the bone gp.lineStyle(lineWidth + refRation / 2.4, 0x00eecc, 1); gp.beginFill(0x000000, 0.6); gp.drawCircle(0, c, refRation * 1.2); gp.endFill(); } - // 绘制骨架起点『X』形式 + // Draw the skeleton starting point "X" form const startDotSize = lineWidth * 3; this.debugObjects.skeletonXY.moveTo(skeletonX - startDotSize, skeletonY - startDotSize); this.debugObjects.skeletonXY.lineTo(skeletonX + startDotSize, skeletonY + startDotSize); @@ -1032,7 +1032,7 @@ export abstract class SpineBase 0) { hullLength = (hullLength >> 1) * 2; let lastX = vertices[hullLength - 2], @@ -1105,7 +1104,6 @@ export abstract class SpineBase Date: Sat, 6 Aug 2022 19:20:09 -0300 Subject: [PATCH 4/6] Final touchup to the readme Thanks to @sbfkcel for the initial work that made this debugging tool possible <3 --- README.md | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e660bca9..737f9d2c 100644 --- a/README.md +++ b/README.md @@ -117,9 +117,36 @@ let spine = new PIXI.heaven.Spine(spineData); ### Debug -To show bones and bounds you can use [pixi-spine-debug](https://github.com/sbfkcel/pixi-spine-debug). If you want to write your own debug plugin, look at how this one [was created](https://github.com/pixijs/pixi-spine/issues/324) +To show bones and bounds you can set `yourSpine.drawDebug = true` +Only after you set `drawDebug` to true, debug graphics are created. -Demo: https://sbfkcel.github.io/pixi-spine-debug/ +Control what gets drawn with the following flags: + +```js +yourSpine.drawMeshHull = true; +yourSpine.drawMeshTriangles = true; +yourSpine.drawBones = true; +yourSpine.drawPaths = true; +yourSpine.drawBoundingBoxes = true; +yourSpine.drawClipping = true; +yourSpine.drawRegionAttachments = true; +``` + +To have even more control, you can customize the color and line thickness with +```js +yourSpine.debugOptions.lineWidth = 1; +yourSpine.debugOptions.regionAttachmentsColor = 0x0078ff; +yourSpine.debugOptions.meshHullColor = 0x0078ff; +yourSpine.debugOptions.meshTrianglesColor = 0xffcc00; +yourSpine.debugOptions.clippingPolygonColor = 0xff00ff; +yourSpine.debugOptions.boundingBoxesRectColor = 0x00ff00; +yourSpine.debugOptions.boundingBoxesPolygonColor = 0x00ff00; +yourSpine.debugOptions.boundingBoxesCircleColor = 0x00ff00; +yourSpine.debugOptions.pathsCurveColor = 0xff0000; +yourSpine.debugOptions.pathsLineColor = 0xff00ff; +yourSpine.debugOptions.skeletonXYColor = 0xff0000; +yourSpine.debugOptions.bonesColor = 0x00eecc; +``` ## Build & Development From 036f9f5b34eb0da92ad6e572d2304aab81a9ffd2 Mon Sep 17 00:00:00 2001 From: eCode Date: Sun, 7 Aug 2022 19:53:45 -0300 Subject: [PATCH 5/6] externalized the SpineDebugRenderer --- packages/base/src/SpineBase.ts | 492 +----------------------- packages/base/src/SpineDebugRenderer.ts | 472 +++++++++++++++++++++++ packages/base/src/index.ts | 1 + 3 files changed, 486 insertions(+), 479 deletions(-) create mode 100644 packages/base/src/SpineDebugRenderer.ts diff --git a/packages/base/src/SpineBase.ts b/packages/base/src/SpineBase.ts index e0d37f72..8d893342 100644 --- a/packages/base/src/SpineBase.ts +++ b/packages/base/src/SpineBase.ts @@ -1,7 +1,6 @@ import {AttachmentType} from './core/AttachmentType'; import {TextureRegion} from './core/TextureRegion'; import {MathUtils} from './core/Utils'; -import {SkeletonBoundsBase} from './core/SkeletonBoundsBase'; import type { IAnimationState, IAnimationStateData @@ -24,41 +23,10 @@ import {Rectangle, Polygon, Transform} from '@pixi/math'; import {hex2rgb, rgb2hex} from '@pixi/utils'; import type {Texture} from '@pixi/core'; import {settings} from "./settings"; +import { ISpineDebugRenderer } from './SpineDebugRenderer'; let tempRgb = [0, 0, 0]; -type DebugDisplayObjects = { - bones: Container; - skeletonXY: Graphics; - regionAttachmentsShape: Graphics; - meshTrianglesLine: Graphics; - meshHullLine: Graphics; - clippingPolygon: Graphics; - boundingBoxesRect: Graphics; - boundingBoxesCircle: Graphics; - boundingBoxesPolygon: Graphics; - pathsCurve: Graphics; - pathsLine: Graphics; -} - -/** - * @public - */ -export interface SpineDebugOptions { - lineWidth: number; // 1 - regionAttachmentsColor: number; // 0x0078ff - meshHullColor: number; // 0x0078ff - meshTrianglesColor: number; // 0xffcc00 - clippingPolygonColor: number; // 0xff00ff - boundingBoxesRectColor: number; // 0x00ff00 - boundingBoxesPolygonColor: number; // 0x00ff00 - boundingBoxesCircleColor: number; // 0x00ff00 - pathsCurveColor: number; // 0xff0000 - pathsLineColor: number; // 0xff00ff - skeletonXYColor:number; // 0xff0000 - bonesColor:number; // 0x00eecc -} - /** * @public */ @@ -117,37 +85,18 @@ export abstract class SpineBase 0; len--) { - elem.children[len - 1].destroy({ children: true, texture: true, baseTexture: true }); - } - } - } - - const scale = this.scale.x || this.scale.y || 1; - const lineWidth = this.debugOptions.lineWidth / scale; - - if (this.drawBones) { - this.drawBonesFunc(lineWidth, scale); - } - - if (this.drawPaths) { - this.drawPathsFunc(lineWidth); - } - - if (this.drawBoundingBoxes) { - this.drawBoundingBoxesFunc(lineWidth); - } - - if (this.drawClipping) { - this.drawClippingFunc(lineWidth); - } - - if (this.drawMeshHull || this.drawMeshTriangles) { - this.drawMeshHullAndMeshTriangles(lineWidth); - } - - if (this.drawRegionAttachments) { - this.drawRegionAttachmentsFunc(lineWidth); - } - } - - private drawBonesFunc(lineWidth:number, scale:number): void { - const skeleton = this.skeleton; - const skeletonX = skeleton.x; - const skeletonY = skeleton.y; - const bones = skeleton.bones; - - this.debugObjects.skeletonXY.lineStyle(lineWidth, 0xff0000, 1); - - for (let i = 0, len = bones.length; i < len; i++) { - const bone = bones[i], - boneLen = bone.data.length, - starX = skeletonX + bone.matrix.tx, - starY = skeletonY + bone.matrix.ty, - endX = skeletonX + boneLen * bone.matrix.a + bone.matrix.tx, - endY = skeletonY + boneLen * bone.matrix.b + bone.matrix.ty; - - if (bone.data.name === "root" || bone.data.parent === null) { - continue; - } - - // Triangle calculation formula - // area: A=sqrt((a+b+c)*(-a+b+c)*(a-b+c)*(a+b-c))/4 - // alpha: alpha=acos((pow(b, 2)+pow(c, 2)-pow(a, 2))/(2*b*c)) - // beta: beta=acos((pow(a, 2)+pow(c, 2)-pow(b, 2))/(2*a*c)) - // gamma: gamma=acos((pow(a, 2)+pow(b, 2)-pow(c, 2))/(2*a*b)) - - const w = Math.abs(starX - endX), - h = Math.abs(starY - endY), - // a = w, // side length a - a2 = Math.pow(w, 2), // square root of side length a - b = h, // side length b - b2 = Math.pow(h, 2), // square root of side length b - c = Math.sqrt(a2 + b2), // side length c - c2 = Math.pow(c, 2), // square root of side length c - rad = Math.PI / 180, - // A = Math.acos([a2 + c2 - b2] / [2 * a * c]) || 0, // Angle A - // C = Math.acos([a2 + b2 - c2] / [2 * a * b]) || 0, // C angle - B = Math.acos((c2 + b2 - a2) / (2 * b * c)) || 0; // angle of corner B - if (c === 0) { - continue; - } - - const gp = new Graphics(); - this.debugObjects.bones.addChild(gp); - - // draw bone - const refRation = c / 50 / scale; - gp.beginFill(0x00eecc, 1); - gp.drawPolygon(0, 0, 0 - refRation, c - refRation * 3, 0, c - refRation, 0 + refRation, c - refRation * 3); - gp.endFill(); - gp.x = starX; - gp.y = starY; - gp.pivot.y = c; - - // Calculate bone rotation angle - let rotation = 0; - if (starX < endX && starY < endY) { - // bottom right - rotation = -B + 180 * rad; - } else if (starX > endX && starY < endY) { - // bottom left - rotation = 180 * rad + B; - } else if (starX > endX && starY > endY) { - // top left - rotation = -B; - } else if (starX < endX && starY > endY) { - // bottom left - rotation = B; - } else if (starY === endY && starX < endX) { - // To the right - rotation = 90 * rad; - } else if (starY === endY && starX > endX) { - // go left - rotation = -90 * rad; - } else if (starX === endX && starY < endY) { - // down - rotation = 180 * rad; - } else if (starX === endX && starY > endY) { - // up - rotation = 0; - } - gp.rotation = rotation; - - // Draw the starting rotation point of the bone - gp.lineStyle(lineWidth + refRation / 2.4, 0x00eecc, 1); - gp.beginFill(0x000000, 0.6); - gp.drawCircle(0, c, refRation * 1.2); - gp.endFill(); - } - - // Draw the skeleton starting point "X" form - const startDotSize = lineWidth * 3; - this.debugObjects.skeletonXY.moveTo(skeletonX - startDotSize, skeletonY - startDotSize); - this.debugObjects.skeletonXY.lineTo(skeletonX + startDotSize, skeletonY + startDotSize); - this.debugObjects.skeletonXY.moveTo(skeletonX + startDotSize, skeletonY - startDotSize); - this.debugObjects.skeletonXY.lineTo(skeletonX - startDotSize, skeletonY + startDotSize); - } - - private drawRegionAttachmentsFunc(lineWidth:number): void { - const skeleton = this.skeleton; - const slots = skeleton.slots; - - this.debugObjects.regionAttachmentsShape.lineStyle(lineWidth, 0x0078ff, 1); - - for (let i = 0, len = slots.length; i < len; i++) { - const slot = slots[i], - attachment = slot.getAttachment(); - if (attachment == null || attachment.type !== AttachmentType.Region) { - continue; - } - - const regionAttachment = attachment as IRegionAttachment & { - computeWorldVertices:(slot: unknown, worldVertices: unknown, offset: unknown, stride: unknown) => void, - updateOffset?:() => void, - }; - - const vertices = new Float32Array(8); - - - regionAttachment?.updateOffset(); // We don't need this on all versions - - regionAttachment.computeWorldVertices(slot, vertices, 0, 2); - this.debugObjects.regionAttachmentsShape.drawPolygon(Array.from(vertices.slice(0, 8))); - - } - } - - private drawMeshHullAndMeshTriangles(lineWidth:number): void { - const skeleton = this.skeleton; - const slots = skeleton.slots; - - this.debugObjects.meshHullLine.lineStyle(lineWidth, 0x0078ff, 1); - this.debugObjects.meshTrianglesLine.lineStyle(lineWidth, 0xffcc00, 1); - - for (let i = 0, len = slots.length; i < len; i++) { - const slot = slots[i]; - if (!slot.bone.active) { - continue; - } - const attachment = slot.getAttachment(); - if (attachment == null || attachment.type !== AttachmentType.Mesh) { - continue; - } - - const meshAttachment:IMeshAttachment = attachment as IMeshAttachment; - - const vertices = new Float32Array(meshAttachment.worldVerticesLength), - triangles = meshAttachment.triangles; - let hullLength = meshAttachment.hullLength; - meshAttachment.computeWorldVertices(slot, 0, meshAttachment.worldVerticesLength, vertices, 0, 2); - // draw the skinned mesh (triangle) - if (this.drawMeshTriangles) { - for (let i = 0, len = triangles.length; i < len; i += 3) { - const v1 = triangles[i] * 2, - v2 = triangles[i + 1] * 2, - v3 = triangles[i + 2] * 2; - this.debugObjects.meshTrianglesLine.moveTo(vertices[v1], vertices[v1 + 1]); - this.debugObjects.meshTrianglesLine.lineTo(vertices[v2], vertices[v2 + 1]); - this.debugObjects.meshTrianglesLine.lineTo(vertices[v3], vertices[v3 + 1]); - } - } - - // draw skin border - if (this.drawMeshHull && hullLength > 0) { - hullLength = (hullLength >> 1) * 2; - let lastX = vertices[hullLength - 2], - lastY = vertices[hullLength - 1]; - for (let i = 0, len = hullLength; i < len; i += 2) { - const x = vertices[i], - y = vertices[i + 1]; - this.debugObjects.meshHullLine.moveTo(x, y); - this.debugObjects.meshHullLine.lineTo(lastX, lastY); - lastX = x; - lastY = y; - } - } - } - } - - private drawClippingFunc(lineWidth:number): void { - const skeleton = this.skeleton; - const slots = skeleton.slots; - - this.debugObjects.clippingPolygon.lineStyle(lineWidth, 0xff00ff, 1); - for (let i = 0, len = slots.length; i < len; i++) { - const slot = slots[i]; - if (!slot.bone.active) { - continue; - } - const attachment = slot.getAttachment(); - if (attachment == null || attachment.type !== AttachmentType.Clipping) { - continue; - } - - const clippingAttachment: IClippingAttachment = attachment as IClippingAttachment; - - const nn = clippingAttachment.worldVerticesLength, - world = new Float32Array(nn); - clippingAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2); - this.debugObjects.clippingPolygon.drawPolygon(Array.from(world)); - } - } - - private drawBoundingBoxesFunc(lineWidth:number): void { - // draw the total outline of the bounding box - this.debugObjects.boundingBoxesRect.lineStyle(lineWidth, 0x00ff00, 5); - - const bounds = new SkeletonBoundsBase(); - bounds.update(this.skeleton, true); - this.debugObjects.boundingBoxesRect.drawRect(bounds.minX, bounds.minY, bounds.getWidth(), bounds.getHeight()); - - const polygons = bounds.polygons, - drawPolygon = (polygonVertices: ArrayLike, _offset: unknown, count: number): void => { - this.debugObjects.boundingBoxesPolygon.lineStyle(lineWidth, 0x00ff00, 1); - this.debugObjects.boundingBoxesPolygon.beginFill(0x00ff00, 0.1); - - if (count < 3) { - throw new Error("Polygon must contain at least 3 vertices"); - } - const paths = [], - dotSize = lineWidth * 2; - for (let i = 0, len = polygonVertices.length; i < len; i += 2) { - const x1 = polygonVertices[i], - y1 = polygonVertices[i + 1]; - - // draw the bounding box node - this.debugObjects.boundingBoxesCircle.lineStyle(0); - this.debugObjects.boundingBoxesCircle.beginFill(0x00dd00); - this.debugObjects.boundingBoxesCircle.drawCircle(x1, y1, dotSize); - this.debugObjects.boundingBoxesCircle.endFill(); - - paths.push(x1, y1); - } - - // draw the bounding box area - this.debugObjects.boundingBoxesPolygon.drawPolygon(paths); - this.debugObjects.boundingBoxesPolygon.endFill(); - }; - - for (let i = 0, len = polygons.length; i < len; i++) { - const polygon = polygons[i]; - drawPolygon(polygon, 0, polygon.length); - } - } - - private drawPathsFunc(lineWidth:number): void { - const skeleton = this.skeleton; - const slots = skeleton.slots; - - this.debugObjects.pathsCurve.lineStyle(lineWidth, 0xff0000, 1); - this.debugObjects.pathsLine.lineStyle(lineWidth, 0xff00ff, 1); - - for (let i = 0, len = slots.length; i < len; i++) { - const slot = slots[i]; - if (!slot.bone.active) { - continue; - } - const attachment = slot.getAttachment(); - if (attachment == null || attachment.type !== AttachmentType.Path) { - continue - } - - const pathAttachment = attachment as IVertexAttachment & {closed:boolean}; - let nn = pathAttachment.worldVerticesLength; - const world = new Float32Array(nn); - pathAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2); - let x1 = world[2], - y1 = world[3], - x2 = 0, - y2 = 0; - if (pathAttachment.closed) { - const cx1 = world[0], - cy1 = world[1], - cx2 = world[nn - 2], - cy2 = world[nn - 1]; - x2 = world[nn - 4]; - y2 = world[nn - 3]; - - // curve - this.debugObjects.pathsCurve.moveTo(x1, y1); - this.debugObjects.pathsCurve.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2); - - // handle - this.debugObjects.pathsLine.moveTo(x1, y1); - this.debugObjects.pathsLine.lineTo(cx1, cy1); - this.debugObjects.pathsLine.moveTo(x2, y2); - this.debugObjects.pathsLine.lineTo(cx2, cy2); - } - nn -= 4; - for (let ii = 4; ii < nn; ii += 6) { - const cx1 = world[ii], - cy1 = world[ii + 1], - cx2 = world[ii + 2], - cy2 = world[ii + 3]; - x2 = world[ii + 4]; - y2 = world[ii + 5]; - // curve - this.debugObjects.pathsCurve.moveTo(x1, y1); - this.debugObjects.pathsCurve.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2); - - // handle - this.debugObjects.pathsLine.moveTo(x1, y1); - this.debugObjects.pathsLine.lineTo(cx1, cy1); - this.debugObjects.pathsLine.moveTo(x2, y2); - this.debugObjects.pathsLine.lineTo(cx2, cy2); - x1 = x2; - y1 = y2; - } - } - } - destroy(options?: any): void { + this.debug = null; // setter will do the cleanup + for (let i = 0, n = this.skeleton.slots.length; i < n; i++) { let slot = this.skeleton.slots[i]; for (let name in slot.meshes) { @@ -1261,20 +809,6 @@ export abstract class SpineBase):void; + + unregisterSpine(spine:SpineBase< ISkeleton, ISkeletonData, IAnimationState, IAnimationStateData>):void + registerSpine(spine:SpineBase< ISkeleton, ISkeletonData, IAnimationState, IAnimationStateData>):void +} + +type DebugDisplayObjects = { + bones: Container; + skeletonXY: Graphics; + regionAttachmentsShape: Graphics; + meshTrianglesLine: Graphics; + meshHullLine: Graphics; + clippingPolygon: Graphics; + boundingBoxesRect: Graphics; + boundingBoxesCircle: Graphics; + boundingBoxesPolygon: Graphics; + pathsCurve: Graphics; + pathsLine: Graphics; + parentDebugContainer: Container; +} + +/** + * @public + */ +export class SpineDebugRenderer implements ISpineDebugRenderer { + private registeredSpines:Map, DebugDisplayObjects> = new Map(); + + public drawDebug: boolean = true; + public drawMeshHull: boolean = true; + public drawMeshTriangles: boolean = true; + public drawBones: boolean = true; + public drawPaths: boolean = true; + public drawBoundingBoxes: boolean = true; + public drawClipping: boolean = true; + public drawRegionAttachments: boolean = true; + + public lineWidth: number = 1; + public regionAttachmentsColor: number = 0x0078ff; + public meshHullColor: number = 0x0078ff; + public meshTrianglesColor: number = 0xffcc00; + public clippingPolygonColor: number = 0xff00ff; + public boundingBoxesRectColor: number = 0x00ff00; + public boundingBoxesPolygonColor: number = 0x00ff00; + public boundingBoxesCircleColor: number = 0x00ff00; + public pathsCurveColor: number = 0xff0000; + public pathsLineColor: number = 0xff00ff; + public skeletonXYColor:number = 0xff0000; + public bonesColor:number = 0x00eecc; + + /** + * The debug is attached by force to each spine object. So we need to create it inside the spine when we get the first update + */ + public registerSpine(spine: SpineBase) { + if (this.registeredSpines.has(spine)) + { + console.warn("SpineDebugRenderer.registerSpine() - this spine is already registered!", spine); + } + const debugDisplayObjects = { + parentDebugContainer: new Container(), + bones: new Container(), + skeletonXY: new Graphics(), + regionAttachmentsShape: new Graphics(), + meshTrianglesLine: new Graphics(), + meshHullLine: new Graphics(), + clippingPolygon: new Graphics(), + boundingBoxesRect: new Graphics(), + boundingBoxesCircle: new Graphics(), + boundingBoxesPolygon: new Graphics(), + pathsCurve: new Graphics(), + pathsLine: new Graphics(), + }; + + debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.bones); + debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.skeletonXY); + debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.regionAttachmentsShape); + debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.meshTrianglesLine); + debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.meshHullLine); + debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.clippingPolygon); + debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.boundingBoxesRect); + debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.boundingBoxesCircle); + debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.boundingBoxesPolygon); + debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.pathsCurve); + debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.pathsLine); + + spine.addChild(debugDisplayObjects.parentDebugContainer); + + this.registeredSpines.set(spine, debugDisplayObjects); + } + public renderDebug(spine:SpineBase):void { + + if (!this.registeredSpines.has(spine)) { + // This should never happen. Spines are registered when you assign spine.debug + this.registerSpine(spine); + } + + const debugDisplayObjects = this.registeredSpines.get(spine); + + debugDisplayObjects.skeletonXY.clear(); + debugDisplayObjects.regionAttachmentsShape.clear(); + debugDisplayObjects.meshTrianglesLine.clear(); + debugDisplayObjects.meshHullLine.clear(); + debugDisplayObjects.clippingPolygon.clear(); + debugDisplayObjects.boundingBoxesRect.clear(); + debugDisplayObjects.boundingBoxesCircle.clear(); + debugDisplayObjects.boundingBoxesPolygon.clear(); + debugDisplayObjects.pathsCurve.clear(); + debugDisplayObjects.pathsLine.clear(); + + for (let len = debugDisplayObjects.bones.children.length; len > 0; len--) { + debugDisplayObjects.bones.children[len - 1].destroy({ children: true, texture: true, baseTexture: true }); + } + + const scale = spine.scale.x || spine.scale.y || 1; + const lineWidth = this.lineWidth / scale; + + if (this.drawBones) { + this.drawBonesFunc(spine, debugDisplayObjects, lineWidth, scale); + } + + if (this.drawPaths) { + this.drawPathsFunc(spine, debugDisplayObjects, lineWidth); + } + + if (this.drawBoundingBoxes) { + this.drawBoundingBoxesFunc(spine, debugDisplayObjects, lineWidth); + } + + if (this.drawClipping) { + this.drawClippingFunc(spine, debugDisplayObjects, lineWidth); + } + + if (this.drawMeshHull || this.drawMeshTriangles) { + this.drawMeshHullAndMeshTriangles(spine, debugDisplayObjects, lineWidth); + } + + if (this.drawRegionAttachments) { + this.drawRegionAttachmentsFunc(spine, debugDisplayObjects, lineWidth); + } + } + + private drawBonesFunc(spine:SpineBase, debugDisplayObjects: DebugDisplayObjects, lineWidth:number, scale:number): void { + const skeleton = spine.skeleton; + const skeletonX = skeleton.x; + const skeletonY = skeleton.y; + const bones = skeleton.bones; + + debugDisplayObjects.skeletonXY.lineStyle(lineWidth, this.skeletonXYColor, 1); + + for (let i = 0, len = bones.length; i < len; i++) { + const bone = bones[i], + boneLen = bone.data.length, + starX = skeletonX + bone.matrix.tx, + starY = skeletonY + bone.matrix.ty, + endX = skeletonX + boneLen * bone.matrix.a + bone.matrix.tx, + endY = skeletonY + boneLen * bone.matrix.b + bone.matrix.ty; + + if (bone.data.name === "root" || bone.data.parent === null) { + continue; + } + + // Triangle calculation formula + // area: A=sqrt((a+b+c)*(-a+b+c)*(a-b+c)*(a+b-c))/4 + // alpha: alpha=acos((pow(b, 2)+pow(c, 2)-pow(a, 2))/(2*b*c)) + // beta: beta=acos((pow(a, 2)+pow(c, 2)-pow(b, 2))/(2*a*c)) + // gamma: gamma=acos((pow(a, 2)+pow(b, 2)-pow(c, 2))/(2*a*b)) + + const w = Math.abs(starX - endX), + h = Math.abs(starY - endY), + // a = w, // side length a + a2 = Math.pow(w, 2), // square root of side length a + b = h, // side length b + b2 = Math.pow(h, 2), // square root of side length b + c = Math.sqrt(a2 + b2), // side length c + c2 = Math.pow(c, 2), // square root of side length c + rad = Math.PI / 180, + // A = Math.acos([a2 + c2 - b2] / [2 * a * c]) || 0, // Angle A + // C = Math.acos([a2 + b2 - c2] / [2 * a * b]) || 0, // C angle + B = Math.acos((c2 + b2 - a2) / (2 * b * c)) || 0; // angle of corner B + if (c === 0) { + continue; + } + + const gp = new Graphics(); + debugDisplayObjects.bones.addChild(gp); + + // draw bone + const refRation = c / 50 / scale; + gp.beginFill(this.bonesColor, 1); + gp.drawPolygon(0, 0, 0 - refRation, c - refRation * 3, 0, c - refRation, 0 + refRation, c - refRation * 3); + gp.endFill(); + gp.x = starX; + gp.y = starY; + gp.pivot.y = c; + + // Calculate bone rotation angle + let rotation = 0; + if (starX < endX && starY < endY) { + // bottom right + rotation = -B + 180 * rad; + } else if (starX > endX && starY < endY) { + // bottom left + rotation = 180 * rad + B; + } else if (starX > endX && starY > endY) { + // top left + rotation = -B; + } else if (starX < endX && starY > endY) { + // bottom left + rotation = B; + } else if (starY === endY && starX < endX) { + // To the right + rotation = 90 * rad; + } else if (starY === endY && starX > endX) { + // go left + rotation = -90 * rad; + } else if (starX === endX && starY < endY) { + // down + rotation = 180 * rad; + } else if (starX === endX && starY > endY) { + // up + rotation = 0; + } + gp.rotation = rotation; + + // Draw the starting rotation point of the bone + gp.lineStyle(lineWidth + refRation / 2.4, this.bonesColor, 1); + gp.beginFill(0x000000, 0.6); + gp.drawCircle(0, c, refRation * 1.2); + gp.endFill(); + } + + // Draw the skeleton starting point "X" form + const startDotSize = lineWidth * 3; + debugDisplayObjects.skeletonXY.moveTo(skeletonX - startDotSize, skeletonY - startDotSize); + debugDisplayObjects.skeletonXY.lineTo(skeletonX + startDotSize, skeletonY + startDotSize); + debugDisplayObjects.skeletonXY.moveTo(skeletonX + startDotSize, skeletonY - startDotSize); + debugDisplayObjects.skeletonXY.lineTo(skeletonX - startDotSize, skeletonY + startDotSize); + } + + private drawRegionAttachmentsFunc(spine:SpineBase, debugDisplayObjects: DebugDisplayObjects, lineWidth:number): void { + const skeleton = spine.skeleton; + const slots = skeleton.slots; + + debugDisplayObjects.regionAttachmentsShape.lineStyle(lineWidth, this.regionAttachmentsColor, 1); + + for (let i = 0, len = slots.length; i < len; i++) { + const slot = slots[i], + attachment = slot.getAttachment(); + if (attachment == null || attachment.type !== AttachmentType.Region) { + continue; + } + + const regionAttachment = attachment as IRegionAttachment & { + computeWorldVertices:(slot: unknown, worldVertices: unknown, offset: unknown, stride: unknown) => void, + updateOffset?:() => void, + }; + + const vertices = new Float32Array(8); + + + regionAttachment?.updateOffset(); // We don't need this on all versions + + regionAttachment.computeWorldVertices(slot, vertices, 0, 2); + debugDisplayObjects.regionAttachmentsShape.drawPolygon(Array.from(vertices.slice(0, 8))); + + } + } + + private drawMeshHullAndMeshTriangles(spine:SpineBase, debugDisplayObjects: DebugDisplayObjects, lineWidth:number): void { + const skeleton = spine.skeleton; + const slots = skeleton.slots; + + debugDisplayObjects.meshHullLine.lineStyle(lineWidth, this.meshHullColor, 1); + debugDisplayObjects.meshTrianglesLine.lineStyle(lineWidth, this.meshTrianglesColor, 1); + + for (let i = 0, len = slots.length; i < len; i++) { + const slot = slots[i]; + if (!slot.bone.active) { + continue; + } + const attachment = slot.getAttachment(); + if (attachment == null || attachment.type !== AttachmentType.Mesh) { + continue; + } + + const meshAttachment:IMeshAttachment = attachment as IMeshAttachment; + + const vertices = new Float32Array(meshAttachment.worldVerticesLength), + triangles = meshAttachment.triangles; + let hullLength = meshAttachment.hullLength; + meshAttachment.computeWorldVertices(slot, 0, meshAttachment.worldVerticesLength, vertices, 0, 2); + // draw the skinned mesh (triangle) + if (this.drawMeshTriangles) { + for (let i = 0, len = triangles.length; i < len; i += 3) { + const v1 = triangles[i] * 2, + v2 = triangles[i + 1] * 2, + v3 = triangles[i + 2] * 2; + debugDisplayObjects.meshTrianglesLine.moveTo(vertices[v1], vertices[v1 + 1]); + debugDisplayObjects.meshTrianglesLine.lineTo(vertices[v2], vertices[v2 + 1]); + debugDisplayObjects.meshTrianglesLine.lineTo(vertices[v3], vertices[v3 + 1]); + } + } + + // draw skin border + if (this.drawMeshHull && hullLength > 0) { + hullLength = (hullLength >> 1) * 2; + let lastX = vertices[hullLength - 2], + lastY = vertices[hullLength - 1]; + for (let i = 0, len = hullLength; i < len; i += 2) { + const x = vertices[i], + y = vertices[i + 1]; + debugDisplayObjects.meshHullLine.moveTo(x, y); + debugDisplayObjects.meshHullLine.lineTo(lastX, lastY); + lastX = x; + lastY = y; + } + } + } + } + + private drawClippingFunc(spine:SpineBase, debugDisplayObjects: DebugDisplayObjects, lineWidth:number): void { + const skeleton = spine.skeleton; + const slots = skeleton.slots; + + debugDisplayObjects.clippingPolygon.lineStyle(lineWidth, this.clippingPolygonColor, 1); + for (let i = 0, len = slots.length; i < len; i++) { + const slot = slots[i]; + if (!slot.bone.active) { + continue; + } + const attachment = slot.getAttachment(); + if (attachment == null || attachment.type !== AttachmentType.Clipping) { + continue; + } + + const clippingAttachment: IClippingAttachment = attachment as IClippingAttachment; + + const nn = clippingAttachment.worldVerticesLength, + world = new Float32Array(nn); + clippingAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2); + debugDisplayObjects.clippingPolygon.drawPolygon(Array.from(world)); + } + } + + private drawBoundingBoxesFunc(spine:SpineBase, debugDisplayObjects: DebugDisplayObjects, lineWidth:number): void { + // draw the total outline of the bounding box + debugDisplayObjects.boundingBoxesRect.lineStyle(lineWidth, this.boundingBoxesRectColor, 5); + + const bounds = new SkeletonBoundsBase(); + bounds.update(spine.skeleton, true); + debugDisplayObjects.boundingBoxesRect.drawRect(bounds.minX, bounds.minY, bounds.getWidth(), bounds.getHeight()); + + const polygons = bounds.polygons, + drawPolygon = (polygonVertices: ArrayLike, _offset: unknown, count: number): void => { + debugDisplayObjects.boundingBoxesPolygon.lineStyle(lineWidth, this.boundingBoxesPolygonColor, 1); + debugDisplayObjects.boundingBoxesPolygon.beginFill(this.boundingBoxesPolygonColor, 0.1); + + if (count < 3) { + throw new Error("Polygon must contain at least 3 vertices"); + } + const paths = [], + dotSize = lineWidth * 2; + for (let i = 0, len = polygonVertices.length; i < len; i += 2) { + const x1 = polygonVertices[i], + y1 = polygonVertices[i + 1]; + + // draw the bounding box node + debugDisplayObjects.boundingBoxesCircle.lineStyle(0); + debugDisplayObjects.boundingBoxesCircle.beginFill(this.boundingBoxesCircleColor); + debugDisplayObjects.boundingBoxesCircle.drawCircle(x1, y1, dotSize); + debugDisplayObjects.boundingBoxesCircle.endFill(); + + paths.push(x1, y1); + } + + // draw the bounding box area + debugDisplayObjects.boundingBoxesPolygon.drawPolygon(paths); + debugDisplayObjects.boundingBoxesPolygon.endFill(); + }; + + for (let i = 0, len = polygons.length; i < len; i++) { + const polygon = polygons[i]; + drawPolygon(polygon, 0, polygon.length); + } + } + + private drawPathsFunc(spine:SpineBase, debugDisplayObjects: DebugDisplayObjects, lineWidth:number): void { + const skeleton = spine.skeleton; + const slots = skeleton.slots; + + debugDisplayObjects.pathsCurve.lineStyle(lineWidth, this.pathsCurveColor, 1); + debugDisplayObjects.pathsLine.lineStyle(lineWidth, this.pathsLineColor, 1); + + for (let i = 0, len = slots.length; i < len; i++) { + const slot = slots[i]; + if (!slot.bone.active) { + continue; + } + const attachment = slot.getAttachment(); + if (attachment == null || attachment.type !== AttachmentType.Path) { + continue + } + + const pathAttachment = attachment as IVertexAttachment & {closed:boolean}; + let nn = pathAttachment.worldVerticesLength; + const world = new Float32Array(nn); + pathAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2); + let x1 = world[2], + y1 = world[3], + x2 = 0, + y2 = 0; + if (pathAttachment.closed) { + const cx1 = world[0], + cy1 = world[1], + cx2 = world[nn - 2], + cy2 = world[nn - 1]; + x2 = world[nn - 4]; + y2 = world[nn - 3]; + + // curve + debugDisplayObjects.pathsCurve.moveTo(x1, y1); + debugDisplayObjects.pathsCurve.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2); + + // handle + debugDisplayObjects.pathsLine.moveTo(x1, y1); + debugDisplayObjects.pathsLine.lineTo(cx1, cy1); + debugDisplayObjects.pathsLine.moveTo(x2, y2); + debugDisplayObjects.pathsLine.lineTo(cx2, cy2); + } + nn -= 4; + for (let ii = 4; ii < nn; ii += 6) { + const cx1 = world[ii], + cy1 = world[ii + 1], + cx2 = world[ii + 2], + cy2 = world[ii + 3]; + x2 = world[ii + 4]; + y2 = world[ii + 5]; + // curve + debugDisplayObjects.pathsCurve.moveTo(x1, y1); + debugDisplayObjects.pathsCurve.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2); + + // handle + debugDisplayObjects.pathsLine.moveTo(x1, y1); + debugDisplayObjects.pathsLine.lineTo(cx1, cy1); + debugDisplayObjects.pathsLine.moveTo(x2, y2); + debugDisplayObjects.pathsLine.lineTo(cx2, cy2); + x1 = x2; + y1 = y2; + } + } + } + + public unregisterSpine(spine:SpineBase):void{ + if (!this.registeredSpines.has(spine)) { + console.warn("SpineDebugRenderer.unregisterSpine() - spine is not registered, can't unregister!", spine); + } + const debugDisplayObjects = this.registeredSpines.get(spine); + debugDisplayObjects.parentDebugContainer.destroy({baseTexture:true, children:true, texture:true}); + this.registeredSpines.delete(spine); + } +} \ No newline at end of file diff --git a/packages/base/src/index.ts b/packages/base/src/index.ts index a292d9b2..a37aa6a8 100644 --- a/packages/base/src/index.ts +++ b/packages/base/src/index.ts @@ -11,3 +11,4 @@ export * from './core/SkeletonBoundsBase'; export * from './settings'; export * from './SpineBase'; +export * from './SpineDebugRenderer'; From 94edb6af057942cffd5a3818b1d39067cc2990ab Mon Sep 17 00:00:00 2001 From: eCode Date: Sun, 7 Aug 2022 20:03:17 -0300 Subject: [PATCH 6/6] added docs for the new extendable debug thingy --- README.md | 55 +++++++++++++++---------- packages/base/src/SpineDebugRenderer.ts | 12 ++++++ 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 737f9d2c..f2aaf7a1 100644 --- a/README.md +++ b/README.md @@ -117,37 +117,50 @@ let spine = new PIXI.heaven.Spine(spineData); ### Debug -To show bones and bounds you can set `yourSpine.drawDebug = true` -Only after you set `drawDebug` to true, debug graphics are created. +To show debug graphics you can set `yourSpine.debug = new SpineDebugRenderer()` Control what gets drawn with the following flags: ```js -yourSpine.drawMeshHull = true; -yourSpine.drawMeshTriangles = true; -yourSpine.drawBones = true; -yourSpine.drawPaths = true; -yourSpine.drawBoundingBoxes = true; -yourSpine.drawClipping = true; -yourSpine.drawRegionAttachments = true; +// Master toggle +yourSpine.debug.drawDebug = true; + +// Per feature toggle +yourSpine.debug.drawMeshHull = true; +yourSpine.debug.drawMeshTriangles = true; +yourSpine.debug.drawBones = true; +yourSpine.debug.drawPaths = true; +yourSpine.debug.drawBoundingBoxes = true; +yourSpine.debug.drawClipping = true; +yourSpine.debug.drawRegionAttachments = true; ``` To have even more control, you can customize the color and line thickness with ```js -yourSpine.debugOptions.lineWidth = 1; -yourSpine.debugOptions.regionAttachmentsColor = 0x0078ff; -yourSpine.debugOptions.meshHullColor = 0x0078ff; -yourSpine.debugOptions.meshTrianglesColor = 0xffcc00; -yourSpine.debugOptions.clippingPolygonColor = 0xff00ff; -yourSpine.debugOptions.boundingBoxesRectColor = 0x00ff00; -yourSpine.debugOptions.boundingBoxesPolygonColor = 0x00ff00; -yourSpine.debugOptions.boundingBoxesCircleColor = 0x00ff00; -yourSpine.debugOptions.pathsCurveColor = 0xff0000; -yourSpine.debugOptions.pathsLineColor = 0xff00ff; -yourSpine.debugOptions.skeletonXYColor = 0xff0000; -yourSpine.debugOptions.bonesColor = 0x00eecc; +yourSpine.debug.debugOptions.lineWidth = 1; +yourSpine.debug.debugOptions.regionAttachmentsColor = 0x0078ff; +yourSpine.debug.debugOptions.meshHullColor = 0x0078ff; +yourSpine.debug.debugOptions.meshTrianglesColor = 0xffcc00; +yourSpine.debug.debugOptions.clippingPolygonColor = 0xff00ff; +yourSpine.debug.debugOptions.boundingBoxesRectColor = 0x00ff00; +yourSpine.debug.debugOptions.boundingBoxesPolygonColor = 0x00ff00; +yourSpine.debug.debugOptions.boundingBoxesCircleColor = 0x00ff00; +yourSpine.debug.debugOptions.pathsCurveColor = 0xff0000; +yourSpine.debug.debugOptions.pathsLineColor = 0xff00ff; +yourSpine.debug.debugOptions.skeletonXYColor = 0xff0000; +yourSpine.debug.debugOptions.bonesColor = 0x00eecc; ``` +You can reuse a single debug renderer and they will share the debug settings! +```js +const debugRenderer = new SpineDebugRenderer(); + +oneSpine.debug = debugRenderer; +anotherSpine.debug = debugRenderer; +``` + +If you want to create your own debugger you can extend `SpineDebugRenderer` or create a class from scratch that implements `ISpineDebugRenderer`! + ## Build & Development You will need to have [node][node] setup on your machine. diff --git a/packages/base/src/SpineDebugRenderer.ts b/packages/base/src/SpineDebugRenderer.ts index 50076ed0..8cbdee7a 100644 --- a/packages/base/src/SpineDebugRenderer.ts +++ b/packages/base/src/SpineDebugRenderer.ts @@ -7,12 +7,23 @@ import { AttachmentType } from "./core/AttachmentType"; import { SkeletonBoundsBase } from "./core/SkeletonBoundsBase"; /** + * Make a class that extends from this interface to create your own debug renderer. * @public */ export interface ISpineDebugRenderer { + /** + * This will be called every frame, after the spine has been updated. + */ renderDebug(spine:SpineBase< ISkeleton, ISkeletonData, IAnimationState, IAnimationStateData>):void; + /** + * This is called when the `spine.debug` object is set to null or when the spine is destroyed. + */ unregisterSpine(spine:SpineBase< ISkeleton, ISkeletonData, IAnimationState, IAnimationStateData>):void + + /** + * This is called when the `spine.debug` object is set to a new instance of a debug renderer. + */ registerSpine(spine:SpineBase< ISkeleton, ISkeletonData, IAnimationState, IAnimationStateData>):void } @@ -32,6 +43,7 @@ type DebugDisplayObjects = { } /** + * This is a debug renderer that uses PixiJS Graphics under the hood. * @public */ export class SpineDebugRenderer implements ISpineDebugRenderer {