Merge branch 'main' into VorTechnix

This commit is contained in:
VorTechnix 2022-01-03 06:56:58 -08:00
commit 9995ea2fb9
68 changed files with 1772 additions and 472 deletions

@ -75,7 +75,7 @@ async function fetch(url) {
}
module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy("img2brush/img2brush.js");
eleventyConfig.addAsyncShortcode("fetch", fetch);
// eleventyConfig.addPassthroughCopy("images");

@ -8,14 +8,14 @@
<li>A full reference, with detailed explanations for each command</li>
</ol>
<p>There is also a <a href="#filter">filter box</a> for filtering the detailed explanations to quickly find the one you're after.</p>
<p>There is also a <a href="#filter">filter box</a> for filtering everything to quickly find the one you're after.</p>
</section>
<section id="filter" class="panel-generic">
<div class="form-item bigsearch">
<label for="input-filter" >Filter:</label>
<input type="search" id="input-filter" placeholder="Start typing to filter." />
<label for="input-filter">Filter:</label>
<input type="search" id="input-filter" placeholder="Start typing to filter." autofocus />
</div>
<div class="form-item centre checkbox">
<input type="checkbox" id="input-searchall" />

@ -411,3 +411,30 @@ footer {
flex-direction: column;
align-items: center;
}
@keyframes move-diagonal {
from {
background-position: -2px -2px, -2px -2px, -1px -1px, -1px -1px;
}
to {
background-position: -52px -52px, -52px -52px, -51px -51px, -51px -51px;
}
}
#dropzone {
border: 0.3em dashed #aaaaaa;
transition: border 0.2s;
justify-content: flex-start;
}
#dropzone.dropzone-active {
border: 0.3em dashed hsl(203, 79%, 55%);
/* Ref https://www.magicpattern.design/tools/css-backgrounds */
background-image: linear-gradient(var(--bg-bright) 2px, transparent 2px), linear-gradient(90deg, var(--bg-bright) 2px, transparent 2px), linear-gradient(var(--bg-bright) 1px, transparent 1px), linear-gradient(90deg, var(--bg-bright) 1px, var(--bg-transcluscent) 1px);
background-size: 50px 50px, 50px 50px, 10px 10px, 10px 10px;
background-position: -2px -2px, -2px -2px, -1px -1px, -1px -1px;
animation: move-diagonal 5s linear infinite;
}
#brushimg-preview { flex: 1; }

40
.docs/grammars/axes.bnf Normal file

@ -0,0 +1,40 @@
{%
# Lists of axes
In various commands such as `//copy+`, `//move+`, and others lists of axes are used. These are all underpinned by a single grammar and a single parser (located in `worldeditadditions/utils/parse/axes.lua`). While the parser itself requires pre-split tokens (see `split_shell` for that), the grammar which it parses is documnted here.
Examples:
```
front 3 left 10 y 77 x 30 back 99
```
%}
<axes> ::= <axis_part> *
<axes_part> ::= <axis_name> <number>
| <axis_name> <number> <reverse>
<reverse> ::= <number>
| <reverse_name>
<reverse_name> ::= sym | symmetrical | mirror | mir | rev | reverse | true
<axis_name> ::= <axis_name_absolute>
| <axis_name_relative>
<axis_name_absolute> ::= <axis_letters_absolute>
| "-" <axis_letters_absolute>
<axis_name_relative> ::= front | back | left | right | up | down | "?"
<axis_letters_absolute> ::= x | y | z | h | v
<number> ::= <digit> *
<digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0

@ -0,0 +1,93 @@
window.addEventListener("load", () => {
let dropzone = document.querySelector("#dropzone");
dropzone.addEventListener("dragenter", handle_drag_enter);
dropzone.addEventListener("dragleave", handle_drag_leave);
dropzone.addEventListener("dragover", handle_drag_over);
dropzone.addEventListener("drop", handle_drop);
document.querySelector("#brushimg-tsv").addEventListener("click", select_output);
let button_copy = document.querySelector("#brushimg-copy")
button_copy.addEventListener("click", () => {
select_output();
button_copy.innerHTML = document.execCommand("copy") ? "Copied!" : "Failed to copy :-(";
})
});
function select_output() {
let output = document.querySelector("#brushimg-tsv");
let selection = window.getSelection();
if (selection.rangeCount > 0)
selection.removeAllRanges();
let range = document.createRange();
range.selectNode(output);
selection.addRange(range);
}
function handle_drag_enter(event) {
event.target.classList.add("dropzone-active");
}
function handle_drag_leave(event) {
event.target.classList.remove("dropzone-active");
}
function handle_drag_over(event) {
event.preventDefault();
}
function handle_drop(event) {
event.stopPropagation();
event.preventDefault();
event.target.classList.remove("dropzone-active");
let image_file = null;
image_file = event.dataTransfer.files[0];
let reader = new FileReader();
reader.addEventListener("load", function(_event) {
let image = new Image();
image.src = reader.result;
image.addEventListener("load", () => handle_new_image(image));
document.querySelector("#brushimg-preview").src = image.src;
});
reader.readAsDataURL(image_file);
return false;
}
function image2pixels(image) {
let canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d");
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
return ctx.getImageData(0, 0, image.width, image.height);
}
function handle_new_image(image) {
let tsv = pixels2tsv(image2pixels(image));
document.querySelector("#brushimg-stats").value = `${image.width} x ${image.height} | ${image.width * image.height} pixels`;
document.querySelector("#brushimg-tsv").value = tsv;
}
function pixels2tsv(pixels) {
let result = "";
for(let y = 0; y < pixels.height; y++) {
let row = [];
for(let x = 0; x < pixels.width; x++) {
// No need to rescale here - this is done automagically by WorldEditAdditions.
row.push(pixels.data[((y*pixels.width + x) * 4) + 3] / 255);
}
result += row.join(`\t`) + `\n`;
}
return result;
}

@ -0,0 +1,34 @@
---
layout: theme.njk
title: Image to brush converter
---
<section class="panel-generic">
<h1>Image to sculpting brush converter</h1>
<p>Convert any image to a sculpting brush here! The <strong>alpha (opacity) channel</strong> is used to determine the weight of the brush - <strong>all colour is ignored</strong>.</p>
</section>
<section id="dropzone" class="bigbox panel-generic">
<h2>Input</h2>
<p>Drop your image here.</p>
<img id="brushimg-preview" alt="" />
<output id="brushimg-stats"></output>
</section>
<section class="panel-generic">
<h2>Output</h2>
<p>Paste the output below into a text file in <code>worldeditadditions/lib/sculpt/brushes</code> with the file extension <code>.brush.tsv</code> and restart your Minetest server for the brush to be recognised.</p>
<p class="text-centre">
<button id="brushimg-copy" class="bigbutton">Copy</button>
</p>
<pre><output id="brushimg-tsv"><em>(your output will appear here as soon as you drop an image above)</em></output></pre>
</section>
<script src="./img2brush.js"></script>

@ -0,0 +1,100 @@
local Vector3 = require("worldeditadditions.utils.vector3")
describe("Vector3.clamp", function()
it("should work by not changing values if it's already clamped", function()
local a = Vector3.new(5, 6, 7)
local pos1 = Vector3.new(0, 0, 0)
local pos2 = Vector3.new(10, 10, 10)
local result = Vector3.clamp(a, pos1, pos2)
assert.are.same(
Vector3.new(5, 6, 7),
result
)
result.z = 999
assert.are.same(Vector3.new(5, 6, 7), a)
assert.are.same(Vector3.new(0, 0, 0), pos1)
assert.are.same(Vector3.new(10, 10, 10), pos2)
end)
it("should work with positive vectors", function()
local a = Vector3.new(30, 3, 3)
local pos1 = Vector3.new(0, 0, 0)
local pos2 = Vector3.new(10, 10, 10)
assert.are.same(
Vector3.new(10, 3, 3),
Vector3.clamp(a, pos1, pos2)
)
end)
it("should work with multiple positive vectors", function()
local a = Vector3.new(30, 99, 88)
local pos1 = Vector3.new(4, 4, 4)
local pos2 = Vector3.new(13, 13, 13)
assert.are.same(
Vector3.new(13, 13, 13),
Vector3.clamp(a, pos1, pos2)
)
end)
it("should work with multiple positive vectors with an irregular defined region", function()
local a = Vector3.new(30, 99, 88)
local pos1 = Vector3.new(1, 5, 3)
local pos2 = Vector3.new(10, 15, 8)
assert.are.same(
Vector3.new(10, 15, 8),
Vector3.clamp(a, pos1, pos2)
)
end)
it("should work with multiple negative vectors", function()
local a = Vector3.new(-30, -99, -88)
local pos1 = Vector3.new(4, 4, 4)
local pos2 = Vector3.new(13, 13, 13)
assert.are.same(
Vector3.new(4, 4, 4),
Vector3.clamp(a, pos1, pos2)
)
end)
it("should work with multiple negative vectors with an irregular defined region", function()
local a = Vector3.new(-30, -99, -88)
local pos1 = Vector3.new(1, 5, 3)
local pos2 = Vector3.new(10, 15, 8)
assert.are.same(
Vector3.new(1, 5, 3),
Vector3.clamp(a, pos1, pos2)
)
end)
it("should work with multiple negative vectors with an irregular defined region with mixed signs", function()
local a = Vector3.new(-30, -99, -88)
local pos1 = Vector3.new(-1, 5, 3)
local pos2 = Vector3.new(10, 15, 8)
assert.are.same(
Vector3.new(-1, 5, 3),
Vector3.clamp(a, pos1, pos2)
)
end)
it("should return new Vector3 instances", function()
local a = Vector3.new(30, 3, 3)
local pos1 = Vector3.new(0, 0, 0)
local pos2 = Vector3.new(10, 10, 10)
local result = Vector3.clamp(a, pos1, pos2)
assert.are.same(Vector3.new(10, 3, 3), result)
result.y = 999
assert.are.same(Vector3.new(30, 3, 3), a)
assert.are.same(Vector3.new(0, 0, 0), pos1)
assert.are.same(Vector3.new(10, 10, 10), pos2)
end)
end)

@ -0,0 +1,40 @@
local polyfill = require("worldeditadditions.utils.strings.polyfill")
describe("str_ends", function()
it("should return true for a single character", function()
assert.are.equal(
true,
polyfill.str_ends("test", "t")
)
end)
it("should return true for a multiple characters", function()
assert.are.equal(
true,
polyfill.str_ends("test", "st")
)
end)
it("should return true for identical strings", function()
assert.are.equal(
true,
polyfill.str_ends("test", "test")
)
end)
it("should return false for a single character ", function()
assert.are.equal(
false,
polyfill.str_ends("test", "y")
)
end)
it("should return false for a character present elsewherer", function()
assert.are.equal(
false,
polyfill.str_ends("test", "e")
)
end)
it("should return false for another substring", function()
assert.are.equal(
false,
polyfill.str_ends("test", "tes")
)
end)
end)

@ -3,38 +3,38 @@ local polyfill = require("worldeditadditions.utils.strings.polyfill")
describe("str_starts", function()
it("should return true for a single character", function()
assert.are.equal(
polyfill.str_starts("test", "t"),
true
true,
polyfill.str_starts("test", "t")
)
end)
it("should return true for a multiple characters", function()
assert.are.equal(
polyfill.str_starts("test", "te"),
true
true,
polyfill.str_starts("test", "te")
)
end)
it("should return true for identical strings", function()
assert.are.equal(
polyfill.str_starts("test", "test"),
true
true,
polyfill.str_starts("test", "test")
)
end)
it("should return false for a single character ", function()
assert.are.equal(
polyfill.str_starts("test", "y"),
false
false,
polyfill.str_starts("test", "y")
)
end)
it("should return false for a character present elsewherer", function()
assert.are.equal(
polyfill.str_starts("test", "e"),
false
false,
polyfill.str_starts("test", "e")
)
end)
it("should return false for another substring", function()
assert.are.equal(
polyfill.str_starts("test", "est"),
false
false,
polyfill.str_starts("test", "est")
)
end)
end)

