right panel

This commit is contained in:
2023-12-15 17:03:07 +08:00
parent dad2f61aaa
commit ca56a184b2
8 changed files with 171 additions and 138 deletions

View File

@ -1,9 +1,5 @@
import "./App.css"; import "./App.css";
import ThreeDesigner from "@/components/ThreeDesigner.tsx";
import { ChakraProvider, CircularProgress } from "@chakra-ui/react"; import { ChakraProvider, CircularProgress } from "@chakra-ui/react";
import AreaIndicator from "@/components/AreaIndicator.tsx";
import TextureSelector from "@/components/TextureSelector.tsx";
import useModelStore from "@/store/useModelStore.ts"; import useModelStore from "@/store/useModelStore.ts";
import ThreeScene from "@/components/ThreeScene.tsx"; import ThreeScene from "@/components/ThreeScene.tsx";
import RightPanel from "@/components/RightPanel.tsx"; import RightPanel from "@/components/RightPanel.tsx";
@ -20,8 +16,7 @@ function App() {
<div className={"flex-[2] shrink-0 bg-blue-50 p-4"}> <div className={"flex-[2] shrink-0 bg-blue-50 p-4"}>
<RightPanel /> <RightPanel />
</div> </div>
<AreaIndicator /> {/* <AreaIndicator /> */}
<TextureSelector />
{modelLoading !== 0 && modelLoading !== 100 && ( {modelLoading !== 0 && modelLoading !== 100 && (
<div <div
className={ className={

View File

@ -1,99 +1,18 @@
import { models } from "@/constant/models.ts"; import { models } from "@/constant/models.ts";
import useModelStore from "@/store/useModelStore.ts"; import useModelStore from "@/store/useModelStore.ts";
import styles from "./AreaIndicator.module.scss"; import styles from "./AreaIndicator.module.scss";
import {
Button,
ButtonGroup,
Icon,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverFooter,
PopoverHeader,
PopoverTrigger,
} from "@chakra-ui/react";
import { CgArrowsExchange } from "react-icons/cg";
import { useState } from "react";
import { DecalGeometry } from "three/addons/geometries/DecalGeometry.js";
const AreaIndicator = () => { const AreaIndicator = () => {
const activeModel = useModelStore((state) => state.activeModel); const activeModel = useModelStore((state) => state.activeModel);
const setActiveModel = useModelStore((state) => state.setActiveModel);
const activeArea = useModelStore((state) => state.activeArea); const activeArea = useModelStore((state) => state.activeArea);
const setActiveArea = useModelStore((state) => state.setActiveArea); const setActiveArea = useModelStore((state) => state.setActiveArea);
const [showModelPicker, setShowModelPicker] = useState(false);
return ( return (
<div <div
className={ className={
"absolute left-2 top-1/2 flex -translate-y-1/2 flex-col items-center justify-center rounded bg-red-200 p-2 shadow-lg transition-shadow hover:shadow-2xl" "absolute left-2 top-1/2 flex -translate-y-1/2 flex-col items-center justify-center rounded bg-red-200 p-2 shadow-lg transition-shadow hover:shadow-2xl"
} }
> >
<div className={"flex"}>
<div className={"flex flex-col"}>
<div className={"text-center text-[10px]"}></div>
<IconButton
className={"mt-1"}
onClick={() => {
setShowModelPicker(() => true);
}}
isRound={true}
boxShadow={"lg"}
aria-label={"switch"}
icon={<Icon boxSize={8} as={CgArrowsExchange} />}
/>
</div>
<Popover
closeOnBlur={false}
placement={"right"}
onClose={() => {
setShowModelPicker(() => false);
}}
isOpen={showModelPicker}
>
<PopoverTrigger>
<div className={"w-0"}></div>
</PopoverTrigger>
<PopoverContent>
<PopoverHeader fontWeight="semibold"></PopoverHeader>
<PopoverArrow />
<PopoverCloseButton />
<PopoverBody>
<div className={"max-h-48 overflow-y-auto p-1"}>
{models.map((el, index) => (
<div
onClick={() => {
setActiveModel(index);
setShowModelPicker(() => false);
}}
className={`${styles.modelItem} ${
activeModel === index ? "bg-blue-50" : ""
} flex h-10 cursor-pointer items-center rounded px-3 shadow transition-shadow hover:shadow-lg`}
key={el.path}
>
{el.name}
</div>
))}
</div>
</PopoverBody>
<PopoverFooter display="flex" justifyContent="flex-end">
<ButtonGroup size="sm">
<Button
variant="outline"
onClick={() => {
setShowModelPicker(() => false);
}}
>
</Button>
</ButtonGroup>
</PopoverFooter>
</PopoverContent>
</Popover>
</div>
{models[activeModel].mesh.map((item, index) => ( {models[activeModel].mesh.map((item, index) => (
<div <div
onClick={() => { onClick={() => {

View File

@ -0,0 +1,35 @@
import { useState } from "react"
import { AddIcon } from "@chakra-ui/icons"
import { Button, Modal, ModalBody, ModalContent, ModalOverlay } from "@chakra-ui/react"
const LogoPanel = () => {
const [open, setOpen] = useState(false)
return (
<>
<div className="flex justify-center">
<Button colorScheme='messenger' onClick={() => {
setOpen(() => true)
}} leftIcon={<AddIcon />}>LOGO</Button>
</div>
<Modal isOpen={open} onClose={() => {
setOpen(() => false)
}}>
<ModalOverlay />
<ModalContent>
<ModalBody>
<div className="flex justify-center p-4">
<div onClick={() => {
}} className={"w-24 border-2 border-gray-300 rounded-lg aspect-square flex justify-center items-center cursor-pointer"}>
<AddIcon />
</div>
</div>
</ModalBody>
</ModalContent>
</Modal>
</>
)
}
export default LogoPanel

View File

@ -1,9 +1,12 @@
import { Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; import { Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
import { models } from "@/constant/models.ts"; import { models } from "@/constant/models.ts";
import useModelStore from "@/store/useModelStore.ts"; import useModelStore from "@/store/useModelStore.ts";
import TextureSelectorPanel from "./TextureSelectorPanel";
import LogoPanel from "./LogoPanel";
const RightPanel = () => { const RightPanel = () => {
const activeModel = useModelStore(state => state.activeModel); const activeModel = useModelStore(state => state.activeModel);
const setActiveModel = useModelStore(state => state.setActiveModel);
return ( return (
<> <>
<Tabs variant={"soft-rounded"}> <Tabs variant={"soft-rounded"}>
@ -17,13 +20,21 @@ const RightPanel = () => {
<TabPanel> <TabPanel>
<div className={"w-full grid grid-cols-6 gap-4"}> <div className={"w-full grid grid-cols-6 gap-4"}>
{models.map((model, index) => <div key={model.path} {models.map((model, index) => <div key={model.path}
onClick={()=>{}} onClick={() => {
className={`${activeModel === index ? "border-green-300 border-2" : ""} flex justify-center items-center aspect-square bg-blue-100 rounded-xl`}>{model.name}</div>)} setActiveModel(index)
}}
className={`${activeModel === index ? "border-green-300 border-2" : ""} flex justify-center items-center aspect-square bg-blue-100 overflow-hidden rounded-xl`}>
<img className="w-full h-full object-cover" src={models[activeModel].icon} />
</div>)}
</div> </div>
</TabPanel> </TabPanel>
<TabPanel>2</TabPanel> <TabPanel>
<TextureSelectorPanel />
</TabPanel>
<TabPanel>3</TabPanel> <TabPanel>3</TabPanel>
<TabPanel>4</TabPanel> <TabPanel>
<LogoPanel />
</TabPanel>
</TabPanels> </TabPanels>
</Tabs> </Tabs>
</> </>

View File

@ -0,0 +1,8 @@
const Sticker = () => {
return (
<>
</>
)
}
export default Sticker

View File

@ -0,0 +1,35 @@
import { models } from "@/constant/models"
import { textures } from "@/constant/textures"
import useModelStore from "@/store/useModelStore"
function TextureSelectorPanel() {
const activeTextures = useModelStore(state => state.activeTextures)
const activeArea = useModelStore(state => state.activeArea)
const setActiveArea = useModelStore(state => state.setActiveArea)
const activeModel = useModelStore(state => state.activeModel)
const setActiveTextures = useModelStore(state => state.setActiveTextures)
return (
<>
<h2 className={"text-sm font-bold"}></h2>
<div className="mt-2 grid grid-cols-8 gap-3">
{models[activeModel].mesh.map((mesh, index) => <div onClick={() => {
setActiveArea(index)
}} className={`${activeArea === index ? "border-2 border-green-300" : ""} aspect-square cursor-pointer rounded overflow-hidden`} key={mesh.name}>
<img src={mesh.icon} className="object-cover w-full h-full" />
</div>)}
</div>
<h2 className={"mt-3 text-sm font-bold"}></h2>
<div className="mt-2 grid grid-cols-6 gap-3">
{textures.map((texture, index) => <div key={texture.path}
onClick={() => {
setActiveTextures(activeArea, index)
}}
className={`${activeTextures[activeArea] === index ? "border-2 border-green-300" : ""} rounded overflow-hidden cursor-pointer`}>
<img className={"w-full h-full object-cover"} src={texture.path} />
</div>)}
</div>
</>
)
}
export default TextureSelectorPanel

View File

@ -1,20 +1,36 @@
import { Canvas } from "@react-three/fiber"; import { Canvas } from "@react-three/fiber";
import { useState } from "react"; import { useEffect, useRef, useState } from "react";
import { TextureLoader, Vector3 } from "three"; import { TextureLoader, Vector3 } from "three";
import { Decal, Environment, OrbitControls, useTexture } from "@react-three/drei"; import { Decal, Environment, OrbitControls, useTexture } from "@react-three/drei";
import useModelStore from "@/store/useModelStore.ts"; import useModelStore from "@/store/useModelStore.ts";
import { models } from "@/constant/models.ts"; import { models } from "@/constant/models.ts";
import { useGLTF } from "@react-three/drei/core/useGLTF"; import { useGLTF } from "@react-three/drei/core/useGLTF";
import { textures } from "@/constant/textures.ts"; import { textures } from "@/constant/textures.ts";
import { fabric } from 'fabric';
const CapModel = () => { const CapModel = () => {
const activeModel = useModelStore(state => state.activeModel); const activeModel = useModelStore(state => state.activeModel);
// const activeArea = useModelStore(state => state.activeArea); // const activeArea = useModelStore(state => state.activeArea);
const activeTextures = useModelStore(state => state.activeTextures); const activeTextures = useModelStore(state => state.activeTextures);
const [pos, setPos] = useState<Vector3>(new Vector3(0, 0, 0)); const [pos, setPos] = useState<Vector3>(new Vector3(0, 0, 0));
// const canvasRef = useRef<string | undefined>()
const [logoUrl, setLogoUrl] = useState<string | undefined>()
const logo = useTexture("/textures/archlogo.png"); const logo = useTexture("/textures/archlogo.png");
useEffect(() => {
const canvas = document.createElement("canvas")
const fabricCanvas = new fabric.Canvas(canvas)
const text = new fabric.Text("archlinux")
fabricCanvas.add(text)
setLogoUrl(() => canvas.toDataURL())
// canvasRef.current = canvas.toDataURL()
return () => {
// canvasRef.current = undefined
fabricCanvas.dispose()
}
})
const { nodes } = useGLTF(models[activeModel].path); const { nodes } = useGLTF(models[activeModel].path);
return ( return (
<>
<group scale={models[activeModel].scale}> <group scale={models[activeModel].scale}>
{Object.keys(nodes).map((keyName) => { {Object.keys(nodes).map((keyName) => {
if (nodes[keyName].type === "Mesh") { if (nodes[keyName].type === "Mesh") {
@ -23,7 +39,9 @@ const CapModel = () => {
<mesh <mesh
key={keyName} key={keyName}
onClick={(ev) => { onClick={(ev) => {
ev.stopPropagation()
const { x, y, z } = ev.point; const { x, y, z } = ev.point;
const canvas = new fabric.Canvas("canvas")
setPos(new Vector3(x / models[activeModel].scale, y / models[activeModel].scale, z / models[activeModel].scale)); setPos(new Vector3(x / models[activeModel].scale, y / models[activeModel].scale, z / models[activeModel].scale));
}} }}
castShadow castShadow
@ -36,7 +54,14 @@ const CapModel = () => {
{areaIndex > -1 ? {areaIndex > -1 ?
<meshStandardMaterial map={new TextureLoader().load(textures[activeTextures[areaIndex]].path)} /> <meshStandardMaterial map={new TextureLoader().load(textures[activeTextures[areaIndex]].path)} />
: <meshStandardMaterial color="gray" />} : <meshStandardMaterial color="gray" />}
<Decal position={pos} scale={0.05} rotation={Math.PI}> <Decal position={pos} scale={0.05}
onClick={(ev) => {
ev.stopPropagation()
console.log(ev);
}}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
rotation={Math.PI}>
<meshPhysicalMaterial <meshPhysicalMaterial
transparent transparent
polygonOffset polygonOffset
@ -58,17 +83,20 @@ const CapModel = () => {
} }
})} })}
</group> </group>
<OrbitControls key={activeModel} />
</>
); );
}; };
const ThreeScene = () => { const ThreeScene = () => {
// const activeModel = useModelStore(state => state.activeModel)
return ( return (
<Canvas> <Canvas>
<ambientLight></ambientLight> <ambientLight></ambientLight>
<pointLight position={[10, 10, 10]}></pointLight> <pointLight position={[10, 10, 10]}></pointLight>
<perspectiveCamera /> <perspectiveCamera />
<OrbitControls /> {/* {activeModel > -1 && } */}
<CapModel /> <CapModel />
<Environment preset={"city"} background={false} /> <Environment preset={"city"} background={false} />
</Canvas> </Canvas>

View File

@ -2,6 +2,7 @@ export const models = [
{ {
name: "版型1", name: "版型1",
path: "/models/baseball_cap_3d.glb", path: "/models/baseball_cap_3d.glb",
icon: "/icon/baseball_cap_3d.png",
scale: 18, scale: 18,
mesh: [ mesh: [
{ {
@ -48,6 +49,7 @@ export const models = [
}, },
{ {
name: "版型2", name: "版型2",
icon: "/icon/baseball_cap_3d_logo.png",
path: "/models/baseball_cap.glb", path: "/models/baseball_cap.glb",
scale: 0.02, scale: 0.02,
mesh: [ mesh: [