diff --git a/deno.json b/deno.json index 937d7ed..58a2d1d 100644 --- a/deno.json +++ b/deno.json @@ -1,11 +1,11 @@ { "tasks": { - "dev": "deno run -A --node-modules-dir npm:vite & deno run --allow-net --allow-read --allow-write --allow-env --watch ./server/main.ts", + "dev": "deno run -A --node-modules-dir npm:vite & deno run -A --watch ./server/main.ts", "fdev": "deno run -A --node-modules-dir npm:vite", "build": "deno run -A --node-modules-dir npm:vite build", "preview": "deno run -A --node-modules-dir npm:vite preview", - "serve": "deno run --allow-net --allow-read --allow-write --allow-env ./server/main.ts", - "bdev": "deno run --allow-net --allow-read --allow-write --allow-env --watch ./server/main.ts", + "serve": "deno run -A ./server/main.ts", + "bdev": "deno run -A --watch ./server/main.ts", "tmux": "./session.sh" }, "compilerOptions": { @@ -19,6 +19,7 @@ "@cgg/sockpuppet": "../sockpuppet.ts/server/mod.ts", "@cgg/sockpuppet/client": "../sockpuppet.ts/client/mod.ts", "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.0", + "@gfx/canvas": "jsr:@gfx/canvas@^0.5.8", "@preact/preset-vite": "npm:@preact/preset-vite@^2.9.1", "@std/encoding": "jsr:@std/encoding@^1.0.5", "@std/fs": "jsr:@std/fs@^1.0.4", diff --git a/deno.lock b/deno.lock index 8a9da87..98ab8ab 100644 --- a/deno.lock +++ b/deno.lock @@ -2,14 +2,26 @@ "version": "4", "specifiers": { "jsr:@bearmetal/store@^0.0.5": "0.0.5", + "jsr:@denosaurs/plug@1.0.5": "1.0.5", + "jsr:@gfx/canvas@~0.5.8": "0.5.8", + "jsr:@std/assert@0.214": "0.214.0", + "jsr:@std/assert@0.217": "0.217.0", "jsr:@std/cli@^1.0.6": "1.0.6", + "jsr:@std/encoding@0.214": "0.214.0", + "jsr:@std/encoding@0.217.0": "0.217.0", "jsr:@std/encoding@^1.0.5": "1.0.5", + "jsr:@std/fmt@0.214": "0.214.0", "jsr:@std/fmt@^1.0.2": "1.0.2", "jsr:@std/fs@*": "1.0.4", + "jsr:@std/fs@0.214": "0.214.0", + "jsr:@std/fs@0.217.0": "0.217.0", "jsr:@std/fs@^1.0.4": "1.0.4", "jsr:@std/http@^1.0.8": "1.0.8", "jsr:@std/media-types@^1.0.3": "1.0.3", "jsr:@std/net@^1.0.4": "1.0.4", + "jsr:@std/path@0.214": "0.214.0", + "jsr:@std/path@0.217": "0.217.0", + "jsr:@std/path@0.217.0": "0.217.0", "jsr:@std/path@^1.0.6": "1.0.6", "jsr:@std/streams@^1.0.7": "1.0.7", "npm:@babel/plugin-transform-react-jsx-development@^7.25.7": "7.25.7_@babel+core@7.25.8", @@ -37,30 +49,77 @@ "jsr:@std/fs@^1.0.4" ] }, + "@denosaurs/plug@1.0.5": { + "integrity": "04cd988da558adc226202d88c3a434d5fcc08146eaf4baf0cea0c2284b16d2bf", + "dependencies": [ + "jsr:@std/encoding@0.214", + "jsr:@std/fmt@0.214", + "jsr:@std/fs@0.214", + "jsr:@std/path@0.214" + ] + }, + "@gfx/canvas@0.5.8": { + "integrity": "a61c80292528e7433d428556b494a0ea496dd8e6abd4a338b8b25fc04e46ea3e", + "dependencies": [ + "jsr:@denosaurs/plug", + "jsr:@std/encoding@0.217.0", + "jsr:@std/fs@0.217.0", + "jsr:@std/path@0.217.0" + ] + }, + "@std/assert@0.214.0": { + "integrity": "55d398de76a9828fd3b1aa653f4dba3eee4c6985d90c514865d2be9bd082b140" + }, + "@std/assert@0.217.0": { + "integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642" + }, "@std/cli@1.0.6": { "integrity": "d22d8b38c66c666d7ad1f2a66c5b122da1704f985d3c47f01129f05abb6c5d3d" }, + "@std/encoding@0.214.0": { + "integrity": "30a8713e1db22986c7e780555ffd2fefd1d4f9374d734bb41f5970f6c3352af5" + }, + "@std/encoding@0.217.0": { + "integrity": "b03e8ff94c98d6b6a02c02c5cf8e5d203400155516248964fc4559abc04669dc" + }, "@std/encoding@1.0.5": { "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" }, + "@std/fmt@0.214.0": { + "integrity": "40382cff88a0783b347b4d69b94cf931ab8e549a733916718cb866c08efac4d4" + }, "@std/fmt@1.0.2": { "integrity": "87e9dfcdd3ca7c066e0c3c657c1f987c82888eb8103a3a3baa62684ffeb0f7a7" }, + "@std/fs@0.214.0": { + "integrity": "bc880fea0be120cb1550b1ed7faf92fe071003d83f2456a1e129b39193d85bea", + "dependencies": [ + "jsr:@std/assert@0.214", + "jsr:@std/path@0.214" + ] + }, + "@std/fs@0.217.0": { + "integrity": "0bfff5f3618d68c385b28b4ffbf3a15c98293a0f1186444458b62e0111ce77b2", + "dependencies": [ + "jsr:@std/assert@0.217", + "jsr:@std/path@0.217" + ] + }, "@std/fs@1.0.4": { "integrity": "2907d32d8d1d9e540588fd5fe0ec21ee638134bd51df327ad4e443aaef07123c", "dependencies": [ - "jsr:@std/path" + "jsr:@std/path@^1.0.6" ] }, "@std/http@1.0.8": { "integrity": "6ea1b2e8d33929967754a3b6d6c6f399ad6647d7bbb5a466c1eaf9b294a6ebcd", "dependencies": [ "jsr:@std/cli", - "jsr:@std/encoding", - "jsr:@std/fmt", + "jsr:@std/encoding@^1.0.5", + "jsr:@std/fmt@^1.0.2", "jsr:@std/media-types", "jsr:@std/net", - "jsr:@std/path", + "jsr:@std/path@^1.0.6", "jsr:@std/streams" ] }, @@ -70,6 +129,18 @@ "@std/net@1.0.4": { "integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852" }, + "@std/path@0.214.0": { + "integrity": "d5577c0b8d66f7e8e3586d864ebdf178bb326145a3611da5a51c961740300285", + "dependencies": [ + "jsr:@std/assert@0.214" + ] + }, + "@std/path@0.217.0": { + "integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11", + "dependencies": [ + "jsr:@std/assert@0.217" + ] + }, "@std/path@1.0.6": { "integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed" }, @@ -1418,6 +1489,7 @@ "workspace": { "dependencies": [ "jsr:@bearmetal/store@^0.0.5", + "jsr:@gfx/canvas@~0.5.8", "jsr:@std/encoding@^1.0.5", "jsr:@std/fs@^1.0.4", "jsr:@std/http@^1.0.8", diff --git a/server/resources/readers.ts b/server/resources/readers.ts index 730046a..8d2436f 100644 --- a/server/resources/readers.ts +++ b/server/resources/readers.ts @@ -1,5 +1,6 @@ import { encodeBase64 } from "@std/encoding/base64"; import { readDirFiles } from "../util/readDir.ts"; +import { createIsometricCube } from "./renderer.ts"; interface BlockItem { name: string; @@ -16,13 +17,287 @@ export const readBlocks = async (path: string) => { images: [], })); - const texPath = path + "/assets/minecraft/textures/block"; - for await (const image of Deno.readDir(texPath)) { - for (const block of blocks) { - if (image.name.startsWith(block.name)) { - const data = await Deno.readFile(texPath + "/" + image.name); - block.images.push("image/png;base64," + encodeBase64(data)); + const blockTextures = await readDirFiles( + path + "/assets/minecraft/textures/block", + ); + + const modelPath = path + "/assets/minecraft/models/block"; + for (const block of blocks) { + let name = block.name; + if ( + block.name.startsWith("air") || + block.name.startsWith("void_air") || + block.name.startsWith("cave_air") || + block.name.startsWith("end_gateway") || + block.name.endsWith("_door") || + block.name.endsWith("_trapdoor") || + block.name.endsWith("_banner") || + block.name.endsWith("_fence_gate") || + block.name.endsWith("_pressure_plate") || + block.name.endsWith("_button") || + block.name.endsWith("lever") || + block.name.endsWith("_sign") || + block.name.endsWith("torch") || + block.name.endsWith("_fence") || + block.name.endsWith("_wall") || + block.name.endsWith("_skull") || + block.name.endsWith("_head") || + block.name.includes("bubble_column") || + block.name.includes("tripwire_hook") || + block.name.includes("tripwire") || + block.name == undefined || + block.name.includes("chest") || + block.name.includes("command") + ) continue; + if (block.name.includes("water") || block.name.includes("lava")) { + const what = block.name.includes("water") ? "water" : "lava"; + const itemTexLoc = path + + `/assets/minecraft/textures/block/${what}_still.png`; + const itemImage = await Deno.readFile(itemTexLoc); + block.images.push("data:image/png;base64," + encodeBase64(itemImage)); + continue; + } + if (block.name === "nether_portal") { + const itemTexLoc = path + + `/assets/minecraft/textures/block/nether_portal.png`; + const itemImage = await Deno.readFile(itemTexLoc); + block.images.push("data:image/png;base64," + encodeBase64(itemImage)); + continue; + } + if (["fire", "soul_fire"].includes(block.name)) { + const itemTexLoc = path + + `/assets/minecraft/textures/block/${block.name}_0.png`; + const itemImage = await Deno.readFile(itemTexLoc); + block.images.push("data:image/png;base64," + encodeBase64(itemImage)); + continue; + } + if (block.name.includes("cake")) { + const itemTexLoc = path + + `/assets/minecraft/textures/item/cake.png`; + const itemImage = await Deno.readFile(itemTexLoc); + block.images.push("data:image/png;base64," + encodeBase64(itemImage)); + continue; + } + if (block.name.endsWith("_bed")) { + const itemTexLoc = path + "/assets/minecraft/textures/entity/bed/" + + block.name.replace(/_bed$/, "") + ".png"; + const itemImage = await Deno.readFile(itemTexLoc); + block.images.push("data:image/png;base64," + encodeBase64(itemImage)); + continue; + } + if (block.name === "nether_portal") { + const itemTexLoc = path + + `/assets/minecraft/textures/block/nether_portal.png`; + const itemImage = await Deno.readFile(itemTexLoc); + block.images.push("data:image/png;base64," + encodeBase64(itemImage)); + continue; + } + if (block.name.includes("dripleaf") && !block.name.includes("stem")) { + const itemTexLoc = path + + `/assets/minecraft/textures/block/${block.name}_top.png`; + const itemImage = await Deno.readFile(itemTexLoc); + block.images.push("data:image/png;base64," + encodeBase64(itemImage)); + continue; + } + if ( + [ + "carrots", + "potatoes", + "wheat", + "beetroots", + "grass", + "kelp", + "light", + "bell", + "redstone_wire", + "sweet_berry_bush", + "cocoa", + "nether_wart", + "repeater", + "bamboo", + "pitcher_plant", + "campfire", + "soul_campfire", + ].includes(block.name) + ) { + const itemTexLoc = path + "/assets/minecraft/textures/item/" + + block.name + .replace("large_fern", "fern") + .replace("tall_seagrass", "seagrass") + .replace(/ss$/, "?") + .replace(/e?s$/, "") + .replace("?", "ss") + .replace("_bush", "") + .replace("berry", "berries") + .replace("cocoa", "cocoa_beans") + .replace("_wire", "") + + ".png"; + const itemImage = await Deno.readFile(itemTexLoc); + block.images.push("data:image/png;base64," + encodeBase64(itemImage)); + continue; + } + if ( + [ + "melon_stem", + "pumpkin_stem", + "fern", + "large_fern", + "lilac", + "azure_bluet", + "red_tulip", + "orange_tulip", + "pink_tulip", + "white_tulip", + "oxeye_daisy", + "cornflower", + "lily_of_the_valley", + "wither_rose", + "rose_bush", + "peony", + "sunflower", + "torchflower_crop", + "suspicious_sand", + "suspicious_gravel", + "scaffolding", + "respawn_anchor", + "short_grass", + "tall_grass", + "seagrass", + "tall_seagrass", + "frosted_ice", + "pitcher_crop", + "iron_bars", + ].includes(block.name) + ) { + const textures = blockTextures.filter((t) => t.includes(block.name)) + .reverse().filter((t) => !t.endsWith("mcmeta")); + const itemTexLoc = path + "/assets/minecraft/textures/block/" + + textures[0]; + const itemImage = await Deno.readFile(itemTexLoc); + block.images.push("data:image/png;base64," + encodeBase64(itemImage)); + continue; + } + if (block.name.startsWith("waxed")) { + name = block.name.replace(/^waxed_?/, ""); + } + if (block.name.startsWith("infested")) { + name = block.name.replace(/^infested_?/, ""); + } + if (block.name.endsWith("_pane")) { + name = block.name.replace(/_pane$/, ""); + } + if (block.name === "pink_petals") { + name = "pink_petals_4"; + } + if (block.name.includes("candle")) { + const itemTexLoc = path + + `/assets/minecraft/textures/item/${block.name}.png`; + const itemImage = await Deno.readFile(itemTexLoc); + block.images.push("data:image/png;base64," + encodeBase64(itemImage)); + continue; + } + if (block.name.includes("cauldron")) { + const textures = blockTextures.filter((t) => t.includes("cauldron")); + for (const texture of textures) { + const texLoc = texture.replace("minecraft:", ""); + const image = await Deno.readFile( + path + "/assets/minecraft/textures/block/" + texLoc, + ); + const b64 = "data:image/png;base64," + encodeBase64(image); + block.images.push(await createIsometricCube(b64, b64, b64)); } + continue; + } + if (block.name === "snow") { + name = "snow_height2"; + } + const data = await Deno.readTextFile( + modelPath + "/" + name + ".json", + ); + const modelJson = JSON.parse(data); + if (modelJson.textures && !modelJson.elements) { + if (modelJson.textures.all) { + const texLoc = modelJson.textures.all; + const image = await Deno.readFile( + path + "/assets/minecraft/textures/" + texLoc.replace( + "minecraft:", + "", + ) + ".png", + ); + const b64 = "data:image/png;base64," + encodeBase64(image); + block.images.push(await createIsometricCube(b64, b64, b64)); + continue; + } + if (modelJson.textures.front) { + const frontImage = await Deno.readFile( + path + "/assets/minecraft/textures/" + + modelJson.textures.front.replace( + "minecraft:", + "", + ) + ".png", + ); + + const frontB64 = "data:image/png;base64," + encodeBase64(frontImage); + + const topImage = await Deno.readFile( + path + "/assets/minecraft/textures/" + + (modelJson.textures.top ?? modelJson.textures.side).replace( + "minecraft:", + "", + ) + ".png", + ); + + const topB64 = "data:image/png;base64," + encodeBase64(topImage); + + const sideImage = await Deno.readFile( + path + "/assets/minecraft/textures/" + + modelJson.textures.side.replace( + "minecraft:", + "", + ) + ".png", + ); + + const sideB64 = "data:image/png;base64," + encodeBase64(sideImage); + block.images.push(await createIsometricCube(frontB64, sideB64, topB64)); + continue; + } + if (modelJson.textures.side) { + const sideImage = await Deno.readFile( + path + "/assets/minecraft/textures/" + + modelJson.textures.side.replace( + "minecraft:", + "", + ) + ".png", + ); + + const sideB64 = "data:image/png;base64," + encodeBase64(sideImage); + + const topImage = await Deno.readFile( + path + "/assets/minecraft/textures/" + + (modelJson.textures.top ?? modelJson.textures.bottom ?? + modelJson.textures.side).replace( + "minecraft:", + "", + ) + + ".png", + ); + + const topB64 = "data:image/png;base64," + encodeBase64(topImage); + block.images.push(await createIsometricCube(sideB64, sideB64, topB64)); + continue; + } + } + const textures = blockTextures.filter((t) => + t.includes(block.name) && !t.includes("mcmeta") + ); + + for (const texture of textures) { + const texLoc = texture.replace("minecraft:", ""); + const image = await Deno.readFile( + path + "/assets/minecraft/textures/block/" + texLoc, + ); + const b64 = "data:image/png;base64," + encodeBase64(image); + block.images.push(await createIsometricCube(b64, b64, b64)); } } return blocks; @@ -34,11 +309,15 @@ export const readItems = async (path: string) => { name: i.replace(".json", ""), resourceLocation: "minecraft:" + i.replace(".json", ""), images: [], - })).slice(0, 10); + })); for (const item of items) { + let name = item.name; + if (item.name.startsWith("air")) continue; + if (item.name.startsWith("waxed")) name = item.name.replace(/^waxed_?/, ""); + const data = await Deno.readFile( - path + "/assets/minecraft/models/item/" + item.name + ".json", + path + "/assets/minecraft/models/item/" + name + ".json", ); const json = JSON.parse(new TextDecoder().decode(data)); const texDir = path + "/assets/minecraft/textures/"; @@ -48,7 +327,7 @@ export const readItems = async (path: string) => { const data = await Deno.readFile( texDir + texLoc.replace("minecraft:", "") + ".png", ); - item.images.push("image/png;base64," + encodeBase64(data)); + item.images.push("data:image/png;base64," + encodeBase64(data)); } } else if (json.parent) { const parent = await Deno.readFile( @@ -65,7 +344,7 @@ export const readItems = async (path: string) => { const data = await Deno.readFile( texDir + texLoc.replace("minecraft:", "") + ".png", ); - item.images.push("image/png;base64," + encodeBase64(data)); + item.images.push("data:image/png;base64," + encodeBase64(data)); } } } diff --git a/server/resources/renderer.ts b/server/resources/renderer.ts new file mode 100644 index 0000000..31a496b --- /dev/null +++ b/server/resources/renderer.ts @@ -0,0 +1,81 @@ +import { createCanvas, Image } from "@gfx/canvas"; + +function loadImage(src: string): Promise { + return new Promise((resolve, reject) => { + if (!src.startsWith("data:image/png;base64,")) { + console.log("src", src); + } + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = () => { + console.log(src); + reject(); + }; + img.src = src; + }); +} + +export async function createIsometricCube( + face1Src: string, + face2Src: string, + face3Src: string, + width: number = 96, +) { + const canvasWidth = width; // Set a size for the canvas + const canvasHeight = Math.ceil(width / .866); + const canvas = createCanvas(canvasWidth, canvasHeight); + const ctx = canvas.getContext("2d"); + + try { + const [face1, face2, face3] = await Promise.all([ + loadImage(face1Src), + loadImage(face2Src), + loadImage(face3Src), + ]).catch(); + + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + + ctx.setTransform( + 1, + 0, + Math.tan(degToRad(30)), + 1, + 0, + canvasWidth / 3.5, + ); + ctx.drawImage(face1, 0, 0, canvasWidth / 2, canvasHeight / 2); + + // Face 2 + ctx.setTransform( + 1, + 0, + -Math.tan(degToRad(30)), + 1, + canvasWidth / 2, + canvasHeight / 2, + ); + ctx.drawImage(face2, 0, 0, canvasWidth / 2, canvasHeight / 2); + + // Face 3 + ctx.setTransform( + 1, + -Math.tan(degToRad(60)), + Math.tan(degToRad(30)), + 1, + canvasWidth / 2, + 0, + ); + ctx.drawImage(face3, 0, 0, canvasWidth / 2, canvasWidth / 3.5); + + ctx.setTransform(1, 0, 0, 1, 0, 0); + } catch (e) { + // console.log("error", e); + } + + const b64 = canvas.toDataURL("png"); + return b64; +} + +function degToRad(deg: number) { + return deg * (Math.PI / 180); +} diff --git a/server/resources/routes.ts b/server/resources/routes.ts index c070b74..1594763 100644 --- a/server/resources/routes.ts +++ b/server/resources/routes.ts @@ -20,16 +20,27 @@ export const createResourcesRoutes = (router: Router) => { const packVersionJson = JSON.parse(packVersion); const mcVersion = packVersionJson.mcVersion; const resourceVersions = await readDirDirs("./resources"); + console.log("resourceVersions", resourceVersions); for (const resourceVersion of resourceVersions) { if (versionCompat(resourceVersion, mcVersion)) { const resourcePath = "./resources/" + resourceVersion; const splitPath = path.split("/"); switch (splitPath[0]) { + case "block": case "blocks": { return new Response( JSON.stringify(await readBlocks(resourcePath)), ); } + case "item": + case "items": { + return new Response( + JSON.stringify(await readBlocks(resourcePath)), + ); + } + default: { + return new Response("invalid path", { status: 400 }); + } } } } diff --git a/server/util/versionCompat.ts b/server/util/versionCompat.ts index 68469ed..6de97e9 100644 --- a/server/util/versionCompat.ts +++ b/server/util/versionCompat.ts @@ -5,7 +5,7 @@ export const versionCompat = (version: string, targetVersion: string) => { const versionSplit = version.split("."); const targetVersionSplit = targetVersion.split("."); for (let i = 0; i < versionSplit.length; i++) { - if (versionSplit[i] > targetVersionSplit[i]) { + if (versionSplit[i] ?? "0" > targetVersionSplit[i] ?? "0") { return true; } } diff --git a/session.sh b/session.sh index c33dbd1..52b4c6e 100755 --- a/session.sh +++ b/session.sh @@ -11,4 +11,6 @@ tmux send-keys "deno task bdev" C-m tmux split-window -h tmux send-keys "deno task fdev" C-m +tmux split-window -v + tmux attach -t $SESSION diff --git a/src/components/editor/tags/editor.tsx b/src/components/editor/tags/editor.tsx index 3761a6a..508b00a 100644 --- a/src/components/editor/tags/editor.tsx +++ b/src/components/editor/tags/editor.tsx @@ -25,30 +25,35 @@ function TagList() { const [showNewTagModal, setShowNewTagModal] = useState(false); - if (isLoading || !data) { + if (isLoading) { return ; } return (
-
); } @@ -92,7 +97,8 @@ function TagEditor() { onChange={(e) => setTag({ ...tag, replace: (e.target as any).checked })} /> - {tag.values.map((value, i) => ( + { + /* {tag.values.map((value, i) => (
- ))} + ))} */ + } {tag.values.length === 0 && (