/****************************************************************************** * 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. *****************************************************************************/ package spine; import spine.attachments.ClippingAttachment; class SkeletonClipping { private var triangulator = new Triangulator(); private var clippingPolygon = new Array(); private var clipOutput = new Array(); public var clippedVertices = new Array(); public var clippedUvs = new Array(); public var clippedTriangles = new Array(); private var scratch = new Array(); private var clipAttachment:ClippingAttachment; private var clippingPolygons:Array>; public function new() {} public function clipStart(skeleton:Skeleton, slot:Slot, clip:ClippingAttachment):Int { if (clipAttachment != null) return 0; var n = clip.worldVerticesLength; if (n < 6) return 0; clipAttachment = clip; clippingPolygon.resize(n); clip.computeWorldVertices(skeleton, slot, 0, n, clippingPolygon, 0, 2); SkeletonClipping.makeClockwise(clippingPolygon); clippingPolygons = triangulator.decompose(clippingPolygon, triangulator.triangulate(clippingPolygon)); for (polygon in clippingPolygons) { SkeletonClipping.makeClockwise(polygon); polygon.push(polygon[0]); polygon.push(polygon[1]); } return clippingPolygons.length; } public function clipEnd(?slot:Slot):Void { if (clipAttachment == null || (slot != null && clipAttachment.endSlot != slot.data)) return; clipAttachment = null; clippingPolygons = null; clippedVertices.resize(0); clippedUvs.resize(0); clippedTriangles.resize(0); clippingPolygon.resize(0); clipOutput.resize(0); } public function isClipping():Bool { return clipAttachment != null; } private function clipTrianglesNoRender(vertices:Array, triangles:Array, trianglesLength:Float):Bool { var polygonsCount:Int = clippingPolygons.length; var index:Int = 0; clippedVertices.resize(0); clippedTriangles.resize(0); var i:Int = 0; var clipOutputItems:Array = null; while (i < trianglesLength) { var v:Int = triangles[i] << 1; var x1:Float = vertices[v], y1:Float = vertices[v + 1]; v = triangles[i + 1] << 1; var x2:Float = vertices[v], y2:Float = vertices[v + 1]; v = triangles[i + 2] << 1; var x3:Float = vertices[v], y3:Float = vertices[v + 1]; for (p in 0...polygonsCount) { var s:Int = clippedVertices.length; var clippedVerticesItems:Array; var clippedTrianglesItems:Array; if (this.clip(x1, y1, x2, y2, x3, y3, clippingPolygons[p], clipOutput)) { clipOutputItems = clipOutput; var clipOutputLength:Int = clipOutput.length; if (clipOutputLength == 0) continue; var clipOutputCount:Int = clipOutputLength >> 1; clippedVerticesItems = clippedVertices; clippedVerticesItems.resize(s + clipOutputLength); var ii:Int = 0; while (ii < clipOutputLength) { var x:Float = clipOutputItems[ii], y:Float = clipOutputItems[ii + 1]; clippedVerticesItems[s] = x; clippedVerticesItems[s + 1] = y; s += 2; ii += 2; } s = clippedTriangles.length; clippedTrianglesItems = clippedTriangles; clippedTrianglesItems.resize(s + 3 * (clipOutputCount - 2)); clipOutputCount--; for (ii in 1...clipOutputCount) { clippedTrianglesItems[s] = index; clippedTrianglesItems[s + 1] = (index + ii); clippedTrianglesItems[s + 2] = (index + ii + 1); s += 3; } index += clipOutputCount + 1; } else { clippedVerticesItems = clippedVertices; clippedVerticesItems.resize(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; clippedTrianglesItems = clippedTriangles; clippedTrianglesItems.resize(s + 3); clippedTrianglesItems[s] = index; clippedTrianglesItems[s + 1] = (index + 1); clippedTrianglesItems[s + 2] = (index + 2); index += 3; break; } } i += 3; } return clipOutputItems != null; } public function clipTriangles(vertices:Array, triangles:Array, trianglesLength:Float, uvs:Array = null):Bool { if (uvs == null) { return clipTrianglesNoRender(vertices, triangles, trianglesLength); } var polygonsCount:Int = clippingPolygons.length; var index:Int = 0; clippedVertices.resize(0); clippedUvs.resize(0); clippedTriangles.resize(0); var i:Int = 0; var clipOutputItems:Array = null; while (i < trianglesLength) { var vertexOffset:Int = triangles[i] << 1; var x1 = vertices[vertexOffset], y1 = vertices[vertexOffset + 1]; var u1 = uvs[vertexOffset], v1 = uvs[vertexOffset + 1]; vertexOffset = triangles[i + 1] << 1; var x2 = vertices[vertexOffset], y2 = vertices[vertexOffset + 1]; var u2 = uvs[vertexOffset], v2 = uvs[vertexOffset + 1]; vertexOffset = triangles[i + 2] << 1; var x3 = vertices[vertexOffset], y3 = vertices[vertexOffset + 1]; var u3 = uvs[vertexOffset], v3 = uvs[vertexOffset + 1]; for (p in 0...polygonsCount) { var s:Int = clippedVertices.length; var clippedVerticesItems:Array; var clippedUvsItems:Array; var clippedTrianglesItems:Array; if (this.clip(x1, y1, x2, y2, x3, y3, clippingPolygons[p], clipOutput)) { clipOutputItems = clipOutput; var clipOutputLength:Int = clipOutput.length; if (clipOutputLength == 0) continue; var d0 = y2 - y3, d1 = x3 - x2, d2 = x1 - x3, d4 = y3 - y1; var d = 1 / (d0 * d2 + d1 * (y1 - y3)); var clipOutputCount:Int = clipOutputLength >> 1; clippedVerticesItems = clippedVertices; clippedVerticesItems.resize(s + clipOutputLength); clippedUvsItems = clippedUvs; clippedUvsItems.resize(s + clipOutputLength); var ii:Int = 0; while (ii < clipOutputLength) { var x = clipOutputItems[ii], y = clipOutputItems[ii + 1]; clippedVerticesItems[s] = x; clippedVerticesItems[s + 1] = y; var c0 = x - x3, c1 = y - y3; var a = (d0 * c0 + d1 * c1) * d; var b = (d4 * c0 + d2 * c1) * d; var c = 1 - a - b; clippedUvsItems[s] = u1 * a + u2 * b + u3 * c; clippedUvsItems[s + 1] = v1 * a + v2 * b + v3 * c; s += 2; ii += 2; } s = clippedTriangles.length; clippedTrianglesItems = clippedTriangles; clippedTrianglesItems.resize(s + 3 * (clipOutputCount - 2)); clipOutputCount--; for (ii in 1...clipOutputCount) { clippedTrianglesItems[s] = index; clippedTrianglesItems[s + 1] = (index + ii); clippedTrianglesItems[s + 2] = (index + ii + 1); s += 3; } index += clipOutputCount + 1; } else { clippedVerticesItems = clippedVertices; clippedVerticesItems.resize(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; clippedUvsItems = clippedUvs; clippedUvsItems.resize(s + 3); clippedUvsItems[s] = u1; clippedUvsItems[s + 1] = v1; clippedUvsItems[s + 2] = u2; clippedUvsItems[s + 3] = v2; clippedUvsItems[s + 4] = u3; clippedUvsItems[s + 5] = v3; s = clippedTriangles.length; clippedTrianglesItems = clippedTriangles; clippedTrianglesItems.resize(s + 3); clippedTrianglesItems[s] = index; clippedTrianglesItems[s + 1] = (index + 1); clippedTrianglesItems[s + 2] = (index + 2); index += 3; break; } } i += 3; } return clipOutputItems != null; } /** * 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 function clip(x1:Float, y1:Float, x2:Float, y2:Float, x3:Float, y3:Float, clippingArea:Array, output:Array):Bool { var originalOutput:Array = output; var clipped:Bool = false; // Avoid copy at the end. var input:Array = null; if (clippingArea.length % 4 >= 2) { input = output; output = scratch; } else { input = scratch; } input.resize(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.resize(0); var clippingVerticesLast:Int = clippingArea.length - 4; var clippingVertices:Array = clippingArea; var ix:Float, iy:Float, t:Float; var i:Int = 0; var n:Int = 0; while (true) { var edgeX:Float = clippingVertices[i], edgeY:Float = clippingVertices[i + 1]; var ex:Float = edgeX - clippingVertices[i + 2], ey:Float = edgeY - clippingVertices[i + 3]; var outputStart:Int = output.length; var inputVertices:Array = input; var ii:Int = 0; var nn:Int = input.length - 2; while (ii < nn) { var inputX:Float = inputVertices[ii], inputY:Float = inputVertices[ii + 1]; ii += 2; var inputX2:Float = inputVertices[ii], inputY2:Float = inputVertices[ii + 1]; var s2:Bool = ey * (edgeX - inputX2) > ex * (edgeY - inputY2); var s1:Float = 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 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 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.resize(0); return true; } output.push(output[0]); output.push(output[1]); if (i == clippingVerticesLast) break; var temp:Array = output; output = input; output.resize(0); input = temp; i += 2; } if (originalOutput != output) { originalOutput.resize(0); n = output.length - 2; for (i in 0...n) { originalOutput[i] = output[i]; } } else { originalOutput.resize(originalOutput.length - 2); } return clipped; } public static function makeClockwise(polygon:Array):Void { var vertices:Array = polygon; var verticeslength:Int = polygon.length; var area:Float = vertices[verticeslength - 2] * vertices[1] - vertices[0] * vertices[verticeslength - 1]; var p1x:Float = 0, p1y:Float = 0, p2x:Float = 0, p2y:Float = 0; var i:Int = 0; var n:Int = verticeslength - 3; while (i < n) { p1x = vertices[i]; p1y = vertices[i + 1]; p2x = vertices[i + 2]; p2y = vertices[i + 3]; area += p1x * p2y - p2x * p1y; i += 2; } if (area < 0) return; i = 0; n = verticeslength >> 1; var lastX:Int = verticeslength - 2; while (i < n) { var x:Float = vertices[i], y:Float = vertices[i + 1]; var other:Int = lastX - i; vertices[i] = vertices[other]; vertices[i + 1] = vertices[other + 1]; vertices[other] = x; vertices[other + 1] = y; i += 2; } } }