@ -3,18 +3,20 @@ It's about time I started a changelog! This will serve from now on as the main c
Note to self: See the bottom of this file for the release template text.
## v1.13: Untitled update (unreleased)
- Add `//sfactor` (_selection factor_) - Selection Tools by @VorTechnix are finished for now.
- Add `//mface` (_measure facing_), `//midpos` (_measure middle position_), `//msize` (_measure size_), `//mtrig` (_measure trigonometry_) - Measuring Tools implemented by @VorTechnix.
- Add `//airapply` for applying commands only to air nodes in the defined region
- Add `//wcorner` (_wireframe corners_), `//wbox` (_wireframe box_), `//compass` (_wireframe compass_) - Wireframes implemented by @VorTechnix.
- Add `//for` for executing commands while changing their arguments - Implemented by @VorTechnix.
- Add `//sshift` (_selection shift_) - WorldEdit cuboid manipulator replacements implemented by @VorTechnix.
- Add `//noise2d` for perturbing terrain with multiple different noise functions
- Add `//noiseapply2d` for running commands on columns where a noise value is over a threshold
- Add `//ellipsoid2` which creates an ellipsoid that fills the defined region
- Add `//spiral2` for creating both square and circular spirals
- Add `//copy+` for copying a defined region across multiple axes at once
## v1.13: The transformational update (2nd January 2022)
- Add [`//sfactor`](https://worldeditadditions.mooncarrot.space/Reference/#sfactor) (_selection factor_) - Selection Tools by @VorTechnix are finished for now.
- Add [`//mface`](https://worldeditadditions.mooncarrot.space/Reference/#mface) (_measure facing_), [`//midpos`](https://worldeditadditions.mooncarrot.space/Reference/#midpos) (_measure middle position_), [`//msize`](https://worldeditadditions.mooncarrot.space/Reference/#msize) (_measure size_), [`//mtrig`](#mtrig) (_measure trigonometry_) - Measuring Tools implemented by @VorTechnix.
- Add [`//airapply`](https://worldeditadditions.mooncarrot.space/Reference/#airapply) for applying commands only to air nodes in the defined region
- Add [`//wcorner`](https://worldeditadditions.mooncarrot.space/Reference/#wcorner) (_wireframe corners_), [`//wbox`](https://worldeditadditions.mooncarrot.space/Reference/#wbox) (_wireframe box_), [`//wcompass`](https://worldeditadditions.mooncarrot.space/Reference/#wcompass) (_wireframe compass_) - Wireframes implemented by @VorTechnix.
- Add [`//for`](https://worldeditadditions.mooncarrot.space/Reference/#for) for executing commands while changing their arguments - Implemented by @VorTechnix.
- Add [`//sshift`](https://worldeditadditions.mooncarrot.space/Reference/#sshift) (_selection shift_) - WorldEdit cuboid manipulator replacements implemented by @VorTechnix.
- Add [`//noise2d`](https://worldeditadditions.mooncarrot.space/Reference/#noise2d) for perturbing terrain with multiple different noise functions
- Add [`//noiseapply2d`](https://worldeditadditions.mooncarrot.space/Reference/#noiseapply2d) for running commands on columns where a noise value is over a threshold
- Add [`//ellipsoid2`](https://worldeditadditions.mooncarrot.space/Reference/#ellipsoid2) which creates an ellipsoid that fills the defined region
- Add [`//spiral2`](https://worldeditadditions.mooncarrot.space/Reference/#spiral2) for creating both square and circular spirals
- Add [`//copy+`](https://worldeditadditions.mooncarrot.space/Reference/#copy) for copying a defined region across multiple axes at once
- Add [`//move+`](https://worldeditadditions.mooncarrot.space/Reference/#move) for moving a defined region across multiple axes at once
- Add [`//sculpt`](https://worldeditadditions.mooncarrot.space/Reference/#sculpt) and [`//sculptlist`](https://worldeditadditions.mooncarrot.space/Reference/#sculptlist) for sculpting terrain using a number of custom brushes.
- Use [luacheck](https://github.com/mpeterv/luacheck) to find and fix a large number of bugs and other issues [code quality from now on will be significantly improved]
- Multiple commands: Allow using quotes (`"thing"`, `'thing'`) to quote values when splitting
- `//layers`: Add optional slope constraint (inspired by [WorldPainter](https://worldpainter.net/))
@ -24,7 +26,6 @@ Note to self: See the bottom of this file for the release template text.
### Bugfixes
- `//airapply`: Improve error handling, fix safe_region node counter
- `//floodfill`: Fix crash caused by internal refactoring of the `Queue` data structure
- `//spop`: Fix wording in displayed message
- Sapling alias compatibility:
@ -35,7 +36,15 @@ Note to self: See the bottom of this file for the release template text.
- `//replacemix`: Improve error handling to avoid crashes (thanks, Jonathon for reporting via Discord!)
- Cloud wand: Improve chat message text
- Fix `bonemeal` mod detection to look for the global `bonemeal`, not whether the `bonemeal` mod name has been loaded
- `//walls`: Prevent crash if not parameters are specified by defaulting to `dirt` as the replace_node
- `//bonemeal`: Fix argument parsing
- `//walls`: Prevent crash if no parameters are specified by defaulting to `dirt` as the replace_node
- `//maze`, `//maze3d`:
- Fix generated maze not reaching the very edge of the defined region
- Fix crash if no arguments are specified
- Fix automatic seed when generating many mazes in the same second (e.g. with `//for`, `//many`)
- `//convolve`: Fix those super tall pillars appearing randomly
- cloud wand: improve feedback messages sent to players
- `//forest`: Update sapling aliases for `bamboo``bambo:sprout` instead of `bamboo:sapling`
## v1.12: The selection tools update (26th June 2021)
@ -121,7 +130,7 @@ The text below is used as a template when making releases.
INTRO
See below for instructions on how to update.
See below the changelog for instructions on how to update.
CHANGELOG HERE

@ -616,6 +616,67 @@ Example invocations:
```
### `//sculptlist [preview]`
Lists all the available sculpting brushes for use with `//sculpt`. If the `preview` keyword is specified as an argument, then the brushes are also rendered in ASCII as a preview. See [`//sculpt`](#sculpt).
```
//sculptlist
//sculptlist preview
```
### `//sculpt [<brush_name=default> [<height=5> [<brush_size=10>]]]`
Applies a specified brush to the terrain at position 1 with a given height and a given size. Multiple brushes exist (see [`//sculptlist`](#sculptlist)) - and are represented as a 2D grid of values between 0 and 1, which are then scaled to the specified height. The terrain around position 1 is first converted to a 2D heightmap (as in [`//convolve`](#convolve) before the brush "heightmap" is applied to it.
Similar to [`//sphere`](https://github.com/Uberi/Minetest-WorldEdit/blob/master/ChatCommands.md#sphere-radius-node), [`//cubeapply 10 set`](https://github.com/Uberi/Minetest-WorldEdit/blob/master/ChatCommands.md#cubeapply-sizesizex-sizey-sizez-command-parameters), or [`//cylinder y 5 10 10 dirt`](https://github.com/Uberi/Minetest-WorldEdit/blob/master/ChatCommands.md#cylinder-xyz-length-radius1-radius2-node) (all from [WorldEdit](https://content.minetest.net/packages/sfan5/worldedit/)), but has a number of added advantages:
- No accidental overhangs
- Multiple custom brushes (see below on how you can add your own!)
A negative height value causes the terrain to be lowered by the specified number of nodes instead of raised.
While sculpting brushes cannot yet be rotated, this is a known issue. Rotating sculpting brushes will be implemented in a future version of WorldEditAdditions.
The selection of available brushes is limited at the moment, but see below on how to easily create your own!
```
//sculpt
//sculpt default 10 25
//sculpt ellipse
//sculpt circle 5 50
```
#### Create your own brushes
2 types of brush exist:
1. Dynamic (lua-generated) brushes
2. Static brushes
All brushes are located in `worldeditadditions/lib/sculpt/brushes` (relative to the root of WorldEditAdditions' installation directory).
Lua-generated brushes are not the focus here, but are a file with the extension `.lua` and return a function that returns a brush - see other existing Lua-generated brushes for examples (and don't forget to update `worldeditadditions/lib/sculpt/.init.lua`).
Static brushes on the other hand are simply a list of tab-separated values arranged in a grid. For example, here is a simple brush:
```tsv
0 1 0
1 2 1
0 1 0
```
Values are automatically rescaled to be between 0 and 1 based on the minimum and maximum values, so don't worry about which numbers to use. Static brushes are saved with the file extension `.brush.tsv` in the aforementioned directory, and are automatically rescanned when your Minetest server starts. While they can't be rescaled automatically to fix a target size (without creating multiple variants of a brush manually of course, though this may be implemented in the future), static brushes are much easier to create than dynamic brushes.
To assist with the creation of static brushes, a tool exists to convert any image to a static brush:
<https://worldeditadditions.mooncarrot.space/img2brush/>
The tool operates on the **alpha channel only**, so it's recommended to use an image format that supports transparency. All colours in the R, G, and B channels are ignored.
If you've created a cool new brush (be it static or dynamic), **please contribute it to WorldEditAdditions**! That way, everyone can enjoy using your awesome brush. [WorldPainter](https://www.worldpainter.net/) has many brushes available in the community, but `//sculpt` for WorldEditAdditions is new so don't have the same sized collection yet :-)
To contribute your new brush back, you can either [open a pull request](https://github.com/sbrl/Minetest-WorldEditAdditions/pulls) if you're confident using GitHub, or [open an issue](https://github.com/sbrl/Minetest-WorldEditAdditions/issues) with your brush attached if not.
## Flora
<!--
@ -1070,6 +1131,30 @@ Any suggestions on how to provide more customisability without making this comm
//noiseapply2d 0.4 3 layers dirt_with_snow dirt 3 stone 10
```
### `//for <value1> <value2> <value3>... do //<command> <arg> %% <arg>`
For a given list of values, executes the specified command for each value, replacing `%%` with each value. Implementation thanks to @VorTechnix.
To better illustrate what happens, consider the following command:
```
//for dirt stone glass do //replacemix air 10 %%
```
This is equivalent to executing the following 3 commands in sequence:
```
//replacemix air 10 dirt
//replacemix air 10 stone
//replacemix air 10 glass
```
Here are some more examples:
```
//for meselamp brick snowblock bamboo:wood do { //multi //maze %% //sshift y 5 }
//for 5 10 15 do //torus %% 3 dirt xz
```
## Extras

@ -25,7 +25,7 @@ _(Do you have a cool build that you used WorldEditAdditions to build? [Get in to
## Quick Command Reference
The detailed explanations have moved! Check them out [here](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md), or click the links below.
The detailed explanations have moved! Check them out [here](https://worldeditadditions.mooncarrot.space/Reference/) (or edit at [Chat-Command-Reference.md](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md)), or click the links below.
### Geometry
- [`//ellipsoid <rx> <ry> <rz> <node_name> [h[ollow]]`](https://worldeditadditions.mooncarrot.space/Reference/#ellipsoid)
@ -39,10 +39,13 @@ The detailed explanations have moved! Check them out [here](https://github.com/s
- [`//maze <replace_node> [<path_length> [<path_width> [<seed>]]]`](https://worldeditadditions.mooncarrot.space/Reference/#maze)
- [`//maze3d <replace_node> [<path_length> [<path_width> [<path_depth> [<seed>]]]]`](https://worldeditadditions.mooncarrot.space/Reference/#maze3d)
- [`//spiral2 [<circle|square>] [<replace_node=dirt> [<interval=3> [<acceleration=0>] ] ]`](https://worldeditadditions.mooncarrot.space/Reference/#spiral2)
- [`//wbox <replace_node>`](https://worldeditadditions.mooncarrot.space/Reference/#wbox)
- [`//wcompass <replace_node> [<bead_node>]`](https://worldeditadditions.mooncarrot.space/Reference/#wcompass)
- [`//wcorner <replace_node>`](https://worldeditadditions.mooncarrot.space/Reference/#wcorner)
### Misc
- [`//copy+ <axis:x|y|z|-x|-y|-z|?|front|back|left|right|up|down> <count> [<axis> <count> [...]]`](https://worldeditadditions.mooncarrot.space/Reference/#copy+)
- [`//move+ <axis:x|y|z|-x|-y|-z|?|front|back|left|right|up|down> <count> [<axis> <count> [...]]`](https://worldeditadditions.mooncarrot.space/Reference/#copy+)
- [`//copy+ <axis:x|y|z|-x|-y|-z|?|front|back|left|right|up|down> <count> [<axis> <count> [...]]`](https://worldeditadditions.mooncarrot.space/Reference/#copy)
- [`//move+ <axis:x|y|z|-x|-y|-z|?|front|back|left|right|up|down> <count> [<axis> <count> [...]]`](https://worldeditadditions.mooncarrot.space/Reference/#move)
- [`//replacemix <target_node> [<chance>] <replace_node_a> [<chance_a>] [<replace_node_b> [<chance_b>]] [<replace_node_N> [<chance_N>]] ....`](https://worldeditadditions.mooncarrot.space/Reference/#replacemix)
- [`//floodfill [<replace_node> [<radius>]]`](https://worldeditadditions.mooncarrot.space/Reference/#floodfill)
- [`//scale <axis> <scale_factor> | <factor_x> [<factor_y> <factor_z> [<anchor_x> <anchor_y> <anchor_z>]]`](https://worldeditadditions.mooncarrot.space/Reference/#scale) **experimental**
@ -54,6 +57,8 @@ The detailed explanations have moved! Check them out [here](https://github.com/s
- [`//convolve <kernel> [<width>[,<height>]] [<sigma>]`](https://worldeditadditions.mooncarrot.space/Reference/#convolve)
- [`//erode [<snowballs|river> [<key_1> [<value_1>]] [<key_2> [<value_2>]] ...]`](https://worldeditadditions.mooncarrot.space/Reference/#erode) **experimental**
- [`//noise2d [<key_1> [<value_1>]] [<key_2> [<value_2>]] ...]`](https://worldeditadditions.mooncarrot.space/Reference/#noise2d)
- [`//sculpt [<brush_name=default> [<height=5> [<brush_size=10>]]]`](https://worldeditadditions.mooncarrot.space/Reference/#sculpt)
- [`//sculptlist [preview]`](https://worldeditadditions.mooncarrot.space/Reference/#sculptlist)
### Flora
- [`//bonemeal [<strength> [<chance> [<node_name> [<node_name> ...]]]]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#bonemeal-strength-chance)
@ -75,6 +80,8 @@ The detailed explanations have moved! Check them out [here](https://github.com/s
- [`//sstack`](https://worldeditadditions.mooncarrot.space/Reference/#sstack)
- [`//spush`](https://worldeditadditions.mooncarrot.space/Reference/#spush)
- [`//spop`](https://worldeditadditions.mooncarrot.space/Reference/#spop)
- [`//sshift <axis1> <length1> [<axis2> <length2> [<axis3> <length3>]]`](https://worldeditadditions.mooncarrot.space/Reference/#sshift)
- [`//sfactor <mode:grow|shrink|average> <factor> [<target=xz>]`](https://worldeditadditions.mooncarrot.space/Reference/#sfactor)
### Measure
- [`//mface`](https://worldeditadditions.mooncarrot.space/Reference/#mface) _(new in v1.13)_
@ -89,6 +96,7 @@ The detailed explanations have moved! Check them out [here](https://github.com/s
- [`//ellipsoidapply <command_name> <args>`](https://worldeditadditions.mooncarrot.space/Reference/#ellipsoidapply) _(new in v1.9)_
- [`//airapply <command_name> <args>`](https://worldeditadditions.mooncarrot.space/Reference/#airapply) _(new in v1.9)_
- [`//noiseapply2d <threshold> <scale> <command_name> <args>`](https://worldeditadditions.mooncarrot.space/Reference/#noiseapply2d) _(new in v1.13)_
- [`//for <value1> <value2> <value3>... do //<command> <arg> %% <arg>`](https://worldeditadditions.mooncarrot.space/Reference/#for) _(new in v1.13)_
### Extras
- [`//y`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#y)

@ -18,8 +18,12 @@ wea.Queue = dofile(wea.modpath.."/utils/queue.lua")
wea.LRU = dofile(wea.modpath.."/utils/lru.lua")
wea.inspect = dofile(wea.modpath.."/utils/inspect.lua")
-- I/O compatibility layer
wea.io = dofile(wea.modpath.."/utils/io.lua")
wea.bit = dofile(wea.modpath.."/utils/bit.lua")
wea.terrain = dofile(wea.modpath.."/utils/terrain/init.lua")
dofile(wea.modpath.."/utils/vector.lua")
dofile(wea.modpath.."/utils/strings/init.lua")
@ -30,7 +34,7 @@ dofile(wea.modpath.."/utils/tables/init.lua")
dofile(wea.modpath.."/utils/numbers.lua")
dofile(wea.modpath.."/utils/nodes.lua")
dofile(wea.modpath.."/utils/node_identification.lua")
dofile(wea.modpath.."/utils/terrain.lua")
dofile(wea.modpath.."/utils/raycast_adv.lua") -- For the farwand
dofile(wea.modpath.."/utils/player.lua") -- Player info functions
@ -57,6 +61,7 @@ dofile(wea.modpath.."/lib/spiral_circle.lua")
dofile(wea.modpath.."/lib/conv/conv.lua")
dofile(wea.modpath.."/lib/erode/erode.lua")
dofile(wea.modpath.."/lib/noise/init.lua")
wea.sculpt = dofile(wea.modpath.."/lib/sculpt/init.lua")
dofile(wea.modpath.."/lib/copy.lua")
dofile(wea.modpath.."/lib/move.lua")
@ -77,3 +82,14 @@ dofile(wea.modpath.."/lib/selection/init.lua") -- Helpers for selections
dofile(wea.modpath.."/lib/wireframe/corner_set.lua")
dofile(wea.modpath.."/lib/wireframe/make_compass.lua")
dofile(wea.modpath.."/lib/wireframe/wire_box.lua")
---
-- Post-setup tasks
---
--- 1: Scan for an import static brushes
-- Static brushes live in lib/sculpt/brushes (relative to this file), and have
-- the file extension ".brush.tsv" (without quotes, of course).
wea.sculpt.scan_static(wea.modpath.."/lib/sculpt/brushes")

@ -84,7 +84,7 @@ if minetest.get_modpath("baldcypress") then
worldeditadditions.register_sapling_alias("baldcypress:sapling", "baldcypress")
end
if minetest.get_modpath("bamboo") then
worldeditadditions.register_sapling_alias("bamboo:sapling", "bamboo")
worldeditadditions.register_sapling_alias("bamboo:sprout", "bamboo")
end
if minetest.get_modpath("birch") then
worldeditadditions.register_sapling_alias("birch:sapling", "birch")

@ -1,9 +1,12 @@
worldeditadditions.conv = {}
local wea = worldeditadditions
local Vector3 = wea.Vector3
dofile(worldeditadditions.modpath.."/lib/conv/kernels.lua")
dofile(worldeditadditions.modpath.."/lib/conv/kernel_gaussian.lua")
wea.conv = {}
dofile(worldeditadditions.modpath.."/lib/conv/convolve.lua")
dofile(wea.modpath.."/lib/conv/kernels.lua")
dofile(wea.modpath.."/lib/conv/kernel_gaussian.lua")
dofile(wea.modpath.."/lib/conv/convolve.lua")
--- Creates a new kernel.
-- Note that the gaussian kernel only allows for creating a square kernel.
@ -12,7 +15,7 @@ dofile(worldeditadditions.modpath.."/lib/conv/convolve.lua")
-- @param width number The width of the kernel to create (must be an odd integer).
-- @param height number The height of the kernel to create (must be an odd integer).
-- @param arg number The argument to pass when creating the kernel. Currently only used by gaussian kernel as the sigma value.
function worldeditadditions.get_conv_kernel(name, width, height, arg)
function wea.get_conv_kernel(name, width, height, arg)
if width % 2 ~= 1 then
return false, "Error: The width must be an odd integer.";
end
@ -21,16 +24,16 @@ function worldeditadditions.get_conv_kernel(name, width, height, arg)
end
if name == "box" then
return true, worldeditadditions.conv.kernel_box(width, height)
return true, wea.conv.kernel_box(width, height)
elseif name == "pascal" then
return true, worldeditadditions.conv.kernel_pascal(width, height, true)
return true, wea.conv.kernel_pascal(width, height, true)
elseif name == "gaussian" then
if width ~= height then
return false, "Error: When using a gaussian kernel the width and height must be identical."
end
-- Default to sigma = 2
if arg == nil then arg = 2 end
local success, result = worldeditadditions.conv.kernel_gaussian(width, arg)
local success, result = wea.conv.kernel_gaussian(width, arg)
return success, result
end
@ -38,17 +41,19 @@ function worldeditadditions.get_conv_kernel(name, width, height, arg)
end
function worldeditadditions.convolve(pos1, pos2, kernel, kernel_size)
function wea.convolve(pos1, pos2, kernel, kernel_size)
pos1, pos2 = worldedit.sort_pos(pos1, pos2)
local border_size = {}
border_size[0] = (kernel_size[0]-1) / 2 -- height
border_size[1] = (kernel_size[1]-1) / 2 -- width
local border_size = Vector3.new(
(kernel_size.x-1) / 2, -- x = height
0,
(kernel_size.z-1) / 2 -- z = width
)
pos1.z = pos1.z - border_size[0]
pos2.z = pos2.z + border_size[0]
pos1.x = pos1.x - border_size[1]
pos2.x = pos2.x + border_size[1]
pos1.z = pos1.z - border_size.x
pos2.z = pos2.z + border_size.x
pos1.x = pos1.x - border_size.z
pos2.x = pos2.x + border_size.z
local manip, area = worldedit.manip_helpers.init(pos1, pos2)
local data = manip:get_data()
@ -57,10 +62,10 @@ function worldeditadditions.convolve(pos1, pos2, kernel, kernel_size)
local node_id_air = minetest.get_content_id("air")
local heightmap, heightmap_size = worldeditadditions.make_heightmap(pos1, pos2, manip, area, data)
local heightmap_conv = worldeditadditions.table.shallowcopy(heightmap)
local heightmap, heightmap_size = wea.terrain.make_heightmap(pos1, pos2, manip, area, data)
local heightmap_conv = wea.table.shallowcopy(heightmap)
worldeditadditions.conv.convolve(
wea.conv.convolve(
heightmap_conv,
heightmap_size,
kernel,
@ -68,11 +73,11 @@ function worldeditadditions.convolve(pos1, pos2, kernel, kernel_size)
)
-- print("original")
-- worldeditadditions.format.array_2d(heightmap, (pos2.z - pos1.z) + 1)
-- wea.format.array_2d(heightmap, (pos2.z - pos1.z) + 1)
-- print("transformed")
-- worldeditadditions.format.array_2d(heightmap_conv, (pos2.z - pos1.z) + 1)
-- wea.format.array_2d(heightmap_conv, (pos2.z - pos1.z) + 1)
worldeditadditions.apply_heightmap_changes(
wea.terrain.apply_heightmap_changes(
pos1, pos2, area, data,
heightmap, heightmap_conv, heightmap_size
)

@ -1,31 +1,39 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--[[
Convolves over a given 2D heightmap with a given matrix.
Note that this *mutates* the given heightmap.
Note also that the dimensions of the matrix must *only* be odd.
@param {number[]} heightmap The 2D heightmap to convolve over.
@param {[number,number]} heightmap_size The size of the heightmap as [ height, width ]
@param {number[]} matrix The matrix to convolve with.
@param {[number, number]} matrix_size The size of the convolution matrix as [ height, width ]
@param {number[]} heightmap The 2D heightmap to convolve over.
@param Vector3 heightmap_size The size of the heightmap as an X/Z Vector3 instance.
@param {number[]} matrix The matrix to convolve with.
@param Vector3 matrix_size The size of the convolution matrix as an X/Z Vector3 instance.
]]--
function worldeditadditions.conv.convolve(heightmap, heightmap_size, matrix, matrix_size)
if matrix_size[0] % 2 ~= 1 or matrix_size[1] % 2 ~= 1 then
if matrix_size.x % 2 ~= 1 or matrix_size.z % 2 ~= 1 then
return false, "Error: The matrix size must contain only odd numbers (even number detected)"
end
local border_size = {}
border_size[0] = (matrix_size[0]-1) / 2 -- height
border_size[1] = (matrix_size[1]-1) / 2 -- width
-- print("[convolve] matrix_size", matrix_size[0], matrix_size[1])
-- print("[convolve] border_size", border_size[0], border_size[1])
-- We need to reference a *copy* of the heightmap when convolving
-- This is because we need the original values when we perform a
-- convolution on a given pixel
local heightmap_copy = wea.table.shallowcopy(heightmap)
local border_size = Vector3.new(
(matrix_size.x-1) / 2, -- x = height
0,
(matrix_size.z-1) / 2 -- z = width
)
-- print("[convolve] matrix_size", matrix_size.x, matrix_size.z)
-- print("[convolve] border_size", border_size.x, border_size.z)
-- print("[convolve] heightmap_size: ", heightmap_size.z, heightmap_size.x)
--
-- print("[convolve] z: from", (heightmap_size.z-border_size[0]) - 1, "to", border_size[0], "step", -1)
-- print("[convolve] x: from", (heightmap_size.x-border_size[1]) - 1, "to", border_size[1], "step", -1)
-- print("[convolve] z: from", (heightmap_size.z-border_size.x) - 1, "to", border_size.x, "step", -1)
-- print("[convolve] x: from", (heightmap_size.x-border_size.z) - 1, "to", border_size.z, "step", -1)
-- Convolve over only the bit that allows us to use the full convolution matrix
for z = (heightmap_size.z-border_size[0]) - 1, border_size[0], -1 do
for x = (heightmap_size.x-border_size[1]) - 1, border_size[1], -1 do
for z = (heightmap_size.z-border_size.x) - 1, border_size.x, -1 do
for x = (heightmap_size.x-border_size.z) - 1, border_size.z, -1 do
local total = 0
@ -33,21 +41,22 @@ function worldeditadditions.conv.convolve(heightmap, heightmap_size, matrix, mat
-- print("[convolve/internal] z", z, "x", x, "hi", hi)
-- No continue statement in Lua :-/
if heightmap[hi] ~= -1 then
for mz = matrix_size[0]-1, 0, -1 do
for mx = matrix_size[1]-1, 0, -1 do
local mi = (mz * matrix_size[1]) + mx
local cz = z + (mz - border_size[0])
local cx = x + (mx - border_size[1])
if heightmap_copy[hi] ~= -1 then
for mz = matrix_size.x-1, 0, -1 do
for mx = matrix_size.z-1, 0, -1 do
local mi = (mz * matrix_size.z) + mx
local cz = z + (mz - border_size.x)
local cx = x + (mx - border_size.z)
local i = (cz * heightmap_size.x) + cx
-- A value of -1 = nothing in this column (so we should ignore it)
if heightmap[i] ~= -1 then
total = total + (matrix[mi] * heightmap[i])
if heightmap_copy[i] ~= -1 then
total = total + (matrix[mi] * heightmap_copy[i])
end
end
end
-- Rounding hack - ref https://stackoverflow.com/a/18313481/1460422
-- heightmap[hi] = math.floor(total + 0.5)
heightmap[hi] = math.ceil(total)

@ -1,10 +1,11 @@
worldeditadditions.erode = {}
local wea = worldeditadditions
wea.erode = {}
dofile(worldeditadditions.modpath.."/lib/erode/snowballs.lua")
dofile(worldeditadditions.modpath.."/lib/erode/river.lua")
dofile(wea.modpath.."/lib/erode/snowballs.lua")
dofile(wea.modpath.."/lib/erode/river.lua")
function worldeditadditions.erode.run(pos1, pos2, algorithm, params)
function wea.erode.run(pos1, pos2, algorithm, params)
pos1, pos2 = worldedit.sort_pos(pos1, pos2)
local manip, area = worldedit.manip_helpers.init(pos1, pos2)
@ -17,15 +18,15 @@ function worldeditadditions.erode.run(pos1, pos2, algorithm, params)
local region_height = (pos2.y - pos1.y) + 1
local heightmap = worldeditadditions.make_heightmap(pos1, pos2, manip, area, data)
local heightmap_eroded = worldeditadditions.table.shallowcopy(heightmap)
local heightmap = wea.terrain.make_heightmap(pos1, pos2, manip, area, data)
local heightmap_eroded = wea.table.shallowcopy(heightmap)
-- print("[erode.run] algorithm: "..algorithm..", params:");
-- print(worldeditadditions.format.map(params))
-- worldeditadditions.format.array_2d(heightmap, heightmap_size.x)
-- print(wea.format.map(params))
-- wea.format.array_2d(heightmap, heightmap_size.x)
local success, msg, stats
if algorithm == "snowballs" then
success, msg = worldeditadditions.erode.snowballs(
success, msg = wea.erode.snowballs(
heightmap, heightmap_eroded,
heightmap_size,
region_height,
@ -33,7 +34,7 @@ function worldeditadditions.erode.run(pos1, pos2, algorithm, params)
)
if not success then return success, msg end
elseif algorithm == "river" then
success, msg = worldeditadditions.erode.river(
success, msg = wea.erode.river(
heightmap, heightmap_eroded,
heightmap_size,
region_height,
@ -48,7 +49,7 @@ function worldeditadditions.erode.run(pos1, pos2, algorithm, params)
return false, "Error: Unknown algorithm '"..algorithm.."'. Currently implemented algorithms: snowballs (2d; hydraulic-like), river (2d; cellular automata-like; fills potholes and lowers towers). Ideas for algorithms to implement are welcome!"
end
success, stats = worldeditadditions.apply_heightmap_changes(
success, stats = wea.terrain.apply_heightmap_changes(
pos1, pos2, area, data,
heightmap, heightmap_eroded, heightmap_size
)

@ -1,3 +1,5 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
-- Test command: //multi //fp set1 1313 6 5540 //fp set2 1338 17 5521 //erode snowballs
@ -27,7 +29,7 @@ local function snowball(heightmap, normalmap, heightmap_size, startpos, params)
end
if #hist_velocity > 0 and i > 5
and worldeditadditions.average(hist_velocity) < 0.03 then
and wea.average(hist_velocity) < 0.03 then
-- print("[snowball] It looks like we've stopped")
return true, i
end
@ -50,11 +52,11 @@ local function snowball(heightmap, normalmap, heightmap_size, startpos, params)
velocity.x = params.friction * velocity.x + normalmap[hi].x * params.speed
velocity.z = params.friction * velocity.z + normalmap[hi].y * params.speed
-- print("[snowball] now at ("..x..", "..z..") velocity "..worldeditadditions.vector.lengthsquared(velocity)..", sediment "..sediment)
local new_vel_sq = worldeditadditions.vector.lengthsquared(velocity)
-- print("[snowball] now at ("..x..", "..z..") velocity "..wea.vector.lengthsquared(velocity)..", sediment "..sediment)
local new_vel_sq = wea.vector.lengthsquared(velocity)
if new_vel_sq > 1 then
-- print("[snowball] velocity squared over 1, normalising")
velocity = worldeditadditions.vector.normalize(velocity)
velocity = wea.vector.normalize(velocity)
end
table.insert(hist_velocity, new_vel_sq)
if #hist_velocity > params.velocity_hist_count then table.remove(hist_velocity, 1) end
@ -73,7 +75,7 @@ Note that this *mutates* the given heightmap.
@source https://jobtalle.com/simulating_hydraulic_erosion.html
]]--
function worldeditadditions.erode.snowballs(heightmap_initial, heightmap, heightmap_size, region_height, params_custom)
function wea.erode.snowballs(heightmap_initial, heightmap, heightmap_size, region_height, params_custom)
local params = {
rate_deposit = 0.03, -- 0.03
rate_erosion = 0.04, -- 0.04
@ -87,12 +89,12 @@ function worldeditadditions.erode.snowballs(heightmap_initial, heightmap, height
count = 25000
}
-- Apply the custom settings
worldeditadditions.table.apply(params_custom, params)
wea.table.apply(params_custom, params)
-- print("[erode/snowballs] params: ")
-- print(worldeditadditions.format.map(params))
-- print(wea.format.map(params))
local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size)
local normals = wea.terrain.calculate_normals(heightmap, heightmap_size)
local stats_steps = {}
for i = 1, params.count do
@ -110,7 +112,7 @@ function worldeditadditions.erode.snowballs(heightmap_initial, heightmap, height
if not success then return false, "Error: Failed at snowball "..i..":"..steps end
end
-- print("[snowballs] "..#stats_steps.." snowballs simulated, max "..params.max_steps.." steps, averaged ~"..worldeditadditions.average(stats_steps).."")
-- print("[snowballs] "..#stats_steps.." snowballs simulated, max "..params.max_steps.." steps, averaged ~"..wea.average(stats_steps).."")
-- Round everything to the nearest int, since you can't really have
-- something like .141592671 of a node
@ -130,15 +132,15 @@ function worldeditadditions.erode.snowballs(heightmap_initial, heightmap, height
end
if not params.noconv then
local success, matrix = worldeditadditions.get_conv_kernel("gaussian", 3, 3)
local success, matrix = wea.get_conv_kernel("gaussian", 3, 3)
if not success then return success, matrix end
local matrix_size = {} matrix_size[0] = 3 matrix_size[1] = 3
worldeditadditions.conv.convolve(
local matrix_size = Vector3.new(3, 0, 3)
wea.conv.convolve(
heightmap, heightmap_size,
matrix,
matrix_size
)
end
return true, ""..#stats_steps.." snowballs simulated, max "..params.max_steps.." steps (averaged ~"..worldeditadditions.average(stats_steps).." steps)"
return true, ""..#stats_steps.." snowballs simulated, max "..params.max_steps.." steps (averaged ~"..wea.average(stats_steps).." steps)"
end

@ -35,11 +35,11 @@ function worldeditadditions.layers(pos1, pos2, node_weights, min_slope, max_slop
local node_ids, node_ids_count = wea.unwind_node_list(node_weights)
local heightmap, heightmap_size = wea.make_heightmap(
local heightmap, heightmap_size = wea.terrain.make_heightmap(
pos1, pos2,
manip, area, data
)
local slopemap = wea.calculate_slopes(heightmap, heightmap_size)
local slopemap = wea.terrain.calculate_slopes(heightmap, heightmap_size)
-- worldeditadditions.format.array_2d(heightmap, heightmap_size.x)
-- print_slopes(slopemap, heightmap_size.x)
--luacheck:ignore 311

@ -27,7 +27,7 @@ local function generate_maze(seed, width, height, path_length, path_width)
width = width - 1
height = height - 1
local world = {}
for y = 0, height, 1 do
world[y] = {}
@ -48,13 +48,13 @@ local function generate_maze(seed, width, height, path_length, path_width)
if cy - path_length > 0 and world[cy - path_length][cx] == "#" then
directions = directions .. "u"
end
if cy + path_length < height-path_width and world[cy + path_length][cx] == "#" then
if cy + path_length < height-path_width+1 and world[cy + path_length][cx] == "#" then
directions = directions .. "d"
end
if cx - path_length > 0 and world[cy][cx - path_length] == "#" then
directions = directions .. "l"
end
if cx + path_length < width-path_width and world[cy][cx + path_length] == "#" then
if cx + path_length < width-path_width+1 and world[cy][cx + path_length] == "#" then
directions = directions .. "r"
end

@ -11,6 +11,10 @@ local Vector3 = wea.Vector3
-- ██ ██ ██████ ████ ███████
function worldeditadditions.move(source_pos1, source_pos2, target_pos1, target_pos2)
---
-- 0: Preamble
---
source_pos1, source_pos2 = Vector3.sort(source_pos1, source_pos2)
target_pos1, target_pos2 = Vector3.sort(target_pos1, target_pos2)
@ -27,7 +31,59 @@ function worldeditadditions.move(source_pos1, source_pos2, target_pos1, target_p
local manip_target, area_target = worldedit.manip_helpers.init(target_pos1, target_pos2)
local data_target = manip_target:get_data()
---
-- 1: Perform Copy
---
-- z y x is the preferred loop order (because CPU cache, since then we're iterating linearly through the data array backwards. This only holds true for little-endian machines however)
for z = source_pos2.z, source_pos1.z, -1 do
for y = source_pos2.y, source_pos1.y, -1 do
for x = source_pos2.x, source_pos1.x, -1 do
local source = Vector3.new(x, y, z)
local source_i = area_source:index(x, y, z)
local target = source:subtract(offset)
local target_i = area_target:index(target.x, target.y, target.z)
data_target[target_i] = data_source[source_i]
end
end
end
-- Save the modified nodes back to disk & return
-- Note that we save the source region *first* to avoid issues with overlap
-- BUG: Voxel Manipulators cover a larger area than the source area if the target position is too close the to the source, then the source will be overwritten by the target by accident.
-- Potential solution:
-- 1. Grab source & target
-- 2. Perform copy → target
-- 3. Save target
-- 4. Re-grab source
-- 5. Zero out source, but avoiding touching any nodes that fall within target
-- 6. Save source
---
-- 2: Save target
---
worldedit.manip_helpers.finish(manip_target, data_target)
---
-- 3: Re-grab source
---
manip_source, area_source = worldedit.manip_helpers.init(source_pos1, source_pos2)
data_source = manip_source:get_data()
---
-- 4: Zero out source, but avoiding touching any nodes that fall within target
---
for z = source_pos2.z, source_pos1.z, -1 do
for y = source_pos2.y, source_pos1.y, -1 do
@ -37,16 +93,25 @@ function worldeditadditions.move(source_pos1, source_pos2, target_pos1, target_p
local target = source:subtract(offset)
local target_i = area_target:index(target.x, target.y, target.z)
data_target[target_i] = data_source[source_i]
data_source[source_i] = node_id_air
if not source:is_contained(target_pos1, target_pos2) then
data_source[source_i] = node_id_air
end
end
end
end
-- Save the modified nodes back to disk & return
-- Note that we save the source region *first* to avoid issues with overlap
---
-- 5: Save source
---
worldedit.manip_helpers.finish(manip_source, data_source)
worldedit.manip_helpers.finish(manip_target, data_target)
---
-- 6: Finish up and return
---
return true, worldedit.volume(target_pos1, target_pos2)
end

@ -3,13 +3,13 @@ local wea = worldeditadditions
--- Applies the given noise field to the given heightmap.
-- Mutates the given heightmap.
-- @param heightmap number[] A table of ZERO indexed numbers representing the heghtmap - see worldeditadditions.make_heightmap().
-- @param heightmap number[] A table of ZERO indexed numbers representing the heghtmap - see worldeditadditions.terrain.make_heightmap().
-- @param noise number[] An table identical in structure to the heightmap containing the noise values to apply.
-- @param heightmap_size {x:number,z:number} A 2d vector representing the size of the heightmap.
-- @param region_height number The height of the defined region.
-- @param apply_mode string The apply mode to use to apply the noise to the heightmap.
-- @returns bool[,string] A boolean value representing whether the application was successful or not. If false, then an error message as a string is also returned describing the error that occurred.
function worldeditadditions.noise.apply_2d(heightmap, noise, heightmap_size, pos1, pos2, apply_mode)
function wea.noise.apply_2d(heightmap, noise, heightmap_size, pos1, pos2, apply_mode)
if type(apply_mode) ~= "string" and type(apply_mode) ~= "number" then
return false, "Error: Expected value of type string or number for apply_mode, but received value of type "..type(apply_mode)
end
@ -17,12 +17,12 @@ function worldeditadditions.noise.apply_2d(heightmap, noise, heightmap_size, pos
local region_height = pos2.y - pos1.y
print("NOISE APPLY_2D\n")
worldeditadditions.format.array_2d(noise, heightmap_size.x)
-- print("NOISE APPLY_2D\n")
wea.format.array_2d(noise, heightmap_size.x)
local height = tonumber(apply_mode)
print("DEBUG apply_mode", apply_mode, "as height", height)
-- print("DEBUG apply_mode", apply_mode, "as height", height)
for z = heightmap_size.z - 1, 0, -1 do
for x = heightmap_size.x - 1, 0, -1 do
@ -53,8 +53,8 @@ function worldeditadditions.noise.apply_2d(heightmap, noise, heightmap_size, pos
-- heightmap[(z * heightmap_size.x) + x] = z
-- end
print("HEIGHTMAP\n")
worldeditadditions.format.array_2d(heightmap, heightmap_size.x)
-- print("HEIGHTMAP\n")
-- worldeditadditions.format.array_2d(heightmap, heightmap_size.x)
return true

@ -12,25 +12,25 @@ local wea = worldeditadditions
-- @param pos1 Vector pos1 of the defined region
-- @param pos2 Vector pos2 of the defined region
-- @param noise_params table A noise parameters table.
function worldeditadditions.noise.run2d(pos1, pos2, noise_params)
function wea.noise.run2d(pos1, pos2, noise_params)
pos1, pos2 = worldedit.sort_pos(pos1, pos2)
-- pos2 will always have the highest co-ordinates now
-- Fill in the default params
-- print("DEBUG noise_params_custom ", wea.format.map(noise_params))
noise_params = worldeditadditions.noise.params_apply_default(noise_params)
noise_params = wea.noise.params_apply_default(noise_params)
-- print("DEBUG noise_params[1] ", wea.format.map(noise_params[1]))
-- Fetch the nodes in the specified area
local manip, area = worldedit.manip_helpers.init(pos1, pos2)
local data = manip:get_data()
local heightmap_old, heightmap_size = worldeditadditions.make_heightmap(
local heightmap_old, heightmap_size = wea.terrain.make_heightmap(
pos1, pos2,
manip, area,
data
)
local heightmap_new = worldeditadditions.table.shallowcopy(heightmap_old)
local heightmap_new = wea.table.shallowcopy(heightmap_old)
local success, noisemap = wea.noise.make_2d(
heightmap_size,
@ -49,7 +49,7 @@ function worldeditadditions.noise.run2d(pos1, pos2, noise_params)
if not success then return success, message end
local stats
success, stats = wea.apply_heightmap_changes(
success, stats = wea.terrain.apply_heightmap_changes(
pos1, pos2,
area, data,
heightmap_old, heightmap_new,

@ -0,0 +1,69 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- Applies the given brush with the given height and size to the given position.
-- @param pos1 Vector3 The position at which to apply the brush.
-- @param brush_name string The name of the brush to apply.
-- @param height number The height of the brush application.
-- @param brush_size Vector3 The size of the brush application. Values are interpreted on the X/Y coordinates, and NOT X/Z!
-- @returns bool, string|{ added: number, removed: number } A bool indicating whether the operation was successful or not, followed by either an error message as a string (if it was not successful) or a table of statistics (if it was successful).
local function apply(pos1, brush_name, height, brush_size)
-- 1: Get & validate brush
local success, brush, brush_size_actual = wea.sculpt.make_brush(brush_name, brush_size)
if not success then return success, brush end
-- print(wea.sculpt.make_preview(brush, brush_size_actual))
local brush_size_terrain = Vector3.new(
brush_size_actual.x,
0,
brush_size_actual.y
)
local brush_size_radius = (brush_size_terrain / 2):floor()
-- To try and make sure we catch height variations
local buffer = Vector3.new(0, math.min(height*2, 100), 0)
local pos1_compute = pos1 - brush_size_radius - buffer
local pos2_compute = pos1 + brush_size_radius + Vector3.new(0, height, 0) + buffer
pos1_compute, pos2_compute = Vector3.sort(
pos1_compute,
pos2_compute
)
-- 2: Fetch the nodes in the specified area, extract heightmap
local manip, area = worldedit.manip_helpers.init(pos1_compute, pos2_compute)
local data = manip:get_data()
local heightmap, heightmap_size = wea.terrain.make_heightmap(
pos1_compute, pos2_compute,
manip, area,
data
)
local heightmap_orig = wea.table.shallowcopy(heightmap)
local success2, added, removed = wea.sculpt.apply_heightmap(
brush, brush_size_actual,
height,
(heightmap_size / 2):floor(),
heightmap, heightmap_size
)
if not success2 then return success2, added end
-- 3: Save back to disk & return
local success3, stats = wea.terrain.apply_heightmap_changes(
pos1_compute, pos2_compute,
area, data,
heightmap_orig, heightmap,
heightmap_size
)
if not success3 then return success2, stats end
worldedit.manip_helpers.finish(manip, data)
return true, stats
end
return apply

@ -0,0 +1,54 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- Applies the given brush at the given x/z position to the given heightmap.
-- Important: Where a Vector3 is mentioned in the parameter list, it reall MUST
-- be a Vector3 instance.
-- Also important: Remember that the position there is RELATIVE TO THE HEIGHTMAP'S origin (0, 0) and is on the X and Z axes!
-- @param brush table The ZERO-indexed brush to apply. Values should be normalised to be between 0 and 1.
-- @param brush_size Vector3 The size of the brush on the x/y axes.
-- @pram height number The multiplier to apply to each brush pixel value just before applying it. Negative values are allowed - this will cause a subtraction operation instead of an addition.
-- @param position Vector3 The position RELATIVE TO THE HEIGHTMAP on the x/z coordinates to centre the brush application on.
-- @param heightmap table The heightmap to apply the brush to. See worldeditadditions.make_heightmap for how to obtain one of these.
-- @param heightmap_size Vector3 The size of the aforementioned heightmap. See worldeditadditions.make_heightmap for more information.
-- @returns true,number,number|false,string If the operation was not successful, then false followed by an error message as a string is returned. If it was successful however, 3 values are returned: true, then the number of nodes added, then the number of nodes removed.
local function apply_heightmap(brush, brush_size, height, position, heightmap, heightmap_size)
-- Convert brush_size to match the scheme used in the heightmap
brush_size = brush_size:clone()
brush_size.z = brush_size.y
brush_size.y = 0
local brush_radius = (brush_size/2):ceil() - 1
local pos_start = (position - brush_radius)
:clamp(Vector3.new(0, 0, 0), heightmap_size)
local pos_end = (pos_start + brush_size)
:clamp(Vector3.new(0, 0, 0), heightmap_size)
local added = 0
local removed = 0
-- Iterate over the heightmap and apply the brush
-- Note that we do not iterate over the brush, because we don't know if the
-- brush actually fits inside the region.... O.o
for z = pos_end.z - 1, pos_start.z, -1 do
for x = pos_end.x - 1, pos_start.x, -1 do
local hi = z*heightmap_size.x + x
local pos_brush = Vector3.new(x, 0, z) - pos_start
local bi = pos_brush.z*brush_size.x + pos_brush.x
local adjustment = math.floor(brush[bi]*height)
if adjustment > 0 then
added = added + adjustment
elseif adjustment < 0 then
removed = removed + math.abs(adjustment)
end
heightmap[hi] = heightmap[hi] + adjustment
end
end
return true, added, removed
end
return apply_heightmap

@ -0,0 +1,23 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- Returns a smooth gaussian brush.
-- @param size Vector3 The target size of the brush. Note that the actual size of the brush will be different, as the gaussian function has some limitations.
-- @param sigma=2 number The 'smoothness' of the brush. Higher values are more smooth.
return function(size, sigma)
local size = math.min(size.x, size.y)
if size % 2 == 0 then size = size - 1 end
if size < 1 then
return false, "Error: Invalid brush size."
end
local success, gaussian = wea.conv.kernel_gaussian(size, sigma)
-- Normalise values to fill the range 0 - 1
-- By default, wea.conv.kernel_gaussian values add up to 1 in total
local max = wea.max(gaussian)
for i=0,size*size-1 do
gaussian[i] = gaussian[i] / max
end
return success, gaussian, Vector3.new(size, size, 0)
end

@ -0,0 +1,24 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
return function(size)
local brush = {}
local centre = (size / 2):floor()
local minsize = math.floor(math.min(size.x, size.y) / 2)
for y = size.y - 1, 0, -1 do
for x = size.x - 1, 0, -1 do
local i = y*size.x + x
if math.floor((centre - Vector3.new(x, y, 0)):length()) < minsize then
brush[i] = 1
else
brush[i] = 0
end
end
end
return true, brush, size
end

@ -0,0 +1,46 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
return function(size)
local brush = {}
local centre = (size / 2):floor()
local minsize = math.floor(math.min(size.x, size.y) / 2)
local border = 1
local kernel_size = 3
-- Make the circle
-- We don't use 0 to 1 here, because we have to blur it and the existing convolutional
-- system rounds values.
for y = size.y - 1, 0, -1 do
for x = size.x - 1, 0, -1 do
local i = y*size.x + x
if math.floor((centre - Vector3.new(x, y, 0)):length()) < minsize - border then
brush[i] = 100000
else
brush[i] = 0
end
end
end
-- Make the kernel & blur it
local success, kernel = wea.conv.kernel_gaussian(kernel_size, 2)
if not success then return success, kernel end
local success2, msg = worldeditadditions.conv.convolve(
brush, Vector3.new(size.x, 0, size.y),
kernel, Vector3.new(kernel_size, 0, kernel_size)
)
if not success2 then return success2, msg end
-- Rescale to be between 0 and 1
local max_value = wea.max(brush)
for i,value in pairs(brush) do
brush[i] = brush[i] / max_value
end
return true, brush, size
end

@ -0,0 +1,27 @@
0.000 0.000 0.000 0.000 0.000 0.004 0.008 0.004 0.004 0.000 0.000 0.000 0.000
0.000 0.000 0.000 0.004 0.012 0.031 0.043 0.031 0.012 0.004 0.000 0.000 0.000
0.000 0.000 0.000 0.012 0.047 0.110 0.149 0.118 0.051 0.012 0.000 0.000 0.000
0.000 0.000 0.004 0.031 0.118 0.259 0.345 0.271 0.125 0.035 0.004 0.000 0.000
0.000 0.000 0.012 0.063 0.212 0.435 0.561 0.455 0.231 0.075 0.012 0.000 0.000
0.000 0.000 0.024 0.106 0.318 0.588 0.729 0.612 0.341 0.122 0.027 0.004 0.000
0.000 0.004 0.039 0.161 0.416 0.702 0.835 0.725 0.447 0.180 0.047 0.008 0.000
0.000 0.008 0.055 0.220 0.506 0.780 0.894 0.800 0.537 0.239 0.067 0.012 0.000
0.000 0.012 0.075 0.271 0.580 0.835 0.929 0.855 0.608 0.294 0.086 0.016 0.000
0.000 0.016 0.094 0.318 0.635 0.875 0.953 0.886 0.659 0.341 0.110 0.020 0.000
0.004 0.024 0.118 0.357 0.678 0.902 0.965 0.906 0.698 0.376 0.129 0.027 0.004
0.004 0.031 0.137 0.392 0.710 0.914 0.973 0.918 0.718 0.404 0.145 0.031 0.004
0.004 0.035 0.153 0.412 0.725 0.922 0.976 0.922 0.729 0.416 0.157 0.035 0.004
0.004 0.035 0.161 0.424 0.733 0.922 0.976 0.922 0.729 0.420 0.157 0.035 0.004
0.004 0.035 0.161 0.424 0.733 0.922 0.976 0.922 0.722 0.408 0.149 0.035 0.004
0.004 0.035 0.149 0.412 0.722 0.922 0.973 0.914 0.706 0.388 0.137 0.031 0.004
0.004 0.027 0.137 0.388 0.706 0.910 0.969 0.902 0.678 0.361 0.118 0.024 0.004
0.004 0.020 0.114 0.349 0.671 0.894 0.957 0.875 0.639 0.318 0.094 0.016 0.000
0.000 0.016 0.090 0.302 0.620 0.859 0.933 0.835 0.580 0.271 0.075 0.012 0.000
0.000 0.012 0.071 0.251 0.549 0.812 0.898 0.780 0.506 0.220 0.059 0.008 0.000
0.000 0.008 0.047 0.192 0.459 0.733 0.839 0.702 0.420 0.165 0.039 0.004 0.000
0.000 0.004 0.027 0.129 0.349 0.624 0.733 0.592 0.318 0.110 0.024 0.000 0.000
0.000 0.000 0.016 0.075 0.235 0.463 0.565 0.439 0.212 0.063 0.012 0.000 0.000
0.000 0.000 0.008 0.035 0.129 0.275 0.345 0.263 0.118 0.031 0.004 0.000 0.000
0.000 0.000 0.004 0.012 0.051 0.118 0.153 0.114 0.047 0.012 0.000 0.000 0.000
0.000 0.000 0.000 0.004 0.012 0.031 0.043 0.031 0.012 0.004 0.000 0.000 0.000
0.000 0.000 0.000 0.000 0.004 0.004 0.008 0.004 0.000 0.000 0.000 0.000 0.000
1 0.000 0.000 0.000 0.000 0.000 0.004 0.008 0.004 0.004 0.000 0.000 0.000 0.000
2 0.000 0.000 0.000 0.004 0.012 0.031 0.043 0.031 0.012 0.004 0.000 0.000 0.000
3 0.000 0.000 0.000 0.012 0.047 0.110 0.149 0.118 0.051 0.012 0.000 0.000 0.000
4 0.000 0.000 0.004 0.031 0.118 0.259 0.345 0.271 0.125 0.035 0.004 0.000 0.000
5 0.000 0.000 0.012 0.063 0.212 0.435 0.561 0.455 0.231 0.075 0.012 0.000 0.000
6 0.000 0.000 0.024 0.106 0.318 0.588 0.729 0.612 0.341 0.122 0.027 0.004 0.000
7 0.000 0.004 0.039 0.161 0.416 0.702 0.835 0.725 0.447 0.180 0.047 0.008 0.000
8 0.000 0.008 0.055 0.220 0.506 0.780 0.894 0.800 0.537 0.239 0.067 0.012 0.000
9 0.000 0.012 0.075 0.271 0.580 0.835 0.929 0.855 0.608 0.294 0.086 0.016 0.000
10 0.000 0.016 0.094 0.318 0.635 0.875 0.953 0.886 0.659 0.341 0.110 0.020 0.000
11 0.004 0.024 0.118 0.357 0.678 0.902 0.965 0.906 0.698 0.376 0.129 0.027 0.004
12 0.004 0.031 0.137 0.392 0.710 0.914 0.973 0.918 0.718 0.404 0.145 0.031 0.004
13 0.004 0.035 0.153 0.412 0.725 0.922 0.976 0.922 0.729 0.416 0.157 0.035 0.004
14 0.004 0.035 0.161 0.424 0.733 0.922 0.976 0.922 0.729 0.420 0.157 0.035 0.004
15 0.004 0.035 0.161 0.424 0.733 0.922 0.976 0.922 0.722 0.408 0.149 0.035 0.004
16 0.004 0.035 0.149 0.412 0.722 0.922 0.973 0.914 0.706 0.388 0.137 0.031 0.004
17 0.004 0.027 0.137 0.388 0.706 0.910 0.969 0.902 0.678 0.361 0.118 0.024 0.004
18 0.004 0.020 0.114 0.349 0.671 0.894 0.957 0.875 0.639 0.318 0.094 0.016 0.000
19 0.000 0.016 0.090 0.302 0.620 0.859 0.933 0.835 0.580 0.271 0.075 0.012 0.000
20 0.000 0.012 0.071 0.251 0.549 0.812 0.898 0.780 0.506 0.220 0.059 0.008 0.000
21 0.000 0.008 0.047 0.192 0.459 0.733 0.839 0.702 0.420 0.165 0.039 0.004 0.000
22 0.000 0.004 0.027 0.129 0.349 0.624 0.733 0.592 0.318 0.110 0.024 0.000 0.000
23 0.000 0.000 0.016 0.075 0.235 0.463 0.565 0.439 0.212 0.063 0.012 0.000 0.000
24 0.000 0.000 0.008 0.035 0.129 0.275 0.345 0.263 0.118 0.031 0.004 0.000 0.000
25 0.000 0.000 0.004 0.012 0.051 0.118 0.153 0.114 0.047 0.012 0.000 0.000 0.000
26 0.000 0.000 0.000 0.004 0.012 0.031 0.043 0.031 0.012 0.004 0.000 0.000 0.000
27 0.000 0.000 0.000 0.000 0.004 0.004 0.008 0.004 0.000 0.000 0.000 0.000 0.000

@ -0,0 +1,8 @@
local wea = worldeditadditions
local __smooth = dofile(wea.modpath.."/lib/sculpt/brushes/__gaussian.lua")
return function(size)
local success, brush, size_actual = __smooth(size, 3)
return success, brush, size_actual
end

@ -0,0 +1,8 @@
local wea = worldeditadditions
local __smooth = dofile(wea.modpath.."/lib/sculpt/brushes/__gaussian.lua")
return function(size)
local success, brush, size_actual = __smooth(size, 2)
return success, brush, size_actual
end

@ -0,0 +1,8 @@
local wea = worldeditadditions
local __smooth = dofile(wea.modpath.."/lib/sculpt/brushes/__gaussian.lua")
return function(size)
local success, brush, size_actual = __smooth(size, 5)
return success, brush, size_actual
end

@ -0,0 +1,11 @@
--- Returns a simple square brush with 100% weight for every pixel.
return function(size)
local result = {}
for y=0, size.y do
for x=0, size.x do
result[y*size.x + x] = 1
end
end
return true, result, size
end

@ -0,0 +1,23 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
local parse_static = dofile(wea.modpath.."/lib/sculpt/parse_static.lua")
--- Reads and parses the brush stored in the specified file.
-- @param filepath string The path to file that contains the static brush to read in.
-- @returns true,table,Vector3|false,string A success boolean, followed either by an error message as a string or the brush (as a table) and it's size (as an X/Y Vector3)
return function(filepath)
local handle = io.open(filepath)
if not handle then
handle:close()
return false, "Error: Failed to open the static brush file at '"..filepath.."'."
end
local data = handle:read("*all")
handle:close()
local success, brush, brush_size = parse_static(data)
if not success then return success, brush end
return true, brush, brush_size
end

@ -0,0 +1,31 @@
local wea = worldeditadditions
local sculpt = {
brushes = {
circle_soft1 = dofile(wea.modpath.."/lib/sculpt/brushes/circle_soft1.lua"),
circle = dofile(wea.modpath.."/lib/sculpt/brushes/circle.lua"),
square = dofile(wea.modpath.."/lib/sculpt/brushes/square.lua"),
gaussian_hard = dofile(wea.modpath.."/lib/sculpt/brushes/gaussian_hard.lua"),
gaussian = dofile(wea.modpath.."/lib/sculpt/brushes/gaussian.lua"),
gaussian_soft = dofile(wea.modpath.."/lib/sculpt/brushes/gaussian_soft.lua"),
},
make_brush = dofile(wea.modpath.."/lib/sculpt/make_brush.lua"),
make_preview = dofile(wea.modpath.."/lib/sculpt/make_preview.lua"),
preview_brush = dofile(wea.modpath.."/lib/sculpt/preview_brush.lua"),
read_brush_static = dofile(wea.modpath.."/lib/sculpt/read_brush_static.lua"),
apply_heightmap = dofile(wea.modpath.."/lib/sculpt/apply_heightmap.lua"),
apply = dofile(wea.modpath.."/lib/sculpt/apply.lua"),
scan_static = dofile(wea.modpath.."/lib/sculpt/scan_static.lua"),
import_static = dofile(wea.modpath.."/lib/sculpt/import_static.lua"),
parse_static = dofile(wea.modpath.."/lib/sculpt/parse_static.lua")
}
-- scan_sculpt is called after everything is loaded in the main init file
return sculpt
-- TODO: Automatically find & register all text file based brushes in the brushes directory
-- TODO: Implement automatic scaling of static brushes to the correct size. We have scale already, but we probably need to implement a proper 2d canvas scaling algorithm. Some options to consider: linear < [bi]cubic < nohalo/lohalo
-- Note that we do NOT automatically find & register computed brushes because that's an easy way to execute arbitrary Lua code & cause a security issue unless handled very carefully

@ -0,0 +1,24 @@
local wea = worldeditadditions
--- Makes a sculpting brush that is as close to a target size as possible.
-- @param brush_name string The name of the brush to create.
-- @param target_size Vector3 The target size of the brush to create.
-- @returns true,table,Vector3|false,string If the operation was successful, true followed by the brush in a 1D ZERO-indexed table followed by the actual size of the brush as a Vector3 (x & y components used only). If the operation was not successful, false and an error message string is returned instead.
local function make_brush(brush_name, target_size)
if not wea.sculpt.brushes[brush_name] then return false, "Error: That brush does not exist. Try using //sculptbrushes to list all available sculpting brushes." end
local brush_def = wea.sculpt.brushes[brush_name]
local success, brush, size_actual
if type(brush_def) == "function" then
success, brush, size_actual = brush_def(target_size)
if not success then return success, brush end
else
brush = brush_def.brush
size_actual = brush_def.size
end
return true, brush, size_actual
end
return make_brush

@ -0,0 +1,54 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
local make_brush = dofile(wea.modpath.."/lib/sculpt/make_brush.lua")
--- Generates a textual preview of a given brush.
-- @param brush table The brush in question to preview.
-- @param size Vector3 The size of the brush.
-- @returns string A preview of the brush as a string.
local function make_preview(brush, size, framed)
if framed == nil then framed = true end
-- Values to map brush pixel values to.
-- Brush pixel values are first multiplied by 10 before comparing to these numbers
local values = {}
values["@"] = 9.5
values["#"] = 8
values["="] = 6
values[":"] = 5
values["-"] = 4
values["."] = 1
values[" "] = 0
local frame_vertical = "+"..string.rep("-", math.max(0, size.x)).."+"
local result = {}
if framed then table.insert(result, frame_vertical) end
for y = size.y-1, 0, -1 do
local row = {}
if framed then table.insert(row, "|") end
for x = size.x-1, 0, -1 do
local i = y*size.x + x
local pixel = " "
local threshold_cur = -1
for value,threshold in pairs(values) do
if brush[i] * 10 > threshold and threshold_cur < threshold then
pixel = value
threshold_cur = threshold
end
end
table.insert(row, pixel)
end
if framed then table.insert(row, "|") end
table.insert(result, table.concat(row))
end
if framed then table.insert(result, frame_vertical) end
return table.concat(result, "\n")
end
return make_preview

@ -0,0 +1,48 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- Parses a static brush definition.
-- @param source string The source string that contains the static brush, formatted as TSV.
-- @returns true,table,Vector3|false,string A success boolean, followed either by an error message as a string or the brush (as a table) and it's size (as an X/Y Vector3)
return function(source)
local width = -1
local height
local maxvalue, minvalue, range
-- Parse out the TSV into a table of tables, while also parsing values as numbers
-- Also keeps track of the maximum/minimum values found for rescaling later.
local values = wea.table.map(
wea.split(source, "\n", false),
function(line)
local row = wea.split(line, "%s+", false)
width = math.max(width, #row)
return wea.table.map(
row,
function(pixel)
local value = tonumber(pixel)
if not value then value = 0 end
if maxvalue == nil or value > maxvalue then
maxvalue = value
end
if minvalue == nil or value < minvalue then
minvalue = value
end
return value
end
)
end
)
height = #values
range = maxvalue - minvalue
local brush = {}
for y,row in ipairs(values) do
for x,value in ipairs(row) do
local i = (y-1)*width + (x-1)
brush[i] = (value - minvalue) / range
end
end
return true, brush, Vector3.new(width, height, 0)
end

@ -0,0 +1,21 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
local make_brush = dofile(wea.modpath.."/lib/sculpt/make_brush.lua")
local make_preview = dofile(wea.modpath.."/lib/sculpt/make_preview.lua")
--- Generates a textual preview of a given brush.
-- @param brush_name string The name of the brush to create a preview for.
-- @param target_size Vector3 The target size of the brush to create. Default: (10, 10, 0).
-- @returns bool,string If the operation was successful, true followed by a preview of the brush as a string. If the operation was not successful, false and an error message string is returned instead.
local function preview_brush(brush_name, target_size, framed)
if framed == nil then framed = true end
if not target_size then target_size = Vector3.new(10, 10, 0) end
local success, brush, brush_size = make_brush(brush_name, target_size)
if not success then return success, brush end
return true, make_preview(brush, brush_size, framed)
end
return preview_brush

@ -0,0 +1,11 @@
return function(filepath)
local brush_size = { x = 0, y = 0 }
local brush = { }
-- TODO: Import brush here
return false, "Error: Not implemented yet"
-- return true, brush, brush_size
end

@ -0,0 +1,48 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
local import_static = dofile(wea.modpath.."/lib/sculpt/import_static.lua")
local function import_filepath(filepath, name, overwrite_existing)
if overwrite_existing and wea.sculpt.brushes[name] ~= nil then
return false, "Error: A brush with the name '"..name.."' already exists."
end
local success, brush, brush_size = import_static(filepath)
if not success then return success, "Error while reading from '"..filepath.."': "..brush end
wea.sculpt.brushes[name] = {
brush = brush,
size = brush_size
}
return true
end
--- Scans the given directory and imports all static brushes found.
-- Static brushes have the file extension ".brush.tsv" (without quotes).
-- @param dirpath string The path to directory that contains the static brushs to import.
-- @returns bool,loaded,errors A success boolean, followed by the number of brushes loaded, followed by the number of errors encountered while loading brushes (errors are logged as warnings with Minetest)
return function(dirpath, overwrite_existing)
if overwrite_existing == nil then overwrite_existing = false end
local files = wea.io.scandir_files(dirpath)
local brushes_loaded = 0
local errors = 0
for i, filename in pairs(files) do
if wea.str_ends(filename, ".brush.tsv") then
local filepath = dirpath.."/"..filename
local name = filename:gsub(".brush.tsv", "")
local success, msg = import_filepath(filepath, name, overwrite_existing)
if not success then
minetest.log("warning", "[WorldEditAdditions:sculpt] Encountered error when loading brush from '"..filepath.."':"..msg)
end
brushes_loaded = brushes_loaded + 1
end
end
return true, brushes_loaded, errors
end

@ -3,7 +3,7 @@
-- ███████ █████ ██ █████ ██ ██ ██ ██ ██ ██ ██ ██
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ███████ ███████ ███████ ██████ ██ ██ ██████ ██ ████
local v3 = worldeditadditions.Vector3
---Selection helpers and modifiers
local selection = {}
@ -13,7 +13,7 @@ local selection = {}
-- @param pos vector The position to include.
function selection.add_point(name, pos)
if pos ~= nil then
local is_new = not worldedit.pos1[name] and not worldedit.pos2[name]
local created_new = not worldedit.pos1[name] or not worldedit.pos2[name]
-- print("[set_pos1]", name, "("..pos.x..", "..pos.y..", "..pos.z..")")
if not worldedit.pos1[name] then worldedit.pos1[name] = vector.new(pos) end
if not worldedit.pos2[name] then worldedit.pos2[name] = vector.new(pos) end
@ -27,18 +27,17 @@ function selection.add_point(name, pos)
local volume_after = worldedit.volume(worldedit.pos1[name], worldedit.pos2[name])
local volume_difference = volume_after - volume_before
if volume_difference == 0 and created_new then
volume_difference = 1
end
local msg = "Expanded region by "
if created_new then msg = "Created new region with " end
msg = msg..volume_difference.." node"
if volume_difference ~= 1 then msg = msg.."s" end
worldedit.marker_update(name)
-- print("DEBUG volume_before", volume_before, "volume_after", volume_after)
if is_new then
local msg = "Created new region of "..volume_after.." node"
if volume_after ~= 1 then msg = msg.."s" end
worldedit.player_notify(name, msg)
else
local msg = "Expanded region by "..volume_difference.." node"
if volume_difference ~= 1 then msg = msg.."s" end
worldedit.player_notify(name, msg)
end
worldedit.player_notify(name, msg)
else
worldedit.player_notify(name, "Error: Too far away (try raising your maxdist with //farwand maxdist <number>)")
-- print("[set_pos1]", name, "nil")
@ -75,24 +74,4 @@ function selection.check_dir(str)
return (str == "front" or str == "back" or str == "left" or str == "right" or str == "up" or str == "down")
end
--- Makes two vectors from the given positions and expands or contracts them
-- (based on mode) by a third provided vector.
-- @param mode string "grow" | "shrink".
-- @param pos1 vector worldedit pos1.
-- @param pos2 vector worldedit pos2.
-- @param vec vector The modifying vector.
-- @return Vector3,Vector3 New vectors for worldedit positions.
function selection.resize(mode, pos1, pos2, vec)
local pos1, pos2 = v3.sort(pos1, pos2)
local vmin = v3.min(vec,v3.new()):abs()
local vmax = v3.max(vec,v3.new())
if mode == "grow" then
return pos1 - vmin, pos2 + vMax
elseif mode == "shrink" then
return pos1 + vmin, pos2 - vMax
else
error("Resize Error: invalid mode \""..tostring(mode).."\".")
end
end
return selection

@ -12,8 +12,8 @@ function worldeditadditions.corner_set(pos1,pos2,node)
-- z y x is the preferred loop order (because CPU cache I'd guess, since then we're iterating linearly through the data array)
local counts = { replaced = 0 }
for k,z in pairs({pos1.z,pos2.z}) do
for k,y in pairs({pos1.y,pos2.y}) do
for i,z in pairs({pos1.z,pos2.z}) do
for j,y in pairs({pos1.y,pos2.y}) do
for k,x in pairs({pos1.x,pos2.x}) do
minetest.set_node(vector.new(x,y,z), {name=node})
counts.replaced = counts.replaced + 1

@ -13,27 +13,27 @@
-- module: bit
local bit
local bit_local
if minetest.global_exists("bit") then
bit = bit
bit_local = bit
else
bit = {}
bit.bits = 32
bit.powtab = { 1 }
for b = 1, bit.bits - 1 do
bit.powtab[#bit.powtab + 1] = math.pow(2, b)
bit_local = {
bits = 32,
powtab = { 1 }
}
for b = 1, bit_local.bits - 1 do
bit_local.powtab[#bit_local.powtab + 1] = math.pow(2, b)
end
end
-- Functions
-- bit.band
if not bit.band then
bit.band = function(a, b)
-- bit_local.band
if not bit_local.band then
bit_local.band = function(a, b)
local result = 0
for x = 1, bit.bits do
for x = 1, bit_local.bits do
result = result + result
if (a < 0) then
if (b < 0) then
@ -47,11 +47,11 @@ if not bit.band then
end
end
-- bit.bor
if not bit.bor then
bit.bor = function(a, b)
-- bit_local.bor
if not bit_local.bor then
bit_local.bor = function(a, b)
local result = 0
for x = 1, bit.bits do
for x = 1, bit_local.bits do
result = result + result
if (a < 0) then
result = result + 1
@ -65,47 +65,47 @@ if not bit.bor then
end
end
-- bit.bnot
if not bit.bnot then
bit.bnot = function(x)
return bit.bxor(x, math.pow((bit.bits or math.floor(math.log(x, 2))), 2) - 1)
-- bit_local.bnot
if not bit_local.bnot then
bit_local.bnot = function(x)
return bit_local.bxor(x, math.pow((bit_local.bits or math.floor(math.log(x, 2))), 2) - 1)
end
end
-- bit.lshift
if not bit.lshift then
bit.lshift = function(a, n)
if (n > bit.bits) then
-- bit_local.lshift
if not bit_local.lshift then
bit_local.lshift = function(a, n)
if (n > bit_local.bits) then
a = 0
else
a = a * bit.powtab[n]
a = a * bit_local.powtab[n]
end
return a
end
end
-- bit.rshift
if not bit.rshift then
bit.rshift = function(a, n)
if (n > bit.bits) then
-- bit_local.rshift
if not bit_local.rshift then
bit_local.rshift = function(a, n)
if (n > bit_local.bits) then
a = 0
elseif (n > 0) then
if (a < 0) then
a = a - bit.powtab[#bit.powtab]
a = a / bit.powtab[n]
a = a + bit.powtab[bit.bits - n]
a = a - bit_local.powtab[#bit_local.powtab]
a = a / bit_local.powtab[n]
a = a + bit_local.powtab[bit_local.bits - n]
else
a = a / bit.powtab[n]
a = a / bit_local.powtab[n]
end
end
return a
end
end
-- bit.arshift
if not bit.arshift then
bit.arshift = function(a, n)
if (n >= bit.bits) then
-- bit_local.arshift
if not bit_local.arshift then
bit_local.arshift = function(a, n)
if (n >= bit_local.bits) then
if (a < 0) then
a = -1
else
@ -113,22 +113,22 @@ if not bit.arshift then
end
elseif (n > 0) then
if (a < 0) then
a = a - bit.powtab[#bit.powtab]
a = a / bit.powtab[n]
a = a - bit.powtab[bit.bits - n]
a = a - bit_local.powtab[#bit_local.powtab]
a = a / bit_local.powtab[n]
a = a - bit_local.powtab[bit_local.bits - n]
else
a = a / bit.powtab[n]
a = a / bit_local.powtab[n]
end
end
return a
end
end
-- bit.bxor
if not bit.bxor then
bit.bxor = function(a, b)
-- bit_local.bxor
if not bit_local.bxor then
bit_local.bxor = function(a, b)
local result = 0
for x = 1, bit.bits, 1 do
for x = 1, bit_local.bits, 1 do
result = result + result
if (a < 0) then
if (b >= 0) then
@ -144,40 +144,40 @@ if not bit.bxor then
end
end
-- bit.rol
if not bit.rol then
bit.rol = function(a, b)
local bits = bit.band(b, bit.bits - 1)
a = bit.band(a, 0xffffffff)
a = bit.bor(bit.lshift(a, b), bit.rshift(a, ((bit.bits - 1) - b)))
return bit.band(n, 0xffffffff)
-- bit_local.rol
if not bit_local.rol then
bit_local.rol = function(a, b)
local bits = bit_local.band(b, bit_local.bits - 1)
a = bit_local.band(a, 0xffffffff)
a = bit_local.bor(bit_local.lshift(a, b), bit_local.rshift(a, ((bit_local.bits - 1) - b)))
return bit_local.band(n, 0xffffffff)
end
end
-- bit.ror
if not bit.ror then
bit.ror = function(a, b)
return bit.rol(a, - b)
-- bit_local.ror
if not bit_local.ror then
bit_local.ror = function(a, b)
return bit_local.rol(a, - b)
end
end
-- bit.bswap
if not bit.bswap then
bit.bswap = function(n)
local a = bit.band(n, 0xff)
n = bit.rshift(n, 8)
local b = bit.band(n, 0xff)
n = bit.rshift(n, 8)
local c = bit.band(n, 0xff)
n = bit.rshift(n, 8)
local d = bit.band(n, 0xff)
return bit.lshift(bit.lshift(bit.lshift(a, 8) + b, 8) + c, 8) + d
-- bit_local.bswap
if not bit_local.bswap then
bit_local.bswap = function(n)
local a = bit_local.band(n, 0xff)
n = bit_local.rshift(n, 8)
local b = bit_local.band(n, 0xff)
n = bit_local.rshift(n, 8)
local c = bit_local.band(n, 0xff)
n = bit_local.rshift(n, 8)
local d = bit_local.band(n, 0xff)
return bit_local.lshift(bit_local.lshift(bit_local.lshift(a, 8) + b, 8) + c, 8) + d
end
end
-- bit.tobit
if not bit.tobit then
bit.tobit = function(n)
-- bit_local.tobit
if not bit_local.tobit then
bit_local.tobit = function(n)
local MOD = 2^32
n = n % MOD
if (n >= 0x80000000) then
@ -187,9 +187,9 @@ if not bit.tobit then
end
end
-- bit.tohex
if not bit.tohex then
bit.tohex = function(x, n)
-- bit_local.tohex
if not bit_local.tohex then
bit_local.tohex = function(x, n)
n = n or 8
local up
if n <= 0 then
@ -199,9 +199,9 @@ if not bit.tohex then
up = true
n = -n
end
x = bit.band(x, 16^n - 1)
x = bit_local.band(x, 16^n - 1)
return ('%0'..n..(up and 'X' or 'x')):format(x)
end
end
return bit
return bit_local

@ -1,5 +1,10 @@
--- Serialises an arbitrary value to a string.
-- Note that although the resulting table *looks* like valid Lua, it isn't.
-- Completely arbitrarily, if a table (or it's associated metatable) has the
-- key __name then it is conidered the name of the parent metatable. This can
-- be useful for identifying custom table-based types.
-- Should anyone come across a 'proper' way to obtain the name of a metatable
-- in pure vanilla Lua, I will update this to follow that standard instead.
-- @param item any Input item to serialise.
-- @param sep string key value seperator
-- @param new_line string key value pair delimiter
@ -12,7 +17,13 @@ local function inspect(item, maxdepth)
end
if maxdepth < 1 then return "[truncated]" end
local result = { "{\n" }
local result = { }
-- Consider our (arbitrarily decided) property __name to the type of this item
-- Remember that this implicitly checks the metatable so long as __index is set.
if type(item.__name) == "string" then
table.insert(result, "("..item.__name..") ")
end
table.insert(result, "{\n")
for key,value in pairs(item) do
local value_text = inspect(value, maxdepth - 1)
:gsub("\n", "\n\t")

@ -0,0 +1,14 @@
local io = {
-- Ref https://minetest.gitlab.io/minetest/minetest-namespace-reference/#utilities
scandir = function(dirpath)
return minetest.get_dir_list(dirpath, nil)
end,
scandir_files = function(dirpath)
return minetest.get_dir_list(dirpath, false)
end,
scandir_dirs = function(dirpath)
return minetest.get_dir_list(dirpath, true)
end,
}
return io

@ -33,7 +33,7 @@ end
function worldeditadditions.min(list)
if #list == 0 then return nil end
local min = nil
for i,value in ipairs(list) do
for i,value in pairs(list) do
if min == nil or min > value then
min = value
end
@ -46,8 +46,11 @@ end
-- @returns number The maximum value in the given list.
function worldeditadditions.max(list)
if #list == 0 then return nil end
-- We use pairs() instead of ipairs() here, because then we can support
-- zero-indexed 1D heightmaps too - and we don't care that the order is
-- undefined with pairs().
local max = nil
for i,value in ipairs(list) do
for i,value in pairs(list) do
if max == nil or max < value then
max = value
end

@ -37,12 +37,14 @@ local function tokenise(str)
local char = str:sub(nextpos, nextpos)
if char == "}" then
-- Decrease the nested depth
nested_depth = nested_depth - 1
-- Pop the start of this block off the stack and find this block's contents
local block_start = table.remove(nested_stack, #nested_stack)
local substr = str:sub(block_start, nextpos - 1)
if #substr > 0 and nested_depth == 0 then table.insert(result, substr) end
if nested_depth > 0 then
-- Decrease the nested depth
nested_depth = nested_depth - 1
-- Pop the start of this block off the stack and find this block's contents
local block_start = table.remove(nested_stack, #nested_stack)
local substr = str:sub(block_start, nextpos - 1)
if #substr > 0 and nested_depth == 0 then table.insert(result, substr) end
end
elseif char == "{" then
-- Increase the nested depth, and store this position on the stack for later
nested_depth = nested_depth + 1

@ -26,6 +26,14 @@ local function str_starts(str, start)
return string.sub(str, 1, string.len(start)) == start
end
--- Equivalent to string.endsWith in JS
-- @param str string The string to operate on
-- @param substr_end number The ending string to look for
-- @returns bool Whether substr_end is present at the end of str
local function str_ends(str, substr_end)
return string.sub(str, -string.len(substr_end)) == substr_end
end
--- Trims whitespace from a string from the beginning and the end.
-- From http://lua-users.org/wiki/StringTrim
-- @param str string The string to trim the whitespace from.
@ -39,12 +47,14 @@ if worldeditadditions then
worldeditadditions.str_padend = str_padend
worldeditadditions.str_padstart = str_padstart
worldeditadditions.str_starts = str_starts
worldeditadditions.str_ends = str_ends
worldeditadditions.trim = trim
else
return {
str_padend = str_padend,
str_padstart = str_padstart,
str_starts = str_starts,
str_ends = str_ends,
trim = trim
}
end

@ -62,7 +62,7 @@ end
-- @param plain boolean If true (or truthy), pattern is interpreted as a
-- plain string, not a Lua pattern
-- @returns table A sequence table containing the substrings
function worldeditadditions.split (str,dlm,plain)
function worldeditadditions.split(str,dlm,plain)
local pos, ret = 0, {}
local ins, i = str:find(dlm,pos,plain)
-- "if plain" shaves off some time in the while statement

@ -5,7 +5,7 @@ local function is_whitespace(char)
return char:match("%s")
end
local function split_shell(text)
local function split_shell(text, autotrim)
local text_length = #text
local scan_pos = 1
local result = { }
@ -26,7 +26,10 @@ local function split_shell(text)
if mode == "NORMAL" then
if is_whitespace(curchar) and #acc > 0 then
table.insert(result, table.concat(acc, ""))
local nextval = worldeditadditions.trim(table.concat(acc, ""))
if #nextval > 0 then
table.insert(result, table.concat(acc, ""))
end
acc = {}
elseif (curchar == "\"" or curchar == "'") and #acc == 0 and prevchar ~= "\\" then
if curchar == "\"" then

@ -1,178 +0,0 @@
local wea = worldeditadditions
--- Given a manip object and associates, generates a 2D x/z heightmap.
-- Note that pos1 and pos2 should have already been pushed through
-- worldedit.sort_pos(pos1, pos2) before passing them to this function.
-- @param pos1 Vector Position 1 of the region to operate on
-- @param pos2 Vector Position 2 of the region to operate on
-- @param manip VoxelManip The VoxelManip object.
-- @param area area The associated area object.
-- @param data table The associated data object.
-- @return table,table The ZERO-indexed heightmap data (as 1 single flat array), followed by the size of the heightmap in the form { z = size_z, x = size_x }.
function worldeditadditions.make_heightmap(pos1, pos2, manip, area, data)
-- z y x (in reverse for little-endian machines) is the preferred loop order, but that isn't really possible here
local heightmap = {}
local hi = 0
local changes = { updated = 0, skipped_columns = 0 }
for z = pos1.z, pos2.z, 1 do
for x = pos1.x, pos2.x, 1 do
local found_node = false
-- Scan each column top to bottom
for y = pos2.y+1, pos1.y, -1 do
local i = area:index(x, y, z)
if not (wea.is_airlike(data[i]) or wea.is_liquidlike(data[i])) then
-- It's the first non-airlike node in this column
-- Start heightmap values from 1 (i.e. there's at least 1 node in the column)
heightmap[hi] = (y - pos1.y) + 1
found_node = true
break
end
end
if not found_node then heightmap[hi] = -1 end
hi = hi + 1
end
end
local heightmap_size = {
z = (pos2.z - pos1.z) + 1,
x = (pos2.x - pos1.x) + 1
}
return heightmap, heightmap_size
end
--- Calculates a normal map for the given heightmap.
-- Caution: This method (like worldeditadditions.make_heightmap) works on
-- X AND Z, and NOT x and y. This means that the resulting 3d normal vectors
-- will have the z and y values swapped.
-- @param heightmap table A ZERO indexed flat heightmap. See worldeditadditions.make_heightmap().
-- @param heightmap_size int[] The size of the heightmap in the form [ z, x ]
-- @return Vector[] The calculated normal map, in the same form as the input heightmap. Each element of the array is a Vector3 instance representing a normal.
function worldeditadditions.calculate_normals(heightmap, heightmap_size)
-- print("heightmap_size: "..heightmap_size.x.."x"..heightmap_size.z)
local result = {}
for z = heightmap_size.z-1, 0, -1 do
for x = heightmap_size.x-1, 0, -1 do
-- Algorithm ref https://stackoverflow.com/a/13983431/1460422
-- Also ref Vector.mjs, which I implemented myself (available upon request)
local hi = z*heightmap_size.x + x
-- Default to this pixel's height
local up = heightmap[hi]
local down = heightmap[hi]
local left = heightmap[hi]
local right = heightmap[hi]
if z - 1 > 0 then up = heightmap[(z-1)*heightmap_size.x + x] end
if z + 1 < heightmap_size.z-1 then down = heightmap[(z+1)*heightmap_size.x + x] end
if x - 1 > 0 then left = heightmap[z*heightmap_size.x + (x-1)] end
if x + 1 < heightmap_size.x-1 then right = heightmap[z*heightmap_size.x + (x+1)] end
-- print("[normals] UP | index", (z-1)*heightmap_size.x + x, "z", z, "z-1", z - 1, "up", up, "limit", 0)
-- print("[normals] DOWN | index", (z+1)*heightmap_size.x + x, "z", z, "z+1", z + 1, "down", down, "limit", heightmap_size.x-1)
-- print("[normals] LEFT | index", z*heightmap_size.x + (x-1), "x", x, "x-1", x - 1, "left", left, "limit", 0)
-- print("[normals] RIGHT | index", z*heightmap_size.x + (x+1), "x", x, "x+1", x + 1, "right", right, "limit", heightmap_size.x-1)
result[hi] = wea.Vector3.new(
left - right, -- x
2, -- y - Z & Y are flipped
down - up -- z
):normalise()
-- print("[normals] at "..hi.." ("..x..", "..z..") normal "..result[hi])
end
end
return result
end
--- Converts a 2d heightmap into slope values in radians.
-- Convert a radians to degrees by doing (radians*math.pi) / 180 for display,
-- but it is STRONGLY recommended to keep all internal calculations in radians.
-- @param heightmap table A ZERO indexed flat heightmap. See worldeditadditions.make_heightmap().
-- @param heightmap_size int[] The size of the heightmap in the form [ z, x ]
-- @return Vector[] The calculated slope map, in the same form as the input heightmap. Each element of the array is a (floating-point) number representing the slope in that cell in radians.
function worldeditadditions.calculate_slopes(heightmap, heightmap_size)
local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size)
local slopes = { }
local up = wea.Vector3.new(0, 1, 0) -- Z & Y are flipped
for z = heightmap_size.z-1, 0, -1 do
for x = heightmap_size.x-1, 0, -1 do
local hi = z*heightmap_size.x + x
-- Ref https://stackoverflow.com/a/16669463/1460422
-- slopes[hi] = wea.Vector3.dot_product(normals[hi], up)
slopes[hi] = math.acos(normals[hi].y)
end
end
return slopes
end
--- Applies changes to a heightmap to a Voxel Manipulator data block.
-- @param pos1 vector Position 1 of the defined region
-- @param pos2 vector Position 2 of the defined region
-- @param area VoxelArea The VoxelArea object (see worldedit.manip_helpers.init)
-- @param data number[] The node ids data array containing the slice of the Minetest world extracted using the Voxel Manipulator.
-- @param heightmap_old number[] The original heightmap from worldeditadditions.make_heightmap.
-- @param heightmap_new number[] The new heightmap containing the altered updated values. It is expected that worldeditadditions.table.shallowcopy be used to make a COPY of the data worldeditadditions.make_heightmap for this purpose. Both heightmap_old AND heightmap_new are REQUIRED in order for this function to work.
-- @param heightmap_size vector The x / z size of the heightmap. Any y value set in the vector is ignored.
-- @returns bool, string|{ added: number, removed: number } A bool indicating whether the operation was successful or not, followed by either an error message as a string (if it was not successful) or a table of statistics (if it was successful).
function worldeditadditions.apply_heightmap_changes(pos1, pos2, area, data, heightmap_old, heightmap_new, heightmap_size)
local stats = { added = 0, removed = 0 }
local node_id_air = minetest.get_content_id("air")
local node_id_ignore = minetest.get_content_id("ignore")
for z = heightmap_size.z - 1, 0, -1 do
for x = heightmap_size.x - 1, 0, -1 do
local hi = z*heightmap_size.x + x
local height_old = heightmap_old[hi]
local height_new = heightmap_new[hi]
-- print("[conv/save] hi", hi, "height_old", heightmap_old[hi], "height_new", heightmap_new[hi], "z", z, "x", x, "pos1.y", pos1.y)
-- Lua doesn't have a continue statement :-/
if height_old == height_new then
-- noop
elseif height_new < height_old then
local node_id_replace = data[area:index(
pos1.x + x,
pos1.y + height_old + 1,
pos1.z + z
)]
-- Unlikely, but if it can happen, it *will* happen.....
if node_id_replace == node_id_ignore then
node_id_replace = node_id_air
end
stats.removed = stats.removed + (height_old - height_new)
local y = height_new
while y < height_old do
local ci = area:index(pos1.x + x, pos1.y + y, pos1.z + z)
-- print("[conv/save] remove at y", y, "→", pos1.y + y, "current:", minetest.get_name_from_content_id(data[ci]))
if data[ci] ~= node_id_ignore then
data[ci] = node_id_replace
end
y = y + 1
end
else -- height_new > height_old
-- We subtract one because the heightmap starts at 1 (i.e. 1 = 1 node in the column), but the selected region is inclusive
local node_id = data[area:index(pos1.x + x, pos1.y + (height_old - 1), pos1.z + z)]
-- print("[conv/save] filling with ", node_id, "→", minetest.get_name_from_content_id(node_id))
stats.added = stats.added + (height_new - height_old)
local y = height_old
while y < height_new do
local ci = area:index(pos1.x + x, pos1.y + y, pos1.z + z)
-- print("[conv/save] add at y", y, "→", pos1.y + y, "current:", minetest.get_name_from_content_id(data[ci]))
if data[ci] ~= node_id_ignore then
data[ci] = node_id
end
y = y + 1
end
end
end
end
return true, stats
end

@ -0,0 +1,71 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- Applies changes to a heightmap to a Voxel Manipulator data block.
-- @param pos1 vector Position 1 of the defined region
-- @param pos2 vector Position 2 of the defined region
-- @param area VoxelArea The VoxelArea object (see worldedit.manip_helpers.init)
-- @param data number[] The node ids data array containing the slice of the Minetest world extracted using the Voxel Manipulator.
-- @param heightmap_old number[] The original heightmap from worldeditadditions.make_heightmap.
-- @param heightmap_new number[] The new heightmap containing the altered updated values. It is expected that worldeditadditions.table.shallowcopy be used to make a COPY of the data worldeditadditions.make_heightmap for this purpose. Both heightmap_old AND heightmap_new are REQUIRED in order for this function to work.
-- @param heightmap_size vector The x / z size of the heightmap. Any y value set in the vector is ignored.
-- @returns bool, string|{ added: number, removed: number } A bool indicating whether the operation was successful or not, followed by either an error message as a string (if it was not successful) or a table of statistics (if it was successful).
local function apply_heightmap_changes(pos1, pos2, area, data, heightmap_old, heightmap_new, heightmap_size)
local stats = { added = 0, removed = 0 }
local node_id_air = minetest.get_content_id("air")
local node_id_ignore = minetest.get_content_id("ignore")
for z = heightmap_size.z - 1, 0, -1 do
for x = heightmap_size.x - 1, 0, -1 do
local hi = z*heightmap_size.x + x
local height_old = heightmap_old[hi]
local height_new = heightmap_new[hi]
-- print("[conv/save] hi", hi, "height_old", heightmap_old[hi], "height_new", heightmap_new[hi], "z", z, "x", x, "pos1.y", pos1.y)
-- Lua doesn't have a continue statement :-/
if height_old == height_new then
-- noop
elseif height_new < height_old then
local node_id_replace = data[area:index(
pos1.x + x,
pos1.y + height_old + 1,
pos1.z + z
)]
-- Unlikely, but if it can happen, it *will* happen.....
if node_id_replace == node_id_ignore then
node_id_replace = node_id_air
end
stats.removed = stats.removed + (height_old - height_new)
local y = height_new
while y < height_old do
local ci = area:index(pos1.x + x, pos1.y + y, pos1.z + z)
-- print("[conv/save] remove at y", y, "→", pos1.y + y, "current:", minetest.get_name_from_content_id(data[ci]))
if data[ci] ~= node_id_ignore then
data[ci] = node_id_replace
end
y = y + 1
end
else -- height_new > height_old
-- We subtract one because the heightmap starts at 1 (i.e. 1 = 1 node in the column), but the selected region is inclusive
local node_id = data[area:index(pos1.x + x, pos1.y + (height_old - 1), pos1.z + z)]
-- print("[conv/save] filling with ", node_id, "→", minetest.get_name_from_content_id(node_id))
stats.added = stats.added + (height_new - height_old)
local y = height_old
while y < height_new do
local ci = area:index(pos1.x + x, pos1.y + y, pos1.z + z)
-- print("[conv/save] add at y", y, "→", pos1.y + y, "current:", minetest.get_name_from_content_id(data[ci]))
if data[ci] ~= node_id_ignore then
data[ci] = node_id
end
y = y + 1
end
end
end
end
return true, stats
end
return apply_heightmap_changes

@ -0,0 +1,47 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- Calculates a normal map for the given heightmap.
-- Caution: This method (like worldeditadditions.make_heightmap) works on
-- X AND Z, and NOT x and y. This means that the resulting 3d normal vectors
-- will have the z and y values swapped.
-- @param heightmap table A ZERO indexed flat heightmap. See worldeditadditions.terrain.make_heightmap().
-- @param heightmap_size int[] The size of the heightmap in the form [ z, x ]
-- @return Vector[] The calculated normal map, in the same form as the input heightmap. Each element of the array is a Vector3 instance representing a normal.
local function calculate_normals(heightmap, heightmap_size)
-- print("heightmap_size: "..heightmap_size.x.."x"..heightmap_size.z)
local result = {}
for z = heightmap_size.z-1, 0, -1 do
for x = heightmap_size.x-1, 0, -1 do
-- Algorithm ref https://stackoverflow.com/a/13983431/1460422
-- Also ref Vector.mjs, which I implemented myself (available upon request)
local hi = z*heightmap_size.x + x
-- Default to this pixel's height
local up = heightmap[hi]
local down = heightmap[hi]
local left = heightmap[hi]
local right = heightmap[hi]
if z - 1 > 0 then up = heightmap[(z-1)*heightmap_size.x + x] end
if z + 1 < heightmap_size.z-1 then down = heightmap[(z+1)*heightmap_size.x + x] end
if x - 1 > 0 then left = heightmap[z*heightmap_size.x + (x-1)] end
if x + 1 < heightmap_size.x-1 then right = heightmap[z*heightmap_size.x + (x+1)] end
-- print("[normals] UP | index", (z-1)*heightmap_size.x + x, "z", z, "z-1", z - 1, "up", up, "limit", 0)
-- print("[normals] DOWN | index", (z+1)*heightmap_size.x + x, "z", z, "z+1", z + 1, "down", down, "limit", heightmap_size.x-1)
-- print("[normals] LEFT | index", z*heightmap_size.x + (x-1), "x", x, "x-1", x - 1, "left", left, "limit", 0)
-- print("[normals] RIGHT | index", z*heightmap_size.x + (x+1), "x", x, "x+1", x + 1, "right", right, "limit", heightmap_size.x-1)
result[hi] = Vector3.new(
left - right, -- x
2, -- y - Z & Y are flipped
down - up -- z
):normalise()
-- print("[normals] at "..hi.." ("..x..", "..z..") normal "..result[hi])
end
end
return result
end
return calculate_normals

@ -0,0 +1,30 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- Converts a 2d heightmap into slope values in radians.
-- Convert a radians to degrees by doing (radians*math.pi) / 180 for display,
-- but it is STRONGLY recommended to keep all internal calculations in radians.
-- @param heightmap table A ZERO indexed flat heightmap. See worldeditadditions.terrain.make_heightmap().
-- @param heightmap_size int[] The size of the heightmap in the form [ z, x ]
-- @return Vector[] The calculated slope map, in the same form as the input heightmap. Each element of the array is a (floating-point) number representing the slope in that cell in radians.
local function calculate_slopes(heightmap, heightmap_size)
local normals = wea.terrain.calculate_normals(heightmap, heightmap_size)
local slopes = { }
local up = wea.Vector3.new(0, 1, 0) -- Z & Y are flipped
for z = heightmap_size.z-1, 0, -1 do
for x = heightmap_size.x-1, 0, -1 do
local hi = z*heightmap_size.x + x
-- Ref https://stackoverflow.com/a/16669463/1460422
-- slopes[hi] = wea.Vector3.dot_product(normals[hi], up)
slopes[hi] = math.acos(normals[hi].y)
end
end
return slopes
end
return calculate_slopes

@ -0,0 +1,12 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
local terrain = {
make_heightmap = dofile(wea.modpath.."/utils/terrain/make_heightmap.lua"),
calculate_normals = dofile(wea.modpath.."/utils/terrain/calculate_normals.lua"),
calculate_slopes = dofile(wea.modpath.."/utils/terrain/calculate_slopes.lua"),
apply_heightmap_changes = dofile(wea.modpath.."/utils/terrain/apply_heightmap_changes.lua")
}
return terrain

@ -0,0 +1,50 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- Given a manip object and associates, generates a 2D x/z heightmap.
-- Note that pos1 and pos2 should have already been pushed through
-- worldedit.sort_pos(pos1, pos2) before passing them to this function.
-- @param pos1 Vector Position 1 of the region to operate on
-- @param pos2 Vector Position 2 of the region to operate on
-- @param manip VoxelManip The VoxelManip object.
-- @param area area The associated area object.
-- @param data table The associated data object.
-- @return table,table The ZERO-indexed heightmap data (as 1 single flat array), followed by the size of the heightmap in the form { z = size_z, x = size_x }.
local function make_heightmap(pos1, pos2, manip, area, data)
-- z y x (in reverse for little-endian machines) is the preferred loop order, but that isn't really possible here
local heightmap = {}
local hi = 0
local changes = { updated = 0, skipped_columns = 0 }
for z = pos1.z, pos2.z, 1 do
for x = pos1.x, pos2.x, 1 do
local found_node = false
-- Scan each column top to bottom
for y = pos2.y+1, pos1.y, -1 do
local i = area:index(x, y, z)
if not (wea.is_airlike(data[i]) or wea.is_liquidlike(data[i])) then
-- It's the first non-airlike node in this column
-- Start heightmap values from 1 (i.e. there's at least 1 node in the column)
heightmap[hi] = (y - pos1.y) + 1
found_node = true
break
end
end
if not found_node then heightmap[hi] = -1 end
hi = hi + 1
end
end
local heightmap_size = Vector3.new(
(pos2.x - pos1.x) + 1, -- x
0, -- y
(pos2.z - pos1.z) + 1 -- z
)
return heightmap, heightmap_size
end
return make_heightmap

@ -2,6 +2,7 @@
-- @class
local Vector3 = {}
Vector3.__index = Vector3
Vector3.__name = "Vector3" -- A hack to allow identification in wea.inspect
--- Creates a new Vector3 instance.
-- @param x number The x co-ordinate value.
@ -295,6 +296,17 @@ function Vector3.is_contained(target, pos1, pos2)
and pos2.z >= target.z
end
--- Clamps the given point to fall within the region defined by 2 points.
-- @param a Vector3 The target vector to clamp.
-- @param pos1 Vector3 pos1 of the defined region.
-- @param pos2 Vector3 pos2 of the defined region.
-- @returns Vector3 The target vector, clamped to fall within the defined region.
function Vector3.clamp(a, pos1, pos2)
pos1, pos2 = Vector3.sort(pos1, pos2)
return Vector3.min(Vector3.max(a, pos1), pos2)
end
--- Expands the defined region to include the given point.
-- @param target Vector3 The target vector to include.

@ -1,4 +1,5 @@
local we_c = worldeditadditions_commands
local wea = worldeditadditions
-- ██████ ██████ ███ ██ ███████ ███ ███ ███████ █████ ██
-- ██ ██ ██ ██ ████ ██ ██ ████ ████ ██ ██ ██ ██
@ -15,19 +16,19 @@ worldedit.register_command("bonemeal", {
params_text = "1"
end
local parts = worldeditadditions.split_shell(params_text, "%s+", false)
local parts = wea.split_shell(params_text)
local strength = 1
local chance = 1
local node_names = {} -- An empty table means all nodes
if #parts >= 1 then
strength = tonumber(table.remove(parts, 1))
strength = tonumber(wea.trim(table.remove(parts, 1)))
if not strength then
return false, "Invalid strength value (value must be an integer)"
end
end
if #parts >= 2 then
if #parts >= 1 then
chance = worldeditadditions.parse.chance(table.remove(parts, 1))
if not chance then
return false, "Invalid chance value (must be a positive integer)"

@ -1,3 +1,6 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
-- ██████ ██████ ███ ██ ██ ██ ██████ ██ ██ ██ ███████
-- ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █████
@ -11,8 +14,8 @@ worldedit.register_command("convolve", {
parse = function(params_text)
if not params_text then params_text = "" end
-- local parts = worldeditadditions.split(params_text, "%s+", false)
local parts = worldeditadditions.split_shell(params_text)
-- local parts = wea.split(params_text, "%s+", false)
local parts = wea.split_shell(params_text)
local kernel_name = "gaussian"
local width = 5
@ -23,7 +26,7 @@ worldedit.register_command("convolve", {
kernel_name = parts[1]
end
if #parts >= 2 then
local parts_dimension = worldeditadditions.split(parts[2], ",%s*", false)
local parts_dimension = wea.split(parts[2], ",%s*", false)
width = tonumber(parts_dimension[1])
if not width then
return false, "Error: Invalid width (it must be a positive odd integer)."
@ -50,26 +53,33 @@ worldedit.register_command("convolve", {
return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name])
end,
func = function(name, kernel_name, kernel_width, kernel_height, sigma)
local start_time = worldeditadditions.get_ms_time()
local start_time = wea.get_ms_time()
local success, kernel = worldeditadditions.get_conv_kernel(kernel_name, kernel_width, kernel_height, sigma)
local success, kernel = wea.get_conv_kernel(kernel_name, kernel_width, kernel_height, sigma)
if not success then return success, kernel end
local kernel_size = {}
kernel_size[0] = kernel_height
kernel_size[1] = kernel_width
local kernel_size = Vector3.new(
kernel_height,
0,
kernel_width
)
local pos1, pos2 = Vector3.sort(
worldedit.pos1[name],
worldedit.pos2[name]
)
local stats
success, stats = worldeditadditions.convolve(
worldedit.pos1[name], worldedit.pos2[name],
success, stats = wea.convolve(
pos1, pos2,
kernel, kernel_size
)
if not success then return success, stats end
local time_taken = worldeditadditions.get_ms_time() - start_time
local time_taken = wea.get_ms_time() - start_time
minetest.log("action", name.." used //convolve at "..worldeditadditions.vector.tostring(worldedit.pos1[name]).." - "..worldeditadditions.vector.tostring(worldedit.pos2[name])..", adding "..stats.added.." nodes and removing "..stats.removed.." nodes in "..time_taken.."s")
return true, "Added "..stats.added.." and removed "..stats.removed.." nodes in " .. worldeditadditions.format.human_time(time_taken)
minetest.log("action", name.." used //convolve at "..pos1.." - "..pos2..", adding "..stats.added.." nodes and removing "..stats.removed.." nodes in "..time_taken.."s")
return true, "Added "..stats.added.." and removed "..stats.removed.." nodes in " .. wea.format.human_time(time_taken)
end
})

@ -0,0 +1,57 @@
local wea = worldeditadditions
-- ███████ ██████ ██ ██ ██ ██████ ████████ ██ ██ ███████ ████████
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██ ██ ██ ██ ██████ ██ ██ ██ ███████ ██
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██████ ██████ ███████ ██ ██ ███████ ██ ███████ ██
minetest.register_chatcommand("/sculptlist", {
params = "[preview]",
description = "Lists all the currently registered sculpting brushes and their associated metadata. If the keyword preview is specified as an argument, a preview of each brush is also printed.",
privs = { worldedit = true },
func = function(name, params_text)
if name == nil then return end
if not params_text then params_text = "" end
params_text = wea.trim(params_text)
local msg = {}
table.insert(msg, "Currently registered sculpting brushes:\n")
if params_text == "preview" then
for brush_name, brush_def in pairs(wea.sculpt.brushes) do
local success, preview = wea.sculpt.preview_brush(brush_name)
local brush_size = "dynamic"
if type(brush_def) ~= "function" then
brush_size = brush_def.size.x.."x"..brush_def.size.y
end
print("//sculptlist: preview for "..brush_name..":")
print(preview)
table.insert(msg, brush_name.." ["..brush_size.."]:\n")
table.insert(msg, preview.."\n\n")
end
else
local display = { { "Name", "Native Size" } }
for brush_name, brush_def in pairs(wea.sculpt.brushes) do
local brush_size = "dynamic"
if type(brush_def) ~= "function" then
brush_size = brush_def.size
end
table.insert(display, {
brush_name,
brush_size
})
end
-- Sort by brush name
table.sort(display, function(a, b) return a[1] < b[1] end)
table.insert(msg, worldeditadditions.format.make_ascii_table(display))
end
worldedit.player_notify(name, table.concat(msg))
end
})

@ -1,3 +1,5 @@
local wea = worldeditadditions
-- ███████ ██████ ██████ ███████ ███████ ████████
-- ██ ██ ██ ██ ██ ██ ██ ██
-- █████ ██ ██ ██████ █████ ███████ ██
@ -17,12 +19,12 @@ worldedit.register_command("forest", {
params_text = params_text:sub(#match_start + 1) -- everything starts at 1 in Lua :-/
end
local success, sapling_list = worldeditadditions.parse.weighted_nodes(
worldeditadditions.split_shell(params_text),
local success, sapling_list = wea.parse.weighted_nodes(
wea.split_shell(params_text),
false,
function(name)
return worldedit.normalize_nodename(
worldeditadditions.normalise_saplingname(name)
wea.normalise_saplingname(name)
)
end
)
@ -35,22 +37,22 @@ worldedit.register_command("forest", {
return (pos2.x - pos1.x) * (pos2.y - pos1.y)
end,
func = function(name, density, sapling_list)
local start_time = worldeditadditions.get_ms_time()
local success, stats = worldeditadditions.forest(
local start_time = wea.get_ms_time()
local success, stats = wea.forest(
worldedit.pos1[name], worldedit.pos2[name],
density,
sapling_list
)
if not success then return success, stats end
local time_taken = worldeditadditions.format.human_time(worldeditadditions.get_ms_time() - start_time)
local time_taken = wea.format.human_time(wea.get_ms_time() - start_time)
local distribution_display = worldeditadditions.format.node_distribution(
local distribution_display = wea.format.node_distribution(
stats.placed,
stats.successes,
false -- no grand total at the bottom
)
minetest.log("action", name.." used //forest at "..worldeditadditions.vector.tostring(worldedit.pos1[name]).." - "..worldeditadditions.vector.tostring(worldedit.pos2[name])..", "..stats.successes.." trees placed, averaging "..stats.attempts_avg.." growth attempts / tree and "..stats.failures.." failed attempts in "..time_taken)
minetest.log("action", name.." used //forest at "..wea.vector.tostring(worldedit.pos1[name]).." - "..wea.vector.tostring(worldedit.pos2[name])..", "..stats.successes.." trees placed, averaging "..stats.attempts_avg.." growth attempts / tree and "..stats.failures.." failed attempts in "..time_taken)
return true, distribution_display.."\n=========================\n"..stats.successes.." trees placed, averaging "..stats.attempts_avg.." growth attempts / tree and "..stats.failures.." failed attempts in "..time_taken
end
})

@ -68,8 +68,8 @@ worldedit.register_command("layers", {
)
local time_taken = worldeditadditions.get_ms_time() - start_time
print("DEBUG min_slope", min_slope, "max_slope", max_slope)
print("DEBUG min_slope", math.deg(min_slope), "max_slope", math.deg(max_slope))
-- print("DEBUG min_slope", min_slope, "max_slope", max_slope)
-- print("DEBUG min_slope", math.deg(min_slope), "max_slope", math.deg(max_slope))
minetest.log("action", name .. " used //layers at " .. worldeditadditions.vector.tostring(worldedit.pos1[name]) .. ", replacing " .. changes.replaced .. " nodes and skipping " .. changes.skipped_columns .. " columns ("..changes.skipped_columns_slope.." due to slope constraints) in " .. time_taken .. "s")
return true, changes.replaced .. " nodes replaced and " .. changes.skipped_columns .. " columns skipped ("..changes.skipped_columns_slope.." due to slope constraints) in " .. worldeditadditions.format.human_time(time_taken)

@ -1,14 +1,18 @@
local wea = worldeditadditions
local we_c = worldeditadditions_commands
local Vector3 = worldeditadditions.Vector3
local function parse_params_maze(params_text, is_3d)
if not params_text then
if not params_text then params_text = "" end
params_text = wea.trim(params_text)
if #params_text == 0 then
return false, "No arguments specified"
end
local parts = worldeditadditions.split_shell(params_text)
local replace_node = parts[1]
local seed = os.time()
local seed = os.time() * math.random()
local path_length = 2
local path_width = 1
local path_depth = 1
@ -71,12 +75,20 @@ worldedit.register_command("maze", {
return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name])
end,
func = function(name, replace_node, seed, path_length, path_width)
local start_time = worldeditadditions.get_ms_time()
local replaced = worldeditadditions.maze2d(worldedit.pos1[name], worldedit.pos2[name], replace_node, seed, path_length, path_width)
local time_taken = worldeditadditions.get_ms_time() - start_time
local start_time = wea.get_ms_time()
minetest.log("action", name .. " used //maze at " .. worldeditadditions.vector.tostring(worldedit.pos1[name]) .. " - "..worldeditadditions.vector.tostring(worldedit.pos2[name])..", replacing " .. replaced .. " nodes in " .. time_taken .. "s")
return true, replaced .. " nodes replaced in " .. worldeditadditions.format.human_time(time_taken)
local pos1, pos2 = Vector3.sort(worldedit.pos1[name], worldedit.pos2[name])
local replaced = wea.maze2d(
pos1, pos2,
replace_node,
seed,
path_length, path_width
)
local time_taken = wea.get_ms_time() - start_time
minetest.log("action", name.." used //maze at "..pos1.." - "..pos2..", replacing "..replaced.." nodes in "..time_taken.."s")
return true, replaced.." nodes replaced in "..wea.format.human_time(time_taken)
end
})
@ -106,7 +118,7 @@ worldedit.register_command("maze3d", {
local time_taken = worldeditadditions.get_ms_time() - start_time
minetest.log("action", name .. " used //maze3d at " .. worldeditadditions.vector.tostring(worldedit.pos1[name]) .. " - "..worldeditadditions.vector.tostring(worldedit.pos2[name])..", replacing " .. replaced .. " nodes in " .. time_taken .. "s")
return true, replaced .. " nodes replaced in " .. worldeditadditions.format.human_time(time_taken)
minetest.log("action", name.." used //maze3d at "..worldeditadditions.vector.tostring(worldedit.pos1[name]).." - "..worldeditadditions.vector.tostring(worldedit.pos2[name])..", replacing "..replaced.." nodes in "..time_taken.."s")
return true, replaced.." nodes replaced in "..worldeditadditions.format.human_time(time_taken)
end
})

@ -0,0 +1,77 @@
local we_c = worldeditadditions_commands
local wea = worldeditadditions
local Vector3 = wea.Vector3
-- ███████ ██████ ██ ██ ██ ██████ ████████
-- ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██ ██ ██ ██ ██████ ██
-- ██ ██ ██ ██ ██ ██ ██
-- ███████ ██████ ██████ ███████ ██ ██
worldedit.register_command("sculpt", {
params = "[<brush_name=default> [<height=5> [<brush_size=10>]]]",
description = "Applies a sculpting brush to the terrain with a given height. See //sculptlist to list all available brushes. Note that while the brush size is configurable, the actual brush size you end up with may be slightly different to that which you request due to brush size restrictions.",
privs = { worldedit = true },
require_pos = 1,
parse = function(params_text)
if not params_text or params_text == "" then
params_text = "circle_soft1"
end
local parts = wea.split_shell(params_text)
local brush_name = "circle_soft1"
local height = 5
local brush_size = 10
if #parts >= 1 then
brush_name = table.remove(parts, 1)
if not wea.sculpt.brushes[brush_name] then
return false, "A brush with the name '"..brush_name.."' doesn't exist. Try using //sculptlist to list all available brushes."
end
end
if #parts >= 1 then
height = tonumber(table.remove(parts, 1))
if not height then
return false, "Invalid height value (must be an integer - negative values lower terrain instead of raising it)"
end
end
if #parts >= 1 then
brush_size = tonumber(table.remove(parts, 1))
if not brush_size or brush_size < 1 then
return false, "Invalid brush size. Brush sizes must be a positive integer."
end
end
brush_size = Vector3.new(brush_size, brush_size, 0):floor()
return true, brush_name, math.floor(height), brush_size
end,
nodes_needed = function(name, brush_name, height, brush_size)
local success, brush, size_actual = wea.sculpt.make_brush(brush_name, brush_size)
if not success then return 0 end
-- This solution allows for brushes with negative values
-- it also allows for brushes that 'break the rules' and have values
-- that exceed the -1 to 1 range
local brush_min = wea.min(brush)
local brush_max = wea.max(brush)
local range_nodes = (brush_max * height) - (brush_min * height)
return size_actual.x * size_actual.y * range_nodes
end,
func = function(name, brush_name, height, brush_size)
local start_time = wea.get_ms_time()
local pos1 = wea.Vector3.clone(worldedit.pos1[name])
local success, stats = wea.sculpt.apply(
pos1,
brush_name, height, brush_size
)
if not success then return success, stats.added end
local time_taken = wea.get_ms_time() - start_time
minetest.log("action", name .. " used //sculpt at "..pos1..", adding " .. stats.added.." nodes and removing "..stats.removed.." nodes in "..time_taken.."s")
return true, stats.added.." nodes added and "..stats.removed.." removed in "..wea.format.human_time(time_taken)
end
})

@ -16,7 +16,7 @@ worldedit.register_command("sfactor", {
return false, "Error: Not enough arguments. Expected \"<mode> <factor> [<target>]\"."
end
local mode, fac, targ = unpack(parts)
local modeSet = wea.makeset {"grow", "shrink", "avg"}
local modeSet = wea.table.makeset {"grow", "shrink", "avg"}
-- Mode parsing
if mode == "average" then -- If mode is average set to avg
@ -60,16 +60,16 @@ worldedit.register_command("sfactor", {
end
-- Equasion: round(delta[<axis>] / factor) * factor
local eval = function(int,fac)
local tmp, abs, neg = int / fac, math.abs(int), int < 0
local eval = function(int,fac_inner)
local tmp, abs, neg = int / fac_inner, math.abs(int), int < 0
if mode == "avg" then
if int > _m then int = math.floor(abs / fac) * fac
else int = math.ceil(abs / fac) * fac end
elseif mode == "shrink" then int = math.floor(abs / fac) * fac
else int = math.ceil(abs / fac) * fac end
if int > _m then int = math.floor(abs / fac_inner) * fac_inner
else int = math.ceil(abs / fac_inner) * fac_inner end
elseif mode == "shrink" then int = math.floor(abs / fac_inner) * fac_inner
else int = math.ceil(abs / fac_inner) * fac_inner end
if int < fac then int = fac end -- Ensure selection doesn't collapse to 0
if int < fac_inner then int = fac_inner end -- Ensure selection doesn't collapse to 0
if neg then int = int * -1 end -- Ensure correct facing direction
return int
end

@ -65,7 +65,7 @@ worldedit.register_command("smake", {
end,
func = function(name, oper, mode, targ, base)
local p1, p2 = vector.new(worldedit.pos1[name]), vector.new(worldedit.pos2[name])
local eval = function() end -- Declare eval placeholder function to edit later
local eval -- Declare eval placeholder function to edit later
local delta = vector.subtract(p2,p1) -- local delta equation: Vd(a) = V2(a) - V1(a)
local _tl = #targ -- Get targ length as a variable incase mode is "average"/"avg"

@ -40,6 +40,7 @@ dofile(we_c.modpath.."/commands/copy.lua")
dofile(we_c.modpath.."/commands/move.lua")
dofile(we_c.modpath.."/commands/count.lua")
dofile(we_c.modpath.."/commands/sculpt.lua")
-- Meta Commands
dofile(we_c.modpath.."/commands/meta/init.lua")
@ -55,8 +56,9 @@ dofile(we_c.modpath.."/commands/wireframe/init.lua")
dofile(we_c.modpath.."/commands/extra/saplingaliases.lua")
dofile(we_c.modpath.."/commands/extra/basename.lua")
dofile(we_c.modpath.."/commands/extra/sculptlist.lua")
-- Don't registry the //bonemeal command if the bonemeal mod isn't present
-- Don't register the //bonemeal command if the bonemeal mod isn't present
if minetest.global_exists("bonemeal") then
dofile(we_c.modpath.."/commands/bonemeal.lua")
dofile(we_c.modpath.."/commands/forest.lua")
@ -94,6 +96,7 @@ worldedit.alias_command("mfacing", "mface")
-- These are commented out for now, as they could be potentially dangerous to stability
-- Thorough testing is required of our replacement commands before these are uncommented
-- TODO: Depend on worldeditadditions_core before uncommenting this
-- BUG: //move+ seems to be leaving stuff behind for some strange reason --@sbrl 2021-12-26
-- worldeditadditions_core.alias_override("copy", "copy+")
-- worldeditadditions_core.alias_override("move", "move+") -- MAY have issues where it doesn't overwrite the old region properly, but haven't been able to reliably reproduce this
-- worldeditadditions_core.alias_override("replace", "replacemix")