diff --git a/.docs/.eleventy.js b/.docs/.eleventy.js index cddd9b5..f410716 100644 --- a/.docs/.eleventy.js +++ b/.docs/.eleventy.js @@ -75,7 +75,7 @@ async function fetch(url) { } module.exports = function(eleventyConfig) { - + eleventyConfig.addPassthroughCopy("img2brush/img2brush.js"); eleventyConfig.addAsyncShortcode("fetch", fetch); // eleventyConfig.addPassthroughCopy("images"); diff --git a/.docs/Reference.html b/.docs/Reference.html index 2cfefcc..6b830bc 100644 --- a/.docs/Reference.html +++ b/.docs/Reference.html @@ -8,14 +8,14 @@
  • A full reference, with detailed explanations for each command
  • -

    There is also a filter box for filtering the detailed explanations to quickly find the one you're after.

    +

    There is also a filter box for filtering everything to quickly find the one you're after.

    - - + +
    diff --git a/.docs/css/theme.css b/.docs/css/theme.css index 22658f3..01a98e6 100644 --- a/.docs/css/theme.css +++ b/.docs/css/theme.css @@ -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; } diff --git a/.docs/grammars/axes.bnf b/.docs/grammars/axes.bnf new file mode 100644 index 0000000..a75764c --- /dev/null +++ b/.docs/grammars/axes.bnf @@ -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 +``` + +%} + + ::= * + + ::= + | + + ::= + | + + ::= sym | symmetrical | mirror | mir | rev | reverse | true + + + + ::= + | + + ::= + | "-" + + ::= front | back | left | right | up | down | "?" + + ::= x | y | z | h | v + + + + ::= * + + ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0 diff --git a/.docs/img2brush/img2brush.js b/.docs/img2brush/img2brush.js new file mode 100644 index 0000000..ccbea47 --- /dev/null +++ b/.docs/img2brush/img2brush.js @@ -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; +} diff --git a/.docs/img2brush/index.html b/.docs/img2brush/index.html new file mode 100644 index 0000000..4e68b2e --- /dev/null +++ b/.docs/img2brush/index.html @@ -0,0 +1,34 @@ +--- +layout: theme.njk +title: Image to brush converter +--- + +
    +

    Image to sculpting brush converter

    + +

    Convert any image to a sculpting brush here! The alpha (opacity) channel is used to determine the weight of the brush - all colour is ignored.

    +
    + +
    +

    Input

    +

    Drop your image here.

    + + + + +
    + + +
    +

    Output

    + +

    Paste the output below into a text file in worldeditadditions/lib/sculpt/brushes with the file extension .brush.tsv and restart your Minetest server for the brush to be recognised.

    + +

    + +

    + +
    (your output will appear here as soon as you drop an image above)
    +
    + + diff --git a/.tests/Vector3/clamp.test.lua b/.tests/Vector3/clamp.test.lua new file mode 100644 index 0000000..a866e76 --- /dev/null +++ b/.tests/Vector3/clamp.test.lua @@ -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) diff --git a/.tests/strings/str_ends.test.lua b/.tests/strings/str_ends.test.lua new file mode 100644 index 0000000..b17665c --- /dev/null +++ b/.tests/strings/str_ends.test.lua @@ -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) diff --git a/.tests/strings/str_starts.test.lua b/.tests/strings/str_starts.test.lua index 606a311..747b3eb 100644 --- a/.tests/strings/str_starts.test.lua +++ b/.tests/strings/str_starts.test.lua @@ -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) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3575fe0..039d5b0 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/Chat-Command-Reference.md b/Chat-Command-Reference.md index 402364b..519aff6 100644 --- a/Chat-Command-Reference.md +++ b/Chat-Command-Reference.md @@ -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 [ [ []]]` +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: + + + +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