#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Primitives;
using OpenRA.Support;
namespace OpenRA.Mods.Common.MapGenerator
{
/// Path to be tiled onto a map using TemplateSegments.
public sealed class TilingPath
{
/// Describes the type and direction of the start or end of a TilingPath.
public struct Terminal
{
public string Type;
///
/// Direction to use for this terminal.
/// If the direction here is null, it will be determined automatically later.
///
public int? Direction;
///
/// A string which can match the format used by
/// OpenRA.Mods.Common.Terrain.TemplateSegment's Start or End.
///
public readonly string SegmentType
{
get
{
var direction =
Direction ?? throw new InvalidOperationException("Direction is null");
return $"{Type}.{MapGenerator.Direction.ToString(direction)}";
}
}
public Terminal(string type, int? direction)
{
Type = type;
Direction = direction;
}
}
///
/// Describes the permitted start, middle, and end segments/templates that can be used to
/// tile the path.
///
public sealed class PermittedSegments
{
public readonly ITemplatedTerrainInfo TemplatedTerrainInfo;
public readonly ImmutableArray Start;
public readonly ImmutableArray Inner;
public readonly ImmutableArray End;
public IEnumerable All => Start.Union(Inner).Union(End);
public PermittedSegments(
ITemplatedTerrainInfo templatedTerrainInfo,
IEnumerable start,
IEnumerable inner,
IEnumerable end)
{
TemplatedTerrainInfo = templatedTerrainInfo;
Start = start.ToImmutableArray();
Inner = inner.ToImmutableArray();
End = end.ToImmutableArray();
}
public PermittedSegments(
ITemplatedTerrainInfo templatedTerrainInfo,
IEnumerable all)
{
TemplatedTerrainInfo = templatedTerrainInfo;
var array = all.ToImmutableArray();
Start = array;
Inner = array;
End = array;
}
///
/// Creates a PermittedSegments using only the given types.
///
public static PermittedSegments FromType(
ITemplatedTerrainInfo templatedTerrainInfo,
IEnumerable types)
=> new(templatedTerrainInfo, FindSegments(templatedTerrainInfo, types));
///
/// Creates a PermittedSegments suitable for a path with given inner and terminal types
/// at the start and end.
///
public static PermittedSegments FromInnerAndTerminalTypes(
ITemplatedTerrainInfo templatedTerrainInfo,
IEnumerable innerTypes,
IEnumerable terminalTypes)
{
var innerTypesArray = innerTypes.ToImmutableArray();
var terminalTypesArray = terminalTypes.ToImmutableArray();
return new(
templatedTerrainInfo,
FindSegments(templatedTerrainInfo, terminalTypesArray, innerTypesArray, innerTypesArray),
FindSegments(templatedTerrainInfo, innerTypesArray),
FindSegments(templatedTerrainInfo, innerTypesArray, innerTypesArray, terminalTypesArray));
}
///
/// Equivalent to FindSegments(templatedTerrainInfo, types, types, types).
///
public static IEnumerable FindSegments(
ITemplatedTerrainInfo templatedTerrainInfo,
IEnumerable types)
{
var array = types.ToImmutableArray();
return FindSegments(templatedTerrainInfo, array, array, array);
}
///
/// Find templates that use some combination of the given start, inner, and end types.
///
public static IEnumerable FindSegments(
ITemplatedTerrainInfo templatedTerrainInfo,
IEnumerable startTypes,
IEnumerable innerTypes,
IEnumerable endTypes)
{
var templateSegments = new List();
foreach (var templateInfo in templatedTerrainInfo.Templates.Values.OrderBy(tti => tti.Id))
foreach (var segment in templateInfo.Segments)
{
if (startTypes.Any(segment.HasStartType) &&
innerTypes.Any(segment.HasInnerType) &&
endTypes.Any(segment.HasEndType))
{
templateSegments.Add(segment);
}
}
return templateSegments.ToArray();
}
///
/// Returns all possible templates that could be layed, ordered by template id.
///
public IEnumerable PossibleTemplates()
{
var templates = new List();
var segments = Start.Union(Inner).Union(End).ToHashSet();
foreach (var template in TemplatedTerrainInfo.Templates.Values.OrderBy(tti => tti.Id))
if (template.Segments.Any(segment => segments.Contains(segment)))
templates.Add(template);
return templates;
}
///
/// Returns all possible tiles that could be layed, ordered by template id, tile index.
///
public IEnumerable PossibleTiles()
{
var tiles = new List();
foreach (var template in PossibleTemplates())
for (var index = 0; index < template.TilesCount; index++)
if (template[index] != null)
tiles.Add(new TerrainTile(template.Id, (byte)index));
return tiles;
}
}
public Map Map;
///
///
/// Target point sequence to fit TemplateSegments to. Whether these CPos positions
/// represent cell corners or cell centers is dependent on the system used by the path's
/// PermittedSegments' TemplateSegments.
///
///
/// If null, Tiling will be a no-op. If non-null, must have at least two points.
///
///
/// A loop must have the start and end points equal.
///
///
public CPos[] Points;
///
/// Maximum permitted Chebychev distance that layed TemplateSegments may be from the
/// specified points.
///
public int MaxDeviation;
///
/// Determines how much corner-cutting is allowed.
/// A value of zero will result in a value being derived from MaxDeviation.
///
public int MaxSkip;
///
/// Increases separation between permitted tiling regions of different parts of the path.
///
public int MinSeparation;
///
/// Stores start type and direction.
///
public Terminal Start;
///
/// Stores end type and direction.
///
public Terminal End;
public PermittedSegments Segments;
/// Whether the start and end points are the same.
public bool IsLoop
{
get => Points != null && Points[0] == Points[^1];
}
public TilingPath(
Map map,
CPos[] points,
int maxDeviation,
string startType,
string endType,
PermittedSegments permittedTemplates)
{
Map = map;
Points = points;
MaxDeviation = maxDeviation;
MaxSkip = 0;
MinSeparation = 0;
Start = new Terminal(startType, null);
End = new Terminal(endType, null);
Segments = permittedTemplates;
}
sealed class TilingSegment
{
public readonly TerrainTemplateInfo TemplateInfo;
public readonly TemplateSegment TemplateSegment;
public readonly int StartTypeId;
public readonly int EndTypeId;
public readonly CVec Offset;
public readonly CVec Moves;
public readonly CVec[] RelativePoints;
public readonly int[] Directions;
public readonly int[] DirectionMasks;
public readonly int[] ReverseDirectionMasks;
public TilingSegment(TerrainTemplateInfo templateInfo, TemplateSegment templateSegment, int startId, int endId)
{
TemplateInfo = templateInfo;
TemplateSegment = templateSegment;
StartTypeId = startId;
EndTypeId = endId;
Offset = templateSegment.Points[0];
Moves = templateSegment.Points[^1] - Offset;
RelativePoints = templateSegment.Points
.Select(p => p - templateSegment.Points[0])
.ToArray();
Directions = new int[RelativePoints.Length];
DirectionMasks = new int[RelativePoints.Length];
ReverseDirectionMasks = new int[RelativePoints.Length];
// Last point has no direction.
Directions[^1] = Direction.None;
DirectionMasks[^1] = 0;
ReverseDirectionMasks[^1] = 0;
for (var i = 0; i < RelativePoints.Length - 1; i++)
{
var direction = Direction.FromCVec(RelativePoints[i + 1] - RelativePoints[i]);
if (direction == Direction.None)
throw new ArgumentException("TemplateSegment has duplicate points in sequence");
Directions[i] = direction;
DirectionMasks[i] = 1 << direction;
ReverseDirectionMasks[i] = 1 << Direction.Reverse(direction);
}
}
}
///
///
/// Attempt to tile the given path onto a map.
///
///
/// If the path could be tiled, returns the sequence of points actually traversed by the
/// chosen TemplateSegments. Returns null if the path could not be tiled within constraints.
///
///
public CPos[] Tile(MersenneTwister random)
{
// This is essentially a Dijkstra's algorithm best-first search.
//
// The search is performed over a 3-dimensional space: (x, y, connection type).
// Connection types correspond to the .Start or .End values of TemplateSegments.
//
// The best found costs of the nodes in this space are stored as an array of matrices.
// There is a matrix for each possible connection type, and each matrix stores the
// (current) best costs at the (x, y) locations for that given connection type.
//
// The directed edges between the nodes of this 3-dimensional space are defined by the
// TemplateSegments within the permitted set of templates. For example, a segment
// defined as
//
// Segment:
// Start: Beach.L
// End: Beach.D
// Points: 3,1, 2,1, 2,2, 2,3
//
// may connect a node from (10, 10) in the "Beach.L" matrix to node (9, 12) in the
// "Beach.D" matrix. (The overall point displacement is (2,3) - (3,1) = (-1, +2))
//
// The cost of a transition/link/edge between nodes is defined by how well the
// template segment fits the path (how little "deviation" is accumulates). However, in
// order for a transition to be allowed at all, it must satisfy some constraints:
//
// - It must not regress backward along the path (but no immediate progress is OK).
// - It must not deviate at any point in the segment beyond MaxDeviation from the path.
// - It must not skip to much later path points (which may be within MaxDeviation).
//
// Progress is measured as a combo of both the earliest and latest closest path points.
//
// The search is conducted from the path start node until the best possible cost of
// the end node is confirmed. This also populates possible intermediate nodes' costs.
//
// Then, from the end node, it works backwards. It finds any (random) suitable template
// segment which connects back to a previous node where the difference in cost is
// that of the template segment's cost, implying that that previous node is on an
// optimal path towards the end node. This process repeats until the start node is
// reached, painting templates along the way.
//
// Note that this algorithm makes a few (reasonable) assumptions about the shapes of
// templates, such as that they don't individually snake around too much. The actual
// tiles of a template are ignored during the search, with only the segment being used
// to calculate transition cost and validity.
if (Points == null)
return null;
var start = Start;
var end = End;
start.Direction ??= Direction.FromCVec(Points[1] - Points[0]);
end.Direction ??= Direction.FromCVec(IsLoop ? Points[1] - Points[0] : Points[^1] - Points[^2]);
var maxSkip = MaxSkip > 0 ? MaxSkip : (2 * MaxDeviation + 1);
var scanRange = MaxDeviation + MinSeparation;
var minPoint = new CPos(
Points.Min(p => p.X) - scanRange,
Points.Min(p => p.Y) - scanRange);
var maxPoint = new CPos(
Points.Max(p => p.X) + scanRange,
Points.Max(p => p.Y) + scanRange);
var points = Points
.Select(point => point - minPoint)
.ToArray();
var isLoop = IsLoop;
// grid points (not squares), so these are offset 0.5 from tile centers.
var size = new int2(1 + maxPoint.X - minPoint.X, 1 + maxPoint.Y - minPoint.Y);
var sizeXY = size.X * size.Y;
const int OverDeviation = int.MaxValue;
const int InvalidProgress = int.MaxValue;
// How far away from the path this point is.
var deviations = new Matrix(size).Fill(OverDeviation);
var lowProgress = new Matrix(size).Fill(InvalidProgress);
var highProgress = new Matrix(size).Fill(InvalidProgress);
var progressModulus = IsLoop ? points.Length - 1 : points.Length;
// The following only apply to looped paths
var forwardProgressLimit = (progressModulus + 1) / 2;
var backwardProgressLimit = progressModulus / 2;
// MinValue essentially means "never match me".
var oppositeProgress =
(IsLoop && forwardProgressLimit == backwardProgressLimit)
? forwardProgressLimit
: int.MinValue;
int Progress(int from, int to)
{
if (IsLoop)
{
var progress = (progressModulus + to - from) % progressModulus;
if (progress < forwardProgressLimit)
return progress;
else if (progress > backwardProgressLimit)
return progress - progressModulus;
else
return oppositeProgress;
}
else
{
return to - from;
}
}
{
var progressSeeds = new List<(int2, int)>();
for (var pointI = 0; pointI < progressModulus; pointI++)
{
var point = points[pointI];
lowProgress[point.X, point.Y] = pointI;
highProgress[point.X, point.Y] = pointI;
progressSeeds.Add((new int2(point.X, point.Y), 0));
}
(int Low, int High) FindLowAndHigh(List values)
{
Debug.Assert(values.Count > 0, "No values");
if (values.Count == 1)
return (values[0], values[0]);
if (IsLoop)
{
if (Progress(values[^1], values[0]) < 0)
return (values[0], values[^1]);
for (var i = 0; i < values.Count - 1; i++)
if (Progress(values[i], values[i + 1]) < 0)
return (values[i + 1], values[i]);
return (InvalidProgress, InvalidProgress);
}
else
{
return (values[0], values[^1]);
}
}
var lows = new List(8);
var highs = new List(8);
int? ProgressFiller(int2 xy, int deviation)
{
if (deviations[xy] != OverDeviation)
return null;
deviations[xy] = deviation;
// low and high progress is preset for 0-deviation.
if (deviation == 0)
return 1;
lows.Clear();
highs.Clear();
for (var i = 0; i < 8; i++)
{
var offset = Direction.Spread8[i];
var neighbor = xy + offset;
if (!deviations.ContainsXY(neighbor) ||
deviations[neighbor] >= deviation ||
lowProgress[neighbor] == InvalidProgress ||
highProgress[neighbor] == InvalidProgress)
{
continue;
}
lows.Add(lowProgress[neighbor]);
highs.Add(highProgress[neighbor]);
}
lows.Sort();
highs.Sort();
(lowProgress[xy], _) = FindLowAndHigh(lows);
(_, highProgress[xy]) = FindLowAndHigh(highs);
if (deviation == scanRange)
return null;
return deviation + 1;
}
MatrixUtils.FloodFill(
size,
progressSeeds,
ProgressFiller,
Direction.Spread8);
var separationSeeds = new List<(int2, int)>();
for (var y = 0; y < size.Y; y++)
for (var x = 0; x < size.X; x++)
{
var xy = new int2(x, y);
var low = lowProgress[xy];
var high = highProgress[xy];
if (low == InvalidProgress ||
high == InvalidProgress)
{
separationSeeds.Add((xy, MinSeparation));
continue;
}
if (MinSeparation > 0)
{
foreach (var offset in Direction.Spread8)
{
var neighbor = xy + offset;
if (!deviations.ContainsXY(neighbor) ||
Math.Abs(Progress(low, lowProgress[neighbor])) > maxSkip ||
Math.Abs(Progress(high, highProgress[neighbor])) > maxSkip)
{
separationSeeds.Add((xy, MinSeparation - 1));
break;
}
}
// Last so that any greater range seeds take priority.
if (deviations[xy] > MaxDeviation)
separationSeeds.Add((xy, 0));
}
}
int? SeparationFiller(int2 xy, int range)
{
if (deviations[xy] == 0 || deviations[xy] == OverDeviation)
return null;
deviations[xy] = OverDeviation;
if (range == 0)
return null;
return range - 1;
}
MatrixUtils.FloodFill(
size,
separationSeeds,
SeparationFiller,
Direction.Spread8);
}
var pathStart = points[0];
var pathEnd = points[^1];
var orderedPermittedSegments = Segments.All.ToImmutableArray();
var permittedSegments = orderedPermittedSegments.ToImmutableHashSet();
const int MaxCost = int.MaxValue;
var segmentTypeToId = new Dictionary();
var segmentsByStart = new List>();
var segmentsByEnd = new List>();
var costs = new List>();
{
void RegisterSegmentType(string type)
{
if (segmentTypeToId.ContainsKey(type))
return;
var newId = segmentTypeToId.Count;
segmentTypeToId.Add(type, newId);
segmentsByStart.Add(new List());
segmentsByEnd.Add(new List());
costs.Add(new Matrix(size).Fill(MaxCost));
}
foreach (var segment in orderedPermittedSegments)
{
var template = Segments.TemplatedTerrainInfo.SegmentsToTemplates[segment];
RegisterSegmentType(segment.Start);
RegisterSegmentType(segment.End);
var startTypeId = segmentTypeToId[segment.Start];
var endTypeId = segmentTypeToId[segment.End];
var tilePathSegment = new TilingSegment(template, segment, startTypeId, endTypeId);
segmentsByStart[startTypeId].Add(tilePathSegment);
segmentsByEnd[endTypeId].Add(tilePathSegment);
}
}
var totalTypeIds = segmentTypeToId.Count;
var priorities = new PriorityArray(totalTypeIds * size.X * size.Y, MaxCost);
void SetPriorityAt(int typeId, CVec pos, int priority)
=> priorities[typeId * sizeXY + pos.Y * size.X + pos.X] = priority;
(int TypeId, CVec Pos, int Priority) GetNextPriority()
{
var index = priorities.GetMinIndex();
var priority = priorities[index];
var typeId = index / sizeXY;
var xy = index % sizeXY;
return (typeId, new CVec(xy % size.X, xy / size.X), priority);
}
var pathStartTypeId = segmentTypeToId[start.SegmentType];
var pathEndTypeId = segmentTypeToId[end.SegmentType];
var innerTypeIds = Segments.Inner
.SelectMany(segment => new[] { segment.Start, segment.End })
.Select(segmentType => segmentTypeToId[segmentType])
.ToImmutableHashSet();
// Lower (closer to zero) costs are better matches.
// MaxScore means totally unacceptable.
int ScoreSegment(TilingSegment segment, CVec from)
{
if (from == pathStart)
{
if (segment.StartTypeId != pathStartTypeId)
return MaxCost;
}
else
{
if (!innerTypeIds.Contains(segment.StartTypeId))
return MaxCost;
}
var to = from + segment.Moves;
if (to == pathEnd)
{
if (segment.EndTypeId != pathEndTypeId)
return MaxCost;
}
else
{
if (!innerTypeIds.Contains(segment.EndTypeId))
return MaxCost;
if (isLoop && lowProgress[from.X, from.Y] > highProgress[to.X, to.Y] && highProgress[to.X, to.Y] != 0)
{
// We've missed the start/end of the loop and have potentially gone past it
// (as far as low and high progress are concerned).
return MaxCost;
}
}
var deviationAcc = 0;
var lowProgressionAcc = 0;
var highProgressionAcc = 0;
var lastPointI = segment.RelativePoints.Length - 1;
for (var pointI = 0; pointI <= lastPointI; pointI++)
{
var point = from + segment.RelativePoints[pointI];
if (!deviations.ContainsXY(point.X, point.Y) || deviations[point.X, point.Y] == OverDeviation)
{
// Point escapes bounds or is in an excluded position.
return MaxCost;
}
if (pointI < lastPointI)
{
var pointNext = from + segment.RelativePoints[pointI + 1];
if (!deviations.ContainsXY(pointNext.X, pointNext.Y) || deviations[pointNext.X, pointNext.Y] == OverDeviation)
{
// Next point escapes bounds or is in an excluded position.
return MaxCost;
}
var lowProgression = Progress(lowProgress[point.X, point.Y], lowProgress[pointNext.X, pointNext.Y]);
var highProgression = Progress(highProgress[point.X, point.Y], highProgress[pointNext.X, pointNext.Y]);
if (Math.Abs(lowProgression) > maxSkip ||
Math.Abs(highProgression) > maxSkip)
{
// Fails skip rule.
return MaxCost;
}
lowProgressionAcc += lowProgression;
highProgressionAcc += highProgression;
}
// pointI > 0 is needed to avoid double-counting the segments's start with the
// previous one's end.
if (pointI > 0)
deviationAcc += deviations[point.X, point.Y];
}
if (lowProgressionAcc < 0 || highProgressionAcc < 0)
{
// Fails progression rule.
return MaxCost;
}
// Satisfies all requirements.
return deviationAcc;
}
void UpdateFrom(CVec from, int fromTypeId, int fromCost)
{
foreach (var segment in segmentsByStart[fromTypeId])
{
var to = from + segment.Moves;
if (to.X < 0 || to.X >= size.X || to.Y < 0 || to.Y >= size.Y)
continue;
// Most likely to fail. Check first.
if (deviations[to.X, to.Y] == OverDeviation)
{
// End escapes bounds.
continue;
}
var segmentCost = ScoreSegment(segment, from);
if (segmentCost == MaxCost)
continue;
var toCost = fromCost + segmentCost;
var toTypeId = segment.EndTypeId;
if (toCost < costs[toTypeId][to.X, to.Y])
{
costs[toTypeId][to.X, to.Y] = toCost;
SetPriorityAt(toTypeId, to, toCost);
}
}
SetPriorityAt(fromTypeId, from, MaxCost);
}
// costs[pathStartTypeId][pathStart.X, pathStart.Y] is preset to
// MaxCost, but we pass in a cost of 0 for the first iteration. We
// leave it like this in case this is a looped path with a shared
// start and end point. We set it to 0 later when tracing back.
UpdateFrom(pathStart, pathStartTypeId, 0);
while (true)
{
var (fromTypeId, from, priority) = GetNextPriority();
if (priority == MaxCost || from == pathEnd)
break;
UpdateFrom(from, fromTypeId, costs[fromTypeId][from.X, from.Y]);
}
// Trace back and update tiles
var resultPoints = new List
{
new(pathEnd.X + minPoint.X, pathEnd.Y + minPoint.Y)
};
(CVec From, int FromTypeId) TraceBackStep(CVec to, int toTypeId, int toCost)
{
var candidates = new List();
foreach (var segment in segmentsByEnd[toTypeId])
{
var from = to - segment.Moves;
if (from.X < 0 || from.X >= size.X || from.Y < 0 || from.Y >= size.Y)
continue;
// Most likely to fail. Check first.
if (deviations[from.X, from.Y] == OverDeviation)
{
// Start escapes bounds.
continue;
}
var segmentCost = ScoreSegment(segment, from);
if (segmentCost == MaxCost)
continue;
var fromCost = toCost - segmentCost;
if (fromCost == costs[segment.StartTypeId][from.X, from.Y])
candidates.Add(segment);
}
Debug.Assert(candidates.Count >= 1, "TraceBack didn't find an original route");
var chosenSegment = candidates[random.Next(candidates.Count)];
var chosenFrom = to - chosenSegment.Moves;
PaintTemplate(Map, chosenFrom - chosenSegment.Offset + minPoint, chosenSegment.TemplateInfo);
// Skip end point as it is recorded in the previous template.
for (var i = chosenSegment.RelativePoints.Length - 2; i >= 0; i--)
{
var point = chosenFrom + chosenSegment.RelativePoints[i] + minPoint;
resultPoints.Add(point);
}
return (chosenFrom, chosenSegment.StartTypeId);
}
{
var to = pathEnd;
var toTypeId = pathEndTypeId;
var bestCost = costs[toTypeId][to.X, to.Y];
if (bestCost == MaxCost)
return null;
// For non-loops, this remained unset at MaxCost. For loops,
// this was the shared start and end point and got set to
// bestCost. We set it to 0 for traceback, but perform the
// first iteration using bestCost. (The opposite of how we
// traced forward.)
costs[pathStartTypeId][pathStart.X, pathStart.Y] = 0;
(to, toTypeId) = TraceBackStep(to, toTypeId, bestCost);
// No need to check direction. If that is an issue, I have bigger problems to worry about.
while (to != pathStart)
(to, toTypeId) = TraceBackStep(to, toTypeId, costs[toTypeId][to.X, to.Y]);
}
// Traced back in reverse, so reverse the reversal.
resultPoints.Reverse();
return resultPoints.ToArray();
}
static void PaintTemplate(Map map, CPos at, TerrainTemplateInfo template)
{
if (template.PickAny)
throw new ArgumentException("PaintTemplate does not expect PickAny");
for (var y = 0; y < template.Size.Y; y++)
for (var x = 0; x < template.Size.X; x++)
{
var i = (byte)(y * template.Size.X + x);
if (template[i] == null)
continue;
var tile = new TerrainTile(template.Id, i);
var mpos = new CPos(at.X + x, at.Y + y).ToMPos(map);
if (map.Tiles.Contains(mpos))
map.Tiles[mpos] = tile;
}
}
///
///
/// Extend the start and end of a path by extensionLength points. The directions of the
/// extensions are based on the overall direction of the outermost inertialRange points.
///
///
/// Returns the object being called on.
///
///
public TilingPath InertiallyExtend(int extensionLength, int inertialRange)
{
Points = InertiallyExtendPathPoints(Points, extensionLength, inertialRange);
return this;
}
///
/// Extend the start and end of a path by extensionLength points. The directions of the
/// extensions are based on the overall direction of the outermost inertialRange points.
///
public static CPos[] InertiallyExtendPathPoints(CPos[] points, int extensionLength, int inertialRange)
{
if (points == null)
return null;
if (inertialRange > points.Length - 1)
inertialRange = points.Length - 1;
var sd = Direction.FromCVecNonDiagonal(points[inertialRange] - points[0]);
var ed = Direction.FromCVecNonDiagonal(points[^1] - points[^(inertialRange + 1)]);
var newPoints = new CPos[points.Length + extensionLength * 2];
for (var i = 0; i < extensionLength; i++)
newPoints[i] = points[0] - Direction.ToCVec(sd) * (extensionLength - i);
Array.Copy(points, 0, newPoints, extensionLength, points.Length);
for (var i = 0; i < extensionLength; i++)
newPoints[extensionLength + points.Length + i] = points[^1] + Direction.ToCVec(ed) * (i + 1);
return newPoints;
}
///
///
/// For map edge-connected (non-loop) starts/ends, the path is extended beyond the edge.
/// For loops or paths which don't connect to the map edge, no change is applied.
///
///
/// For the purposes of this function, the map edges are defined as the borders of a
/// minimal CPos-aligned rectangle covering the entire map. These are not the true edges of
/// a RectangularIsometric map.
///
///
/// Starts/ends which are corner-connected or already extend beyond the edge are unaltered.
///
///
/// Returns the object being called on.
///
///
public TilingPath ExtendEdge(int extensionLength)
{
Points = ExtendEdgePathPoints(Points, CellLayerUtils.CellBounds(Map), extensionLength);
return this;
}
///
///
/// For bounds edge-connected (non-loop) starts/ends, the path is extended beyond the edge.
/// For loops or paths which don't connect to the edges, the input points are returned
/// unaltered.
///
///
/// Starts/ends which are corner-connected or already extend beyond the edge are unaltered.
///
///
public static CPos[] ExtendEdgePathPoints(CPos[] points, Rectangle bounds, int extensionLength)
{
if (points == null)
return null;
if (points[0] == points[^1])
{
// Is a loop.
return points;
}
var left = bounds.Left;
var top = bounds.Top;
var right = bounds.Right;
var bottom = bounds.Bottom;
CPos[] Extend(CPos point)
{
var ox = (point.X == left) ? -1
: (point.X == right) ? 1
: 0;
var oy = (point.Y == top) ? -1
: (point.Y == bottom) ? 1
: 0;
if (ox == oy)
{
// We're either not on an edge or we're at a corner, so don't extend.
return Array.Empty();
}
var offset = new CVec(ox, oy);
var extension = new CPos[extensionLength];
var newPoint = point;
for (var i = 0; i < extensionLength; i++)
{
newPoint += offset;
extension[i] = newPoint;
}
return extension;
}
// Open paths. Extend if beyond edges.
var startExt = Extend(points[0]).Reverse().ToArray();
var endExt = Extend(points[^1]);
// [...startExt, ...points, ...endExt];
var tweaked = new CPos[points.Length + startExt.Length + endExt.Length];
Array.Copy(startExt, 0, tweaked, 0, startExt.Length);
Array.Copy(points, 0, tweaked, startExt.Length, points.Length);
Array.Copy(endExt, 0, tweaked, points.Length + startExt.Length, endExt.Length);
return tweaked;
}
///
///
/// For loops, points are rotated such that the start/end reside in the longest straight.
/// For non-loops, the input points are returned unaltered.
///
///
/// Returns the object being called on.
///
///
public TilingPath OptimizeLoop()
{
Points = OptimizeLoopPathPoints(Points);
return this;
}
///
/// For loops, points are rotated such that the start/end reside in the longest straight.
/// For non-loops, the input points are returned unaltered.
///
public static CPos[] OptimizeLoopPathPoints(CPos[] points)
{
if (points == null)
return null;
if (points[0] == points[^1])
{
// Closed loop. Find the longest straight
// (nrlen excludes the repeated point at the end.)
var nrlen = points.Length - 1;
var prevDim = -1;
var scanStart = -1;
var bestScore = -1;
var bestBend = -1;
var prevBend = -1;
var prevI = 0;
for (var i = 1; ; i++)
{
if (i == nrlen)
i = 0;
var dim = points[i].X == points[prevI].X ? 1 : 0;
if (prevDim != -1 && prevDim != dim)
{
if (scanStart == -1)
{
// This is technically just after the bend. But that's fine.
scanStart = i;
}
else
{
var score = prevI - prevBend;
if (score < 0)
score += nrlen;
if (score > bestScore)
{
bestBend = prevBend;
bestScore = score;
}
if (i == scanStart)
break;
}
prevBend = prevI;
}
prevDim = dim;
prevI = i;
}
var favouritePoint = (bestBend + (bestScore >> 1)) % nrlen;
// Repeat the start at the end.
// [...points.slice(favouritePoint, nrlen), ...points.slice(0, favouritePoint + 1)];
var tweaked = new CPos[points.Length];
Array.Copy(points, favouritePoint, tweaked, 0, nrlen - favouritePoint);
Array.Copy(points, 0, tweaked, nrlen - favouritePoint, favouritePoint + 1);
return tweaked;
}
else
{
return points;
}
}
///
///
/// Shrink a path by a given amount at both ends. If the number of points in the path drops
/// below minimumLength, the path is nullified.
///
///
/// If a loop is provided, the path is not shrunk, but the minimumLength requirement still
/// holds.
///
///
/// Returns the object being called on.
///
///
public TilingPath Shrink(int shrinkBy, int minimumLength)
{
Points = ShrinkPathPoints(Points, shrinkBy, minimumLength);
return this;
}
///
///
/// Shrink a path by a given amount at both ends. If the number of points in the path drops
/// below minimumLength, null is returned.
///
///
/// If a loop is provided, the path is not shrunk, but the minimumLength requirement still
/// holds.
///
///
public static CPos[] ShrinkPathPoints(CPos[] points, int shrinkBy, int minimumLength)
{
if (points == null)
return null;
if (minimumLength <= 1)
throw new ArgumentException("minimumLength must be greater than 1");
if (points[0] == points[^1])
{
// Loop.
if (points.Length < minimumLength)
return null;
return points[0..^0];
}
if (points.Length < shrinkBy * 2 + minimumLength)
return null;
return points[shrinkBy..(points.Length - shrinkBy)];
}
///
///
/// Takes a path and normalizes its progression direction around the map center.
/// Normalized but opposing paths rotate around the center in the same direction.
///
///
/// The measureFromCenter function must convert CVec positions to WVec offsets from the map
/// center.
///
///
public TilingPath ChirallyNormalize(Func measureFromCenter)
{
Points = ChirallyNormalizePathPoints(Points, measureFromCenter);
return this;
}
///
///
/// Takes a path and normalizes its progression direction around the map center.
/// Normalized but opposing paths rotate around the center in the same direction.
///
///
/// The measureFromCenter function must convert CVec positions to WVec offsets from the map
/// center.
///
///
public static CPos[] ChirallyNormalizePathPoints(CPos[] points, Func measureFromCenter)
{
if (points == null || points.Length < 2)
return points;
var normalized = (CPos[])points.Clone();
var start = points[0];
var end = points[^1];
if (start == end)
{
// Is loop
start = points[1];
end = points[^2];
}
bool ShouldReverse(CPos start, CPos end)
{
var v1 = measureFromCenter(start);
var v2 = measureFromCenter(end);
// Rotation around center?
var crossProd = v1.X * v2.Y - v2.X * v1.Y;
if (crossProd != 0)
return crossProd < 0;
// Distance from center?
var r1 = v1.X * v1.X + v1.Y * v1.Y;
var r2 = v2.X * v2.X + v2.Y * v2.Y;
if (r1 != r2)
return r1 < r2;
// Absolute angle
return v1.Y == v2.Y ? v1.X > v2.X : v1.Y > v2.Y;
}
if (ShouldReverse(start, end))
Array.Reverse(normalized);
return normalized;
}
///
///
/// Retains paths which have no points in common with earlier (previous and retained) paths
/// from the input.
///
///
/// The underlying point sequences are NOT cloned.
///
///
/// All input sequences must be non-null.
///
///
public static CPos[][] RetainDisjointPaths(IEnumerable inputs)
{
var outputs = new List();
var lookup = new HashSet();
foreach (var points in inputs)
{
var retain = true;
foreach (var point in points)
{
if (lookup.Contains(point))
{
retain = false;
break;
}
}
if (retain)
{
outputs.Add(points);
foreach (var point in points)
lookup.Add(point);
}
}
return outputs.ToArray();
}
}
}