feat: open schematic (.litematic)

known bug: blobs of air blocks in some schematics
This commit is contained in:
trafficlunar 2025-01-10 21:01:28 +00:00
parent 8726ffeedd
commit 612d519068
5 changed files with 174 additions and 12 deletions

View file

@ -1,16 +1,13 @@
name: Ntfy Deployment Notifications name: Deployment Notifications
on: on: [deployment_status]
push:
branches:
- main
jobs: jobs:
notify: notify:
if: failure() || success() if: github.event.deployment_status.state == "failure" | github.event.deployment_status.state == "success"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Notify on Success - name: Notify on Success
if: success() if: github.event.deployment_status.state == "success"
run: | run: |
curl \ curl \
-H "Authorization: Bearer ${{ secrets.NTFY_TOKEN }}" \ -H "Authorization: Bearer ${{ secrets.NTFY_TOKEN }}" \
@ -21,7 +18,7 @@ jobs:
${{ secrets.NTFY_URL }} ${{ secrets.NTFY_URL }}
- name: Notify on Failure - name: Notify on Failure
if: failure() if: github.event.deployment_status.state == "failure"
run: | run: |
curl \ curl \
-H "Authorization: Bearer ${{ secrets.NTFY_TOKEN }}" \ -H "Authorization: Bearer ${{ secrets.NTFY_TOKEN }}" \

View file

@ -152,8 +152,12 @@ function OpenImage({ close }: DialogProps) {
})} })}
> >
<input {...getInputProps({ multiple: false })} /> <input {...getInputProps({ multiple: false })} />
<UploadIcon /> <UploadIcon size={30} />
<p>Drag and drop your image here or click to open</p> <p className="text-center">
Drag and drop your image here
<br />
or click to open
</p>
</div> </div>
<div className="grid grid-cols-[auto,1fr] gap-2"> <div className="grid grid-cols-[auto,1fr] gap-2">

View file

@ -0,0 +1,160 @@
import { useContext } from "react";
import { useDropzone } from "react-dropzone";
import { UploadIcon } from "lucide-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 _blockData from "@/data/blocks/data.json";
const blockData: BlockData = _blockData;
interface BlockPalette extends nbt.CompoundTagLike {
Name: string;
Properties?: Record<string, string>;
}
interface LitematicNBT extends nbt.ListTagLike {
Regions: {
Image: {
BlockStatePalette: BlockPalette[];
BlockStates: BigInt64Array;
Size: { x: number; y: number; z: number };
};
};
}
function OpenSchematic({ close }: DialogProps) {
const { setBlocks } = useContext(CanvasContext);
const { setLoading } = useContext(LoadingContext);
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
accept: {
"application/x-gzip-compressed": [".litematic", ".schem"],
},
});
const onSubmit = async () => {
const file = acceptedFiles[0];
if (file) {
setLoading(true);
// Wait for loading indicator to appear
await new Promise((resolve) => setTimeout(resolve, 100));
const fileExtension = file.name.split(".").pop();
const bytes = await file.bytes();
const data = await nbt.read(bytes);
if (fileExtension == "litematic") {
const litematicData = (data as nbt.NBTData<LitematicNBT>).data;
const imageRegion = litematicData.Regions.Image;
// todo: set version
const requiredBits = Math.max(Math.ceil(Math.log2(imageRegion.BlockStatePalette.length)), 2);
const getPaletteIndex = (index: number): bigint => {
const originalY = Math.floor(index / imageRegion.Size.x);
const originalX = index % imageRegion.Size.x;
const reversedY = imageRegion.Size.y - 1 - originalY;
const reversedIndex = reversedY * imageRegion.Size.x + originalX;
// getAt() implementation - LitematicaBitArray.java
const startOffset = reversedIndex * requiredBits;
const startArrayIndex = Math.floor(startOffset / 64);
const endArrayIndex = ((reversedIndex + 1) * requiredBits - 1) >> 6;
const bitOffset = startOffset % 64;
const mask = (1 << requiredBits) - 1;
if (startArrayIndex === endArrayIndex) {
return (imageRegion.BlockStates[startArrayIndex] >> BigInt(bitOffset)) & BigInt(mask);
} else {
const endOffset = 64 - bitOffset;
return (
((imageRegion.BlockStates[startArrayIndex] >> BigInt(bitOffset)) | (imageRegion.BlockStates[endArrayIndex] << BigInt(endOffset))) &
BigInt(mask)
);
}
};
// Add every block to the canvas
const blocks: Block[] = [];
let index = 0;
for (let y = 0; y < imageRegion.Size.y; y++) {
for (let x = 0; x < imageRegion.Size.x; x++) {
const paletteIndex = Number(getPaletteIndex(index));
const paletteBlock = imageRegion.BlockStatePalette.at(paletteIndex);
index++;
if (!paletteBlock) continue;
const blockId = paletteBlock.Name.replace("minecraft:", "");
if (blockId == "air") continue;
for (const name in blockData) {
const dataId = blockData[name].id[0];
if (dataId !== blockId) continue;
const paletteProperties = paletteBlock.Properties;
const dataProperties = blockData[name].properties;
if (paletteProperties) {
if (!dataProperties) continue;
if (!Object.keys(paletteProperties).every((key) => paletteProperties[key] === dataProperties[key])) {
continue;
}
}
blocks.push({ x, y, name });
break;
}
}
}
setBlocks(blocks);
}
}
setLoading(false);
close();
};
return (
<DialogContent>
<DialogHeader>
<DialogTitle>Open Schematic</DialogTitle>
<DialogDescription>Open your schematic file to load into the canvas</DialogDescription>
</DialogHeader>
<div
{...getRootProps({
className: "flex flex-col justify-center items-center gap-2 p-4 rounded border border-2 border-dashed select-none",
})}
>
<input {...getInputProps({ multiple: false })} />
<UploadIcon size={30} />
<p className="text-center">
Drag and drop your schematic file here
<br />
or click to open
</p>
</div>
<DialogFooter>
<Button variant="outline" onClick={close}>
Cancel
</Button>
<Button type="submit" onClick={onSubmit}>
Submit
</Button>
</DialogFooter>
</DialogContent>
);
}
export default OpenSchematic;

