907 lines
46 KiB
C#
907 lines
46 KiB
C#
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<SpriteRenderer>()
|
|
&& !shape.GetComponent<Image>()) {
|
|
if (shape.GetComponentInParent<Canvas>() == null) {
|
|
Undo.AddComponent<SpriteRenderer>(shape.gameObject);
|
|
} else {
|
|
Undo.AddComponent<Image>(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<GraphicState> DisableUIGraphics(Canvas canvas) {
|
|
List<GraphicState> graphicStates = new List<GraphicState>();
|
|
List<Graphic> graphics = new List<Graphic>();
|
|
graphics.AddRange(canvas.GetComponentsInChildren<Graphic>());
|
|
graphics.RemoveAll(g => !g.enabled);
|
|
foreach (Graphic g in graphics) {
|
|
GraphicState gs = new GraphicState();
|
|
graphicStates.Add(gs);
|
|
gs.graphic = g;
|
|
Mask mask = g.GetComponent<Mask>();
|
|
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<Shape> 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<Canvas>();
|
|
int oldCanvasLayer = -1;
|
|
RenderMode oldRenderMode = 0;
|
|
Vector3 oldCanvasScale = Vector3.one;
|
|
List<GraphicState> 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<Camera>();
|
|
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<Graphic>();
|
|
g.enabled = true;
|
|
if (s.GetComponent<Mask>() != null) {
|
|
foreach (GraphicState gs in modifiedGraphics) {
|
|
if (gs.graphic == g && gs.showMaskGraphic)
|
|
s.GetComponent<Mask>().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<Mask>() == null) {
|
|
s.GetComponent<Graphic>().enabled = false;
|
|
} else {
|
|
s.GetComponent<Mask>().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<Mask>().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<Material>("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<Vector3> 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<Vector3> verts = new List<Vector3>(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<PathSegment> 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<PathSegment> 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<PathSegment> 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<PathSegment> segments = new List<PathSegment>(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<Shapes2DPrefs>(prefsPath);
|
|
}
|
|
|
|
private void SetPolygonCollider2D(Shape shape) {
|
|
PolygonCollider2D pc2d = shape.GetComponent<PolygonCollider2D>();
|
|
if (shape.settings.shapeType == ShapeType.Polygon) {
|
|
if (!pc2d)
|
|
pc2d = shape.gameObject.AddComponent<PolygonCollider2D>();
|
|
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<PolygonCollider2D>();
|
|
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<Sprite>(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<PolygonCollider2D>() && 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<PolygonCollider2D>() && 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());
|
|
}
|
|
}
|
|
}
|
|
|
|
} |