ShapeEditor.cs 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907
  1. namespace Shapes2D {
  2. using UnityEngine;
  3. using UnityEditor;
  4. using UnityEngine.UI;
  5. using System.Collections.Generic;
  6. [CustomEditor(typeof(Shape))]
  7. [CanEditMultipleObjects]
  8. public class ShapeEditor : Editor {
  9. // used during sprite conversion to keep track of objects that we have
  10. // to temporarily modify
  11. class GraphicState {
  12. public Graphic graphic;
  13. public bool hasMask;
  14. public bool showMaskGraphic;
  15. }
  16. SerializedProperty shapeTypeProp, outlineSizeProp, blurProp,
  17. outlineColorProp, fillTypeProp, fillColorProp,
  18. fillColor2Prop, gradientTypeProp, roundnessProp, roundnessTLProp,
  19. roundnessTRProp, roundnessBLProp, roundnessBRProp, roundnessPerCornerProp,
  20. fillRotationProp, fillOffsetProp, gradientStartProp, fillTextureProp,
  21. gridSizeProp, lineSizeProp, triangleOffsetProp, fillScaleProp,
  22. gradientAxisProp, polygonPresetProp, usePolygonMapProp,
  23. startAngleProp, endAngleProp, invertArcProp, innerCutoutProp,
  24. pathThicknessProp, fillPathLoopsProp;
  25. bool isEditing; // true if we're in polygon/path edit mode in the scene view
  26. Tool preEditTool = Tool.None; // the tool the user had selected before clicking edit
  27. void OnEnable () {
  28. Shape shape = (Shape) serializedObject.targetObject;
  29. if (!shape.GetComponent<SpriteRenderer>()
  30. && !shape.GetComponent<Image>()) {
  31. if (shape.GetComponentInParent<Canvas>() == null) {
  32. Undo.AddComponent<SpriteRenderer>(shape.gameObject);
  33. } else {
  34. Undo.AddComponent<Image>(shape.gameObject);
  35. }
  36. // collapse into the operation that made this happen
  37. Undo.CollapseUndoOperations(Undo.GetCurrentGroup());
  38. shape.Configure();
  39. }
  40. shapeTypeProp = serializedObject.FindProperty("settings._shapeType");
  41. outlineSizeProp = serializedObject.FindProperty("settings._outlineSize");
  42. blurProp = serializedObject.FindProperty("settings._blur");
  43. outlineColorProp = serializedObject.FindProperty("settings._outlineColor");
  44. roundnessPerCornerProp = serializedObject.FindProperty("settings._roundnessPerCorner");
  45. roundnessProp = serializedObject.FindProperty("settings._roundness");
  46. roundnessTLProp = serializedObject.FindProperty("settings._roundnessTopLeft");
  47. roundnessTRProp = serializedObject.FindProperty("settings._roundnessTopRight");
  48. roundnessBLProp = serializedObject.FindProperty("settings._roundnessBottomLeft");
  49. roundnessBRProp = serializedObject.FindProperty("settings._roundnessBottomRight");
  50. innerCutoutProp = serializedObject.FindProperty("settings._innerCutout");
  51. startAngleProp = serializedObject.FindProperty("settings._startAngle");
  52. endAngleProp = serializedObject.FindProperty("settings._endAngle");
  53. invertArcProp = serializedObject.FindProperty("settings._invertArc");
  54. fillTypeProp = serializedObject.FindProperty("settings._fillType");
  55. fillColorProp = serializedObject.FindProperty("settings._fillColor");
  56. fillColor2Prop = serializedObject.FindProperty("settings._fillColor2");
  57. fillRotationProp = serializedObject.FindProperty("settings._fillRotation");
  58. fillOffsetProp = serializedObject.FindProperty("settings._fillOffset");
  59. fillScaleProp = serializedObject.FindProperty("settings._fillScale");
  60. gradientTypeProp = serializedObject.FindProperty("settings._gradientType");
  61. gradientStartProp = serializedObject.FindProperty("settings._gradientStart");
  62. gradientAxisProp = serializedObject.FindProperty("settings._gradientAxis");
  63. fillTextureProp = serializedObject.FindProperty("settings._fillTexture");
  64. gridSizeProp = serializedObject.FindProperty("settings._gridSize");
  65. lineSizeProp = serializedObject.FindProperty("settings._lineSize");
  66. triangleOffsetProp = serializedObject.FindProperty("settings._triangleOffset");
  67. polygonPresetProp = serializedObject.FindProperty("settings._polygonPreset");
  68. usePolygonMapProp = serializedObject.FindProperty("settings._usePolygonMap");
  69. pathThicknessProp = serializedObject.FindProperty("settings._pathThickness");
  70. fillPathLoopsProp = serializedObject.FindProperty("settings._fillPathLoops");
  71. }
  72. // blend two colors in the same way the shape shader would (premultiplied alpha,
  73. // but for this process we have turned off the premultiply step so it's just
  74. // normal alpha blending). note that for layered semi-transparent regions this
  75. // will not result in the same color you see in unity. that's because the color
  76. // you see includes blending with the background, i.e. the end result when drawn
  77. // with a shader is: blend(blend(bg_color, shape1_color), shape2_color) whereas
  78. // when drawn from the converted sprite it's:
  79. // blend(bg_color, blend(shape1_color, shape2_color)).
  80. private static Color BlendColors(Color dst, Color src) {
  81. Color c = src * src.a + dst * (1 - src.a);
  82. c.a = src.a + dst.a;
  83. return c;
  84. }
  85. private static void BlendTextures(Texture2D dstTex, Texture2D srcTex) {
  86. for (int x = 0; x < dstTex.width; x++) {
  87. for (int y = 0; y < dstTex.height; y++) {
  88. Color src = srcTex.GetPixel(x, y);
  89. if (src.a == 0) {
  90. // source pixel is fully transparent, so nothing to do
  91. continue;
  92. }
  93. if (src.a == 1) {
  94. // src pixel is fully opaque, so use the child's
  95. dstTex.SetPixel(x, y, src);
  96. continue;
  97. }
  98. Color dst = dstTex.GetPixel(x, y);
  99. Color result;
  100. if (dst.a == 0) {
  101. // parent pixel is fully transparent, so use the child's
  102. result = src;
  103. } else {
  104. // both pixels have alpha, so blend them in the same way
  105. // the shader would
  106. result = BlendColors(dst, src);
  107. }
  108. dstTex.SetPixel(x, y, result);
  109. }
  110. }
  111. }
  112. private static List<GraphicState> DisableUIGraphics(Canvas canvas) {
  113. List<GraphicState> graphicStates = new List<GraphicState>();
  114. List<Graphic> graphics = new List<Graphic>();
  115. graphics.AddRange(canvas.GetComponentsInChildren<Graphic>());
  116. graphics.RemoveAll(g => !g.enabled);
  117. foreach (Graphic g in graphics) {
  118. GraphicState gs = new GraphicState();
  119. graphicStates.Add(gs);
  120. gs.graphic = g;
  121. Mask mask = g.GetComponent<Mask>();
  122. gs.hasMask = mask != null && mask.enabled;
  123. if (gs.hasMask) {
  124. gs.showMaskGraphic = mask.showMaskGraphic;
  125. mask.showMaskGraphic = false;
  126. } else {
  127. g.enabled = false;
  128. }
  129. }
  130. return graphicStates;
  131. }
  132. // fixme - this needs more error handling so we don't leave objects in a weird place if something goes wrong
  133. private static Vector2 RenderToTexture2D(string path, Shape shape, float pixelsPerUnit = 100) {
  134. // reset the shape's rotation
  135. Quaternion oldRotation = shape.transform.rotation;
  136. shape.transform.rotation = Quaternion.identity;
  137. // get the desired pixel size of our shape and all its children, which will be the size of our texture
  138. Vector2 size = shape.GetShapePixelSize(pixelsPerUnit: pixelsPerUnit);
  139. int w = (int) size.x;
  140. int h = (int) size.y;
  141. // get all the shapes in draw order
  142. List<Shape> shapes = shape.GetShapesInDrawOrder();
  143. // if the shape is a UI component, we need to set up the canvas in a way
  144. // that the camera can point to the image only without the UI
  145. // components moving around based on the camera
  146. Canvas canvas = shape.GetComponentInParent<Canvas>();
  147. int oldCanvasLayer = -1;
  148. RenderMode oldRenderMode = 0;
  149. Vector3 oldCanvasScale = Vector3.one;
  150. List<GraphicState> modifiedGraphics = null;
  151. if (canvas) {
  152. oldCanvasLayer = canvas.gameObject.layer;
  153. canvas.gameObject.layer = 31;
  154. oldRenderMode = canvas.renderMode;
  155. // fixme - what happens with nested canvases?
  156. canvas.renderMode = RenderMode.WorldSpace;
  157. // if the canvas was in RenderMode.ScreenSpaceCamera then the scale will be weird now that we switched
  158. // to WorldSpace. in that case we set the scale to one.
  159. if (oldRenderMode == RenderMode.ScreenSpaceCamera) {
  160. oldCanvasScale = canvas.transform.localScale;
  161. canvas.transform.localScale = Vector3.one;
  162. }
  163. // without a way to selectively show just the shape we want, this is
  164. // the only way I can think of to do it. even this won't work if the
  165. // user has a UI component not found by this function.
  166. modifiedGraphics = DisableUIGraphics(canvas);
  167. }
  168. // make a new render texture
  169. RenderTexture rt = new RenderTexture(w, h, 32, RenderTextureFormat.ARGB32);
  170. rt.filterMode = FilterMode.Point;
  171. #if UNITY_5_6_OR_NEWER
  172. rt.autoGenerateMips = false;
  173. #else
  174. rt.generateMips = false;
  175. #endif
  176. rt.Create();
  177. // figure out the world space bounds of the shape so we can point the camera at it
  178. Bounds bounds = shape.GetShapeBounds();
  179. // set up the camera to point exactly at the object's bounds and set its
  180. // culling layer to show only layer 31
  181. // note that if the user has anything on layer 31 then it will also
  182. // show up in the png, but in that event they can just move it away
  183. Camera cam = new GameObject().AddComponent<Camera>();
  184. cam.backgroundColor = new Color(1, 1, 1, 0);
  185. cam.clearFlags = CameraClearFlags.SolidColor;
  186. cam.transform.position = bounds.center;
  187. cam.transform.position -= new Vector3(0, 0, 10);
  188. cam.orthographic = true;
  189. cam.orthographicSize = bounds.extents.y;
  190. cam.aspect = bounds.size.x / bounds.size.y;
  191. cam.targetTexture = rt;
  192. cam.cullingMask = 1 << 31;
  193. // make the render texture active so calls to Texture2D.ReadPixels() will
  194. // read from it
  195. RenderTexture oldRT = RenderTexture.active;
  196. RenderTexture.active = rt;
  197. // draw each shape with blending turned off, and layer them on top of each
  198. // other with blending between shapes but not between a shape and the
  199. // camera's background color, which is what would happen if we just let the
  200. // camera render all of them - the camera's background color affects the
  201. // color of semi-transparent pixels even if the background color has an
  202. // alpha value of 0. this is because the shader's blending is trying to
  203. // alias against the background color, but in the png we don't want that.
  204. // there's no shader option I'm aware of that would be able to blend between
  205. // shapes but not between a shape and the background color, so that's why
  206. // we have to do it manually.
  207. Texture2D dstTex = null;
  208. foreach (Shape s in shapes) {
  209. if (canvas) {
  210. Graphic g = s.GetComponent<Graphic>();
  211. g.enabled = true;
  212. if (s.GetComponent<Mask>() != null) {
  213. foreach (GraphicState gs in modifiedGraphics) {
  214. if (gs.graphic == g && gs.showMaskGraphic)
  215. s.GetComponent<Mask>().showMaskGraphic = true;
  216. }
  217. }
  218. }
  219. if (dstTex == null) {
  220. dstTex = s.DrawToTexture2D(cam, w, h);
  221. } else {
  222. Texture2D srcTex = s.DrawToTexture2D(cam, w, h);
  223. BlendTextures(dstTex, srcTex);
  224. DestroyImmediate(srcTex);
  225. }
  226. if (canvas) {
  227. if (s.GetComponent<Mask>() == null) {
  228. s.GetComponent<Graphic>().enabled = false;
  229. } else {
  230. s.GetComponent<Mask>().showMaskGraphic = false;
  231. }
  232. }
  233. }
  234. // put the old render texture back
  235. RenderTexture.active = oldRT;
  236. // grab the sprite's pivot point based on the top object's location
  237. // relative to its children
  238. Vector2 pivot = shape.GetPivot();
  239. // restore the shape's rotation
  240. shape.transform.rotation = oldRotation;
  241. // restore the canvas
  242. if (canvas) {
  243. canvas.gameObject.layer = oldCanvasLayer;
  244. if (oldRenderMode == RenderMode.ScreenSpaceCamera)
  245. canvas.transform.localScale = oldCanvasScale;
  246. canvas.renderMode = oldRenderMode;
  247. foreach (GraphicState gs in modifiedGraphics) {
  248. gs.graphic.enabled = true;
  249. if (gs.hasMask)
  250. gs.graphic.GetComponent<Mask>().showMaskGraphic = gs.showMaskGraphic;
  251. }
  252. }
  253. // save the png
  254. byte[] bytes = dstTex.EncodeToPNG();
  255. System.IO.File.WriteAllBytes(path, bytes);
  256. // clean up
  257. DestroyImmediate(dstTex);
  258. DestroyImmediate(cam.gameObject);
  259. DestroyImmediate(rt);
  260. return pivot;
  261. }
  262. private static Material GetDefaultSpriteMaterial() {
  263. // this doesn't work on 5.3.3 but does on 5.4
  264. // AssetDatabase.GetBuiltinExtraResource<Material>("Sprites-Default.mat")
  265. // tried various things like creating a new SpriteRenderer and using its
  266. // material but Unity doesn't seem to like that and will do weird things
  267. // like destroy the material when you hit play. so we have a Sprite
  268. // Template prefab and we'll grab the material from that.
  269. SpriteRenderer sr = (SpriteRenderer) Resources.Load(
  270. "Shapes2D/Sprite Template", typeof(SpriteRenderer));
  271. if (!sr) {
  272. Debug.LogError("Shapes2D: Couldn't get the sprite template from "
  273. + "Shapes2D/Resources. You'll have to manually assign the "
  274. + "SpriteRenderer's material.");
  275. return null;
  276. }
  277. return sr.sharedMaterial;
  278. }
  279. private void EditShape() {
  280. isEditing = true;
  281. preEditTool = Tools.current;
  282. Tools.current = Tool.None;
  283. SceneView.RepaintAll();
  284. }
  285. private void StopEditingShape(bool restoreTool) {
  286. isEditing = false;
  287. if (restoreTool)
  288. Tools.current = preEditTool;
  289. UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
  290. }
  291. // verts has the first point duplicated at the end as well
  292. int GetClosestLineToPoint(Vector3 pos, List<Vector3> verts) {
  293. Vector2 p = HandleUtility.WorldToGUIPoint(pos);
  294. int closest = -1;
  295. float distance = -1;
  296. for (int i = 0; i < verts.Count - 1; i++) {
  297. Vector2 v1 = HandleUtility.WorldToGUIPoint(verts[i]);
  298. Vector2 v2 = HandleUtility.WorldToGUIPoint(verts[i + 1]);
  299. float testDistance = HandleUtility.DistancePointToLineSegment(p, v1, v2);
  300. if (closest == -1 || testDistance < distance) {
  301. closest = i;
  302. distance = testDistance;
  303. }
  304. }
  305. return closest;
  306. }
  307. // show an outline when in edit mode
  308. void DrawShapeBorders(Shape shape) {
  309. Vector3[] corners = new Vector3[5];
  310. shape.GetWorldCorners(corners);
  311. Handles.color = Color.white;
  312. Handles.DrawAAPolyLine(2f, corners);
  313. }
  314. Vector3 GetMouseWorldPos() {
  315. Vector2 mouseScreenPos = Event.current.mousePosition;
  316. return HandleUtility.GUIPointToWorldRay(mouseScreenPos).origin;
  317. }
  318. void EditPolygon(Shape shape) {
  319. HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive));
  320. // get the existing verts
  321. Vector3[] oldVerts = shape.GetPolygonWorldVertices();
  322. List<Vector3> verts = new List<Vector3>(oldVerts);
  323. bool hasMaxVerts = verts.Count == Shape.MaxPolygonVertices;
  324. // add the first vert at the end as well so Unity will draw it right etc
  325. verts.Add(verts[0]);
  326. // are we in delete mode? what color should handles be?
  327. Color pink = new Color(1, 0, 0.75f);
  328. bool deleteMode = false;
  329. if ((Event.current.control || Event.current.command) && verts.Count > 4) {
  330. Handles.color = Color.red;
  331. deleteMode = true;
  332. } else {
  333. Handles.color = pink;
  334. }
  335. // draw the shape
  336. Handles.DrawAAPolyLine(3f, verts.ToArray());
  337. // drag handle result for getting info from our handles
  338. CustomHandles.DragHandleResult dhResult;
  339. // draw handles for each existing vert and check if they've been moved or clicked
  340. bool changed = false;
  341. for (int i = verts.Count - 2; i >= 0; i--) {
  342. Vector3 v = verts[i];
  343. #if UNITY_5_6_OR_NEWER
  344. Vector3 newPos = CustomHandles.DragHandle(v, 0.05f * HandleUtility.GetHandleSize(v),
  345. Handles.DotHandleCap, pink, out dhResult);
  346. #else
  347. Vector3 newPos = CustomHandles.DragHandle(v, 0.05f * HandleUtility.GetHandleSize(v),
  348. Handles.DotCap, pink, out dhResult);
  349. #endif
  350. if (deleteMode && dhResult == CustomHandles.DragHandleResult.LMBPress) {
  351. // the user clicked on the handle while in delete mode, so delete the vert
  352. verts.RemoveAt(i);
  353. changed = true;
  354. } else if (!deleteMode && newPos != v) {
  355. // the handle has been dragged, so move the vert to the new position
  356. verts[i] = new Vector2(newPos.x, newPos.y);
  357. changed = true;
  358. }
  359. }
  360. // check if the mouse is hovering over a space where we could add a new vert,
  361. // and draw it if so
  362. bool snapped = false;
  363. Vector3 closestPos = HandleUtility.ClosestPointToPolyLine(verts.ToArray());
  364. float distance = HandleUtility.DistanceToPolyLine(verts.ToArray());
  365. bool isCloseToLine = distance < 25;
  366. if (!changed && isCloseToLine && !hasMaxVerts && !deleteMode) {
  367. // todo - ClosestPointToPolyLine doesn't work very well in 3D...
  368. foreach (Vector3 v in verts) {
  369. // if close to an existing vert, we don't want to add a new one
  370. if (Vector2.Distance(HandleUtility.WorldToGUIPoint(closestPos),
  371. HandleUtility.WorldToGUIPoint(v)) < 15) {
  372. snapped = true;
  373. break;
  374. }
  375. }
  376. if (!snapped) {
  377. // not too close to an existing vert, so draw a new one. don't
  378. // use an actual handle cause we want to intercept nearby clicks
  379. // and not just clicks directly on the handle.
  380. Rect rect = new Rect();
  381. float dim = 0.05f * HandleUtility.GetHandleSize(closestPos);
  382. rect.center = closestPos - new Vector3(dim, dim, 0);
  383. rect.size = new Vector2(dim * 2, dim * 2);
  384. Handles.color = Color.white; // remove the weird tint it does
  385. Handles.DrawSolidRectangleWithOutline(rect, Color.green, Color.clear);
  386. if (Event.current.type == EventType.MouseDown) {
  387. // the user has clicked the new vert, so add it for real
  388. // figure out which line segment it's on
  389. int lineStart = GetClosestLineToPoint(closestPos, verts);
  390. verts.Insert(lineStart + 1, closestPos);
  391. changed = true;
  392. }
  393. }
  394. }
  395. // something has been changed, so apply the new verts back to the shape
  396. if (changed) {
  397. // make sure to remove the duplicated last vert we added
  398. Undo.RecordObject(shape, "Edit Shapes2D Polygon Vertices");
  399. shape.SetPolygonWorldVertices(
  400. verts.GetRange(0, verts.Count - 1).ToArray());
  401. EditorUtility.SetDirty(target);
  402. } else {
  403. HandleUtility.Repaint(); // to draw the new vert placeholder handle
  404. if (Event.current.type == EventType.MouseDown && !isCloseToLine)
  405. StopEditingShape(true);
  406. }
  407. }
  408. void DoMovePathPoint(List<PathSegment> segments, int i, Vector2 newPos) {
  409. PathSegment seg = segments[i / 3];
  410. Vector2 offset = seg.p1 - seg.midpoint;
  411. if (i % 3 == 0)
  412. seg.p0 = newPos;
  413. if (i % 3 == 2)
  414. seg.p2 = newPos;
  415. seg.p1 = seg.midpoint + (Vector3) offset;
  416. segments[i / 3] = seg;
  417. }
  418. void MovePathPointsAtPosition(List<PathSegment> segments, Vector2 oldPos, Vector2 newPos) {
  419. for (int i = 0; i < segments.Count; i++) {
  420. PathSegment seg = segments[i];
  421. if ((Vector2) seg.p0 == oldPos)
  422. DoMovePathPoint(segments, i * 3, newPos);
  423. if ((Vector2) seg.p2 == oldPos)
  424. DoMovePathPoint(segments, i * 3 + 2, newPos);
  425. }
  426. }
  427. void MovePathPoint(List<PathSegment> segments, int i, Vector3 newPos, bool moveConnected = true) {
  428. PathSegment segment = segments[i / 3];
  429. if (i % 3 == 1) {
  430. segment.p1 = (Vector2) newPos;
  431. segments[i / 3] = segment;
  432. return;
  433. }
  434. if (moveConnected)
  435. MovePathPointsAtPosition(segments, i % 3 == 0 ? segment.p0 : segment.p2, newPos);
  436. else
  437. DoMovePathPoint(segments, i, newPos);
  438. }
  439. void EditPath(Shape shape) {
  440. // get the existing verts
  441. PathSegment[] oldSegments = shape.GetPathWorldSegments();
  442. List<PathSegment> segments = new List<PathSegment>(oldSegments);
  443. bool hasMaxSegments = segments.Count == Shape.MaxPathSegments;
  444. // are we in delete mode? what color should handles be?
  445. Color pink = new Color(1, 0, 0.75f);
  446. bool deleteMode = false;
  447. if ((Event.current.control || Event.current.command) && segments.Count > 1) {
  448. Handles.color = Color.red;
  449. deleteMode = true;
  450. } else {
  451. Handles.color = pink;
  452. }
  453. bool splitMode = Event.current.shift;
  454. Vector3 mouseWorldPos = GetMouseWorldPos();
  455. bool isWithinBounds = shape.PointIsWithinShapeBounds(mouseWorldPos);
  456. // drag handle result for getting info from our handles
  457. CustomHandles.DragHandleResult dhResult;
  458. // draw handles for each existing point and check if they've been moved or clicked
  459. bool changed = false;
  460. for (int i = segments.Count * 3 - 1; i >= 0; i--) {
  461. PathSegment segment = segments[i / 3];
  462. Vector3 p = segment.p0;
  463. bool isInfluencePoint = false;
  464. if (i % 3 == 1) {
  465. p = segment.p1;
  466. isInfluencePoint = true;
  467. } else if (i % 3 == 2) {
  468. p = segment.p2;
  469. }
  470. if (deleteMode && isInfluencePoint)
  471. continue;
  472. float size = 0.04f * HandleUtility.GetHandleSize(p);
  473. #if UNITY_5_6_OR_NEWER
  474. Handles.CapFunction cap = Handles.RectangleHandleCap;
  475. #else
  476. Handles.DrawCapFunction cap = Handles.RectangleCap;
  477. #endif
  478. if (isInfluencePoint) {
  479. #if UNITY_5_6_OR_NEWER
  480. cap = Handles.CircleHandleCap;
  481. #else
  482. cap = Handles.CircleCap;
  483. #endif
  484. size = 0.05f * HandleUtility.GetHandleSize(p);
  485. }
  486. if (isInfluencePoint) {
  487. Color oldColor = Handles.color;
  488. Handles.color = Color.grey;
  489. Handles.DrawDottedLine(p, segment.p0, HandleUtility.GetHandleSize(p));
  490. Handles.DrawDottedLine(p, segment.p2, HandleUtility.GetHandleSize(p));
  491. Handles.color = oldColor;
  492. }
  493. Vector3 newPos = CustomHandles.DragHandle(p, size, cap, pink, out dhResult);
  494. if (deleteMode && !isInfluencePoint && dhResult == CustomHandles.DragHandleResult.LMBPress) {
  495. // the user clicked on the handle while in delete mode, so delete the segment
  496. segments.RemoveAt(i / 3);
  497. changed = true;
  498. break;
  499. } else if (!deleteMode && isInfluencePoint && dhResult == CustomHandles.DragHandleResult.LMBDoubleClick) {
  500. segment.MakeLinear();
  501. segments[i / 3] = segment;
  502. changed = true;
  503. } else if (!deleteMode && newPos != p) {
  504. // the handle has been dragged, so move the point to the new position
  505. if (isInfluencePoint || isWithinBounds) {
  506. MovePathPoint(segments, i, newPos, moveConnected: !splitMode);
  507. changed = true;
  508. }
  509. } else if (!splitMode && !deleteMode && !isInfluencePoint
  510. && dhResult == CustomHandles.DragHandleResult.LMBRelease) {
  511. // the handle has been released. snap it to any nearby points.
  512. for (int c = 0; c < segments.Count; c++) {
  513. PathSegment seg2 = segments[c];
  514. if (seg2.p0 != newPos && Vector2.Distance(seg2.p0, newPos) < HandleUtility.GetHandleSize(newPos) * 0.25f) {
  515. newPos = seg2.p0;
  516. break;
  517. }
  518. if (seg2.p2 != newPos && Vector2.Distance(seg2.p2, newPos) < HandleUtility.GetHandleSize(newPos) * 0.25f) {
  519. newPos = seg2.p2;
  520. break;
  521. }
  522. }
  523. MovePathPoint(segments, i, newPos, moveConnected: true);
  524. changed = true;
  525. }
  526. }
  527. // check if the mouse is hovering over a space where we could add a new point,
  528. // and draw it if so
  529. bool closeToExistingPoint = false;
  530. if (!changed && !hasMaxSegments && !deleteMode) {
  531. foreach (PathSegment s in segments) {
  532. // if close to an existing point, we don't want to add a new one
  533. if (Vector2.Distance(HandleUtility.WorldToGUIPoint(mouseWorldPos),
  534. HandleUtility.WorldToGUIPoint(s.p0)) < 15) {
  535. closeToExistingPoint = true;
  536. break;
  537. }
  538. if (Vector2.Distance(HandleUtility.WorldToGUIPoint(mouseWorldPos),
  539. HandleUtility.WorldToGUIPoint(s.p1)) < 15) {
  540. closeToExistingPoint = true;
  541. break;
  542. }
  543. if (Vector2.Distance(HandleUtility.WorldToGUIPoint(mouseWorldPos),
  544. HandleUtility.WorldToGUIPoint(s.p2)) < 15) {
  545. closeToExistingPoint = true;
  546. break;
  547. }
  548. }
  549. if (!closeToExistingPoint && isWithinBounds) {
  550. // not too close to an existing vert, so draw a new one
  551. // find the closest point
  552. float closestDistance = 99999;
  553. Vector2 closestPoint = Vector2.zero;
  554. for (int i = 0; i < segments.Count; i++) {
  555. float dist = Vector2.Distance(segments[i].p0, (Vector2) mouseWorldPos);
  556. if (dist < closestDistance) {
  557. closestPoint = segments[i].p0;
  558. closestDistance = dist;
  559. }
  560. dist = Vector2.Distance(segments[i].p2, (Vector2) mouseWorldPos);
  561. if (dist < closestDistance) {
  562. closestPoint = segments[i].p2;
  563. closestDistance = dist;
  564. }
  565. }
  566. // don't use an actual handle cause we want to intercept nearby clicks
  567. // and not just clicks directly on the handle.
  568. Rect rect = new Rect();
  569. float dim = 0.05f * HandleUtility.GetHandleSize(mouseWorldPos);
  570. rect.center = mouseWorldPos - new Vector3(dim, dim, 0);
  571. rect.size = new Vector2(dim * 2, dim * 2);
  572. Handles.color = Color.white; // remove the weird tint it does
  573. Handles.DrawSolidRectangleWithOutline(rect, Color.green, Color.clear);
  574. Color oldColor = Handles.color;
  575. Handles.color = Color.grey;
  576. Handles.DrawDottedLine(rect.center, closestPoint, HandleUtility.GetHandleSize(closestPoint));
  577. Handles.color = oldColor;
  578. if (Event.current.type == EventType.MouseDown && !Event.current.alt
  579. && !Event.current.shift && !Event.current.command && !Event.current.control) {
  580. // the user has clicked to add a new segment, so add it for real
  581. segments.Add(new PathSegment(closestPoint, mouseWorldPos));
  582. changed = true;
  583. }
  584. }
  585. }
  586. // something has been changed, so apply the new points back to the shape
  587. if (changed) {
  588. Undo.RecordObject(shape, "Edit Shapes2D Path Points");
  589. shape.SetPathWorldSegments(segments.GetRange(0, segments.Count).ToArray());
  590. EditorUtility.SetDirty(target);
  591. } else {
  592. HandleUtility.Repaint(); // to draw the new point placeholder handle
  593. if (Event.current.type == EventType.MouseDown && !isWithinBounds)
  594. StopEditingShape(true);
  595. }
  596. }
  597. void OnSceneGUI() {
  598. Shape shape = (Shape) target;
  599. if (!isEditing || (shape.settings.shapeType != ShapeType.Polygon && shape.settings.shapeType != ShapeType.Path))
  600. return;
  601. if (Tools.current != Tool.None) {
  602. StopEditingShape(false);
  603. return;
  604. }
  605. // draw some borders so the user knows where the shape should live
  606. DrawShapeBorders(shape);
  607. if (shape.settings.shapeType == ShapeType.Polygon)
  608. EditPolygon(shape);
  609. else
  610. EditPath(shape);
  611. // this prevents the user selecting another object when they are
  612. // adding poly/path nodes
  613. if (Event.current.type == EventType.Layout)
  614. HandleUtility.AddDefaultControl(GUIUtility.GetControlID(GetHashCode(), FocusType.Passive));
  615. }
  616. private Shapes2DPrefs GetPreferences() {
  617. string path = AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(this));
  618. int index = path.IndexOf("Shapes2D");
  619. if (index == -1)
  620. return null;
  621. string prefsPath = path.Substring(0, index + 8) + "/Preferences.asset";
  622. return AssetDatabase.LoadAssetAtPath<Shapes2DPrefs>(prefsPath);
  623. }
  624. private void SetPolygonCollider2D(Shape shape) {
  625. PolygonCollider2D pc2d = shape.GetComponent<PolygonCollider2D>();
  626. if (shape.settings.shapeType == ShapeType.Polygon) {
  627. if (!pc2d)
  628. pc2d = shape.gameObject.AddComponent<PolygonCollider2D>();
  629. Vector3[] points = shape.GetPolygonWorldVertices();
  630. Vector2[] colliderPoints = new Vector2[points.Length];
  631. for (int i = 0; i < points.Length; i++)
  632. colliderPoints[i] = shape.transform.InverseTransformPoint(points[i]);
  633. Undo.RecordObject(pc2d, "Set PolygonCollider2D Points");
  634. pc2d.points = colliderPoints;
  635. }
  636. }
  637. private void FromPolygonCollider2D(Shape shape) {
  638. PolygonCollider2D pc2d = shape.GetComponent<PolygonCollider2D>();
  639. if (shape.settings.shapeType == ShapeType.Polygon) {
  640. if (pc2d.points.Length >= 64) {
  641. EditorUtility.DisplayDialog("Too many points",
  642. "The PolygonCollider2D has too many points (max 64).", "Okay");
  643. return;
  644. }
  645. Vector3[] points = new Vector3[pc2d.points.Length];
  646. for (int i = 0; i < pc2d.points.Length; i++)
  647. points[i] = shape.transform.TransformPoint(pc2d.points[i]);
  648. Undo.RecordObject(shape, "Edit Shapes2D Polygon Vertices");
  649. shape.SetPolygonWorldVertices(points);
  650. EditorUtility.SetDirty(target);
  651. } else if (shape.settings.shapeType == ShapeType.Path) {
  652. if (pc2d.points.Length >= 32) {
  653. EditorUtility.DisplayDialog("Too many points",
  654. "The PolygonCollider2D has too many points (max 32).", "Okay");
  655. return;
  656. }
  657. PathSegment[] segments = new PathSegment[pc2d.points.Length];
  658. for (int i = 0; i < pc2d.points.Length; i++) {
  659. Vector3 p0 = shape.transform.TransformPoint(pc2d.points[i]);
  660. Vector3 p2 = i == pc2d.points.Length - 1
  661. ? shape.transform.TransformPoint(pc2d.points[0])
  662. : shape.transform.TransformPoint(pc2d.points[i + 1]);
  663. segments[i] = new PathSegment(p0, p2);
  664. }
  665. Undo.RecordObject(shape, "Edit Shapes2D Path Segments");
  666. shape.SetPathWorldSegments(segments);
  667. EditorUtility.SetDirty(target);
  668. }
  669. }
  670. private void ConvertToSprite(Shape shape) {
  671. string dname = "Assets/Resources/Shapes2D Sprites";
  672. string fname = dname + "/" + shape.name + ".png";
  673. string rname = "Shapes2D Sprites/" + shape.name;
  674. if (!System.IO.Directory.Exists(dname))
  675. System.IO.Directory.CreateDirectory(dname);
  676. if (System.IO.File.Exists(fname)
  677. && !EditorUtility.DisplayDialog("Overwrite File?",
  678. "A file with the name " + fname + " already exists. "
  679. + "Are you sure you want to overwrite it?", "Yes", "Cancel"))
  680. return;
  681. float pixelsPerUnit = 100;
  682. Shapes2DPrefs prefs = GetPreferences();
  683. if (prefs) {
  684. pixelsPerUnit = prefs.pixelsPerUnit;
  685. } else {
  686. Debug.LogWarning("Can't find Shapes2D Preferences in Shapes2D/Preferences. Please re-import Shapes2D.");
  687. }
  688. Vector2 pivot = RenderToTexture2D(fname, shape, pixelsPerUnit: pixelsPerUnit);
  689. // refresh the asset
  690. AssetDatabase.ImportAsset(fname);
  691. // set the sprite's pivot point so any rotations/position stay the same
  692. TextureImporter textureImporter = AssetImporter.GetAtPath(fname)
  693. as TextureImporter;
  694. TextureImporterSettings texSettings = new TextureImporterSettings();
  695. textureImporter.ReadTextureSettings(texSettings);
  696. #if UNITY_5_5_OR_NEWER
  697. texSettings.ApplyTextureType(TextureImporterType.Sprite);
  698. #else
  699. texSettings.ApplyTextureType(TextureImporterType.Sprite, true);
  700. #endif
  701. texSettings.spritePixelsPerUnit = pixelsPerUnit;
  702. if (Vector2.Distance(pivot, new Vector2(0.5f, 0.5f)) < 0.01f) {
  703. texSettings.spriteAlignment = (int) SpriteAlignment.Center;
  704. textureImporter.SetTextureSettings(texSettings);
  705. } else {
  706. texSettings.spriteAlignment = (int) SpriteAlignment.Custom;
  707. textureImporter.SetTextureSettings(texSettings);
  708. textureImporter.spritePivot = pivot;
  709. }
  710. AssetDatabase.ImportAsset(fname, ImportAssetOptions.ForceUpdate);
  711. Sprite sprite = Resources.Load<Sprite>(rname);
  712. Undo.RecordObjects(shape.GetUndoObjects().ToArray(),
  713. "Convert to Sprite");
  714. shape.SetAsSprite(sprite, GetDefaultSpriteMaterial());
  715. // exit the gui routine because otherwise we get annoying errors because
  716. // we deleted a material and unity still wants to draw it
  717. EditorGUIUtility.ExitGUI();
  718. }
  719. public override void OnInspectorGUI() {
  720. serializedObject.Update(); // dunno what this does but it's in the examples?
  721. Shape shape = (Shape) serializedObject.targetObject;
  722. EditorGUI.BeginDisabledGroup(!shape.enabled);
  723. // shape type
  724. EditorGUILayout.PropertyField(shapeTypeProp);
  725. ShapeType shapeType = (ShapeType) shapeTypeProp.enumValueIndex;
  726. if (shapeType == ShapeType.Rectangle) {
  727. // rectangle props
  728. EditorGUILayout.PropertyField(roundnessPerCornerProp);
  729. if (shape.settings.roundnessPerCorner) {
  730. EditorGUILayout.PropertyField(roundnessTLProp);
  731. EditorGUILayout.PropertyField(roundnessTRProp);
  732. EditorGUILayout.PropertyField(roundnessBLProp);
  733. EditorGUILayout.PropertyField(roundnessBRProp);
  734. } else {
  735. EditorGUILayout.PropertyField(roundnessProp);
  736. roundnessTLProp.floatValue = roundnessProp.floatValue;
  737. roundnessTRProp.floatValue = roundnessProp.floatValue;
  738. roundnessBLProp.floatValue = roundnessProp.floatValue;
  739. roundnessBRProp.floatValue = roundnessProp.floatValue;
  740. }
  741. } else if (shapeType == ShapeType.Ellipse) {
  742. //ellipse props
  743. EditorGUILayout.PropertyField(startAngleProp);
  744. EditorGUILayout.PropertyField(endAngleProp);
  745. EditorGUILayout.PropertyField(invertArcProp);
  746. EditorGUILayout.PropertyField(innerCutoutProp);
  747. } else if (shapeType == ShapeType.Polygon) {
  748. // polygon props
  749. EditorGUILayout.PropertyField(polygonPresetProp);
  750. EditorGUI.BeginDisabledGroup(Selection.objects.Length != 1);
  751. if (GUILayout.Toggle(isEditing, "Edit Shape", "Button")) {
  752. if (!isEditing)
  753. EditShape();
  754. GUIStyle helpStyle = new GUIStyle(GUI.skin.label);
  755. helpStyle.wordWrap = true;
  756. helpStyle.normal.textColor = Color.green;
  757. EditorGUILayout.LabelField("Click on a segment to add a new node (up to 64).\nCtrl-click nodes to delete.\nSee docs about performance!", helpStyle);
  758. } else {
  759. if (isEditing)
  760. StopEditingShape(true);
  761. }
  762. if (shape.GetComponent<PolygonCollider2D>() && GUILayout.Button("From Polygon Collider 2D"))
  763. FromPolygonCollider2D(shape);
  764. if (GUILayout.Button("Set Polygon Collider 2D"))
  765. SetPolygonCollider2D(shape);
  766. EditorGUI.EndDisabledGroup();
  767. EditorGUILayout.PropertyField(usePolygonMapProp,
  768. new GUIContent("Optimize rendering (see docs!)"));
  769. } else if (shapeType == ShapeType.Triangle) {
  770. // triangle props
  771. EditorGUILayout.PropertyField(triangleOffsetProp);
  772. } else if (shapeType == ShapeType.Path) {
  773. EditorGUILayout.PropertyField(pathThicknessProp);
  774. EditorGUILayout.PropertyField(fillPathLoopsProp);
  775. EditorGUI.BeginDisabledGroup(Selection.objects.Length != 1);
  776. if (GUILayout.Toggle(isEditing, "Edit Path", "Button")) {
  777. if (!isEditing)
  778. EditShape();
  779. GUIStyle helpStyle = new GUIStyle(GUI.skin.label);
  780. helpStyle.wordWrap = true;
  781. helpStyle.normal.textColor = Color.green;
  782. 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);
  783. } else {
  784. if (isEditing)
  785. StopEditingShape(true);
  786. }
  787. if (shape.GetComponent<PolygonCollider2D>() && GUILayout.Button("From Polygon Collider 2D"))
  788. FromPolygonCollider2D(shape);
  789. EditorGUI.EndDisabledGroup();
  790. }
  791. // common props
  792. EditorGUILayout.PropertyField(blurProp);
  793. EditorGUILayout.PropertyField(outlineSizeProp);
  794. EditorGUILayout.PropertyField(outlineColorProp);
  795. // fill props
  796. EditorGUILayout.PropertyField(fillTypeProp);
  797. FillType fillType = (FillType) fillTypeProp.enumValueIndex;
  798. if (fillType == FillType.Gradient) {
  799. EditorGUILayout.PropertyField(gradientTypeProp);
  800. if ((GradientType) gradientTypeProp.enumValueIndex < GradientType.Radial)
  801. EditorGUILayout.PropertyField(gradientAxisProp);
  802. EditorGUILayout.PropertyField(gradientStartProp);
  803. }
  804. if (fillType >= FillType.SolidColor && fillType < FillType.Texture) {
  805. EditorGUILayout.PropertyField(fillColorProp);
  806. }
  807. if (fillType == FillType.Texture) {
  808. EditorGUILayout.PropertyField(fillTextureProp);
  809. EditorGUILayout.PropertyField(fillScaleProp);
  810. }
  811. if (fillType >= FillType.Gradient && fillType < FillType.Texture) {
  812. EditorGUILayout.PropertyField(fillColor2Prop);
  813. }
  814. if (fillType >= FillType.Gradient) {
  815. EditorGUILayout.PropertyField(fillOffsetProp);
  816. EditorGUILayout.PropertyField(fillRotationProp);
  817. }
  818. if (fillType == FillType.Grid || fillType == FillType.Stripes) {
  819. EditorGUILayout.PropertyField(lineSizeProp);
  820. }
  821. if (fillType == FillType.Grid || fillType == FillType.Stripes
  822. || fillType == FillType.CheckerBoard) {
  823. EditorGUILayout.PropertyField(gridSizeProp);
  824. }
  825. EditorGUI.BeginDisabledGroup(Selection.objects.Length != 1);
  826. if (GUILayout.Button("Convert to Sprite"))
  827. ConvertToSprite(shape);
  828. EditorGUI.EndDisabledGroup();
  829. EditorGUI.EndDisabledGroup();
  830. serializedObject.ApplyModifiedProperties();
  831. // if the material has been destroyed, configure everything again.
  832. // or if the shape has been re-enabled after being converted to a sprite,
  833. // attempt to restore the scale it had previously
  834. if (shape.enabled && (!shape.IsConfigured() || shape.wasConverted)) {
  835. Undo.RecordObjects(shape.GetUndoObjects().ToArray(),
  836. "Re-enable Shapes2D Component");
  837. shape.Configure();
  838. if (shape.wasConverted)
  839. shape.RestoreFromConversion();
  840. // combine with the re-enable action
  841. Undo.CollapseUndoOperations(Undo.GetCurrentGroup());
  842. }
  843. }
  844. }
  845. }