From b16bf4f9b7fc1c990c30efb457c94267609a509e Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 6 Sep 2026 19:57:32 +0200 Subject: [PATCH] feat: add brushHash and listed --- core/src/Brush.cpp | 152 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 core/src/Brush.cpp diff --git a/core/src/Brush.cpp b/core/src/Brush.cpp new file mode 100644 index 0000000..8e89aa0 --- /dev/null +++ b/core/src/Brush.cpp @@ -0,0 +1,152 @@ +#include "vf/Brush.h" + +#include +#include + +namespace vf { + +namespace { + +const Vec3i faceOffsets[6] = { + {1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}, +}; + +// Position hash used by the gradient brush. Pure function of the seed and the +// block coordinate, so the same stroke on the same seed always paints the same +// pattern - no clock, no global generator state. +uint32_t brushHash(uint32_t seed, const Vec3i& cell) { + uint32_t h = seed * 2654435761u; + h ^= static_cast(cell.x) * 73856093u; + h ^= static_cast(cell.y) * 19349663u; + h ^= static_cast(cell.z) * 83492791u; + h ^= h >> 13; + h *= 1274126177u; + h ^= h >> 16; + return h; +} + +bool listed(const std::vector& blocks, Block block) { + return std::find(blocks.begin(), blocks.end(), block) != blocks.end(); +} + +Selection innerVolume(const BrushParams& params) { + int innerRadius = params.radius - params.shellThickness; + if (innerRadius < 0) return Selection{}; + if (params.kind == BrushKind::Cylinder) { + int innerHeight = params.height - 2 * params.shellThickness; + if (innerHeight < 1) return Selection{}; + Vec3i base{params.center.x, params.center.y + params.shellThickness, params.center.z}; + return Selection::cylinder(base, innerRadius, innerHeight); + } + return Selection::sphere(params.center, innerRadius); +} + +} // namespace + +BrushStroke::BrushStroke(World& world, EditHistory& history) : world(world), history(history) {} + +const char* BrushStroke::kindName(BrushKind kind) { + switch (kind) { + case BrushKind::Sphere: return "sphere"; + case BrushKind::Cylinder: return "cylinder"; + case BrushKind::Replace: return "replace"; + case BrushKind::Smooth: return "smooth"; + case BrushKind::Erode: return "erode"; + case BrushKind::Overlay: return "overlay"; + case BrushKind::Naturalize: return "naturalize"; + case BrushKind::Gradient: return "gradient"; + } + return "brush"; +} + +Selection BrushStroke::regionOf(const BrushParams& params) { + if (params.kind == BrushKind::Cylinder) + return Selection::cylinder(params.center, params.radius, params.height); + return Selection::sphere(params.center, params.radius); +} + +BrushResult BrushStroke::apply(const BrushParams& params, uint64_t tick) { + return applyWithin(params, regionOf(params), tick); +} + +BrushResult BrushStroke::applyWithin(const BrushParams& params, const Selection& limit, + uint64_t tick) { + BrushResult outcome; + std::string label = params.label.empty() ? std::string(kindName(params.kind)) : params.label; + history.beginBatch(label, tick); + + switch (params.kind) { + case BrushKind::Sphere: + case BrushKind::Cylinder: + paintVolume(params, limit, outcome); + break; + case BrushKind::Replace: + paintReplace(params, limit, outcome); + break; + case BrushKind::Smooth: + paintSmooth(params, limit, outcome); + break; + case BrushKind::Erode: + paintNeighbourRule(params, limit, true, outcome); + break; + case BrushKind::Overlay: + paintNeighbourRule(params, limit, false, outcome); + break; + case BrushKind::Naturalize: + paintNaturalize(params, limit, outcome); + break; + case BrushKind::Gradient: + paintGradient(params, limit, outcome); + break; + } + + outcome.recorded = history.endBatch(); + previous = outcome; + return outcome; +} + +void BrushStroke::write(const BrushParams& params, const Vec3i& cell, Block block, + BrushResult& outcome) { + ++outcome.visited; + if (cell.y < params.worldMinY || cell.y >= params.worldMaxY) { + outcome.clippedAtHeightLimit = true; + return; + } + Vec3i chunkCoord{floorDiv(cell.x, chunkSize), floorDiv(cell.y, chunkSize), + floorDiv(cell.z, chunkSize)}; + if (world.chunkAt(chunkCoord) == nullptr) outcome.outsideLoadedWorld = true; + if (history.recordSet(world, cell, block)) ++outcome.changed; +} + +void BrushStroke::paintVolume(const BrushParams& params, const Selection& limit, + BrushResult& outcome) { + Selection shape = regionOf(params); + if (params.mode == BrushParams::VolumeMode::Shell) + shape = shape.differenceWith(innerVolume(params)); + Selection painted = shape.intersectWith(limit); + Block value = params.mode == BrushParams::VolumeMode::Hollow ? Block::Air : params.block; + painted.forEach([&](const Vec3i& cell) { write(params, cell, value, outcome); }); +} + +void BrushStroke::paintReplace(const BrushParams& params, const Selection& limit, + BrushResult& outcome) { + limit.forEach([&](const Vec3i& cell) { + Block existing = world.blockAt(cell.x, cell.y, cell.z); + if (params.onlyReplace.empty()) { + if (isAir(existing)) return; + } else if (!listed(params.onlyReplace, existing)) { + return; + } + write(params, cell, params.block, outcome); + }); +} + +void BrushStroke::paintSmooth(const BrushParams& params, const Selection& limit, + BrushResult& outcome) { + if (limit.empty()) return; + Vec3i lo = limit.min(); + Vec3i hi = limit.max(); + std::size_t spanX = static_cast(hi.x - lo.x + 1); + std::size_t spanZ = static_cast(hi.z - lo.z + 1); + std::size_t columns = spanX * spanZ; +