View file

@ -47,7 +47,6 @@ function SaveLitematic({ close }: DialogProps) {
new Set( new Set(
blocks.map((block) => { blocks.map((block) => {
const blockInfo = blockData[block.name.replace("minecraft:", "")]; const blockInfo = blockData[block.name.replace("minecraft:", "")];
const returnData: { Name: string; Properties?: Record<string, string> } = { const returnData: { Name: string; Properties?: Record<string, string> } = {
Name: `minecraft:${blockInfo.id[0]}`, Name: `minecraft:${blockInfo.id[0]}`,
...(blockInfo.properties ? { Properties: blockInfo.properties } : {}), ...(blockInfo.properties ? { Properties: blockInfo.properties } : {}),
@ -72,6 +71,8 @@ function SaveLitematic({ close }: DialogProps) {
const reversedY = height - 1 - block.y; const reversedY = height - 1 - block.y;
const index = reversedY * width + block.x; const index = reversedY * width + block.x;
// setAt() implementation - LitematicaBitArray.java
const startOffset = index * requiredBits; const startOffset = index * requiredBits;
const startArrayIndex = Math.floor(startOffset / 64); const startArrayIndex = Math.floor(startOffset / 64);
const endArrayIndex = ((index + 1) * requiredBits - 1) >> 6; const endArrayIndex = ((index + 1) * requiredBits - 1) >> 6;

View file

@ -20,7 +20,7 @@ function FileMenu() {
<MenubarMenu> <MenubarMenu>
<MenubarTrigger>File</MenubarTrigger> <MenubarTrigger>File</MenubarTrigger>
<MenubarContent> <MenubarContent>
<MenubarItem>Open Schematic</MenubarItem> <MenubarItem onClick={() => openDialog("OpenSchematic")}>Open Schematic</MenubarItem>
<MenubarItem onClick={() => openDialog("OpenImage")}>Open Image</MenubarItem> <MenubarItem onClick={() => openDialog("OpenImage")}>Open Image</MenubarItem>
<MenubarSeparator /> <MenubarSeparator />