Layout and geometry for river junctions. Computes arms for rivers meeting at a node, samples centre/left frames, finds where left bank of one arm meets right bank of the next, optionally fits fillet arcs, computes trims and builds a counterclockwise outline polygon for the confluence.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace RedSnail.WaterTool2;
/// <summary>
/// Where rivers meet at a node (a confluence, a fork, or a bend sharp enough to need one): how far back from the node
/// each river's own mesh has to stop, and the outline of the water that fills the space between them, with rounded
/// banks. All of it is worked out on the rivers' real curved banks, so the outline lands exactly on the rows the
/// rivers stop at. Worked out from the graph on every reconcile, never stored.
/// </summary>
internal sealed class JunctionLayout
{
public sealed class Arm
{
public WaterRiver River;
public WaterCurveTable Table;
/// <summary>
/// The river starts at this node, so its water leaves the confluence (otherwise it ends here, and its water
/// arrives).
/// </summary>
public bool AtStart;
/// <summary>
/// Horizontal unit direction leaving the node.
/// </summary>
public Vector2 Direction;
public RiverWidth Width;
/// <summary>
/// The trim the corners ask for; <see cref="Trim"/> is this unless a short river had to give some back.
/// <see cref="SharpTrim"/> is what the corners would ask for with no fillet at all.
/// </summary>
public float RawTrim;
public float SharpTrim;
/// <summary>
/// How far from the node, along the curve, the river's own mesh starts.
/// </summary>
public float Trim;
/// <summary>
/// Samples of the river near the node, from a straight extension behind the node (negative distances, where
/// the outer corner of a sharp bend lives) out along the curve. Banks are offsets of these.
/// </summary>
public float[] Distances;
public Vector3[] Centres;
public Vector3[] Lefts;
public float[] Halves;
public int NodeIndex;
public float Length => Table.Length;
public int SampleCount => Distances.Length;
/// <summary>
/// The curve distance of a point measured from this node along the arm.
/// </summary>
public float CurveDistance(float _FromNode)
{
return AtStart ? _FromNode : Table.Length - _FromNode;
}
/// <summary>
/// How far each bank sits from the centre at a distance from the node; the node's own value behind it.
/// </summary>
public float Half(float _FromNode)
{
return Width.HalfAt(CurveDistance(Math.Clamp(_FromNode, 0.0f, Length)));
}
public float OffsetAt(in EdgeRef _Edge, float _FromNode)
{
return _Edge.Side * (Half(_FromNode) + _Edge.Extra);
}
public float OffsetAt(in EdgeRef _Edge, int _Index)
{
return _Edge.Side * (Halves[_Index] + _Edge.Extra);
}
public Vector3 EdgePoint(int _Index, float _Offset)
{
return Centres[_Index] + Lefts[_Index] * _Offset;
}
public Vector3[] OffsetPolyline(in EdgeRef _Edge)
{
var points = new Vector3[SampleCount];
for (int i = 0; i < SampleCount; i++)
points[i] = EdgePoint(i, OffsetAt(_Edge, i));
return points;
}
}
/// <summary>
/// A line along one of an arm's banks (+1 its left, -1 its right, looking out of the node), pushed outward from
/// that bank by <see cref="Extra"/> (inward when negative).
/// </summary>
public readonly record struct EdgeRef(float Side, float Extra)
{
public static readonly EdgeRef Left = new(1.0f, 0.0f);
public static readonly EdgeRef Right = new(-1.0f, 0.0f);
}
/// <summary>
/// Where arm A's left bank meets the right bank of arm B, the next arm counter-clockwise.
/// </summary>
private sealed class Corner
{
public int ArmA;
public int ArmB;
public Vector3 Point;
public float ParamA;
public float ParamB;
public bool HasFillet;
public Vector2 Centre;
public float Radius;
public Vector3 TangentA;
public Vector3 TangentB;
public float TangentParamA;
public float TangentParamB;
public Vector2 DirectionA;
public Vector2 DirectionB;
public float Angle;
/// <summary>
/// Which way each bank is pushed sideways to move into the corner's angle.
/// </summary>
public float SignA = 1.0f;
public float SignB = 1.0f;
public float EndA => HasFillet ? TangentParamA : ParamA;
public float EndB => HasFillet ? TangentParamB : ParamB;
}
private readonly struct Hit
{
public readonly Vector3 Point;
public readonly float DistanceA;
public readonly float DistanceB;
public readonly Vector2 DirectionA;
public readonly Vector2 DirectionB;
public Hit(Vector3 _Point, float _DistanceA, float _DistanceB, Vector2 _DirectionA, Vector2 _DirectionB)
{
Point = _Point;
DistanceA = _DistanceA;
DistanceB = _DistanceB;
DirectionA = _DirectionA;
DirectionB = _DirectionB;
}
}
/// <summary>
/// How far out along each arm (in river widths) corners are looked for.
/// </summary>
private const float ForwardReach = 4.0f;
/// <summary>
/// How far behind the node (in river widths) the arms are extended straight, to catch the outside corner of a
/// sharp bend.
/// </summary>
private const float BackwardReach = 3.0f;
private const float SampleStep = 30.0f;
private const float ArcStepDegrees = 15.0f;
/// <summary>
/// How far past a sharp corner's point the arm rows sit.
/// </summary>
private const float SharpMargin = 3.0f;
public WaterNode Node { get; private set; }
/// <summary>
/// The fillet radius the corners were built with: the node's, unless a short river forced it smaller.
/// </summary>
public float Radius { get; }
public List<Arm> Arms { get; } = new();
private readonly List<Corner> m_Corners = new();
private JunctionLayout(WaterNode _Node, float _Radius)
{
Node = _Node;
Radius = _Radius;
}
/// <summary>
/// The layout of the node's confluence, or null when there is nothing to fill: fewer than two usable arms, or arms
/// running straight through each other (a smooth bend).
/// </summary>
public static JunctionLayout Create(WaterNetwork _Network, WaterNode _Node, float _Radius, IReadOnlyDictionary<int, RiverWidth> _Widths)
{
var layout = new JunctionLayout(_Node, MathF.Max(0.0f, _Radius));
foreach (var river in _Network.GetRiversAt(_Node.Id))
{
var table = _Network.GetCurveTable(river);
if (table is null || !_Widths.TryGetValue(river.Id, out var width))
continue;
bool atStart = river.StartNodeId == _Node.Id;
Vector3 tangent = atStart ? table.Curve.Tangent(0.0f) : -table.Curve.Tangent(1.0f);
var direction = new Vector2(tangent.x, tangent.y);
// A vertical arm (a waterfall) has no bank to meet anything on.
if (direction.Length < 0.001f)
continue;
layout.Arms.Add(new Arm
{
River = river,
Table = table,
AtStart = atStart,
Direction = direction.Normal,
Width = width
});
}
if (layout.Arms.Count < 2)
return null;
layout.Arms.Sort((a, b) => MathF.Atan2(a.Direction.y, a.Direction.x).CompareTo(MathF.Atan2(b.Direction.y, b.Direction.x)));
float maxWidth = layout.Arms.Max(arm => arm.Width.MaxHalf * 2.0f);
foreach (var arm in layout.Arms)
layout.BuildSamples(arm, maxWidth);
layout.FindCorners();
// Nothing was trimmed: the arms run straight into each other and the plain strips already join up.
if (layout.Arms.All(arm => arm.RawTrim <= 0.01f))
return null;
layout.ResetTrims();
return layout;
}
public Arm FindArm(int _RiverId)
{
foreach (var arm in Arms)
{
if (arm.River.Id == _RiverId)
return arm;
}
return null;
}
/// <summary>
/// Puts the trims back to what the corners asked for, before the network gives short rivers some back.
/// </summary>
public void ResetTrims()
{
foreach (var arm in Arms)
arm.Trim = arm.RawTrim;
}
private void BuildSamples(Arm _Arm, float _MaxWidth)
{
float forward = MathF.Min(_Arm.Length, ForwardReach * _MaxWidth);
float backward = BackwardReach * _MaxWidth;
int forwardCount = Math.Max(1, (int)MathF.Ceiling(forward / SampleStep));
int backwardCount = Math.Max(1, (int)MathF.Ceiling(backward / SampleStep));
int total = backwardCount + forwardCount + 1;
_Arm.Distances = new float[total];
_Arm.Centres = new Vector3[total];
_Arm.Lefts = new Vector3[total];
_Arm.Halves = new float[total];
_Arm.NodeIndex = backwardCount;
for (int k = 0; k <= forwardCount; k++)
{
float distance = k == forwardCount ? forward : forward * k / forwardCount;
int index = backwardCount + k;
ArmFrame(_Arm, distance, out _Arm.Centres[index], out _Arm.Lefts[index]);
_Arm.Distances[index] = distance;
_Arm.Halves[index] = _Arm.Half(distance);
}
// Behind the node the river does not exist; carry its frame at the node straight backwards.
var direction = new Vector3(_Arm.Direction.x, _Arm.Direction.y, 0.0f);
for (int k = 1; k <= backwardCount; k++)
{
float distance = -backward * k / backwardCount;
int index = backwardCount - k;
_Arm.Distances[index] = distance;
_Arm.Centres[index] = _Arm.Centres[backwardCount] + direction * distance;
_Arm.Lefts[index] = _Arm.Lefts[backwardCount];
_Arm.Halves[index] = _Arm.Halves[backwardCount];
}
}
/// <summary>
/// Meets each arm's left bank with the right bank of the next arm round, rounds the corner off and pushes the arm
/// trims out past it.
/// </summary>
private void FindCorners()
{
int count = Arms.Count;
for (int i = 0; i < count; i++)
{
int j = (i + 1) % count;
var a = Arms[i];
var b = Arms[j];
if (!Intersect(a, EdgeRef.Left, b, EdgeRef.Right, out Hit hit))
continue;
RefineCrossing(a, EdgeRef.Left, b, EdgeRef.Right, ref hit);
float angle = MathF.Acos(Math.Clamp(Vector2.Dot(hit.DirectionA, hit.DirectionB), -1.0f, 1.0f)).RadianToDegree();
var bisector = (hit.DirectionA + hit.DirectionB).Normal;
var leftNormalA = new Vector2(-hit.DirectionA.y, hit.DirectionA.x);
var rightNormalB = new Vector2(hit.DirectionB.y, -hit.DirectionB.x);
var corner = new Corner
{
ArmA = i,
ArmB = j,
Point = hit.Point,
ParamA = hit.DistanceA,
ParamB = hit.DistanceB,
DirectionA = hit.DirectionA,
DirectionB = hit.DirectionB,
Angle = angle,
SignA = Vector2.Dot(bisector, leftNormalA) >= 0.0f ? 1.0f : -1.0f,
SignB = Vector2.Dot(bisector, rightNormalB) >= 0.0f ? 1.0f : -1.0f
};
TryFillet(corner, a, b);
m_Corners.Add(corner);
// A sharp corner keeps the rows a hair past its point, so the stretch from row to corner exists.
float margin = !corner.HasFillet && angle >= 5.0f && angle <= 175.0f ? SharpMargin : 0.0f;
a.RawTrim = MathF.Max(a.RawTrim, corner.EndA + margin);
b.RawTrim = MathF.Max(b.RawTrim, corner.EndB + margin);
a.SharpTrim = MathF.Max(a.SharpTrim, corner.ParamA);
b.SharpTrim = MathF.Max(b.SharpTrim, corner.ParamB);
}
}
/// <summary>
/// A circle of the node's radius tangent to both banks: its centre is where the banks, each pushed sideways by the
/// radius towards the angle between them, cross; the tangent points are the closest points of the banks to it.
/// </summary>
private void TryFillet(Corner _Corner, Arm _A, Arm _B)
{
float radius = Radius;
if (radius <= 0.0f)
return;
// Nearly straight through, or folded back on itself: nothing sensible to round.
if (_Corner.Angle < 5.0f || _Corner.Angle > 175.0f)
return;
var pushedA = new EdgeRef(1.0f, _Corner.SignA * radius);
var pushedB = new EdgeRef(-1.0f, _Corner.SignB * radius);
if (!Intersect(_A, pushedA, _B, pushedB, out Hit centre))
return;
var centre2 = new Vector2(centre.Point.x, centre.Point.y);
Vector3 tangentA;
Vector3 tangentB;
float paramA;
float paramB;
if (RefineCrossing(_A, pushedA, _B, pushedB, ref centre))
{
centre2 = new Vector2(centre.Point.x, centre.Point.y);
paramA = centre.DistanceA;
paramB = centre.DistanceB;
ArmFrame(_A, paramA, out Vector3 centreA, out Vector3 leftA);
ArmFrame(_B, paramB, out Vector3 centreB, out Vector3 leftB);
tangentA = centreA + leftA * _A.OffsetAt(EdgeRef.Left, paramA);
tangentB = centreB + leftB * _B.OffsetAt(EdgeRef.Right, paramB);
}
else
{
ClosestPoint(_A, EdgeRef.Left, centre2, out tangentA, out paramA);
ClosestPoint(_B, EdgeRef.Right, centre2, out tangentB, out paramB);
}
_Corner.HasFillet = true;
_Corner.Centre = centre2;
_Corner.Radius = radius;
_Corner.TangentA = tangentA;
_Corner.TangentB = tangentB;
_Corner.TangentParamA = paramA;
_Corner.TangentParamB = paramB;
}
/// <summary>
/// The centreline point and left direction of an arm at a distance from the node along it, straight on behind the
/// node and past the arm's end, computed exactly as the river's rows are.
/// </summary>
private static void ArmFrame(Arm _Arm, float _FromNode, out Vector3 _Centre, out Vector3 _Left)
{
if (_FromNode < 0.0f)
{
_Centre = _Arm.Centres[_Arm.NodeIndex] + new Vector3(_Arm.Direction.x, _Arm.Direction.y, 0.0f) * _FromNode;
_Left = _Arm.Lefts[_Arm.NodeIndex];
return;
}
float onCurve = MathF.Min(_FromNode, _Arm.Length);
float curveDistance = _Arm.CurveDistance(onCurve);
_Arm.Table.Frame(curveDistance, out Vector3 position, out Vector3 tangent);
Vector3 right = WaterCurve.RightOf(tangent);
// Looking out of the node, an arm that ends here runs the other way, so its sides swap.
_Centre = position;
_Left = _Arm.AtStart ? -right : right;
if (_FromNode > _Arm.Length)
_Centre += Vector3.Cross(_Left, Vector3.Up) * (_FromNode - _Arm.Length);
}
private static Vector2 OffsetPoint(Arm _Arm, float _FromNode, in EdgeRef _Edge)
{
ArmFrame(_Arm, _FromNode, out Vector3 centre, out Vector3 left);
return Flat(centre + left * _Arm.OffsetAt(_Edge, _FromNode));
}
/// <summary>
/// Moves a crossing found on the sampled polylines onto the curves themselves (Newton on the two distances along
/// the arms), so corner points and fillet centres are exact rather than a sample step off. False, with the hit
/// untouched, when it does not converge.
/// </summary>
private static bool RefineCrossing(Arm _A, in EdgeRef _OffsetA, Arm _B, in EdgeRef _OffsetB, ref Hit _Hit)
{
const float step = 0.5f;
const float maxStep = 200.0f;
float distanceA = _Hit.DistanceA;
float distanceB = _Hit.DistanceB;
for (int iteration = 0; iteration < 10; iteration++)
{
Vector2 error = OffsetPoint(_A, distanceA, _OffsetA) - OffsetPoint(_B, distanceB, _OffsetB);
if (error.Length < 0.002f)
break;
Vector2 slopeA = (OffsetPoint(_A, distanceA + step, _OffsetA) - OffsetPoint(_A, distanceA - step, _OffsetA)) / (2.0f * step);
Vector2 slopeB = (OffsetPoint(_B, distanceB + step, _OffsetB) - OffsetPoint(_B, distanceB - step, _OffsetB)) / (2.0f * step);
// Solve slopeA * deltaA - slopeB * deltaB = -error.
float determinant = slopeB.x * slopeA.y - slopeA.x * slopeB.y;
if (MathF.Abs(determinant) < 0.000001f)
return false;
float deltaA = (error.x * slopeB.y - slopeB.x * error.y) / determinant;
float deltaB = (error.x * slopeA.y - slopeA.x * error.y) / determinant;
if (MathF.Abs(deltaA) > maxStep || MathF.Abs(deltaB) > maxStep)
return false;
distanceA = Math.Clamp(distanceA + deltaA, _A.Distances[0], _A.Distances[^1]);
distanceB = Math.Clamp(distanceB + deltaB, _B.Distances[0], _B.Distances[^1]);
}
if ((OffsetPoint(_A, distanceA, _OffsetA) - OffsetPoint(_B, distanceB, _OffsetB)).Length > 0.05f)
return false;
ArmFrame(_A, distanceA, out Vector3 centreA, out Vector3 leftA);
ArmFrame(_B, distanceB, out Vector3 centreB, out Vector3 leftB);
Vector3 point = (centreA + leftA * _A.OffsetAt(_OffsetA, distanceA) + centreB + leftB * _B.OffsetAt(_OffsetB, distanceB)) * 0.5f;
Vector2 directionA = Flat(Vector3.Cross(leftA, Vector3.Up)).Normal;
Vector2 directionB = Flat(Vector3.Cross(leftB, Vector3.Up)).Normal;
_Hit = new Hit(point, distanceA, distanceB, directionA, directionB);
return true;
}
/// <summary>
/// First crossing of two arms' offset polylines, scanning outwards from behind the node so the crossing nearest the
/// node wins. Two banks that start on the same point are a river running straight through: they meet at the node
/// and nothing needs rounding.
/// </summary>
private static bool Intersect(Arm _A, in EdgeRef _OffsetA, Arm _B, in EdgeRef _OffsetB, out Hit _Hit)
{
var polyA = _A.OffsetPolyline(_OffsetA);
var polyB = _B.OffsetPolyline(_OffsetB);
Vector3 startA = polyA[_A.NodeIndex];
Vector3 startB = polyB[_B.NodeIndex];
if (Flat(startA).Distance(Flat(startB)) < 1.0f)
{
_Hit = new Hit((startA + startB) * 0.5f, 0.0f, 0.0f, SegmentDirection(polyA, _A.NodeIndex), SegmentDirection(polyB, _B.NodeIndex));
return true;
}
for (int i = 0; i < polyA.Length - 1; i++)
{
Vector2 p = Flat(polyA[i]);
Vector2 r = Flat(polyA[i + 1]) - p;
for (int j = 0; j < polyB.Length - 1; j++)
{
Vector2 q = Flat(polyB[j]);
Vector2 s = Flat(polyB[j + 1]) - q;
float denominator = Cross(r, s);
if (MathF.Abs(denominator) < 0.000001f)
continue;
Vector2 offset = q - p;
float t = Cross(offset, s) / denominator;
float u = Cross(offset, r) / denominator;
if (t < -0.0001f || t > 1.0001f || u < -0.0001f || u > 1.0001f)
continue;
Vector3 pointA = Vector3.Lerp(polyA[i], polyA[i + 1], t);
Vector3 pointB = Vector3.Lerp(polyB[j], polyB[j + 1], u);
_Hit = new Hit((pointA + pointB) * 0.5f,
MathX.Lerp(_A.Distances[i], _A.Distances[i + 1], t),
MathX.Lerp(_B.Distances[j], _B.Distances[j + 1], u),
r.Normal, s.Normal);
return true;
}
}
_Hit = default;
return false;
}
/// <summary>
/// The point of an arm's offset polyline nearest to a point, and its distance from the node.
/// </summary>
private static void ClosestPoint(Arm _Arm, in EdgeRef _Offset, Vector2 _Target, out Vector3 _Point, out float _Distance)
{
var poly = _Arm.OffsetPolyline(_Offset);
float best = float.MaxValue;
_Point = poly[_Arm.NodeIndex];
_Distance = 0.0f;
for (int i = 0; i < poly.Length - 1; i++)
{
Vector2 p = Flat(poly[i]);
Vector2 segment = Flat(poly[i + 1]) - p;
float lengthSquared = segment.LengthSquared;
if (lengthSquared <= 0.0f)
continue;
float t = Math.Clamp(Vector2.Dot(_Target - p, segment) / lengthSquared, 0.0f, 1.0f);
float distance = (p + segment * t).Distance(_Target);
if (distance >= best)
continue;
best = distance;
_Point = Vector3.Lerp(poly[i], poly[i + 1], t);
_Distance = MathX.Lerp(_Arm.Distances[i], _Arm.Distances[i + 1], t);
}
}
private static Vector2 SegmentDirection(Vector3[] _Poly, int _Index)
{
int from = Math.Min(_Index, _Poly.Length - 2);
return (Flat(_Poly[from + 1]) - Flat(_Poly[from])).Normal;
}
/// <summary>
/// Builds the outline of the confluence once the arm trims are final, counter-clockwise round the node: each arm
/// contributes the whole row its river stops at (every point of it, exactly as the river has it), then its left
/// bank back to the corner it shares with the next arm, the corner (or its fillet arc), and the next arm's right
/// bank out to that arm's row. <paramref name="_RowOf"/> hands over each arm's row, left bank first looking
/// downstream; each row is given its own id, counting on from <paramref name="_FirstRowId"/>.
/// </summary>
public List<OutlineVertex> BuildOutline(Func<Arm, RiverRow> _RowOf, int _FirstRowId)
{
var outline = new List<OutlineVertex>();
for (int i = 0; i < Arms.Count; i++)
{
var arm = Arms[i];
var row = _RowOf(arm);
if (row is null || row.Count < 2)
return null;
// Looking out of the node the polygon crosses each row from its right bank to its left. For a river leaving
// the node that is its own right to left; for one arriving, its own left to right.
int rowId = _FirstRowId + i;
for (int k = 0; k < row.Count; k++)
AddVertex(outline, OutlineVertex.OnRow(row, arm.AtStart ? row.Count - 1 - k : k, rowId));
var corner = m_Corners.Find(candidate => candidate.ArmA == i);
// No corner, or a corner the arms had to be trimmed short of: nothing joins this side to the next arm, the
// outline cuts straight across to its row.
if (corner is null || corner.EndA > arm.Trim + 0.01f || corner.EndB > Arms[corner.ArmB].Trim + 0.01f)
continue;
var next = Arms[corner.ArmB];
// Down A's left bank from its row to the corner...
for (int k = arm.SampleCount - 1; k >= 0; k--)
{
if (arm.Distances[k] < arm.Trim && arm.Distances[k] > corner.EndA)
AddVertex(outline, OutlineVertex.Free(arm.EdgePoint(k, arm.Halves[k])));
}
AddCorner(outline, corner);
// ...and out along B's right bank to its row, which the next arm adds itself.
for (int k = 0; k < next.SampleCount; k++)
{
if (next.Distances[k] > corner.EndB && next.Distances[k] < next.Trim)
AddVertex(outline, OutlineVertex.Free(next.EdgePoint(k, -next.Halves[k])));
}
}
return SurfaceMesher.RemoveDuplicates(outline);
}
/// <summary>
/// The corner itself, or its fillet arc, its height eased from one bank to the other.
/// </summary>
private static void AddCorner(List<OutlineVertex> _Outline, Corner _Corner)
{
if (!_Corner.HasFillet)
{
AddVertex(_Outline, OutlineVertex.Free(_Corner.Point));
return;
}
AddVertex(_Outline, OutlineVertex.Free(_Corner.TangentA));
float angle0 = MathF.Atan2(_Corner.TangentA.y - _Corner.Centre.y, _Corner.TangentA.x - _Corner.Centre.x);
float angle1 = MathF.Atan2(_Corner.TangentB.y - _Corner.Centre.y, _Corner.TangentB.x - _Corner.Centre.x);
float delta = angle1 - angle0;
// Shortest way round: a fillet arc is always less than a half turn.
if (delta > MathF.PI)
delta -= MathF.PI * 2.0f;
else if (delta < -MathF.PI)
delta += MathF.PI * 2.0f;
int steps = Math.Max(2, (int)MathF.Ceiling(MathF.Abs(delta) / ArcStepDegrees.DegreeToRadian()));
for (int k = 1; k < steps; k++)
{
float fraction = (float)k / steps;
float angle = angle0 + delta * fraction;
var point = _Corner.Centre + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * _Corner.Radius;
AddVertex(_Outline, OutlineVertex.Free(new Vector3(point.x, point.y, MathX.Lerp(_Corner.TangentA.z, _Corner.TangentB.z, fraction))));
}
AddVertex(_Outline, OutlineVertex.Free(_Corner.TangentB));
}
/// <summary>
/// Appends a point unless it sits on the one before. A row's point always wins over a free point there: the
/// row is shared with the river and has to be there exactly as it is.
/// </summary>
private static void AddVertex(List<OutlineVertex> _Outline, OutlineVertex _Vertex)
{
if (_Outline.Count > 0 && Flat(_Outline[^1].Position).Distance(Flat(_Vertex.Position)) < 0.5f)
{
if (_Outline[^1].IsFixed || !_Vertex.IsFixed)
return;
_Outline[^1] = _Vertex;
return;
}
_Outline.Add(_Vertex);
}
private static Vector2 Flat(Vector3 _Vector)
{
return new Vector2(_Vector.x, _Vector.y);
}
private static float Cross(Vector2 _A, Vector2 _B)
{
return _A.x * _B.y - _A.y * _B.x;
}
}