right panel
This commit is contained in:
@ -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={
|
||||||
|
@ -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={() => {
|
||||||
|
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 { 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>
|
||||||
</>
|
</>
|
||||||
|
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 { 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>
|
||||||
|
@ -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: [
|
||||||
|
Reference in New Issue
Block a user