From 9d37715870d14a2d2c0c76712c65f6b4930a9284 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 20 Jul 2024 09:56:55 -0500 Subject: [PATCH] upd: .mcfunction output in frontend (#20) --- .gitignore | 3 +- README.md | 22 ++-------- app/pages/app.tsx | 47 +++++++++++++++++++- deno.json | 2 +- example/animation.ts | 33 +++++++------- example/dalle.ts | 4 +- example/glowing.ts | 4 +- example/qr.ts | 4 +- example/rainbow.ts | 4 +- example/rgb.ts | 3 +- example/setBlock.ts | 52 ---------------------- example/stainedGlassWindow.ts | 4 +- example/vanilla.ts | 4 +- package.json | 3 +- server.tsx | 2 +- src/_constants.ts | 8 +++- src/mcaddon/cli.ts | 14 ++++-- src/mcaddon/mod.ts | 81 +++++++++++++++++++---------------- src/mcfunction/mod.ts | 5 ++- src/nbt/mod.ts | 36 +++++++++++++--- src/vox/mod.ts | 11 +++++ 21 files changed, 190 insertions(+), 156 deletions(-) delete mode 100644 example/setBlock.ts diff --git a/.gitignore b/.gitignore index f4ec8f7..2c01f5e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ cache/ !.env.example dist/* build/* -node_modules/ \ No newline at end of file +node_modules/ +tests/ \ No newline at end of file diff --git a/README.md b/README.md index 216fe04..e612a30 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,7 @@ # img2mcstructure +[![img2mcstructure on JSR](https://jsr.io/badges/@jjg/img2mcstructure)](https://jsr.io/@jjg/img2mcstructure "Add JSR package") +![JSR structure](https://github.com/user-attachments/assets/3266865a-d6db-4063-9dc9-c0d9c6291860) -> ![RGB example](https://github.com/jasonjgardner/img2mcstructure/assets/1903667/3f98a433-9f41-4009-b840-d8341eb2c2f7) -> [Made with RGB add-on](https://cdn.discordapp.com/attachments/830521962383802368/1201692650122792990/RGB.mcaddon) +### Demo: https://mcstructure.deno.dev/ -> ![Minecraft mural made with RAINBOW III!!!](https://github.com/jasonjgardner/img2mcstructure/assets/1903667/dcc165d9-4cab-4858-9106-330426a4a0e7) -> [_RAINBOW III!!!_ add-on](https://cdn.discordapp.com/attachments/830521962383802368/1200453046304518164/RAINBOW_III-beta.mcaddon). - -Try it at https://mcstructure.deno.dev/ - -Images will be clamped to 64px. To create a larger structure, download Deno and -run: - -```powershell -deno run --allow-net --allow-write --allow-env https://raw.githubusercontent.com/jasonjgardner/img2mcstructure/main/main.ts "http://placekitten.com/256/256" y -``` - -Set the axis parameter to `y` to create ceiling and floors. Omit the axis to -default to walls. - -##### [See examples](./example/README.md) for advanced usage. +##### [See examples](https://github.com/jasonjgardner/img2mcstructure/blob/main/example/README.md) for advanced usage. diff --git a/app/pages/app.tsx b/app/pages/app.tsx index 9fcf32b..dcfc04a 100644 --- a/app/pages/app.tsx +++ b/app/pages/app.tsx @@ -4,6 +4,7 @@ import { DropImage, SelectPalette, SelectSize } from "../components/index.tsx"; import type { PaletteSource } from "../../src/types.ts"; const SVC_URL = "/v1/structure"; +const SVC_URL_FUNCTION = "/v1/fill"; const MAX_HEIGHT = 512; const MAX_WIDTH = 512; @@ -12,6 +13,7 @@ export default function App() { const [userSetSize, setUserSetSize] = useState(false); const [size, setSize] = useState(64); const [axis, setAxis] = useState<"x" | "y">("y"); + const [isStructure, setIsStructure] = useState(true); const [image, setImage] = useState(null); const [title, setTitle] = useState(nanoid(8)); const [db, setDb] = useState([]); @@ -63,7 +65,7 @@ export default function App() { const handleSubmit = useCallback(async () => { setIsProcessing(true); - const res = await fetch(SVC_URL, { + const res = await fetch(isStructure ? SVC_URL : SVC_URL_FUNCTION, { method: "POST", headers: { "Content-Type": "application/json", @@ -80,7 +82,7 @@ export default function App() { const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = `${title.toLowerCase().replace(/\s+/g, "_")}.mcstructure`; + a.download = `${title.toLowerCase().replace(/\s+/g, "_")}.${isStructure ? "mcstructure" : "mcfunction"}`; document.body.appendChild(a); a.click(); a.remove(); @@ -183,6 +185,47 @@ export default function App() { Wall + +
+ Structure + + + +
handlePaletteChange(value)} /> diff --git a/deno.json b/deno.json index aada13a..c5913c1 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@jjg/img2mcstructure", - "version": "1.2.0", + "version": "1.2.1", "exports": "./mod.ts", "tasks": { "build:tailwind": "tailwindcss -i ./style.css -o ./static/style.css --minify", diff --git a/example/animation.ts b/example/animation.ts index 21e202f..ab3fa23 100644 --- a/example/animation.ts +++ b/example/animation.ts @@ -1,27 +1,26 @@ -import createFunction from "./setBlock.ts"; -import { basename, extname, join, readdir, writeFile } from "../deps.ts"; +import { img2mcfunction } from "../mod.ts"; +import { basename, extname, join } from "node:path"; +import { readdir, writeFile } from "node:fs/promises"; import process from "node:process"; +import db from "../db/minecraft.json" with { type: "json" }; if (import.meta.main) { - const dir = process.argv[0] ?? process.cwd(); + const dir = process.argv[0] ?? process.cwd(); - let itr = 0; - const files = await readdir(dir, { recursive: true }); + let itr = 0; + const files = await readdir(dir, { recursive: true }); - for await (const path of files) { - if (!path.endsWith(".png")) { - continue; - } + for await (const path of files) { + if (!path.endsWith(".png")) { + continue; + } - const fn = `${basename(path, extname(path))}.mcfunction`; + const fn = `${basename(path, extname(path))}.mcfunction`; - await writeFile( - join(dir, fn), - await createFunction(path, [200, 100, 200]), - ); + await writeFile(join(dir, fn), await img2mcfunction(path, db, [0, 0, 0])); - itr++; - } + itr++; + } - process.exit(0); + process.exit(0); } diff --git a/example/dalle.ts b/example/dalle.ts index bec6029..a698ef0 100644 --- a/example/dalle.ts +++ b/example/dalle.ts @@ -1,10 +1,10 @@ import type { Axis } from "../src/types.ts"; -import { nanoid } from "../deps.ts"; +import { nanoid } from "nanoid"; import img2mcstructure, { createPalette } from "../src/mcstructure/mod.ts"; import OpenAI from "openai"; import { load } from "@std/dotenv"; import db from "../db/rainbow.json" with { type: "json" }; -import { writeFile } from "../deps.ts"; +import { writeFile } from "node:fs/promises"; import process from "node:process"; await load(); diff --git a/example/glowing.ts b/example/glowing.ts index 825463a..6424bbc 100644 --- a/example/glowing.ts +++ b/example/glowing.ts @@ -1,8 +1,8 @@ import type { Axis } from "../src/types.ts"; -import { nanoid } from "../deps.ts"; +import { nanoid } from "nanoid"; import img2mcstructure, { createPalette } from "../src/mcstructure/mod.ts"; import db from "../db/rainbow.json" with { type: "json" }; -import { writeFile } from "../deps.ts"; +import { writeFile } from "node:fs/promises"; import process from "node:process"; const palette = createPalette(Object.fromEntries( diff --git a/example/qr.ts b/example/qr.ts index d381974..d8d3d19 100644 --- a/example/qr.ts +++ b/example/qr.ts @@ -1,9 +1,9 @@ import { qrPng } from "@sigmasd/qrpng"; import type { Axis } from "../src/types.ts"; -import { nanoid } from "../deps.ts"; +import { nanoid } from "nanoid"; import img2mcstructure, { createPalette } from "../src/mcstructure/mod.ts"; import db from "../db/minecraft.json" with { type: "json" }; -import { writeFile } from "../deps.ts"; +import { writeFile } from "node:fs/promises"; import process from "node:process"; const palette = createPalette(Object.fromEntries( diff --git a/example/rainbow.ts b/example/rainbow.ts index a9d5a62..2001465 100644 --- a/example/rainbow.ts +++ b/example/rainbow.ts @@ -1,8 +1,8 @@ import type { Axis } from "../src/types.ts"; -import { nanoid } from "../deps.ts"; +import { nanoid } from "nanoid"; import img2mcstructure, { createPalette } from "../src/mcstructure/mod.ts"; import db from "../db/rainbow.json" with { type: "json" }; -import { writeFile } from "../deps.ts"; +import { writeFile } from "node:fs/promises"; import process from "node:process"; const structureId = nanoid(6); diff --git a/example/rgb.ts b/example/rgb.ts index 257b303..f544c5b 100644 --- a/example/rgb.ts +++ b/example/rgb.ts @@ -2,7 +2,8 @@ import type { Axis } from "../src/types.ts"; import { createMcStructure } from "../src/mcstructure/mod.ts"; import decode from "../src/_decode.ts"; import blocks from "../db/rgb.ts"; -import { join, writeFile } from "../deps.ts"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; import process from "node:process"; export default async function main( diff --git a/example/setBlock.ts b/example/setBlock.ts deleted file mode 100644 index f6bcb43..0000000 --- a/example/setBlock.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { basename, extname, imagescript, join } from "../deps.ts"; -import decode from "../src/_decode.ts"; -import { getNearestColor } from "../src/_lib.ts"; -import blocks from "../db/rainbow.json" with { type: "json" }; -import createPalette from "../src/_palette.ts"; -import { writeFile } from "../deps.ts"; -import process from "node:process"; -import createFunction from "../src/mcfunction/mod.ts"; - -export async function writeTsFunction(imgSrc: string) { - const frames = await decode(imgSrc); - - const len = Math.min(256, frames.length); - - const functionName = basename(imgSrc, extname(imgSrc)); - - const lines = [`function ${functionName}Create() { - const overworld = world.getDimension("overworld"); - `]; - - const palette = createPalette(blocks); - - for (let z = 0; z < len; z++) { - const img = frames[z]; - for (const [x, y, c] of img.iterateWithColors()) { - const [r, g, b, a] = imagescript.Image.colorToRGBA(c); - - const nearest = getNearestColor([r, g, b], palette); - - lines.push( - `overworld.setBlockState(new BlockLocation(${x}, ${y}, ${z}), BlockStates.get("${nearest.id}"), BlockStates.get("${ - JSON.stringify(nearest.states) - }"));`, - ); - } - } - - lines.push("}"); - - return lines.join("\n"); -} - - -if (import.meta.main) { - await writeFile( - join(process.cwd(), `rgb_${Date.now()}.mcfunction`), - await createFunction( - blocks, - process.argv[0] ?? join(process.cwd(), "example", "skull.png"), - ), - ); -} diff --git a/example/stainedGlassWindow.ts b/example/stainedGlassWindow.ts index a355ead..4027b40 100644 --- a/example/stainedGlassWindow.ts +++ b/example/stainedGlassWindow.ts @@ -1,7 +1,7 @@ import type { Axis } from "../src/types.ts"; -import { nanoid } from "../deps.ts"; +import { nanoid } from "nanoid"; import img2mcstructure, { createPalette } from "../src/mcstructure/mod.ts"; -import { writeFile } from "../deps.ts"; +import { writeFile } from "node:fs/promises"; import process from "node:process"; const { default: mc } = await import("../db/minecraft.json"); diff --git a/example/vanilla.ts b/example/vanilla.ts index 852715a..0eabdb2 100644 --- a/example/vanilla.ts +++ b/example/vanilla.ts @@ -1,9 +1,9 @@ import type { Axis } from "../src/types.ts"; -import { nanoid } from "../deps.ts"; +import { nanoid } from "nanoid"; import img2mcstructure, { createPalette } from "../src/mcstructure/mod.ts"; import db from "../db/minecraft.json" with { type: "json" }; import process from "node:process"; -import { writeFile } from "../deps.ts"; +import { writeFile } from "node:fs/promises"; const structureId = nanoid(6); diff --git a/package.json b/package.json index 3bbe8ce..52869a9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "type": "module", "scripts": { - "format": "bunx @biomejs/biome format --write ./**/*.{js,ts,json,jsx,tsx,html,css}" + "format": "bunx @biomejs/biome format --write ./**/*.{js,ts,json,jsx,tsx,html,css}", + "build": "bun build ./app/index.tsx > ./dist/bundle.js" }, "dependencies": { "@hono/hono": "npm:@jsr/hono__hono", diff --git a/server.tsx b/server.tsx index c96a50a..2da3e78 100644 --- a/server.tsx +++ b/server.tsx @@ -33,7 +33,7 @@ export default function main(defaultDb: PaletteSource) { const { img, axis, db } = await ctx.req.json(); try { - const data = await createFunction(db ?? defaultDb, img, axis); + const data = await createFunction(img, db ?? defaultDb, [0, 0, 0]); return new Response(data, { headers: { diff --git a/src/_constants.ts b/src/_constants.ts index 2be8f17..a9fd6a3 100644 --- a/src/_constants.ts +++ b/src/_constants.ts @@ -5,6 +5,11 @@ import process from "node:process"; */ export const BLOCK_VERSION = 18153475; +/** + * NBT tag data version. + */ +export const NBT_DATA_VERSION = 3093; + /** * Minecraft behavior block format version. */ @@ -35,5 +40,6 @@ export const MAX_WIDTH = Number(process.env.MAX_SIZE ?? 256); /** * Maximum depth of structure. + * Limited to 1 chunk when deployed to Deno Deploy. */ -export const MAX_DEPTH = 256; +export const MAX_DEPTH = process.env.DENO_DEPLOYMENT_ID !== undefined ? 16 : 256; diff --git a/src/mcaddon/cli.ts b/src/mcaddon/cli.ts index 2ef9529..be07c5a 100644 --- a/src/mcaddon/cli.ts +++ b/src/mcaddon/cli.ts @@ -10,9 +10,10 @@ async function createAddon( gridSize = 3, resolution = 16, dest?: string, - axis?: Axis + axis?: Axis, + pbr = false ) { - const addon = await img2mcaddon(src, gridSize, resolution, axis ?? "z"); + const addon = await img2mcaddon(src, gridSize, resolution, axis ?? "z", pbr); const addonDest = dest ?? @@ -25,7 +26,7 @@ async function createAddon( if (import.meta.main) { const { - values: { src, gridSize, resolution, dest, axis }, + values: { src, gridSize, resolution, dest, axis, pbr }, } = parseArgs({ args: process.argv.slice(2), options: { @@ -51,6 +52,10 @@ if (import.meta.main) { type: "string", multiple: false, default: "z", + }, + pbr: { + type: "boolean", + default: false, } }, }); @@ -60,7 +65,8 @@ if (import.meta.main) { Math.max(1, Number(gridSize)), Math.max(16, Math.min(1024, Number(resolution))), dest, - axis as Axis + axis as Axis, + pbr ); process.exit(0); } diff --git a/src/mcaddon/mod.ts b/src/mcaddon/mod.ts index b44412e..e561d3f 100644 --- a/src/mcaddon/mod.ts +++ b/src/mcaddon/mod.ts @@ -100,6 +100,7 @@ function createBlock({ return JSON.stringify(data, null, 2); } +// TODO: Refactor function. It is redundant with rotateStructure function function rotateVolume(volume: number[][][], axis: Axis): number[][][] { const rotatedVolume = volume.map((z) => z.map((y) => y.map(() => -1))); @@ -127,18 +128,27 @@ function rotateVolume(volume: number[][][], axis: Axis): number[][][] { return rotatedVolume; } + /** * Convert an image to a mosaic using custom Minecraft blocks. * @param src Image source - * @param gridSize The target size of the grid structure output + * @param gridSize The target size of the grid structure output. For best results, grid size should be the image width and height divided by the resolution. * @param resolution The target resolution of the block texture output - * @returns JSZip archive data of the .mcaddon + * @param axis The axis to rotate the structure on + * @param pbr Enable PBR textures + * @returns Archive data of the .mcaddon + * @example Split an image into a 3×3 grid with 16x texture output. + * ```ts + * const file = await img2mcaddon("path/to/image.png", 3, 16); + * await writeFile("output.mcaddon", file); + * ``` */ export default async function img2mcaddon( src: string | URL, gridSize: number, resolution: number, axis: Axis = "z", + pbr = false, ): Promise { const jobId = nanoid(7); const addon = new JSZip(); @@ -185,7 +195,7 @@ export default async function img2mcaddon( console.warn(`Failed to decode normal map: ${err}`); } - const namespace = baseName.replace(/\W|\.\@\$\%/g, "_"); + const namespace = baseName.replace(/\W|\.\@\$\%/g, "_").substring(0, 16); const terrainData: Record< string, @@ -206,8 +216,6 @@ export default async function img2mcaddon( const cropSize = Math.min(resolution, Math.round(frames[0].width / gridSize)); - let pbr = false; - for (let z = 0; z < depth; z++) { const resizeTo = gridSize * cropSize; const frame: imagescript.Image = ( @@ -216,7 +224,7 @@ export default async function img2mcaddon( for (let x = 0; x < gridSize; x++) { for (let y = 0; y < gridSize; y++) { - const sliceId = `slice_${x}_${y}_${z}`; + const sliceId = `${namespace}_${x}_${y}_${z}`; const xPos = x * cropSize; const yPos = y * cropSize; const slice = frame.clone().crop(xPos, yPos, cropSize, cropSize); @@ -243,34 +251,34 @@ export default async function img2mcaddon( color: sliceId, }; - try { - addon.file( - `rp/textures/blocks/${sliceId}_mer.png`, - await merTexture[z] - .clone() - .resize(resizeTo, resizeTo) - .crop(xPos, yPos, cropSize, cropSize) - .encode(), - ); - textureSet.metalness_emissive_roughness = `${sliceId}_mer`; - pbr = true; - } catch (err) { - console.warn(`Failed to add MER map: ${err}`); - } - - try { - addon.file( - `rp/textures/blocks/${sliceId}_normal.png`, - await normalTexture[z] - .clone() - .resize(resizeTo, resizeTo) - .crop(xPos, yPos, cropSize, cropSize) - .encode(), - ); - textureSet.normal = `${sliceId}_normal`; - pbr = true; - } catch (err) { - console.warn(`Failed to add normal map: ${err}`); + if (pbr) { + try { + addon.file( + `rp/textures/blocks/${sliceId}_mer.png`, + await merTexture[z] + .clone() + .resize(resizeTo, resizeTo) + .crop(xPos, yPos, cropSize, cropSize) + .encode(), + ); + textureSet.metalness_emissive_roughness = `${sliceId}_mer`; + } catch (err) { + console.warn(`Failed to add MER map: ${err}`); + } + + try { + addon.file( + `rp/textures/blocks/${sliceId}_normal.png`, + await normalTexture[z] + .clone() + .resize(resizeTo, resizeTo) + .crop(xPos, yPos, cropSize, cropSize) + .encode(), + ); + textureSet.normal = `${sliceId}_normal`; + } catch (err) { + console.warn(`Failed to add normal map: ${err}`); + } } addon.file( @@ -285,7 +293,7 @@ export default async function img2mcaddon( ), ); - terrainData[`${namespace}_${sliceId}`] = { + terrainData[sliceId] = { textures: `textures/blocks/${sliceId}`, }; @@ -345,13 +353,12 @@ export default async function img2mcaddon( }; const mcstructure = await nbt.write(nbt.parse(JSON.stringify(tag)), { - // name, endian: "little", compression: null, bedrockLevel: false, }); - addon.file(`bp/structures/${namespace}.mcstructure`, mcstructure); + addon.file(`bp/structures/mosaic/${namespace}.mcstructure`, mcstructure); const mipLevels = { diff --git a/src/mcfunction/mod.ts b/src/mcfunction/mod.ts index df5b14a..0b3f37a 100644 --- a/src/mcfunction/mod.ts +++ b/src/mcfunction/mod.ts @@ -3,6 +3,7 @@ import * as imagescript from "imagescript"; import { getNearestColor } from "../_lib.ts"; import decode from "../_decode.ts"; import createPalette from "../_palette.ts"; +import { MAX_DEPTH } from "../_constants.ts"; /** * Convert an image to a series of `setblock` commands. @@ -18,7 +19,7 @@ export default async function img2mcfunction( ): Promise { const frames = await decode(imgSrc); - const len = Math.min(256, frames.length); + const len = Math.min(MAX_DEPTH, frames.length); const lines = []; @@ -33,7 +34,7 @@ export default async function img2mcfunction( const nearest = getNearestColor([r, g, b], createPalette(blocks)); lines.push( - `setblock ${x + offset[0]} ${Math.abs(img.height - y + offset[1])} ${ + `setblock ~${Number(x + offset[0])}~${Math.abs(img.height - y + offset[1])}~${ offset[2] } ${nearest.id} replace`, ); diff --git a/src/nbt/mod.ts b/src/nbt/mod.ts index e0353f5..2697c15 100644 --- a/src/nbt/mod.ts +++ b/src/nbt/mod.ts @@ -1,7 +1,7 @@ import type { Axis, IBlock } from "../types.ts"; import * as nbt from "nbtify"; import * as imagescript from "imagescript"; -import { DEFAULT_BLOCK, MASK_BLOCK, MAX_DEPTH } from "../_constants.ts"; +import { DEFAULT_BLOCK, MASK_BLOCK, MAX_DEPTH, NBT_DATA_VERSION } from "../_constants.ts"; import { compareStates, getNearestColor } from "../_lib.ts"; import decode from "../_decode.ts"; @@ -23,6 +23,17 @@ export interface INbtBlock { state: number; } +/** + * NBT structure format + */ +export interface INbtTag { + size: [number, number, number]; + blocks: INbtBlock[]; + palette: IPaletteEntry[]; + entities: Record[]; + DataVersion: number; +} + /** * Get the appropriate block for the given pixel color. * @param c Pixel color @@ -69,11 +80,18 @@ function findBlock( return [nearest, blockIdx]; } +/** + * Create an NBT structure from image frames. + * @param frames Image frames to convert to NBT structure layers + * @param palette Block palette + * @param axis The axis on which to orient the structure + * @returns NBT tag + */ export function constructDecoded( frames: imagescript.GIF | Array, palette: IBlock[], axis: Axis = "x", -) { +): INbtTag { /** * Structure size (X, Y, Z) */ @@ -125,7 +143,7 @@ export function constructDecoded( } } - const tag = { + const tag: INbtTag = { size: axis === "y" ? [width, height, depth] : axis === "z" @@ -134,17 +152,23 @@ export function constructDecoded( blocks, palette: blockPalette, entities: [], - DataVersion: 3093, + DataVersion: NBT_DATA_VERSION, }; return tag; } +/** + * Create a NBT structure from image frames. + * @param frames Image frames to convert to NBT structure layers + * @param palette Block palette + * @param axis Axis on which to orient the structure + * @returns NBT structure data + */ export async function createNbtStructure( frames: imagescript.GIF | Array, palette: IBlock[], axis: Axis = "x", - name = "img2nbt", ): Promise { const decoded = constructDecoded(frames, palette, axis); const structure = JSON.stringify(decoded, null, 2); @@ -158,7 +182,7 @@ export async function createNbtStructure( } /** - * Create a NBT structure from an image. + * Decode an image and convert it to a NBT structure. * @param imgSrc Image source * @param db Block palette * @param axis Axis orientation diff --git a/src/vox/mod.ts b/src/vox/mod.ts index 08e1503..6dffa18 100644 --- a/src/vox/mod.ts +++ b/src/vox/mod.ts @@ -166,6 +166,11 @@ export function constructDecoded( return tag; } +/** + * Convert a VOX file to a GIF representation. + * @param voxSrc VOX file path + * @returns ImageScript GIF or Frame array created from VOX structure layers + */ export async function vox2gif( voxSrc: string, ): Promise { @@ -228,6 +233,12 @@ export async function vox2gif( return frames; } +/** + * Convert a VOX file to a .mcstructure file. + * @param voxSrc VOX file path + * @param db Minecraft block palette + * @returns NBT data + */ export default async function vox2mcstructure( voxSrc: string, db: IBlock[] = [],