From e05826998b391931e491a00a8e6b21790c32ca21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Tue, 30 Jun 2026 11:25:11 +0200 Subject: [PATCH 1/3] Add selection shrink command --- src/server/command/_list.zig | 1 + src/server/command/worldedit/selection.zig | 187 +++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 src/server/command/worldedit/selection.zig diff --git a/src/server/command/_list.zig b/src/server/command/_list.zig index 00b2dd1f4e..0ef19314ba 100644 --- a/src/server/command/_list.zig +++ b/src/server/command/_list.zig @@ -25,6 +25,7 @@ pub const copy = @import("worldedit/copy.zig"); pub const paste = @import("worldedit/paste.zig"); pub const blueprint = @import("worldedit/blueprint.zig"); pub const rotate = @import("worldedit/rotate.zig"); +pub const selection = @import("worldedit/selection.zig"); pub const set = @import("worldedit/set.zig"); pub const mask = @import("worldedit/mask.zig"); pub const replace = @import("worldedit/replace.zig"); diff --git a/src/server/command/worldedit/selection.zig b/src/server/command/worldedit/selection.zig new file mode 100644 index 0000000000..e55b2b4444 --- /dev/null +++ b/src/server/command/worldedit/selection.zig @@ -0,0 +1,187 @@ +const std = @import("std"); + +const main = @import("main"); +const Block = main.blocks.Block; +const command = main.server.command; +const User = main.server.User; +const Vec3i = main.vec.Vec3i; + +pub const description = "Operate on selection"; +pub const usage = + \\/selection norm + \\/selection normalize + \\ Ensure pos1 is set to minimal coordinates and pos2 is set to maximal coordinates from selection. + \\/selection query + \\ info - log general information about selection (pos1, pos2, size, number blocks and entities inside) + \\ blocks - log list of unique blocks in selection and count them. + \\/selection edit + \\/selection shrink + \\ Automatically shrink the selection to fit a structure, non-air blocks stop shrinking process. + \\/selection grow + \\ Automatically grow the selection to fit a structure. +; + +const Mode = enum {info, blocks}; +const Direction = enum {@"+x", @"-x", @"+y", @"-y", @"+z", @"-z", @"front", @"back", @"left", @"rigth", @"up", @"down"}; + +const Args = union(enum) { + @"/selection query ": struct {_: enum{query}, mode: Mode}, + @"/selection edit ": struct {_: enum{edit}, direction: Direction, amount: i32}, + @"/selection shrink ": struct {_: enum{@"shrink"}, limit: ?u32}, + @"/selection grow ": struct {_: enum{@"grow"}, limit: ?u32}, +}; + +const ArgParser = main.argparse.Parser(Args, .{.commandName = "/selection"}); + +pub fn execute(args: []const u8, source: *User) void { + var errorMessage: main.List(u8) = .empty; + defer errorMessage.deinit(main.stackAllocator); + + const result = ArgParser.parse(main.stackAllocator, args, &errorMessage) catch { + source.sendMessage("#ff0000{s}", .{errorMessage.items}); + return; + }; + + switch (result) { + .@"/selection query " => |cmd| query(source, cmd.mode), + .@"/selection edit " => |cmd| edit(source, cmd.direction, cmd.amount), + .@"/selection shrink " => |cmd| shrink(source, cmd.limit orelse 32), + .@"/selection grow " => |cmd| autoGrow(source, cmd.limit orelse 32), + } +} + +fn query(source: *User, mode: Mode) void { + _ = source; + _ = mode; +} + +fn edit(source: *User, direction: Direction, amount: i32) void { + _ = source; + _ = direction; + _ = amount; +} + +fn shrink(source: *User, limit: u32) void { + const current = command.getCurrentSelection(source) catch return; + + updateWorldEditPos(source, current.minPos, current.maxPos); + + const minX, const minY, const minZ = current.minPos; + const maxX, const maxY, const maxZ = current.maxPos; + + const xRange: Range = .init2(minX, maxX); + const yRange: Range = .init2(minY, maxY); + const zRange: Range = .init2(minZ, maxZ); + + const newMinX = Search(.xyz).search3D(xRange, yRange, zRange, limit) orelse minX; + const newMinY = Search(.yxz).search3D(yRange, xRange, zRange, limit) orelse minY; + const newMinZ = Search(.zyx).search3D(zRange, yRange, xRange, limit) orelse minZ; + + const xRangeReverse: Range = xRange.reverse(); + const yRangeReverse: Range = yRange.reverse(); + const zRangeReverse: Range = zRange.reverse(); + + const newMaxX = Search(.xyz).search3D(xRangeReverse, yRange, zRange, limit) orelse maxX; + const newMaxY = Search(.yxz).search3D(yRangeReverse, xRange, zRange, limit) orelse maxY; + const newMaxZ = Search(.zyx).search3D(zRangeReverse, yRange, xRange, limit) orelse maxZ; + + updateWorldEditPos(source, .{newMinX, newMinY, newMinZ}, .{newMaxX, newMaxY, newMaxZ}); +} + +fn updateWorldEditPos(source: *User, pos1: Vec3i, pos2: Vec3i) void { + source.worldEditData.selectionPosition1 = pos1; + main.network.protocols.genericUpdate.sendWorldEditPos(source.conn, .selectedPos1, pos1); + + source.worldEditData.selectionPosition2 = pos2; + main.network.protocols.genericUpdate.sendWorldEditPos(source.conn, .selectedPos2, pos2); +} + + +fn Search(comptime orientation: enum{xyz, yxz, zyx}) type { + return struct { + const Self = @This(); + + fn search3D(iRange: Range, jRange: Range, kRange: Range, limit: u32) ?i32 { + std.log.debug("{s} {} {} {}", .{@tagName(orientation), iRange, jRange, kRange}); + var iLimit = 0; + var iIterator = iRange.iter(); + while (iIterator.next()) |i| { + var jLimit = 0; + + var jIterator = jRange.iter(); + while (jIterator.next()) |j| { + var kLimit = 0; + + var kIterator = kRange.iter(); + while (kIterator.next()) |k| { + if (Self.getBlock(i, j, k)) |block| { + if (block.typ != 0) { + return i; + } + } + + kLimit += 1; + // We didn't even finish scanning one JK plane, so we can't return updated I + if (kLimit > limit) return null; + } + + jLimit += 1; + // We didn't even finish scanning one JK plane, so we can't return updated I + if (jLimit > limit) return null; + } + + iLimit += 1; + // We scanned one JK plane and it was empty (we didn't return) so we can update I + if (iLimit > limit) return i; + } + return null; + } + + fn getBlock(i: i32, j: i32, k: i32) ?Block { + return switch (orientation) { + .xyz => main.server.world.?.getBlock(i, j, k), + .yxz => main.server.world.?.getBlock(j, i, k), + .zyx => main.server.world.?.getBlock(k, j, i), + }; + } + }; +} + +const Range = struct { + start: i32, + stop: i32, + step: i32, + + pub fn init2(start: i32, stop: i32) Range { + const step: i32 = if (start < stop) 1 else -1; + return .{.start = start, .stop = stop, .step = step}; + } + + const Iterator = struct { + current: i32, + range: Range, + + fn next(self: *Iterator) ?i32 { + if (self.current != self.range.stop) { + defer self.current += self.range.step; + return self.current; + } else { + return null; + } + } + }; + + pub fn iter(self: Range) Iterator { + return .{.current = self.start, .range = self}; + } + + pub fn reverse(self: Range) Range { + return .{.start = self.stop, .stop = self.start, .step = -self.step}; + } +}; + + +fn autoGrow(source: *User, limit: ?u32) void { + _ = source; + _ = limit; +} \ No newline at end of file From 2f11ea449613744cdaff2a06d4cb3248dd328620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Tue, 30 Jun 2026 20:40:43 +0200 Subject: [PATCH 2/3] Add limits to shrink --- src/server/command/worldedit/selection.zig | 55 +++++++++++----------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/src/server/command/worldedit/selection.zig b/src/server/command/worldedit/selection.zig index e55b2b4444..f0baa984c1 100644 --- a/src/server/command/worldedit/selection.zig +++ b/src/server/command/worldedit/selection.zig @@ -45,8 +45,8 @@ pub fn execute(args: []const u8, source: *User) void { switch (result) { .@"/selection query " => |cmd| query(source, cmd.mode), .@"/selection edit " => |cmd| edit(source, cmd.direction, cmd.amount), - .@"/selection shrink " => |cmd| shrink(source, cmd.limit orelse 32), - .@"/selection grow " => |cmd| autoGrow(source, cmd.limit orelse 32), + .@"/selection shrink " => |cmd| shrink(source, @intCast(@as(u31, @truncate(cmd.limit orelse 32)))), + .@"/selection grow " => |cmd| autoGrow(source, @intCast(@as(u31, @truncate(cmd.limit orelse 32)))), } } @@ -61,7 +61,9 @@ fn edit(source: *User, direction: Direction, amount: i32) void { _ = amount; } -fn shrink(source: *User, limit: u32) void { +fn shrink(source: *User, limit: i32) void { + if (limit <= 1) return; // This is a noop. + const current = command.getCurrentSelection(source) catch return; updateWorldEditPos(source, current.minPos, current.maxPos); @@ -69,21 +71,17 @@ fn shrink(source: *User, limit: u32) void { const minX, const minY, const minZ = current.minPos; const maxX, const maxY, const maxZ = current.maxPos; - const xRange: Range = .init2(minX, maxX); - const yRange: Range = .init2(minY, maxY); - const zRange: Range = .init2(minZ, maxZ); - - const newMinX = Search(.xyz).search3D(xRange, yRange, zRange, limit) orelse minX; - const newMinY = Search(.yxz).search3D(yRange, xRange, zRange, limit) orelse minY; - const newMinZ = Search(.zyx).search3D(zRange, yRange, xRange, limit) orelse minZ; + const xRange: Range = .init(minX, maxX, 1); + const yRange: Range = .init(minY, maxY, 1); + const zRange: Range = .init(minZ, maxZ, 1); - const xRangeReverse: Range = xRange.reverse(); - const yRangeReverse: Range = yRange.reverse(); - const zRangeReverse: Range = zRange.reverse(); + const newMinX = Search(.xyz).search3D(.init(minX, @min(minX + limit, maxX), 1), yRange, zRange, limit) orelse minX; + const newMinY = Search(.yxz).search3D(.init(minY, @min(minY + limit, maxY), 1), xRange, zRange, limit) orelse minY; + const newMinZ = Search(.zyx).search3D(.init(minZ, @min(minZ + limit, maxZ), 1), yRange, xRange, limit) orelse minZ; - const newMaxX = Search(.xyz).search3D(xRangeReverse, yRange, zRange, limit) orelse maxX; - const newMaxY = Search(.yxz).search3D(yRangeReverse, xRange, zRange, limit) orelse maxY; - const newMaxZ = Search(.zyx).search3D(zRangeReverse, yRange, xRange, limit) orelse maxZ; + const newMaxX = Search(.xyz).search3D(.init(maxX, @max(minX, maxX - limit), -1), yRange, zRange, limit) orelse maxX; + const newMaxY = Search(.yxz).search3D(.init(maxY, @max(minY, maxY - limit), -1), xRange, zRange, limit) orelse maxY; + const newMaxZ = Search(.zyx).search3D(.init(maxZ, @max(minZ, maxZ - limit), -1), yRange, xRange, limit) orelse maxZ; updateWorldEditPos(source, .{newMinX, newMinY, newMinZ}, .{newMaxX, newMaxY, newMaxZ}); } @@ -101,16 +99,16 @@ fn Search(comptime orientation: enum{xyz, yxz, zyx}) type { return struct { const Self = @This(); - fn search3D(iRange: Range, jRange: Range, kRange: Range, limit: u32) ?i32 { + fn search3D(iRange: Range, jRange: Range, kRange: Range, limit: i32) ?i32 { std.log.debug("{s} {} {} {}", .{@tagName(orientation), iRange, jRange, kRange}); - var iLimit = 0; + var iLimit: i32 = 0; var iIterator = iRange.iter(); while (iIterator.next()) |i| { - var jLimit = 0; + var jLimit: i32 = 0; var jIterator = jRange.iter(); while (jIterator.next()) |j| { - var kLimit = 0; + var kLimit: i32 = 0; var kIterator = kRange.iter(); while (kIterator.next()) |k| { @@ -152,8 +150,15 @@ const Range = struct { stop: i32, step: i32, - pub fn init2(start: i32, stop: i32) Range { - const step: i32 = if (start < stop) 1 else -1; + /// Initialize a range. + /// Start and stop are not allowed to be equal. + /// When start is smaller than stop, step has to be positive, negative otherwise. + /// Step is not allowed to be equal to 0. + pub fn init(start: i32, stop: i32, step: i32) Range { + std.debug.assert(start != stop); + std.debug.assert(step != 0); + std.debug.assert(if (start < stop) step > 0 else step < 0); + return .{.start = start, .stop = stop, .step = step}; } @@ -174,14 +179,10 @@ const Range = struct { pub fn iter(self: Range) Iterator { return .{.current = self.start, .range = self}; } - - pub fn reverse(self: Range) Range { - return .{.start = self.stop, .stop = self.start, .step = -self.step}; - } }; -fn autoGrow(source: *User, limit: ?u32) void { +fn autoGrow(source: *User, limit: ?i32) void { _ = source; _ = limit; } \ No newline at end of file From 006b3f0942b9b9072ec76b1d4820b8ef02f00c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Wed, 1 Jul 2026 23:14:19 +0200 Subject: [PATCH 3/3] Refactor and implement shrink and grow --- src/renderer.zig | 4 + src/server/command/worldedit/selection.zig | 365 +++++++++++++++------ 2 files changed, 272 insertions(+), 97 deletions(-) diff --git a/src/renderer.zig b/src/renderer.zig index b194bd3e27..7961971600 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -1171,7 +1171,11 @@ pub const MeshSelection = struct { // MARK: MeshSelection drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(_selectedBlockPos)) - playerPos, selectionMin, selectionMax); } if (game.Player.selectionPosition1) |pos1| { + drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(pos1)) - playerPos, .{0, 0, 0}, @floatFromInt(Vec3i{1, 1, 1})); + if (game.Player.selectionPosition2) |pos2| { + drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(pos2)) - playerPos, .{0, 0, 0}, @floatFromInt(Vec3i{1, 1, 1})); + const bottomLeft: Vec3i = @min(pos1, pos2); const topRight: Vec3i = @max(pos1, pos2); drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(bottomLeft)) - playerPos, .{0, 0, 0}, @floatFromInt(topRight - bottomLeft + Vec3i{1, 1, 1})); diff --git a/src/server/command/worldedit/selection.zig b/src/server/command/worldedit/selection.zig index f0baa984c1..b8109caa0c 100644 --- a/src/server/command/worldedit/selection.zig +++ b/src/server/command/worldedit/selection.zig @@ -8,27 +8,25 @@ const Vec3i = main.vec.Vec3i; pub const description = "Operate on selection"; pub const usage = - \\/selection norm \\/selection normalize \\ Ensure pos1 is set to minimal coordinates and pos2 is set to maximal coordinates from selection. - \\/selection query - \\ info - log general information about selection (pos1, pos2, size, number blocks and entities inside) - \\ blocks - log list of unique blocks in selection and count them. - \\/selection edit + \\/selection cube + \\ Create a cube selection with center at current player position. \\/selection shrink \\ Automatically shrink the selection to fit a structure, non-air blocks stop shrinking process. \\/selection grow \\ Automatically grow the selection to fit a structure. + \\ Non-air blocks stop growing process. + \\/selection adjust + \\ Same as grow followed by shrink. ; -const Mode = enum {info, blocks}; -const Direction = enum {@"+x", @"-x", @"+y", @"-y", @"+z", @"-z", @"front", @"back", @"left", @"rigth", @"up", @"down"}; - const Args = union(enum) { - @"/selection query ": struct {_: enum{query}, mode: Mode}, - @"/selection edit ": struct {_: enum{edit}, direction: Direction, amount: i32}, - @"/selection shrink ": struct {_: enum{@"shrink"}, limit: ?u32}, - @"/selection grow ": struct {_: enum{@"grow"}, limit: ?u32}, + @"/selection normalize": struct { subcommand: enum { normalize } }, + @"/selection cube ": struct { subcommand: enum { cube }, radius: ?u32 }, + @"/selection shrink ": struct { subcommand: enum { shrink }, limit: ?u32 }, + @"/selection grow ": struct { subcommand: enum { grow }, limit: ?u32 }, + @"/selection adjust ": struct { subcommand: enum { adjust }, limit: ?u32 }, }; const ArgParser = main.argparse.Parser(Args, .{.commandName = "/selection"}); @@ -43,47 +41,41 @@ pub fn execute(args: []const u8, source: *User) void { }; switch (result) { - .@"/selection query " => |cmd| query(source, cmd.mode), - .@"/selection edit " => |cmd| edit(source, cmd.direction, cmd.amount), - .@"/selection shrink " => |cmd| shrink(source, @intCast(@as(u31, @truncate(cmd.limit orelse 32)))), - .@"/selection grow " => |cmd| autoGrow(source, @intCast(@as(u31, @truncate(cmd.limit orelse 32)))), + .@"/selection normalize" => normalize(source), + .@"/selection cube " => |cmd| cube(source, @intCast(@as(u31, @truncate(cmd.radius orelse 5)))), + .@"/selection shrink " => |cmd| adjust(.shrink, source, @intCast(@as(u31, @truncate(cmd.limit orelse 32)))), + .@"/selection grow " => |cmd| adjust(.grow, source, @intCast(@as(u31, @truncate(cmd.limit orelse 32)))), + .@"/selection adjust " => |cmd| { + adjust(.shrink, source, @intCast(@as(u31, @truncate(cmd.limit orelse 32)))); + adjust(.grow, source, @intCast(@as(u31, @truncate(cmd.limit orelse 32)))); + }, } } -fn query(source: *User, mode: Mode) void { - _ = source; - _ = mode; +fn normalize(source: *User) void { + const current = command.getCurrentSelection(source) catch return; + const minPos = current.minPos; + const maxPos = current.maxPos - Vec3i{1, 1, 1}; + + updateWorldEditPos(source, minPos, maxPos); } -fn edit(source: *User, direction: Direction, amount: i32) void { - _ = source; - _ = direction; - _ = amount; +fn cube(source: *User, radius: i32) void { + const pos: Vec3i = @floor(source.player().pos); + updateWorldEditPos(source, pos - @as(Vec3i, @splat(radius)), pos + @as(Vec3i, @splat(radius))); } -fn shrink(source: *User, limit: i32) void { - if (limit <= 1) return; // This is a noop. +fn adjust(comptime mode: ScannerMode, source: *User, limit: i32) void { + if (limit <= 1) return; const current = command.getCurrentSelection(source) catch return; + const minPos = current.minPos; + const maxPos = current.maxPos - Vec3i{1, 1, 1}; - updateWorldEditPos(source, current.minPos, current.maxPos); - - const minX, const minY, const minZ = current.minPos; - const maxX, const maxY, const maxZ = current.maxPos; - - const xRange: Range = .init(minX, maxX, 1); - const yRange: Range = .init(minY, maxY, 1); - const zRange: Range = .init(minZ, maxZ, 1); - - const newMinX = Search(.xyz).search3D(.init(minX, @min(minX + limit, maxX), 1), yRange, zRange, limit) orelse minX; - const newMinY = Search(.yxz).search3D(.init(minY, @min(minY + limit, maxY), 1), xRange, zRange, limit) orelse minY; - const newMinZ = Search(.zyx).search3D(.init(minZ, @min(minZ + limit, maxZ), 1), yRange, xRange, limit) orelse minZ; + var scanner: Scanner3D(mode) = .init(minPos, maxPos, limit); + const newMin, const newMax = scanner.scan3D(); - const newMaxX = Search(.xyz).search3D(.init(maxX, @max(minX, maxX - limit), -1), yRange, zRange, limit) orelse maxX; - const newMaxY = Search(.yxz).search3D(.init(maxY, @max(minY, maxY - limit), -1), xRange, zRange, limit) orelse maxY; - const newMaxZ = Search(.zyx).search3D(.init(maxZ, @max(minZ, maxZ - limit), -1), yRange, xRange, limit) orelse maxZ; - - updateWorldEditPos(source, .{newMinX, newMinY, newMinZ}, .{newMaxX, newMaxY, newMaxZ}); + updateWorldEditPos(source, newMin, newMax); } fn updateWorldEditPos(source: *User, pos1: Vec3i, pos2: Vec3i) void { @@ -94,57 +86,6 @@ fn updateWorldEditPos(source: *User, pos1: Vec3i, pos2: Vec3i) void { main.network.protocols.genericUpdate.sendWorldEditPos(source.conn, .selectedPos2, pos2); } - -fn Search(comptime orientation: enum{xyz, yxz, zyx}) type { - return struct { - const Self = @This(); - - fn search3D(iRange: Range, jRange: Range, kRange: Range, limit: i32) ?i32 { - std.log.debug("{s} {} {} {}", .{@tagName(orientation), iRange, jRange, kRange}); - var iLimit: i32 = 0; - var iIterator = iRange.iter(); - while (iIterator.next()) |i| { - var jLimit: i32 = 0; - - var jIterator = jRange.iter(); - while (jIterator.next()) |j| { - var kLimit: i32 = 0; - - var kIterator = kRange.iter(); - while (kIterator.next()) |k| { - if (Self.getBlock(i, j, k)) |block| { - if (block.typ != 0) { - return i; - } - } - - kLimit += 1; - // We didn't even finish scanning one JK plane, so we can't return updated I - if (kLimit > limit) return null; - } - - jLimit += 1; - // We didn't even finish scanning one JK plane, so we can't return updated I - if (jLimit > limit) return null; - } - - iLimit += 1; - // We scanned one JK plane and it was empty (we didn't return) so we can update I - if (iLimit > limit) return i; - } - return null; - } - - fn getBlock(i: i32, j: i32, k: i32) ?Block { - return switch (orientation) { - .xyz => main.server.world.?.getBlock(i, j, k), - .yxz => main.server.world.?.getBlock(j, i, k), - .zyx => main.server.world.?.getBlock(k, j, i), - }; - } - }; -} - const Range = struct { start: i32, stop: i32, @@ -181,8 +122,238 @@ const Range = struct { } }; +const ScannerMode = enum { shrink, grow }; + +fn Scanner3D(comptime mode: ScannerMode) type { + return struct { + const Self = @This(); + + min: [3]i32, + max: [3]i32, + + limit: i32, + + originalMin: [3]i32 = @splat(0), + originalMax: [3]i32 = @splat(0), + + const Axis = enum(u2) { + x = 0, + y = 1, + z = 2, + const iterator: [3]Axis = .{.x, .y, .z}; + }; + const Candidate = enum(u1) { + min = 0, + max = 1, + const iterator: [2]Candidate = .{.min, .max}; + }; + const Stage = struct { + axis: Axis, + candidate: Candidate, + isComplete: bool, + }; + + fn init(min: Vec3i, max: Vec3i, limit: i32) Self { + return .{.min = min, .max = max, .limit = limit}; + } + + fn getRange(self: Self, axis: Axis) Range { + const i: usize = @intFromEnum(axis); + return .init(self.min[i], self.max[i] + 1, 1); + } + + pub fn scan3D(self: *Self) struct { Vec3i, Vec3i } { + self.originalMin = self.min; + self.originalMax = self.max; + + var scanningSequence: [6]Stage = .{ + .{.axis = .x, .candidate = .min, .isComplete = false}, + .{.axis = .x, .candidate = .max, .isComplete = false}, + .{.axis = .y, .candidate = .min, .isComplete = false}, + .{.axis = .y, .candidate = .max, .isComplete = false}, + .{.axis = .z, .candidate = .min, .isComplete = false}, + .{.axis = .z, .candidate = .max, .isComplete = false}, + }; + + // For a simple shrinking process, this could have been much simpler: Just three nested loops in + // each out of 6 directions would be enough. However, if we want to properly implmenet growing, + // every consecutive direction of scanning needs to account for the previous iteration results. + // This is especially important when working with clusters of small objects, which could potentially + // be cut off if we just kept the original size of the selection and only scanned in a star shape. + // + // Example: + // Structure consisting of two L like parts, overalpping but not touching: + // + // ** + // * * + // ** + // ┃ ┃ + // ┃ ┃ * this space is never checked if we don't account for previous iterations + // ┏━━━┓ ╋━━━╋━━━ + // ┃ ┃ ┃ * ┃ + // ┗━━━┛ ┗━━━┻━━━ + // + // ┃ ┃ + // * ┃ * ┃ if we account for previous iterations we capture clustered objects + // ┏━━━┓ ╋━━━╋━━━ ╋━━━━━━╋ + // ┃ ┃ ┃ * ┃ ┃ * ┃ + // ┗━━━┛ ┗━━━┻━━━ ┗━━━━━━┻ + // + // Now, this code does an extra effort of altering between directions until all are saturated. + // This part might not be necessary, but I don't think it changes much in the design, so I did it this way. + + while (true) doScan: { + for (&scanningSequence) |*stage| { + if (stage.isComplete) continue; + + switch (mode) { + .shrink => self.shrink(stage), + .grow => self.grow(stage), + } + } + for (scanningSequence) |stage| if (!stage.isComplete) break :doScan; + break; + } + + return .{self.min, self.max}; + } + + fn getCurrentValue(self: Self, axis: Axis, candidate: Candidate) i32 { + const i: usize = @intFromEnum(axis); + return switch (candidate) { + .min => self.min[i], + .max => self.max[i], + }; + } + + fn shrink(self: *Self, stage: *Stage) void { + const currentValue = self.getCurrentValue(stage.axis, stage.candidate); + + switch (self.scanPerpendicularPlane(stage.axis, currentValue)) { + .failure, .limitExceeded => { + stage.isComplete = true; + return; + }, + .success => {}, + } -fn autoGrow(source: *User, limit: ?i32) void { - _ = source; - _ = limit; -} \ No newline at end of file + const newValue = self.getCandidate(stage.axis, stage.candidate); + + if (!self.isValidCandidate(stage.axis, stage.candidate, newValue)) { + stage.isComplete = true; + return; + } + + switch (stage.candidate) { + .min => self.min[@intFromEnum(stage.axis)] = newValue, + .max => self.max[@intFromEnum(stage.axis)] = newValue, + } + } + + fn grow(self: *Self, stage: *Stage) void { + const newValue = self.getCandidate(stage.axis, stage.candidate); + + if (!self.isValidCandidate(stage.axis, stage.candidate, newValue)) { + stage.isComplete = true; + return; + } + + switch (self.scanPerpendicularPlane(stage.axis, newValue)) { + .failure, .limitExceeded => { + stage.isComplete = true; + return; + }, + .success => {}, + } + + switch (stage.candidate) { + .min => self.min[@intFromEnum(stage.axis)] = newValue, + .max => self.max[@intFromEnum(stage.axis)] = newValue, + } + } + + fn getCandidate(self: Self, axis: Axis, candidate: Candidate) i32 { + const i: usize = @intFromEnum(axis); + return switch (mode) { + .shrink => switch (candidate) { + .min => self.min[i] + 1, + .max => self.max[i] - 1, + }, + .grow => switch (candidate) { + .min => self.min[i] - 1, + .max => self.max[i] + 1, + }, + }; + } + + /// Check external limits to the iteration - fully collapsing the selection or exceeding the limit of iterations. + fn isValidCandidate(self: Self, axis: Axis, candidate: Candidate, newValue: i32) bool { + const i: usize = @intFromEnum(axis); + return switch (mode) { + .shrink => switch (candidate) { + .min => newValue < self.originalMax[i] and newValue < self.originalMin[i] + self.limit, + .max => newValue > self.originalMin[i] and newValue > self.originalMax[i] - self.limit, + }, + .grow => switch (candidate) { + .min => newValue > self.originalMin[i] - self.limit, + .max => newValue < self.originalMax[i] + self.limit, + }, + }; + } + + /// Scan a 2D plane of blocks perpendicular to the given axis. + /// `currentValue` determines which of infinitely many planes to choose using a coordinate on `axis`. + fn scanPerpendicularPlane(self: Self, axis: Axis, currentValue: i32) ScanStatus { + return switch (axis) { + .x => Scanner2D(.yz, mode).scanPlane(currentValue, self.getRange(.y), self.getRange(.z), self.limit), + .y => Scanner2D(.xz, mode).scanPlane(currentValue, self.getRange(.x), self.getRange(.z), self.limit), + .z => Scanner2D(.yx, mode).scanPlane(currentValue, self.getRange(.y), self.getRange(.x), self.limit), + }; + } + }; +} + +const ScanStatus = enum { success, failure, limitExceeded }; + +fn Scanner2D(comptime plane: enum { yz, xz, yx }, comptime mode: ScannerMode) type { + return struct { + const Self = @This(); + + fn scanPlane(i: i32, jRange: Range, kRange: Range, limit: i32) ScanStatus { + var jLimit: i32 = 0; + + var jIterator = jRange.iter(); + while (jIterator.next()) |j| { + var kLimit: i32 = 0; + + var kIterator = kRange.iter(); + while (kIterator.next()) |k| { + const x, const y, const z = Self.mapCoordinates(i, j, k); + + if (main.server.world.?.getBlock(x, y, z)) |block| { + // Finding a non-air block in shrink mode means we have to stop contracting, + // but in growing mode it means we can continue expading to possibly find more. + if (block.typ != 0) return if (mode == .shrink) .failure else .success; + } + + kLimit += 1; + // We didn't even finish scanning one JK plane, so we can't return updated I + if (kLimit > limit) return .limitExceeded; + } + + jLimit += 1; + // We didn't even finish scanning one JK plane, so we can't return updated I + if (jLimit > limit) return .limitExceeded; + } + return if (mode == .shrink) .success else .failure; + } + + fn mapCoordinates(i: i32, j: i32, k: i32) struct { i32, i32, i32 } { + return switch (plane) { + .yz => .{i, j, k}, + .xz => .{j, i, k}, + .yx => .{k, j, i}, + }; + } + }; +}