namespace Shapes2D { using UnityEngine; using UnityEditor; using UnityEngine.UI; using System.Collections.Generic; [CustomEditor(typeof(Shape))] [CanEditMultipleObjects] public class ShapeEditor : Editor { // used during sprite conversion to keep track of objects that we have // to temporarily modify class GraphicState { public Graphic graphic; public bool hasMask; public bool showMaskGraphic; } SerializedProperty shapeTypeProp, outlineSizeProp, blurProp, outlineColorProp, fillTypeProp, fillColorProp, fillColor2Prop, gradientTypeProp, roundnessProp, roundnessTLProp, roundnessTRProp, roundnessBLProp, roundnessBRProp, roundnessPerCornerProp, fillRotationProp, fillOffsetProp, gradientStartProp, fillTextureProp, gridSizeProp, lineSizeProp, triangleOffsetProp, fillScaleProp, gradientAxisProp, polygonPresetProp, usePolygonMapProp, startAngleProp, endAngleProp, invertArcProp, innerCutoutProp, pathThicknessProp, fillPathLoopsProp; bool isEditing; // true if we're in polygon/path edit mode in the scene view Tool preEditTool = Tool.None; // the tool the user had selected before clicking edit void OnEnable () { Shape shape = (Shape) serializedObject.targetObject; if (!shape.GetComponent() && !shape.GetComponent()) { if (shape.GetComponentInParent() == null) { Undo.AddComponent(shape.gameObject); } else { Undo.AddComponent(shape.gameObject); } // collapse into the operation that made this happen Undo.CollapseUndoOperations(Undo.GetCurrentGroup()); shape.Configure(); } shapeTypeProp = serializedObject.FindProperty("settings._shapeType"); outlineSizeProp = serializedObject.FindProperty("settings._outlineSize"); blurProp = serializedObject.FindProperty("settings._blur"); outlineColorProp = serializedObject.FindProperty("settings._outlineColor"); roundnessPerCornerProp = serializedObject.FindProperty("settings._roundnessPerCorner"); roundnessProp = serializedObject.FindProperty("settings._roundness"); roundnessTLProp = serializedObject.FindProperty("settings._roundnessTopLeft"); roundnessTRProp = serializedObject.FindProperty("settings._roundnessTopRight"); roundnessBLProp = serializedObject.FindProperty("settings._roundnessBottomLeft"); roundnessBRProp = serializedObject.FindProperty("settings._roundnessBottomRight"); innerCutoutProp = serializedObject.FindProperty("settings._innerCutout"); startAngleProp = serializedObject.FindProperty("settings._startAngle"); endAngleProp = serializedObject.FindProperty("settings._endAngle"); invertArcProp = serializedObject.FindProperty("settings._invertArc"); fillTypeProp = serializedObject.FindProperty("settings._fillType"); fillColorProp = serializedObject.FindProperty("settings._fillColor"); fillColor2Prop = serializedObject.FindProperty("settings._fillColor2"); fillRotationProp = serializedObject.FindProperty("settings._fillRotation"); fillOffsetProp = serializedObject.FindProperty("settings._fillOffset"); fillScaleProp = serializedObject.FindProperty("settings._fillScale"); gradientTypeProp = serializedObject.FindProperty("settings._gradientType"); gradientStartProp = serializedObject.FindProperty("settings._gradientStart"); gradientAxisProp = serializedObject.FindProperty("settings._gradientAxis"); fillTextureProp = serializedObject.FindProperty("settings._fillTexture"); gridSizeProp = serializedObject.FindProperty("settings._gridSize"); lineSizeProp = serializedObject.FindProperty("settings._lineSize"); triangleOffsetProp = serializedObject.FindProperty("settings._triangleOffset"); polygonPresetProp = serializedObject.FindProperty("settings._polygonPreset"); usePolygonMapProp = serializedObject.FindProperty("settings._usePolygonMap"); pathThicknessProp = serializedObject.FindProperty("settings._pathThickness"); fillPathLoopsProp = serializedObject.FindProperty("settings._fillPathLoops"); } // blend two colors in the same way the shape shader would (premultiplied alpha, // but for this process we have turned off the premultiply step so it's just // normal alpha blending). note that for layered semi-transparent regions this // will not result in the same color you see in unity. that's because the color // you see includes blending with the background, i.e. the end result when drawn // with a shader is: blend(blend(bg_color, shape1_color), shape2_color) whereas // when drawn from the converted sprite it's: // blend(bg_color, blend(shape1_color, shape2_color)). private static Color BlendColors(Color dst, Color src) { Color c = src * src.a + dst * (1 - src.a); c.a = src.a + dst.a; return c; } private static void BlendTextures(Texture2D dstTex, Texture2D srcTex) { for (int x = 0; x < dstTex.width; x++) { for (int y = 0; y < dstTex.height; y++) { Color src = srcTex.GetPixel(x, y); if (src.a == 0) { // source pixel is fully transparent, so nothing to do continue; } if (src.a == 1) { // src pixel is fully opaque, so use the child's dstTex.SetPixel(x, y, src); continue; } Color dst = dstTex.GetPixel(x, y); Color result; if (dst.a == 0) { // parent pixel is fully transparent, so use the child's result = src; } else { // both pixels have alpha, so blend them in the same way // the shader would result = BlendColors(dst, src); } dstTex.SetPixel(x, y, result); } } } private static List DisableUIGraphics(Canvas canvas) { List graphicStates = new List(); List graphics = new List(); graphics.AddRange(canvas.GetComponentsInChildren()); graphics.RemoveAll(g => !g.enabled); foreach (Graphic g in graphics) { GraphicState gs = new GraphicState(); graphicStates.Add(gs); gs.graphic = g; Mask mask = g.GetComponent(); gs.hasMask = mask != null && mask.enabled; if (gs.hasMask) { gs.showMaskGraphic = mask.showMaskGraphic; mask.showMaskGraphic = false; } else { g.enabled = false; } } return graphicStates; } // fixme - this needs more error handling so we don't leave objects in a weird place if something goes wrong private static Vector2 RenderToTexture2D(string path, Shape shape, float pixelsPerUnit = 100) { // reset the shape's rotation Quaternion oldRotation = shape.transform.rotation; shape.transform.rotation = Quaternion.identity; // get the desired pixel size of our shape and all its Children, which will be the size of our texture Vector2 size = shape.GetShapePixelSize(pixelsPerUnit: pixelsPerUnit); int w = (int) size.x; int h = (int) size.y; // get all the shapes in draw order List shapes = shape.GetShapesInDrawOrder(); // if the shape is a UI component, we need to set up the canvas in a way // that the camera can point to the image only without the UI // components moving around based on the camera Canvas canvas = shape.GetComponentInParent(); int oldCanvasLayer = -1; RenderMode oldRenderMode = 0; Vector3 oldCanvasScale = Vector3.one; List modifiedGraphics = null; if (canvas) { oldCanvasLayer = canvas.gameObject.layer; canvas.gameObject.layer = 31; oldRenderMode = canvas.renderMode; // fixme - what happens with nested canvases? canvas.renderMode = RenderMode.WorldSpace; // if the canvas was in RenderMode.ScreenSpaceCamera then the scale will be weird now that we switched // to WorldSpace. in that case we set the scale to one. if (oldRenderMode == RenderMode.ScreenSpaceCamera) { oldCanvasScale = canvas.transform.localScale; canvas.transform.localScale = Vector3.one; } // without a way to selectively show just the shape we want, this is // the only way I can think of to do it. even this won't work if the // user has a UI component not found by this function. modifiedGraphics = DisableUIGraphics(canvas); } // make a new render texture RenderTexture rt = new RenderTexture(w, h, 32, RenderTextureFormat.ARGB32); rt.filterMode = FilterMode.Point; #if UNITY_5_6_OR_NEWER rt.autoGenerateMips = false; #else rt.generateMips = false; #endif rt.Create(); // figure out the world space bounds of the shape so we can point the camera at it Bounds bounds = shape.GetShapeBounds(); // set up the camera to point exactly at the object's bounds and set its // culling layer to show only layer 31 // note that if the user has anything on layer 31 then it will also // show up in the png, but in that event they can just move it away Camera cam = new GameObject().AddComponent(); cam.backgroundColor = new Color(1, 1, 1, 0); cam.clearFlags = CameraClearFlags.SolidColor; cam.transform.position = bounds.center; cam.transform.position -= new Vector3(0, 0, 10); cam.orthographic = true; cam.orthographicSize = bounds.extents.y; cam.aspect = bounds.size.x / bounds.size.y; cam.targetTexture = rt; cam.cullingMask = 1 << 31; // make the render texture active so calls to Texture2D.ReadPixels() will // read from it RenderTexture oldRT = RenderTexture.active; RenderTexture.active = rt; // draw each shape with blending turned off, and layer them on top of each // other with blending between shapes but not between a shape and the // camera's background color, which is what would happen if we just let the // camera render all of them - the camera's background color affects the // color of semi-transparent pixels even if the background color has an // alpha value of 0. this is because the shader's blending is trying to // alias against the background color, but in the png we don't want that. // there's no shader option I'm aware of that would be able to blend between // shapes but not between a shape and the background color, so that's why // we have to do it manually. Texture2D dstTex = null; foreach (Shape s in shapes) { if (canvas) { Graphic g = s.GetComponent(); g.enabled = true; if (s.GetComponent() != null) { foreach (GraphicState gs in modifiedGraphics) { if (gs.graphic == g && gs.showMaskGraphic) s.GetComponent().showMaskGraphic = true; } } } if (dstTex == null) { dstTex = s.DrawToTexture2D(cam, w, h); } else { Texture2D srcTex = s.DrawToTexture2D(cam, w, h); BlendTextures(dstTex, srcTex); DestroyImmediate(srcTex); } if (canvas) { if (s.GetComponent() == null) { s.GetComponent().enabled = false; } else { s.GetComponent().showMaskGraphic = false; } } } // put the old render texture back RenderTexture.active = oldRT; // grab the sprite's pivot point based on the top object's location // relative to its Children Vector2 pivot = shape.GetPivot(); // restore the shape's rotation shape.transform.rotation = oldRotation; // restore the canvas if (canvas) { canvas.gameObject.layer = oldCanvasLayer; if (oldRenderMode == RenderMode.ScreenSpaceCamera) canvas.transform.localScale = oldCanvasScale; canvas.renderMode = oldRenderMode; foreach (GraphicState gs in modifiedGraphics) { gs.graphic.enabled = true; if (gs.hasMask) gs.graphic.GetComponent().showMaskGraphic = gs.showMaskGraphic; } } // save the png byte[] bytes = dstTex.EncodeToPNG(); System.IO.File.WriteAllBytes(path, bytes); // clean up DestroyImmediate(dstTex); DestroyImmediate(cam.gameObject); DestroyImmediate(rt); return pivot; } private static Material GetDefaultSpriteMaterial() { // this doesn't work on 5.3.3 but does on 5.4 // AssetDatabase.GetBuiltinExtraResource("Sprites-Default.mat") // tried various things like creating a new SpriteRenderer and using its // material but Unity doesn't seem to like that and will do weird things // like destroy the material when you hit play. so we have a Sprite // Template prefab and we'll grab the material from that. SpriteRenderer sr = (SpriteRenderer) Resources.Load( "Shapes2D/Sprite Template", typeof(SpriteRenderer)); if (!sr) { Debug.LogError("Shapes2D: Couldn't get the sprite template from " + "Shapes2D/Resources. You'll have to manually assign the " + "SpriteRenderer's material."); return null; } return sr.sharedMaterial; } private void EditShape() { isEditing = true; preEditTool = Tools.current; Tools.current = Tool.None; SceneView.RepaintAll(); } private void StopEditingShape(bool restoreTool) { isEditing = false; if (restoreTool) Tools.current = preEditTool; UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); } // verts has the first point duplicated at the end as well int GetClosestLineToPoint(Vector3 pos, List verts) { Vector2 p = HandleUtility.WorldToGUIPoint(pos); int closest = -1; float distance = -1; for (int i = 0; i < verts.Count - 1; i++) { Vector2 v1 = HandleUtility.WorldToGUIPoint(verts[i]); Vector2 v2 = HandleUtility.WorldToGUIPoint(verts[i + 1]); float testDistance = HandleUtility.DistancePointToLineSegment(p, v1, v2); if (closest == -1 || testDistance < distance) { closest = i; distance = testDistance; } } return closest; } // show an outline when in edit mode void DrawShapeBorders(Shape shape) { Vector3[] corners = new Vector3[5]; shape.GetWorldCorners(corners); Handles.color = Color.white; Handles.DrawAAPolyLine(2f, corners); } Vector3 GetMouseWorldPos() { Vector2 mouseScreenPos = Event.current.mousePosition; return HandleUtility.GUIPointToWorldRay(mouseScreenPos).origin; } void EditPolygon(Shape shape) { HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive)); // get the existing verts Vector3[] oldVerts = shape.GetPolygonWorldVertices(); List verts = new List(oldVerts); bool hasMaxVerts = verts.Count == Shape.MaxPolygonVertices; // add the first vert at the end as well so Unity will draw it right etc verts.Add(verts[0]); // are we in delete mode? what color should handles be? Color pink = new Color(1, 0, 0.75f); bool deleteMode = false; if ((Event.current.control || Event.current.command) && verts.Count > 4) { Handles.color = Color.red; deleteMode = true; } else { Handles.color = pink; } // draw the shape Handles.DrawAAPolyLine(3f, verts.ToArray()); // drag handle result for getting info from our handles CustomHandles.DragHandleResult dhResult; // draw handles for each existing vert and check if they've been moved or clicked bool changed = false; for (int i = verts.Count - 2; i >= 0; i--) { Vector3 v = verts[i]; #if UNITY_5_6_OR_NEWER Vector3 newPos = CustomHandles.DragHandle(v, 0.05f * HandleUtility.GetHandleSize(v), Handles.DotHandleCap, pink, out dhResult); #else Vector3 newPos = CustomHandles.DragHandle(v, 0.05f * HandleUtility.GetHandleSize(v), Handles.DotCap, pink, out dhResult); #endif if (deleteMode && dhResult == CustomHandles.DragHandleResult.LMBPress) { // the user clicked on the handle while in delete mode, so delete the vert verts.RemoveAt(i); changed = true; } else if (!deleteMode && newPos != v) { // the handle has been dragged, so move the vert to the new position verts[i] = new Vector2(newPos.x, newPos.y); changed = true; } } // check if the mouse is hovering over a space where we could add a new vert, // and draw it if so bool snapped = false; Vector3 closestPos = HandleUtility.ClosestPointToPolyLine(verts.ToArray()); float distance = HandleUtility.DistanceToPolyLine(verts.ToArray()); bool isCloseToLine = distance < 25; if (!changed && isCloseToLine && !hasMaxVerts && !deleteMode) { // todo - ClosestPointToPolyLine doesn't work very well in 3D... foreach (Vector3 v in verts) { // if close to an existing vert, we don't want to add a new one if (Vector2.Distance(HandleUtility.WorldToGUIPoint(closestPos), HandleUtility.WorldToGUIPoint(v)) < 15) { snapped = true; break; } } if (!snapped) { // not too close to an existing vert, so draw a new one. don't // use an actual handle cause we want to intercept nearby clicks // and not just clicks directly on the handle. Rect rect = new Rect(); float dim = 0.05f * HandleUtility.GetHandleSize(closestPos); rect.center = closestPos - new Vector3(dim, dim, 0); rect.size = new Vector2(dim * 2, dim * 2); Handles.color = Color.white; // remove the weird tint it does Handles.DrawSolidRectangleWithOutline(rect, Color.green, Color.clear); if (Event.current.type == EventType.MouseDown) { // the user has clicked the new vert, so add it for real // figure out which line segment it's on int lineStart = GetClosestLineToPoint(closestPos, verts); verts.Insert(lineStart + 1, closestPos); changed = true; } } } // something has been changed, so apply the new verts back to the shape if (changed) { // make sure to remove the duplicated last vert we added Undo.RecordObject(shape, "Edit Shapes2D Polygon Vertices"); shape.SetPolygonWorldVertices( verts.GetRange(0, verts.Count - 1).ToArray()); EditorUtility.SetDirty(target); } else { HandleUtility.Repaint(); // to draw the new vert placeholder handle if (Event.current.type == EventType.MouseDown && !isCloseToLine) StopEditingShape(true); } } void DoMovePathPoint(List segments, int i, Vector2 newPos) { PathSegment seg = segments[i / 3]; Vector2 offset = seg.p1 - seg.midpoint; if (i % 3 == 0) seg.p0 = newPos; if (i % 3 == 2) seg.p2 = newPos; seg.p1 = seg.midpoint + (Vector3) offset; segments[i / 3] = seg; } void MovePathPointsAtPosition(List segments, Vector2 oldPos, Vector2 newPos) { for (int i = 0; i < segments.Count; i++) { PathSegment seg = segments[i]; if ((Vector2) seg.p0 == oldPos) DoMovePathPoint(segments, i * 3, newPos); if ((Vector2) seg.p2 == oldPos) DoMovePathPoint(segments, i * 3 + 2, newPos); } } void MovePathPoint(List segments, int i, Vector3 newPos, bool moveConnected = true) { PathSegment segment = segments[i / 3]; if (i % 3 == 1) { segment.p1 = (Vector2) newPos; segments[i / 3] = segment; return; } if (moveConnected) MovePathPointsAtPosition(segments, i % 3 == 0 ? segment.p0 : segment.p2, newPos); else DoMovePathPoint(segments, i, newPos); } void EditPath(Shape shape) { // get the existing verts PathSegment[] oldSegments = shape.GetPathWorldSegments(); List segments = new List(oldSegments); bool hasMaxSegments = segments.Count == Shape.MaxPathSegments; // are we in delete mode? what color should handles be? Color pink = new Color(1, 0, 0.75f); bool deleteMode = false; if ((Event.current.control || Event.current.command) && segments.Count > 1) { Handles.color = Color.red; deleteMode = true; } else { Handles.color = pink; } bool splitMode = Event.current.shift; Vector3 mouseWorldPos = GetMouseWorldPos(); bool isWithinBounds = shape.PointIsWithinShapeBounds(mouseWorldPos); // drag handle result for getting info from our handles CustomHandles.DragHandleResult dhResult; // draw handles for each existing point and check if they've been moved or clicked bool changed = false; for (int i = segments.Count * 3 - 1; i >= 0; i--) { PathSegment segment = segments[i / 3]; Vector3 p = segment.p0; bool isInfluencePoint = false; if (i % 3 == 1) { p = segment.p1; isInfluencePoint = true; } else if (i % 3 == 2) { p = segment.p2; } if (deleteMode && isInfluencePoint) continue; float size = 0.04f * HandleUtility.GetHandleSize(p); #if UNITY_5_6_OR_NEWER Handles.CapFunction cap = Handles.RectangleHandleCap; #else Handles.DrawCapFunction cap = Handles.RectangleCap; #endif if (isInfluencePoint) { #if UNITY_5_6_OR_NEWER cap = Handles.CircleHandleCap; #else cap = Handles.CircleCap; #endif size = 0.05f * HandleUtility.GetHandleSize(p); } if (isInfluencePoint) { Color oldColor = Handles.color; Handles.color = Color.grey; Handles.DrawDottedLine(p, segment.p0, HandleUtility.GetHandleSize(p)); Handles.DrawDottedLine(p, segment.p2, HandleUtility.GetHandleSize(p)); Handles.color = oldColor; } Vector3 newPos = CustomHandles.DragHandle(p, size, cap, pink, out dhResult); if (deleteMode && !isInfluencePoint && dhResult == CustomHandles.DragHandleResult.LMBPress) { // the user clicked on the handle while in delete mode, so delete the segment segments.RemoveAt(i / 3); changed = true; break; } else if (!deleteMode && isInfluencePoint && dhResult == CustomHandles.DragHandleResult.LMBDoubleClick) { segment.MakeLinear(); segments[i / 3] = segment; changed = true; } else if (!deleteMode && newPos != p) { // the handle has been dragged, so move the point to the new position if (isInfluencePoint || isWithinBounds) { MovePathPoint(segments, i, newPos, moveConnected: !splitMode); changed = true; } } else if (!splitMode && !deleteMode && !isInfluencePoint && dhResult == CustomHandles.DragHandleResult.LMBRelease) { // the handle has been released. snap it to any nearby points. for (int c = 0; c < segments.Count; c++) { PathSegment seg2 = segments[c]; if (seg2.p0 != newPos && Vector2.Distance(seg2.p0, newPos) < HandleUtility.GetHandleSize(newPos) * 0.25f) { newPos = seg2.p0; break; } if (seg2.p2 != newPos && Vector2.Distance(seg2.p2, newPos) < HandleUtility.GetHandleSize(newPos) * 0.25f) { newPos = seg2.p2; break; } } MovePathPoint(segments, i, newPos, moveConnected: true); changed = true; } } // check if the mouse is hovering over a space where we could add a new point, // and draw it if so bool closeToExistingPoint = false; if (!changed && !hasMaxSegments && !deleteMode) { foreach (PathSegment s in segments) { // if close to an existing point, we don't want to add a new one if (Vector2.Distance(HandleUtility.WorldToGUIPoint(mouseWorldPos), HandleUtility.WorldToGUIPoint(s.p0)) < 15) { closeToExistingPoint = true; break; } if (Vector2.Distance(HandleUtility.WorldToGUIPoint(mouseWorldPos), HandleUtility.WorldToGUIPoint(s.p1)) < 15) { closeToExistingPoint = true; break; } if (Vector2.Distance(HandleUtility.WorldToGUIPoint(mouseWorldPos), HandleUtility.WorldToGUIPoint(s.p2)) < 15) { closeToExistingPoint = true; break; } } if (!closeToExistingPoint && isWithinBounds) { // not too close to an existing vert, so draw a new one // find the closest point float closestDistance = 99999; Vector2 closestPoint = Vector2.zero; for (int i = 0; i < segments.Count; i++) { float dist = Vector2.Distance(segments[i].p0, (Vector2) mouseWorldPos); if (dist < closestDistance) { closestPoint = segments[i].p0; closestDistance = dist; } dist = Vector2.Distance(segments[i].p2, (Vector2) mouseWorldPos); if (dist < closestDistance) { closestPoint = segments[i].p2; closestDistance = dist; } } // don't use an actual handle cause we want to intercept nearby clicks // and not just clicks directly on the handle. Rect rect = new Rect(); float dim = 0.05f * HandleUtility.GetHandleSize(mouseWorldPos); rect.center = mouseWorldPos - new Vector3(dim, dim, 0); rect.size = new Vector2(dim * 2, dim * 2); Handles.color = Color.white; // remove the weird tint it does Handles.DrawSolidRectangleWithOutline(rect, Color.green, Color.clear); Color oldColor = Handles.color; Handles.color = Color.grey; Handles.DrawDottedLine(rect.center, closestPoint, HandleUtility.GetHandleSize(closestPoint)); Handles.color = oldColor; if (Event.current.type == EventType.MouseDown && !Event.current.alt && !Event.current.shift && !Event.current.command && !Event.current.control) { // the user has clicked to add a new segment, so add it for real segments.Add(new PathSegment(closestPoint, mouseWorldPos)); changed = true; } } } // something has been changed, so apply the new points back to the shape if (changed) { Undo.RecordObject(shape, "Edit Shapes2D Path Points"); shape.SetPathWorldSegments(segments.GetRange(0, segments.Count).ToArray()); EditorUtility.SetDirty(target); } else { HandleUtility.Repaint(); // to draw the new point placeholder handle if (Event.current.type == EventType.MouseDown && !isWithinBounds) StopEditingShape(true); } } void OnSceneGUI() { Shape shape = (Shape) target; if (!isEditing || (shape.settings.shapeType != ShapeType.Polygon && shape.settings.shapeType != ShapeType.Path)) return; if (Tools.current != Tool.None) { StopEditingShape(false); return; } // draw some borders so the user knows where the shape should live DrawShapeBorders(shape); if (shape.settings.shapeType == ShapeType.Polygon) EditPolygon(shape); else EditPath(shape); // this prevents the user selecting another object when they are // adding poly/path nodes if (Event.current.type == EventType.Layout) HandleUtility.AddDefaultControl(GUIUtility.GetControlID(GetHashCode(), FocusType.Passive)); } private Shapes2DPrefs GetPreferences() { string path = AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(this)); int index = path.IndexOf("Shapes2D"); if (index == -1) return null; string prefsPath = path.Substring(0, index + 8) + "/Preferences.asset"; return AssetDatabase.LoadAssetAtPath(prefsPath); } private void SetPolygonCollider2D(Shape shape) { PolygonCollider2D pc2d = shape.GetComponent(); if (shape.settings.shapeType == ShapeType.Polygon) { if (!pc2d) pc2d = shape.gameObject.AddComponent(); Vector3[] points = shape.GetPolygonWorldVertices(); Vector2[] colliderPoints = new Vector2[points.Length]; for (int i = 0; i < points.Length; i++) colliderPoints[i] = shape.transform.InverseTransformPoint(points[i]); Undo.RecordObject(pc2d, "Set PolygonCollider2D Points"); pc2d.points = colliderPoints; } } private void FromPolygonCollider2D(Shape shape) { PolygonCollider2D pc2d = shape.GetComponent(); if (shape.settings.shapeType == ShapeType.Polygon) { if (pc2d.points.Length >= 64) { EditorUtility.DisplayDialog("Too many points", "The PolygonCollider2D has too many points (max 64).", "Okay"); return; } Vector3[] points = new Vector3[pc2d.points.Length]; for (int i = 0; i < pc2d.points.Length; i++) points[i] = shape.transform.TransformPoint(pc2d.points[i]); Undo.RecordObject(shape, "Edit Shapes2D Polygon Vertices"); shape.SetPolygonWorldVertices(points); EditorUtility.SetDirty(target); } else if (shape.settings.shapeType == ShapeType.Path) { if (pc2d.points.Length >= 32) { EditorUtility.DisplayDialog("Too many points", "The PolygonCollider2D has too many points (max 32).", "Okay"); return; } PathSegment[] segments = new PathSegment[pc2d.points.Length]; for (int i = 0; i < pc2d.points.Length; i++) { Vector3 p0 = shape.transform.TransformPoint(pc2d.points[i]); Vector3 p2 = i == pc2d.points.Length - 1 ? shape.transform.TransformPoint(pc2d.points[0]) : shape.transform.TransformPoint(pc2d.points[i + 1]); segments[i] = new PathSegment(p0, p2); } Undo.RecordObject(shape, "Edit Shapes2D Path Segments"); shape.SetPathWorldSegments(segments); EditorUtility.SetDirty(target); } } private void ConvertToSprite(Shape shape) { string dname = "Assets/Resources/Shapes2D Sprites"; string fname = dname + "/" + shape.name + ".png"; string rname = "Shapes2D Sprites/" + shape.name; if (!System.IO.Directory.Exists(dname)) System.IO.Directory.CreateDirectory(dname); if (System.IO.File.Exists(fname) && !EditorUtility.DisplayDialog("Overwrite File?", "A file with the name " + fname + " already exists. " + "Are you sure you want to overwrite it?", "Yes", "Cancel")) return; float pixelsPerUnit = 100; Shapes2DPrefs prefs = GetPreferences(); if (prefs) { pixelsPerUnit = prefs.pixelsPerUnit; } else { Debug.LogWarning("Can't find Shapes2D Preferences in Shapes2D/Preferences. Please re-import Shapes2D."); } Vector2 pivot = RenderToTexture2D(fname, shape, pixelsPerUnit: pixelsPerUnit); // refresh the asset AssetDatabase.ImportAsset(fname); // set the sprite's pivot point so any rotations/position stay the same TextureImporter textureImporter = AssetImporter.GetAtPath(fname) as TextureImporter; TextureImporterSettings texSettings = new TextureImporterSettings(); textureImporter.ReadTextureSettings(texSettings); #if UNITY_5_5_OR_NEWER texSettings.ApplyTextureType(TextureImporterType.Sprite); #else texSettings.ApplyTextureType(TextureImporterType.Sprite, true); #endif texSettings.spritePixelsPerUnit = pixelsPerUnit; if (Vector2.Distance(pivot, new Vector2(0.5f, 0.5f)) < 0.01f) { texSettings.spriteAlignment = (int) SpriteAlignment.Center; textureImporter.SetTextureSettings(texSettings); } else { texSettings.spriteAlignment = (int) SpriteAlignment.Custom; textureImporter.SetTextureSettings(texSettings); textureImporter.spritePivot = pivot; } AssetDatabase.ImportAsset(fname, ImportAssetOptions.ForceUpdate); Sprite sprite = Resources.Load(rname); Undo.RecordObjects(shape.GetUndoObjects().ToArray(), "Convert to Sprite"); shape.SetAsSprite(sprite, GetDefaultSpriteMaterial()); // exit the gui routine because otherwise we get annoying errors because // we deleted a material and unity still wants to draw it EditorGUIUtility.ExitGUI(); } public override void OnInspectorGUI() { serializedObject.Update(); // dunno what this does but it's in the examples? Shape shape = (Shape) serializedObject.targetObject; EditorGUI.BeginDisabledGroup(!shape.enabled); // shape type EditorGUILayout.PropertyField(shapeTypeProp); ShapeType shapeType = (ShapeType) shapeTypeProp.enumValueIndex; if (shapeType == ShapeType.Rectangle) { // rectangle props EditorGUILayout.PropertyField(roundnessPerCornerProp); if (shape.settings.roundnessPerCorner) { EditorGUILayout.PropertyField(roundnessTLProp); EditorGUILayout.PropertyField(roundnessTRProp); EditorGUILayout.PropertyField(roundnessBLProp); EditorGUILayout.PropertyField(roundnessBRProp); } else { EditorGUILayout.PropertyField(roundnessProp); roundnessTLProp.floatValue = roundnessProp.floatValue; roundnessTRProp.floatValue = roundnessProp.floatValue; roundnessBLProp.floatValue = roundnessProp.floatValue; roundnessBRProp.floatValue = roundnessProp.floatValue; } } else if (shapeType == ShapeType.Ellipse) { //ellipse props EditorGUILayout.PropertyField(startAngleProp); EditorGUILayout.PropertyField(endAngleProp); EditorGUILayout.PropertyField(invertArcProp); EditorGUILayout.PropertyField(innerCutoutProp); } else if (shapeType == ShapeType.Polygon) { // polygon props EditorGUILayout.PropertyField(polygonPresetProp); EditorGUI.BeginDisabledGroup(Selection.objects.Length != 1); if (GUILayout.Toggle(isEditing, "Edit Shape", "Button")) { if (!isEditing) EditShape(); GUIStyle helpStyle = new GUIStyle(GUI.skin.label); helpStyle.wordWrap = true; helpStyle.normal.textColor = Color.green; EditorGUILayout.LabelField("Click on a segment to add a new node (up to 64).\nCtrl-click nodes to delete.\nSee docs about performance!", helpStyle); } else { if (isEditing) StopEditingShape(true); } if (shape.GetComponent() && GUILayout.Button("From Polygon Collider 2D")) FromPolygonCollider2D(shape); if (GUILayout.Button("Set Polygon Collider 2D")) SetPolygonCollider2D(shape); EditorGUI.EndDisabledGroup(); EditorGUILayout.PropertyField(usePolygonMapProp, new GUIContent("Optimize rendering (see docs!)")); } else if (shapeType == ShapeType.Triangle) { // triangle props EditorGUILayout.PropertyField(triangleOffsetProp); } else if (shapeType == ShapeType.Path) { EditorGUILayout.PropertyField(pathThicknessProp); EditorGUILayout.PropertyField(fillPathLoopsProp); EditorGUI.BeginDisabledGroup(Selection.objects.Length != 1); if (GUILayout.Toggle(isEditing, "Edit Path", "Button")) { if (!isEditing) EditShape(); GUIStyle helpStyle = new GUIStyle(GUI.skin.label); helpStyle.wordWrap = true; helpStyle.normal.textColor = Color.green; EditorGUILayout.LabelField("Click to add a new segment (up to 32).\nShift-drag to separate connected nodes or prevent snapping.\nCtrl-click nodes to delete.\nDouble-click a circular node to make it linear.\nSee docs about performance!", helpStyle); } else { if (isEditing) StopEditingShape(true); } if (shape.GetComponent() && GUILayout.Button("From Polygon Collider 2D")) FromPolygonCollider2D(shape); EditorGUI.EndDisabledGroup(); } // common props EditorGUILayout.PropertyField(blurProp); EditorGUILayout.PropertyField(outlineSizeProp); EditorGUILayout.PropertyField(outlineColorProp); // fill props EditorGUILayout.PropertyField(fillTypeProp); FillType fillType = (FillType) fillTypeProp.enumValueIndex; if (fillType == FillType.Gradient) { EditorGUILayout.PropertyField(gradientTypeProp); if ((GradientType) gradientTypeProp.enumValueIndex < GradientType.Radial) EditorGUILayout.PropertyField(gradientAxisProp); EditorGUILayout.PropertyField(gradientStartProp); } if (fillType >= FillType.SolidColor && fillType < FillType.Texture) { EditorGUILayout.PropertyField(fillColorProp); } if (fillType == FillType.Texture) { EditorGUILayout.PropertyField(fillTextureProp); EditorGUILayout.PropertyField(fillScaleProp); } if (fillType >= FillType.Gradient && fillType < FillType.Texture) { EditorGUILayout.PropertyField(fillColor2Prop); } if (fillType >= FillType.Gradient) { EditorGUILayout.PropertyField(fillOffsetProp); EditorGUILayout.PropertyField(fillRotationProp); } if (fillType == FillType.Grid || fillType == FillType.Stripes) { EditorGUILayout.PropertyField(lineSizeProp); } if (fillType == FillType.Grid || fillType == FillType.Stripes || fillType == FillType.CheckerBoard) { EditorGUILayout.PropertyField(gridSizeProp); } EditorGUI.BeginDisabledGroup(Selection.objects.Length != 1); if (GUILayout.Button("Convert to Sprite")) ConvertToSprite(shape); EditorGUI.EndDisabledGroup(); EditorGUI.EndDisabledGroup(); serializedObject.ApplyModifiedProperties(); // if the material has been destroyed, configure everything again. // or if the shape has been re-enabled after being converted to a sprite, // attempt to restore the scale it had previously if (shape.enabled && (!shape.IsConfigured() || shape.wasConverted)) { Undo.RecordObjects(shape.GetUndoObjects().ToArray(), "Re-enable Shapes2D Component"); shape.Configure(); if (shape.wasConverted) shape.RestoreFromConversion(); // combine with the re-enable action Undo.CollapseUndoOperations(Undo.GetCurrentGroup()); } } } }