轨迹插件引入
This commit is contained in:
1 parent
89b5275efd
commit
195893a1b0
1023 files changed
+378944
-18404
No files matched your search
@@ -0,0 +1,359 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ToolBuddy.ThirdParty.VectorGraphics
|
||||
{
|
||||
/// <summary>A 2x3 transformation matrix used for 2D operations.</summary>
|
||||
public struct Matrix2D
|
||||
{
|
||||
// memory layout:
|
||||
//
|
||||
// row no (=vertical)
|
||||
// | 0 1 2
|
||||
// ---+------------
|
||||
// 0 | m00 m10 0
|
||||
// column no 1 | m01 m11 0
|
||||
// (=horiz) 2 | m02 m12 1
|
||||
|
||||
/// <summary>The matrix member at (0,0)</summary>
|
||||
public float m00;
|
||||
/// <summary>The matrix member at (1,0)</summary>
|
||||
public float m10;
|
||||
|
||||
/// <summary>The matrix member at (0,1)</summary>
|
||||
public float m01;
|
||||
/// <summary>The matrix member at (1,1)</summary>
|
||||
public float m11;
|
||||
|
||||
/// <summary>The matrix member at (0,2)</summary>
|
||||
public float m02;
|
||||
/// <summary>The matrix member at (1,2)</summary>
|
||||
public float m12;
|
||||
|
||||
/// <summary>Initializes a Matrix2D with column vectors</summary>
|
||||
/// <param name="column0">The first column</param>
|
||||
/// <param name="column1">The second column</param>
|
||||
/// <param name="column2">The third column</param>
|
||||
public Matrix2D(Vector2 column0, Vector2 column1, Vector2 column2)
|
||||
{
|
||||
this.m00 = column0.x; this.m01 = column1.x; this.m02 = column2.x;
|
||||
this.m10 = column0.y; this.m11 = column1.y; this.m12 = column2.y;
|
||||
}
|
||||
|
||||
/// <summary>Access element at [row, column].</summary>
|
||||
/// <returns>The value at [row, column]</returns>
|
||||
public float this[int row, int column]
|
||||
{
|
||||
get
|
||||
{
|
||||
return this[row + column * 2];
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
this[row + column * 2] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Access element at sequential index (0..5 inclusive).</summary>
|
||||
/// <returns>The value at [index]</returns>
|
||||
public float this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0: return m00;
|
||||
case 1: return m10;
|
||||
case 2: return m01;
|
||||
case 3: return m11;
|
||||
case 4: return m02;
|
||||
case 5: return m12;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid matrix index!");
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0: m00 = value; break;
|
||||
case 1: m10 = value; break;
|
||||
case 2: m01 = value; break;
|
||||
case 3: m11 = value; break;
|
||||
case 4: m02 = value; break;
|
||||
case 5: m12 = value; break;
|
||||
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid matrix index!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets a hashcode of the matrix.</summary>
|
||||
/// <remarks>Used to allow Matrix3x3s to be used as keys in hash tables.</remarks>
|
||||
/// <returns>The hashcode of the matrix</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return GetColumn(0).GetHashCode() ^ (GetColumn(1).GetHashCode() << 2) ^ (GetColumn(2).GetHashCode() >> 2);
|
||||
}
|
||||
|
||||
/// <summary>Checks if two matrices are equal.</summary>
|
||||
/// <param name="other">The other matrix to compare with</param>
|
||||
/// <remarks>Used to allow Matrix3x3s to be used as keys in hash tables.</remarks>
|
||||
/// <returns>True when the matrix is equal to "other"</returns>
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (!(other is Matrix2D)) return false;
|
||||
|
||||
Matrix2D rhs = (Matrix2D)other;
|
||||
return GetColumn(0).Equals(rhs.GetColumn(0))
|
||||
&& GetColumn(1).Equals(rhs.GetColumn(1))
|
||||
&& GetColumn(2).Equals(rhs.GetColumn(2));
|
||||
}
|
||||
|
||||
/// <summary>Multiplies two matrices.</summary>
|
||||
/// <param name="lhs">The left hand side matrix of the operation</param>
|
||||
/// <param name="rhs">The right hand side matrix of the operation</param>
|
||||
/// <returns>The multiplied matrix</returns>
|
||||
public static Matrix2D operator*(Matrix2D lhs, Matrix2D rhs)
|
||||
{
|
||||
Matrix2D res;
|
||||
res.m00 = lhs.m00 * rhs.m00 + lhs.m01 * rhs.m10;
|
||||
res.m01 = lhs.m00 * rhs.m01 + lhs.m01 * rhs.m11;
|
||||
res.m02 = lhs.m00 * rhs.m02 + lhs.m01 * rhs.m12 + lhs.m02;
|
||||
|
||||
res.m10 = lhs.m10 * rhs.m00 + lhs.m11 * rhs.m10;
|
||||
res.m11 = lhs.m10 * rhs.m01 + lhs.m11 * rhs.m11;
|
||||
res.m12 = lhs.m10 * rhs.m02 + lhs.m11 * rhs.m12 + lhs.m12;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>Transforms a Vector2 by a matrix.</summary>
|
||||
/// <param name="lhs">The left hand side matrix of the operation</param>
|
||||
/// <param name="vector">The vector the matrix will be multiplied with</param>
|
||||
/// <returns>The transformed vector</returns>
|
||||
public static Vector2 operator*(Matrix2D lhs, Vector2 vector)
|
||||
{
|
||||
Vector2 res;
|
||||
res.x = lhs.m00 * vector.x + lhs.m01 * vector.y + lhs.m02;
|
||||
res.y = lhs.m10 * vector.x + lhs.m11 * vector.y + lhs.m12;
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>Checks if two matrices are equal.</summary>
|
||||
/// <param name="lhs">The left hand side matrix of the comparison</param>
|
||||
/// <param name="rhs">The right hand side matrix of the comparison</param>
|
||||
/// <returns>True if "lhs" and "rhs" are equal, or false otherwise.</returns>
|
||||
public static bool operator==(Matrix2D lhs, Matrix2D rhs)
|
||||
{
|
||||
// Returns false in the presence of NaN values.
|
||||
return lhs.GetColumn(0) == rhs.GetColumn(0)
|
||||
&& lhs.GetColumn(1) == rhs.GetColumn(1)
|
||||
&& lhs.GetColumn(2) == rhs.GetColumn(2);
|
||||
}
|
||||
|
||||
/// <summary>Checks if two matrices are not equal.</summary>
|
||||
/// <param name="lhs">The left hand side matrix of the comparison</param>
|
||||
/// <param name="rhs">The right hand side matrix of the comparison</param>
|
||||
/// <returns>True if "lhs" and "rhs" not are equal, or false otherwise.</returns>
|
||||
public static bool operator!=(Matrix2D lhs, Matrix2D rhs)
|
||||
{
|
||||
// Returns true in the presence of NaN values.
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
/// <summary>Gets a column of the matrix.</summary>
|
||||
/// <param name="index">The column index, between 0 and 2 inclusively</param>
|
||||
/// <returns>The column at "index"</returns>
|
||||
public Vector2 GetColumn(int index)
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0: return new Vector2(m00, m10);
|
||||
case 1: return new Vector2(m01, m11);
|
||||
case 2: return new Vector2(m02, m12);
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid column index!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets a row of the matrix.</summary>
|
||||
/// <param name="index">The row index, between 0 and 1 inclusively</param>
|
||||
/// <returns>The row at "index"</returns>
|
||||
public Vector3 GetRow(int index)
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0: return new Vector3(m00, m01, m02);
|
||||
case 1: return new Vector3(m10, m11, m12);
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid row index!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sets a column of the matrix.</summary>
|
||||
/// <param name="index">The column index, between 0 and 2 inclusively</param>
|
||||
/// <param name="column">The column</param>
|
||||
public void SetColumn(int index, Vector2 column)
|
||||
{
|
||||
this[0, index] = column.x;
|
||||
this[1, index] = column.y;
|
||||
}
|
||||
|
||||
/// <summary>Sets a row of the matrix.</summary>
|
||||
/// <param name="index">The column index, between 0 and 1 inclusively</param>
|
||||
/// <param name="row">The row</param>
|
||||
public void SetRow(int index, Vector3 row)
|
||||
{
|
||||
this[index, 0] = row.x;
|
||||
this[index, 1] = row.y;
|
||||
this[index, 2] = row.z;
|
||||
}
|
||||
|
||||
/// <summary>Transforms a position by this matrix (effectively by 2x3).</summary>
|
||||
/// <param name="point">The point to multiply with this matrix</param>
|
||||
/// <returns>The multiplied point</returns>
|
||||
public Vector2 MultiplyPoint(Vector2 point)
|
||||
{
|
||||
Vector2 res;
|
||||
res.x = this.m00 * point.x + this.m01 * point.y + this.m02;
|
||||
res.y = this.m10 * point.x + this.m11 * point.y + this.m12;
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>Transforms a direction by this matrix.</summary>
|
||||
/// <param name="vector">The direction to multiply with this matrix</param>
|
||||
/// <returns>The multiplied direction</returns>
|
||||
public Vector2 MultiplyVector(Vector2 vector)
|
||||
{
|
||||
Vector2 res;
|
||||
res.x = this.m00 * vector.x + this.m01 * vector.y;
|
||||
res.y = this.m10 * vector.x + this.m11 * vector.y;
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>Computes the inverse of the matrix.</summary>
|
||||
/// <returns>The inverse matrix</returns>
|
||||
public Matrix2D Inverse()
|
||||
{
|
||||
Matrix2D invMat = new Matrix2D();
|
||||
|
||||
float det = this[0, 0] * this[1, 1] - this[0, 1] * this[1, 0];
|
||||
if (Mathf.Approximately(0.0f, det))
|
||||
return zero;
|
||||
|
||||
float invDet = 1.0F / det;
|
||||
|
||||
invMat[0, 0] = this[1, 1] * invDet;
|
||||
invMat[0, 1] = -this[0, 1] * invDet;
|
||||
invMat[1, 0] = -this[1, 0] * invDet;
|
||||
invMat[1, 1] = this[0, 0] * invDet;
|
||||
|
||||
// Do the translation part
|
||||
invMat[0, 2] = -(this[0, 2] * invMat[0, 0] + this[1, 2] * invMat[0, 1]);
|
||||
invMat[1, 2] = -(this[0, 2] * invMat[1, 0] + this[1, 2] * invMat[1, 1]);
|
||||
|
||||
return invMat;
|
||||
}
|
||||
|
||||
/// <summary>Creates a scaling matrix.</summary>
|
||||
/// <param name="vector">The scaling vector</param>
|
||||
/// <returns>The scaling matrix</returns>
|
||||
public static Matrix2D Scale(Vector2 vector)
|
||||
{
|
||||
Matrix2D m;
|
||||
m.m00 = vector.x; m.m01 = 0F; m.m02 = 0F;
|
||||
m.m10 = 0F; m.m11 = vector.y; m.m12 = 0F;
|
||||
return m;
|
||||
}
|
||||
|
||||
/// <summary>Creates a translation matrix.</summary>
|
||||
/// <param name="vector">The translation vector</param>
|
||||
/// <returns>The translation matrix</returns>
|
||||
public static Matrix2D Translate(Vector2 vector)
|
||||
{
|
||||
Matrix2D m;
|
||||
m.m00 = 1F; m.m01 = 0F; m.m02 = vector.x;
|
||||
m.m10 = 0F; m.m11 = 1F; m.m12 = vector.y;
|
||||
return m;
|
||||
}
|
||||
|
||||
/// <summary>Creates a right-hand side rotation matrix.</summary>
|
||||
/// <param name="angleRadians">The rotation angle, in radians</param>
|
||||
/// <returns>The rotation matrix</returns>
|
||||
public static Matrix2D RotateRH(float angleRadians)
|
||||
{
|
||||
return RotateLH(-angleRadians);
|
||||
}
|
||||
|
||||
/// <summary>Creates a left-hand side rotation matrix.</summary>
|
||||
/// <param name="angleRadians">The rotation angle, in radians</param>
|
||||
/// <returns>The rotation matrix</returns>
|
||||
public static Matrix2D RotateLH(float angleRadians)
|
||||
{
|
||||
// No SinCos? I hope the compiler optimizes this
|
||||
float s = Mathf.Sin(angleRadians);
|
||||
float c = Mathf.Cos(angleRadians);
|
||||
|
||||
Matrix2D m;
|
||||
m.m00 = c; m.m10 = -s;
|
||||
m.m01 = s; m.m11 = c;
|
||||
m.m02 = 0.0F; m.m12 = 0.0F;
|
||||
return m;
|
||||
}
|
||||
|
||||
/// <summary>Creates a skew matrix on X.</summary>
|
||||
/// <param name="angleRadians">The skew angle, in radians</param>
|
||||
/// <returns>The skew matrix</returns>
|
||||
public static Matrix2D SkewX(float angleRadians)
|
||||
{
|
||||
Matrix2D m;
|
||||
m.m00 = 1.0f; m.m01 = Mathf.Tan(angleRadians); m.m02 = 0F;
|
||||
m.m10 = 0F; m.m11 = 1.0f; m.m12 = 0F;
|
||||
return m;
|
||||
}
|
||||
|
||||
/// <summary>Creates a skew matrix on U.</summary>
|
||||
/// <param name="angleRadians">The skew angle, in radians</param>
|
||||
/// <returns>The skew matrix</returns>
|
||||
public static Matrix2D SkewY(float angleRadians)
|
||||
{
|
||||
Matrix2D m;
|
||||
m.m00 = 1.0f; m.m01 = 0F; m.m02 = 0F;
|
||||
m.m10 = Mathf.Tan(angleRadians); m.m11 = 1.0f; m.m12 = 0F;
|
||||
return m;
|
||||
}
|
||||
|
||||
static readonly Matrix2D zeroMatrix = new Matrix2D(new Vector2(0, 0), new Vector2(0, 0), new Vector2(0, 0));
|
||||
|
||||
/// <summary>Returns a matrix with all elements set to zero (read-only).</summary>
|
||||
/// <returns>The zero matrix</returns>
|
||||
public static Matrix2D zero { get { return zeroMatrix; } }
|
||||
|
||||
static readonly Matrix2D identityMatrix = new Matrix2D(new Vector2(1, 0), new Vector2(0, 1), new Vector2(0, 0));
|
||||
|
||||
/// <summary>Returns the identity matrix (read-only).</summary>
|
||||
/// <returns>The identity matrix</returns>
|
||||
public static Matrix2D identity { get { return identityMatrix; } }
|
||||
|
||||
/// <summary>Returns a string representation of the matrix.</summary>
|
||||
/// <returns>The matrix string representation</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0:F5}\t{1:F5}\t{2:F5}\n{3:F5}\t{4:F5}\t{5:F5}\n", m00, m01, m02, m10, m11, m12);
|
||||
}
|
||||
|
||||
/// <summary>Returns a string representation of the matrix using a format.</summary>
|
||||
/// <param name="format">The format to be used for the matrix components</param>
|
||||
/// <returns>The matrix string representation</returns>
|
||||
public string ToString(string format)
|
||||
{
|
||||
return string.Format("{0}\t{1}\t{2}\n{3}\t{4}\t{5}\n",
|
||||
m00.ToString(format), m01.ToString(format), m02.ToString(format),
|
||||
m10.ToString(format), m11.ToString(format), m12.ToString(format));
|
||||
}
|
||||
}
|
||||
} //namespace
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 600ae451aa325bb479e48e4f91cf17e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa2162f8539d856468fef95fc49e02e8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,269 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ToolBuddy.ThirdParty.VectorGraphics
|
||||
{
|
||||
internal class SVGPropertySheet : Dictionary<string, string> { }
|
||||
|
||||
internal class SVGStyleSheet
|
||||
{
|
||||
private List<KeyValuePair<string, SVGPropertySheet>> m_Selectors = new List<KeyValuePair<string, SVGPropertySheet>>();
|
||||
|
||||
public SVGPropertySheet this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
int i = m_Selectors.FindIndex(x => x.Key == key);
|
||||
if (i != -1)
|
||||
return m_Selectors[i].Value;
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
var v = new KeyValuePair<string, SVGPropertySheet>(key, value);
|
||||
int i = m_Selectors.FindIndex(x => x.Key == key);
|
||||
if (i != -1)
|
||||
m_Selectors[i] = v;
|
||||
m_Selectors.Add(v);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> selectors
|
||||
{
|
||||
get { return m_Selectors.Select(x => x.Key); }
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get { return m_Selectors.Count; }
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
m_Selectors.Clear();
|
||||
}
|
||||
};
|
||||
|
||||
internal static class SVGStyleSheetUtils
|
||||
{
|
||||
public static SVGStyleSheet Parse(string cssText)
|
||||
{
|
||||
var result = new SVGStyleSheet();
|
||||
var tokens = Tokenize(cssText);
|
||||
|
||||
var sheet = new SVGStyleSheet();
|
||||
while (ParseSelector(tokens, sheet))
|
||||
{
|
||||
foreach (var sel in sheet.selectors)
|
||||
{
|
||||
if (result.selectors.Contains(sel))
|
||||
CombineProperties(result[sel], sheet[sel]);
|
||||
else
|
||||
result[sel] = sheet[sel];
|
||||
}
|
||||
sheet.Clear();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static SVGPropertySheet ParseInline(string cssText)
|
||||
{
|
||||
var tokens = Tokenize(cssText);
|
||||
var props = new SVGPropertySheet();
|
||||
ParseProperties(tokens, props);
|
||||
return props;
|
||||
}
|
||||
|
||||
private static bool ParseSelector(List<string> tokens, SVGStyleSheet sheet)
|
||||
{
|
||||
if (tokens.Count == 0)
|
||||
return false;
|
||||
|
||||
var newSheet = new SVGStyleSheet();
|
||||
while (true)
|
||||
{
|
||||
var selectorName = PopToken(tokens);
|
||||
newSheet[selectorName] = new SVGPropertySheet();
|
||||
|
||||
while (PeekToken(tokens) == ",")
|
||||
PopToken(tokens);
|
||||
|
||||
if (PeekToken(tokens) == "" || PeekToken(tokens) == "{")
|
||||
break;
|
||||
}
|
||||
|
||||
var sep = PopToken(tokens);
|
||||
if (sep != "{")
|
||||
{
|
||||
Debug.LogError("Invalid CSS selector opening bracket: \"" + sep + "\"");
|
||||
return false;
|
||||
}
|
||||
|
||||
var props = new SVGPropertySheet();
|
||||
ParseProperties(tokens, props);
|
||||
|
||||
// Transfer properties to the new selectors
|
||||
foreach (var key in newSheet.selectors)
|
||||
sheet[key] = CopyProperties(props);
|
||||
|
||||
sep = PopToken(tokens);
|
||||
if (sep != "}")
|
||||
{
|
||||
Debug.LogError("Invalid CSS selector closing bracket: \"" + sep + "\"");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void CombineProperties(SVGPropertySheet first, SVGPropertySheet second)
|
||||
{
|
||||
foreach (var key in second.Keys)
|
||||
first[key] = second[key];
|
||||
}
|
||||
|
||||
private static SVGPropertySheet CopyProperties(SVGPropertySheet props)
|
||||
{
|
||||
var newProps = new SVGPropertySheet();
|
||||
foreach (var v in props)
|
||||
newProps[v.Key] = v.Value;
|
||||
return newProps;
|
||||
}
|
||||
|
||||
private static bool ParseProperties(List<string> tokens, SVGPropertySheet props)
|
||||
{
|
||||
string name;
|
||||
string value;
|
||||
while (ParseProperty(tokens, out name, out value))
|
||||
{
|
||||
props[name] = value;
|
||||
while (PeekToken(tokens) == ";")
|
||||
PopToken(tokens);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ParseProperty(List<string> tokens, out string name, out string value)
|
||||
{
|
||||
name = null;
|
||||
value = null;
|
||||
|
||||
if (PeekToken(tokens) == "" || PeekToken(tokens) == "}")
|
||||
return false;
|
||||
|
||||
name = PopToken(tokens);
|
||||
|
||||
var sep = PopToken(tokens);
|
||||
if (sep != ":")
|
||||
{
|
||||
Debug.LogError("Invalid CSS property separator: \"" + sep + "\"");
|
||||
return false;
|
||||
}
|
||||
|
||||
value = "";
|
||||
while (PeekToken(tokens) != "" && PeekToken(tokens) != ";" && PeekToken(tokens) != "}")
|
||||
{
|
||||
value = (value == "") ? PopToken(tokens) : value + " " + PopToken(tokens);
|
||||
if (PeekToken(tokens) == "(")
|
||||
value += ParseParenValue(tokens);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string ParseParenValue(List<string> tokens)
|
||||
{
|
||||
var opening = PopToken(tokens);
|
||||
if (opening != "(")
|
||||
{
|
||||
Debug.LogError("Invaid CSS value opening");
|
||||
return "";
|
||||
}
|
||||
|
||||
var value = opening;
|
||||
while (PeekToken(tokens) != "" && PeekToken(tokens) != ")")
|
||||
value += PopToken(tokens);
|
||||
|
||||
if (PeekToken(tokens) != ")")
|
||||
{
|
||||
Debug.LogError("Invaid CSS value closing");
|
||||
return "";
|
||||
}
|
||||
|
||||
value += PopToken(tokens);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>Breaks a CSS input into tokens</summary>
|
||||
/// <param name="cssText">The CSS text</param>
|
||||
/// <returns>A list of tokens</returns>
|
||||
public static List<string> Tokenize(string cssText)
|
||||
{
|
||||
var tokens = new List<string>();
|
||||
|
||||
cssText = cssText.Replace(System.Environment.NewLine, ""); // Remove newlines
|
||||
cssText = Regex.Replace(cssText, @"/\*.*?\*/", ""); // Remove CSS comments
|
||||
cssText = Regex.Replace(cssText, @"<!--.*?-->", ""); // Remove XML comments
|
||||
|
||||
int from = 0;
|
||||
int to = 0;
|
||||
|
||||
while (from < cssText.Length)
|
||||
{
|
||||
while (from < cssText.Length && IsWhitespace(cssText[from]))
|
||||
++from;
|
||||
|
||||
to = from;
|
||||
|
||||
while (to < cssText.Length && !IsSeparator(cssText[to]))
|
||||
++to;
|
||||
|
||||
if (from == to)
|
||||
{
|
||||
if (from < cssText.Length)
|
||||
tokens.Add(cssText[from].ToString());
|
||||
++to;
|
||||
}
|
||||
else
|
||||
{
|
||||
tokens.Add(cssText.Substring(from, to-from));
|
||||
}
|
||||
|
||||
from = to;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private static string PeekToken(List<string> tokens)
|
||||
{
|
||||
if (tokens.Count == 0)
|
||||
return "";
|
||||
return tokens[0];
|
||||
}
|
||||
|
||||
private static string PopToken(List<string> tokens)
|
||||
{
|
||||
if (tokens.Count == 0)
|
||||
return "";
|
||||
var tok = tokens[0];
|
||||
tokens.RemoveAt(0);
|
||||
return tok;
|
||||
}
|
||||
|
||||
private static bool IsSeparator(char ch)
|
||||
{
|
||||
return IsWhitespace(ch) || ch == ';' || ch == ':' || ch == '{' || ch == '}' || ch == '(' || ch == ')' || ch == ',';
|
||||
}
|
||||
|
||||
private static bool IsWhitespace(char ch)
|
||||
{
|
||||
return ch == ' ' || ch == '\n' || ch == '\t';
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad42887d8ca6e094093524d976e32104
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ToolBuddy.ThirdParty.VectorGraphics
|
||||
{
|
||||
public partial class VectorUtils
|
||||
{
|
||||
internal static BezierPathSegment[] BuildEllipsePath(Vector2 p0, Vector2 p1, float rotation, float rx, float ry, bool largeArc, bool sweep)
|
||||
{
|
||||
if ((p1-p0).magnitude < VectorUtils.Epsilon)
|
||||
return new BezierPathSegment[0];
|
||||
|
||||
Vector2 c;
|
||||
float theta1;
|
||||
float sweepTheta;
|
||||
float adjustedRx;
|
||||
float adjustedRy;
|
||||
ComputeEllipseParameters(p0, p1, rotation, rx, ry, largeArc, sweep, out c, out theta1, out sweepTheta, out adjustedRx, out adjustedRy);
|
||||
|
||||
BezierPathSegment[] path;
|
||||
if (Mathf.Abs(sweepTheta) <= Mathf.Epsilon)
|
||||
{
|
||||
// Use a straight line if the sweep angle is tiny
|
||||
path = VectorUtils.BezierSegmentToPath(VectorUtils.MakeLine(p0, p1));
|
||||
}
|
||||
else
|
||||
{
|
||||
path = VectorUtils.MakeArc(Vector2.zero, theta1, sweepTheta, 1.0f);
|
||||
|
||||
var scaling = new Vector2(adjustedRx, adjustedRy);
|
||||
path = VectorUtils.TransformBezierPath(path, c, rotation, scaling);
|
||||
}
|
||||
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void ComputeEllipseParameters(Vector2 p0, Vector2 p1, float phi, float rx, float ry, bool fa, bool fs, out Vector2 c, out float theta1, out float sweepTheta, out float adjustedRx, out float adjustedRy)
|
||||
{
|
||||
// See https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
||||
var cosPhi = Mathf.Cos(phi);
|
||||
var sinPhi = Mathf.Sin(phi);
|
||||
|
||||
var m0 = Matrix2D.identity;
|
||||
m0.m00 = cosPhi;
|
||||
m0.m01 = -sinPhi;
|
||||
m0.m10 = sinPhi;
|
||||
m0.m11 = cosPhi;
|
||||
|
||||
var m1 = m0;
|
||||
m1.m01 = -m1.m01;
|
||||
m1.m10 = -m1.m10;
|
||||
|
||||
var pp = m0 * new Vector2((p0.x - p1.x) / 2.0f, (p0.y - p1.y) / 2.0f);
|
||||
|
||||
rx = Mathf.Abs(rx);
|
||||
ry = Mathf.Abs(ry);
|
||||
EnsureRadiiAreLargeEnough(pp, ref rx, ref ry);
|
||||
adjustedRx = rx;
|
||||
adjustedRy = ry;
|
||||
|
||||
var ppx2 = pp.x * pp.x;
|
||||
var ppy2 = pp.y * pp.y;
|
||||
var rx2 = rx * rx;
|
||||
var ry2 = ry * ry;
|
||||
var cp = new Vector2((rx * pp.y) / ry, -(ry * pp.x) / rx);
|
||||
cp *= Mathf.Sqrt(Mathf.Abs((rx2 * ry2 - rx2 * ppy2 - ry2 * ppx2) / (rx2 * ppy2 + ry2 * ppx2)));
|
||||
if (fa == fs)
|
||||
cp = -cp;
|
||||
|
||||
c = (m1 * cp) + new Vector2((p0.x + p1.x) / 2.0f, (p0.y + p1.y) / 2.0f);
|
||||
|
||||
theta1 = Vector2.SignedAngle(new Vector2(1, 0), new Vector2((pp.x - cp.x) / rx, (pp.y - cp.y) / ry)) % 360.0f;
|
||||
sweepTheta = Vector2.SignedAngle(new Vector2((pp.x - cp.x) / rx, (pp.y - cp.y) / ry), new Vector2((-pp.x - cp.x) / rx, (-pp.y - cp.y) / ry));
|
||||
|
||||
if (!fs && sweepTheta > 0)
|
||||
sweepTheta -= 360;
|
||||
if (fs && sweepTheta < 0)
|
||||
sweepTheta += 360;
|
||||
|
||||
theta1 *= Mathf.Deg2Rad;
|
||||
sweepTheta *= Mathf.Deg2Rad;
|
||||
}
|
||||
|
||||
private static void EnsureRadiiAreLargeEnough(Vector2 p, ref float rx, ref float ry)
|
||||
{
|
||||
var d = (p.x * p.x) / (rx * rx) + (p.y * p.y) / (ry * ry);
|
||||
if (d > 1.0f)
|
||||
{
|
||||
var sqrtD = Mathf.Sqrt(d);
|
||||
rx *= sqrtD;
|
||||
ry *= sqrtD;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5c16ccd991535447911bd905325bbc0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ToolBuddy.ThirdParty.VectorGraphics
|
||||
{
|
||||
public partial class VectorUtils
|
||||
{
|
||||
/// <summary>Builds a BezierContour from a Rectangle.</summary>
|
||||
/// <param name="rect">The rectangle to build the contour from</param>
|
||||
/// <param name="radiusTL">The top-left radius of the rectangle</param>
|
||||
/// <param name="radiusTR">The top-right radius of the rectangle</param>
|
||||
/// <param name="radiusBR">The bottom-right radius of the rectangle</param>
|
||||
/// <param name="radiusBL">The bottom-left radius of the rectangle</param>
|
||||
/// <returns>A BezierContour that follows the rectangle contour</returns>
|
||||
public static BezierContour BuildRectangleContour(Rect rect, Vector2 radiusTL, Vector2 radiusTR, Vector2 radiusBR, Vector2 radiusBL)
|
||||
{
|
||||
var width = rect.size.x;
|
||||
var height = rect.size.y;
|
||||
|
||||
var halfSize = new Vector2(width / 2.0f, height / 2.0f);
|
||||
radiusTL = Vector2.Max(Vector2.Min(radiusTL, halfSize), Vector2.zero);
|
||||
radiusTR = Vector2.Max(Vector2.Min(radiusTR, halfSize), Vector2.zero);
|
||||
radiusBR = Vector2.Max(Vector2.Min(radiusBR, halfSize), Vector2.zero);
|
||||
radiusBL = Vector2.Max(Vector2.Min(radiusBL, halfSize), Vector2.zero);
|
||||
|
||||
var leftSegmentSize = height - (radiusBL.y + radiusTL.y);
|
||||
var topSegmentSize = width - (radiusTL.x + radiusTR.x);
|
||||
var rightSegmentSize = height - (radiusBR.y + radiusTR.y);
|
||||
var bottomSegmentSize = width - (radiusBL.x + radiusBR.x);
|
||||
|
||||
var segments = new List<BezierPathSegment>(8);
|
||||
BezierPathSegment seg;
|
||||
|
||||
if (leftSegmentSize > VectorUtils.Epsilon)
|
||||
{
|
||||
seg = MakePathLine(new Vector2(0.0f, radiusTL.y + leftSegmentSize), new Vector2(0.0f, radiusTL.y))[0];
|
||||
segments.Add(seg);
|
||||
}
|
||||
|
||||
if (radiusTL.magnitude > VectorUtils.Epsilon)
|
||||
{
|
||||
var circleArc = VectorUtils.MakeArc(Vector2.zero, -Mathf.PI, Mathf.PI / 2.0f, 1.0f);
|
||||
circleArc = VectorUtils.TransformBezierPath(circleArc, radiusTL, 0.0f, radiusTL);
|
||||
segments.Add(circleArc[0]);
|
||||
}
|
||||
|
||||
if (topSegmentSize > VectorUtils.Epsilon)
|
||||
{
|
||||
seg = MakePathLine(new Vector2(radiusTL.x, 0.0f), new Vector2(radiusTL.x + topSegmentSize, 0.0f))[0];
|
||||
segments.Add(seg);
|
||||
}
|
||||
|
||||
if (radiusTR.magnitude > VectorUtils.Epsilon)
|
||||
{
|
||||
var topRight = new Vector2(width - radiusTR.x, radiusTR.y);
|
||||
var circleArc = VectorUtils.MakeArc(Vector2.zero, -Mathf.PI / 2.0f, Mathf.PI / 2.0f, 1.0f);
|
||||
circleArc = VectorUtils.TransformBezierPath(circleArc, topRight, 0.0f, radiusTR);
|
||||
segments.Add(circleArc[0]);
|
||||
}
|
||||
|
||||
if (rightSegmentSize > VectorUtils.Epsilon)
|
||||
{
|
||||
seg = MakePathLine(new Vector2(width, radiusTR.y), new Vector2(width, radiusTR.y + rightSegmentSize))[0];
|
||||
segments.Add(seg);
|
||||
}
|
||||
|
||||
if (radiusBR.magnitude > VectorUtils.Epsilon)
|
||||
{
|
||||
var bottomRight = new Vector2(width - radiusBR.x, height - radiusBR.y);
|
||||
var circleArc = VectorUtils.MakeArc(Vector2.zero, 0.0f, Mathf.PI / 2.0f, 1.0f);
|
||||
circleArc = VectorUtils.TransformBezierPath(circleArc, bottomRight, 0.0f, radiusBR);
|
||||
segments.Add(circleArc[0]);
|
||||
}
|
||||
|
||||
if (bottomSegmentSize > VectorUtils.Epsilon)
|
||||
{
|
||||
seg = MakePathLine(new Vector2(width - radiusBR.x, height), new Vector2(width - (radiusBR.x + bottomSegmentSize), height))[0];
|
||||
segments.Add(seg);
|
||||
}
|
||||
|
||||
if (radiusBL.magnitude > VectorUtils.Epsilon)
|
||||
{
|
||||
var bottomLeft = new Vector2(radiusBL.x, height - radiusBL.y);
|
||||
var circleArc = VectorUtils.MakeArc(Vector2.zero, Mathf.PI / 2.0f, Mathf.PI / 2.0f, 1.0f);
|
||||
circleArc = VectorUtils.TransformBezierPath(circleArc, bottomLeft, 0.0f, radiusBL);
|
||||
segments.Add(circleArc[0]);
|
||||
}
|
||||
|
||||
// Offset segments to position
|
||||
for (int i = 0; i < segments.Count; ++i)
|
||||
{
|
||||
var s = segments[i];
|
||||
s.P0 += rect.position;
|
||||
s.P1 += rect.position;
|
||||
s.P2 += rect.position;
|
||||
segments[i] = s;
|
||||
}
|
||||
|
||||
return new BezierContour() { Segments = segments.ToArray(), Closed = true };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ab9bcde9af121f45a0d120f28f80d7c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,315 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ToolBuddy.ThirdParty.VectorGraphics
|
||||
{
|
||||
/// <summary>The gradient fill types.</summary>
|
||||
public enum GradientFillType
|
||||
{
|
||||
/// <summary>A linear gradient.</summary>
|
||||
Linear,
|
||||
|
||||
/// <summary>A radial gradient, centered at the radial focus of the gradient fill.</summary>
|
||||
Radial
|
||||
}
|
||||
|
||||
/// <summary>The path corner types, for joining path segments together.</summary>
|
||||
public enum PathCorner
|
||||
{
|
||||
/// <summary>A tipped corner with a sharp edge.</summary>
|
||||
Tipped,
|
||||
|
||||
/// <summary>A rounded corner.</summary>
|
||||
Round,
|
||||
|
||||
/// <summary>A beveled corner.</summary>
|
||||
Beveled
|
||||
}
|
||||
|
||||
/// <summary>The path ending types.</summary>
|
||||
public enum PathEnding
|
||||
{
|
||||
/// <summary>A square path ending.</summary>
|
||||
Chop,
|
||||
|
||||
/// <summary>A square path ending with a small extrusion.</summary>
|
||||
Square,
|
||||
|
||||
/// <summary>A rounded path ending.</summary>
|
||||
Round
|
||||
}
|
||||
|
||||
/// <summary>The fill mode types.</summary>
|
||||
public enum FillMode
|
||||
{
|
||||
/// <summary>Determines the "insideness" of the shape by evaluating the direction of the edges crossed.</summary>
|
||||
NonZero,
|
||||
|
||||
/// <summary>Determines the "insideness" of the shape by counting the number of edges crossed.</summary>
|
||||
OddEven
|
||||
}
|
||||
|
||||
/// <summary>The addressing mode, defining how textures or gradients behave when being addressed outside their unit range.</summary>
|
||||
public enum AddressMode
|
||||
{
|
||||
/// <summary>Textures/gradients are wrapping around with a repeating pattern.</summary>
|
||||
Wrap,
|
||||
|
||||
/// <summary>Textures/gradients are clamped on the borders.</summary>
|
||||
Clamp,
|
||||
|
||||
/// <summary>Textures/gradients are repeated with a mirroring pattern.</summary>
|
||||
Mirror
|
||||
}
|
||||
|
||||
/// <summary>The gradient stops used for gradient fills.</summary>
|
||||
public struct GradientStop
|
||||
{
|
||||
/// <summary>The color of the stop.</summary>
|
||||
public Color Color { get; set; }
|
||||
|
||||
/// <summary>At which percentage this stop applies. Should be between 0 and 1, inclusively.</summary>
|
||||
public float StopPercentage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>A bezier segment.</summary>
|
||||
/// <remarks>
|
||||
/// Cubic Bezier segment starts from P0, flies in tangent to direction from P0 to P1,
|
||||
/// then lands in direction from P2 to P3, to finally end exactly at P3.
|
||||
/// </remarks>
|
||||
public struct BezierSegment
|
||||
{
|
||||
/// <summary>Origin point of the segment.</summary>
|
||||
public Vector2 P0;
|
||||
|
||||
/// <summary>First control point of the segment.</summary>
|
||||
public Vector2 P1;
|
||||
|
||||
/// <summary>Second control point of the segment.</summary>
|
||||
public Vector2 P2;
|
||||
|
||||
/// <summary>Ending point of the segment.</summary>
|
||||
public Vector2 P3;
|
||||
}
|
||||
|
||||
/// <summary>A bezier path segment.</summary>
|
||||
/// <remarks>
|
||||
/// Like BezierSegment but implies connectivity of segments, where segments[0].P3 is actually segments[1].P0
|
||||
/// </remarks>
|
||||
public struct BezierPathSegment
|
||||
{
|
||||
/// <summary>Origin point of the segment.</summary>
|
||||
public Vector2 P0;
|
||||
|
||||
/// <summary>First control point of the segment.</summary>
|
||||
public Vector2 P1;
|
||||
|
||||
/// <summary>Second control point of the segment.</summary>
|
||||
public Vector2 P2;
|
||||
}
|
||||
|
||||
/// <summary>A chain of bezier paths, optionnally closed.</summary>
|
||||
public struct BezierContour
|
||||
{
|
||||
/// <summary>An array of every path segments on the contour.</summary>
|
||||
/// <remarks>Closed paths should not add a dedicated closing segment. It is implied by the 'closed' property.</remarks>
|
||||
public BezierPathSegment[] Segments { get; set; }
|
||||
|
||||
/// <summary>A boolean indicating if the contour should be closed.</summary>
|
||||
/// <remarks>
|
||||
/// When set to true, closed path will connect the last path segment to the first path segment, by using the
|
||||
/// last path segment's P1 and P2 as control points.
|
||||
/// </remarks>
|
||||
public bool Closed { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>The IFill interface is implemented by filling techniques (solid, texture or gradient).</summary>
|
||||
public interface IFill
|
||||
{
|
||||
/// <summary>The filling method (non-zero or even-odd) of the fill.</summary>
|
||||
FillMode Mode { get; set; }
|
||||
|
||||
/// <summary>The opacity of the fill.</summary>
|
||||
float Opacity { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Fills a shape with a single color.</summary>
|
||||
public class SolidFill : IFill
|
||||
{
|
||||
/// <summary>The color of the fill.</summary>
|
||||
public Color Color { get; set; }
|
||||
|
||||
/// <summary>The opacity of the fill.</summary>
|
||||
public float Opacity { get { return m_Opacity; } set { m_Opacity = value; } }
|
||||
private float m_Opacity = 1.0f;
|
||||
|
||||
/// <summary>The filling method (non-zero or even-odd) of the fill.</summary>
|
||||
public FillMode Mode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Fills a shape with a gradient.</summary>
|
||||
/// <remarks>
|
||||
/// Size of the fill is always assumed to cover the entire element's bounding box.
|
||||
/// Radial fills are centered in the element's bounding box. Its radii are half the bounding box dimensions in each direction.
|
||||
/// Linear fills start from the left edge to the right edge of the element's bounding box.
|
||||
/// </remarks>
|
||||
public class GradientFill : IFill
|
||||
{
|
||||
/// <summary>The fill type (linear or gradient).</summary>
|
||||
public GradientFillType Type { get; set; }
|
||||
|
||||
/// <summary>An array of stops defining the gradient colors.</summary>
|
||||
public GradientStop[] Stops { get; set; }
|
||||
|
||||
/// <summary>The filling method (non-zero or even-odd) of the fill.</summary>
|
||||
public FillMode Mode { get; set; }
|
||||
|
||||
/// <summary>The opacity of the fill.</summary>
|
||||
public float Opacity { get { return m_Opacity; } set { m_Opacity = value; } }
|
||||
private float m_Opacity = 1.0f;
|
||||
|
||||
/// <summary>The adressing mode (wrap, clamp or mirror) of this fill.</summary>
|
||||
public AddressMode Addressing { get; set; }
|
||||
|
||||
/// <summary>A position within the unit circle (-1,1) where 0 falls in the middle of the fill.</summary>
|
||||
public Vector2 RadialFocus { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Fills a shape with a texture.</summary>
|
||||
public class TextureFill : IFill
|
||||
{
|
||||
/// <summary>The texture to fill the shape with.</summary>
|
||||
public Texture2D Texture { get; set; }
|
||||
|
||||
/// <summary>The filling method (non-zero or even-odd) of the fill.</summary>
|
||||
public FillMode Mode { get; set; }
|
||||
|
||||
/// <summary>The opacity of the fill.</summary>
|
||||
public float Opacity { get { return m_Opacity; } set { m_Opacity = value; } }
|
||||
private float m_Opacity = 1.0f;
|
||||
|
||||
/// <summary>The adressing mode (wrap, clamp or mirror) of this fill.</summary>
|
||||
public AddressMode Addressing { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Fills a shape with a pattern.</summary>
|
||||
public class PatternFill : IFill
|
||||
{
|
||||
/// <summary>The filling method (non-zero or even-odd) of the fill.</summary>
|
||||
public FillMode Mode { get; set; }
|
||||
|
||||
/// <summary>The opacity of the fill.</summary>
|
||||
public float Opacity { get { return m_Opacity; } set { m_Opacity = value; } }
|
||||
private float m_Opacity = 1.0f;
|
||||
|
||||
/// <summary>The root node of the pattern</summary>
|
||||
public SceneNode Pattern { get; set; }
|
||||
|
||||
/// <summary>The rectangle that is repeated</summary>
|
||||
public Rect Rect { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Defines how strokes should be rendered.</summary>
|
||||
public class Stroke
|
||||
{
|
||||
/// <summary>The stroke color.</summary>
|
||||
public Color Color {
|
||||
get {
|
||||
var solidFill = Fill as SolidFill;
|
||||
if (solidFill == null) return new Color();
|
||||
return solidFill.Color;
|
||||
}
|
||||
set {
|
||||
Fill = new SolidFill() { Color = value };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The stroke fill.</summary>
|
||||
public IFill Fill { get; set; }
|
||||
|
||||
/// <summary>A transformation specific to the fill.</summary>
|
||||
public Matrix2D FillTransform { get { return m_FillTransform; } set { m_FillTransform = value; } }
|
||||
private Matrix2D m_FillTransform = Matrix2D.identity;
|
||||
|
||||
/// <summary>The stroke half-thickness.</summary>
|
||||
public float HalfThickness { get; set; }
|
||||
|
||||
/// <summary>The stroke pattern (dashes).</summary>
|
||||
/// <remarks>Even entries mark a fill and odd entries mark void</remarks>
|
||||
public float[] Pattern { get; set; }
|
||||
|
||||
/// <summary>An offset to where the pattern should start.</summary>
|
||||
public float PatternOffset { get; set; }
|
||||
|
||||
/// <summary>How far the tipped corners may extrude.</summary>
|
||||
public float TippedCornerLimit { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Defines properties of paths.</summary>
|
||||
public struct PathProperties
|
||||
{
|
||||
/// <summary>The stroke used to render the path.</summary>
|
||||
public Stroke Stroke { get; set; }
|
||||
|
||||
/// <summary>How the beginning of the path should be displayed.</summary>
|
||||
public PathEnding Head { get; set; }
|
||||
|
||||
/// <summary>How the end of the path should be displayed.</summary>
|
||||
public PathEnding Tail { get; set; }
|
||||
|
||||
/// <summary>How the corners of the path should be displayed.</summary>
|
||||
public PathCorner Corners { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>A generic filled shape.</summary>
|
||||
public class Shape
|
||||
{
|
||||
/// <summary>All the contours defining the shape.</summary>
|
||||
/// <remarks>
|
||||
/// Some of these coutours may be holes in the shape, depending on the fill mode used <see cref="FillMode"/>.
|
||||
/// </remarks>
|
||||
public BezierContour[] Contours { get; set; }
|
||||
|
||||
/// <summary>The fill used on the shape.</summary>
|
||||
public IFill Fill { get; set; }
|
||||
|
||||
/// <summary>A transformation specific to the fill.</summary>
|
||||
public Matrix2D FillTransform { get { return m_FillTransform; } set { m_FillTransform = value; } }
|
||||
private Matrix2D m_FillTransform = Matrix2D.identity;
|
||||
|
||||
/// <summary>The path properties.</summary>
|
||||
public PathProperties PathProps { get; set; }
|
||||
|
||||
/// <summary>Whether the specified contours are convex or not</summary>
|
||||
/// <remarks>
|
||||
/// Set this to true when you know the shape contours are convex.
|
||||
/// This will allow for a faster tessellation process in some circumstances.
|
||||
/// </remarks>
|
||||
public bool IsConvex { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>A node inside a hierarchy.</summary>
|
||||
public class SceneNode
|
||||
{
|
||||
/// <summary>The list of children nodes.</summary>
|
||||
public List<SceneNode> Children { get; set; }
|
||||
|
||||
/// <summary>The list of shapes inside this node.</summary>
|
||||
public List<Shape> Shapes { get; set; }
|
||||
|
||||
/// <summary>The transform of the node.</summary>
|
||||
public Matrix2D Transform { get { return m_Transform; } set { m_Transform = value; } }
|
||||
private Matrix2D m_Transform = Matrix2D.identity;
|
||||
|
||||
/// <summary>A clipper hierarchy that will clip this node.</summary>
|
||||
public SceneNode Clipper { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>A scene contains the whole node hierarchy.</summary>
|
||||
public class Scene
|
||||
{
|
||||
/// <summary>The root of the node hierarchy.</summary>
|
||||
public SceneNode Root { get; set; }
|
||||
}
|
||||
} // namespace
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6317336283684d540b187222a9055e78
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,317 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using Unity.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.U2D;
|
||||
#if !(UNITY_2019_3_OR_NEWER)
|
||||
using UnityEngine.Experimental.U2D;
|
||||
#endif
|
||||
using UnityEngine.Experimental.Rendering;
|
||||
|
||||
namespace ToolBuddy.ThirdParty.VectorGraphics
|
||||
{
|
||||
public static partial class VectorUtils
|
||||
{
|
||||
/// <summary>The alignement of the sprite, to determine the location of the pivot.</summary>
|
||||
public enum Alignment
|
||||
{
|
||||
/// <summary>Center alignment.</summary>
|
||||
Center = 0,
|
||||
|
||||
/// <summary>Top-left alignment.</summary>
|
||||
TopLeft = 1,
|
||||
|
||||
/// <summary>Top-center alignment.</summary>
|
||||
TopCenter = 2,
|
||||
|
||||
/// <summary>Top-right alignment.</summary>
|
||||
TopRight = 3,
|
||||
|
||||
/// <summary>Left-center alignment.</summary>
|
||||
LeftCenter = 4,
|
||||
|
||||
/// <summary>Right-center alignment.</summary>
|
||||
RightCenter = 5,
|
||||
|
||||
/// <summary>Bottom-left alignment.</summary>
|
||||
BottomLeft = 6,
|
||||
|
||||
/// <summary>Bottom-center alignment.</summary>
|
||||
BottomCenter = 7,
|
||||
|
||||
/// <summary>Bottom-right alignment.</summary>
|
||||
BottomRight = 8,
|
||||
|
||||
/// <summary>Custom alignment.</summary>
|
||||
/// <remarks>
|
||||
/// Uses a custom alignment that will be used when building the sprite using the <see cref="BuildSprite"/> method.
|
||||
/// </remarks>
|
||||
Custom = 9,
|
||||
|
||||
/// <summary>SVG origin alignment.</summary>
|
||||
/// <remarks>
|
||||
/// This will use the origin of the SVG document as the origin of the sprite.
|
||||
/// </remarks>
|
||||
SVGOrigin = 10
|
||||
}
|
||||
|
||||
private static void FlipYAxis(IList<Vector2> vertices)
|
||||
{
|
||||
var bbox = Bounds(vertices);
|
||||
var h = bbox.height;
|
||||
for (int i = 0; i < vertices.Count; ++i)
|
||||
{
|
||||
var v = vertices[i];
|
||||
v.y -= bbox.position.y;
|
||||
v.y = h - v.y;
|
||||
v.y += bbox.position.y;
|
||||
vertices[i] = v;
|
||||
}
|
||||
}
|
||||
|
||||
internal enum WindingDir
|
||||
{
|
||||
CW,
|
||||
CCW
|
||||
}
|
||||
|
||||
internal static void AdjustWinding(Vector2[] vertices, UInt16[] indices, WindingDir dir)
|
||||
{
|
||||
bool shouldFlip = false;
|
||||
var length = indices.Length;
|
||||
for (int i = 0; i < (length - 2); i += 3)
|
||||
{
|
||||
var v0 = (Vector3)vertices[indices[i]];
|
||||
var v1 = (Vector3)vertices[indices[i+1]];
|
||||
var v2 = (Vector3)vertices[indices[i+2]];
|
||||
var s = (v1 - v0).normalized;
|
||||
var t = (v2 - v0).normalized;
|
||||
float dot = Vector3.Dot(s, t);
|
||||
if (s == Vector3.zero || t == Vector3.zero || dot > 0.9999f || dot < -0.9999f)
|
||||
continue;
|
||||
var n = Vector3.Cross(s, t);
|
||||
if (n.sqrMagnitude < 0.0001f)
|
||||
continue;
|
||||
shouldFlip = dir == WindingDir.CCW ? n.z < 0.0f : n.z > 0.0f;
|
||||
break;
|
||||
}
|
||||
if (shouldFlip)
|
||||
{
|
||||
for (int i = 0; i < (length - 2); i += 3)
|
||||
{
|
||||
var tmp = indices[i];
|
||||
indices[i] = indices[i+1];
|
||||
indices[i+1] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void FlipRangeIfNecessary(List<Vector2> vertices, List<UInt16> indices, int indexStart, int indexEnd, bool flipYAxis)
|
||||
{
|
||||
// For the range, find the first valid triangle and check its winding order. If that triangle needs flipping, then flip the whole range.
|
||||
bool shouldFlip = false;
|
||||
for (int i = indexStart; i < (indexEnd - 2); i += 3)
|
||||
{
|
||||
var v0 = (Vector3)vertices[indices[i]];
|
||||
var v1 = (Vector3)vertices[indices[i + 1]];
|
||||
var v2 = (Vector3)vertices[indices[i + 2]];
|
||||
var s = (v1 - v0).normalized;
|
||||
var t = (v2 - v0).normalized;
|
||||
float dot = Vector3.Dot(s, t);
|
||||
if (s == Vector3.zero || t == Vector3.zero || dot > 0.99f || dot < -0.99f)
|
||||
continue;
|
||||
var n = Vector3.Cross(s, t);
|
||||
if (n.sqrMagnitude < 0.001f)
|
||||
continue;
|
||||
shouldFlip = flipYAxis ? n.z < 0.0f : n.z > 0.0f;
|
||||
break;
|
||||
}
|
||||
if (shouldFlip)
|
||||
{
|
||||
for (int i = indexStart; i < (indexEnd - 2); i += 3)
|
||||
{
|
||||
var tmp = indices[i + 1];
|
||||
indices[i + 1] = indices[i + 2];
|
||||
indices[i + 2] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void RenderFromArrays(Vector2[] vertices, UInt16[] indices, Vector2[] uvs, Color[] colors, Vector2[] settings, Texture2D texture, Material mat, bool clear = true)
|
||||
{
|
||||
mat.SetTexture("_MainTex", texture);
|
||||
mat.SetPass(0);
|
||||
|
||||
if (clear)
|
||||
GL.Clear(true, true, Color.clear);
|
||||
|
||||
GL.PushMatrix();
|
||||
GL.LoadOrtho();
|
||||
GL.Color(new Color(1, 1, 1, 1));
|
||||
GL.Begin(GL.TRIANGLES);
|
||||
for (int i = 0; i < indices.Length; ++i)
|
||||
{
|
||||
ushort index = indices[i];
|
||||
Vector2 vertex = vertices[index];
|
||||
Vector2 uv = uvs[index];
|
||||
GL.TexCoord2(uv.x, uv.y);
|
||||
if (settings != null)
|
||||
{
|
||||
var setting = settings[index];
|
||||
GL.MultiTexCoord2(2, setting.x, setting.y);
|
||||
}
|
||||
if (colors != null)
|
||||
GL.Color(colors[index]);
|
||||
GL.Vertex3(vertex.x, vertex.y, 0);
|
||||
}
|
||||
GL.End();
|
||||
GL.PopMatrix();
|
||||
|
||||
mat.SetTexture("_MainTex", null);
|
||||
}
|
||||
|
||||
/// <summary>Draws a vector sprite using the provided material.</summary>
|
||||
/// <param name="sprite">The sprite to render</param>
|
||||
/// <param name="mat">The material used for rendering</param>
|
||||
/// <param name="clear">If true, clear the render target before rendering</param>
|
||||
public static void RenderSprite(Sprite sprite, Material mat, bool clear = true)
|
||||
{
|
||||
float spriteWidth = sprite.rect.width;
|
||||
float spriteHeight = sprite.rect.height;
|
||||
float pixelsToUnits = sprite.rect.width / sprite.bounds.size.x;
|
||||
|
||||
var uvs = sprite.uv;
|
||||
var triangles = sprite.triangles;
|
||||
var pivot = sprite.pivot;
|
||||
|
||||
var vertices = sprite.vertices.Select(v =>
|
||||
new Vector2((v.x * pixelsToUnits + pivot.x)/spriteWidth,
|
||||
(v.y * pixelsToUnits + pivot.y)/spriteHeight)
|
||||
).ToArray();
|
||||
|
||||
Color[] colors = null;
|
||||
if (sprite.HasVertexAttribute(VertexAttribute.Color))
|
||||
colors = sprite.GetVertexAttribute<Color32>(VertexAttribute.Color).Select(c => (Color)c).ToArray();
|
||||
|
||||
Vector2[] settings = null;
|
||||
if (sprite.HasVertexAttribute(VertexAttribute.TexCoord2))
|
||||
settings = sprite.GetVertexAttribute<Vector2>(VertexAttribute.TexCoord2).ToArray();
|
||||
|
||||
RenderFromArrays(vertices, sprite.triangles, sprite.uv, colors, settings, sprite.texture, mat, clear);
|
||||
}
|
||||
|
||||
private static Material s_ExpandEdgesMat;
|
||||
|
||||
/// <summary>Renders a vector sprite to Texture2D.</summary>
|
||||
/// <param name="sprite">The sprite to render</param>
|
||||
/// <param name="width">The desired width of the resulting texture</param>
|
||||
/// <param name="height">The desired height of the resulting texture</param>
|
||||
/// <param name="mat">The material used to render the sprite</param>
|
||||
/// <param name="antiAliasing">The number of samples per pixel for anti-aliasing</param>
|
||||
/// <param name="expandEdges">When true, expand the edges to avoid a dark banding effect caused by filtering. This is slower to render and uses more graphics memory.</param>
|
||||
/// <returns>A Texture2D object containing the rendered vector sprite</returns>
|
||||
public static Texture2D RenderSpriteToTexture2D(Sprite sprite, int width, int height, Material mat, int antiAliasing = 1, bool expandEdges = false)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
return null;
|
||||
|
||||
RenderTexture tex = null;
|
||||
var oldActive = RenderTexture.active;
|
||||
|
||||
var desc = new RenderTextureDescriptor(width, height, RenderTextureFormat.ARGB32, 0) {
|
||||
msaaSamples = 1,
|
||||
sRGB = QualitySettings.activeColorSpace == ColorSpace.Linear
|
||||
};
|
||||
|
||||
if (expandEdges)
|
||||
{
|
||||
// Draw the sprite normally to be used as a background, no-antialiasing
|
||||
var normalTex = RenderTexture.GetTemporary(desc);
|
||||
RenderTexture.active = normalTex;
|
||||
RenderSprite(sprite, mat);
|
||||
|
||||
// Expand the edges and make completely transparent
|
||||
if (s_ExpandEdgesMat == null)
|
||||
{
|
||||
var shader = Shader.Find("Hidden/VectorExpandEdges");
|
||||
if (shader == null)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// Workaround for case 1167309.
|
||||
// Shader.Find() seems to fail on the package shader when doing a fresh import with a clean Library folder,
|
||||
// but AssetDatabase.LoadAssetAtPath() works fine though.
|
||||
shader = UnityEditor.AssetDatabase.LoadAssetAtPath<Shader>("Packages/com.unity.vectorgraphics/Runtime/Shaders/VectorExpandEdges.shader");
|
||||
#else
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
s_ExpandEdgesMat = new Material(shader);
|
||||
}
|
||||
|
||||
var expandTex = RenderTexture.GetTemporary(desc);
|
||||
RenderTexture.active = expandTex;
|
||||
GL.Clear(false, true, Color.clear);
|
||||
Graphics.Blit(normalTex, expandTex, s_ExpandEdgesMat, 0);
|
||||
RenderTexture.ReleaseTemporary(normalTex);
|
||||
|
||||
// Draw the sprite again, but clear with the texture rendered in the previous step,
|
||||
// this will make the bilinear filter to interpolate the colors with values different
|
||||
// than "transparent black", which causes black-ish outlines around the shape.
|
||||
desc.msaaSamples = antiAliasing;
|
||||
tex = RenderTexture.GetTemporary(desc);
|
||||
RenderTexture.active = tex;
|
||||
Graphics.Blit(expandTex, tex);
|
||||
RenderTexture.ReleaseTemporary(expandTex); // Use the expanded texture to clear the buffer
|
||||
|
||||
RenderTexture.active = tex;
|
||||
RenderSprite(sprite, mat, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
desc.msaaSamples = antiAliasing;
|
||||
tex = RenderTexture.GetTemporary(desc);
|
||||
RenderTexture.active = tex;
|
||||
RenderSprite(sprite, mat);
|
||||
}
|
||||
|
||||
Texture2D copy = new Texture2D(width, height, TextureFormat.RGBA32, false);
|
||||
copy.hideFlags = HideFlags.HideAndDontSave;
|
||||
copy.ReadPixels(new Rect(0, 0, width, height), 0, 0);
|
||||
copy.Apply();
|
||||
|
||||
RenderTexture.active = oldActive;
|
||||
RenderTexture.ReleaseTemporary(tex);
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
internal static Vector2 GetPivot(Alignment alignment, Vector2 customPivot, Rect bbox, bool flipYAxis)
|
||||
{
|
||||
switch (alignment)
|
||||
{
|
||||
case Alignment.Center: return new Vector2(0.5f, 0.5f);
|
||||
case Alignment.TopLeft: return new Vector2(0.0f, 1.0f);
|
||||
case Alignment.TopCenter: return new Vector2(0.5f, 1.0f);
|
||||
case Alignment.TopRight: return new Vector2(1.0f, 1.0f);
|
||||
case Alignment.LeftCenter: return new Vector2(0.0f, 0.5f);
|
||||
case Alignment.RightCenter: return new Vector2(1.0f, 0.5f);
|
||||
case Alignment.BottomLeft: return new Vector2(0.0f, 0.0f);
|
||||
case Alignment.BottomCenter: return new Vector2(0.5f, 0.0f);
|
||||
case Alignment.BottomRight: return new Vector2(1.0f, 0.0f);
|
||||
case Alignment.SVGOrigin:
|
||||
{
|
||||
var p = -bbox.position / bbox.size;
|
||||
if (flipYAxis)
|
||||
p.y = 1.0f - p.y;
|
||||
return p;
|
||||
}
|
||||
case Alignment.Custom: return customPivot;
|
||||
}
|
||||
return Vector2.zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c138b02f3c416c146aa000916ae2f816
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,861 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ToolBuddy.ThirdParty.VectorGraphics
|
||||
{
|
||||
public static partial class VectorUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Structure to store the tessellation options.
|
||||
/// </summary>
|
||||
public struct TessellationOptions
|
||||
{
|
||||
private float m_MaxCordDev, m_MaxCordDevSq, m_MaxTanAngleDev, m_MaxTanAngleDevCosine, m_StepSize;
|
||||
|
||||
/// <summary>
|
||||
/// The uniform tessellation step distance.
|
||||
/// </summary>
|
||||
public float StepDistance { get; set; } // A split to happen uniformly at fixed distances
|
||||
|
||||
/// <summary>
|
||||
/// The maximum distance on the cord to a straight line between to points after which more tessellation will be generated.
|
||||
/// To disable, specify float.MaxValue.
|
||||
/// </summary>
|
||||
public float MaxCordDeviation // Maximum distance allowed between a cord and its line projection
|
||||
{
|
||||
get { return m_MaxCordDev; }
|
||||
set
|
||||
{
|
||||
m_MaxCordDev = Mathf.Max(value, 0.0f);
|
||||
m_MaxCordDevSq = (m_MaxCordDev == float.MaxValue) ? float.MaxValue : m_MaxCordDev * m_MaxCordDev;
|
||||
}
|
||||
}
|
||||
internal float MaxCordDeviationSquared { get { return m_MaxCordDevSq; } }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum angle (in degrees) between the curve tangent and the next point after which more tessellation will be generated.
|
||||
/// To disable, specify float.MaxValue.
|
||||
/// </summary>
|
||||
public float MaxTanAngleDeviation // The maximum angle allowed (in radians) between tangents before a split happens
|
||||
{
|
||||
get { return m_MaxTanAngleDev; }
|
||||
set
|
||||
{
|
||||
m_MaxTanAngleDev = Mathf.Clamp(value, VectorUtils.Epsilon, Mathf.PI * 0.5f);
|
||||
m_MaxTanAngleDevCosine = Mathf.Cos(m_MaxTanAngleDev);
|
||||
}
|
||||
}
|
||||
internal float MaxTanAngleDeviationCosine { get { return m_MaxTanAngleDevCosine; } } // Cosine of the maximum angle allowed between tangents before a split happens
|
||||
|
||||
/// <summary>
|
||||
/// The number of samples used internally to evaluate the curves. More samples = higher quality.
|
||||
/// Should be between 0 and 1 (inclusive).
|
||||
/// </summary>
|
||||
public float SamplingStepSize
|
||||
{
|
||||
get { return m_StepSize; }
|
||||
set { m_StepSize = Mathf.Clamp(value, Epsilon, 1.0f); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tessellates a path.
|
||||
/// </summary>
|
||||
/// <param name="contour">The path to tessellate</param>
|
||||
/// <param name="pathProps">The path properties</param>
|
||||
/// <param name="tessellateOptions">The tessellation options</param>
|
||||
/// <param name="vertices">The resulting vertices</param>
|
||||
/// <param name="indices">The resulting triangles</param>
|
||||
/// <remarks>
|
||||
/// The individual line segments generated during tessellation are made out of a set of ordered vertices. It is important
|
||||
/// to honor this ordering so joining and and capping connect properly with the existing vertices without generating dupes.
|
||||
/// The ordering assumed is as follows:
|
||||
/// The last two vertices of a piece must be such that the first is generated at the end with a positive half-thickness
|
||||
/// while the second vertex is at the end too but at a negative half-thickness.
|
||||
/// No assumptions are enforced for other vertices before the two last vertices.
|
||||
/// </remarks>
|
||||
public static void TessellatePath(BezierContour contour, PathProperties pathProps, TessellationOptions tessellateOptions, out Vector2[] vertices, out UInt16[] indices)
|
||||
{
|
||||
if (tessellateOptions.StepDistance < Epsilon)
|
||||
throw new Exception("stepDistance too small");
|
||||
|
||||
if (contour.Segments.Length < 2)
|
||||
{
|
||||
vertices = new Vector2[0];
|
||||
indices = new UInt16[0];
|
||||
return;
|
||||
}
|
||||
|
||||
tessellateOptions.MaxCordDeviation = Mathf.Max(0.0001f, tessellateOptions.MaxCordDeviation);
|
||||
tessellateOptions.MaxTanAngleDeviation = Mathf.Max(0.0001f, tessellateOptions.MaxTanAngleDeviation);
|
||||
|
||||
UnityEngine.Profiling.Profiler.BeginSample("TessellatePath");
|
||||
|
||||
float[] segmentLengths = VectorUtils.SegmentsLengths(contour.Segments, contour.Closed);
|
||||
|
||||
// Approximate the number of vertices/indices we need to store the results so we reduce memory reallocations during work
|
||||
float approxTotalLength = 0.0f;
|
||||
foreach (var s in segmentLengths)
|
||||
approxTotalLength += s;
|
||||
|
||||
int approxStepCount = Math.Max((int)(approxTotalLength / tessellateOptions.StepDistance + 0.5f), 2);
|
||||
if (pathProps.Stroke.Pattern != null)
|
||||
approxStepCount += pathProps.Stroke.Pattern.Length * 2;
|
||||
|
||||
List<Vector2> verts = new List<Vector2>(approxStepCount * 2 + 32); // A little bit possibly for the endings
|
||||
List<UInt16> inds = new List<UInt16>((int)(verts.Capacity * 1.5f)); // Usually every 4 verts represent a quad that uses 6 indices
|
||||
|
||||
var patternIt = new PathPatternIterator(pathProps.Stroke.Pattern, pathProps.Stroke.PatternOffset);
|
||||
var pathIt = new PathDistanceForwardIterator(contour.Segments, contour.Closed, tessellateOptions.MaxCordDeviationSquared, tessellateOptions.MaxTanAngleDeviationCosine, tessellateOptions.SamplingStepSize);
|
||||
|
||||
JoiningInfo[] joiningInfo = new JoiningInfo[2];
|
||||
HandleNewSegmentJoining(pathIt, patternIt, joiningInfo, pathProps.Stroke.HalfThickness, segmentLengths);
|
||||
|
||||
int rangeIndex = 0;
|
||||
while (!pathIt.Ended)
|
||||
{
|
||||
if (patternIt.IsSolid)
|
||||
TessellateRange(patternIt.SegmentLength, pathIt, patternIt, pathProps, tessellateOptions, joiningInfo, segmentLengths, approxTotalLength, rangeIndex++, verts, inds);
|
||||
else
|
||||
SkipRange(patternIt.SegmentLength, pathIt, patternIt, pathProps, joiningInfo, segmentLengths);
|
||||
patternIt.Advance();
|
||||
}
|
||||
|
||||
vertices = verts.ToArray();
|
||||
indices = inds.ToArray();
|
||||
|
||||
UnityEngine.Profiling.Profiler.EndSample();
|
||||
}
|
||||
|
||||
static Vector2[] TraceShape(BezierContour contour, Stroke stroke, TessellationOptions tessellateOptions)
|
||||
{
|
||||
if (tessellateOptions.StepDistance < Epsilon)
|
||||
throw new Exception("stepDistance too small");
|
||||
|
||||
if (contour.Segments.Length < 2)
|
||||
return new Vector2[0];
|
||||
|
||||
float[] segmentLengths = VectorUtils.SegmentsLengths(contour.Segments, contour.Closed);
|
||||
|
||||
// Approximate the number of vertices/indices we need to store the results so we reduce memory reallocations during work
|
||||
float approxTotalLength = 0.0f;
|
||||
foreach (var s in segmentLengths)
|
||||
approxTotalLength += s;
|
||||
|
||||
int approxStepCount = Math.Max((int)(approxTotalLength / tessellateOptions.StepDistance + 0.5f), 2);
|
||||
var strokePattern = stroke != null ? stroke.Pattern : null;
|
||||
var strokePatternOffset = stroke != null ? stroke.PatternOffset : 0.0f;
|
||||
if (strokePattern != null)
|
||||
approxStepCount += strokePattern.Length * 2;
|
||||
|
||||
List<Vector2> verts = new List<Vector2>(approxStepCount); // A little bit possibly for the endings
|
||||
|
||||
var patternIt = new PathPatternIterator(strokePattern, strokePatternOffset);
|
||||
var pathIt = new PathDistanceForwardIterator(contour.Segments, true, tessellateOptions.MaxCordDeviationSquared, tessellateOptions.MaxTanAngleDeviationCosine, tessellateOptions.SamplingStepSize);
|
||||
verts.Add(pathIt.EvalCurrent());
|
||||
|
||||
while (!pathIt.Ended)
|
||||
{
|
||||
float distance = patternIt.SegmentLength;
|
||||
float startingLength = pathIt.LengthSoFar;
|
||||
float unitsRemaining = Mathf.Min(tessellateOptions.StepDistance, distance);
|
||||
bool endedEntirePath = false;
|
||||
for (;;)
|
||||
{
|
||||
var result = pathIt.AdvanceBy(unitsRemaining, out unitsRemaining);
|
||||
if (result == PathDistanceForwardIterator.Result.Ended)
|
||||
{
|
||||
endedEntirePath = true;
|
||||
break;
|
||||
}
|
||||
else if (result == PathDistanceForwardIterator.Result.NewSegment)
|
||||
verts.Add(pathIt.EvalCurrent());
|
||||
|
||||
if ((unitsRemaining <= Epsilon) &&
|
||||
!TryGetMoreRemainingUnits(ref unitsRemaining, pathIt, startingLength, distance, tessellateOptions.StepDistance))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (result == PathDistanceForwardIterator.Result.Stepped)
|
||||
verts.Add(pathIt.EvalCurrent());
|
||||
}
|
||||
|
||||
// Ending
|
||||
if (endedEntirePath)
|
||||
break;
|
||||
else verts.Add(pathIt.EvalCurrent());
|
||||
patternIt.Advance();
|
||||
}
|
||||
|
||||
if ((verts[0] - verts[verts.Count - 1]).sqrMagnitude < Epsilon)
|
||||
verts.RemoveAt(verts.Count - 1);
|
||||
return verts.ToArray(); // Why not return verts itself?
|
||||
}
|
||||
|
||||
static bool TryGetMoreRemainingUnits(ref float unitsRemaining, PathDistanceForwardIterator pathIt, float startingLength, float distance, float stepDistance)
|
||||
{
|
||||
float distanceCrossedSoFar = pathIt.LengthSoFar - startingLength;
|
||||
float epsilon = Math.Max(Epsilon, distance * Epsilon * 100.0f);
|
||||
if ((distance - distanceCrossedSoFar) <= epsilon)
|
||||
return false;
|
||||
if (distanceCrossedSoFar + stepDistance > distance)
|
||||
unitsRemaining = distance - distanceCrossedSoFar;
|
||||
else unitsRemaining = stepDistance;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void HandleNewSegmentJoining(PathDistanceForwardIterator pathIt, PathPatternIterator patternIt, JoiningInfo[] joiningInfo, float halfThickness, float[] segmentLengths)
|
||||
{
|
||||
joiningInfo[0] = joiningInfo[1];
|
||||
joiningInfo[1] = null;
|
||||
|
||||
if (!patternIt.IsSolidAt(pathIt.LengthSoFar + segmentLengths[pathIt.CurrentSegment]))
|
||||
return; // The joining center falls outside the pattern, so don't join... period
|
||||
|
||||
if (pathIt.Closed && pathIt.Segments.Count <= 2)
|
||||
return; // Not enough segments to do proper closing
|
||||
|
||||
if (pathIt.Closed)
|
||||
{
|
||||
JoiningInfo closing;
|
||||
if ((pathIt.CurrentSegment == 0) || (pathIt.CurrentSegment == pathIt.Segments.Count - 2))
|
||||
{
|
||||
closing = ForeseeJoining(
|
||||
VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.Segments.Count - 2),
|
||||
VectorUtils.PathSegmentAtIndex(pathIt.Segments, 0),
|
||||
halfThickness, segmentLengths[pathIt.Segments.Count - 2]);
|
||||
|
||||
if (pathIt.CurrentSegment == 0)
|
||||
joiningInfo[0] = closing;
|
||||
else
|
||||
{
|
||||
joiningInfo[1] = closing;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (pathIt.CurrentSegment > pathIt.Segments.Count - 2)
|
||||
return;
|
||||
}
|
||||
else if (pathIt.CurrentSegment >= pathIt.Segments.Count - 2)
|
||||
return;
|
||||
|
||||
joiningInfo[1] = ForeseeJoining(
|
||||
VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.CurrentSegment),
|
||||
VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.CurrentSegment + 1),
|
||||
halfThickness, segmentLengths[pathIt.CurrentSegment]);
|
||||
}
|
||||
|
||||
static void SkipRange(
|
||||
float distance, PathDistanceForwardIterator pathIt, PathPatternIterator patternIt,
|
||||
PathProperties pathProps, JoiningInfo[] joiningInfo, float[] segmentLengths)
|
||||
{
|
||||
float unitsRemaining = distance;
|
||||
while (unitsRemaining > Epsilon)
|
||||
{
|
||||
var result = pathIt.AdvanceBy(unitsRemaining, out unitsRemaining);
|
||||
switch (result)
|
||||
{
|
||||
case PathDistanceForwardIterator.Result.Ended:
|
||||
return;
|
||||
case PathDistanceForwardIterator.Result.Stepped:
|
||||
if (unitsRemaining < Epsilon)
|
||||
return;
|
||||
break;
|
||||
case PathDistanceForwardIterator.Result.NewSegment:
|
||||
HandleNewSegmentJoining(pathIt, patternIt, joiningInfo, pathProps.Stroke.HalfThickness, segmentLengths);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void TessellateRange(
|
||||
float distance, PathDistanceForwardIterator pathIt, PathPatternIterator patternIt, PathProperties pathProps,
|
||||
TessellationOptions tessellateOptions, JoiningInfo[] joiningInfo, float[] segmentLengths, float totalLength, int rangeIndex, List<Vector2> verts, List<UInt16> inds)
|
||||
{
|
||||
bool startOfLoop = pathIt.Closed && (pathIt.CurrentSegment == 0) && (pathIt.CurrentT == 0.0f);
|
||||
if (startOfLoop && (joiningInfo[0] != null))
|
||||
{
|
||||
GenerateJoining(joiningInfo[0], pathProps.Corners, pathProps.Stroke.HalfThickness, pathProps.Stroke.TippedCornerLimit, tessellateOptions, verts, inds);
|
||||
}
|
||||
else
|
||||
{
|
||||
var pathEnding = pathProps.Head;
|
||||
|
||||
// If pattern at the end will overlap with beginning, use a chopped ending to allow merging
|
||||
if (pathIt.Closed && rangeIndex == 0 && patternIt.IsSolidAt(pathIt.CurrentT) && patternIt.IsSolidAt(totalLength))
|
||||
pathEnding = PathEnding.Chop;
|
||||
|
||||
GenerateTip(VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.CurrentSegment), true, pathIt.CurrentT, pathEnding, pathProps.Stroke.HalfThickness, tessellateOptions, verts, inds);
|
||||
}
|
||||
|
||||
float startingLength = pathIt.LengthSoFar;
|
||||
float unitsRemaining = Mathf.Min(tessellateOptions.StepDistance, distance);
|
||||
bool endedEntirePath = false;
|
||||
for (;;)
|
||||
{
|
||||
var result = pathIt.AdvanceBy(unitsRemaining, out unitsRemaining);
|
||||
if (result == PathDistanceForwardIterator.Result.Ended)
|
||||
{
|
||||
endedEntirePath = true;
|
||||
break;
|
||||
}
|
||||
else if (result == PathDistanceForwardIterator.Result.NewSegment)
|
||||
{
|
||||
if (joiningInfo[1] != null)
|
||||
GenerateJoining(joiningInfo[1], pathProps.Corners, pathProps.Stroke.HalfThickness, pathProps.Stroke.TippedCornerLimit, tessellateOptions, verts, inds);
|
||||
else AddSegment(VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.CurrentSegment), pathIt.CurrentT, pathProps.Stroke.HalfThickness, null, pathIt.SegmentLengthSoFar, verts, inds);
|
||||
HandleNewSegmentJoining(pathIt, patternIt, joiningInfo, pathProps.Stroke.HalfThickness, segmentLengths);
|
||||
}
|
||||
|
||||
if ((unitsRemaining <= Epsilon) &&
|
||||
!TryGetMoreRemainingUnits(ref unitsRemaining, pathIt, startingLength, distance, tessellateOptions.StepDistance))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (result == PathDistanceForwardIterator.Result.Stepped)
|
||||
AddSegment(VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.CurrentSegment), pathIt.CurrentT, pathProps.Stroke.HalfThickness, joiningInfo, pathIt.SegmentLengthSoFar, verts, inds);
|
||||
}
|
||||
|
||||
// Ending
|
||||
if (endedEntirePath && pathIt.Closed)
|
||||
{
|
||||
// No joining needed, the start and end of the path should just connect
|
||||
inds.Add(0);
|
||||
inds.Add(1);
|
||||
inds.Add((UInt16)(verts.Count - 2));
|
||||
inds.Add((UInt16)(verts.Count - 1));
|
||||
inds.Add((UInt16)(verts.Count - 2));
|
||||
inds.Add(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddSegment(VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.CurrentSegment), pathIt.CurrentT, pathProps.Stroke.HalfThickness, joiningInfo, pathIt.SegmentLengthSoFar, verts, inds);
|
||||
GenerateTip(VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.CurrentSegment), false, pathIt.CurrentT, pathProps.Tail, pathProps.Stroke.HalfThickness, tessellateOptions, verts, inds);
|
||||
}
|
||||
}
|
||||
|
||||
static void AddSegment(BezierSegment segment, float toT, float halfThickness, JoiningInfo[] joinInfo, float segmentLengthSoFar, List<Vector2> verts, List<UInt16> inds)
|
||||
{
|
||||
Vector2 tanTo, normTo;
|
||||
Vector2 posTo = VectorUtils.EvalFull(segment, toT, out tanTo, out normTo);
|
||||
|
||||
Vector2 posThickness = posTo + normTo * halfThickness;
|
||||
Vector2 negThickness = posTo + normTo * -halfThickness;
|
||||
|
||||
if (joinInfo != null)
|
||||
{
|
||||
if ((joinInfo[0] != null) && (segmentLengthSoFar < joinInfo[0].InnerCornerDistFromStart))
|
||||
{
|
||||
if (joinInfo[0].RoundPosThickness)
|
||||
negThickness = joinInfo[0].InnerCornerVertex;
|
||||
else posThickness = joinInfo[0].InnerCornerVertex;
|
||||
}
|
||||
|
||||
if ((joinInfo[1] != null) && (segmentLengthSoFar > joinInfo[1].InnerCornerDistToEnd))
|
||||
{
|
||||
if (joinInfo[1].RoundPosThickness)
|
||||
negThickness = joinInfo[1].InnerCornerVertex;
|
||||
else posThickness = joinInfo[1].InnerCornerVertex;
|
||||
}
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.Assert(verts.Count >= 2);
|
||||
int indexStart = verts.Count - 2;
|
||||
verts.Add(posThickness);
|
||||
verts.Add(negThickness);
|
||||
inds.Add((UInt16)(indexStart + 0));
|
||||
inds.Add((UInt16)(indexStart + 3));
|
||||
inds.Add((UInt16)(indexStart + 1));
|
||||
inds.Add((UInt16)(indexStart + 0));
|
||||
inds.Add((UInt16)(indexStart + 2));
|
||||
inds.Add((UInt16)(indexStart + 3));
|
||||
}
|
||||
|
||||
class JoiningInfo
|
||||
{
|
||||
public Vector2 JoinPos;
|
||||
public Vector2 TanAtEnd, TanAtStart;
|
||||
public Vector2 NormAtEnd, NormAtStart;
|
||||
public Vector2 PosThicknessStart, NegThicknessStart;
|
||||
public Vector2 PosThicknessEnd, NegThicknessEnd;
|
||||
public Vector2 PosThicknessClosingPoint, NegThicknessClosingPoint;
|
||||
public bool RoundPosThickness;
|
||||
public bool SimpleJoin;
|
||||
public Vector2 InnerCornerVertex;
|
||||
public float InnerCornerDistToEnd, InnerCornerDistFromStart;
|
||||
}
|
||||
|
||||
static JoiningInfo ForeseeJoining(BezierSegment end, BezierSegment start, float halfThickness, float endSegmentLength)
|
||||
{
|
||||
JoiningInfo joinInfo = new JoiningInfo();
|
||||
|
||||
// The joining generates the vertices at both ends as well as the joining itself
|
||||
joinInfo.JoinPos = end.P3;
|
||||
joinInfo.TanAtEnd = VectorUtils.EvalTangent(end, 1.0f);
|
||||
joinInfo.NormAtEnd = Vector2.Perpendicular(joinInfo.TanAtEnd);
|
||||
joinInfo.TanAtStart = VectorUtils.EvalTangent(start, 0.0f);
|
||||
joinInfo.NormAtStart = Vector2.Perpendicular(joinInfo.TanAtStart);
|
||||
|
||||
// If the tangents are continuous at the join location, we don't have
|
||||
// to generate a corner, we do a "simple" join by just connecting the vertices
|
||||
// from the two segments directly
|
||||
float cosAngleBetweenTans = Vector2.Dot(joinInfo.TanAtEnd, joinInfo.TanAtStart);
|
||||
joinInfo.SimpleJoin = Mathf.Approximately(Mathf.Abs(cosAngleBetweenTans), 1.0f);
|
||||
if (joinInfo.SimpleJoin)
|
||||
return null;
|
||||
|
||||
joinInfo.PosThicknessEnd = joinInfo.JoinPos + joinInfo.NormAtEnd * halfThickness;
|
||||
joinInfo.NegThicknessEnd = joinInfo.JoinPos - joinInfo.NormAtEnd * halfThickness;
|
||||
joinInfo.PosThicknessStart = joinInfo.JoinPos + joinInfo.NormAtStart * halfThickness;
|
||||
joinInfo.NegThicknessStart = joinInfo.JoinPos - joinInfo.NormAtStart * halfThickness;
|
||||
|
||||
if (joinInfo.SimpleJoin)
|
||||
{
|
||||
joinInfo.PosThicknessClosingPoint = Vector2.LerpUnclamped(joinInfo.PosThicknessEnd, joinInfo.PosThicknessStart, 0.5f);
|
||||
joinInfo.NegThicknessClosingPoint = Vector2.LerpUnclamped(joinInfo.NegThicknessEnd, joinInfo.NegThicknessStart, 0.5f);
|
||||
}
|
||||
else
|
||||
{
|
||||
joinInfo.PosThicknessClosingPoint = VectorUtils.IntersectLines(joinInfo.PosThicknessEnd, joinInfo.PosThicknessEnd + joinInfo.TanAtEnd, joinInfo.PosThicknessStart, joinInfo.PosThicknessStart + joinInfo.TanAtStart);
|
||||
joinInfo.NegThicknessClosingPoint = VectorUtils.IntersectLines(joinInfo.NegThicknessEnd, joinInfo.NegThicknessEnd + joinInfo.TanAtEnd, joinInfo.NegThicknessStart, joinInfo.NegThicknessStart + joinInfo.TanAtStart);
|
||||
|
||||
if (float.IsInfinity(joinInfo.PosThicknessClosingPoint.x) || float.IsInfinity(joinInfo.PosThicknessClosingPoint.y))
|
||||
joinInfo.PosThicknessClosingPoint = joinInfo.JoinPos;
|
||||
if (float.IsInfinity(joinInfo.NegThicknessClosingPoint.x) || float.IsInfinity(joinInfo.NegThicknessClosingPoint.y))
|
||||
joinInfo.NegThicknessClosingPoint = joinInfo.JoinPos;
|
||||
}
|
||||
|
||||
// Should we round the positive thickness side or the negative thickness side?
|
||||
joinInfo.RoundPosThickness = PointOnTheLeftOfLine(Vector2.zero, joinInfo.TanAtEnd, joinInfo.TanAtStart);
|
||||
|
||||
// Inner corner vertex should be calculated by intersection of the inner segments
|
||||
Vector2[] startTrail = null, endTrail = null;
|
||||
Vector2 intersectionOnStart = Vector2.zero, intersectionOnEnd = Vector2.zero;
|
||||
if (!joinInfo.SimpleJoin)
|
||||
{
|
||||
BezierSegment endFlipped = VectorUtils.FlipSegment(end);
|
||||
Vector2 thicknessClosingPoint = joinInfo.RoundPosThickness ? joinInfo.PosThicknessClosingPoint : joinInfo.NegThicknessClosingPoint;
|
||||
Vector2 meetingPoint = end.P3;
|
||||
Vector2 thicknessDiagonalEnd = meetingPoint + (thicknessClosingPoint - meetingPoint) * 10.0f;
|
||||
startTrail = LineBezierThicknessIntersect(
|
||||
start, joinInfo.RoundPosThickness ? -halfThickness : halfThickness, meetingPoint, thicknessDiagonalEnd,
|
||||
out joinInfo.InnerCornerDistFromStart, out intersectionOnStart);
|
||||
endTrail = LineBezierThicknessIntersect(
|
||||
endFlipped, joinInfo.RoundPosThickness ? halfThickness : -halfThickness, meetingPoint, thicknessDiagonalEnd,
|
||||
out joinInfo.InnerCornerDistToEnd, out intersectionOnEnd);
|
||||
}
|
||||
|
||||
bool intersectionFound = false;
|
||||
if ((startTrail != null) && (endTrail != null))
|
||||
{
|
||||
var intersect = VectorUtils.IntersectLines(startTrail[0], startTrail[1], endTrail[0], endTrail[1]);
|
||||
var isOnStartTrail = PointOnLineIsWithinSegment(startTrail[0], startTrail[1], intersect);
|
||||
var isOnEndTrail = PointOnLineIsWithinSegment(endTrail[0], endTrail[1], intersect);
|
||||
if (!float.IsInfinity(intersect.x) && isOnStartTrail && isOnEndTrail)
|
||||
{
|
||||
var vStart = intersectionOnStart - intersect;
|
||||
var vEnd = intersectionOnEnd - intersect;
|
||||
joinInfo.InnerCornerDistFromStart += (vStart == Vector2.zero) ? 0.0f : vStart.magnitude;
|
||||
joinInfo.InnerCornerDistToEnd += (vEnd == Vector2.zero) ? 0.0f : vEnd.magnitude;
|
||||
joinInfo.InnerCornerDistToEnd = endSegmentLength - joinInfo.InnerCornerDistToEnd;
|
||||
joinInfo.InnerCornerVertex = intersect; // Found it!
|
||||
intersectionFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!intersectionFound)
|
||||
{
|
||||
joinInfo.InnerCornerVertex = joinInfo.JoinPos + ((joinInfo.TanAtStart - joinInfo.TanAtEnd) / 2.0f).normalized * halfThickness;
|
||||
joinInfo.InnerCornerDistFromStart = 0;
|
||||
joinInfo.InnerCornerDistToEnd = endSegmentLength;
|
||||
}
|
||||
return joinInfo;
|
||||
}
|
||||
|
||||
static Vector2[] LineBezierThicknessIntersect(BezierSegment seg, float thickness, Vector2 lineFrom, Vector2 lineTo, out float distanceToIntersection, out Vector2 intersection)
|
||||
{
|
||||
Vector2 tan = VectorUtils.EvalTangent(seg, 0.0f);
|
||||
Vector2 nrm = Vector2.Perpendicular(tan);
|
||||
Vector2 lastPoint = seg.P0 + nrm * thickness;
|
||||
distanceToIntersection = 0.0f;
|
||||
intersection = new Vector2(float.PositiveInfinity, float.PositiveInfinity);
|
||||
float stepT = 0.01f;
|
||||
float t = 0;
|
||||
while (t < 1.0f)
|
||||
{
|
||||
t += stepT;
|
||||
var point = VectorUtils.EvalFull(seg, t, out tan, out nrm) + nrm * thickness;
|
||||
intersection = VectorUtils.IntersectLines(lineFrom, lineTo, lastPoint, point);
|
||||
if (PointOnLineIsWithinSegment(lastPoint, point, intersection))
|
||||
{
|
||||
distanceToIntersection += (lastPoint - intersection).magnitude;
|
||||
return new Vector2[] { lastPoint, point };
|
||||
}
|
||||
distanceToIntersection += (lastPoint - point).magnitude;
|
||||
lastPoint = point;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool PointOnLineIsWithinSegment(Vector2 lineFrom, Vector2 lineTo, Vector2 point)
|
||||
{
|
||||
// Point is assumed to be already on the line, but we would like to know if it is within the segment specified
|
||||
var v = (lineTo - lineFrom).normalized;
|
||||
if (Vector2.Dot(point - lineFrom, v) < -Epsilon)
|
||||
return false;
|
||||
if (Vector2.Dot(point - lineTo, v) > Epsilon)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void GenerateJoining(JoiningInfo joinInfo, PathCorner corner, float halfThickness, float tippedCornerLimit, TessellationOptions tessellateOptions, List<Vector2> verts, List<UInt16> inds)
|
||||
{
|
||||
// The joining generates the vertices at both ends as well as the joining itself
|
||||
if (verts.Count == 0)
|
||||
{
|
||||
// Starting a path with a joining (meaning a loop)
|
||||
verts.Add(joinInfo.RoundPosThickness ? joinInfo.PosThicknessEnd : joinInfo.InnerCornerVertex);
|
||||
verts.Add(joinInfo.RoundPosThickness ? joinInfo.InnerCornerVertex : joinInfo.NegThicknessEnd);
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.Assert(verts.Count >= 2);
|
||||
int indexStart = verts.Count - 2; // Using the last two vertices
|
||||
|
||||
// Convert a tipped corner to a beveled one if tippedCornerLimit ratio is reached
|
||||
if (corner == PathCorner.Tipped && tippedCornerLimit >= 1.0f)
|
||||
{
|
||||
var theta = Vector2.Angle(-joinInfo.TanAtEnd, joinInfo.TanAtStart) * Mathf.Deg2Rad;
|
||||
var ratio = 1.0f / Mathf.Sin(theta / 2.0f);
|
||||
if (ratio > tippedCornerLimit)
|
||||
corner = PathCorner.Beveled;
|
||||
}
|
||||
|
||||
if (joinInfo.SimpleJoin)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
else if (corner == PathCorner.Tipped)
|
||||
{
|
||||
verts.Add(joinInfo.PosThicknessClosingPoint);
|
||||
verts.Add(joinInfo.NegThicknessClosingPoint);
|
||||
verts.Add(joinInfo.RoundPosThickness ? joinInfo.PosThicknessStart : joinInfo.InnerCornerVertex);
|
||||
verts.Add(joinInfo.RoundPosThickness ? joinInfo.InnerCornerVertex : joinInfo.NegThicknessStart);
|
||||
|
||||
// Ending to tip
|
||||
inds.Add((UInt16)(indexStart + 0));
|
||||
inds.Add((UInt16)(indexStart + 3));
|
||||
inds.Add((UInt16)(indexStart + 1));
|
||||
inds.Add((UInt16)(indexStart + 0));
|
||||
inds.Add((UInt16)(indexStart + 2));
|
||||
inds.Add((UInt16)(indexStart + 3));
|
||||
|
||||
// Tip to starting
|
||||
inds.Add((UInt16)(indexStart + 4));
|
||||
inds.Add((UInt16)(indexStart + 3));
|
||||
inds.Add((UInt16)(indexStart + 2));
|
||||
inds.Add((UInt16)(indexStart + 4));
|
||||
inds.Add((UInt16)(indexStart + 5));
|
||||
inds.Add((UInt16)(indexStart + 3));
|
||||
|
||||
return;
|
||||
}
|
||||
else if (corner == PathCorner.Beveled)
|
||||
{
|
||||
verts.Add(joinInfo.RoundPosThickness ? joinInfo.PosThicknessEnd : joinInfo.InnerCornerVertex); // 2
|
||||
verts.Add(joinInfo.RoundPosThickness ? joinInfo.InnerCornerVertex : joinInfo.NegThicknessEnd); // 3
|
||||
verts.Add(joinInfo.RoundPosThickness ? joinInfo.PosThicknessStart : joinInfo.InnerCornerVertex); // 4
|
||||
verts.Add(joinInfo.RoundPosThickness ? joinInfo.InnerCornerVertex : joinInfo.NegThicknessStart); // 5
|
||||
|
||||
// Ending to tip
|
||||
inds.Add((UInt16)(indexStart + 0));
|
||||
inds.Add((UInt16)(indexStart + 2));
|
||||
inds.Add((UInt16)(indexStart + 1));
|
||||
inds.Add((UInt16)(indexStart + 1));
|
||||
inds.Add((UInt16)(indexStart + 2));
|
||||
inds.Add((UInt16)(indexStart + 3));
|
||||
|
||||
// Bevel
|
||||
if (joinInfo.RoundPosThickness)
|
||||
{
|
||||
inds.Add((UInt16)(indexStart + 2));
|
||||
inds.Add((UInt16)(indexStart + 4));
|
||||
inds.Add((UInt16)(indexStart + 3));
|
||||
}
|
||||
else
|
||||
{
|
||||
inds.Add((UInt16)(indexStart + 3));
|
||||
inds.Add((UInt16)(indexStart + 2));
|
||||
inds.Add((UInt16)(indexStart + 5));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (corner == PathCorner.Round)
|
||||
{
|
||||
float sweepAngle = Mathf.Acos(Vector2.Dot(joinInfo.NormAtEnd, joinInfo.NormAtStart));
|
||||
bool flipArc = false;
|
||||
if (!PointOnTheLeftOfLine(Vector2.zero, joinInfo.NormAtEnd, joinInfo.NormAtStart))
|
||||
{
|
||||
sweepAngle = -sweepAngle;
|
||||
flipArc = true;
|
||||
}
|
||||
|
||||
UInt16 innerCornerVertexIndex = (UInt16)verts.Count;
|
||||
verts.Add(joinInfo.InnerCornerVertex);
|
||||
|
||||
int arcSegments = CalculateArcSteps(halfThickness, 0, sweepAngle, tessellateOptions);
|
||||
for (int i = 0; i <= arcSegments; i++)
|
||||
{
|
||||
float angle = sweepAngle * (i / (float)arcSegments);
|
||||
Vector2 nrm = Matrix2D.RotateLH(angle) * joinInfo.NormAtEnd;
|
||||
if (flipArc) nrm = -nrm;
|
||||
verts.Add(nrm * halfThickness + joinInfo.JoinPos);
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
inds.Add((UInt16)(indexStart + 0));
|
||||
inds.Add((UInt16)(indexStart + 3));
|
||||
inds.Add((UInt16)(indexStart + (joinInfo.RoundPosThickness ? 2 : 1)));
|
||||
|
||||
inds.Add((UInt16)(indexStart + 0));
|
||||
inds.Add((UInt16)(indexStart + 2));
|
||||
inds.Add((UInt16)(indexStart + (joinInfo.RoundPosThickness ? 1 : 3)));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (joinInfo.RoundPosThickness)
|
||||
{
|
||||
inds.Add((UInt16)(indexStart + i + (flipArc ? 3 : 2)));
|
||||
inds.Add((UInt16)(indexStart + i + (flipArc ? 2 : 3)));
|
||||
inds.Add(innerCornerVertexIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
inds.Add((UInt16)(indexStart + i + (flipArc ? 3 : 2)));
|
||||
inds.Add((UInt16)(indexStart + i + (flipArc ? 2 : 3)));
|
||||
inds.Add(innerCornerVertexIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manually add the last segment, maintain the expected vertex positioning
|
||||
int endingVerticesIndex = verts.Count;
|
||||
if (joinInfo.RoundPosThickness)
|
||||
{
|
||||
verts.Add(joinInfo.PosThicknessStart);
|
||||
verts.Add(joinInfo.InnerCornerVertex);
|
||||
}
|
||||
else
|
||||
{
|
||||
verts.Add(joinInfo.InnerCornerVertex);
|
||||
verts.Add(joinInfo.NegThicknessStart);
|
||||
}
|
||||
inds.Add((UInt16)(endingVerticesIndex - 1));
|
||||
inds.Add((UInt16)(endingVerticesIndex + 0));
|
||||
inds.Add(innerCornerVertexIndex);
|
||||
}
|
||||
}
|
||||
|
||||
static void GenerateTip(BezierSegment segment, bool atStart, float t, PathEnding ending, float halfThickness, TessellationOptions tessellateOptions, List<Vector2> verts, List<UInt16> inds)
|
||||
{
|
||||
// The tip includes the vertices at the end itself
|
||||
Vector2 tan, nrm;
|
||||
var pos = VectorUtils.EvalFull(segment, t, out tan, out nrm);
|
||||
int indexStart = verts.Count;
|
||||
|
||||
switch (ending)
|
||||
{
|
||||
case PathEnding.Chop:
|
||||
if (atStart)
|
||||
{
|
||||
verts.Add(pos + nrm * halfThickness);
|
||||
verts.Add(pos - nrm * halfThickness);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not much, path segments are always expected to be generated perpendicular to the path
|
||||
// at the segment point location, so we don't have to do anything for the ending
|
||||
}
|
||||
break;
|
||||
|
||||
case PathEnding.Square:
|
||||
if (atStart)
|
||||
{
|
||||
verts.Add(pos + nrm * halfThickness - tan * halfThickness);
|
||||
verts.Add(pos - nrm * halfThickness - tan * halfThickness);
|
||||
verts.Add(pos + nrm * halfThickness);
|
||||
verts.Add(pos - nrm * halfThickness);
|
||||
|
||||
inds.Add((UInt16)(indexStart + 0));
|
||||
inds.Add((UInt16)(indexStart + 3));
|
||||
inds.Add((UInt16)(indexStart + 1));
|
||||
inds.Add((UInt16)(indexStart + 0));
|
||||
inds.Add((UInt16)(indexStart + 2));
|
||||
inds.Add((UInt16)(indexStart + 3));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Relying on the last two vertices, and just adding two of our own here
|
||||
verts.Add(pos + nrm * halfThickness + tan * halfThickness);
|
||||
verts.Add(pos - nrm * halfThickness + tan * halfThickness);
|
||||
|
||||
inds.Add((UInt16)(indexStart + 0 - 2));
|
||||
inds.Add((UInt16)(indexStart + 3 - 2));
|
||||
inds.Add((UInt16)(indexStart + 1 - 2));
|
||||
inds.Add((UInt16)(indexStart + 0 - 2));
|
||||
inds.Add((UInt16)(indexStart + 2 - 2));
|
||||
inds.Add((UInt16)(indexStart + 3 - 2));
|
||||
}
|
||||
break;
|
||||
|
||||
case PathEnding.Round:
|
||||
float arcSign = atStart ? -1 : 1;
|
||||
int arcSegments = CalculateArcSteps(halfThickness, 0, Mathf.PI, tessellateOptions);
|
||||
for (int i = 1; i < arcSegments; i++)
|
||||
{
|
||||
float angle = Mathf.PI * (i / (float)arcSegments);
|
||||
verts.Add(pos + Matrix2D.RotateLH(angle) * nrm * halfThickness * arcSign);
|
||||
}
|
||||
|
||||
if (atStart)
|
||||
{
|
||||
// Note how we maintain the last two vertices being setup for connection by the rest of the path vertices
|
||||
int indexTipStart = verts.Count;
|
||||
verts.Add(pos + nrm * halfThickness);
|
||||
verts.Add(pos - nrm * halfThickness);
|
||||
|
||||
for (int i = 1; i < arcSegments; i++)
|
||||
{
|
||||
inds.Add((UInt16)(indexTipStart + 1));
|
||||
inds.Add((UInt16)(indexStart + i - 1));
|
||||
inds.Add((UInt16)(indexStart + i));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
inds.Add((UInt16)(indexStart - 1));
|
||||
inds.Add((UInt16)(indexStart - 2));
|
||||
inds.Add((UInt16)(indexStart + 0));
|
||||
for (int i = 1; i < arcSegments - 1; i++)
|
||||
{
|
||||
inds.Add((UInt16)(indexStart - 1));
|
||||
inds.Add((UInt16)(indexStart + i - 1));
|
||||
inds.Add((UInt16)(indexStart + i));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
System.Diagnostics.Debug.Assert(false); // Joining has its own function
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static int CalculateArcSteps(float radius, float fromAngle, float toAngle, TessellationOptions tessellateOptions)
|
||||
{
|
||||
float stepDivisor = float.MaxValue;
|
||||
|
||||
if (tessellateOptions.StepDistance != float.MaxValue)
|
||||
stepDivisor = tessellateOptions.StepDistance / radius;
|
||||
|
||||
if (tessellateOptions.MaxCordDeviation != float.MaxValue)
|
||||
{
|
||||
float y = radius - tessellateOptions.MaxCordDeviation;
|
||||
float cordHalfLength = Mathf.Sqrt(radius * radius - y * y);
|
||||
float div = Mathf.Min(stepDivisor, Mathf.Asin(cordHalfLength / radius));
|
||||
if (div > VectorUtils.Epsilon)
|
||||
stepDivisor = div;
|
||||
}
|
||||
|
||||
if (tessellateOptions.MaxTanAngleDeviation < Mathf.PI * 0.5f)
|
||||
stepDivisor = Mathf.Min(stepDivisor, tessellateOptions.MaxTanAngleDeviation * 2.0f);
|
||||
|
||||
float stepsInFullCircle = (Mathf.PI * 2.0f) / stepDivisor;
|
||||
float arcPercentage = Mathf.Abs(fromAngle - toAngle) / (Mathf.PI * 2.0f);
|
||||
return (int)Mathf.Max(stepsInFullCircle * arcPercentage + 0.5f, 3); // Never less than 3 segments
|
||||
}
|
||||
|
||||
/// <summary>Tessellates a rectangle.</summary>
|
||||
/// <param name="rect">Rectangle to tessellate</param>
|
||||
/// <param name="vertices">The output vertices</param>
|
||||
/// <param name="indices">The output triangles</param>
|
||||
public static void TessellateRect(Rect rect, out Vector2[] vertices, out UInt16[] indices)
|
||||
{
|
||||
vertices = new Vector2[] {
|
||||
new Vector2(rect.xMin, rect.yMin),
|
||||
new Vector2(rect.xMax, rect.yMin),
|
||||
new Vector2(rect.xMax, rect.yMax),
|
||||
new Vector2(rect.xMin, rect.yMax)
|
||||
};
|
||||
indices = new UInt16[] {
|
||||
1, 0, 2, 2, 0, 3
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Tessellates a rectangle border.</summary>
|
||||
/// <param name="rect">Rectangle to tessellate</param>
|
||||
/// <param name="halfThickness">The half-thickness of the border</param>
|
||||
/// <param name="vertices">The output vertices</param>
|
||||
/// <param name="indices">The output triangles</param>
|
||||
public static void TessellateRectBorder(Rect rect, float halfThickness, out Vector2[] vertices, out UInt16[] indices)
|
||||
{
|
||||
var verts = new List<Vector2>(16);
|
||||
var inds = new List<UInt16>(24);
|
||||
|
||||
// Left edge
|
||||
var p0 = new Vector2(rect.x, rect.y + rect.height);
|
||||
var p1 = new Vector2(rect.x, rect.y);
|
||||
|
||||
var q0 = p0 + new Vector2(-halfThickness, halfThickness);
|
||||
var q1 = p1 + new Vector2(-halfThickness, -halfThickness);
|
||||
var q2 = p1 + new Vector2(halfThickness, halfThickness);
|
||||
var q3 = p0 + new Vector2(halfThickness, -halfThickness);
|
||||
|
||||
verts.Add(q0); verts.Add(q1); verts.Add(q2); verts.Add(q3);
|
||||
inds.Add(0); inds.Add(3); inds.Add(2); inds.Add(2); inds.Add(1); inds.Add(0);
|
||||
|
||||
// Top edge
|
||||
p0 = new Vector2(rect.x, rect.y);
|
||||
p1 = new Vector2(rect.x + rect.width, rect.y);
|
||||
|
||||
q0 = p0 + new Vector2(-halfThickness, -halfThickness);
|
||||
q1 = p1 + new Vector2(halfThickness, -halfThickness);
|
||||
q2 = p1 + new Vector2(-halfThickness, halfThickness);
|
||||
q3 = p0 + new Vector2(halfThickness, halfThickness);
|
||||
|
||||
verts.Add(q0); verts.Add(q1); verts.Add(q2); verts.Add(q3);
|
||||
inds.Add(4); inds.Add(7); inds.Add(6); inds.Add(6); inds.Add(5); inds.Add(4);
|
||||
|
||||
// Right edge
|
||||
p0 = new Vector2(rect.x + rect.width, rect.y);
|
||||
p1 = new Vector2(rect.x + rect.width, rect.y + rect.height);
|
||||
|
||||
q0 = p0 + new Vector2(halfThickness, -halfThickness);
|
||||
q1 = p1 + new Vector2(halfThickness, halfThickness);
|
||||
q2 = p1 + new Vector2(-halfThickness, -halfThickness);
|
||||
q3 = p0 + new Vector2(-halfThickness, halfThickness);
|
||||
|
||||
verts.Add(q0); verts.Add(q1); verts.Add(q2); verts.Add(q3);
|
||||
inds.Add(8); inds.Add(11); inds.Add(10); inds.Add(10); inds.Add(9); inds.Add(8);
|
||||
|
||||
// Bottom edge
|
||||
p0 = new Vector2(rect.x + rect.width, rect.y + rect.height);
|
||||
p1 = new Vector2(rect.x, rect.y + rect.height);
|
||||
|
||||
q0 = p0 + new Vector2(halfThickness, halfThickness);
|
||||
q1 = p1 + new Vector2(-halfThickness, halfThickness);
|
||||
q2 = p1 + new Vector2(halfThickness, -halfThickness);
|
||||
q3 = p0 + new Vector2(-halfThickness, -halfThickness);
|
||||
|
||||
verts.Add(q0); verts.Add(q1); verts.Add(q2); verts.Add(q3);
|
||||
inds.Add(12); inds.Add(15); inds.Add(14); inds.Add(14); inds.Add(13); inds.Add(12);
|
||||
|
||||
vertices = verts.ToArray();
|
||||
indices = inds.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7946c79a48c418944bb4ac48df97c4be
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc6ce72ebfec4b24faaee0fa9df6a984
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in new issue
Block a user