JunctionLayout is an internal class that computes the geometric layout where multiple rivers meet at a WaterNode. It samples each river near the node, finds intersections between bank polylines, refines crossings onto the actual curves, attempts to build rounded fillet corners, computes trims for river meshes, and emits an ordered 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;
}
}