diff --git a/package.json b/package.json index 1c856ba..60f013f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@pixi/react": "7", "@pixi/tilemap": "4.1.0", + "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-menubar": "^1.1.2", @@ -19,6 +20,7 @@ "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slider": "^1.2.2", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.6", diff --git a/src/components/dialogs/OpenImage.tsx b/src/components/dialogs/OpenImage.tsx index 9f3af12..d810484 100644 --- a/src/components/dialogs/OpenImage.tsx +++ b/src/components/dialogs/OpenImage.tsx @@ -1,17 +1,29 @@ -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import { useDropzone } from "react-dropzone"; -import { CircleAlertIcon, UploadIcon } from "lucide-react"; - -import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; +import { CircleAlertIcon, LinkIcon, UploadIcon } from "lucide-react"; +import { CanvasContext } from "@/context/Canvas"; import { ImageContext } from "@/context/Image"; import { LoadingContext } from "@/context/Loading"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { CheckedState } from "@radix-ui/react-checkbox"; +import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Toggle } from "@/components/ui/toggle"; + +import { getBlockData } from "@/utils/getBlockData"; + +import BlockSelector from "./open-image/BlockSelector"; + function OpenImage({ close }: DialogProps) { + const { version } = useContext(CanvasContext); const { setLoading } = useContext(LoadingContext); const { setImage: setContextImage, setImageDimensions: setContextImageDimensions } = useContext(ImageContext); @@ -21,9 +33,24 @@ function OpenImage({ close }: DialogProps) { }, }); + const divRef = useRef(null); + const [image, setImage] = useState(); const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 }); const [aspectRatio, setAspectRatio] = useState(1); + const [linkAspectRatio, setLinkAspectRatio] = useState(true); + + const [searchInput, setSearchInput] = useState(""); + const [stageWidth, setStageWidth] = useState(0); + + const [selectedBlocks, setSelectedBlocks] = useState(["stone"]); + const [blockTypeCheckboxesChecked, setBlockTypeCheckboxesChecked] = useState({ + creative: false, + tile_entity: false, + fallable: false, + }); + + const blockData = getBlockData(version); useEffect(() => { if (acceptedFiles[0]) { @@ -41,15 +68,27 @@ function OpenImage({ close }: DialogProps) { const newDimension = Number(e.target.value); if (newDimension < 1 || newDimension > 10000) return; - setImageDimensions(() => { - if (isWidth) { - return { width: newDimension, height: Math.round(newDimension / aspectRatio) }; - } else { - return { width: Math.round(newDimension * aspectRatio), height: newDimension }; - } + setImageDimensions((prev) => { + if (isWidth) + return linkAspectRatio ? { width: newDimension, height: Math.round(newDimension / aspectRatio) } : { ...prev, width: newDimension }; + return linkAspectRatio ? { width: Math.round(newDimension * aspectRatio), height: newDimension } : { ...prev, height: newDimension }; }); }; + const onBlockTypeCheckedChange = (checked: CheckedState, property: keyof BlockData[string]) => { + const blocksWithProperty = Object.entries(blockData) + .filter(([, data]) => data[property] === true) + .map(([blockName]) => blockName); + + if (checked) { + setSelectedBlocks((prev) => [...prev, ...blocksWithProperty]); + } else { + setSelectedBlocks((prev) => prev.filter((block) => !blocksWithProperty.includes(block))); + } + + setBlockTypeCheckboxesChecked((prev) => ({ ...prev, [property]: checked })); + }; + const onSubmit = () => { if (image) { setLoading(true); @@ -63,74 +102,176 @@ function OpenImage({ close }: DialogProps) { } }; + useEffect(() => { + Object.keys(blockTypeCheckboxesChecked).forEach((property) => { + const blocksWithProperty = Object.entries(blockData) + .filter(([, data]) => data[property as keyof BlockData[string]] === true) + .map(([blockName]) => blockName); + + const propertyChecked = blocksWithProperty.every((block) => selectedBlocks.includes(block)); + setBlockTypeCheckboxesChecked((prev) => ({ ...prev, [property]: propertyChecked })); + }); + }, [selectedBlocks]); + + useEffect(() => { + if (!divRef.current) return; + setStageWidth(divRef.current.clientWidth); + + console.log(stageWidth); + }, []); + return ( - + Open Image Open your image to load as blocks into the canvas -
-
- - -

Drag and drop your image here

-
+
+ + + Image + Blocks + -
- {image && acceptedFiles[0] && ( - <> - your image + +
+ + +

Drag and drop your image here or click to open

+
-
-
- -

{acceptedFiles[0].name}

-
- -
- - onDimensionChange(e, true)} /> -
- -
- - onDimensionChange(e, false)} +
+ {image && acceptedFiles[0] && ( + <> + your image + +
+
+ +

+ {acceptedFiles[0].name} +

+
+ +
+
+ + onDimensionChange(e, true)} + /> +
+ + setLinkAspectRatio(!linkAspectRatio)} + className="h-8 !min-w-8 p-0 mt-auto mb-1" + > + + + +
+ + onDimensionChange(e, false)} + /> +
+
+ + {imageDimensions.height > 384 && ( +
+ + The height is above 384 blocks! +
+ )} +
+ + )} +
+ + + +
+
+
+ onBlockTypeCheckedChange(value, "creative")} + /> + +
+
+ onBlockTypeCheckedChange(value, "tile_entity")} + /> + +
+
+ onBlockTypeCheckedChange(value, "fallable")} + /> +
- - )} -
+ +
+ + +
+
+ + + + setSearchInput(e.target.value)} /> + + + + + +
- - {imageDimensions.height > 384 && ( -
- - The height is above 384 blocks! -
- )} + + {/* todo: add version selector here */} diff --git a/src/components/dialogs/open-image/BlockSelector.tsx b/src/components/dialogs/open-image/BlockSelector.tsx new file mode 100644 index 0000000..82a0ea9 --- /dev/null +++ b/src/components/dialogs/open-image/BlockSelector.tsx @@ -0,0 +1,95 @@ +import React, { useContext, useMemo, useState } from "react"; +import { Container, Graphics, Sprite, Stage } from "@pixi/react"; + +import { CanvasContext } from "@/context/Canvas"; +import { TexturesContext } from "@/context/Textures"; +import { ThemeContext } from "@/context/Theme"; + +import { getBlockData } from "@/utils/getBlockData"; + +interface Props { + stageWidth: number; + searchInput: string; + selectedBlocks: string[]; + setSelectedBlocks: React.Dispatch>; +} + +function BlockSelector({ stageWidth, searchInput, selectedBlocks, setSelectedBlocks }: Props) { + const { version } = useContext(CanvasContext); + const { missingTexture, textures } = useContext(TexturesContext); + const { isDark } = useContext(ThemeContext); + + const blockData = getBlockData(version); + + const [hoverPosition, setHoverPosition] = useState(null); + + const filteredBlocks = useMemo(() => Object.keys(blockData).filter((value) => value.includes(searchInput)), [searchInput, blockData]); + const blocksPerColumn = Math.floor(462 / (32 + 2)); + + const onClick = (block: string) => { + if (selectedBlocks.includes(block)) { + setSelectedBlocks((prev) => prev.filter((blockName) => blockName !== block)); + } else { + setSelectedBlocks((prev) => [...prev, block]); + } + }; + + return ( + setHoverPosition(null)} + > + + {filteredBlocks.map((block, index) => { + const texture = textures[`${block}.png`] ?? missingTexture; + const x = (index % blocksPerColumn) * (32 + 2) + 2; + const y = Math.floor(index / blocksPerColumn) * (32 + 2) + 2; + + return ( + <> + setHoverPosition({ x, y })} + click={() => onClick(block)} + /> + + {selectedBlocks.includes(block) && ( + { + g.clear(); + g.beginFill(0x000000, 0.5); + g.lineStyle(2, isDark ? 0xffffff : 0x000000, 1, 1); + g.drawRect(0, 0, 32, 32); + }} + /> + )} + + ); + })} + + {hoverPosition && ( + { + g.clear(); + g.lineStyle(2, isDark ? 0xffffff : 0x000000, 1, 1); + g.drawRect(0, 0, 32, 32); + }} + /> + )} + + + ); +} + +export default BlockSelector; diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..f6ee516 --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..5bedb73 --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/src/components/ui/toggle.tsx b/src/components/ui/toggle.tsx index 14b0108..d48aa62 100644 --- a/src/components/ui/toggle.tsx +++ b/src/components/ui/toggle.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import * as TogglePrimitive from "@radix-ui/react-toggle" import { cva, type VariantProps } from "class-variance-authority" diff --git a/src/context/Textures.tsx b/src/context/Textures.tsx index fd17a17..85a2f41 100644 --- a/src/context/Textures.tsx +++ b/src/context/Textures.tsx @@ -27,7 +27,7 @@ export const TexturesProvider = ({ children }: Props) => { const [solidTextures, setSolidTextures] = useState>({}); useEffect(() => { - // Load missing texture + // Load missing texture through data string just incase of network errors const missingBaseTexture = new PIXI.BaseTexture( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGUlEQVR42mPABX4w/MCKaKJhVMPgcOuoBgDZRfgBVl5QdQAAAABJRU5ErkJggg==" );