diff --git a/.vscode/launch.json b/.vscode/launch.json index a315c99..217e48b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -55,9 +55,14 @@ "--resolution", "256", "--src", - "./test/connected/lobby_ceiling.png", + "./tests/connected/lobby_ceiling.png", "--dest", - "./build/test.mcaddon", + "./build/lobby_ceiling.mcaddon", + "--pbr", + "--frames", + "1", + "--gridSize", + "16", "--axis", "y" ], diff --git a/app/components/index.tsx b/app/components/index.tsx index 75e236c..d9baa0a 100644 --- a/app/components/index.tsx +++ b/app/components/index.tsx @@ -33,7 +33,7 @@ export function DropImage({ onChange }: { onChange: (file: File) => void }) { const className = cx( "border-dashed border-2 border-gray-500 dark:border-gray-400 rounded-md flex flex-grow items-center justify-center cursor-pointer h-full min-h-40", - dragging && "border-blue-500" + dragging && "border-blue-500", ); return ( @@ -129,15 +129,17 @@ export function SelectPalette({ onChange={({ target }) => { onChange( Array.from((target as HTMLSelectElement).selectedOptions).map( - (o) => o.value - ) + (o) => o.value, + ), ); }} > {Object.keys(options).map((optGroup) => ( {options[optGroup].map((option) => ( - + ))} ))} @@ -170,7 +172,9 @@ export function SelectSize({ value={value} > {options.map((option) => ( - + ))} diff --git a/app/index.tsx b/app/index.tsx index 4cfd7a3..20231ca 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,4 +1,4 @@ import { render } from "@hono/hono/jsx/dom"; import App from "./pages/app.tsx"; -render(, document.getElementById("app")); \ No newline at end of file +render(, document.getElementById("app")); diff --git a/app/pages/app.tsx b/app/pages/app.tsx index dcfc04a..b612054 100644 --- a/app/pages/app.tsx +++ b/app/pages/app.tsx @@ -54,7 +54,7 @@ export default function App() { 0, 0, canvasRef.current.width, - canvasRef.current.height + canvasRef.current.height, ); ctx.drawImage(img, 0, 0, newWidth, newHeight); } @@ -82,7 +82,9 @@ 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, "_")}.${isStructure ? "mcstructure" : "mcfunction"}`; + a.download = `${title.toLowerCase().replace(/\s+/g, "_")}.${ + isStructure ? "mcstructure" : "mcfunction" + }`; document.body.appendChild(a); a.click(); a.remove(); @@ -98,7 +100,7 @@ export default function App() { palettes.map(async (id) => { const res = await fetch(`/db/${id}`); return res.json() as Promise; - }) + }), ) ).flat(); @@ -160,9 +162,8 @@ export default function App() { checked={axis === "y"} onChange={(e) => setAxis( - (e.target as HTMLInputElement).value === "y" ? "y" : "x" - ) - } + (e.target as HTMLInputElement).value === "y" ? "y" : "x", + )} />{" "} Ceiling / Floor @@ -178,9 +179,8 @@ export default function App() { checked={axis === "x"} onChange={({ target }) => setAxis( - (target as HTMLInputElement).value === "x" ? "x" : "y" - ) - } + (target as HTMLInputElement).value === "x" ? "x" : "y", + )} /> Wall @@ -188,7 +188,7 @@ export default function App() {
Structure - @@ -217,11 +213,7 @@ export default function App() { id="mcfunction" value="mcfunction" checked={!isStructure} - onChange={(e) => - setIsStructure( - !e.target.checked - ) - } + onChange={(e) => setIsStructure(!e.target.checked)} /> .mcfunction diff --git a/bds/_bds.ts b/bds/_bds.ts deleted file mode 100644 index cb4ef3a..0000000 --- a/bds/_bds.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Application } from "https://deno.land/x/oak/mod.ts"; - -const app = new Application(); -const lines: string[] = []; -for (let itr = 0; itr < 22; itr++) { - const fileContents = await Deno.readTextFile( - `${itr}.mcfunction`, - ); - lines.push(...fileContents.split("\n")); -} - -// Split lines into even groups of 10 -const commands: string[][] = []; -const groupSize = 32; - -for (let itr = 0; itr < lines.length; itr += groupSize) { - commands.push(lines.slice(itr, itr + groupSize)); -} -let frame = 0; - -app.use((ctx) => { - if (ctx.request.method !== "GET") { - console.log(ctx.request); - } - - if (frame >= commands.length) { - frame = 0; - } - - ctx.response.headers.set("Content-Type", "text/plain"); - - ctx.response.body = JSON.stringify({ - command: commands[frame].join(";"), - target: "player", - }); - - frame++; -}); - -await app.listen({ port: 8000 }); diff --git a/bds/_pack.ts b/bds/_pack.ts deleted file mode 100644 index 2601498..0000000 --- a/bds/_pack.ts +++ /dev/null @@ -1,119 +0,0 @@ -import * as esbuild from "https://deno.land/x/esbuild@v0.15.16/wasm.js"; -import { basename, extname, join, JSZip } from "../deps.ts"; - -export async function compile( - src: string, -) { - const code = await Deno.readTextFile(src); - - const name = basename(src, extname(src)); - const transformed = await esbuild.transform(code, { - loader: "ts", - sourcemap: true, - sourcefile: src, - sourcesContent: true, - tsconfigRaw: `{ - "compilerOptions":{ - "target":"es6", - "moduleResolution":"node", - "module":"es2020", - "declaration":false, - "noLib":false, - "emitDecoratorMetadata":true, - "experimentalDecorators":true, - "sourceMap":true, - "pretty":true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "allowUnreachableCode":true, - "allowUnusedLabels":true, - "noImplicitAny":true, - "noImplicitReturns":false, - "noImplicitUseStrict":false, - "outDir":"build/", - "rootDir": ".", - "baseUrl":"development_behavior_packs/", - "listFiles":false, - "noEmitHelpers":true - }, - "exclude":[ - "node_modules" - ], - "compileOnSave":false - }`, - }); - - const out = join(Deno.cwd(), `${name}.js`); - - await Deno.writeTextFile( - out, - transformed.code.replace(/npm:@/g, "@"), - ); - await Deno.writeTextFile( - join(Deno.cwd(), `${name}.js.map`), - transformed.map, - ); - - esbuild.stop(); - - return await Deno.readTextFile(out); -} - -function writeManifests() { - const TARGET_VERSION = [1, 20, 50]; - const SERVER_VERSION = "1.8.0-beta"; - const manifest = { - format_version: 2, - header: { - name: "Sever Connection", - description: "Scripts to call HTTP server", - uuid: crypto.randomUUID(), - version: [1, 0, 0], - min_engine_version: TARGET_VERSION, - }, - modules: [ - { - entry: "server.js", - type: "script", - language: "javascript", - uuid: crypto.randomUUID(), - version: [1, 0, 0], - }, - ], - dependencies: [ - { - module_name: "@minecraft/server", - version: SERVER_VERSION, - }, - { - module_name: "@minecraft/server-net", - version: "1.0.0-beta", - }, - ], - }; - - return JSON.stringify(manifest, null, 2); -} - -export async function createPack() { - const mcaddon = new JSZip(); - - mcaddon.folder("scripts")!.file( - "server.js", - await compile( - join(Deno.cwd(), "bds", "_script.ts"), - ), - ); - - mcaddon.file("manifest.json", writeManifests()); - - await Deno.writeFile( - join(Deno.cwd(), "server.mcpack"), - await mcaddon.generateAsync({ type: "uint8array" }), - ); -} - -if (import.meta.main) { - await createPack(); - Deno.exit(0); -} diff --git a/bds/_script.ts b/bds/_script.ts deleted file mode 100644 index 397244c..0000000 --- a/bds/_script.ts +++ /dev/null @@ -1,36 +0,0 @@ -/// @deno-types="npm:@types/mojang-gametest" -/// @deno-types="npm:@minecraft/server" -/// @deno-types="npm:@minecraft/server-net" -import { GameMode, system, world } from "npm:@minecraft/server"; -import { http, type HttpResponse } from "npm:@minecraft/server-net"; - -const SERVER_URL = "http://127.0.0.1:8000/bds/"; -const tickSpeed = 10; - -function processCommandResponse({ body }: HttpResponse) { - const overworld = world.getDimension("overworld"); - const players = overworld.getPlayers({ - excludeGameModes: [GameMode.spectator], - }); - const { command, target } = JSON.parse(body); - const commands = command.split(";"); - for (const player of players) { - if (player.name !== target) { - continue; - } - - for (const command of commands) { - player.runCommand(command); - } - } -} - -function getHttpCommand() { - http.get(SERVER_URL).then(processCommandResponse).catch((err) => { - console.warn(err); - }); -} - -system.runInterval(function intervalTick() { - getHttpCommand(); -}, tickSpeed); diff --git a/biome.json b/biome.json deleted file mode 100644 index f084ee6..0000000 --- a/biome.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - } -} \ No newline at end of file diff --git a/deno.json b/deno.json index c5913c1..d87f24a 100644 --- a/deno.json +++ b/deno.json @@ -1,16 +1,24 @@ { "name": "@jjg/img2mcstructure", - "version": "1.2.1", + "version": "1.2.2", "exports": "./mod.ts", - "tasks": { - "build:tailwind": "tailwindcss -i ./style.css -o ./static/style.css --minify", - "build:packup": "packup build ./public/index.html", - "dev:frontend": "packup serve ./public/index.html", - "dev:backend": "deno run -A ./main.ts", - "dev:tailwind": "tailwindcss -i ./style.css -o ./static/style.css --watch", - "ci:deploy": "deno fmt && deno task build:tailwind && deno task build:packup", - "dev": "deno task ci:deploy && deno task dev:backend", - "demo:bds": "deno run -A ./bds/_pack.ts && deno run -A ./bds/_bds.ts" + "fmt": { + "include": [ + "./app", + "./src", + "./*.ts", + "./*.tsx", + "./example", + "./db" + ], + "exclude": [ + "./static", + "./dist", + "./build", + "./cache", + "./node_modules", + "./tests" + ] }, "compilerOptions": { "lib": [ diff --git a/example/animation.ts b/example/animation.ts index ab3fa23..75d78ef 100644 --- a/example/animation.ts +++ b/example/animation.ts @@ -5,22 +5,22 @@ 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 img2mcfunction(path, db, [0, 0, 0])); + 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 a698ef0..409b147 100644 --- a/example/dalle.ts +++ b/example/dalle.ts @@ -31,11 +31,7 @@ export default async function main( const imageUrl: string = response.data[0].url ?? ""; - return await img2mcstructure( - imageUrl, - createPalette(db), - axis, - ); + return await img2mcstructure(imageUrl, createPalette(db), axis); } if (import.meta.main) { diff --git a/example/glowing.ts b/example/glowing.ts index 6424bbc..b30c100 100644 --- a/example/glowing.ts +++ b/example/glowing.ts @@ -5,11 +5,13 @@ import db from "../db/rainbow.json" with { type: "json" }; import { writeFile } from "node:fs/promises"; import process from "node:process"; -const palette = createPalette(Object.fromEntries( - Object.keys(db).filter((id) => id.includes("lit") || id.includes("lamp")).map( - (id) => [id, db[id as keyof typeof db]], +const palette = createPalette( + Object.fromEntries( + Object.keys(db) + .filter((id) => id.includes("lit") || id.includes("lamp")) + .map((id) => [id, db[id as keyof typeof db]]), ), -)); +); const structureId = nanoid(6); diff --git a/example/qr.ts b/example/qr.ts index d8d3d19..af49697 100644 --- a/example/qr.ts +++ b/example/qr.ts @@ -6,14 +6,17 @@ import db from "../db/minecraft.json" with { type: "json" }; import { writeFile } from "node:fs/promises"; import process from "node:process"; -const palette = createPalette(Object.fromEntries( - Object.keys(db).filter((id) => - !id.includes("stained_glass") && ( - id.includes("black") || id.includes("white") || - id.includes("gray") - ) - ).map((id) => [id, db[id as keyof typeof db]]), -)); +const palette = createPalette( + Object.fromEntries( + Object.keys(db) + .filter( + (id) => + !id.includes("stained_glass") && + (id.includes("black") || id.includes("white") || id.includes("gray")), + ) + .map((id) => [id, db[id as keyof typeof db]]), + ), +); const qr = qrPng( new TextEncoder().encode( diff --git a/example/rgb.ts b/example/rgb.ts index f544c5b..afe2771 100644 --- a/example/rgb.ts +++ b/example/rgb.ts @@ -6,10 +6,7 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import process from "node:process"; -export default async function main( - imgSrc: string, - axis: Axis = "x", -) { +export default async function main(imgSrc: string, axis: Axis = "x") { const img = await decode(imgSrc); if (!img.length) { diff --git a/example/stainedGlassWindow.ts b/example/stainedGlassWindow.ts index 4027b40..4b56ba9 100644 --- a/example/stainedGlassWindow.ts +++ b/example/stainedGlassWindow.ts @@ -12,11 +12,13 @@ const db = { ...rainbow, }; -const palette = createPalette(Object.fromEntries( - Object.keys(db).filter((id) => id.includes("stained_glass")).map(( - id, - ) => [id, db[id as keyof typeof db]]), -)); +const palette = createPalette( + Object.fromEntries( + Object.keys(db) + .filter((id) => id.includes("stained_glass")) + .map((id) => [id, db[id as keyof typeof db]]), + ), +); const structureId = nanoid(6); diff --git a/main.ts b/main.ts index 88e56df..3c87f90 100644 --- a/main.ts +++ b/main.ts @@ -1,6 +1,6 @@ import type { Axis } from "./src/types.ts"; import cli from "./src/mcstructure/cli.ts"; -import server from "./server.tsx"; +import server from "./server.ts"; import db from "./db/minecraft.json" with { type: "json" }; import process from "node:process"; diff --git a/mod.ts b/mod.ts index f6c9931..47dc69f 100644 --- a/mod.ts +++ b/mod.ts @@ -2,5 +2,6 @@ export { default as img2schematic } from "./src/schematic/mod.ts"; export { default as img2mcstructure } from "./src/mcstructure/mod.ts"; export { default as img2nbt } from "./src/nbt/mod.ts"; export { default as vox2mcstructure, vox2gif } from "./src/vox/mod.ts"; -export { default as img2mcaddon } from "./src/mcaddon/mod.ts" -export { default as img2mcfunction } from "./src/mcfunction/mod.ts"; \ No newline at end of file +export { default as img2mcaddon } from "./src/mcaddon/mod.ts"; +export { default as img2mcfunction } from "./src/mcfunction/mod.ts"; +export { createImageSeries, dir2series, series2atlas } from "./src/atlas.ts"; diff --git a/package.json b/package.json index 52869a9..baa9bc1 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { "type": "module", "scripts": { - "format": "bunx @biomejs/biome format --write ./**/*.{js,ts,json,jsx,tsx,html,css}", - "build": "bun build ./app/index.tsx > ./dist/bundle.js" + "build": "bun run format && bun build ./app/index.tsx > ./dist/bundle.js", + "start": "bun run build && bun run ./main.ts", + "bump": "deno run -A jsr:@mys/bump@1" }, "dependencies": { "@hono/hono": "npm:@jsr/hono__hono", @@ -17,7 +18,6 @@ "vox-reader": "2.1.2" }, "devDependencies": { - "@biomejs/biome": "1.8.3", "@types/bun": "^1.1.6", "@types/node": "^20.14.9" } diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..ae4167d --- /dev/null +++ b/server.ts @@ -0,0 +1,140 @@ +import type { PaletteSource } from "./src/types.ts"; +import { Hono } from "@hono/hono"; +import { serveStatic } from "@hono/hono/serve-static"; +import { readFile } from "node:fs/promises"; +import img2mcstructure, { createPalette } from "./src/mcstructure/mod.ts"; +import createFunction from "./src/mcfunction/mod.ts"; +import { img2mcaddon } from "./mod.ts"; + +export default function main(defaultDb: PaletteSource) { + const app = new Hono(); + + app.post("/v1/structure", async (ctx) => { + const { img, axis, db } = await ctx.req.json(); + + try { + const data = await img2mcstructure( + img, + createPalette(db ?? defaultDb), + axis, + ); + + return new Response(data, { + headers: { + "Content-Disposition": 'attachment; filename="img.mcstructure"', + "Content-Type": "application/octet-stream", + "Access-Control-Allow-Origin": "*", + }, + }); + } catch (err) { + return new Response(err.message, { status: 500 }); + } + }); + + app.post("/v1/fill", async (ctx) => { + const { img, axis, db } = await ctx.req.json(); + + try { + const data = await createFunction(img, db ?? defaultDb, [0, 0, 0]); + + return new Response(data, { + headers: { + "Content-Disposition": 'attachment; filename="img.mcfunction"', + "Content-Type": "text/plain", + "Access-Control-Allow-Origin": "*", + }, + }); + } catch (err) { + return new Response(err.message, { status: 500 }); + } + }); + + app.post("/v1/addon", async (ctx) => { + const { + img, + gridSize, + resolution, + axis, + mer, + normal, + frames = 1, + } = await ctx.req.json(); + + const pbr = mer || normal; + + try { + const data = await img2mcaddon( + img, + gridSize, + resolution, + axis, + pbr, + frames, + ); + + return new Response(data, { + headers: { + "Content-Disposition": 'attachment; filename="img.mcaddon"', + "Content-Type": "application/octet-stream", + "Access-Control-Allow-Origin": "*", + }, + }); + } catch (err) { + return new Response(err.message, { status: 500 }); + } + }); + + app.get("/db/:dbName", async (ctx) => { + const { dbName } = ctx.req.param(); + let db: string; + try { + db = await readFile(`db/${dbName}.json`, "utf-8"); + } catch (err) { + return new Response(err.message, { status: 404 }); + } + + try { + const palette = createPalette(JSON.parse(db)); + + return new Response(JSON.stringify(palette), { + headers: { + "Content-Type": "application/json", + }, + }); + } catch (err) { + return new Response(err.message, { status: 500 }); + } + }); + + app.get("/", (c) => + c.html( + ` + + + + + Image to Minecraft + + + +
+ + + `, + )); + + app.get( + "static/*", + serveStatic({ + root: "./", + rewriteRequestPath(path) { + return path.replace(/^\/static\//, "dist/"); + }, + getContent(path, c) { + return readFile(path, "utf-8"); + }, + }), + ); + + return app; +} diff --git a/server.tsx b/server.tsx deleted file mode 100644 index 2da3e78..0000000 --- a/server.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import type { PaletteSource } from "./src/types.ts"; -import { Hono } from "@hono/hono"; -import { readFile } from "node:fs/promises"; -import img2mcstructure, { createPalette } from "./src/mcstructure/mod.ts"; -import createFunction from "./src/mcfunction/mod.ts"; - -export default function main(defaultDb: PaletteSource) { - const app = new Hono(); - - app.post("/v1/structure", async (ctx) => { - const { img, axis, db } = await ctx.req.json(); - - try { - const data = await img2mcstructure( - img, - createPalette(db ?? defaultDb), - axis, - ); - - return new Response(data, { - headers: { - "Content-Disposition": 'attachment; filename="img.mcstructure"', - "Content-Type": "application/octet-stream", - "Access-Control-Allow-Origin": "*", - }, - }); - } catch (err) { - return new Response(err.message, { status: 500 }); - } - }); - - app.post("/v1/fill", async (ctx) => { - const { img, axis, db } = await ctx.req.json(); - - try { - const data = await createFunction(img, db ?? defaultDb, [0, 0, 0]); - - return new Response(data, { - headers: { - "Content-Disposition": 'attachment; filename="img.mcfunction"', - "Content-Type": "text/plain", - "Access-Control-Allow-Origin": "*", - }, - }); - } catch (err) { - return new Response(err.message, { status: 500 }); - } - }); - - app.get("/db/:dbName", async (ctx) => { - const { dbName } = ctx.req.param(); - let db: string; - try { - db = await readFile(`db/${dbName}.json`, "utf-8"); - } catch (err) { - return new Response(err.message, { status: 404 }); - } - - try { - const palette = createPalette(JSON.parse(db)); - - return new Response(JSON.stringify(palette), { - headers: { - "Content-Type": "application/json", - }, - }); - } catch (err) { - return new Response(err.message, { status: 500 }); - } - }); - - app.get("/style.css", async (ctx) => { - const style = await readFile("dist/style.css", "utf-8"); - - return new Response(style, { - headers: { - "Content-Type": "text/css", - }, - }); - }); - - app.get("/bundle.js", async (ctx) => { - const bundle = await readFile("dist/bundle.js", "utf-8"); - - return new Response(bundle, { - headers: { - "Content-Type": "application/javascript", - }, - }); - }); - - app.get("/", (c) => - c.html( - ` - - - - - Image to Minecraft - - - -
- - - `, - ), - ); - - return app; -} diff --git a/src/_constants.ts b/src/_constants.ts index a9fd6a3..0e8320d 100644 --- a/src/_constants.ts +++ b/src/_constants.ts @@ -8,7 +8,7 @@ export const BLOCK_VERSION = 18153475; /** * NBT tag data version. */ -export const NBT_DATA_VERSION = 3093; +export const NBT_DATA_VERSION = 3953; /** * Minecraft behavior block format version. @@ -42,4 +42,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 = process.env.DENO_DEPLOYMENT_ID !== undefined ? 16 : 256; +export const MAX_DEPTH = process.env.DENO_DEPLOYMENT_ID !== undefined + ? 16 + : 256; diff --git a/src/_decode.ts b/src/_decode.ts index 7ebe035..06e67d4 100644 --- a/src/_decode.ts +++ b/src/_decode.ts @@ -1,25 +1,21 @@ -import { GIF, Image, type Frame } from "imagescript"; +import { type Frame, GIF, Image } from "imagescript"; import { readFile } from "node:fs/promises"; import { MAX_HEIGHT, MAX_WIDTH } from "./_constants.ts"; -export type DecodedFrames = - | GIF - | Array; +export type DecodedFrames = GIF | Array; /** * Decode an image from a URL * @param imgSrc Image URL * @returns Array of decoded frames */ -async function decodeUrl( - { href }: URL, -): Promise { +async function decodeUrl({ href }: URL): Promise { const res = await fetch(href); const data = new Uint8Array(await res.arrayBuffer()); return !href.endsWith(".gif") ? [await Image.decode(data)] - : [...(await GIF.decode(data, false))] as GIF; + : ([...(await GIF.decode(data, false))] as GIF); } /** @@ -29,11 +25,11 @@ async function decodeUrl( */ async function decodeImageFile( path: string, - data: Uint8Array + data: Uint8Array, ): Promise { return !path.endsWith(".gif") ? [await Image.decode(data)] - : [...(await GIF.decode(data, false))] as GIF; + : ([...(await GIF.decode(data, false))] as GIF); } /** @@ -41,19 +37,16 @@ async function decodeImageFile( * @param base64 Base64 string * @returns Array of decoded frames */ -async function decodeBase64( - base64: string, -): Promise { +async function decodeBase64(base64: string): Promise { const data = new Uint8Array( - atob(base64.replace( - /^data:image\/(png|jpeg|gif);base64,/, - "", - )).split("").map((x) => x.charCodeAt(0)), + atob(base64.replace(/^data:image\/(png|jpeg|gif);base64,/, "")) + .split("") + .map((x) => x.charCodeAt(0)), ); return !base64.startsWith("data:image/gif") ? [await Image.decode(data)] - : [...(await GIF.decode(data, false))] as GIF; + : ([...(await GIF.decode(data, false))] as GIF); } /** @@ -86,13 +79,14 @@ export default async function decode( } // Resize every frame above the max width/height - const frames = img?.map((i: Image | Frame) => - i.height > MAX_HEIGHT - ? i.resize(Image.RESIZE_AUTO, MAX_HEIGHT) - : i.width > MAX_WIDTH - ? i.resize(MAX_WIDTH, Image.RESIZE_AUTO) - : i - ) ?? []; + const frames = + img?.map((i: Image | Frame) => + i.height > MAX_HEIGHT + ? i.resize(Image.RESIZE_AUTO, MAX_HEIGHT) + : i.width > MAX_WIDTH + ? i.resize(MAX_WIDTH, Image.RESIZE_AUTO) + : i + ) ?? []; return frames satisfies DecodedFrames; } diff --git a/src/_lib.ts b/src/_lib.ts index bbac101..40e349b 100644 --- a/src/_lib.ts +++ b/src/_lib.ts @@ -1,12 +1,13 @@ -import type { IBlock, RGB, PaletteSource } from "./types.ts"; +import type { IBlock, PaletteSource, RGB } from "./types.ts"; import { readFile } from "node:fs/promises"; export function compareStates( a: Record, b: Record, ) { - return (Object.keys(a).length === Object.keys(b).length) && - Object.entries(a).sort().toString() === - Object.entries(b).sort().toString(); + return ( + Object.keys(a).length === Object.keys(b).length && + Object.entries(a).sort().toString() === Object.entries(b).sort().toString() + ); } /** @@ -17,7 +18,8 @@ export function compareStates( */ export function colorDistance(color1: RGB, color2: RGB) { return Math.sqrt( - (color1[0] - color2[0]) ** 2 + (color1[1] - color2[1]) ** 2 + + (color1[0] - color2[0]) ** 2 + + (color1[1] - color2[1]) ** 2 + (color1[2] - color2[2]) ** 2, ); } @@ -28,26 +30,22 @@ export function colorDistance(color1: RGB, color2: RGB) { * @param palette Array of blocks to compare against * @returns The block which is closest to the given color */ -export function getNearestColor( - color: RGB, - palette: IBlock[], -): IBlock { +export function getNearestColor(color: RGB, palette: IBlock[]): IBlock { // https://gist.github.com/Ademking/560d541e87043bfff0eb8470d3ef4894?permalink_comment_id=3720151#gistcomment-3720151 return palette.reduce( (prev: [number, IBlock], curr: IBlock): [number, IBlock] => { const distance = colorDistance(color, curr.color.slice(0, 3) as RGB); - return (distance < prev[0]) ? [distance, curr] : prev; + return distance < prev[0] ? [distance, curr] : prev; }, [Number.POSITIVE_INFINITY, palette[0]] as [number, IBlock], )[1]; } -export async function parseDbInput( - db: string, -): Promise { +export async function parseDbInput(db: string): Promise { if ( - db.startsWith("http://") || db.startsWith("https://") || + db.startsWith("http://") || + db.startsWith("https://") || db.startsWith("file://") ) { const res = await fetch(db); @@ -62,7 +60,9 @@ export function hex2rgb(hex: string): RGB { } export function rgb2hex(rgb: RGB): string { - return `#${rgb[0].toString(16).padStart(2, "0")}${rgb[1].toString(16).padStart(2, "0")}${rgb[2].toString(16).padStart(2, "0")}`; + return `#${rgb[0].toString(16).padStart(2, "0")}${ + rgb[1].toString(16).padStart(2, "0") + }${rgb[2].toString(16).padStart(2, "0")}`; } export function uint8arrayToBase64(arr: Uint8Array): string { @@ -71,4 +71,4 @@ export function uint8arrayToBase64(arr: Uint8Array): string { export function base642uint8array(str: string): Uint8Array { return new TextEncoder().encode(atob(str)); -} \ No newline at end of file +} diff --git a/src/_palette.ts b/src/_palette.ts index 6714b97..bc99127 100644 --- a/src/_palette.ts +++ b/src/_palette.ts @@ -8,9 +8,7 @@ import { hex2rgb } from "./_lib.ts"; * @param db Block ID/Color database. * @returns Array of blocks. */ -export default function createPalette( - db: PaletteSource, -): IBlock[] { +export default function createPalette(db: PaletteSource): IBlock[] { const blockPalette: IBlock[] = []; for (const idx in db) { @@ -28,8 +26,7 @@ export default function createPalette( blockPalette.push({ id, hexColor, - color: color ?? - (hexColor ? hex2rgb(hexColor) : [0, 0, 0, 0] as RGBA), + color: color ?? (hexColor ? hex2rgb(hexColor) : ([0, 0, 0, 0] as RGBA)), states, version, }); diff --git a/src/_rotate.ts b/src/_rotate.ts index 81e1420..47bdebd 100644 --- a/src/_rotate.ts +++ b/src/_rotate.ts @@ -1,7 +1,12 @@ import type { Axis, IMcStructure } from "./types.ts"; function rotateOverY(structure: IMcStructure): IMcStructure { - const { size, structure: { block_indices: [layer] } } = structure; + const { + size, + structure: { + block_indices: [layer], + }, + } = structure; const [width, height, depth] = size; const newLayer = Array.from({ length: width * height * depth }, () => -1); @@ -9,13 +14,10 @@ function rotateOverY(structure: IMcStructure): IMcStructure { for (let z = 0; z < depth; z++) { for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - const key = (z * width * height) + (y * width) + (width - x - 1); + const key = z * width * height + y * width + (width - x - 1); - newLayer[key] = layer[ - (z * width * height) + ((height - y - 1) * - width) + - x - ]; + newLayer[key] = + layer[z * width * height + (height - y - 1) * width + x]; } } } @@ -28,7 +30,12 @@ function rotateOverY(structure: IMcStructure): IMcStructure { } function rotateOverZ(structure: IMcStructure): IMcStructure { - const { size, structure: { block_indices: [layer] } } = structure; + const { + size, + structure: { + block_indices: [layer], + }, + } = structure; const [width, height, depth] = size; const newLayer = Array.from({ length: width * height * depth }, () => -1); @@ -36,12 +43,9 @@ function rotateOverZ(structure: IMcStructure): IMcStructure { for (let z = 0; z < depth; z++) { for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - const key = (z * width * height) + (y * width) + (width - x - 1); + const key = z * width * height + y * width + (width - x - 1); - newLayer[key] = layer[ - ((depth - z - 1) * width * height) + - (y * width) + x - ]; + newLayer[key] = layer[(depth - z - 1) * width * height + y * width + x]; } } } @@ -54,7 +58,12 @@ function rotateOverZ(structure: IMcStructure): IMcStructure { } function rotateOverX(structure: IMcStructure): IMcStructure { - const { size, structure: { block_indices: [layer] } } = structure; + const { + size, + structure: { + block_indices: [layer], + }, + } = structure; const [width, height, depth] = size; const newLayer = Array.from({ length: width * height * depth }, () => -1); @@ -62,9 +71,9 @@ function rotateOverX(structure: IMcStructure): IMcStructure { for (let z = 0; z < depth; z++) { for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - const key = (z * width * height) + (y * width) + (width - x - 1); + const key = z * width * height + y * width + (width - x - 1); - newLayer[key] = layer[(z * width * height) + (y * width) + x]; + newLayer[key] = layer[z * width * height + y * width + x]; } } } diff --git a/src/atlas.ts b/src/atlas.ts new file mode 100644 index 0000000..d9dd050 --- /dev/null +++ b/src/atlas.ts @@ -0,0 +1,60 @@ +import * as imagescript from "imagescript"; +import { readdir, readFile } from "node:fs/promises"; +import { join } from "node:path"; + +/** + * Create an image series from a list of image sources + * @param srcs Image sources + * @returns Image series + */ +export async function createImageSeries( + srcs: string[], +): Promise { + const images = await Promise.all( + srcs.map(async (src) => { + if (src.startsWith("http")) { + const url = new URL(src); + const res = await fetch(url); + const data = new Uint8Array(await res.arrayBuffer()); + return imagescript.Image.decode(data); + } + + const data = await readFile(src); + return imagescript.Image.decode(data); + }), + ); + + return images; +} + +/** + * Use the contents of a directory as an image series + * @param dir Directory which contains the image series + * @returns Image series + */ +export async function dir2series(dir: string): Promise { + const files = await readdir(dir); + const images = await createImageSeries(files.map((file) => join(dir, file))); + + return images; +} + +/** + * Convert an image series to a flipbook texture atlas + * @param images Image sequence + * @returns Texture atlas of the image sequence + */ +export async function series2atlas( + images: imagescript.Image[], +): Promise { + const atlas = new imagescript.Image( + images[0].width, + images[0].height * images.length, + ); + + images.forEach((image, i) => { + atlas.composite(image, 0, i * image.height); + }); + + return atlas; +} diff --git a/src/mcaddon/cli.ts b/src/mcaddon/cli.ts index be07c5a..12df972 100644 --- a/src/mcaddon/cli.ts +++ b/src/mcaddon/cli.ts @@ -6,67 +6,81 @@ import img2mcaddon from "./mod.ts"; import type { Axis } from "../types.ts"; async function createAddon( - src: string | URL, - gridSize = 3, - resolution = 16, - dest?: string, - axis?: Axis, - pbr = false + src: string | URL, + gridSize = 3, + resolution = 16, + dest?: string, + axis?: Axis, + pbr = false, + frames = 1, ) { - const addon = await img2mcaddon(src, gridSize, resolution, axis ?? "z", pbr); + const addon = await img2mcaddon( + src, + gridSize, + resolution, + axis ?? "z", + pbr, + frames, + ); - const addonDest = - dest ?? - `${basename(src instanceof URL ? src.pathname : src).replace(/\.\w+$/, "")}.mcaddon`; + const addonDest = dest ?? + `${ + basename(src instanceof URL ? src.pathname : src).replace(/\.\w+$/, "") + }.mcaddon`; - await writeFile(addonDest, addon); + await writeFile(addonDest, addon); - console.log(`Created ${addonDest}`); + console.log(`Created ${addonDest}`); } if (import.meta.main) { - const { - values: { src, gridSize, resolution, dest, axis, pbr }, - } = parseArgs({ - args: process.argv.slice(2), - options: { - src: { - type: "string", - multiple: false, - }, - gridSize: { - type: "string", - multiple: false, - default: "3", - }, - resolution: { - type: "string", - multiple: false, - default: "16", - }, - dest: { - type: "string", - multiple: false, - }, - axis: { - type: "string", - multiple: false, - default: "z", - }, - pbr: { - type: "boolean", - default: false, - } - }, - }); + const { + values: { src, gridSize, resolution, dest, axis, pbr, frames }, + } = parseArgs({ + args: process.argv.slice(2), + options: { + src: { + type: "string", + multiple: false, + }, + gridSize: { + type: "string", + multiple: false, + default: "3", + }, + resolution: { + type: "string", + multiple: false, + default: "16", + }, + dest: { + type: "string", + multiple: false, + }, + axis: { + type: "string", + multiple: false, + default: "z", + }, + pbr: { + type: "boolean", + default: false, + }, + frames: { + type: "string", + default: "1", + }, + }, + }); - await createAddon( - src, - Math.max(1, Number(gridSize)), - Math.max(16, Math.min(1024, Number(resolution))), - dest, - axis as Axis, - pbr - ); - process.exit(0); + await createAddon( + src, + Math.max(1, Number(gridSize)), + Math.max(16, Math.min(1024, Number(resolution))), + dest, + axis as Axis, + pbr, + Number(frames), + ); + process.exit(0); } diff --git a/src/mcaddon/mod.ts b/src/mcaddon/mod.ts index e561d3f..1ddef36 100644 --- a/src/mcaddon/mod.ts +++ b/src/mcaddon/mod.ts @@ -1,132 +1,446 @@ -import type { RGB, IMcStructure, StructurePalette, Axis } from "../types.ts"; +import type { Axis, IMcStructure, RGB, StructurePalette } from "../types.ts"; import { basename, extname } from "node:path"; import JSZip from "jszip"; import * as imagescript from "imagescript"; import type { DecodedFrames } from "../_decode.ts"; import decode from "../_decode.ts"; -import { BLOCK_VERSION, BLOCK_FORMAT_VERSION } from "../_constants.ts"; +import { BLOCK_FORMAT_VERSION, BLOCK_VERSION } from "../_constants.ts"; import { rgb2hex } from "../_lib.ts"; import * as nbt from "nbtify"; import { nanoid } from "nanoid"; +import { dir2series, series2atlas } from "../atlas.ts"; function getAverageColor(image: imagescript.Image): string { - return rgb2hex(imagescript.Image.colorToRGB(image.averageColor()) as RGB); + return rgb2hex(imagescript.Image.colorToRGB(image.averageColor()) as RGB); } function createBlock({ - namespace, - image, - gridSize, - x, - y, - z = 1, + namespace, + image, + gridSize, + x, + y, + z = 1, + axis = "z", }: { - namespace: string; - image: imagescript.Image; - gridSize: number; - x: number; - y: number; - z: number; + namespace: string; + image: imagescript.Image; + gridSize: number; + x: number; + y: number; + z: number; + axis?: Axis; }): string { - const sliceId = { - front: `slice_${x}_${y}_${z}`, - back: `slice_${gridSize - x - 1}_${y}_${z}`, - top: `slice_${x}_${gridSize - y - 1}_${z}`, - bottom: `slice_${x}_${y}_${z}`, - }; - const data = { - format_version: BLOCK_FORMAT_VERSION, - "minecraft:block": { - description: { - identifier: `${namespace}:${sliceId.front}`, - // menu_category: { - // category: "construction", - // group: "itemGroup.name.concrete" - // }, - traits: {}, - }, - components: { - "minecraft:geometry": "minecraft:geometry.full_block", - "minecraft:map_color": getAverageColor(image), - "minecraft:material_instances": { - north: { - texture: `${namespace}_${sliceId.front}`, - render_method: "opaque", - ambient_occlusion: true, - face_dimming: true, - }, - south: { - texture: `${namespace}_${sliceId.back}`, - render_method: "opaque", - ambient_occlusion: true, - face_dimming: true, - }, - east: { - texture: `${namespace}_${sliceId.front}`, - render_method: "opaque", - ambient_occlusion: true, - face_dimming: true, - }, - west: { - texture: `${namespace}_${sliceId.back}`, - render_method: "opaque", - ambient_occlusion: true, - face_dimming: true, - }, - up: { - texture: `${namespace}_${sliceId.front}`, - render_method: "opaque", - ambient_occlusion: true, - face_dimming: true, - }, - down: { - texture: `${namespace}_${sliceId.bottom}`, - render_method: "opaque", - ambient_occlusion: true, - face_dimming: true, - }, - "*": { - texture: `${namespace}_${sliceId.front}`, - render_method: "opaque", - ambient_occlusion: true, - face_dimming: true, - }, - }, - }, - permutations: [], - }, - }; - - return JSON.stringify(data, null, 2); + const sliceId = { + front: `${namespace}_${x}_${y}_${z}`, + back: `${namespace}_${gridSize - x - 1}_${y}_${z}`, + top: `${namespace}_${x}_${gridSize - y - 1}_${z}`, + bottom: `${namespace}_${x}_${y}_${z}`, + }; + + if (axis === "y") { + sliceId.front = `${namespace}_${x}_${z}_${gridSize - y - 1}`; + sliceId.back = `${namespace}_${gridSize - x - 1}_${z}_${gridSize - y - 1}`; + sliceId.top = `${namespace}_${x}_${z}_${y}`; + sliceId.bottom = `${namespace}_${x}_${z}_${gridSize - y - 1}`; + } + + const data = { + format_version: BLOCK_FORMAT_VERSION, + "minecraft:block": { + description: { + identifier: `${namespace}:${sliceId.front}`, + // menu_category: { + // category: "construction", + // group: "itemGroup.name.concrete" + // }, + traits: {}, + }, + components: { + "minecraft:geometry": "minecraft:geometry.full_block", + "minecraft:map_color": getAverageColor(image), + "minecraft:material_instances": { + north: { + texture: `${namespace}_${sliceId.front}`, + render_method: "opaque", + ambient_occlusion: true, + face_dimming: true, + }, + south: { + texture: `${namespace}_${sliceId.back}`, + render_method: "opaque", + ambient_occlusion: true, + face_dimming: true, + }, + east: { + texture: `${namespace}_${sliceId.front}`, + render_method: "opaque", + ambient_occlusion: true, + face_dimming: true, + }, + west: { + texture: `${namespace}_${sliceId.back}`, + render_method: "opaque", + ambient_occlusion: true, + face_dimming: true, + }, + up: { + texture: `${namespace}_${sliceId.front}`, + render_method: "opaque", + ambient_occlusion: true, + face_dimming: true, + }, + down: { + texture: `${namespace}_${sliceId.bottom}`, + render_method: "opaque", + ambient_occlusion: true, + face_dimming: true, + }, + "*": { + texture: `${namespace}_${sliceId.front}`, + render_method: "opaque", + ambient_occlusion: true, + face_dimming: true, + }, + }, + }, + permutations: [], + }, + }; + + 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))); - - const depth = volume.length; - const gridSize = volume[0].length; - - for (let z = 0; z < depth; z++) { - for (let x = 0; x < gridSize; x++) { - for (let y = 0; y < gridSize; y++) { - const blockIdx = volume[z][x][y]; - if (axis === "y") { - rotatedVolume[z][y][x] = blockIdx; - continue; - } - - if (axis === "x") { - rotatedVolume[x][z][y] = blockIdx; - continue; - } - - rotatedVolume[z][x][y] = blockIdx; - } - } - } - - return rotatedVolume; + const rotatedVolume = volume.map((z) => z.map((y) => y.map(() => -1))); + + const depth = volume.length; + const gridSize = volume[0].length; + + for (let z = 0; z < depth; z++) { + for (let x = 0; x < gridSize; x++) { + for (let y = 0; y < gridSize; y++) { + const blockIdx = volume[z][x][y]; + if (axis === "y") { + rotatedVolume[z][y][x] = blockIdx; + continue; + } + + if (axis === "x") { + rotatedVolume[x][z][y] = blockIdx; + continue; + } + + rotatedVolume[z][x][y] = blockIdx; + } + } + } + + return rotatedVolume; +} + +async function iterateDepth({ + namespace, + addon, + terrainData, + blocksData, + blockPalette, + volume, + merTexture, + normalTexture, + frames, + gridSize, + cropSize, + depth, + pbr, +}: { + namespace: string; + addon: JSZip; + terrainData: Record; + blocksData: Record; + blockPalette: StructurePalette; + volume: number[][][]; + merTexture: DecodedFrames; + normalTexture: DecodedFrames; + frames: DecodedFrames; + gridSize: number; + cropSize: number; + depth: number; + pbr: boolean; +}) { + for (let z = 0; z < depth; z++) { + const resizeTo = gridSize * cropSize; + const frame: imagescript.Image = ( + frames[z].clone() as imagescript.Image + ).resize(resizeTo, resizeTo); + + for (let x = 0; x < gridSize; x++) { + for (let y = 0; y < gridSize; y++) { + const sliceId = `${namespace}_${x}_${y}_${z}`; + const xPos = x * cropSize; + const yPos = y * cropSize; + const slice = frame.clone().crop(xPos, yPos, cropSize, cropSize); + + addon.file( + `bp/blocks/${sliceId}.block.json`, + createBlock({ + namespace, + image: slice, + gridSize, + x, + y, + z, + }), + ); + + addon.file(`rp/textures/blocks/${sliceId}.png`, await slice.encode()); + + const textureSet: { + color: string; + metalness_emissive_roughness?: string; + normal?: string; + } = { + color: sliceId, + }; + + 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( + `rp/textures/blocks/${sliceId}.texture_set.json`, + JSON.stringify( + { + format_version: "1.16.100", + "minecraft:texture_set": textureSet, + }, + null, + 2, + ), + ); + + terrainData[sliceId] = { + textures: `textures/blocks/${sliceId}`, + }; + + const blockIdx = blockPalette.push({ + name: `${namespace}:${sliceId}`, + states: {}, + version: BLOCK_VERSION, + }) - 1; + + volume[z][x][y] = blockIdx; + + blocksData[sliceId] = { + sound: "stone", + isotropic: false, + }; + } + } + } + + addon.file( + "rp/blocks.json", + JSON.stringify( + { + format_version: [1, 0, 0], + ...blocksData, + }, + null, + 2, + ), + ); +} + +async function createFlipbook({ + namespace, + addon, + terrainData, + blocksData, + blockPalette, + volume, + merTexture, + normalTexture, + frames, + gridSize, + cropSize, + pbr, + axis, +}: { + namespace: string; + addon: JSZip; + terrainData: Record; + blocksData: Record; + blockPalette: StructurePalette; + volume: number[][][]; + merTexture: DecodedFrames; + normalTexture: DecodedFrames; + frames: DecodedFrames; + gridSize: number; + cropSize: number; + pbr: boolean; + axis: Axis; +}) { + // Each frame is added to the flipbook atlas + const flipbookTextures: Array<{ + atlas_tile: string; + flipbook_texture: string; + ticks_per_frame: number; + }> = []; + + const tickSpeed = 10; + + for (let x = 0; x < gridSize; x++) { + for (let y = 0; y < gridSize; y++) { + const sliceId = `${namespace}_${x}_${y}_1`; + + const slice = await series2atlas( + frames.map((frame: imagescript.Image) => + frame.clone().crop(x * cropSize, y * cropSize, cropSize, cropSize) + ), + ); + + addon.file( + `bp/blocks/${sliceId}.block.json`, + createBlock({ + namespace, + image: slice, + gridSize, + x, + y, + z: 1, + axis, + }), + ); + + addon.file(`rp/textures/blocks/${sliceId}.png`, await slice.encode()); + + const textureSet: { + color: string; + metalness_emissive_roughness?: string; + normal?: string; + } = { + color: sliceId, + }; + + if (pbr) { + try { + addon.file( + `rp/textures/blocks/${sliceId}_mer.png`, + await ( + await series2atlas( + merTexture.map((frame: imagescript.Image) => + frame + .clone() + .crop(x * cropSize, y * cropSize, 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 ( + await series2atlas( + normalTexture.map((frame: imagescript.Image) => + frame + .clone() + .crop(x * cropSize, y * cropSize, cropSize, cropSize) + ), + ) + ).encode(), + ); + textureSet.normal = `${sliceId}_normal`; + } catch (err) { + console.warn(`Failed to add normal map: ${err}`); + } + } + + addon.file( + `rp/textures/blocks/${sliceId}.texture_set.json`, + JSON.stringify( + { + format_version: "1.16.100", + "minecraft:texture_set": textureSet, + }, + null, + 2, + ), + ); + + terrainData[`${namespace}_${sliceId}`] = { + textures: `textures/blocks/${sliceId}`, + }; + + const blockIdx = blockPalette.push({ + name: `${namespace}:${sliceId}`, + states: {}, + version: BLOCK_VERSION, + }) - 1; + + volume[0][x][y] = blockIdx; + + blocksData[sliceId] = { + sound: "stone", + isotropic: false, + }; + + flipbookTextures.push({ + atlas_tile: `${namespace}_${sliceId}`, + flipbook_texture: `textures/blocks/${sliceId}`, + ticks_per_frame: tickSpeed, + }); + } + } + + addon.file( + "rp/textures/flipbook_textures.json", + JSON.stringify(flipbookTextures, null, 2), + ); + + addon.file( + "rp/blocks.json", + JSON.stringify( + { + format_version: [1, 0, 0], + ...blocksData, + }, + null, + 2, + ), + ); } /** @@ -136,329 +450,279 @@ function rotateVolume(volume: number[][][], axis: Axis): number[][][] { * @param resolution The target resolution of the block texture output * @param axis The axis to rotate the structure on * @param pbr Enable PBR textures + * @param frames Number of frames to use for the flipbook. If greater than 1, the image will be converted to a flipbook. Otherwise, multiple frames are used for the depth of the structure. * @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); * ``` + * @todo Flipbook features will be separated into a different function */ export default async function img2mcaddon( - src: string | URL, - gridSize: number, - resolution: number, - axis: Axis = "z", - pbr = false, + src: string | URL, + gridSize: number, + resolution: number, + axis: Axis = "z", + pbr = false, + frames = 1, ): Promise { - const jobId = nanoid(7); - const addon = new JSZip(); - - const colorSrc = src instanceof URL ? src.href : src; - - const blocksData: Record< - string, - { - sound: string; - isotropic: boolean; - } - > = {}; - - const frames = await decode(colorSrc, false); - - // Convert slices to blocks, textures, and construct the structure, then assemble the addon - const baseName = basename(colorSrc, extname(colorSrc)); - - let merTexture: DecodedFrames = [ - new imagescript.Image(gridSize * resolution, gridSize * resolution).fill( - imagescript.Image.rgbToColor(0, 0, 255), - ), - ]; - let normalTexture: DecodedFrames = [ - new imagescript.Image(gridSize * resolution, gridSize * resolution).fill( - imagescript.Image.rgbToColor(127, 127, 255), - ), - ]; - - try { - const merSrc = `${colorSrc.replace(/\.[^.]+$/gi, "_mer.png")}`; - merTexture = await decode(merSrc, false); - } catch (err) { - console.warn(`Failed to decode MER map: ${err}`); - } - - try { - normalTexture = await decode( - `${colorSrc.replace(/\.[^.]+$/gi, "_normal.png")}`, - false, - ); - } catch (err) { - console.warn(`Failed to decode normal map: ${err}`); - } - - const namespace = baseName.replace(/\W|\.\@\$\%/g, "_").substring(0, 16); - - const terrainData: Record< - string, - { - textures: string; - } - > = {}; - - const blockPalette: StructurePalette = []; - - const depth = frames.length; - - const layer = Array.from({ length: gridSize * gridSize * depth }, () => -1); - const waterLayer = layer.slice(); - const volume: number[][][] = Array.from({ length: depth }, () => - Array.from({ length: gridSize }, () => Array(gridSize).fill(-1)), - ); - - const cropSize = Math.min(resolution, Math.round(frames[0].width / gridSize)); - - for (let z = 0; z < depth; z++) { - const resizeTo = gridSize * cropSize; - const frame: imagescript.Image = ( - frames[z].clone() as imagescript.Image - ).resize(resizeTo, resizeTo); - - for (let x = 0; x < gridSize; x++) { - for (let y = 0; y < gridSize; y++) { - const sliceId = `${namespace}_${x}_${y}_${z}`; - const xPos = x * cropSize; - const yPos = y * cropSize; - const slice = frame.clone().crop(xPos, yPos, cropSize, cropSize); - - addon.file( - `bp/blocks/${sliceId}.block.json`, - createBlock({ - namespace, - image: slice, - gridSize, - x, - y, - z, - }), - ); - - addon.file(`rp/textures/blocks/${sliceId}.png`, await slice.encode()); - - const textureSet: { - color: string; - metalness_emissive_roughness?: string; - normal?: string; - } = { - color: sliceId, - }; - - 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( - `rp/textures/blocks/${sliceId}.texture_set.json`, - JSON.stringify( - { - format_version: "1.16.100", - "minecraft:texture_set": textureSet, - }, - null, - 2, - ), - ); - - terrainData[sliceId] = { - textures: `textures/blocks/${sliceId}`, - }; - - const blockIdx = - blockPalette.push({ - name: `${namespace}:${sliceId}`, - states: {}, - version: BLOCK_VERSION, - }) - 1; - - volume[z][x][y] = blockIdx; - - blocksData[sliceId] = { - sound: "stone", - isotropic: false, - }; - } - } - } - - const rotatedVolume = rotateVolume(volume, axis); - const size: [number, number, number] = - axis === "y" ? [gridSize, depth, gridSize] : [gridSize, gridSize, depth]; - - const flatVolume = rotatedVolume.flat(2); - - if (flatVolume.length !== waterLayer.length) { - throw new Error("Layer lengths do not match"); - } - - addon.file( - "rp/blocks.json", - JSON.stringify( - { - format_version: [1, 0, 0], - ...blocksData, - }, - null, - 2, - ), - ); - - const tag: IMcStructure = { - format_version: 1, - size, - structure: { - block_indices: [flatVolume, waterLayer], - entities: [], - palette: { - default: { - block_palette: blockPalette.reverse(), - block_position_data: {}, - }, - }, - }, - structure_world_origin: [0, 0, 0], - }; - - const mcstructure = await nbt.write(nbt.parse(JSON.stringify(tag)), { - endian: "little", - compression: null, - bedrockLevel: false, - }); - - addon.file(`bp/structures/mosaic/${namespace}.mcstructure`, mcstructure); - - const mipLevels = - { - 256: 0, - 128: 1, - 64: 2, - 32: 3, - 16: 4, - }[resolution] ?? 0; - - const terrainTextureJson = JSON.stringify( - { - resource_pack_name: namespace.toLowerCase(), - texture_name: "atlas.terrain", - padding: mipLevels / 2, - num_mip_levels: mipLevels, - texture_data: terrainData, - }, - null, - 2, - ); - - addon.file("rp/textures/terrain_texture.json", terrainTextureJson); - - const icon = frames[0].clone().resize(150, 150).encode(); - addon.file("rp/pack_icon.png", icon); - addon.file("bp/pack_icon.png", icon); - - const rpUuid = crypto.randomUUID(); - const rpModUuid = crypto.randomUUID(); - const bpUuid = crypto.randomUUID(); - const bpModUuid = crypto.randomUUID(); - const bpVersion = [1, 0, 0]; - const rpVersion = [1, 0, 0]; - const minEngineVersion = [1, 21, 2]; - - addon.file( - "rp/manifest.json", - JSON.stringify( - { - format_version: 2, - header: { - name: `Mosaic Resources: "${baseName}"`, - description: `A mosaic made from an image\n(${jobId})`, - uuid: rpUuid, - version: rpVersion, - min_engine_version: minEngineVersion, - }, - modules: [ - { - description: "Mosaic block textures", - type: "resources", - uuid: rpModUuid, - version: rpVersion, - }, - ], - dependencies: [ - { - uuid: bpUuid, - version: bpVersion, - }, - ], - ...(pbr ? { capabilities: ["raytraced", "pbr"] } : {}), - }, - null, - 2, - ), - ); - - addon.file( - "bp/manifest.json", - JSON.stringify( - { - format_version: 2, - header: { - name: `Mosaic Blocks: "${baseName}"`, - description: `A mosaic made from an image\n(${jobId})`, - uuid: bpUuid, - version: bpVersion, - min_engine_version: minEngineVersion, - }, - modules: [ - { - description: "Mosaic blocks slices", - type: "data", - uuid: bpModUuid, - version: bpVersion, - }, - ], - dependencies: [ - { - uuid: rpUuid, - version: rpVersion, - }, - ], - }, - null, - 2, - ), - ); - - return await addon.generateAsync({ type: "uint8array" }); + const jobId = nanoid(7); + const addon = new JSZip(); + + const colorSrc = src instanceof URL ? src.href : src; + + const blocksData: Record< + string, + { + sound: string; + isotropic: boolean; + } + > = {}; + + const decodedFrames = frames > 1 + ? await dir2series(colorSrc) + : await decode(colorSrc, false); + + // Convert slices to blocks, textures, and construct the structure, then assemble the addon + const baseName = basename(colorSrc, extname(colorSrc)); + + let merTexture: DecodedFrames = [ + new imagescript.Image(gridSize * resolution, gridSize * resolution).fill( + imagescript.Image.rgbToColor(0, 0, 255), + ), + ]; + let normalTexture: DecodedFrames = [ + new imagescript.Image(gridSize * resolution, gridSize * resolution).fill( + imagescript.Image.rgbToColor(127, 127, 255), + ), + ]; + + try { + if (frames > 1) { + const merSrc = `${colorSrc}_mer`; + merTexture = await dir2series(merSrc); + } else { + const merSrc = `${colorSrc.replace(/\.[^.]+$/gi, "_mer.png")}`; + merTexture = await decode(merSrc, false); + } + } catch (err) { + console.warn(`Failed to decode MER map: ${err}`); + } + + try { + if (frames > 1) { + const normalSrc = `${colorSrc}_normal`; + normalTexture = await dir2series(normalSrc); + } else { + normalTexture = await decode( + `${colorSrc.replace(/\.[^.]+$/gi, "_normal.png")}`, + false, + ); + } + } catch (err) { + console.warn(`Failed to decode normal map: ${err}`); + } + + const namespace = baseName.replace(/\W|\.\@\$\%/g, "_").substring(0, 16); + + const terrainData: Record< + string, + { + textures: string; + } + > = {}; + + const blockPalette: StructurePalette = []; + + const depth = frames > 1 ? 1 : decodedFrames.length; + + const volume: number[][][] = Array.from( + { length: depth }, + () => Array.from({ length: gridSize }, () => Array(gridSize).fill(-1)), + ); + + const cropSize = Math.min( + resolution, + Math.round(decodedFrames[0].width / gridSize), + ); + + let flipbookVolume: number[][][] | undefined; + + if (frames > 1) { + flipbookVolume = Array.from( + { length: 1 }, + () => Array.from({ length: gridSize }, () => Array(gridSize).fill(-1)), + ); + + await createFlipbook({ + namespace, + addon, + terrainData, + blocksData, + blockPalette, + volume: flipbookVolume, + merTexture, + normalTexture, + frames: decodedFrames, + gridSize, + cropSize, + pbr, + axis, + }); + } else { + await iterateDepth({ + namespace, + addon, + terrainData, + blocksData, + blockPalette, + volume, + merTexture, + normalTexture, + frames: decodedFrames, + gridSize, + cropSize, + depth, + pbr, + }); + } + + const rotatedVolume = rotateVolume(flipbookVolume ?? volume, axis); + const size: [number, number, number] = axis === "y" + ? [gridSize, depth, gridSize] + : [gridSize, gridSize, depth]; + + const flatVolume = rotatedVolume.flat(2); + const waterLayer = Array.from({ length: flatVolume.length }, () => -1); + + if (flatVolume.length !== waterLayer.length) { + throw new Error("Layer lengths do not match"); + } + + const tag: IMcStructure = { + format_version: 1, + size, + structure: { + block_indices: [flatVolume, waterLayer], + entities: [], + palette: { + default: { + block_palette: blockPalette.reverse(), + block_position_data: {}, + }, + }, + }, + structure_world_origin: [0, 0, 0], + }; + + const mcstructure = await nbt.write(nbt.parse(JSON.stringify(tag)), { + name: `${namespace}_${jobId}`, + endian: "little", + compression: null, + bedrockLevel: false, + }); + + addon.file(`bp/structures/mosaic/${namespace}.mcstructure`, mcstructure); + + const mipLevels = { + 256: 0, + 128: 1, + 64: 2, + 32: 3, + 16: 4, + }[resolution] ?? 0; + + const terrainTextureJson = JSON.stringify( + { + resource_pack_name: namespace.toLowerCase(), + texture_name: "atlas.terrain", + padding: mipLevels / 2, + num_mip_levels: mipLevels, + texture_data: terrainData, + }, + null, + 2, + ); + + addon.file("rp/textures/terrain_texture.json", terrainTextureJson); + + const icon = decodedFrames[0].clone().resize(150, 150).encode(); + addon.file("rp/pack_icon.png", icon); + addon.file("bp/pack_icon.png", icon); + + const rpUuid = crypto.randomUUID(); + const rpModUuid = crypto.randomUUID(); + const bpUuid = crypto.randomUUID(); + const bpModUuid = crypto.randomUUID(); + const bpVersion = [1, 0, 0]; + const rpVersion = [1, 0, 0]; + const minEngineVersion = [1, 21, 2]; + + addon.file( + "rp/manifest.json", + JSON.stringify( + { + format_version: 2, + header: { + name: `Mosaic Resources: "${baseName}"`, + description: `A mosaic made from an image\n(${jobId})`, + uuid: rpUuid, + version: rpVersion, + min_engine_version: minEngineVersion, + }, + modules: [ + { + description: "Mosaic block textures", + type: "resources", + uuid: rpModUuid, + version: rpVersion, + }, + ], + dependencies: [ + { + uuid: bpUuid, + version: bpVersion, + }, + ], + ...(pbr ? { capabilities: ["raytraced", "pbr"] } : {}), + }, + null, + 2, + ), + ); + + addon.file( + "bp/manifest.json", + JSON.stringify( + { + format_version: 2, + header: { + name: `Mosaic Blocks: "${baseName}"`, + description: `A mosaic made from an image\n(${jobId})`, + uuid: bpUuid, + version: bpVersion, + min_engine_version: minEngineVersion, + }, + modules: [ + { + description: "Mosaic blocks slices", + type: "data", + uuid: bpModUuid, + version: bpVersion, + }, + ], + dependencies: [ + { + uuid: rpUuid, + version: rpVersion, + }, + ], + }, + null, + 2, + ), + ); + + return await addon.generateAsync({ type: "uint8array" }); } diff --git a/src/mcfunction/mod.ts b/src/mcfunction/mod.ts index 0b3f37a..34edb5a 100644 --- a/src/mcfunction/mod.ts +++ b/src/mcfunction/mod.ts @@ -34,9 +34,9 @@ export default async function img2mcfunction( const nearest = getNearestColor([r, g, b], createPalette(blocks)); lines.push( - `setblock ~${Number(x + offset[0])}~${Math.abs(img.height - y + offset[1])}~${ - offset[2] - } ${nearest.id} replace`, + `setblock ~${Number(x + offset[0])}~${ + Math.abs(img.height - y + offset[1]) + }~${offset[2]} ${nearest.id} replace`, ); } } diff --git a/src/mcstructure/cli.ts b/src/mcstructure/cli.ts index c06ebd1..3465927 100644 --- a/src/mcstructure/cli.ts +++ b/src/mcstructure/cli.ts @@ -1,6 +1,6 @@ import type { Axis, PaletteSource } from "../types.ts"; import { basename, extname, join } from "node:path"; -import { watch, writeFile} from "node:fs/promises"; +import { watch, writeFile } from "node:fs/promises"; import process from "node:process"; import { parseArgs } from "node:util"; import img2mcstructure, { createPalette } from "../mcstructure/mod.ts"; @@ -19,20 +19,15 @@ export default async function main( `${basename(src, extname(src))}_${axis}_${Date.now()}.mcstructure`, ); - await writeFile( - structureDest, - await img2mcstructure( - src, - palette, - axis, - ), - ); + await writeFile(structureDest, await img2mcstructure(src, palette, axis)); console.log(`Created ${structureDest}`); } if (import.meta.main) { - const { values: { axis, img, db, watch: watchFile, dest } } = parseArgs({ + const { + values: { axis, img, db, watch: watchFile, dest }, + } = parseArgs({ args: process.argv.slice(2), options: { axis: { @@ -79,12 +74,7 @@ if (import.meta.main) { process.exit(0); } - await main( - img, - await parseDbInput(db), - (axis ?? "x") as Axis, - dest, - ); + await main(img, await parseDbInput(db), (axis ?? "x") as Axis, dest); process.exit(0); } diff --git a/src/mcstructure/mod.ts b/src/mcstructure/mod.ts index 93207c4..b5f0854 100644 --- a/src/mcstructure/mod.ts +++ b/src/mcstructure/mod.ts @@ -1,6 +1,6 @@ import type { Axis, IBlock, IMcStructure, StructurePalette } from "../types.ts"; -import * as nbt from "nbtify" -import * as imagescript from "imagescript" +import * as nbt from "nbtify"; +import * as imagescript from "imagescript"; import decode from "../_decode.ts"; import createPalette from "../_palette.ts"; import { @@ -57,8 +57,9 @@ function findBlock( blockPalette: StructurePalette, ): [Pick, number] { const nearest = convertBlock(c, palette); - const blockIdx = blockPalette.findIndex(({ name, states }) => - name === nearest.id && compareStates(nearest.states, states) + const blockIdx = blockPalette.findIndex( + ({ name, states }) => + name === nearest.id && compareStates(nearest.states, states), ); return [nearest, blockIdx]; @@ -118,13 +119,11 @@ export function constructDecoded( findBlock(c, palette, blockPalette); if (blockIdx === -1) { - blockIdx = blockPalette.push( - { - version: nearest.version ?? BLOCK_VERSION, - name: nearest.id ?? DEFAULT_BLOCK, - states: nearest.states ?? {}, - }, - ) - 1; + blockIdx = blockPalette.push({ + version: nearest.version ?? BLOCK_VERSION, + name: nearest.id ?? DEFAULT_BLOCK, + states: nearest.states ?? {}, + }) - 1; memo.set(c, [nearest, blockIdx]); } @@ -174,7 +173,7 @@ export async function createMcStructure( ); return await nbt.write(nbt.parse(structure), { - // name, + name, endian: "little", compression: null, bedrockLevel: false, diff --git a/src/mcworld/mod.ts b/src/mcworld/mod.ts new file mode 100644 index 0000000..9fc250a --- /dev/null +++ b/src/mcworld/mod.ts @@ -0,0 +1,82 @@ +import { NBT_DATA_VERSION } from "../_constants.ts"; +import type { PaletteSource } from "../types.ts"; + +export interface ISubChunk { + Y: number; + block_states: Array<{ + Name: string; + Properties: { + Name: string; + }; + }>; + data: number[]; + biomes: { + palette: Array<{ + Name: string; + }>; + data: number[]; + }; + BlockLight: number[]; + SkyLight: number[]; +} + +export type ProtoChunk = + | "empty" + | "structure_starts" + | "structure_references" + | "biomes" + | "noise" + | "surface" + | "carvers" + | "liquid_carvers" + | "light" + | "spawn" + | "heightmaps"; + +export interface IRegion { + DataVersion: number; + xPos: number; + yPos: number; + zPos: number; + Status: ProtoChunk | "minecraft:full"; + LastUpdate: number; + sections: ISubChunk[]; + block_entities: Array>; + /** + * Only used if Status is a ProtoChunk + */ + CarvingMasks?: Array<{ + AIR: number[]; + LIQUID: number[]; + }>; + /** + * Several different heightmaps corresponding to 256 values compacted at 9 bits per value (lowest being 0, highest being 384, both values inclusive). + * The 9 bit values are stored in an array of 37 longs, each containing 7 values (long = 64 bits, 7 * 9 = 63; the last bit is unused). + * Note: the stored value is the "number of blocks above the bottom of the world", + * this is not the same thing as the block Y value, to compute the block Y value use highestBlockY = (chunk.yPos * 16) - 1 + heightmap_entry_value. + * A highestBlockY of -65 if then indicates there are no blocks at that location (e.g. the void). + */ + Heightmaps: { + MOTION_BLOCKING: number[]; + MOTION_BLOCKING_NO_LEAVES: number[]; + OCEAN_FLOOR: number[]; + OCEAN_FLOOR_WG: number[]; + WORLD_SURFACE: number[]; + WORLD_SURFACE_WG: number[]; + }; + InhabitedTime: number; +} + +function createRegion(x: number, y: number, z: number) { + const nbtRoot = { + DataVersion: NBT_DATA_VERSION, + xPos: x, + yPos: y, + zPos: z, + }; +} + +export default async function img2mcworld( + imgSrc: string, + blocks: PaletteSource, +) {} diff --git a/src/nbt/cli.ts b/src/nbt/cli.ts index 0652dab..d1d090a 100644 --- a/src/nbt/cli.ts +++ b/src/nbt/cli.ts @@ -20,20 +20,15 @@ export default async function main( `${basename(src, extname(src))}_${axis}_${Date.now()}.nbt`, ); - await writeFile( - structureDest, - await img2nbt( - src, - palette, - axis, - ), - ); + await writeFile(structureDest, await img2nbt(src, palette, axis)); console.log(`Created ${structureDest}`); } if (import.meta.main) { - const { values: { axis, img, db, watch: watchFile, dest } } = parseArgs({ + const { + values: { axis, img, db, watch: watchFile, dest }, + } = parseArgs({ args: process.argv.slice(2), options: { axis: { @@ -80,12 +75,7 @@ if (import.meta.main) { process.exit(0); } - await main( - img, - await parseDbInput(db), - (axis ?? "x") as Axis, - dest, - ); + await main(img, await parseDbInput(db), (axis ?? "x") as Axis, dest); process.exit(0); } diff --git a/src/nbt/mod.ts b/src/nbt/mod.ts index 2697c15..a8e4260 100644 --- a/src/nbt/mod.ts +++ b/src/nbt/mod.ts @@ -1,7 +1,12 @@ import type { Axis, IBlock } from "../types.ts"; import * as nbt from "nbtify"; import * as imagescript from "imagescript"; -import { DEFAULT_BLOCK, MASK_BLOCK, MAX_DEPTH, NBT_DATA_VERSION } 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"; @@ -40,10 +45,7 @@ export interface INbtTag { * @param palette Block palette * @returns Nearest, masked, or default block */ -function convertBlock( - c: number, - palette: IBlock[], -): IPaletteEntry { +function convertBlock(c: number, palette: IBlock[]): IPaletteEntry { const [r, g, b, a] = imagescript.Image.colorToRGBA(c); if (a < 128) { @@ -72,9 +74,10 @@ function findBlock( blockPalette: StructurePalette, ): [IPaletteEntry, number] { const nearest = convertBlock(c, palette); - const blockIdx = blockPalette.findIndex(({ Name, Properties }) => - Name === nearest.Name && - compareStates(nearest.Properties ?? {}, Properties ?? {}) + const blockIdx = blockPalette.findIndex( + ({ Name, Properties }) => + Name === nearest.Name && + compareStates(nearest.Properties ?? {}, Properties ?? {}), ); return [nearest, blockIdx]; @@ -103,10 +106,7 @@ export function constructDecoded( const [width, height, depth] = size; - const memo = new Map< - number, - [IPaletteEntry, number] - >(); + const memo = new Map(); /** * Block indices primary layer @@ -122,12 +122,10 @@ export function constructDecoded( findBlock(c, palette, blockPalette); if (blockIdx === -1) { - blockIdx = blockPalette.push( - { - Name: nearest.Name ?? DEFAULT_BLOCK, - Properties: nearest.Properties ?? {}, - }, - ) - 1; + blockIdx = blockPalette.push({ + Name: nearest.Name ?? DEFAULT_BLOCK, + Properties: nearest.Properties ?? {}, + }) - 1; memo.set(c, [nearest, blockIdx]); } diff --git a/src/schematic/cli.ts b/src/schematic/cli.ts index 457c69c..ebb2f37 100644 --- a/src/schematic/cli.ts +++ b/src/schematic/cli.ts @@ -20,47 +20,40 @@ export default async function main( `${basename(src, extname(src))}_${axis}_${Date.now()}.schematic`, ); - await writeFile( - structureDest, - await img2schematic( - src, - palette, - axis, - ), - ); + await writeFile(structureDest, await img2schematic(src, palette, axis)); console.log(`Created ${structureDest}`); } if (import.meta.main) { - const { values: { axis, img, db, watch: watchFile, dest } } = parseArgs( - { - args: process.argv.slice(2), - options: { - axis: { - type: "string", - default: "x", - multiple: false, - }, - img: { - type: "string", - multiple: false, - }, - db: { - type: "string", - multiple: false, - }, - watch: { - type: "boolean", - multiple: false, - }, - dest: { - type: "string", - multiple: false, - }, + const { + values: { axis, img, db, watch: watchFile, dest }, + } = parseArgs({ + args: process.argv.slice(2), + options: { + axis: { + type: "string", + default: "x", + multiple: false, + }, + img: { + type: "string", + multiple: false, + }, + db: { + type: "string", + multiple: false, + }, + watch: { + type: "boolean", + multiple: false, + }, + dest: { + type: "string", + multiple: false, }, }, - ); + }); const watcher = watchFile ? watch(img) : null; @@ -82,12 +75,7 @@ if (import.meta.main) { process.exit(0); } - await main( - img, - await parseDbInput(db), - (axis ?? "x") as Axis, - dest, - ); + await main(img, await parseDbInput(db), (axis ?? "x") as Axis, dest); process.exit(0); } diff --git a/src/schematic/mod.ts b/src/schematic/mod.ts index d19d680..c123c7a 100644 --- a/src/schematic/mod.ts +++ b/src/schematic/mod.ts @@ -37,10 +37,7 @@ export interface ISchematicTag { * @param palette Block palette * @returns Nearest, masked, or default block */ -function convertBlock( - c: number, - palette: IBlock[], -): PaletteBlock { +function convertBlock(c: number, palette: IBlock[]): PaletteBlock { const [r, g, b, a] = imagescript.Image.colorToRGBA(c); if (a < 128) { @@ -83,10 +80,7 @@ export function constructDecoded( const [width, height, depth] = size; - const memo = new Map< - number, - [PaletteBlock, number] - >(); + const memo = new Map(); /** * Block indices primary layer diff --git a/src/types.ts b/src/types.ts index 862d16e..d7338c5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,10 +2,7 @@ export type Axis = "x" | "y" | "z"; export type RGB = [number, number, number]; export type RGBA = [number, number, number, number]; -export type PaletteSource = Record< - string, - string | IBlock ->; +export type PaletteSource = Record; export interface IBlock { version: number; @@ -37,4 +34,4 @@ export interface IMcStructure { export type StructurePalette = Array< Pick & { name: string } ->; \ No newline at end of file +>; diff --git a/src/vox/cli.ts b/src/vox/cli.ts index 09b2f02..5eb54f3 100644 --- a/src/vox/cli.ts +++ b/src/vox/cli.ts @@ -14,25 +14,25 @@ if (import.meta.main) { process.exit(1); } - const { values: { db, src, dest } } = parseArgs( - { - args: process.argv.slice(2), - options: { - db: { - type: "string", - multiple: false, - }, - src: { - type: "string", - multiple: false, - }, - dest: { - type: "string", - multiple: false, - }, + const { + values: { db, src, dest }, + } = parseArgs({ + args: process.argv.slice(2), + options: { + db: { + type: "string", + multiple: false, + }, + src: { + type: "string", + multiple: false, + }, + dest: { + type: "string", + multiple: false, }, }, - ); + }); const gif = new imagescript.GIF(await vox2gif(src)); diff --git a/src/vox/mod.ts b/src/vox/mod.ts index 6dffa18..a3237f6 100644 --- a/src/vox/mod.ts +++ b/src/vox/mod.ts @@ -17,12 +17,14 @@ interface VoxData { }; xyzi: { numVoxels: number; - values: [{ - x: number; - y: number; - z: number; - i: number; - }]; + values: [ + { + x: number; + y: number; + z: number; + i: number; + }, + ]; }; rgba: { values: [{ r: number; g: number; b: number; a: number }]; @@ -79,8 +81,9 @@ function findBlock( [c?.r ?? 0, c?.g ?? 0, c?.b ?? 0, c?.a ?? 0], palette, ); - const blockIdx = blockPalette.findIndex(({ name, states }) => - name === nearest.id && compareStates(nearest.states, states) + const blockIdx = blockPalette.findIndex( + ({ name, states }) => + name === nearest.id && compareStates(nearest.states, states), ); return [nearest, blockIdx]; @@ -105,11 +108,7 @@ export function constructDecoded( /** * Structure size (X, Y, Z) */ - const size: [number, number, number] = [ - vox.size.z, - vox.size.y, - vox.size.x, - ]; + const size: [number, number, number] = [vox.size.z, vox.size.y, vox.size.x]; const [width, height, depth] = size; @@ -131,13 +130,11 @@ export function constructDecoded( findBlock(vox.rgba.values[i], palette, blockPalette); if (blockIdx === -1) { - blockIdx = blockPalette.push( - { - version: nearest.version ?? BLOCK_VERSION, - name: nearest.id ?? DEFAULT_BLOCK, - states: nearest.states ?? {}, - }, - ) - 1; + blockIdx = blockPalette.push({ + version: nearest.version ?? BLOCK_VERSION, + name: nearest.id ?? DEFAULT_BLOCK, + states: nearest.states ?? {}, + }) - 1; memo.set(i, [nearest, blockIdx]); }