right panel
This commit is contained in:
@ -1,9 +1,5 @@
|
||||
import "./App.css";
|
||||
import ThreeDesigner from "@/components/ThreeDesigner.tsx";
|
||||
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 ThreeScene from "@/components/ThreeScene.tsx";
|
||||
import RightPanel from "@/components/RightPanel.tsx";
|
||||
@ -20,8 +16,7 @@ function App() {
|
||||
<div className={"flex-[2] shrink-0 bg-blue-50 p-4"}>
|
||||
<RightPanel />
|
||||
</div>
|
||||
<AreaIndicator />
|
||||
<TextureSelector />
|
||||
{/* <AreaIndicator /> */}
|
||||
{modelLoading !== 0 && modelLoading !== 100 && (
|
||||
<div
|
||||
className={
|
||||
|
@ -1,99 +1,18 @@
|
||||
import { models } from "@/constant/models.ts";
|
||||
import useModelStore from "@/store/useModelStore.ts";
|
||||
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 activeModel = useModelStore((state) => state.activeModel);
|
||||
const setActiveModel = useModelStore((state) => state.setActiveModel);
|
||||
const activeArea = useModelStore((state) => state.activeArea);
|
||||
const setActiveArea = useModelStore((state) => state.setActiveArea);
|
||||
const [showModelPicker, setShowModelPicker] = useState(false);
|
||||
return (
|
||||
<div
|
||||
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"
|
||||
}
|
||||
>
|
||||
<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) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
|
35
src/components/LogoPanel.tsx
Normal file
35
src/components/LogoPanel.tsx
Normal 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
|
@ -1,9 +1,12 @@
|
||||
import { Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
|
||||
import { models } from "@/constant/models.ts";
|
||||
import useModelStore from "@/store/useModelStore.ts";
|
||||
import TextureSelectorPanel from "./TextureSelectorPanel";
|
||||
import LogoPanel from "./LogoPanel";
|
||||
|
||||
const RightPanel = () => {
|
||||
const activeModel = useModelStore(state => state.activeModel);
|
||||
const setActiveModel = useModelStore(state => state.setActiveModel);
|
||||
return (
|
||||
<>
|
||||
<Tabs variant={"soft-rounded"}>
|
||||
@ -17,13 +20,21 @@ const RightPanel = () => {
|
||||
<TabPanel>
|
||||
<div className={"w-full grid grid-cols-6 gap-4"}>
|
||||
{models.map((model, index) => <div key={model.path}
|
||||
onClick={()=>{}}
|
||||
className={`${activeModel === index ? "border-green-300 border-2" : ""} flex justify-center items-center aspect-square bg-blue-100 rounded-xl`}>{model.name}</div>)}
|
||||
onClick={() => {
|
||||
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>
|
||||
</TabPanel>
|
||||
<TabPanel>2</TabPanel>
|
||||
<TabPanel>
|
||||
<TextureSelectorPanel />
|
||||
</TabPanel>
|
||||
<TabPanel>3</TabPanel>
|
||||
<TabPanel>4</TabPanel>
|
||||
<TabPanel>
|
||||
<LogoPanel />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</>
|
||||
|
8
src/components/Sticker.tsx
Normal file
8
src/components/Sticker.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
const Sticker = () => {
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Sticker
|
35
src/components/TextureSelectorPanel.tsx
Normal file
35
src/components/TextureSelectorPanel.tsx
Normal 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
|
@ -1,20 +1,36 @@
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { TextureLoader, Vector3 } from "three";
|
||||
import { Decal, Environment, OrbitControls, useTexture } from "@react-three/drei";
|
||||
import useModelStore from "@/store/useModelStore.ts";
|
||||
import { models } from "@/constant/models.ts";
|
||||
import { useGLTF } from "@react-three/drei/core/useGLTF";
|
||||
import { textures } from "@/constant/textures.ts";
|
||||
import { fabric } from 'fabric';
|
||||
|
||||
const CapModel = () => {
|
||||
const activeModel = useModelStore(state => state.activeModel);
|
||||
// const activeArea = useModelStore(state => state.activeArea);
|
||||
const activeTextures = useModelStore(state => state.activeTextures);
|
||||
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");
|
||||
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);
|
||||
return (
|
||||
<>
|
||||
<group scale={models[activeModel].scale}>
|
||||
{Object.keys(nodes).map((keyName) => {
|
||||
if (nodes[keyName].type === "Mesh") {
|
||||
@ -23,7 +39,9 @@ const CapModel = () => {
|
||||
<mesh
|
||||
key={keyName}
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation()
|
||||
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));
|
||||
}}
|
||||
castShadow
|
||||
@ -36,7 +54,14 @@ const CapModel = () => {
|
||||
{areaIndex > -1 ?
|
||||
<meshStandardMaterial map={new TextureLoader().load(textures[activeTextures[areaIndex]].path)} />
|
||||
: <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
|
||||
transparent
|
||||
polygonOffset
|
||||
@ -58,17 +83,20 @@ const CapModel = () => {
|
||||
}
|
||||
})}
|
||||
</group>
|
||||
<OrbitControls key={activeModel} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const ThreeScene = () => {
|
||||
// const activeModel = useModelStore(state => state.activeModel)
|
||||
return (
|
||||
<Canvas>
|
||||
<ambientLight></ambientLight>
|
||||
<pointLight position={[10, 10, 10]}></pointLight>
|
||||
<perspectiveCamera />
|
||||
<OrbitControls />
|
||||
{/* {activeModel > -1 && } */}
|
||||
<CapModel />
|
||||
<Environment preset={"city"} background={false} />
|
||||
</Canvas>
|
||||
|
@ -2,6 +2,7 @@ export const models = [
|
||||
{
|
||||
name: "版型1",
|
||||
path: "/models/baseball_cap_3d.glb",
|
||||
icon: "/icon/baseball_cap_3d.png",
|
||||
scale: 18,
|
||||
mesh: [
|
||||
{
|
||||
@ -48,6 +49,7 @@ export const models = [
|
||||
},
|
||||
{
|
||||
name: "版型2",
|
||||
icon: "/icon/baseball_cap_3d_logo.png",
|
||||
path: "/models/baseball_cap.glb",
|
||||
scale: 0.02,
|
||||
mesh: [
|
||||
|
Reference in New Issue
Block a user