/// <summary> /// Rotate the grid counter-clockwise. /// </summary> /// <param name="this">(<see langword="this"/> parameter) The grid.</param> /// <returns>The result.</returns> public static Grid RotateCounterClockwise(this IReadOnlyGrid @this) { var result = @this.Clone(); for (int i = 0; i < 81; i++) { int z = CounterClockwiseTable[i]; short temp = result.GetMask(i); result.SetMask(i, result.GetMask(z)); result.SetMask(z, temp); } return(result); }
/// <summary> /// Indicates whether the specified grid contains the digit. /// </summary> /// <param name="grid">The grid.</param> /// <param name="digit">The digit.</param> /// <param name="result">(<see langword="out"/> parameter) The result.</param> /// <returns>A <see cref="bool"/> value.</returns> public bool ContainsDigit(IReadOnlyGrid grid, int digit, out GridMap result) { result = GridMap.Empty; foreach (int cell in Map) { if ((grid.GetCandidatesReversal(cell) >> digit & 1) != 0) { result.Add(cell); } } return(result.IsNotEmpty); }
public static IEnumerable <Point2> TilesOfType(this IReadOnlyGrid <TileType> map, TileType type) { for (var y = 0; y < map.Height; y++) { for (var x = 0; x < map.Width; x++) { if (map[x, y] == type) { yield return(new Point2(x, y)); } } } }
/// <inheritdoc/> public override void GetAll(IBag <TechniqueInfo> accumulator, IReadOnlyGrid grid) { if (EmptyMap.Count < 4) { // SdC needs at least 4 cells like: // abc abd | ab // cd | return; } var list = new List <GridMap>(4); foreach (bool cannibalMode in stackalloc[] { false, true })
/// <summary> /// To check if the puzzle is diamond or not. /// </summary> /// <param name="this">(<see langword="this"/> parameter) The puzzle to check.</param> /// <returns>A <see cref="bool"/> value indicating that.</returns> public static bool IsDiamond(this IReadOnlyGrid @this) { // Using a faster solver to check the grid is unique or not. if (@this.IsValid(out _)) { var result = new ManualSolver().Solve(@this); var(er, pr, dr) = (result.MaxDifficulty, result.PearlDifficulty, result.DiamondDifficulty); return(er == pr && er == dr); } else { // The puzzle does not have unique solution, neither pearl nor diamond one. return(false); } }
/// <summary> /// To check if a puzzle has only one solution or not. /// </summary> /// <param name="this">(<see langword="this"/> parameter) The puzzle to check.</param> /// <param name="solutionIfValid"> /// (<see langword="out"/> parameter) The solution if the puzzle is valid; /// otherwise, <see langword="null"/>. /// </param> /// <returns>A <see cref="bool"/> value indicating that.</returns> public static bool IsValid(this IReadOnlyGrid @this, [NotNullWhen(true)] out IReadOnlyGrid?solutionIfValid) { solutionIfValid = null; if (new BitwiseSolver().CheckValidity(@this.ToString(), out string?solution) || new SukakuBitwiseSolver().CheckValidity(@this.ToString("~"), out solution)) { solutionIfValid = Grid.Parse(solution); return(true); } else { return(false); } }
/// <inheritdoc/> public override void GetAll(IBag <TechniqueInfo> accumulator, IReadOnlyGrid grid) { var r = (Span <int>) stackalloc int[2]; foreach (var((baseSet, coverSet), (a, b, c)) in IntersectionMaps) { if (!EmptyMap.Overlaps(c)) { continue; } short m1 = BitwiseOrMasks(grid, a); short m2 = BitwiseOrMasks(grid, b); short m3 = BitwiseOrMasks(grid, c); short m = (short)(m3 & (m1 ^ m2)); if (m == 0) { continue; } foreach (int digit in m.GetAllSets()) { GridMap elimMap; (r[0], r[1], elimMap) = a.Overlaps(CandMaps[digit]) ? (coverSet, baseSet, a & CandMaps[digit]) : (baseSet, coverSet, b & CandMaps[digit]); if (elimMap.IsEmpty) { continue; } var conclusions = new List <Conclusion>(); foreach (int cell in elimMap) { conclusions.Add(new Conclusion(Elimination, cell, digit)); } var candidateOffsets = new List <(int, int)>(); foreach (int cell in c& CandMaps[digit]) { candidateOffsets.Add((0, cell * 9 + digit)); } accumulator.Add( new LcTechniqueInfo( conclusions, views: new[]
/// <summary> /// Mirror top-bottom the grid. /// </summary> /// <param name="this">(<see langword="this"/> parameter) The grid.</param> /// <returns>The result grid.</returns> public static Grid MirrorTopBottom(this IReadOnlyGrid @this) { var result = @this.Clone(); for (int i = 0; i < 9; i++) { for (int j = 0; j < 4; j++) { short temp = result.GetMask(i * 9 + j); result.SetMask(i * 9 + j, result.GetMask((8 - i) * 9 + j)); result.SetMask((8 - i) * 9 + j, temp); } } return(result); }
/// <summary> /// Mirror diagonal the grid. /// </summary> /// <param name="this">(<see langword="this"/> parameter) The grid.</param> /// <returns>The result grid.</returns> public static Grid MirrorDiagonal(this IReadOnlyGrid @this) { var result = @this.Clone(); for (int i = 1; i < 9; i++) { for (int j = 0; j < i; j++) { short temp = result.GetMask(i * 9 + j); result.SetMask(i * 9 + j, result.GetMask(j * 9 + i)); result.SetMask(j * 9 + i, temp); } } return(result); }
public void Test_Set_BusinessObjectCollectionOnGrid_NoOfRows() { //---------------Set up test pack------------------- MyBO.LoadDefaultClassDef(); BusinessObjectCollection <MyBO> col = CreateCollectionWith_4_Objects(); IReadOnlyGrid readOnlyGrid = GetControlFactory().CreateReadOnlyGrid(); DisposeOnTearDown(readOnlyGrid); AddControlToForm(readOnlyGrid); SetupGridColumnsForMyBo(readOnlyGrid); //---------------Execute Test ---------------------- readOnlyGrid.BusinessObjectCollection = col; //---------------Test Result ----------------------- Assert.AreEqual(4, readOnlyGrid.Rows.Count); //---------------Tear Down ------------------------- }
/// <summary> /// Mirror anti-diagonal the grid. /// </summary> /// <param name="this">(<see langword="this"/> parameter) The grid.</param> /// <returns>The result grid.</returns> public static Grid MirrorAntidiagonal(this IReadOnlyGrid @this) { var result = @this.Clone(); for (int i = 0; i < 9; i++) { for (int j = 0; j < 8 - i; j++) { short temp = result.GetMask(i * 9 + j); result.SetMask(i * 9 + j, result.GetMask((8 - j) * 9 + (8 - i))); result.SetMask((8 - j) * 9 + (8 - i), temp); } } return(result); }
/// <summary> /// Mirror left-right the grid. /// </summary> /// <param name="this">(<see langword="this"/> parameter) The grid.</param> /// <returns>The result grid.</returns> public static Grid MirrorLeftRight(this IReadOnlyGrid @this) { var result = @this.Clone(); for (int i = 0; i < 4; i++) { for (int j = 0; j < 9; j++) { short temp = result.GetMask(i * 9 + j); result.SetMask(i * 9 + j, result.GetMask(i * 9 + (8 - j))); result.SetMask(i * 9 + (8 - j), temp); } } return(result); }
private bool RecordTechnique( List <TechniqueInfo> steps, TechniqueInfo step, IReadOnlyGrid grid, Grid cloneation, Stopwatch stopwatch, IBag <IReadOnlyGrid> stepGrids, [NotNullWhen(true)] out AnalysisResult?result) { bool needAdd = false; foreach (var(t, c, d) in step.Conclusions) { switch (t) { case Assignment when cloneation.GetStatus(c) == CellStatus.Empty: case Elimination when cloneation.Exists(c, d) is true: { needAdd = true; goto Label_Determine; } } } Label_Determine: if (needAdd) { stepGrids.Add(cloneation.Clone()); step.ApplyTo(cloneation); steps.Add(step); if (cloneation.HasSolved) { result = new AnalysisResult( puzzle: grid, solverName: SolverName, hasSolved: true, solution: cloneation, elapsedTime: stopwatch.Elapsed, solvingList: steps, additional: null, stepGrids); return(true); } } result = null; return(false); }
protected override int SelectedIndex(IBOColSelectorControl colSelector) { IReadOnlyGrid gridSelector = ((IReadOnlyGridControl)colSelector).Grid; IDataGridViewRow currentRow = null; if (gridSelector.SelectedRows.Count > 0) { currentRow = gridSelector.SelectedRows[0]; } if (currentRow == null) { return(-1); } return(gridSelector.Rows.IndexOf(currentRow)); }
/// <summary> /// To check the validity of all conclusions. /// </summary> /// <param name="solution">The solution.</param> /// <param name="conclusions">The conclusions.</param> /// <returns>A <see cref="bool"/> indicating that.</returns> private static bool CheckConclusionsValidity(IReadOnlyGrid solution, IEnumerable <Conclusion> conclusions) { foreach (var(t, c, d) in conclusions) { int digit = solution[c]; switch (t) { case Assignment when digit != d: case Elimination when digit == d: { return(false); } } } return(true); }
public static IEnumerable <Point2> NeighboursOfType(this IReadOnlyGrid <TileType> map, Point2 middle, TileType type, bool cornersIncluded = false) { for (var y = -1; y <= 1; y++) { for (var x = -1; x <= 1; x++) { if (!cornersIncluded && Math.Abs(x) == Math.Abs(y)) { continue; } if (map.At(x, y) == type) { yield return(new Point2(middle.X + x, middle.Y + y)); } } } }
/// <summary> /// Search for all W-Wings. /// </summary> /// <param name="result">The result accumulator.</param> /// <param name="grid">The grid.</param> /// <returns>All technique information instances.</returns> public static void TakeAllWWings(IBag <TechniqueInfo> result, IReadOnlyGrid grid) { if (BivalueMap.Count < 2) { return; } // Iterate on each cells. for (int c1 = 0; c1 < 72; c1++) { if (!BivalueMap[c1] || !EmptyMap[c1]) { continue; } // Iterate on each cells which are not peers in 'c1'. int[] digits = grid.GetCandidatesReversal(c1).GetAllSets().ToArray(); foreach (int c2 in BivalueMap - new GridMap(c1)) { if (c2 < c1 || grid.GetCandidatesReversal(c1) != grid.GetCandidatesReversal(c2)) { continue; } var intersection = new GridMap(c1, false) & new GridMap(c2, false); if (!EmptyMap.Overlaps(intersection)) { continue; } for (int region = 9; region < 27; region++) { if (region < 18 && ( GetRegion(c1, RegionLabel.Row) == region || GetRegion(c2, RegionLabel.Row) == region) || region >= 18 && ( GetRegion(c1, RegionLabel.Column) == region || GetRegion(c2, RegionLabel.Column) == region)) { continue; } SearchWWingByRegions(result, grid, digits, region, c1, c2, intersection); } } } }
/// <summary> /// Searches W-Wing technique by region. /// </summary> /// <param name="result">The result.</param> /// <param name="grid">The grid.</param> /// <param name="digits">The digits.</param> /// <param name="region">The region.</param> /// <param name="c1">Cell 1.</param> /// <param name="c2">Cell 2.</param> /// <param name="intersection">The intersection.</param> private static void SearchWWingByRegions( IBag <TechniqueInfo> result, IReadOnlyGrid grid, int[] digits, int region, int c1, int c2, GridMap intersection) { for (int i = 0; i < 2; i++) { int digit = digits[i]; if (!grid.IsBilocationRegion(digit, region, out short mask)) { continue; } int pos1 = mask.FindFirstSet(), pos2 = mask.GetNextSet(pos1); int bridgeStart = RegionCells[region][pos1], bridgeEnd = RegionCells[region][pos2]; if (c1 == bridgeStart || c2 == bridgeStart || c1 == bridgeEnd || c2 == bridgeEnd) { continue; }
public void TestCreateGridBase() { //---------------Set up test pack------------------- //---------------Execute Test ---------------------- IControlHabanero grid = GetControlFactory().CreateReadOnlyGrid(); DisposeOnTearDown(grid); ////---------------Test Result ----------------------- Assert.IsNotNull(grid); Assert.IsTrue(grid is IReadOnlyGrid); IReadOnlyGrid readOnlyGrid = (IReadOnlyGrid)grid; readOnlyGrid.ReadOnly = true; readOnlyGrid.AllowUserToAddRows = false; readOnlyGrid.AllowUserToDeleteRows = false; //Need interfact to test selectionMode not sure if worth it. //see when implementing for windows. // readOnlyGrid.SelectionMode = Gizmox.WebGUI.Forms.DataGridViewSelectionMode.FullRowSelect; }
/// <inheritdoc/> public override void GetAll(IBag <TechniqueInfo> accumulator, IReadOnlyGrid grid) { var templates = GetInvalidPos(grid); for (int digit = 0; digit < 9; digit++) { var template = templates[digit]; if (template.IsEmpty) { continue; } accumulator.Add( new PomTechniqueInfo( conclusions: new List <Conclusion>(from cell in template select new Conclusion(Elimination, cell, digit)), views: View.DefaultViews)); } }
public void Test_GetBusinessObjectAtRow_WhenSetViaCustomLoad_ShouldRetBO() { //---------------Set up test pack------------------- MyBO.LoadDefaultClassDef(); BusinessObjectCollection <MyBO> col = CreateCollectionWith_4_Objects(); IReadOnlyGrid readOnlyGrid = GetControlFactory().CreateReadOnlyGrid(); DisposeOnTearDown(readOnlyGrid); AddControlToForm(readOnlyGrid); SetupGridColumnsForMyBo(readOnlyGrid); readOnlyGrid.BusinessObjectCollection = col; //---------------Assert Precondition---------------- Assert.AreEqual(4, readOnlyGrid.Rows.Count); //---------------Execute Test ---------------------- var actualBO = readOnlyGrid.GetBusinessObjectAtRow(1); //---------------Test Result ----------------------- Assert.AreSame(col[1], actualBO); }
protected override void SetSelectedIndex(IBOColSelectorControl colSelector, int index) { int count = 0; IReadOnlyGrid readOnlyGrid = ((IReadOnlyGridControl)colSelector).Grid; foreach (IDataGridViewRow row in readOnlyGrid.Rows) { if (row == null) { continue; //This is done to stop the Pragma warning. } if (count == index) { IBusinessObject businessObjectAtRow = readOnlyGrid.GetBusinessObjectAtRow(count); colSelector.SelectedBusinessObject = businessObjectAtRow; } count++; } }
/// <summary> /// Search all backdoors whose level is lower or equals than the /// specified depth. /// </summary> /// <param name="grid">The grid.</param> /// <param name="depth"> /// The depth you want to search for. The depth value must be between 0 and 3. /// where value 0 is for searching for assignments. /// </param> /// <returns>All backdoors.</returns> /// <exception cref="SudokuRuntimeException"> /// Throws when the specified grid is invalid. /// </exception> public IEnumerable <IReadOnlyList <Conclusion> > SearchForBackdoors(IReadOnlyGrid grid, int depth) { if (depth < 0 || depth > 3) { return(Array.Empty <IReadOnlyList <Conclusion> >()); } if (!grid.IsValid(out _)) { throw new SudokuRuntimeException(); } var result = new List <IReadOnlyList <Conclusion> >(); for (int dep = 0; dep <= depth; dep++) { SearchForBackdoors(result, grid, dep); } return(result); }
void DrawInternal(TextWriter stream, IReadOnlyGrid <TCell> grid, IEnumerable <Point> possibleMovePoints, TCell owner) { var possibleMovePointSet = new HashSet <Point>(possibleMovePoints); for (int y = 0; y < grid.Height; y++) { for (int x = 0; x < grid.Width; x++) { if (possibleMovePointSet.Contains(new Point(x, y))) { stream.Write(possibleMovePointCharacter); } else { var cell = grid.GetCell(x, y); stream.Write(cellCharacterMap[cell]); } } stream.WriteLine(); } }
/// <inheritdoc/> public override AnalysisResult Solve(IReadOnlyGrid grid) { _grid = grid; int[] gridValues = grid.ToArray(); int[]? result = null; int solutionsCount = 0; var stopwatch = new Stopwatch(); try { stopwatch.Start(); BacktrackinglySolve(ref solutionsCount, ref result, gridValues, 0); stopwatch.Stop(); return(new AnalysisResult( puzzle: grid, solverName: SolverName, hasSolved: true, solution: Grid.CreateInstance(result ?? throw new NoSolutionException(grid)), elapsedTime: stopwatch.Elapsed, solvingList: null, additional: null, stepGrids: null)); } catch (Exception ex) { stopwatch.Stop(); return(new AnalysisResult( puzzle: grid, solverName: SolverName, hasSolved: false, solution: null, elapsedTime: stopwatch.Elapsed, solvingList: null, additional: ex.Message, stepGrids: null)); } }
/// <inheritdoc/> public override AnalysisResult Solve(IReadOnlyGrid grid) { if (grid.IsValid(out var solution, out bool?sukaku)) { // Solve the puzzle. try { return(AnalyzeDifficultyStrictly ? SolveWithStrictDifficultyRating( grid, grid.Clone(), new List <TechniqueInfo>(), solution, sukaku.Value) : SolveNaively(grid, grid.Clone(), new List <TechniqueInfo>(), solution, sukaku.Value)); } catch (WrongHandlingException ex) { return(new AnalysisResult( puzzle: grid, solverName: SolverName, hasSolved: false, solution: null, elapsedTime: TimeSpan.Zero, solvingList: null, additional: ex.Message, stepGrids: null)); } } else { return(new AnalysisResult( puzzle: grid, solverName: SolverName, hasSolved: false, solution: null, elapsedTime: TimeSpan.Zero, solvingList: null, additional: "The puzzle does not have a unique solution (multiple solutions or no solution).", stepGrids: null)); } }
/// <summary> /// Get all invalid positions. /// </summary> /// <param name="grid">The grid.</param> /// <returns>The 9 maps for invalid positions of each digit.</returns> private static GridMap[] GetInvalidPos(IReadOnlyGrid grid) { var result = new GridMap[9]; var invalidPos = new GridMap[9]; var mustPos = new GridMap[9]; for (int digit = 0; digit < 9; digit++) { for (int cell = 0; cell < 81; cell++) { if ((grid.GetCandidatesReversal(cell) >> digit & 1) == 0) { invalidPos[digit].Add(cell); } else if (grid[cell] == digit) { mustPos[digit].Add(cell); } } } for (int digit = 0; digit < 9; digit++) { foreach (var map in GetTemplates()) { if ((mustPos[digit] - map).IsNotEmpty || invalidPos[digit].Overlaps(map)) { continue; } result[digit] |= map; } result[digit] = CandMaps[digit] - result[digit]; } return(result); }
/// <inheritdoc/> public override AnalysisResult Solve(IReadOnlyGrid grid) { var stopwatch = new Stopwatch(); stopwatch.Start(); var results = SolveStrings(grid.ToString("0")); stopwatch.Stop(); return(results.Count switch { 0 => throw new NoSolutionException(grid), 1 => new AnalysisResult( puzzle: grid, solverName: SolverName, hasSolved: true, solution: Grid.Parse(results[0]), elapsedTime: stopwatch.Elapsed, solvingList: null, additional: null, stepGrids: null), _ => throw new MultipleSolutionsException(grid) });
public MultipleRelationshipReadOnlyGridMapper(IReadOnlyGrid readOnlyGrid, string relationshipName) : this(readOnlyGrid, relationshipName, false, GlobalUIRegistry.ControlFactory) { }
private ReadOnlyGridMapper CreateReadOnlyGridMapper(IReadOnlyGrid readOnlyGrid, string propName) { return new ReadOnlyGridMapper(readOnlyGrid, propName, false, GetControlFactory()); }
public static Grid Transpose(this IReadOnlyGrid @this) => @this.MirrorDiagonal();
partial void CheckType1( IList <UrTechniqueInfo> accumulator, IReadOnlyGrid grid, int[] urCells, bool arMode, short comparer, int d1, int d2, int cornerCell, GridMap otherCellsMap) { // ↓ cornerCell // (abc) ab // ab ab // Get the summary mask. short mask = 0; foreach (int cell in otherCellsMap) { mask |= grid.GetCandidatesReversal(cell); } if (mask != comparer) { return; } // Type 1 found. Now check elimination. var conclusions = new List <Conclusion>(); if (grid.Exists(cornerCell, d1) is true) { conclusions.Add(new Conclusion(Elimination, cornerCell, d1)); } if (grid.Exists(cornerCell, d2) is true) { conclusions.Add(new Conclusion(Elimination, cornerCell, d2)); } if (conclusions.Count == 0) { return; } var candidateOffsets = new List <(int, int)>(); foreach (int cell in otherCellsMap) { foreach (int digit in grid.GetCandidatesReversal(cell).GetAllSets()) { candidateOffsets.Add((0, cell * 9 + digit)); } } if (!_allowIncompletedUr && (candidateOffsets.Count != 6 || conclusions.Count != 2)) { return; } accumulator.Add( new UrType1TechniqueInfo( conclusions, views: new[] { new View( cellOffsets: arMode ? GetHighlightCells(urCells) : null, candidateOffsets: arMode ? null : candidateOffsets, regionOffsets: null, links: null) }, digit1: d1, digit2: d2, cells: urCells, isAr: arMode)); }
/// <summary> /// Constructor for the mapper. /// </summary> /// <param name="ctl">The IEditableGridControl</param> /// <param name="relationshipName">This is the relationship name to use - this relationship must be a multiple relationship and exist on the BusinessObject</param> /// <param name="isReadOnly">Whether the editable grid should be read only or not. Ignored</param> /// <param name="factory">The control factory to use</param> public ReadOnlyGridMapper(IReadOnlyGrid ctl, string relationshipName, bool isReadOnly, IControlFactory factory) : base(ctl, relationshipName, isReadOnly, factory) { _readOnlyGrid = ctl; _gridInitialiser = new GridBaseInitialiser(ctl, factory); }
private static MultipleRelationshipReadOnlyGridMapper CreateMultipleRelationshipReadOnlyGridMapper(IReadOnlyGrid grid, string propName) { return new MultipleRelationshipReadOnlyGridMapper(grid, propName, false, GetControlFactory()); }
public MultipleRelationshipReadOnlyGridMapper(IReadOnlyGrid readOnlyGrid, string relationshipName, bool isReadOnly, IControlFactory factory) : base(readOnlyGrid, relationshipName, isReadOnly, factory) { Grid = readOnlyGrid; }
private static void DrawTileHighlights(Canvas canvas, ICmpTilemapRenderer renderer, Point2 origin, IReadOnlyGrid<bool> highlight, ColorRgba fillTint, ColorRgba outlineTint, TileHighlightMode mode, List<Vector2[]> outlineCache = null) { if (highlight.Width == 0 || highlight.Height == 0) return; // Generate strippled line texture if not available yet if (strippledLineTex == null) { PixelData pixels = new PixelData(8, 1); for (int i = 0; i < pixels.Width / 2; i++) pixels[i, 0] = ColorRgba.White; for (int i = pixels.Width / 2; i < pixels.Width; i++) pixels[i, 0] = ColorRgba.TransparentWhite; using (Pixmap pixmap = new Pixmap(pixels)) { strippledLineTex = new Texture(pixmap, TextureSizeMode.Default, TextureMagFilter.Nearest, TextureMinFilter.Nearest, TextureWrapMode.Repeat, TextureWrapMode.Repeat, TexturePixelFormat.Rgba); } } BatchInfo defaultMaterial = new BatchInfo(DrawTechnique.Alpha, canvas.State.Material.MainColor); BatchInfo strippleMaterial = new BatchInfo(DrawTechnique.Alpha, canvas.State.Material.MainColor, strippledLineTex); bool uncertain = (mode & TileHighlightMode.Uncertain) != 0; bool selection = (mode & TileHighlightMode.Selection) != 0; Component component = renderer as Component; Transform transform = component.GameObj.Transform; Tilemap tilemap = renderer.ActiveTilemap; Tileset tileset = tilemap != null ? tilemap.Tileset.Res : null; Vector2 tileSize = tileset != null ? tileset.TileSize : Tileset.DefaultTileSize; Rect localRect = renderer.LocalTilemapRect; // Determine the object's local coordinate system (rotated, scaled) in world space Vector2 worldAxisX = Vector2.UnitX; Vector2 worldAxisY = Vector2.UnitY; MathF.TransformCoord(ref worldAxisX.X, ref worldAxisX.Y, transform.Angle, transform.Scale); MathF.TransformCoord(ref worldAxisY.X, ref worldAxisY.Y, transform.Angle, transform.Scale); Vector2 localOriginPos = tileSize * origin; Vector2 worldOriginPos = localOriginPos.X * worldAxisX + localOriginPos.Y * worldAxisY; canvas.PushState(); { // Configure the canvas so our shapes are properly rotated and scaled canvas.State.TransformHandle = -localRect.TopLeft; canvas.State.TransformAngle = transform.Angle; canvas.State.TransformScale = new Vector2(transform.Scale); // Fill all highlighted tiles that are currently visible { canvas.State.SetMaterial(defaultMaterial); canvas.State.ColorTint = fillTint * ColorRgba.White.WithAlpha(selection ? 0.2f : 0.375f); // Determine tile visibility Vector2 worldTilemapOriginPos = localRect.TopLeft; MathF.TransformCoord(ref worldTilemapOriginPos.X, ref worldTilemapOriginPos.Y, transform.Angle, transform.Scale); TilemapCulling.TileInput cullingIn = new TilemapCulling.TileInput { // Remember: All these transform values are in world space TilemapPos = transform.Pos + new Vector3(worldTilemapOriginPos) + new Vector3(worldOriginPos), TilemapScale = transform.Scale, TilemapAngle = transform.Angle, TileCount = new Point2(highlight.Width, highlight.Height), TileSize = tileSize }; TilemapCulling.TileOutput cullingOut = TilemapCulling.GetVisibleTileRect(canvas.DrawDevice, cullingIn); int renderedTileCount = cullingOut.VisibleTileCount.X * cullingOut.VisibleTileCount.Y; // Draw all visible highlighted tiles { Point2 tileGridPos = cullingOut.VisibleTileStart; Vector2 renderStartPos = worldOriginPos + tileGridPos.X * tileSize.X * worldAxisX + tileGridPos.Y * tileSize.Y * worldAxisY;; Vector2 renderPos = renderStartPos; Vector2 tileXStep = worldAxisX * tileSize.X; Vector2 tileYStep = worldAxisY * tileSize.Y; int lineMergeCount = 0; int totalRects = 0; for (int tileIndex = 0; tileIndex < renderedTileCount; tileIndex++) { bool current = highlight[tileGridPos.X, tileGridPos.Y]; if (current) { // Try to merge consecutive rects in the same line to reduce drawcalls / CPU load bool hasNext = (tileGridPos.X + 1 < highlight.Width) && ((tileGridPos.X + 1 - cullingOut.VisibleTileStart.X) < cullingOut.VisibleTileCount.X); bool next = hasNext ? highlight[tileGridPos.X + 1, tileGridPos.Y] : false; if (next) { lineMergeCount++; } else { totalRects++; canvas.FillRect( transform.Pos.X + renderPos.X - lineMergeCount * tileXStep.X, transform.Pos.Y + renderPos.Y - lineMergeCount * tileXStep.Y, transform.Pos.Z, tileSize.X * (1 + lineMergeCount), tileSize.Y); lineMergeCount = 0; } } tileGridPos.X++; renderPos += tileXStep; if ((tileGridPos.X - cullingOut.VisibleTileStart.X) >= cullingOut.VisibleTileCount.X) { tileGridPos.X = cullingOut.VisibleTileStart.X; tileGridPos.Y++; renderPos = renderStartPos; renderPos += tileYStep * (tileGridPos.Y - cullingOut.VisibleTileStart.Y); } } } } // Draw highlight area outlines, unless flagged as uncertain if (!uncertain) { // Determine the outlines of individual highlighted tile patches if (outlineCache == null) outlineCache = new List<Vector2[]>(); if (outlineCache.Count == 0) { GetTileAreaOutlines(highlight, tileSize, ref outlineCache); } // Draw outlines around all highlighted tile patches canvas.State.SetMaterial(selection ? strippleMaterial : defaultMaterial); canvas.State.ColorTint = outlineTint; foreach (Vector2[] outline in outlineCache) { // For strippled-line display, determine total length of outline if (selection) { float totalLength = 0.0f; for (int i = 1; i < outline.Length; i++) { totalLength += (outline[i - 1] - outline[i]).Length; } canvas.State.TextureCoordinateRect = new Rect(totalLength / strippledLineTex.PixelWidth, 1.0f); } // Draw the outline canvas.DrawPolygon( outline, transform.Pos.X + worldOriginPos.X, transform.Pos.Y + worldOriginPos.Y, transform.Pos.Z); } } // If this is an uncertain highlight, i.e. not actually reflecting the represented action, // draw a gizmo to indicate this for the user. if (uncertain) { Vector2 highlightSize = new Vector2(highlight.Width * tileSize.X, highlight.Height * tileSize.Y); Vector2 highlightCenter = highlightSize * 0.5f; Vector3 circlePos = transform.Pos + new Vector3(worldOriginPos + worldAxisX * highlightCenter + worldAxisY * highlightCenter); float circleRadius = MathF.Min(tileSize.X, tileSize.Y) * 0.2f; canvas.State.SetMaterial(defaultMaterial); canvas.State.ColorTint = outlineTint; canvas.FillCircle( circlePos.X, circlePos.Y, circlePos.Z, circleRadius); } } canvas.PopState(); }
private static void GetTileAreaOutlines(IReadOnlyGrid<bool> tileArea, Vector2 tileSize, ref List<Vector2[]> outlines) { // Initialize the container we'll put our outlines into if (outlines == null) outlines = new List<Vector2[]>(); else outlines.Clear(); // Generate a data structure containing all visible edges TileEdgeMap edgeMap = new TileEdgeMap(tileArea.Width + 1, tileArea.Height + 1); for (int y = 0; y < edgeMap.Height; y++) { for (int x = 0; x < edgeMap.Width; x++) { // Determine highlight state of the four tiles around this node bool topLeft = x > 0 && y > 0 && tileArea[x - 1, y - 1]; bool topRight = x < tileArea.Width && y > 0 && tileArea[x , y - 1]; bool bottomLeft = x > 0 && y < tileArea.Height && tileArea[x - 1, y ]; bool bottomRight = x < tileArea.Width && y < tileArea.Height && tileArea[x , y ]; // Determine which edges are visible if (topLeft != topRight ) edgeMap.AddEdge(new Point2(x, y), new Point2(x , y - 1)); if (topRight != bottomRight) edgeMap.AddEdge(new Point2(x, y), new Point2(x + 1, y )); if (bottomRight != bottomLeft ) edgeMap.AddEdge(new Point2(x, y), new Point2(x , y + 1)); if (bottomLeft != topLeft ) edgeMap.AddEdge(new Point2(x, y), new Point2(x - 1, y )); } } // Traverse edges to form outlines until no more edges are left RawList<Vector2> outlineBuilder = new RawList<Vector2>(); while (true) { // Find the beginning of an outline Point2 current = edgeMap.FindNonEmpty(); if (current.X == -1 || current.Y == -1) break; // Traverse it until no more edges are left while (true) { Point2 next = edgeMap.GetClockwiseNextFrom(current); if (next.X == -1 || next.Y == -1) break; outlineBuilder.Add(next * tileSize); edgeMap.RemoveEdge(current, next); current = next; } // Close the loop by adding the first element again if (outlineBuilder.Count > 0) outlineBuilder.Add(outlineBuilder[0]); // If we have enough vertices, keep the outline for drawing Vector2[] outline = new Vector2[outlineBuilder.Count]; outlineBuilder.CopyTo(outline, 0); outlines.Add(outline); // Reset the outline builder to an empty state outlineBuilder.Clear(); } }