/****************************************************************************** * 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 type { ClippingAttachment } from "./attachments/ClippingAttachment.js"; import type { Skeleton } from "./Skeleton.js"; import type { Slot } from "./Slot.js"; import { Triangulator } from "./Triangulator.js"; import { type Color, type NumberArrayLike, Utils } from "./Utils.js"; export class SkeletonClipping { private triangulator = new Triangulator(); private clippingPolygon = [] as number[]; private clipOutput = [] as number[]; clippedVertices = [] as number[]; /** An empty array unless {@link clipTrianglesUnpacked} was used. **/ clippedUVs = [] as number[]; clippedTriangles = [] as number[]; _clippedVerticesTyped = new Float32Array(1024); _clippedUVsTyped = new Float32Array(1024); _clippedTrianglesTyped = new Uint32Array(1024); clippedVerticesTyped = new Float32Array(0); clippedUVsTyped = new Float32Array(0); clippedTrianglesTyped = new Uint32Array(0); clippedVerticesLength = 0; clippedUVsLength = 0; clippedTrianglesLength = 0; private scratch = [] as number[]; private clipAttachment: ClippingAttachment | null = null; private clippingPolygons: Array> | null = null; clipStart (skeleton: Skeleton, slot: Slot, clip: ClippingAttachment): number { if (this.clipAttachment) return 0; this.clipAttachment = clip; const n = clip.worldVerticesLength; const vertices = Utils.setArraySize(this.clippingPolygon, n); clip.computeWorldVertices(skeleton, slot, 0, n, vertices, 0, 2); const clippingPolygon = this.clippingPolygon; SkeletonClipping.makeClockwise(clippingPolygon); const clippingPolygons = this.clippingPolygons = this.triangulator.decompose(clippingPolygon, this.triangulator.triangulate(clippingPolygon)); for (let i = 0, n = clippingPolygons.length; i < n; i++) { const polygon = clippingPolygons[i]; SkeletonClipping.makeClockwise(polygon); polygon.push(polygon[0]); polygon.push(polygon[1]); } return clippingPolygons.length; } clipEnd (slot?: Slot) { if (!this.clipAttachment) return; if (slot && this.clipAttachment.endSlot !== slot.data) return; this.clipAttachment = null; this.clippingPolygons = null; this.clippedVertices.length = 0; this.clippedTriangles.length = 0; this.clippingPolygon.length = 0; this.clippedVerticesLength = 0; this.clippedUVsLength = 0; this.clippedTrianglesLength = 0; } isClipping (): boolean { return this.clipAttachment != null; } clipTriangles (vertices: NumberArrayLike, triangles: NumberArrayLike, trianglesLength: number): boolean; clipTriangles (vertices: NumberArrayLike, triangles: NumberArrayLike, trianglesLength: number, uvs: NumberArrayLike, light: Color, dark: Color, twoColor: boolean, stride: number): boolean; clipTriangles (vertices: NumberArrayLike, triangles: NumberArrayLike, trianglesLength: number, uvs?: NumberArrayLike, light?: Color, dark?: Color, twoColor?: boolean, stride?: number): boolean { return (uvs && light && dark && typeof twoColor === 'boolean' && typeof stride === 'number') ? this.clipTrianglesRender(vertices, triangles, trianglesLength, uvs, light, dark, twoColor, stride) : this.clipTrianglesNoRender(vertices, triangles, trianglesLength); } private clipTrianglesNoRender (vertices: NumberArrayLike, triangles: NumberArrayLike, trianglesLength: number): boolean { const clipOutput = this.clipOutput, clippedVertices = this.clippedVertices; const clippedTriangles = this.clippedTriangles; // biome-ignore lint/style/noNonNullAssertion: clipStart define it const polygons = this.clippingPolygons!; const polygonsCount = polygons.length; let index = 0; clippedVertices.length = 0; clippedTriangles.length = 0; let clipOutputItems = null; for (let i = 0; i < trianglesLength; i += 3) { let v = triangles[i] << 1; const x1 = vertices[v], y1 = vertices[v + 1]; v = triangles[i + 1] << 1; const x2 = vertices[v], y2 = vertices[v + 1]; v = triangles[i + 2] << 1; const x3 = vertices[v], y3 = vertices[v + 1]; for (let p = 0; p < polygonsCount; p++) { let s = clippedVertices.length; if (this.clip(x1, y1, x2, y2, x3, y3, polygons[p], clipOutput)) { clipOutputItems = this.clipOutput; const clipOutputLength = clipOutput.length; if (clipOutputLength === 0) continue; let clipOutputCount = clipOutputLength >> 1; const clippedVerticesItems = Utils.setArraySize(clippedVertices, s + clipOutputCount * 2); for (let ii = 0; ii < clipOutputLength; ii += 2, s += 2) { const x = clipOutputItems[ii], y = clipOutputItems[ii + 1]; clippedVerticesItems[s] = x; clippedVerticesItems[s + 1] = y; } s = clippedTriangles.length; const clippedTrianglesItems = Utils.setArraySize(clippedTriangles, s + 3 * (clipOutputCount - 2)); clipOutputCount--; for (let ii = 1; ii < clipOutputCount; ii++, s += 3) { clippedTrianglesItems[s] = index; clippedTrianglesItems[s + 1] = (index + ii); clippedTrianglesItems[s + 2] = (index + ii + 1); } index += clipOutputCount + 1; } else { const clippedVerticesItems = Utils.setArraySize(clippedVertices, s + 3 * 2); clippedVerticesItems[s] = x1; clippedVerticesItems[s + 1] = y1; clippedVerticesItems[s + 2] = x2; clippedVerticesItems[s + 3] = y2; clippedVerticesItems[s + 4] = x3; clippedVerticesItems[s + 5] = y3; s = clippedTriangles.length; const clippedTrianglesItems = Utils.setArraySize(clippedTriangles, s + 3); clippedTrianglesItems[s] = index; clippedTrianglesItems[s + 1] = (index + 1); clippedTrianglesItems[s + 2] = (index + 2); index += 3; break; } } } return clipOutputItems != null; } private clipTrianglesRender (vertices: NumberArrayLike, triangles: NumberArrayLike, trianglesLength: number, uvs: NumberArrayLike, light: Color, dark: Color, twoColor: boolean, stride: number): boolean { const clipOutput = this.clipOutput, clippedVertices = this.clippedVertices; const clippedTriangles = this.clippedTriangles; // biome-ignore lint/style/noNonNullAssertion: clipStart define it const polygons = this.clippingPolygons!; const polygonsCount = polygons.length; let index = 0; clippedVertices.length = 0; clippedTriangles.length = 0; let clipOutputItems = null; for (let i = 0; i < trianglesLength; i += 3) { let t = triangles[i]; const u1 = uvs[t << 1], v1 = uvs[(t << 1) + 1]; const x1 = vertices[t * stride], y1 = vertices[t * stride + 1]; t = triangles[i + 1]; const u2 = uvs[t << 1], v2 = uvs[(t << 1) + 1]; const x2 = vertices[t * stride], y2 = vertices[t * stride + 1]; t = triangles[i + 2]; const u3 = uvs[t << 1], v3 = uvs[(t << 1) + 1]; const x3 = vertices[t * stride], y3 = vertices[t * stride + 1]; for (let p = 0; p < polygonsCount; p++) { let s = clippedVertices.length; if (this.clip(x1, y1, x2, y2, x3, y3, polygons[p], clipOutput)) { clipOutputItems = this.clipOutput; const clipOutputLength = clipOutput.length; if (clipOutputLength === 0) continue; const d0 = y2 - y3, d1 = x3 - x2, d2 = x1 - x3, d4 = y3 - y1; const d = 1 / (d0 * d2 + d1 * (y1 - y3)); let clipOutputCount = clipOutputLength >> 1; const clippedVerticesItems = Utils.setArraySize(clippedVertices, s + clipOutputCount * stride); for (let ii = 0; ii < clipOutputLength; ii += 2, s += stride) { const x = clipOutputItems[ii], y = clipOutputItems[ii + 1]; clippedVerticesItems[s] = x; clippedVerticesItems[s + 1] = y; clippedVerticesItems[s + 2] = light.r; clippedVerticesItems[s + 3] = light.g; clippedVerticesItems[s + 4] = light.b; clippedVerticesItems[s + 5] = light.a; const c0 = x - x3, c1 = y - y3; const a = (d0 * c0 + d1 * c1) * d; const b = (d4 * c0 + d2 * c1) * d; const c = 1 - a - b; clippedVerticesItems[s + 6] = u1 * a + u2 * b + u3 * c; clippedVerticesItems[s + 7] = v1 * a + v2 * b + v3 * c; if (twoColor) { clippedVerticesItems[s + 8] = dark.r; clippedVerticesItems[s + 9] = dark.g; clippedVerticesItems[s + 10] = dark.b; clippedVerticesItems[s + 11] = dark.a; } } s = clippedTriangles.length; const clippedTrianglesItems = Utils.setArraySize(clippedTriangles, s + 3 * (clipOutputCount - 2)); clipOutputCount--; for (let ii = 1; ii < clipOutputCount; ii++, s += 3) { clippedTrianglesItems[s] = index; clippedTrianglesItems[s + 1] = (index + ii); clippedTrianglesItems[s + 2] = (index + ii + 1); } index += clipOutputCount + 1; } else { const clippedVerticesItems = Utils.setArraySize(clippedVertices, s + 3 * stride); clippedVerticesItems[s] = x1; clippedVerticesItems[s + 1] = y1; clippedVerticesItems[s + 2] = light.r; clippedVerticesItems[s + 3] = light.g; clippedVerticesItems[s + 4] = light.b; clippedVerticesItems[s + 5] = light.a; if (!twoColor) { clippedVerticesItems[s + 6] = u1; clippedVerticesItems[s + 7] = v1; clippedVerticesItems[s + 8] = x2; clippedVerticesItems[s + 9] = y2; clippedVerticesItems[s + 10] = light.r; clippedVerticesItems[s + 11] = light.g; clippedVerticesItems[s + 12] = light.b; clippedVerticesItems[s + 13] = light.a; clippedVerticesItems[s + 14] = u2; clippedVerticesItems[s + 15] = v2; clippedVerticesItems[s + 16] = x3; clippedVerticesItems[s + 17] = y3; clippedVerticesItems[s + 18] = light.r; clippedVerticesItems[s + 19] = light.g; clippedVerticesItems[s + 20] = light.b; clippedVerticesItems[s + 21] = light.a; clippedVerticesItems[s + 22] = u3; clippedVerticesItems[s + 23] = v3; } else { clippedVerticesItems[s + 6] = u1; clippedVerticesItems[s + 7] = v1; clippedVerticesItems[s + 8] = dark.r; clippedVerticesItems[s + 9] = dark.g; clippedVerticesItems[s + 10] = dark.b; clippedVerticesItems[s + 11] = dark.a; clippedVerticesItems[s + 12] = x2; clippedVerticesItems[s + 13] = y2; clippedVerticesItems[s + 14] = light.r; clippedVerticesItems[s + 15] = light.g; clippedVerticesItems[s + 16] = light.b; clippedVerticesItems[s + 17] = light.a; clippedVerticesItems[s + 18] = u2; clippedVerticesItems[s + 19] = v2; clippedVerticesItems[s + 20] = dark.r; clippedVerticesItems[s + 21] = dark.g; clippedVerticesItems[s + 22] = dark.b; clippedVerticesItems[s + 23] = dark.a; clippedVerticesItems[s + 24] = x3; clippedVerticesItems[s + 25] = y3; clippedVerticesItems[s + 26] = light.r; clippedVerticesItems[s + 27] = light.g; clippedVerticesItems[s + 28] = light.b; clippedVerticesItems[s + 29] = light.a; clippedVerticesItems[s + 30] = u3; clippedVerticesItems[s + 31] = v3; clippedVerticesItems[s + 32] = dark.r; clippedVerticesItems[s + 33] = dark.g; clippedVerticesItems[s + 34] = dark.b; clippedVerticesItems[s + 35] = dark.a; } s = clippedTriangles.length; const clippedTrianglesItems = Utils.setArraySize(clippedTriangles, s + 3); clippedTrianglesItems[s] = index; clippedTrianglesItems[s + 1] = (index + 1); clippedTrianglesItems[s + 2] = (index + 2); index += 3; break; } } } return clipOutputItems != null; } public clipTrianglesUnpacked (vertices: NumberArrayLike, triangles: NumberArrayLike | Uint32Array, trianglesLength: number, uvs: NumberArrayLike, stride = 2) { const clipOutput = this.clipOutput; let clippedVertices = this._clippedVerticesTyped, clippedUVs = this._clippedUVsTyped, clippedTriangles = this._clippedTrianglesTyped; // biome-ignore lint/style/noNonNullAssertion: clipStart define it const polygons = this.clippingPolygons!; const polygonsCount = polygons.length; let index = 0; this.clippedVerticesLength = 0; this.clippedUVsLength = 0; this.clippedTrianglesLength = 0; this._clippedVerticesTyped; this._clippedUVsTyped; this._clippedTrianglesTyped; let clipped = false; for (let i = 0; i < trianglesLength; i += 3) { let t = triangles[i]; let v = t * stride; const x1 = vertices[v], y1 = vertices[v + 1]; let uv = t << 1; const u1 = uvs[uv], v1 = uvs[uv + 1]; t = triangles[i + 1]; v = t * stride; const x2 = vertices[v], y2 = vertices[v + 1]; uv = t << 1; const u2 = uvs[uv], v2 = uvs[uv + 1]; t = triangles[i + 2]; v = t * stride; const x3 = vertices[v], y3 = vertices[v + 1]; uv = t << 1; const u3 = uvs[uv], v3 = uvs[uv + 1]; for (let p = 0; p < polygonsCount; p++) { let s = this.clippedVerticesLength; if (this.clip(x1, y1, x2, y2, x3, y3, polygons[p], clipOutput)) { const clipOutputLength = clipOutput.length; if (clipOutputLength === 0) continue; clipped = true; const d0 = y2 - y3, d1 = x3 - x2, d2 = x1 - x3, d4 = y3 - y1; const d = 1 / (d0 * d2 + d1 * (y1 - y3)); let clipOutputCount = clipOutputLength >> 1; const clipOutputItems = this.clipOutput; const newLength = s + clipOutputCount * stride; if (clippedVertices.length < newLength) { this._clippedVerticesTyped = new Float32Array(newLength * 2); this._clippedVerticesTyped.set(clippedVertices.subarray(0, s)); this._clippedUVsTyped = new Float32Array((this.clippedUVsLength + clipOutputCount * 2) * 2); this._clippedUVsTyped.set(clippedUVs.subarray(0, this.clippedUVsLength)); clippedVertices = this._clippedVerticesTyped; clippedUVs = this._clippedUVsTyped; } const clippedVerticesItems = clippedVertices; const clippedUVsItems = clippedUVs; this.clippedVerticesLength = newLength; let uvIndex = this.clippedUVsLength; this.clippedUVsLength = uvIndex + clipOutputCount * 2; for (let ii = 0; ii < clipOutputLength; ii += 2, s += stride, uvIndex += 2) { const x = clipOutputItems[ii], y = clipOutputItems[ii + 1]; clippedVerticesItems[s] = x; clippedVerticesItems[s + 1] = y; const c0 = x - x3, c1 = y - y3; const a = (d0 * c0 + d1 * c1) * d; const b = (d4 * c0 + d2 * c1) * d; const c = 1 - a - b; clippedUVsItems[uvIndex] = u1 * a + u2 * b + u3 * c; clippedUVsItems[uvIndex + 1] = v1 * a + v2 * b + v3 * c; } s = this.clippedTrianglesLength; const newLengthTriangles = s + 3 * (clipOutputCount - 2) if (clippedTriangles.length < newLengthTriangles) { this._clippedTrianglesTyped = new Uint32Array(newLengthTriangles * 2); this._clippedTrianglesTyped.set(clippedTriangles.subarray(0, s)); clippedTriangles = this._clippedTrianglesTyped; } this.clippedTrianglesLength = newLengthTriangles; const clippedTrianglesItems = clippedTriangles; clipOutputCount--; for (let ii = 1; ii < clipOutputCount; ii++, s += 3) { clippedTrianglesItems[s] = index; clippedTrianglesItems[s + 1] = (index + ii); clippedTrianglesItems[s + 2] = (index + ii + 1); } index += clipOutputCount + 1; } else { let newLength = s + 3 * stride; if (clippedVertices.length < newLength) { this._clippedVerticesTyped = new Float32Array(newLength * 2); this._clippedVerticesTyped.set(clippedVertices.subarray(0, s)); clippedVertices = this._clippedVerticesTyped; } clippedVertices[s] = x1; clippedVertices[s + 1] = y1; clippedVertices[s + stride] = x2; clippedVertices[s + stride + 1] = y2; clippedVertices[s + stride * 2] = x3; clippedVertices[s + stride * 2 + 1] = y3; let uvLength = this.clippedUVsLength + 3 * 2; if (clippedUVs.length < uvLength) { this._clippedUVsTyped = new Float32Array(uvLength * 2); this._clippedUVsTyped.set(clippedUVs.subarray(0, this.clippedUVsLength)); clippedUVs = this._clippedUVsTyped; } let uvIndex = this.clippedUVsLength; clippedUVs[uvIndex] = u1; clippedUVs[uvIndex + 1] = v1; clippedUVs[uvIndex + 2] = u2; clippedUVs[uvIndex + 3] = v2; clippedUVs[uvIndex + 4] = u3; clippedUVs[uvIndex + 5] = v3; this.clippedVerticesLength = newLength; this.clippedUVsLength = uvLength; s = this.clippedTrianglesLength; newLength = s + 3; if (clippedTriangles.length < newLength) { this._clippedTrianglesTyped = new Uint32Array(newLength * 2); this._clippedTrianglesTyped.set(clippedTriangles.subarray(0, s)); clippedTriangles = this._clippedTrianglesTyped; } clippedTriangles[s] = index; clippedTriangles[s + 1] = (index + 1); clippedTriangles[s + 2] = (index + 2); index += 3; this.clippedTrianglesLength = newLength; break; } } } this.clippedVerticesTyped = this._clippedVerticesTyped.subarray(0, this.clippedVerticesLength) this.clippedUVsTyped = this._clippedUVsTyped.subarray(0, this.clippedUVsLength) this.clippedTrianglesTyped = this._clippedTrianglesTyped.subarray(0, this.clippedTrianglesLength) return clipped; } /** Clips the input triangle against the convex, clockwise clipping area. If the triangle lies entirely within the clipping * area, false is returned. The clipping area must duplicate the first vertex at the end of the vertices list. */ private clip (x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, clippingArea: Array, output: Array) { const originalOutput = output; let clipped = false; // Avoid copy at the end. let input: Array; if (clippingArea.length % 4 >= 2) { input = output; output = this.scratch; } else input = this.scratch; input.length = 0; input.push(x1); input.push(y1); input.push(x2); input.push(y2); input.push(x3); input.push(y3); input.push(x1); input.push(y1); output.length = 0; const clippingVerticesLast = clippingArea.length - 4; const clippingVertices = clippingArea; for (let i = 0; ; i += 2) { const edgeX = clippingVertices[i], edgeY = clippingVertices[i + 1]; const ex = edgeX - clippingVertices[i + 2], ey = edgeY - clippingVertices[i + 3]; const outputStart = output.length; const inputVertices = input; for (let ii = 0, nn = input.length - 2; ii < nn;) { const inputX = inputVertices[ii], inputY = inputVertices[ii + 1]; ii += 2; const inputX2 = inputVertices[ii], inputY2 = inputVertices[ii + 1]; const s2 = ey * (edgeX - inputX2) > ex * (edgeY - inputY2); const s1 = ey * (edgeX - inputX) - ex * (edgeY - inputY); if (s1 > 0) { if (s2) { // v1 inside, v2 inside output.push(inputX2); output.push(inputY2); continue; } // v1 inside, v2 outside const ix = inputX2 - inputX, iy = inputY2 - inputY, t = s1 / (ix * ey - iy * ex); if (t >= 0 && t <= 1) { output.push(inputX + ix * t); output.push(inputY + iy * t); } else { output.push(inputX2); output.push(inputY2); continue; } } else if (s2) { // v1 outside, v2 inside const ix = inputX2 - inputX, iy = inputY2 - inputY, t = s1 / (ix * ey - iy * ex); if (t >= 0 && t <= 1) { output.push(inputX + ix * t); output.push(inputY + iy * t); output.push(inputX2); output.push(inputY2); } else { output.push(inputX2); output.push(inputY2); continue; } } clipped = true; } if (outputStart === output.length) { // All edges outside. originalOutput.length = 0; return true; } output.push(output[0]); output.push(output[1]); if (i === clippingVerticesLast) break; const temp = output; output = input; output.length = 0; input = temp; } if (originalOutput !== output) { originalOutput.length = 0; for (let i = 0, n = output.length - 2; i < n; i++) originalOutput[i] = output[i]; } else originalOutput.length = originalOutput.length - 2; return clipped; } public static makeClockwise (polygon: NumberArrayLike) { const vertices = polygon; const verticeslength = polygon.length; let area = vertices[verticeslength - 2] * vertices[1] - vertices[0] * vertices[verticeslength - 1], p1x = 0, p1y = 0, p2x = 0, p2y = 0; for (let i = 0, n = verticeslength - 3; i < n; i += 2) { p1x = vertices[i]; p1y = vertices[i + 1]; p2x = vertices[i + 2]; p2y = vertices[i + 3]; area += p1x * p2y - p2x * p1y; } if (area < 0) return; for (let i = 0, lastX = verticeslength - 2, n = verticeslength >> 1; i < n; i += 2) { const x = vertices[i], y = vertices[i + 1]; const other = lastX - i; vertices[i] = vertices[other]; vertices[i + 1] = vertices[other + 1]; vertices[other] = x; vertices[other + 1] = y; } } }