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
|
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 }}" \
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
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(
|
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;
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue