/* * Copyright (C) 2016 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.apps.santatracker.doodles.shared.physics; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import com.google.android.apps.santatracker.doodles.shared.Vector2D; import com.google.android.apps.santatracker.doodles.tilt.Constants; import com.google.android.apps.santatracker.doodles.tilt.SwimmingFragment; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; /** * A general polygon class (either concave or convex) which can tell whether or not a point * is inside of it. * * <p>NOTE: vertex winding order affects the normals of the line segments, and can affect things * like collisions. A non-inverted (normals pointed out) polygon should have its vertices wound * clockwise.</p> * */ public class Polygon { private static final String TAG = Polygon.class.getSimpleName(); private static final float EPSILON = 0.0001f; private static final float VERTEX_RADIUS = 10; private Paint vertexPaint; private Paint midpointPaint; private Paint linePaint; public List<Vector2D> vertices; public List<Vector2D> normals; public Vector2D min; public Vector2D max; private boolean isInverted; public Polygon(List<Vector2D> vertices) { this.vertices = vertices; min = Vector2D.get(0, 0); max = Vector2D.get(0, 0); vertexPaint = new Paint(Paint.ANTI_ALIAS_FLAG); vertexPaint.setColor(Color.RED); midpointPaint = new Paint(Paint.ANTI_ALIAS_FLAG); midpointPaint.setColor(Color.GREEN); midpointPaint.setAlpha(100); linePaint = new Paint(Paint.ANTI_ALIAS_FLAG); linePaint.setColor(Color.WHITE); linePaint.setStrokeWidth(5); updateExtents(); updateInversionStatus(); calculateNormals(); } public void updateExtents() { min.set(this.vertices.get(0)); max.set(this.vertices.get(0)); for (int i = 0; i < vertices.size(); i++) { Vector2D point = vertices.get(i); min.x = Math.min(min.x, point.x); min.y = Math.min(min.y, point.y); max.x = Math.max(max.x, point.x); max.y = Math.max(max.y, point.y); } } public void calculateNormals() { normals = new ArrayList<>(); for (int i = 0; i < vertices.size(); i++) { Vector2D start = vertices.get(i); Vector2D end = vertices.get((i + 1) % vertices.size()); normals.add(Vector2D.get(end).subtract(start).toNormal()); } } public float getWidth() { return max.x - min.x; } public float getHeight() { return max.y - min.y; } public void moveTo(float x, float y) { float deltaX = x - min.x; float deltaY = y - min.y; move(deltaX, deltaY); } public void move(float x, float y) { for (int i = 0; i < vertices.size(); i++) { Vector2D vertex = vertices.get(i); vertex.x += x; vertex.y += y; } // Rather than update the extents by checking all of the vertices here, we can just update them // manually (they will move by the same amount as the rest of the vertices). min.x += x; min.y += y; max.x += x; max.y += y; } public void moveVertex(int index, Vector2D delta) { Vector2D vertex = vertices.get(index); vertex.x += delta.x; vertex.y += delta.y; updateExtents(); updateInversionStatus(); } public void addVertexAfter(int index) { int nextIndex = index < vertices.size() - 1 ? index + 1 : 0; Vector2D newVertex = Util.getMidpoint(vertices.get(index), vertices.get(nextIndex)); vertices.add(nextIndex, newVertex); updateExtents(); calculateNormals(); } public void removeVertexAt(int index) { vertices.remove(index); updateExtents(); } /** * Return the index of the vertex selected by the given point. * * @param point the point at which to check for a selected vertex. * @param scale the scale of the world, for slackening the selection radius if needed. * @return the index of the selected vertex, or -1 if no vertex was selected. */ public int getSelectedIndex(Vector2D point, float scale) { for (int i = 0; i < vertices.size(); i++) { Vector2D vertex = vertices.get(i); if (point.distanceTo(vertex) < Math.max(Constants.SELECTION_RADIUS, Constants.SELECTION_RADIUS / scale)) { return i; } } return -1; } public int getMidpointIndex(Vector2D point, float scale) { for (int i = 0; i < vertices.size(); i++) { Vector2D start = vertices.get(i); Vector2D end = vertices.get(i < vertices.size() - 1 ? i + 1 : 0); if (point.distanceTo(Util.getMidpoint(start, end)) < Math.max(Constants.SELECTION_RADIUS, Constants.SELECTION_RADIUS / scale)) { return i; } } return -1; } /** * Return whether or not this polygon is inverted (i.e., whether or not the polygon has a normal * which points inwards. * * @return true if the polygon is inverted, false otherwise. */ public boolean isInverted() { return isInverted; } /** * Calculate whether or not this polygon is inverted. This checks to see if the point which is one * unit in the normal direction on the polygon's first segment is within the bounds of the * polygon. If this is the case, then the normal points inwards and the polygon is inverted. * Otherwise, the polygon is not inverted. * * <p>Note: This doesn't deal with polygons which are partially inverted. These sorts of polygons * should be avoided, as they will break this function.</p> */ private void updateInversionStatus() { Vector2D start = vertices.get(0); Vector2D end = vertices.get(1); Vector2D midpoint = Util.getMidpoint(start, end); Vector2D normal = Vector2D.get(end).subtract(start).toNormal().scale(0.1f); if (contains(midpoint.add(normal))) { isInverted = true; } else { isInverted = false; } normal.release(); midpoint.release(); } /** * Return whether or not this polygon's collision boundaries contain a given point. A polygon * contains a point iff the point is contained within the polygon's collision boundaries, * regardless of the direction of the polygon's normals. * * @param point the point to check * @return true if this polygon contains the point, false otherwise. */ public boolean contains(Vector2D point) { // If the bounding box doesn't contain the point, we don't need to do any more calculations. if (!Util.pointIsWithinBounds(min, max, point)) { return false; } // Cast vertical ray from point to outside polygon and counting crossings. Point is in polygon // iff number of edges crossed is odd. // Find a Y value that's definitely outside the polygon. float maxY = max.y + 1; Vector2D outsidePoint = Vector2D.get(point.x, maxY); // Check how many edges lie between (p.x, p.y) and (p.x, maxY). boolean inside = false; for (int i = 0; i < vertices.size(); i++) { Vector2D p1 = vertices.get(i); Vector2D p2; if (i < vertices.size() - 1) { p2 = vertices.get(i + 1); } else { p2 = vertices.get(0); } // First check endpoints. Hitting left-most point counts, hitting right-most // doesn't (to weed out case where ray hits 2 lines at their joining vertex) } if (p1.y >= point.y && Math.abs(p1.x - point.x) <= EPSILON) { if (p2.x >= point.x) { inside = !inside; } continue; } else if (p2.y >= point.y && Math.abs(p2.x - point.x) <= EPSILON) { if (p1.x >= point.x) { inside = !inside; } continue; } // Now check for intersection. if (Util.lineSegmentIntersectsLineSegment(p1, p2, point, outsidePoint)) { inside = !inside; } } outsidePoint.release(); return inside; } public LineSegment getIntersectingLineSegment(Vector2D p, Vector2D q) { for (int i = 0; i < vertices.size(); i++) { Vector2D p1 = vertices.get(i); Vector2D p2; if (i < vertices.size() - 1) { p2 = vertices.get(i + 1); } else { p2 = vertices.get(0); } if (Util.lineSegmentIntersectsLineSegment(p1, p2, p, q)) { return new LineSegment(p1, p2); } } return null; } public void draw(Canvas canvas) { if (!(SwimmingFragment.editorMode)) { return; } for (int i = 0; i < vertices.size(); i++) { Vector2D start = vertices.get(i); Vector2D end; if (i < vertices.size() - 1) { end = vertices.get(i + 1); } else { end = vertices.get(0); } Vector2D midpoint = Util.getMidpoint(start, end); Vector2D normal = Vector2D.get(end).subtract(start).toNormal(); canvas.drawCircle(start.x, start.y, VERTEX_RADIUS, vertexPaint); canvas.drawLine(start.x, start.y, end.x, end.y, linePaint); canvas.drawCircle(midpoint.x, midpoint.y, VERTEX_RADIUS / 2, midpointPaint); canvas.drawLine(midpoint.x, midpoint.y, midpoint.x + normal.x * 20, midpoint.y + normal.y * 20, linePaint); midpoint.release(); normal.release(); } } public void setPaintColors(int vertexColor, int lineColor, int midpointColor) { vertexPaint.setColor(vertexColor); linePaint.setColor(lineColor); midpointPaint.setColor(midpointColor); } public JSONArray toJSON() throws JSONException { JSONArray json = new JSONArray(); for (int i = 0; i < vertices.size(); i++) { JSONObject vertexJson = new JSONObject(); Vector2D vertex = vertices.get(i); vertexJson.put("x", (double) vertex.x); vertexJson.put("y", (double) vertex.y); json.put(vertexJson); } return json; } public static Polygon fromJSON(JSONArray json) throws JSONException { List<Vector2D> vertices = new ArrayList<>(); for (int i = 0; i < json.length(); i++) { JSONObject vertexJson = json.getJSONObject(i); Vector2D vertex = Vector2D.get((float) vertexJson.getDouble("x"), (float) vertexJson.getDouble("y")); vertices.add(vertex); } return new Polygon(vertices); } /** * A class to specify the starting and ending point of a line segment. Currently only used in * determining which line segment is being collided with, so we can determine the normal vector. */ public static class LineSegment { public Vector2D start; public Vector2D end; public LineSegment(Vector2D start, Vector2D end) { this.start = start; this.end = end; } public Vector2D getDirection() { return Vector2D.get(end).subtract(start); } } }