feat: open schematic (.litematic)
known bug: blobs of air blocks in some schematics
This commit is contained in:
parent
8726ffeedd
commit
612d519068
5 changed files with 174 additions and 12 deletions
13
.github/workflows/ntfy.yaml
vendored
13
.github/workflows/ntfy.yaml
vendored
|
|
@ -1,16 +1,13 @@
|
|||
name: Ntfy Deployment Notifications
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
name: Deployment Notifications
|
||||
on: [deployment_status]
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
if: failure() || success()
|
||||
if: github.event.deployment_status.state == "failure" | github.event.deployment_status.state == "success"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify on Success
|
||||
if: success()
|
||||
if: github.event.deployment_status.state == "success"
|
||||
run: |
|
||||
curl \
|
||||
-H "Authorization: Bearer ${{ secrets.NTFY_TOKEN }}" \
|
||||
|
|
@ -21,7 +18,7 @@ jobs:
|
|||
${{ secrets.NTFY_URL }}
|
||||
|
||||
- name: Notify on Failure
|
||||
if: failure()
|
||||
if: github.event.deployment_status.state == "failure"
|
||||
run: |
|
||||
curl \
|
||||
-H "Authorization: Bearer ${{ secrets.NTFY_TOKEN }}" \
|
||||
|
|
|
|||
|
|
@ -152,8 +152,12 @@ function OpenImage({ close }: DialogProps) {
|
|||
})}
|
||||
>
|
||||
<input {...getInputProps({ multiple: false })} />
|
||||
<UploadIcon />
|
||||
<p>Drag and drop your image here or click to open</p>
|
||||
<UploadIcon size={30} />
|
||||
<p className="text-center">
|
||||
Drag and drop your image here
|
||||
<br />
|
||||
or click to open
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[auto,1fr] gap-2">
|
||||
|
|
|
|||
160
src/components/dialogs/OpenSchematic.tsx
Normal file
160
src/components/dialogs/OpenSchematic.tsx
Normal 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;
|
||||
|
|
@ -47,7 +47,6 @@ function SaveLitematic({ close }: DialogProps) {
|
|||
new Set(
|
||||
blocks.map((block) => {
|
||||
const blockInfo = blockData[block.name.replace("minecraft:", "")];
|
||||
|
||||
const returnData: { Name: string; Properties?: Record<string, string> } = {
|
||||
Name: `minecraft:${blockInfo.id[0]}`,
|
||||
...(blockInfo.properties ? { Properties: blockInfo.properties } : {}),
|
||||
|
|
@ -72,6 +71,8 @@ function SaveLitematic({ close }: DialogProps) {
|
|||
|
||||
const reversedY = height - 1 - block.y;
|
||||
const index = reversedY * width + block.x;
|
||||
|
||||
// setAt() implementation - LitematicaBitArray.java
|
||||
const startOffset = index * requiredBits;
|
||||
const startArrayIndex = Math.floor(startOffset / 64);
|
||||
const endArrayIndex = ((index + 1) * requiredBits - 1) >> 6;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ function FileMenu() {
|
|||
<MenubarMenu>
|
||||
<MenubarTrigger>File</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarItem>Open Schematic</MenubarItem>
|
||||
<MenubarItem onClick={() => openDialog("OpenSchematic")}>Open Schematic</MenubarItem>
|
||||
<MenubarItem onClick={() => openDialog("OpenImage")}>Open Image</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue