diff --git a/core/include/vf/Selection.h b/core/include/vf/Selection.h new file mode 100644 index 0000000..d4ae001 --- /dev/null +++ b/core/include/vf/Selection.h @@ -0,0 +1,153 @@ +#pragma once + +#include +#include +#include +#include + +#include "Block.h" +#include "Math.h" +#include "World.h" + +namespace vf { + +// How a selection was built. Every shape collapses into a SelectionMask, so the +// tag is informational: it survives shifting but any set operation produces a +// free-form Mask, because the result no longer has a closed form. +enum class SelectionShape : uint8_t { + Box = 0, + Sphere = 1, + Ellipsoid = 2, + Cylinder = 3, + HollowBox = 4, + Wand = 5, + Mask = 6, +}; + +// One bit per cell of an axis-aligned bounding box, indexed in the same +// (y, z, x) order the chunk storage uses. Storing the box explicitly is what +// makes union/difference/intersection and in-box negation exact rather than +// approximations over a list of positions. +class SelectionMask { +public: + SelectionMask() = default; + SelectionMask(const Vec3i& minCorner, const Vec3i& dims); + + const Vec3i& min() const { return corner; } + Vec3i max() const; + const Vec3i& dims() const { return size; } + + std::size_t cellCount() const; + std::size_t count() const { return setCells; } + bool empty() const { return setCells == 0u; } + + bool inBounds(const Vec3i& position) const; + bool contains(const Vec3i& position) const; + + void add(const Vec3i& position); + void remove(const Vec3i& position); + void setCell(const Vec3i& position, bool on); + void fillAll(); + void clearAll(); + + SelectionMask unionWith(const SelectionMask& other) const; + SelectionMask intersectWith(const SelectionMask& other) const; + SelectionMask differenceWith(const SelectionMask& other) const; + SelectionMask inverted() const; + + // Dilation and erosion over the 6-connected neighbourhood, applied `radius` + // times. Dilation grows the bounding box; erosion keeps it and treats + // everything outside as unset, so eroding a solid box peels its shell. + SelectionMask dilated(int radius) const; + SelectionMask eroded(int radius) const; + + // Cells that have at least one 6-neighbour outside the selection. + SelectionMask outline() const; + + // Same cells, bounding box shrunk to fit them. + SelectionMask trimmed() const; + SelectionMask shifted(const Vec3i& delta) const; + + bool sameCells(const SelectionMask& other) const; + + void forEach(const std::function& visit) const; + std::vector cells() const; + +private: + std::size_t indexOf(const Vec3i& position) const; + void recount(); + + Vec3i corner{0, 0, 0}; + Vec3i size{0, 0, 0}; + std::vector words; + std::size_t setCells = 0; +}; + +// Aggregate view of what a selection covers in a given world. +struct SelectionStats { + std::size_t blockCount = 0; + std::size_t solidCount = 0; + std::size_t airCount = 0; + Vec3i dims{0, 0, 0}; + long long boundingVolume = 0; + std::vector histogram; + + uint32_t countOf(Block block) const; + Block dominantBlock() const; + std::size_t distinctBlocks() const; +}; + +// A world-space selection: a shape tag plus the mask it rasterized into. +// Every operation returns a new selection; nothing mutates in place, so a +// selection handed to a brush cannot change under it. +class Selection { +public: + Selection() = default; + + static Selection box(const Vec3i& cornerA, const Vec3i& cornerB); + static Selection sphere(const Vec3i& center, int radius); + static Selection ellipsoid(const Vec3i& center, const Vec3i& radii); + static Selection cylinder(const Vec3i& center, int radius, int height); + static Selection hollowBox(const Vec3i& cornerA, const Vec3i& cornerB, int thickness); + static Selection fromMask(const SelectionMask& mask); + + // Flood fill over blocks equal to the one at `seed`, 6-connected, stopping + // at `maxCells`. The limit is what keeps a wand on an ocean of air finite, + // so hitting it is a normal outcome reported by truncated(). + static Selection magicWand(const World& world, const Vec3i& seed, std::size_t maxCells); + static Selection magicWandWithin(const World& world, const Vec3i& seed, std::size_t maxCells, + const Vec3i& limitMin, const Vec3i& limitMax); + + SelectionShape shape() const { return kind; } + const SelectionMask& mask() const { return cells; } + bool truncated() const { return hitLimit; } + + bool contains(const Vec3i& position) const { return cells.contains(position); } + std::size_t count() const { return cells.count(); } + bool empty() const { return cells.empty(); } + Vec3i min() const { return cells.min(); } + Vec3i max() const { return cells.max(); } + Vec3i dims() const { return cells.dims(); } + + Selection unionWith(const Selection& other) const; + Selection intersectWith(const Selection& other) const; + Selection differenceWith(const Selection& other) const; + Selection inverted() const; + Selection expanded(int radius) const; + Selection contracted(int radius) const; + Selection outline() const; + Selection shifted(const Vec3i& delta) const; + + SelectionStats stats(const World& world) const; + std::vector blocks() const; + void forEach(const std::function& visit) const; + +private: + static Selection derived(SelectionMask mask); + + SelectionShape kind = SelectionShape::Mask; + SelectionMask cells; + bool hitLimit = false; +}; + +} // namespace vf