feat: add brushHash and listed

This commit is contained in:
dualfroz committed 2026-09-06 19:57:32 +02:00
1 parent be14453cf6
commit b16bf4f9b7
1 file changed
+152
+152
View File
@@ -0,0 +1,152 @@
#include "vf/Brush.h"
#include <algorithm>
#include <cmath>
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<uint32_t>(cell.x) * 73856093u;
h ^= static_cast<uint32_t>(cell.y) * 19349663u;
h ^= static_cast<uint32_t>(cell.z) * 83492791u;
h ^= h >> 13;
h *= 1274126177u;
h ^= h >> 16;
return h;
}
bool listed(const std::vector<Block>& 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<std::size_t>(hi.x - lo.x + 1);
std::size_t spanZ = static_cast<std::size_t>(hi.z - lo.z + 1);
std::size_t columns = spanX * spanZ;