From cbcc0783e3df46b527ce849bd8b86bc3a35daae0 Mon Sep 17 00:00:00 2001 From: "Senzaiken\\Uri" Date: Wed, 17 Jun 2026 10:50:28 -0400 Subject: [PATCH] Land Brush: corner-lattice painting and Fix edges repair mode --- CentrED/Map/MapManager.cs | 4 +- CentrED/Tools/LandBrushTool.cs | 217 ++++++++++++++++++++++++++++++++- 2 files changed, 215 insertions(+), 6 deletions(-) diff --git a/CentrED/Map/MapManager.cs b/CentrED/Map/MapManager.cs index 2a5b4346..c8e3c15f 100644 --- a/CentrED/Map/MapManager.cs +++ b/CentrED/Map/MapManager.cs @@ -848,9 +848,9 @@ public Vector3 Unproject(int x, int y, int z) ); } - private bool CanDrawLand(LandObject lo) + public bool CanDrawLand(LandObject lo) { - if(!ShowLand || (lo.Tile.Id <= 2 && !ShowNoDraw)) + if(!ShowLand || (lo.Tile.Id <= 2 && !ShowNoDraw)) return false; return WithinZRange(lo.Tile.Z); } diff --git a/CentrED/Tools/LandBrushTool.cs b/CentrED/Tools/LandBrushTool.cs index 133be1d8..e3a3070d 100644 --- a/CentrED/Tools/LandBrushTool.cs +++ b/CentrED/Tools/LandBrushTool.cs @@ -16,9 +16,11 @@ public class LandBrushTool : BaseTool private bool _fixedZ = false; private int _fixedHeightZ = 0, _randomZ = 0; + private bool _fixEdges = false; + private bool _fixInside = false; private string _activeLandBrushName; private LandBrushManagerWindow _manager => UIManager.GetWindow(); - + public LandBrushTool() { _activeLandBrushName = ProfileManager.ActiveProfile.LandBrush.Keys.FirstOrDefault(""); @@ -36,8 +38,17 @@ internal override void Draw() { _activeLandBrushName = ProfileManager.ActiveProfile.LandBrush.Keys.FirstOrDefault(""); } - + _manager.LandBrushCombo(ref _activeLandBrushName); + ImGui.Checkbox("Fix edges", ref _fixEdges); + ImGui.SetItemTooltip + ("Repair mode: keeps the existing shapes and only corrects the selected brush's transition tiles. Nothing is painted."); + if (_fixEdges) + { + ImGui.Checkbox("Fix inside", ref _fixInside); + ImGui.SetItemTooltip + ("Also redraw the shape's interior with randomized full tiles."); + } ImGui.Checkbox(LangManager.Get(FIXED_Z), ref _fixedZ); if (_fixedZ) { @@ -65,8 +76,16 @@ private sbyte CalculateNewZ(sbyte height) protected override void GhostApply(TileObject? o) { + if (o is not LandObject lo) + return; + + if (_fixEdges) + { + RepairTile(lo.Tile.X, lo.Tile.Y); + return; + } + var defaultTransitionDirection = Direction.Up; - if (o is LandObject lo) { var direction = AddTransistion(lo, defaultTransitionDirection); var offsets = CalculateOffsets(direction); @@ -77,7 +96,7 @@ protected override void GhostApply(TileObject? o) if (Client.IsValidX(newX) && Client.IsValidY(newY)) { var tile = MapManager.LandTiles[newX, newY]; - if (tile != null) + if (tile != null && MapManager.CanDrawLand(tile)) { AddTransistion(tile, valueTuple.Value); } @@ -334,6 +353,196 @@ out string toBrushName return result; } + #region Fix edges (repair mode) + + // A tile's art is determined by which brush owns each of its four diamond corners + // (Up=NW, Right=NE, Down=SE, Left=SW); each corner is shared with three neighbours. + + private static readonly Direction[] LatticeCorners = { Direction.Up, Direction.Right, Direction.Down, Direction.Left }; + private static readonly Direction CornerMask = Direction.Up | Direction.Right | Direction.Down | Direction.Left; + + private static Direction Complement(Direction d) => (Direction)(byte)~(byte)d; + + private static Direction CornerFromOffset(int dx, int dy) => (dx, dy) switch + { + (-1, -1) => Direction.Up, + (1, -1) => Direction.Right, + (1, 1) => Direction.Down, + (-1, 1) => Direction.Left, + _ => Direction.None, + }; + + private ushort EffectiveId(int x, int y) + { + if (!Client.IsValidX(x) || !Client.IsValidY(y)) + return 0; + var lo = MapManager.LandTiles[x, y]; + if (lo == null) + return 0; + if (MapManager.GhostLandTiles.TryGetValue(lo, out var ghost)) + return ghost.Tile.Id; + return lo.Tile.Id; + } + + // Full tiles give from==to; ambiguous ids prefer a pair involving the active brush. + private bool TryGetPair(ushort tileId, out string from, out string to, out Direction dir) + { + from = to = ""; + dir = Direction.None; + if (!_manager.tileToLandBrushNames.TryGetValue(tileId, out var pairs) || pairs.Count == 0) + return false; + var best = pairs[0]; + foreach (var p in pairs) + { + if (p.Item1 == _activeLandBrushName || p.Item2 == _activeLandBrushName) + { + best = p; + break; + } + } + from = best.Item1; + to = best.Item2; + if (from != to) + { + var lb = ProfileManager.ActiveProfile.LandBrush; + if (lb.TryGetValue(from, out var fb) && fb.Transitions.TryGetValue(to, out var lst)) + { + var tr = lst.FirstOrDefault(t => t.TileID == tileId); + if (tr != null) + dir = tr.Direction; + } + } + return true; + } + + private string? TileCornerBrush(int x, int y, Direction corner) + { + var id = EffectiveId(x, y); + if (id == 0 || !TryGetPair(id, out var from, out var to, out var dir)) + return null; + if (from == to) + return from; + return (dir & corner) != Direction.None ? to : from; + } + + // Majority vote of the four tiles sharing the point, tie-broken toward the active brush. + private string? LatticeBrush(int x, int y, Direction corner) + { + var (dx, dy) = corner.Offset(); + Span<(int tx, int ty, Direction c)> shares = stackalloc (int, int, Direction)[4] + { + (x, y, corner), + (x + dx, y, CornerFromOffset(-dx, dy)), + (x, y + dy, CornerFromOffset(dx, -dy)), + (x + dx, y + dy, CornerFromOffset(-dx, -dy)), + }; + var votes = new Dictionary(); + foreach (var (tx, ty, c) in shares) + { + var b = TileCornerBrush(tx, ty, c); + if (b == null) + continue; + votes.TryGetValue(b, out var n); + votes[b] = n + 1; + } + if (votes.Count == 0) + return null; + var max = votes.Values.Max(); + if (votes.TryGetValue(_activeLandBrushName, out var av) && av == max) + return _activeLandBrushName; + return votes.First(kv => kv.Value == max).Key; + } + + // Corner order Up,Right,Down,Left. + private List ValidTiles(string?[] cornerBrushes) + { + var result = new List(); + if (cornerBrushes.Any(b => b == null)) + return result; + var distinct = cornerBrushes.Distinct().ToList(); + var lb = ProfileManager.ActiveProfile.LandBrush; + if (distinct.Count == 1) + { + if (lb.TryGetValue(distinct[0]!, out var b)) + result.AddRange(b.Tiles); + return result; + } + if (distinct.Count > 2) + return result; // no art for three-brush junctions + string a = distinct[0]!, other = distinct[1]!; + Direction otherBits = Direction.None; + for (var i = 0; i < 4; i++) + { + if (cornerBrushes[i] == other) + otherBits |= LatticeCorners[i]; + } + if (lb.TryGetValue(a, out var ab) && ab.Transitions.TryGetValue(other, out var fwd)) + foreach (var t in fwd) + if ((t.Direction & CornerMask) == otherBits) + result.Add(t.TileID); + if (lb.TryGetValue(other, out var ob) && ob.Transitions.TryGetValue(a, out var rev)) + foreach (var t in rev) + if ((Complement(t.Direction) & CornerMask) == otherBits) + result.Add(t.TileID); + return result; + } + + private void SetGhost(LandObject lo, ushort newId) + { + lo.Visible = false; + if (MapManager.GhostLandTiles.TryGetValue(lo, out var ghost)) + { + ghost.LandTile.Id = newId; + ghost.UpdateId(newId); + } + else + { + MapManager.GhostLandTiles[lo] = new LandObject(new LandTile(newId, lo.Tile.X, lo.Tile.Y, lo.Tile.Z)); + } + } + + private void RepairTile(int x, int y) + { + if (!Client.IsValidX(x) || !Client.IsValidY(y)) + return; + var lo = MapManager.LandTiles[x, y]; + if (lo == null || !MapManager.CanDrawLand(lo)) + return; + if (!ProfileManager.ActiveProfile.LandBrush.TryGetValue(_activeLandBrushName, out var active)) + return; + + var corners = new string?[4]; + for (var i = 0; i < 4; i++) + corners[i] = LatticeBrush(x, y, LatticeCorners[i]); + if (corners.Any(c => c == null)) + return; + + var distinct = corners.Distinct().ToList(); + if (distinct.Count == 1) + { + if (distinct[0] != _activeLandBrushName || !_fixInside) + return; + if (MapManager.GhostLandTiles.ContainsKey(lo)) + return; // one re-roll per drag, not per mouse move + if (active.Tiles.Count == 0) + return; + SetGhost(lo, active.Tiles[Random.Shared.Next(active.Tiles.Count)]); + return; + } + + if (!distinct.Contains(_activeLandBrushName)) + return; + var valid = ValidTiles(corners); + if (valid.Count == 0) + return; + var currentId = EffectiveId(x, y); + if (valid.Contains(currentId)) + return; + SetGhost(lo, valid[Random.Shared.Next(valid.Count)]); + } + + #endregion + protected override void GhostClear(TileObject? o) { if (o is LandObject lo)