#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(); } } }