feat: export as .schem
todo: block properties todo: fix bug with air blocks
This commit is contained in:
parent
6aa619c632
commit
37be502e1a
3 changed files with 140 additions and 3 deletions
|
|
@ -66,8 +66,7 @@ function SaveLitematic({ close }: DialogProps) {
|
|||
|
||||
filledBlocks.forEach((block) => {
|
||||
const blockInfo = blockData[block.name.replace("minecraft:", "")];
|
||||
let blockName = block.name;
|
||||
if (blockInfo) blockName = blockInfo.id[0].toString();
|
||||
const blockName = blockInfo ? blockInfo.id[0].toString() : block.name;
|
||||
|
||||
const blockId = blockStatePallete.findIndex((entry) => entry.Name === `minecraft:${blockName}`);
|
||||
|
||||
|
|
|
|||
138
src/components/dialogs/SaveSchem.tsx
Normal file
138
src/components/dialogs/SaveSchem.tsx
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { useContext, useState } from "react";
|
||||
import * as nbt from "nbtify";
|
||||
|
||||
import { CanvasContext } from "@/context/Canvas";
|
||||
import { LoadingContext } from "@/context/Loading";
|
||||
|
||||
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
import _blockData from "@/data/blocks/data.json";
|
||||
const blockData: BlockData = _blockData;
|
||||
|
||||
function SaveLitematic({ close }: DialogProps) {
|
||||
const { canvasSize, blocks } = useContext(CanvasContext);
|
||||
const { setLoading } = useContext(LoadingContext);
|
||||
|
||||
const [fileName, setFileName] = useState("blockmatic");
|
||||
|
||||
const onSubmit = async () => {
|
||||
setLoading(true);
|
||||
// Wait for loading indicator to appear
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// todo: check if file name input is empty/valid
|
||||
const width = canvasSize.maxX - canvasSize.minX;
|
||||
const height = canvasSize.maxY - canvasSize.minY;
|
||||
|
||||
// Fill in empty blocks with "air"
|
||||
const filledBlocks: Block[] = [];
|
||||
|
||||
for (let x = canvasSize.minX; x <= canvasSize.maxX - 1; x++) {
|
||||
for (let y = canvasSize.minY; y <= canvasSize.maxY - 1; y++) {
|
||||
const existingBlock = blocks.find((block) => block.x === x && block.y === y);
|
||||
if (existingBlock) {
|
||||
filledBlocks.push(existingBlock);
|
||||
} else {
|
||||
filledBlocks.push({ name: "air", x, y });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort array
|
||||
filledBlocks.sort((a, b) => {
|
||||
if (a.y !== b.y) {
|
||||
return b.y - a.y;
|
||||
}
|
||||
return a.x - b.x;
|
||||
});
|
||||
|
||||
// Generate the block pallete
|
||||
const blockPallete = Array.from(
|
||||
new Set(
|
||||
filledBlocks.map((block) => {
|
||||
const blockInfo = blockData[block.name.replace("minecraft:", "")];
|
||||
// todo: add block properties
|
||||
return `minecraft:${blockInfo.id[0]}`;
|
||||
})
|
||||
)
|
||||
).reduce<Record<string, nbt.Int32<number>>>((acc, blockName, index) => {
|
||||
acc[blockName] = new nbt.Int32(index);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Get the block data
|
||||
const buffer = new ArrayBuffer(filledBlocks.length);
|
||||
const blockPlaceData = new Int8Array(buffer);
|
||||
|
||||
filledBlocks.forEach((block, index) => {
|
||||
const blockInfo = blockData[block.name.replace("minecraft:", "")];
|
||||
const blockName = blockInfo ? blockInfo.id[0].toString() : block.name;
|
||||
const blockId = blockPallete[`minecraft:${blockName}`];
|
||||
|
||||
blockPlaceData[index] = parseInt(blockId.toString());
|
||||
});
|
||||
|
||||
// Generate NBT data
|
||||
const data = {
|
||||
Schematic: {
|
||||
DataVersion: new nbt.Int32(4189),
|
||||
Version: new nbt.Int32(3),
|
||||
Width: new nbt.Int16(width),
|
||||
Height: new nbt.Int16(height),
|
||||
Length: new nbt.Int16(1),
|
||||
Metadata: {
|
||||
Date: new Date().getTime(),
|
||||
},
|
||||
Blocks: {
|
||||
Data: blockPlaceData,
|
||||
Palette: blockPallete,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Write to file
|
||||
const bytes = await nbt.write(data);
|
||||
const compressed = await nbt.compress(bytes, "gzip");
|
||||
|
||||
const blob = new Blob([compressed], { type: "application/x-gzip" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
setLoading(false);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `${fileName}.schem`;
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Save as .schem</DialogTitle>
|
||||
<DialogDescription>Save your image as a .schem (Sponge)</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Input value={fileName} onChange={(e) => setFileName(e.target.value)} />
|
||||
<span>.schem</span>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" onClick={onSubmit}>
|
||||
Download
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default SaveLitematic;
|
||||
|
|
@ -27,7 +27,7 @@ function FileMenu() {
|
|||
<MenubarSub>
|
||||
<MenubarSubTrigger>Export to...</MenubarSubTrigger>
|
||||
<MenubarSubContent>
|
||||
<MenubarItem>.schematic</MenubarItem>
|
||||
<MenubarItem onClick={() => openDialog("SaveSchem")}>.schem</MenubarItem>
|
||||
<MenubarItem onClick={() => openDialog("SaveLitematic")}>.litematic</MenubarItem>
|
||||
<MenubarItem onClick={() => openDialog("SaveImage")}>image</MenubarItem>
|
||||
</MenubarSubContent>
|
||||
|
|
|
|||
Loading…
Reference in a new issue