diff --git a/.github/workflows/ntfy.yaml b/.github/workflows/ntfy.yaml
index 4aef1a9..32d99b2 100644
--- a/.github/workflows/ntfy.yaml
+++ b/.github/workflows/ntfy.yaml
@@ -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 }}" \
diff --git a/src/components/dialogs/OpenImage.tsx b/src/components/dialogs/OpenImage.tsx
index 0b2de31..0a08b23 100644
--- a/src/components/dialogs/OpenImage.tsx
+++ b/src/components/dialogs/OpenImage.tsx
@@ -152,8 +152,12 @@ function OpenImage({ close }: DialogProps) {
})}
>
-
-
Drag and drop your image here or click to open
+
+
+ Drag and drop your image here
+
+ or click to open
+
diff --git a/src/components/dialogs/OpenSchematic.tsx b/src/components/dialogs/OpenSchematic.tsx
new file mode 100644
index 0000000..3f0b69b
--- /dev/null
+++ b/src/components/dialogs/OpenSchematic.tsx
@@ -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
;
+}
+
+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).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 (
+
+
+ Open Schematic
+ Open your schematic file to load into the canvas
+
+
+
+
+
+
+ Drag and drop your schematic file here
+
+ or click to open
+
+
+
+
+
+
+
+
+ );
+}
+
+export default OpenSchematic;
diff --git a/src/components/dialogs/SaveLitematic.tsx b/src/components/dialogs/SaveLitematic.tsx
index 640d8e2..2043a2a 100644
--- a/src/components/dialogs/SaveLitematic.tsx
+++ b/src/components/dialogs/SaveLitematic.tsx
@@ -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 } = {
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;
diff --git a/src/components/menubar/FileMenu.tsx b/src/components/menubar/FileMenu.tsx
index e2d0300..b16cb13 100644
--- a/src/components/menubar/FileMenu.tsx
+++ b/src/components/menubar/FileMenu.tsx
@@ -20,7 +20,7 @@ function FileMenu() {
File
- Open Schematic
+ openDialog("OpenSchematic")}>Open Schematic
openDialog("OpenImage")}>Open Image