diff --git a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java index 5001ab8f6..b65a6cd69 100644 --- a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java +++ b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java @@ -38,6 +38,7 @@ import com.esotericsoftware.spine.attachments.AtlasAttachmentLoader; import com.esotericsoftware.spine.attachments.Attachment; import com.esotericsoftware.spine.attachments.AttachmentLoader; import com.esotericsoftware.spine.attachments.AttachmentType; +import com.esotericsoftware.spine.attachments.BoundingBoxAttachment; import com.esotericsoftware.spine.attachments.RegionAttachment; import com.esotericsoftware.spine.attachments.RegionSequenceAttachment; import com.esotericsoftware.spine.attachments.RegionSequenceAttachment.Mode; @@ -187,9 +188,8 @@ public class SkeletonBinary { RegionSequenceAttachment regionSequenceAttachment = (RegionSequenceAttachment)attachment; regionSequenceAttachment.setFrameTime(1 / input.readFloat()); regionSequenceAttachment.setMode(Mode.values()[input.readInt(true)]); - } - if (attachment instanceof RegionAttachment) { + } else if (attachment instanceof RegionAttachment) { RegionAttachment regionAttachment = (RegionAttachment)attachment; regionAttachment.setX(input.readFloat() * scale); regionAttachment.setY(input.readFloat() * scale); @@ -199,6 +199,14 @@ public class SkeletonBinary { regionAttachment.setWidth(input.readFloat() * scale); regionAttachment.setHeight(input.readFloat() * scale); regionAttachment.updateOffset(); + + } else if (attachment instanceof BoundingBoxAttachment) { + BoundingBoxAttachment box = (BoundingBoxAttachment)attachment; + int n = input.readInt(true); + float[] points = new float[n]; + for (int i = 0; i < n; i++) + points[i] = input.readFloat(); + box.setPoints(points); } return attachment; diff --git a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBounds.java b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBounds.java new file mode 100644 index 000000000..807ddaaa8 --- /dev/null +++ b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBounds.java @@ -0,0 +1,182 @@ + +package com.esotericsoftware.spine; + +import com.esotericsoftware.spine.attachments.Attachment; +import com.esotericsoftware.spine.attachments.BoundingBoxAttachment; + +import com.badlogic.gdx.utils.Array; + +public class SkeletonBounds { + private boolean aabb; + private float minX, minY, maxX, maxY; + private Array boundingBoxAttachments = new Array(); + + public void update (Skeleton skeleton) { + aabb = false; + Array polygons = this.boundingBoxAttachments; + polygons.clear(); + Array slots = skeleton.slots; + for (int i = 0, n = slots.size; i < n; i++) { + Slot slot = slots.get(i); + Attachment attachment = slot.attachment; + if (attachment instanceof BoundingBoxAttachment) { + BoundingBoxAttachment boundingBox = (BoundingBoxAttachment)attachment; + boundingBox.updateVertices(slot); + polygons.add(boundingBox); + } + } + } + + private void aabbCompute () { + float minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE; + Array boundingBoxes = this.boundingBoxAttachments; + for (int i = 0, n = boundingBoxes.size; i < n; i++) { + float[] vertices = boundingBoxes.get(i).getVertices(); + for (int ii = 0, nn = vertices.length; ii < nn; ii += 2) { + float x = vertices[ii]; + float 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; + aabb = true; + } + + /** Returns true if the axis aligned bounding box contains the point. */ + public boolean aabbContainsPoint (float x, float y) { + if (!aabb) aabbCompute(); + return x >= minX && x <= maxX && y >= minY && y <= maxY; + } + + /** Returns true if the axis aligned bounding box intersects the line segment. */ + public boolean aabbIntersectsSegment (float x1, float y1, float x2, float y2) { + if (!aabb) aabbCompute(); + float minX = this.minX; + float minY = this.minY; + float maxX = this.maxX; + float maxY = this.maxY; + if ((x1 <= minX && x2 <= minX) || (y1 <= minY && y2 <= minY) || (x1 >= maxX && x2 >= maxX) || (y1 >= maxY && y2 >= maxY)) + return false; + float m = (y2 - y1) / (x2 - x1); + float y = m * (minX - x1) + y1; + if (y > minY && y < maxY) return true; + y = m * (maxX - x1) + y1; + if (y > minY && y < maxY) return true; + float 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. */ + public boolean aabbIntersectsSkeleton (SkeletonBounds bounds) { + if (!aabb) aabbCompute(); + if (!bounds.aabb) bounds.aabbCompute(); + return minX < bounds.maxX && maxX > bounds.minX && minY < bounds.maxY && 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)} return true. */ + public BoundingBoxAttachment containsPoint (float x, float y) { + Array boundingBoxes = this.boundingBoxAttachments; + for (int i = 0, n = boundingBoxes.size; i < n; i++) { + BoundingBoxAttachment attachment = boundingBoxes.get(i); + if (containsPoint(attachment, x, y)) return attachment; + } + return null; + } + + /** Returns true if the bounding box attachment contains the point. */ + public boolean containsPoint (BoundingBoxAttachment attachment, float x, float y) { + float[] vertices = attachment.getVertices(); + int nn = vertices.length; + int prevIndex = nn - 2; + boolean inside = false; + for (int ii = 0; ii < nn; ii += 2) { + float vertexY = vertices[ii + 1]; + float prevY = vertices[prevIndex + 1]; + if (vertexY < y && prevY >= y || prevY < y && vertexY >= y) { + float 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 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)} return true. */ + public BoundingBoxAttachment intersectsSegment (float x1, float y1, float x2, float y2) { + Array boundingBoxes = this.boundingBoxAttachments; + for (int i = 0, n = boundingBoxes.size; i < n; i++) { + BoundingBoxAttachment attachment = boundingBoxes.get(i); + if (intersectsSegment(attachment, x1, y1, x2, y2)) return attachment; + } + return null; + } + + /** Returns true if the bounding box attachment contains the line segment. */ + public boolean intersectsSegment (BoundingBoxAttachment attachment, float x1, float y1, float x2, float y2) { + float[] vertices = attachment.getVertices(); + float width12 = x1 - x2, height12 = y1 - y2; + float det1 = x1 * y2 - y1 * x2; + int nn = vertices.length; + float x3 = vertices[nn - 2], y3 = vertices[nn - 1]; + for (int ii = 0; ii < nn; ii += 2) { + float x4 = vertices[ii], y4 = vertices[ii + 1]; + float det2 = x3 * y4 - y3 * x4; + float width34 = x3 - x4, height34 = y3 - y4; + float det3 = width12 * height34 - height12 * width34; + float x = (det1 * width34 - width12 * det2) / det3; + if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) { + float 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; + } + + public float getMinX () { + if (!aabb) aabbCompute(); + return minX; + } + + public float getMinY () { + if (!aabb) aabbCompute(); + return minY; + } + + public float getMaxX () { + if (!aabb) aabbCompute(); + return maxX; + } + + public float getMaxY () { + if (!aabb) aabbCompute(); + return maxY; + } + + public float getWidth () { + if (!aabb) aabbCompute(); + return maxX - minX; + } + + public float getHeight () { + if (!aabb) aabbCompute(); + return maxY - minY; + } + + public Array getBoundingBoxAttachments () { + return boundingBoxAttachments; + } +} diff --git a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java index 67a627793..4083f7f6e 100644 --- a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java +++ b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java @@ -38,6 +38,7 @@ import com.esotericsoftware.spine.attachments.AtlasAttachmentLoader; import com.esotericsoftware.spine.attachments.Attachment; import com.esotericsoftware.spine.attachments.AttachmentLoader; import com.esotericsoftware.spine.attachments.AttachmentType; +import com.esotericsoftware.spine.attachments.BoundingBoxAttachment; import com.esotericsoftware.spine.attachments.RegionAttachment; import com.esotericsoftware.spine.attachments.RegionSequenceAttachment; import com.esotericsoftware.spine.attachments.RegionSequenceAttachment.Mode; @@ -172,9 +173,8 @@ public class SkeletonJson { String modeString = map.getString("mode"); regionSequenceAttachment.setMode(modeString == null ? Mode.forward : Mode.valueOf(modeString)); - } - if (attachment instanceof RegionAttachment) { + } else if (attachment instanceof RegionAttachment) { RegionAttachment regionAttachment = (RegionAttachment)attachment; regionAttachment.setX(map.getFloat("x", 0) * scale); regionAttachment.setY(map.getFloat("y", 0) * scale); @@ -184,6 +184,15 @@ public class SkeletonJson { regionAttachment.setWidth(map.getFloat("width", 32) * scale); regionAttachment.setHeight(map.getFloat("height", 32) * scale); regionAttachment.updateOffset(); + + } else if (attachment instanceof BoundingBoxAttachment) { + BoundingBoxAttachment box = (BoundingBoxAttachment)attachment; + JsonValue pointsArray = map.require("points"); + float[] points = new float[pointsArray.size]; + int i = 0; + for (JsonValue point = pointsArray.child; point != null; point = point.next()) + points[i++] = point.asFloat(); + box.setPoints(points); } return attachment; diff --git a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRendererDebug.java b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRendererDebug.java index 7b6db72a6..7ea5a29c4 100644 --- a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRendererDebug.java +++ b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRendererDebug.java @@ -2,6 +2,7 @@ package com.esotericsoftware.spine; import com.esotericsoftware.spine.attachments.Attachment; +import com.esotericsoftware.spine.attachments.BoundingBoxAttachment; import com.esotericsoftware.spine.attachments.RegionAttachment; import static com.badlogic.gdx.graphics.g2d.SpriteBatch.*; @@ -14,9 +15,15 @@ import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.badlogic.gdx.utils.Array; public class SkeletonRendererDebug { - static private final Color slotLineColor = new Color(0, 0, 1, 0.5f); + static private final Color boneLineColor = Color.RED; + static private final Color boneOriginColor = Color.GREEN; + static private final Color regionAttachmentLineColor = new Color(0, 0, 1, 0.5f); + static private final Color boundingBoxColor = new Color(0, 1, 0, 0.8f); + static private final Color aabbColor = new Color(0, 1, 0, 0.5f); - private ShapeRenderer renderer; + private final ShapeRenderer renderer; + private boolean drawBones = true, drawRegionAttachments = true, drawBoundingBoxes = true; + private final SkeletonBounds bounds = new SkeletonBounds(); public SkeletonRendererDebug () { renderer = new ShapeRenderer(); @@ -27,47 +34,78 @@ public class SkeletonRendererDebug { float skeletonY = skeleton.getY(); Gdx.gl.glEnable(GL10.GL_BLEND); + ShapeRenderer renderer = this.renderer; renderer.begin(ShapeType.Line); - renderer.setColor(Color.RED); Array bones = skeleton.getBones(); - for (int i = 0, n = bones.size; i < n; i++) { - Bone bone = bones.get(i); - if (bone.parent == null) continue; - float x = skeletonX + bone.data.length * bone.m00 + bone.worldX; - float y = skeletonY + bone.data.length * bone.m10 + bone.worldY; - renderer.line(skeletonX + bone.worldX, skeletonY + bone.worldY, x, y); - } - - renderer.setColor(slotLineColor); - Array slots = skeleton.getSlots(); - for (int i = 0, n = slots.size; i < n; i++) { - Slot slot = slots.get(i); - Attachment attachment = slot.attachment; - if (attachment instanceof RegionAttachment) { - RegionAttachment regionAttachment = (RegionAttachment)attachment; - regionAttachment.updateVertices(slot, false); - float[] vertices = regionAttachment.getVertices(); - renderer.line(vertices[X1], vertices[Y1], vertices[X2], vertices[Y2]); - renderer.line(vertices[X2], vertices[Y2], vertices[X3], vertices[Y3]); - renderer.line(vertices[X3], vertices[Y3], vertices[X4], vertices[Y4]); - renderer.line(vertices[X4], vertices[Y4], vertices[X1], vertices[Y1]); + if (drawBones) { + renderer.setColor(boneLineColor); + for (int i = 0, n = bones.size; i < n; i++) { + Bone bone = bones.get(i); + if (bone.parent == null) continue; + float x = skeletonX + bone.data.length * bone.m00 + bone.worldX; + float y = skeletonY + bone.data.length * bone.m10 + bone.worldY; + renderer.line(skeletonX + bone.worldX, skeletonY + bone.worldY, x, y); } } - renderer.end(); - - renderer.setColor(Color.GREEN); - renderer.begin(ShapeType.Filled); - for (int i = 0, n = bones.size; i < n; i++) { - Bone bone = bones.get(i); - renderer.setColor(Color.GREEN); - renderer.circle(skeletonX + bone.worldX, skeletonY + bone.worldY, 3); + if (drawRegionAttachments) { + renderer.setColor(regionAttachmentLineColor); + Array slots = skeleton.getSlots(); + for (int i = 0, n = slots.size; i < n; i++) { + Slot slot = slots.get(i); + Attachment attachment = slot.attachment; + if (attachment instanceof RegionAttachment) { + RegionAttachment regionAttachment = (RegionAttachment)attachment; + regionAttachment.updateVertices(slot, false); + float[] vertices = regionAttachment.getVertices(); + renderer.line(vertices[X1], vertices[Y1], vertices[X2], vertices[Y2]); + renderer.line(vertices[X2], vertices[Y2], vertices[X3], vertices[Y3]); + renderer.line(vertices[X3], vertices[Y3], vertices[X4], vertices[Y4]); + renderer.line(vertices[X4], vertices[Y4], vertices[X1], vertices[Y1]); + } + } } + + if (drawBoundingBoxes) { + SkeletonBounds bounds = this.bounds; + bounds.update(skeleton); + renderer.setColor(aabbColor); + renderer.rect(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight()); + renderer.setColor(boundingBoxColor); + Array boundingBoxes = bounds.getBoundingBoxAttachments(); + for (int i = 0, n = boundingBoxes.size; i < n; i++) + renderer.polygon(boundingBoxes.get(i).getVertices()); + } + + renderer.end(); + renderer.begin(ShapeType.Filled); + + if (drawBones) { + renderer.setColor(boneOriginColor); + for (int i = 0, n = bones.size; i < n; i++) { + Bone bone = bones.get(i); + renderer.setColor(Color.GREEN); + renderer.circle(skeletonX + bone.worldX, skeletonY + bone.worldY, 3); + } + } + renderer.end(); } public ShapeRenderer getShapeRenderer () { return renderer; } + + public void setBones (boolean bones) { + this.drawBones = bones; + } + + public void setRegionAttachments (boolean regionAttachments) { + this.drawRegionAttachments = regionAttachments; + } + + public void setBoundingBoxes (boolean boundingBoxes) { + this.drawBoundingBoxes = boundingBoxes; + } } diff --git a/spine-libgdx/src/com/esotericsoftware/spine/attachments/AtlasAttachmentLoader.java b/spine-libgdx/src/com/esotericsoftware/spine/attachments/AtlasAttachmentLoader.java index cad145f0e..3587f0c4f 100644 --- a/spine-libgdx/src/com/esotericsoftware/spine/attachments/AtlasAttachmentLoader.java +++ b/spine-libgdx/src/com/esotericsoftware/spine/attachments/AtlasAttachmentLoader.java @@ -44,9 +44,11 @@ public class AtlasAttachmentLoader implements AttachmentLoader { case region: attachment = new RegionAttachment(name); break; - case regionSequence: + case regionsequence: attachment = new RegionSequenceAttachment(name); break; + case boundingbox: + return new BoundingBoxAttachment(name); default: throw new IllegalArgumentException("Unknown attachment type: " + type); } diff --git a/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentType.java b/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentType.java index 78329c3bd..67736cfd8 100644 --- a/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentType.java +++ b/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentType.java @@ -26,5 +26,5 @@ package com.esotericsoftware.spine.attachments; public enum AttachmentType { - region, regionSequence + region, regionsequence, boundingbox } diff --git a/spine-libgdx/src/com/esotericsoftware/spine/attachments/BoundingBoxAttachment.java b/spine-libgdx/src/com/esotericsoftware/spine/attachments/BoundingBoxAttachment.java new file mode 100644 index 000000000..5e8a21a58 --- /dev/null +++ b/spine-libgdx/src/com/esotericsoftware/spine/attachments/BoundingBoxAttachment.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 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 THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +package com.esotericsoftware.spine.attachments; + +import com.esotericsoftware.spine.Bone; +import com.esotericsoftware.spine.Skeleton; +import com.esotericsoftware.spine.Slot; + +public class BoundingBoxAttachment extends Attachment { + private float[] points; + private float[] vertices; + + public BoundingBoxAttachment (String name) { + super(name); + } + + public void updateVertices (Slot slot) { + Bone bone = slot.getBone(); + Skeleton skeleton = slot.getSkeleton(); + float x = bone.getWorldX() + skeleton.getX(); + float y = bone.getWorldY() + skeleton.getY(); + float m00 = bone.getM00(); + float m01 = bone.getM01(); + float m10 = bone.getM10(); + float m11 = bone.getM11(); + float[] vertices = this.vertices; + float[] points = this.points; + for (int i = 0, n = points.length; i < n; i += 2) { + float px = points[i]; + float py = points[i + 1]; + vertices[i] = px * m00 + py * m01 + x; + vertices[i + 1] = px * m10 + py * m11 + y; + } + } + + public float[] getVertices () { + return vertices; + } + + public float[] getPoints () { + return points; + } + + public void setPoints (float[] points) { + this.points = points; + if (vertices == null || vertices.length != points.length) vertices = new float[points.length]; + } +} diff --git a/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java b/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java index 0c3c8e4ac..db9825a48 100644 --- a/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java +++ b/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java @@ -177,6 +177,10 @@ public class RegionAttachment extends Attachment { return vertices; } + public float[] getOffset () { + return offset; + } + public float getX () { return x; }