Merge branch 'main' into VorTechnix

This commit is contained in:
Starbeamrainbowlabs 2021-11-07 14:31:01 +00:00 committed by GitHub
commit 3f48832fff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1236 additions and 1539 deletions

@ -1,12 +1,14 @@
"use strict";
const os = require("os");
const fs = require("fs");
const path = require("path");
const debug = require("debug");
const htmlentities = require("html-entities");
const phin = require("phin");
const Image = require("@11ty/eleventy-img");
const HTMLPicture = require("./lib/HTMLPicture.js");
var nextid = 0;
@ -16,46 +18,22 @@ const image_filename_format = (_id, src, width, format, _options) => {
return `${name}-${width}w.${format}`;
};
function image_metadata_log(metadata, source) {
for(let format in metadata) {
for(let img of metadata[format]) {
console.log(`${source.padEnd(10)} ${format.padEnd(5)} ${`${img.width}x${img.height}`.padEnd(10)} ${img.outputPath}`);
}
}
}
async function shortcode_image(src, alt, classes = "") {
let metadata = await Image(src, {
widths: [300, null],
formats: ["avif", "jpeg"],
outputDir: `./_site/img/`,
filenameFormat: image_filename_format
});
image_metadata_log(metadata, `IMAGE`);
async function shortcode_image(src, alt) {
let imageAttributes = {
class: classes,
alt: htmlentities.encode(alt),
sizes: Object.values(metadata)[0].map((el) => `${el.width}w`).join(" "),
loading: "lazy",
decoding: "async",
};
// You bet we throw an error on missing alt in `imageAttributes` (alt="" works okay)
return Image.generateHTML(metadata, imageAttributes);
return HTMLPicture(
src, alt,
`./_site/img`, `/img`
);
}
async function shortcode_image_url(src) {
let metadata = await Image(src, {
widths: [ null ],
formats: [ "jpeg" ],
outputDir: `./_site/img/`,
filenameFormat: image_filename_format
});
image_metadata_log(metadata, `IMAGE_URL`);
const src_parsed = path.parse(src);
const target = path.join(`./_site/img`, src_parsed.base);
if(!fs.existsSync(path.dirname(target)))
await fs.promises.mkdir(target_dir, { recursive: true });
await fs.promises.copyFile(src, target);
let data = metadata.jpeg[metadata.jpeg.length - 1];
return data.url;
return path.join(`/img`, src_parsed.base);
}
async function shortcode_image_urlpass(src) {
@ -82,14 +60,14 @@ async function shortcode_gallerybox(content, src, idthis, idprev, idnext) {
}
async function fetch(url) {
let package = JSON.parse(await fs.promises.readFile(
const pkg_obj = JSON.parse(await fs.promises.readFile(
path.join(__dirname, "package.json"), "utf8"
));
return (await phin({
url,
headers: {
"user-agent": `WorldEditAdditionsStaticBuilder/${package.version} (Node.js/${process.version}; ${os.platform()} ${os.arch()}) eleventy/${package.devDependencies["@11ty/eleventy"].replace(/\^/, "")}`
"user-agent": `WorldEditAdditionsStaticBuilder/${pkg_obj.version} (Node.js/${process.version}; ${os.platform()} ${os.arch()}) eleventy/${pkg_obj.devDependencies["@11ty/eleventy"].replace(/\^/, "")}`
},
followRedirects: true,
parse: "string"

@ -1,5 +1,12 @@
"use strict";
const fs = require("fs");
const path = require("path");
const columnify = require("columnify");
const htmlentities = require("html-entities");
const a = require("./lib/Ansi.js");
const parse_sections = require("./lib/parse_sections.js");
let { sections, categories } = parse_sections(fs.readFileSync(
@ -15,11 +22,20 @@ sections = sections.slice(1).sort((a, b) => a.title.replace(/^\/+/g, "").localeC
console.log(`REFERENCE SECTION TITLES`)
console.log(sections
.map(s => [s.category, s.title].join(`\t`)).join(`\n`));
console.log(columnify(sections.map(s => { return {
category: `${a.hicol}${a.fyellow}${s.category}${a.reset}`,
command: `${a.hicol}${a.fmagenta}${htmlentities.decode(s.title)}${a.reset}`
} })));
// console.log(sections
// .map(s => `${a.fyellow}${a.hicol}${s.category}${a.reset}\t${a.fmagenta}${a.hicol}${s.title}${a.reset}`).join(`\n`));
console.log(`************************`);
console.log(`REFERENCE SECTION COLOURS`, categories);
console.log(`REFERENCE SECTION COLOURS`);
console.log(columnify(Array.from(categories).map(el => { return {
category: el[0],
colour: el[1]
} })));
module.exports = {
layout: "theme.njk",
title: "Reference",

@ -14,11 +14,11 @@
<section id="filter" class="panel-generic">
<div class="form-item bigsearch">
<label for="input-filter">Filter:</label>
<input type="search" id="input-filter" />
<label for="input-filter" >Filter:</label>
<input type="search" id="input-filter" placeholder="Start typing to filter." />
</div>
<div class="form-item centre checkbox">
<input type="checkbox" id="input-searchall" placeholder="Start typing to filter the sections." />
<input type="checkbox" id="input-searchall" />
<label for="input-searchall" title="If unchecked, only the title will be searched.">Search content</label>
</div>
</section>
@ -128,6 +128,7 @@
function do_filter() {
let el_search = document.querySelector("#input-filter");
let el_searchall = document.querySelector("#input-searchall");
localStorage.setItem("commandlist-searchall", el_searchall.checked);
/* Filterable items
- Sections
- Commands in the command list
@ -165,6 +166,22 @@
el_next.classList.remove("visible", "hidden");
el_next.classList.add(show ? "visible" : "hidden");
}
let commandlist_is_categorical = document.querySelector("button.active[data-mode=categorical]") !== null;
if(commandlist_is_categorical) {
let container = document.querySelector(".command-container");
for(let i = 0; i < container.children.length; i++) {
if(container.children[i].nodeName.toLowerCase() === "ul") {
let class_name = "visible";
if(container.children[i].querySelector("li.visible") === null)
class_name = "hidden";
if(i > 0) {
container.children[i-1].classList.remove("visible", "hidden");
container.children[i-1].classList.add(class_name);
}
}
}
}
}
window.addEventListener("load", (_event) => {
@ -181,6 +198,9 @@
els_cats[i].addEventListener("touchend", handle_display_mode);
}
if(localStorage.getItem("commandlist-searchall") !== null)
el_searchall.checked = localStorage.getItem("commandlist-searchall") === "true";
if(localStorage.getItem("commandlist-displaymode") !== null)
set_display_mode(localStorage.getItem("commandlist-displaymode"))
});

88
.docs/lib/Ansi.js Normal file

@ -0,0 +1,88 @@
"use strict";
/**
* Generates various VT100 ANSI escape sequences.
* Ported from C#.
* @licence MPL-2.0 <https://www.mozilla.org/en-US/MPL/2.0/>
* @source https://gist.github.com/a4edd3204a03f4eedb79785751efb0f3#file-ansi-cs
* @author Starbeamrainbowlabs
* GitHub: @sbrl | Twitter: @SBRLabs | Reddit: u/Starbeamrainbowlabs
***** Changelog *****
* 27th March 2019:
* - Initial public release
* 9th March 2020:
* - Add Italics (\u001b[3m])
* - Export a new instance of it by default (makes it so that there's 1 global instance)
* 5th September 2020:
* - Add support for NO_COLOR environment variable <https://no-color.org/>
*/
class Ansi {
constructor() {
/**
* Whether we should *actually* emit ANSI escape codes or not.
* Useful when we want to output to a log file, for example
* @type {Boolean}
*/
this.enabled = true;
this.escape_codes();
}
escape_codes() {
if(typeof process !== "undefined" && typeof process.env.NO_COLOR == "string") {
this.enabled = false;
return;
}
// Solution on how to output ANSI escape codes in C# from here:
// https://www.jerriepelser.com/blog/using-ansi-color-codes-in-net-console-apps
this.reset = this.enabled ? "\u001b[0m" : "";
this.hicol = this.enabled ? "\u001b[1m" : "";
this.locol = this.enabled ? "\u001b[2m" : "";
this.italics = this.enabled ? "\u001b[3m" : "";
this.underline = this.enabled ? "\u001b[4m" : "";
this.inverse = this.enabled ? "\u001b[7m" : "";
this.fblack = this.enabled ? "\u001b[30m" : "";
this.fred = this.enabled ? "\u001b[31m" : "";
this.fgreen = this.enabled ? "\u001b[32m" : "";
this.fyellow = this.enabled ? "\u001b[33m" : "";
this.fblue = this.enabled ? "\u001b[34m" : "";
this.fmagenta = this.enabled ? "\u001b[35m" : "";
this.fcyan = this.enabled ? "\u001b[36m" : "";
this.fwhite = this.enabled ? "\u001b[37m" : "";
this.bblack = this.enabled ? "\u001b[40m" : "";
this.bred = this.enabled ? "\u001b[41m" : "";
this.bgreen = this.enabled ? "\u001b[42m" : "";
this.byellow = this.enabled ? "\u001b[43m" : "";
this.bblue = this.enabled ? "\u001b[44m" : "";
this.bmagenta = this.enabled ? "\u001b[45m" : "";
this.bcyan = this.enabled ? "\u001b[46m" : "";
this.bwhite = this.enabled ? "\u001b[47m" : "";
}
// Thanks to http://ascii-table.com/ansi-escape-sequences.php for the following ANSI escape sequences
up(lines = 1) {
return this.enabled ? `\u001b[${lines}A` : "";
}
down(lines = 1) {
return this.enabled ? `\u001b[${lines}B` : "";
}
right(lines = 1) {
return this.enabled ? `\u001b[${lines}C` : "";
}
left(lines = 1) {
return this.enabled ? `\u001b[${lines}D` : "";
}
jump_to(x, y) {
return this.enabled ? `\u001b[${y};${x}H` : "";
}
cursor_pos_save() {
return this.enabled ? `\u001b[s` : "";
}
cursor_pos_restore() {
return this.enabled ? `\u001b[u` : "";
}
}
module.exports = new Ansi();

155
.docs/lib/HTMLPicture.js Normal file

@ -0,0 +1,155 @@
"use strict";
const os = require(`os`);
const fs = require("fs");
const path = require("path");
const pretty_ms = require("pretty-ms");
const debug = require("debug")("image");
const imagickal = require("imagickal");
const htmlentities = require("html-entities");
const a = require("./Ansi.js");
function calculate_size(width, height, size_spec) {
if(size_spec.indexOf("%") > -1) {
// It's a percentage
const multiplier = parseInt(size_spec.replace(/%/, ""), 10) / 100;
return {
width: Math.ceil(width * multiplier),
height: Math.ceil(height * multiplier)
};
}
else {
// It's an absolute image width
const new_width = parseInt(size_spec, 10);
return {
width: new_width,
height: Math.ceil(new_width/width * height)
};
}
}
// Main task list - we make sure it completes before exiting.
var queue = null;
var pMemoize = null;
async function make_queue() {
// 1: Setup task queue
const PQueue = (await import("p-queue")).default;
let concurrency = os.cpus().length;
if(process.env["MAX_CONCURRENT"])
concurrency = parseInt(process.env["MAX_CONCURRENT"], 10);
debug(`Image conversion queue concurrency: `, concurrency);
queue = new PQueue({ concurrency });
queue.on("idle", () => console.log(`IMAGE ${a.fcyan}all conversions complete${a.reset}`));
process.on("exit", async () => {
debug(`Waiting for image conversions to finish...`);
await queue.onEmpty();
debug(`All image conversions complete.`);
});
}
async function srcset(source_image, target_dir, urlpath, format = "__AUTO__", sizes = [ "25%", "50%", "100%" ], quality = 95, strip = true) {
if(queue === null) await make_queue();
const source_parsed = path.parse(source_image);
// ext contains the dot . already
const target_format = format == "__AUTO__" ? source_parsed.ext.replace(/\./g, "") : format;
const source_size = await imagickal.dimensions(source_image);
debug(`SOURCE_SIZE`, source_size, `TARGET_FORMAT`, target_format);
let setitems = await Promise.all(sizes.map(async (size) => {
let target_filename = `${source_parsed.name}_${size}.${target_format}`
.replace(/%/, "pcent");
let target_current = path.join(
target_dir,
target_filename
);
queue.add(async () => {
const start = new Date();
await imagickal.transform(source_image, target_current, {
resize: { width: size },
quality,
strip
});
console.log(`IMAGE\t${a.fcyan}${queue.size}/${queue.pending} tasks${a.reset}\t${a.fyellow}${pretty_ms(new Date() - start)}${a.reset}\t${a.fgreen}${target_current}${a.reset}`);
});
// const size_target = await imagickal.dimensions(target_current);
const predict = calculate_size(source_size.width, source_size.height, size);
// debug(`size spec:`, size, `size predicted: ${predict.width}x${predict.height} actual: ${size_target.width}x${size_target.height}`);
return `${path.resolve(urlpath, target_filename)} ${predict.width}w`;
}));
return setitems.join(", ");
}
/**
* Generates a string of HTML for a <picture> element, converting images to the specified formats in the process.
* @param {string} source_image The filepath to the source image.
* @param {string} alt The alt (alternative) text. Automatically run though htmlentities.
* @param {string} target_dir The target directory to save converted images to.
* @param {string} urlpath The path to the aforementionoed target directory as a URL. Image paths in the HTML will be prefixed with this value.
* @param {string} [formats="__AUTO__"] A list of formats to convert the source image to. Defaults to automatically determining the most optimal formats based on the input format. [must be lowercase]
* @param {Array} [sizes=["25%","50%", "100%" ]] The sizes, as imagemagick size specs, to convert the source image to.
* @param {Number} [quality=95] The quality value to use when converting images.
* @param {Boolean} [strip=true] Whether to strip all metadata from images when converting them [saves some space]
* @return {Promise<string>} A Promise that returns a generated string of HTML.
*/
async function picture(source_image, alt, target_dir, urlpath, formats = "__AUTO__", sizes = [ "25%", "50%", "100%" ], quality = 95, strip = true) {
const source_parsed = path.parse(source_image);
const source_format = source_parsed.ext.toLowerCase().replace(".", "");
if(formats == "__AUTO__") {
switch(source_format) {
case "png":
case "gif": // * shudder *
case "bmp":
formats = [ "png" ];
break;
default:
// jxl = JPEG XL <https://jpegxl.info/> - not currently supported by the old version of imagemagick shipped via apt :-/
// Imagemagick v7+ does support it, but isn't shipped yet :-(
formats = [ "jpeg", "webp", "avif", /*"jxl"*/ ];
break;
}
}
const target_original = path.join(target_dir, source_parsed.base);
await fs.promises.copyFile(source_image, target_original);
const sources = await Promise.all(formats.map(async (format) => {
debug(`${format} ${source_image}`);
return {
mime: `image/${format}`,
srcset: await srcset(
source_image,
target_dir, urlpath,
format, sizes,
quality, strip
)
};
}));
let result = `<picture>\n\t`;
result += sources.map(source => `<source srcset="${source.srcset}" type="${source.mime}" />`).join(`\n\t`);
result += `\n\t<img loading="lazy" decoding="async" src="${urlpath}/${source_parsed.base}" alt="${htmlentities.encode(alt)}" />\n`;
result += `</picture>\n`
return result;
}
var picture_memoize = null;
async function setup_memoize() {
const pMemoize = (await import("p-memoize")).default;
picture_memoize = pMemoize(picture);
}
module.exports = async function(...args) {
if(picture_memoize === null) await setup_memoize();
return await picture_memoize(...args);
};

@ -1,3 +1,5 @@
"use strict";
const crypto = require("crypto");
const htmlentities = require("html-entities");
@ -6,7 +8,6 @@ const markdown = require("markdown-it")({
});
const chroma = require("chroma-js");
const markdown_prism = require("markdown-it-prism");
markdown.use(markdown_prism, {
init: (Prism) => {

1776
.docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -21,10 +21,15 @@
"homepage": "https://github.com/sbrl/Minetest-WorldEditAdditions#readme",
"devDependencies": {
"@11ty/eleventy": "^0.12.1",
"@11ty/eleventy-img": "^0.10.0",
"chroma-js": "^2.1.2",
"columnify": "^1.5.4",
"debug": "^4.3.2",
"imagickal": "^5.0.1",
"markdown-it-prism": "^2.2.1",
"phin": "^3.6.0"
"p-memoize": "^6.0.1",
"p-queue": "^7.1.0",
"phin": "^3.6.0",
"pretty-ms": "^7.0.1"
},
"dependencies": {
"html-entities": "^2.3.2"

@ -6,6 +6,7 @@ exclude_files = {
"worldeditadditions/utils/bit.lua"
}
files["worldeditadditions_core/register/check.lua"] = { read_globals = { "table" } }
ignore = {
"631", "61[124]",

@ -12,7 +12,9 @@ Note to self: See the bottom of this file for the release template text.
- 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
- Use [luacheck](https://github.com/mpeterv/luacheck) to find and fix a large number of bugs and other issues
- Add `//ellipsoid2` which creates an ellipsoid that fills the defined region
- Add `//spiral2` for creating both square and circular spirals
- 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/))
- `//bonemeal`: Add optional node list constraint
@ -31,6 +33,8 @@ Note to self: See the bottom of this file for the release template text.
- Add `holly``hollytree:sapling`
- `//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
## v1.12: The selection tools update (26th June 2021)

@ -2,7 +2,7 @@
This is the full chat command reference for WorldEditAdditions. Having trouble finding the section you want? Try the **[quick reference](https://github.com/sbrl/Minetest-WorldEditAdditions#quick-command-reference)** instead, which has links to back to sections of this document!
Check out the reference on the new website - it's even searchable: <https://worldeditadditions.mooncarrot.space/Reference/>
**Check out the reference on the new website - it's even searchable: <https://worldeditadditions.mooncarrot.space/Reference/>**
Other useful links:
@ -42,6 +42,18 @@ Creates a hollow ellipsoid at position 1 with the radius `(rx, ry, rz)`. Works t
//hollowellipsoid 21 11 41 stone
```
### `//ellipsoid2 [<node_name:dirt> [h[ollow]]]`
Creates an (optionally hollow) solid ellipsoid that fills the defined region.
```weacmd
//ellipsoid2
//ellipsoid2 ice
//ellipsoid2 air
//ellipsoid2 steelblock h
//ellipsoid2 papyrus hollow
```
### `//torus <major_radius> <minor_radius> <node_name> [<axes=xy> [h[ollow]]]`
Creates a solid torus at position 1 with the specified major and minor radii. The major radius is the distance from the centre of the torus to the centre of the circle bit, and the minor radius is the radius of the circle bit.
@ -134,10 +146,11 @@ Additional examples:
```
### `//walls <replace_node> [<thickness=1>]`
Creates vertical walls of `<replace_node>` around the inside edges of the defined region, optionally specifying the thickness thereof.
### `//walls [<replace_node=dirt> [<thickness=1>]]`
Creates vertical walls of `<replace_node>` around the inside edges of the defined region, optionally specifying the thickness thereof. Defaults to a replace node of `dirt` and a wall thickness of 1.
```weacmd
//walls
//walls dirt
//walls stone
//walls goldblock
@ -146,6 +159,19 @@ Creates vertical walls of `<replace_node>` around the inside edges of the define
```
### `//spiral2 [<circle|square>] [<replace_node=dirt> [<interval=3> [<acceleration=0>] ] ]`
Generates both square and circular spiral shapes with the given `<replace_node>` - defaulting to square spirals. The interval defines the gap between the spiral in nodes, and the acceleration defines by how much the interval should be increased (a value of 1 means 1 node per revolution).
```
//spiral2
//spiral2 circle stone
//spiral2 square
//spiral2 circle
//spiral2 glass 5
//spiral2 square desert_sand 3 1
```
## Misc
<!--
@ -158,7 +184,7 @@ Creates vertical walls of `<replace_node>` around the inside edges of the define
### `//floodfill [<replace_node> [<radius>]]`
Floods all connected nodes of the same type starting at _pos1_ with <replace_node> (which defaults to `water_source`), in a sphere with a radius of <radius> (which defaults to 50).
Floods all connected nodes of the same type starting at _pos1_ with `<replace_node>` (which defaults to `water_source`), in a sphere with a radius of `<radius>` (which defaults to 50).
```weacmd
//floodfill

@ -29,6 +29,7 @@ The detailed explanations have moved! Check them out [here](https://github.com/s
### Geometry
- [`//ellipsoid <rx> <ry> <rz> <node_name> [h[ollow]]`](https://worldeditadditions.mooncarrot.space/Reference/#ellipsoid)
- [`//ellipsoid2 <node_name> [h[ollow]]`](https://worldeditadditions.mooncarrot.space/Reference/#ellipsoid2)
- [`//hollowellipsoid <rx> <ry> <rz> <node_name>`](https://worldeditadditions.mooncarrot.space/Reference/#hollowellipsoid)
- [`//torus <major_radius> <minor_radius> <node_name> [<axes=xy> [h[ollow]]]`](https://worldeditadditions.mooncarrot.space/Reference/#torus)
- [`//hollowtorus <major_radius> <minor_radius> <node_name> [<axes=xy>]`](https://worldeditadditions.mooncarrot.space/Reference/#hollowtorus)
@ -37,6 +38,7 @@ The detailed explanations have moved! Check them out [here](https://github.com/s
- [`//hollow [<wall_thickness>]`](https://worldeditadditions.mooncarrot.space/Reference/#hollow)
- [`//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)
### Misc
- [`//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)

@ -41,6 +41,7 @@ dofile(wea.modpath.."/lib/overlay.lua")
dofile(wea.modpath.."/lib/layers.lua")
dofile(wea.modpath.."/lib/fillcaves.lua")
dofile(wea.modpath.."/lib/ellipsoid.lua")
dofile(wea.modpath.."/lib/ellipsoid2.lua")
dofile(wea.modpath.."/lib/torus.lua")
dofile(wea.modpath.."/lib/line.lua")
dofile(wea.modpath.."/lib/walls.lua")
@ -51,10 +52,14 @@ dofile(wea.modpath.."/lib/hollow.lua")
dofile(wea.modpath.."/lib/scale_up.lua")
dofile(wea.modpath.."/lib/scale_down.lua")
dofile(wea.modpath.."/lib/scale.lua")
dofile(wea.modpath.."/lib/spiral_square.lua")
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")
dofile(wea.modpath.."/lib/copy.lua")
dofile(wea.modpath.."/lib/count.lua")
dofile(wea.modpath.."/lib/bonemeal.lua")

@ -0,0 +1,47 @@
--- Copies a region to another location, potentially overwriting the exiting region.
-- @module worldeditadditions.copy
local wea = worldeditadditions
local Vector3 = wea.Vector3
-- ██████ ██████ ██████ ██ ██
-- ██ ██ ██ ██ ██ ██ ██
-- ██ ██ ██ ██████ ████
-- ██ ██ ██ ██ ██
-- ██████ ██████ ██ ██
function worldeditadditions.count(source_pos1, source_pos2, target_pos1, target_pos2)
source_pos1, source_pos2 = Vector3.sort(source_pos1, source_pos2)
target_pos1, target_pos2 = Vector3.sort(target_pos1, target_pos2)
local offset = source_pos1:subtract(target_pos1)
-- {source,target}_pos2 will always have the highest co-ordinates now
-- Fetch the nodes in the source area
local manip_source, area_source = worldedit.manip_helpers.init(source_pos1, source_pos2)
local data_source = manip_source:get_data()
-- Fetch a manip for the target area
local manip_target, area_target = worldedit.manip_helpers.init(target_pos1, target_pos2)
local data_target = manip_target:get_data()
-- z y x is the preferred loop order (because CPU cache I'd guess, since then we're iterating linearly through the data array)
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
worldedit.manip_helpers.finish(manip_target, data_target)
return true, worldedit.volume(target_pos1, target_pos2)
end

@ -4,6 +4,14 @@
-- ██ ██ ██ ██ ██ ██ ██
-- ███████ ███████ ███████ ██ ██ ███████ ███████
--- Fills an ellipsoidal area around the given position with the target node.
-- The resulting ellipsoid may optionally be hollow (in which case
-- nodes inside the ellipsoid are left untouched).
-- @param position Vector3 The centre position of the ellipsoid.
-- @param radius Vector3 The radius of the ellipsoid in all 3 dimensions.
-- @param target_node string The name of the node to use to fill the ellipsoid.
-- @param hollow bool Whether the ellipsoid should be hollow or not.
-- @returns number The number of nodes filled to create the (optionally hollow) ellipsoid. This number will be lower with hollow ellipsoids, since the internals of an ellipsoid aren't altered.
function worldeditadditions.ellipsoid(position, radius, target_node, hollow)
-- position = { x, y, z }
local hollow_inner_radius = {
@ -18,7 +26,6 @@ function worldeditadditions.ellipsoid(position, radius, target_node, hollow)
local data = manip:get_data()
local node_id = minetest.get_content_id(target_node)
local node_id_air = minetest.get_content_id("air")
local stride_z, stride_y = area.zstride, area.ystride

@ -0,0 +1,75 @@
local wea = worldeditadditions
-- ███████ ██ ██ ██ ██████ ███████ ███████
-- ██ ██ ██ ██ ██ ██ ██ ██
-- █████ ██ ██ ██ ██████ ███████ █████
-- ██ ██ ██ ██ ██ ██ ██
-- ███████ ███████ ███████ ██ ██ ███████ ███████
function worldeditadditions.ellipsoid2(pos1, pos2, target_node, hollow)
pos1, pos2 = wea.Vector3.sort(pos1, pos2)
local volume = pos2:subtract(pos1)
local volume_half = volume:divide(2)
local radius = pos2:subtract(pos1):divide(2)
print("DEBUG:ellipsoid2 | pos1: "..pos1..", pos2: "..pos2..", target_node: "..target_node)
print("DEBUG:ellipsoid2 radius", radius)
-- position = { x, y, z }
local hollow_inner_radius = {
x = radius.x - 1,
y = radius.y - 1,
z = radius.z - 1
}
-- Fetch the nodes in the specified area
local manip, area = worldedit.manip_helpers.init(pos1, pos2)
local data = manip:get_data()
local node_id = minetest.get_content_id(target_node)
local count = 0 -- The number of nodes replaced
for z = pos2.z, pos1.z, -1 do
for y = pos2.y, pos1.y, -1 do
for x = pos2.x, pos1.x, -1 do
local pos_relative = wea.Vector3.new(x, y, z):subtract(pos1)
:subtract(volume_half)
print("DEBUG pos1", pos1, "pos2", pos2, "volume_half", volume_half, "pos_relative", pos_relative)
-- If we're inside the ellipse, then fill it in
local comp = pos_relative:divide(radius)
local ellipsoid_dist = comp:length_squared()
if ellipsoid_dist <= 1 then
local place_ok = not hollow;
if not place_ok then
-- It must be hollow! Do some additional calculations.
local hx_comp = x/hollow_inner_radius.x
local hy_comp = y/hollow_inner_radius.y
local hz_comp = z/hollow_inner_radius.z
-- It's only ok to place it if it's outside our inner ellipse
place_ok = hx_comp*hx_comp + hy_comp*hy_comp + hz_comp*hz_comp >= 1
end
if place_ok then
data[area:index(x, y, z)] = node_id
count = count + 1
end
end
end
end
end
-- Save the modified nodes back to disk & return
worldedit.manip_helpers.finish(manip, data)
return count
end

@ -0,0 +1,86 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
-- ███████ ██████ ██ ██████ █████ ██
-- ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██████ ██ ██████ ███████ ██
-- ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██ ██ ██ ██ ██ ██ ███████
--
-- ██████ ██ ██████ ██████ ██ ███████
-- ██ ██ ██ ██ ██ ██ ██
-- ██ ██ ██████ ██ ██ █████
-- ██ ██ ██ ██ ██ ██ ██
-- ██████ ██ ██ ██ ██████ ███████ ███████
--- Creates a circular spiral that fills the defined region.
-- @param pos1 Vector3 The 1st position of the defined region.
-- @param pos2 Vector3 The 2nd position of the defined region.
-- @param target_node Vector3 The *normalised* name of the node to use to build the square spiral with.
-- @param interval_initial number The distance between the walls of the spiral.
-- @param acceleration=0 number Increate the interval by this number every time we hit a corner of the square spiral.
-- @returns bool,number|string A success boolean value, followed by either the number of the nodes set or an error message string.
function worldeditadditions.spiral_circle(pos1, pos2, target_node, interval_initial, acceleration)
if not acceleration then acceleration = 0 end
pos1, pos2 = Vector3.sort(pos1, pos2)
local volume = pos2:subtract(pos1)
local volume_half = volume:divide(2)
print("DEBUG:spiral_square | pos1", pos1, "pos2", pos2, "target_node", target_node, "interval_initial:", interval_initial, "acceleration", acceleration)
interval_initial = interval_initial + 1
-- Fetch the nodes in the specified area
local manip, area = worldedit.manip_helpers.init(pos1, pos2)
local data = manip:get_data()
local node_id = minetest.get_content_id(target_node)
local count = 0 -- The number of nodes replaced
local centre = pos2:subtract(pos1):floor():divide(2):add(pos1)
local pos_current = centre:clone():floor()
local interval = interval_initial
local radius = 1
local angle = 0
-- local sides_acc = 0
while pos_current:is_contained(pos1, pos2) do
for y = pos2.y, pos1.y, -1 do
data[area:index(
math.floor(pos_current.x),
y,
math.floor(pos_current.z)
)] = node_id
count = count + 1
end
-- print("DEBUG:spiral_circle centre", centre, "bearing", Vector3.fromBearing(angle, 0, radius))
pos_current = centre:add(Vector3.fromBearing(angle, 0, radius))
local circumference_now = 2 * math.pi * radius
local step = (math.pi*2)/(circumference_now*2)
if angle < math.pi then step = step / 10 end
angle = angle + (step)
local acceleration_constant = 0
if angle > math.pi / 2 then
acceleration_constant = (interval/angle * acceleration) * step
end
radius = 1 + math.floor(interval*(angle / (math.pi*2)), 0)
interval = interval_initial + acceleration_constant
print("DEBUG cpos", pos_current:multiply(1000):floor():divide(1000), "angle", math.deg(angle), "step", wea.round(math.deg(step), 3), "radius", wea.round(radius, 3), "interval", wea.round(interval, 3), "accel_const", acceleration_constant)
end
-- Save the modified nodes back to disk & return
worldedit.manip_helpers.finish(manip, data)
return true, count
end

@ -0,0 +1,92 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
-- ███████ ██████ ██ ██████ █████ ██
-- ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██████ ██ ██████ ███████ ██
-- ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██ ██ ██ ██ ██ ██ ███████
--
-- ███████ ██████ ██ ██ █████ ██████ ███████
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██ ██ ██ ██ ███████ ██████ █████
-- ██ ██ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██████ ██████ ██ ██ ██ ██ ███████
-- ▀▀
--- Creates a square spiral that fills the defined region.
-- @param pos1 Vector3 The 1st position of the defined region.
-- @param pos2 Vector3 The 2nd position of the defined region.
-- @param target_node Vector3 The *normalised* name of the node to use to build the square spiral with.
-- @param interval number The distance between the walls of the spiral.
-- @param acceleration=0 number Increate the interval by this number every time we hit a corner of the square spiral.
-- @returns bool,number|string A success boolean value, followed by either the number of the nodes set or an error message string.
function worldeditadditions.spiral_square(pos1, pos2, target_node, interval, acceleration)
if not acceleration then acceleration = 0 end
pos1, pos2 = Vector3.sort(pos1, pos2)
local volume = pos2:subtract(pos1)
local volume_half = volume:divide(2)
print("DEBUG:spiral_square | pos1: "..pos1..", pos2: "..pos2..", target_node: "..target_node, "interval:"..interval..", acceleration: "..acceleration)
-- Fetch the nodes in the specified area
local manip, area = worldedit.manip_helpers.init(pos1, pos2)
local data = manip:get_data()
local node_id = minetest.get_content_id(target_node)
local count = 0 -- The number of nodes replaced
local centre = pos2:subtract(pos1):floor():divide(2):add(pos1)
local pos_current = centre:clone():floor()
local side_length = 0
local direction = Vector3.new(1, 0, 0)
local side_length_max = interval + 1
local sides_complete = 0
-- local sides_acc = 0
while pos_current:is_contained(pos1, pos2) do
for y = pos2.y, pos1.y, -1 do
data[area:index(pos_current.x, y, pos_current.z)] = node_id
count = count + 1
end
pos_current = pos_current:add(direction)
side_length = side_length + 1
print("DEBUG cpos", pos_current, "side_length", side_length, "side_length_max", side_length_max, "direction", direction)
if side_length >= side_length_max then
sides_complete = sides_complete + 1
-- sides_acc = sides_acc + 1
if sides_complete % 2 == 0 then
-- sides_acc = 0
side_length_max = side_length_max + interval + acceleration + 1
end
side_length = 0
if direction.x == 0 and direction.z == 1 then
direction.x = 1
direction.z = 0
elseif direction.x == 1 and direction.z == 0 then
direction.x = 0
direction.z = -1
elseif direction.x == 0 and direction.z == -1 then
direction.x = -1
direction.z = 0
else
direction.x = 0
direction.z = 1
end
end
end
-- Save the modified nodes back to disk & return
worldedit.manip_helpers.finish(manip, data)
return true, count
end

@ -0,0 +1,55 @@
local wea = worldeditadditions
local Vector3 = dofile(wea.modpath.."/utils/vector3.lua")
-- BUG: This does not exist yet - need to merge @VorTechnix's branch first to get it
-- TODO: Uncomment then once it's implemented
-- local parse_axis = dofile(wea.modpath.."/utils/axis.lua")
--- Parses a token list of axes and counts into a Vector3.
-- For example, "x 4" would become { x = 4, y = 0, z = 0 }, and "? 4 -z 10"
-- might become { x = 4, y = 0, z = -10 }.
-- Note that the input here needs to be *pre split*. wea.split_shell is
-- recommended for this purpose.
-- Uses wea.parse.axis for parsing axis names.
-- @param token_list string[] A list of tokens to parse
-- @returns Vector3 A Vector3 generated from parsing out the input token list.
local function parse_axes(token_list)
local vector_result = wea.Vector3.new()
if #token_list < 2 then
return false, "Error: Not enough arguments (at least 2 are required)"
end
local state = "AXIS"
local current_axis = nil
local success
for i,token in ipairs(token_list) do
if state == "AXIS" then
success, current_axis = parse_axis(token)
if not success then return success, current_axis end
state = "VALUE"
elseif state == "VALUE" then
local offset_this = tonumber(token)
if not offset_this then
return false, "Error: Invalid count value for axis '"..current_axis.."'. Values may only be positive or negative integers."
end
-- Handle negative axes
if current_axis:sub(1, 1) == "-" then
offset_this = -offset_this
current_axis = current_axis:sub(2, 2)
end
vector_result[current_axis] = vector_result[current_axis] + offset_this
state = "AXIS"
else
return false, "Error: Failed to parse input due to unknown state '"..tostring(state).."' (this is probably a bug - please report it!)"
end
end
return vector_result
end
return parse_axes

@ -4,7 +4,9 @@
-- ██ ██ ██ ██ ██ ██ ██
-- ██ ██ ██ ██ ██ ███████ ███████
worldeditadditions.parse = {}
worldeditadditions.parse = {
axes = dofile(worldeditadditions.modpath.."/utils/parse/axes.lua")
}
dofile(worldeditadditions.modpath.."/utils/parse/chance.lua")
dofile(worldeditadditions.modpath.."/utils/parse/map.lua")

@ -361,6 +361,22 @@ function Vector3.max(pos1, pos2)
)
end
--- Given 2 angles and a length, return a Vector3 pointing in that direction.
-- Consider a sphere, with the 2 angles defining a point on the sphere's surface.
-- This function returns that point as a Vector3.
-- @source https://math.stackexchange.com/a/1881767/221181
-- @param angle_x number The X angle.
-- @param angle_y number The Y angle.
-- @param length number The radius of the sphere in question.
-- @returns Vector3 The point on the sphere defined by the aforementioned parameters.
function Vector3.fromBearing(angle_x, angle_y, length)
return Vector3.new( -- X and Y swapped
length * math.cos(angle_x),
length * math.sin(angle_x) * math.sin(angle_y),
length * math.sin(angle_x) * math.cos(angle_y)
)
end
-- ██████ ██████ ███████ ██████ █████ ████████ ██████ ██████

@ -0,0 +1,50 @@
local wea = worldeditadditions
-- ██████ ██████ ██████ ██ ██
-- ██ ██ ██ ██ ██ ██ ██
-- ██ ██ ██ ██████ ████
-- ██ ██ ██ ██ ██
-- ██████ ██████ ██ ██
worldedit.register_command("copy", { -- TODO: Make this an override
params = "<axis:x|y|z|-x|-y|-z|?|front|back|left|right|up|down> <count> [<axis> <count> [...]]",
description = "Copies the defined region to another location - potentially on multiple axes at once.",
privs = { worldedit = true },
require_pos = 2,
parse = function(params_text)
if not params_text then params_text = "" end
local parts = wea.split_shell(params_text)
local copy_offset = wea.parse.axes(parts)
if copy_offset == wea.Vector3.new() then
return false, "Refusing to copy region a distance of 0 nodes"
end
return true, copy_offset:floor()
end,
nodes_needed = function(name)
-- We don't actually modify anything, but without returning a
-- number here safe_region doesn't work
return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name])
end,
func = function(name, copy_offset)
local start_time = wea.get_ms_time()
local source_pos1 = wea.Vector3.clone(worldedit.pos1[name])
local source_pos2 = wea.Vector3.clone(worldedit.pos2[name])
local target_pos1 = source_pos1:add(copy_offset)
local target_pos2 = source_pos2:add(copy_offset)
local success, nodes_modified = wea.copy(
source_pos1, source_pos2,
target_pos1, target_pos2
)
local time_taken = wea.get_ms_time() - start_time
minetest.log("action", name.." used //copy from "..source_pos1.." - "..source_pos2.." to "..target_pos1.." - "..target_pos2..", modifying "..nodes_modified.." nodes in "..wea.format.human_time(time_taken))
return true, nodes_modified.." nodes copied using offset "..copy_offset.." in "..wea.format.human_time(time_taken)
end
})

@ -0,0 +1,51 @@
-- ███████ ██ ██ ██ ██████ ███████ ██████ ██ ██████
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- █████ ██ ██ ██ ██████ ███████ ██ ██ ██ ██ ██
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ███████ ███████ ██ ██ ███████ ██████ ██ ██████
local wea = worldeditadditions
worldedit.register_command("ellipsoid2", {
params = "[<replace_node:dirt> [h[ollow]]]",
description = "Creates am optionally hollow 3D ellipsoid that fills the defined region, filled with <replace_node>.",
privs = { worldedit = true },
require_pos = 2,
parse = function(params_text)
if not params_text or params_text == "" then
params_text = "dirt"
end
local parts = wea.split_shell(params_text)
local replace_node = worldedit.normalize_nodename(parts[1])
if not replace_node then
return false, "Error: Invalid replace_node specified."
end
local hollow = false
if parts[2] == "hollow" or parts[2] == "h" then
hollow = true
end
return true, replace_node, hollow
end,
nodes_needed = function(name, target_node)
local pos1, pos2 = worldedit.sort_pos(worldedit.pos1[name], worldedit.pos2[name])
return math.ceil(4/3 * math.pi * (pos2.x - pos1.x)/2 * (pos2.y - pos1.y)/2 * (pos2.z - pos1.z)/2)
end,
func = function(name, target_node, radius, hollow)
local start_time = wea.get_ms_time()
local pos1, pos2 = wea.Vector3.sort(worldedit.pos1[name], worldedit.pos2[name])
local replaced = wea.ellipsoid2(
pos1, pos2,
target_node,
hollow
)
local time_taken = wea.get_ms_time() - start_time
minetest.log("action", name .. " used //ellipsoid2 at "..pos1.." - "..pos2..", replacing " .. replaced .. " nodes in " .. time_taken .. "s")
return true, replaced .. " nodes replaced in " .. wea.format.human_time(time_taken)
end
})

@ -0,0 +1,88 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
worldedit.register_command("spiral2", {
params = "[<circle|square>] [<replace_node=dirt> [<interval=3> [<acceleration=0>] ] ]",
description = "Generates a spiral that fills the defined region using the specified replace node. The spiral is either square (default) or circular in shape. The interval specifies the distance between the walls of the spiral, and the acceleration specifies how quickly this value should increase.",
privs = { worldedit = true },
require_pos = 2,
parse = function(params_text)
if not params_text then params_text = "" end
params_text = wea.trim(params_text)
if params_text == "" then return true, "square", "dirt", 3, 0 end
local parts = wea.split_shell(params_text)
local mode = "square"
local target_node = "dirt"
local target_node_found = false
local interval = 3
local acceleration = 0
if parts[1] ~= "circle" and parts[1] ~= "square" then
target_node = parts[1]
target_node_found = true
table.remove(parts, 1)
else
mode = parts[1]
table.remove(parts, 1)
end
if #parts >= 1 and not target_node_found then
target_node = parts[1]
table.remove(parts, 1)
end
if #parts >= 1 then
interval = tonumber(parts[1])
if not interval then
return false, "Error: Invalid interval value '"..tostring(parts[1]).."'. Interval values must be integers."
end
table.remove(parts, 1)
end
if #parts >= 1 then
acceleration = tonumber(parts[1])
if not acceleration then
return false, "Error: Invalid acceleration value '"..tostring(parts[1]).."'. Acceleration values must be integers."
end
table.remove(parts, 1)
end
local target_node_norm = worldedit.normalize_nodename(target_node)
if not target_node_norm then
return false, "Error: Unknown node '"..tostring(target_node).."'."
end
return true, mode, target_node_norm, interval, acceleration
end,
nodes_needed = function(name)
return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name])
end,
func = function(name, mode, target_node, interval, acceleration)
local start_time = wea.get_ms_time()
local pos1, pos2 = Vector3.sort(worldedit.pos1[name], worldedit.pos2[name])
local success, count
if mode == "circle" then
success, count = wea.spiral_circle(
pos1, pos2,
target_node,
interval, acceleration
)
if not success then return success, count end
else
success, count = wea.spiral_square(
pos1, pos2,
target_node,
interval, acceleration
)
if not success then return success, count end
end
local time_taken = wea.get_ms_time() - start_time
minetest.log("action", name .. " used //spiral at "..pos1.." - "..pos2..", adding " .. count .. " nodes in " .. time_taken .. "s")
return true, count .. " nodes replaced in " .. wea.format.human_time(time_taken)
end
})

@ -4,11 +4,12 @@
-- ██ ███ ██ ██ ██ ██ ██ ██
-- ███ ███ ██ ██ ███████ ███████ ███████
worldedit.register_command("walls", {
params = "<replace_node> [<thickness=1>]",
params = "[<replace_node=dirt> [<thickness=1>]]",
description = "Creates vertical walls of <replace_node> around the inside edges of the defined region. Optionally specifies a thickness for the walls to be created (defaults to 1)",
privs = { worldedit = true },
require_pos = 2,
parse = function(params_text)
if not params_text or params_text == "" then params_text = "dirt" end
local parts = worldeditadditions.split_shell(params_text)
local target_node

@ -21,6 +21,7 @@ dofile(we_c.modpath.."/player_notify_suppress.lua")
dofile(we_c.modpath.."/commands/convolve.lua")
dofile(we_c.modpath.."/commands/ellipsoid.lua")
dofile(we_c.modpath.."/commands/ellipsoid2.lua")
dofile(we_c.modpath.."/commands/erode.lua")
dofile(we_c.modpath.."/commands/fillcaves.lua")
dofile(we_c.modpath.."/commands/floodfill.lua")
@ -34,6 +35,7 @@ dofile(we_c.modpath.."/commands/replacemix.lua")
dofile(we_c.modpath.."/commands/scale.lua")
dofile(we_c.modpath.."/commands/torus.lua")
dofile(we_c.modpath.."/commands/walls.lua")
dofile(we_c.modpath.."/commands/spiral2.lua")
dofile(we_c.modpath.."/commands/count.lua")
@ -53,7 +55,7 @@ dofile(we_c.modpath.."/commands/extra/saplingaliases.lua")
dofile(we_c.modpath.."/commands/extra/basename.lua")
-- Don't registry the //bonemeal command if the bonemeal mod isn't present
if minetest.get_modpath("bonemeal") then
if minetest.global_exists("bonemeal") then
dofile(we_c.modpath.."/commands/bonemeal.lua")
dofile(we_c.modpath.."/commands/forest.lua")
else

@ -0,0 +1,3 @@
# worldeditadditions_core
This mod's purpose is to provide a solid base upon which the rest of WorldEditAdditions can function. Once it is complete, we will be able to mark our dependency on `worldedit` itself optional. To get to that point though will still require a significant effort in implementing enhanced versions of all existing WorldEdit commands. If you've got some free time and a great idea for a command, please do open a pull request! :D

@ -1,5 +1,5 @@
function worldeditadditions_core.chatcommand_handler(cmd_name, name, param)
local def = assert(worldedit.registered_commands[cmd_name])
local def = assert(worldedit.registered_commands[cmd_name], "Error: Failed to locate worldedit command definition for command '"..name.."' (this is probably a bug).")
if def.require_pos == 2 then
local pos1, pos2 = worldedit.pos1[name], worldedit.pos2[name]

@ -2,7 +2,7 @@ local we_c = worldeditadditions_core
function we_c.override_command(name, def)
local def = table.copy(def)
local success, err = we_c.check_command(name, def)
if not success then
error(err)
return false

@ -2,7 +2,6 @@ local we_c = worldeditadditions_core
function we_c.register_command(name, def)
local def = table.copy(def)
local success, err = we_c.check_command(name, def)
if not success then
return false, err
end

@ -1,5 +1,5 @@
--- WorldEdit dependencies for WorldEditAdditions
--- WorldEdit shim just in case WorldEdit doesn't exist
worldedit = {
registered_commands = {}
registered_commands = { }
}