/****************************************************************************** * Spine Runtimes License Agreement * Last updated April 5, 2025. Replaces all prior versions. * * Copyright (c) 2013-2025, Esoteric Software LLC * * Integration of the Spine Runtimes into software or otherwise creating * derivative works of the Spine Runtimes is permitted under the terms and * conditions of Section 2 of the Spine Editor License Agreement: * http://esotericsoftware.com/spine-editor-license * * Otherwise, it is permitted to integrate the Spine Runtimes into software * or otherwise create derivative works of the Spine Runtimes (collectively, * "Products"), provided that each user of the Products must obtain their own * Spine Editor license and redistribution of the Products in any form must * include this license and copyright notice. * * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ import { Disposable, Color, SkeletonBounds, Utils, Skeleton, RegionAttachment, MeshAttachment, PathAttachment, ClippingAttachment } from "@esotericsoftware/spine-core"; import { ShapeRenderer } from "./ShapeRenderer.js"; import { ManagedWebGLRenderingContext } from "./WebGL.js"; export class SkeletonDebugRenderer implements Disposable { boneLineColor = new Color(1, 0, 0, 1); boneOriginColor = new Color(0, 1, 0, 1); attachmentLineColor = new Color(0, 0, 1, 0.5); triangleLineColor = new Color(1, 0.64, 0, 0.5); pathColor = new Color().setFromString("FF7F00"); clipColor = new Color(0.8, 0, 0, 2); aabbColor = new Color(0, 1, 0, 0.5); drawBones = true; drawRegionAttachments = true; drawBoundingBoxes = true; drawMeshHull = true; drawMeshTriangles = true; drawPaths = true; drawSkeletonXY = false; drawClipping = true; premultipliedAlpha = false; scale = 1; boneWidth = 2; private context: ManagedWebGLRenderingContext; private bounds = new SkeletonBounds(); private temp = new Array(); private vertices = Utils.newFloatArray(2 * 1024); private static LIGHT_GRAY = new Color(192 / 255, 192 / 255, 192 / 255, 1); private static GREEN = new Color(0, 1, 0, 1); constructor (context: ManagedWebGLRenderingContext | WebGLRenderingContext) { this.context = context instanceof ManagedWebGLRenderingContext ? context : new ManagedWebGLRenderingContext(context); } draw (shapes: ShapeRenderer, skeleton: Skeleton, ignoredBones?: Array) { let skeletonX = skeleton.x; let skeletonY = skeleton.y; let gl = this.context.gl; let srcFunc = this.premultipliedAlpha ? gl.ONE : gl.SRC_ALPHA; shapes.setBlendMode(srcFunc, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); let bones = skeleton.bones; if (this.drawBones) { shapes.setColor(this.boneLineColor); for (let i = 0, n = bones.length; i < n; i++) { let bone = bones[i]; if (ignoredBones && ignoredBones.indexOf(bone.data.name) > -1) continue; if (!bone.parent) continue; const boneApplied = bone.applied; let x = bone.data.length * boneApplied.a + boneApplied.worldX; let y = bone.data.length * boneApplied.c + boneApplied.worldY; shapes.rectLine(true, boneApplied.worldX, boneApplied.worldY, x, y, this.boneWidth * this.scale); } if (this.drawSkeletonXY) shapes.x(skeletonX, skeletonY, 4 * this.scale); } if (this.drawRegionAttachments) { shapes.setColor(this.attachmentLineColor); let slots = skeleton.slots; for (let i = 0, n = slots.length; i < n; i++) { let slot = slots[i]; if (!slot.bone.active) continue; let attachment = slot.applied.attachment; if (attachment instanceof RegionAttachment) { let vertices = this.vertices; attachment.computeWorldVertices(slot, vertices, 0, 2); shapes.line(vertices[0], vertices[1], vertices[2], vertices[3]); shapes.line(vertices[2], vertices[3], vertices[4], vertices[5]); shapes.line(vertices[4], vertices[5], vertices[6], vertices[7]); shapes.line(vertices[6], vertices[7], vertices[0], vertices[1]); } } } if (this.drawMeshHull || this.drawMeshTriangles) { let slots = skeleton.slots; for (let i = 0, n = slots.length; i < n; i++) { let slot = slots[i]; if (!slot.bone.active) continue; let attachment = slot.applied.attachment; if (!(attachment instanceof MeshAttachment)) continue; let vertices = this.vertices; attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, vertices, 0, 2); let triangles = attachment.triangles; let hullLength = attachment.hullLength; if (this.drawMeshTriangles) { shapes.setColor(this.triangleLineColor); for (let ii = 0, nn = triangles.length; ii < nn; ii += 3) { let v1 = triangles[ii] * 2, v2 = triangles[ii + 1] * 2, v3 = triangles[ii + 2] * 2; shapes.triangle(false, vertices[v1], vertices[v1 + 1], // vertices[v2], vertices[v2 + 1], // vertices[v3], vertices[v3 + 1] // ); } } if (this.drawMeshHull && hullLength > 0) { shapes.setColor(this.attachmentLineColor); hullLength = (hullLength >> 1) * 2; let lastX = vertices[hullLength - 2], lastY = vertices[hullLength - 1]; for (let ii = 0, nn = hullLength; ii < nn; ii += 2) { let x = vertices[ii], y = vertices[ii + 1]; shapes.line(x, y, lastX, lastY); lastX = x; lastY = y; } } } } if (this.drawBoundingBoxes) { let bounds = this.bounds; bounds.update(skeleton, true); shapes.setColor(this.aabbColor); shapes.rect(false, bounds.minX, bounds.minY, bounds.getWidth(), bounds.getHeight()); let polygons = bounds.polygons; let boxes = bounds.boundingBoxes; for (let i = 0, n = polygons.length; i < n; i++) { let polygon = polygons[i]; shapes.setColor(boxes[i].color); shapes.polygon(polygon, 0, polygon.length); } } if (this.drawPaths) { let slots = skeleton.slots; for (let i = 0, n = slots.length; i < n; i++) { let slot = slots[i]; if (!slot.bone.active) continue; let attachment = slot.applied.attachment; if (!(attachment instanceof PathAttachment)) continue; let nn = attachment.worldVerticesLength; let world = this.temp = Utils.setArraySize(this.temp, nn, 0); attachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2); let color = this.pathColor; let x1 = world[2], y1 = world[3], x2 = 0, y2 = 0; if (attachment.closed) { shapes.setColor(color); let cx1 = world[0], cy1 = world[1], cx2 = world[nn - 2], cy2 = world[nn - 1]; x2 = world[nn - 4]; y2 = world[nn - 3]; shapes.curve(x1, y1, cx1, cy1, cx2, cy2, x2, y2, 32); shapes.setColor(SkeletonDebugRenderer.LIGHT_GRAY); shapes.line(x1, y1, cx1, cy1); shapes.line(x2, y2, cx2, cy2); } nn -= 4; for (let ii = 4; ii < nn; ii += 6) { let cx1 = world[ii], cy1 = world[ii + 1], cx2 = world[ii + 2], cy2 = world[ii + 3]; x2 = world[ii + 4]; y2 = world[ii + 5]; shapes.setColor(color); shapes.curve(x1, y1, cx1, cy1, cx2, cy2, x2, y2, 32); shapes.setColor(SkeletonDebugRenderer.LIGHT_GRAY); shapes.line(x1, y1, cx1, cy1); shapes.line(x2, y2, cx2, cy2); x1 = x2; y1 = y2; } } } if (this.drawBones) { shapes.setColor(this.boneOriginColor); for (let i = 0, n = bones.length; i < n; i++) { let bone = bones[i]; if (ignoredBones && ignoredBones.indexOf(bone.data.name) > -1) continue; let boneApplied = bone.applied; shapes.circle(true, boneApplied.worldX, boneApplied.worldY, 3 * this.scale, this.boneOriginColor, 8); } } if (this.drawClipping) { let slots = skeleton.slots; shapes.setColor(this.clipColor) for (let i = 0, n = slots.length; i < n; i++) { let slot = slots[i]; if (!slot.bone.active) continue; let attachment = slot.applied.attachment; if (!(attachment instanceof ClippingAttachment)) continue; let nn = attachment.worldVerticesLength; let world = this.temp = Utils.setArraySize(this.temp, nn, 0); attachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2); for (let i = 0, n = world.length; i < n; i += 2) { let x = world[i]; let y = world[i + 1]; let x2 = world[(i + 2) % world.length]; let y2 = world[(i + 3) % world.length]; shapes.line(x, y, x2, y2); } } } } dispose () { } }