mirror of
https://github.com/sbrl/Minetest-WorldEditAdditions.git
synced 2024-11-27 17:43:53 +01:00
Merge branch 'main' into VorTechnix
This commit is contained in:
commit
9995ea2fb9
@ -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
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
|
93
.docs/img2brush/img2brush.js
Normal file
93
.docs/img2brush/img2brush.js
Normal file
@ -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;
|
||||
}
|
34
.docs/img2brush/index.html
Normal file
34
.docs/img2brush/index.html
Normal file
@ -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>
|
100
.tests/Vector3/clamp.test.lua
Normal file
100
.tests/Vector3/clamp.test.lua
Normal file
@ -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)
|
40
.tests/strings/str_ends.test.lua
Normal file
40
.tests/strings/str_ends.test.lua
Normal file
@ -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)
|
||||
|
39
CHANGELOG.md
39
CHANGELOG.md
@ -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
|
||||
|
14
README.md
14
README.md
@ -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
|
||||
|
@ -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,
|
||||
|
69
worldeditadditions/lib/sculpt/apply.lua
Normal file
69
worldeditadditions/lib/sculpt/apply.lua
Normal file
@ -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
|
54
worldeditadditions/lib/sculpt/apply_heightmap.lua
Normal file
54
worldeditadditions/lib/sculpt/apply_heightmap.lua
Normal file
@ -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
|
23
worldeditadditions/lib/sculpt/brushes/__gaussian.lua
Normal file
23
worldeditadditions/lib/sculpt/brushes/__gaussian.lua
Normal file
@ -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
|
24
worldeditadditions/lib/sculpt/brushes/circle.lua
Normal file
24
worldeditadditions/lib/sculpt/brushes/circle.lua
Normal file
@ -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
|
46
worldeditadditions/lib/sculpt/brushes/circle_soft1.lua
Normal file
46
worldeditadditions/lib/sculpt/brushes/circle_soft1.lua
Normal file
@ -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
|
27
worldeditadditions/lib/sculpt/brushes/ellipse.brush.tsv
Normal file
27
worldeditadditions/lib/sculpt/brushes/ellipse.brush.tsv
Normal file
@ -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
|
|
8
worldeditadditions/lib/sculpt/brushes/gaussian.lua
Normal file
8
worldeditadditions/lib/sculpt/brushes/gaussian.lua
Normal file
@ -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
|
8
worldeditadditions/lib/sculpt/brushes/gaussian_hard.lua
Normal file
8
worldeditadditions/lib/sculpt/brushes/gaussian_hard.lua
Normal file
@ -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
|
8
worldeditadditions/lib/sculpt/brushes/gaussian_soft.lua
Normal file
8
worldeditadditions/lib/sculpt/brushes/gaussian_soft.lua
Normal file
@ -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
|
11
worldeditadditions/lib/sculpt/brushes/square.lua
Normal file
11
worldeditadditions/lib/sculpt/brushes/square.lua
Normal file
@ -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
|
23
worldeditadditions/lib/sculpt/import_static.lua
Normal file
23
worldeditadditions/lib/sculpt/import_static.lua
Normal file
@ -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
|
31
worldeditadditions/lib/sculpt/init.lua
Normal file
31
worldeditadditions/lib/sculpt/init.lua
Normal file
@ -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
|
24
worldeditadditions/lib/sculpt/make_brush.lua
Normal file
24
worldeditadditions/lib/sculpt/make_brush.lua
Normal file
@ -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
|
54
worldeditadditions/lib/sculpt/make_preview.lua
Normal file
54
worldeditadditions/lib/sculpt/make_preview.lua
Normal file
@ -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
|
48
worldeditadditions/lib/sculpt/parse_static.lua
Normal file
48
worldeditadditions/lib/sculpt/parse_static.lua
Normal file
@ -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
|
21
worldeditadditions/lib/sculpt/preview_brush.lua
Normal file
21
worldeditadditions/lib/sculpt/preview_brush.lua
Normal file
@ -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
|
11
worldeditadditions/lib/sculpt/read_brush_static.lua
Normal file
11
worldeditadditions/lib/sculpt/read_brush_static.lua
Normal file
@ -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
|
48
worldeditadditions/lib/sculpt/scan_static.lua
Normal file
48
worldeditadditions/lib/sculpt/scan_static.lua
Normal file
@ -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")
|
||||
|
14
worldeditadditions/utils/io.lua
Normal file
14
worldeditadditions/utils/io.lua
Normal file
@ -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
|
71
worldeditadditions/utils/terrain/apply_heightmap_changes.lua
Normal file
71
worldeditadditions/utils/terrain/apply_heightmap_changes.lua
Normal file
@ -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
|
47
worldeditadditions/utils/terrain/calculate_normals.lua
Normal file
47
worldeditadditions/utils/terrain/calculate_normals.lua
Normal file
@ -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
|
30
worldeditadditions/utils/terrain/calculate_slopes.lua
Normal file
30
worldeditadditions/utils/terrain/calculate_slopes.lua
Normal file
@ -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
|
12
worldeditadditions/utils/terrain/init.lua
Normal file
12
worldeditadditions/utils/terrain/init.lua
Normal file
@ -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
|
50
worldeditadditions/utils/terrain/make_heightmap.lua
Normal file
50
worldeditadditions/utils/terrain/make_heightmap.lua
Normal file
@ -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
|
||||
})
|
||||
|
57
worldeditadditions_commands/commands/extra/sculptlist.lua
Normal file
57
worldeditadditions_commands/commands/extra/sculptlist.lua
Normal file
@ -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
|
||||
})
|
||||
|
77
worldeditadditions_commands/commands/sculpt.lua
Normal file
77
worldeditadditions_commands/commands/sculpt.lua
Normal file
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user