This commit is contained in:
2023-12-21 17:29:10 +08:00
parent ec316824b7
commit 18fd617a01
9 changed files with 367 additions and 348 deletions

View File

@ -9,14 +9,12 @@ function App() {
return (
<ChakraProvider>
<div className={"relative flex h-screen w-screen"}>
{/*<ThreeDesigner />*/}
<div className={"flex-[5] shrink-0"}>
<ThreeScene />
</div>
<div className={"flex-[2] shrink-0 bg-blue-50 p-4"}>
<RightPanel />
</div>
{/* <AreaIndicator /> */}
{modelLoading !== 0 && modelLoading !== 100 && (
<div
className={

View File

@ -1,65 +0,0 @@
import { Object3D, TextureLoader, Vector3 } from "three";
import useModelStore from "@/store/useModelStore.ts";
import { useState } from "react";
import { models } from "@/constant/models.ts";
import { textures } from "@/constant/textures.ts";
const CapMesh = ({
areaIndex,
mesh,
}: {
areaIndex: number;
mesh: Object3D;
}) => {
const logo = useTexture("/textures/archlogo.png");
const activeModel = useModelStore((state) => state.activeModel);
const activeTextures = useModelStore((state) => state.activeTextures);
const [pos, setPos] = useState<Vector3>(new Vector3(0, 0, 0));
return (
<mesh
onClick={(ev) => {
const { x, y, z } = ev.point;
setPos(
new Vector3(
x / models[activeModel].scale,
y / models[activeModel].scale,
z / models[activeModel].scale,
),
);
}}
castShadow
receiveShadow
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
geometry={mesh.geometry}
dispose={null}
>
{areaIndex > -1 ? (
<meshStandardMaterial
map={new TextureLoader().load(
textures[activeTextures[areaIndex]].path,
)}
/>
) : (
<meshStandardMaterial color="gray" />
)}
<Decal position={pos} scale={0.05} rotation={Math.PI}>
<meshPhysicalMaterial
transparent
polygonOffset
polygonOffsetFactor={-10}
map={logo}
map-flipY={false}
map-anisotropy={16}
iridescence={1}
iridescenceIOR={1}
iridescenceThicknessRange={[0, 1400]}
roughness={1}
clearcoat={0.5}
metalness={0.75}
toneMapped={false}
/>
</Decal>
</mesh>
);
};

View File

@ -1,9 +1,5 @@
const DebugPanel = () => {
return (
<div>DebugPanel</div>
)
}
return <div>DebugPanel</div>;
};
export default DebugPanel
export default DebugPanel;

View File

@ -1,94 +1,108 @@
import { useState } from "react";
import { AddIcon, DeleteIcon } from "@chakra-ui/icons";
import {
Button,
Modal,
ModalBody,
ModalContent,
ModalOverlay,
Image,
IconButton
Button,
Modal,
ModalBody,
ModalContent,
ModalOverlay,
Image,
IconButton,
} from "@chakra-ui/react";
import { pickFile } from "@/lib/upload";
import useModelStore, { StickerType } from "@/store/useModelStore";
import { v4 as uuidv4 } from 'uuid'
import { v4 as uuidv4 } from "uuid";
import { Vector3 } from "three";
const LogoPanel = () => {
const [open, setOpen] = useState(false);
const decals = useModelStore((state) => state.decals);
const setDecals = useModelStore((state) => state.setDecals);
const setActiveDecal = useModelStore((state) => state.setActiveDecal);
const activeDecal = useModelStore((state) => state.activeDecal);
return (
<>
<ul>
{decals
.filter((el) => el.type === StickerType.logo)
.map((decal) => (
<li
onClick={() => {
setActiveDecal(decal.id)
}}
key={decal.id}
className={`mb-2 flex h-12 w-full cursor-pointer items-center justify-between rounded bg-white px-2 shadow ${decal.id === activeDecal ? "border-2 border-green-100 bg-blue-200" : ""
}`}
>
<Image className={"w-12 h-12 object-contain"} src={decal.url} />
<IconButton icon={<DeleteIcon />} aria-label={""} colorScheme="red" size={"xs"} onClick={() => {
setDecals(decals.filter(el => el.id !== decal.id))
}} />
</li>
))}
</ul>
<div className="flex justify-center mt-4">
<Button
colorScheme="messenger"
onClick={() => {
pickFile({
accept: ["image/png"],
}).then((files) => {
if (!files.length) return
const id = uuidv4()
setDecals([...decals, {
id,
url: URL.createObjectURL(files[0]),
postion: new Vector3(0, 0, 0),
type: StickerType.logo
}])
setActiveDecal(id)
});
}}
leftIcon={<AddIcon />}
>
LOGO
</Button>
</div>
<Modal
isOpen={open}
onClose={() => {
setOpen(() => false);
}}
const [open, setOpen] = useState(false);
const decals = useModelStore((state) => state.decals);
const setDecals = useModelStore((state) => state.setDecals);
const setActiveDecal = useModelStore((state) => state.setActiveDecal);
const activeModel = useModelStore((state) => state.activeModel);
const activeDecal = useModelStore((state) => state.activeDecal);
return (
<>
<ul>
{decals
.filter((el) => el.type === StickerType.logo)
.map((decal) => (
<li
onClick={() => {
setActiveDecal(decal.id);
}}
key={decal.id}
className={`mb-2 flex h-12 w-full cursor-pointer items-center justify-between rounded bg-white px-2 shadow ${
decal.id === activeDecal
? "border-2 border-green-100 bg-blue-200"
: ""
}`}
>
<ModalOverlay />
<ModalContent>
<ModalBody>
<div className="flex justify-center p-4">
<div
onClick={() => { }}
className={
"flex aspect-square w-24 cursor-pointer items-center justify-center rounded-lg border-2 border-gray-300"
}
>
<AddIcon />
</div>
</div>
</ModalBody>
</ModalContent>
</Modal>
</>
);
<Image className={"h-12 w-12 object-contain"} src={decal.url} />
<IconButton
icon={<DeleteIcon />}
aria-label={""}
colorScheme="red"
size={"xs"}
onClick={() => {
setDecals(decals.filter((el) => el.id !== decal.id));
}}
/>
</li>
))}
</ul>
<div className="mt-4 flex justify-center">
<Button
colorScheme="messenger"
onClick={() => {
pickFile({
accept: ["image/png"],
}).then((files) => {
if (!files.length) return;
const id = uuidv4();
setDecals([
...decals,
{
id,
url: URL.createObjectURL(files[0]),
postion: new Vector3(0, 0, 0),
type: StickerType.logo,
scale: activeModel ? 1.5 : 0.05,
},
]);
setActiveDecal(id);
});
}}
leftIcon={<AddIcon />}
>
LOGO
</Button>
</div>
<Modal
isOpen={open}
onClose={() => {
setOpen(() => false);
}}
>
<ModalOverlay />
<ModalContent>
<ModalBody>
<div className="flex justify-center p-4">
<div
onClick={() => {}}
className={
"flex aspect-square w-24 cursor-pointer items-center justify-center rounded-lg border-2 border-gray-300"
}
>
<AddIcon />
</div>
</div>
</ModalBody>
</ModalContent>
</Modal>
</>
);
};
export default LogoPanel;

View File

@ -26,8 +26,9 @@ const RightPanel = () => {
onClick={() => {
setActiveModel(index);
}}
className={`${activeModel === index ? "border-2 border-green-300" : ""
} flex aspect-square items-center justify-center overflow-hidden rounded-xl bg-blue-100`}
className={`${
activeModel === index ? "border-2 border-green-300" : ""
} flex aspect-square items-center justify-center overflow-hidden rounded-xl bg-blue-100`}
>
<img
className="h-full w-full object-cover"

View File

@ -1,6 +1,6 @@
import useModelStore, { DecalSticker } from "@/store/useModelStore";
import { Decal, useTexture } from "@react-three/drei";
import { useState } from "react";
import { Euler } from "three";
/**
* logo 和文字标签
@ -8,51 +8,66 @@ import { useState } from "react";
* @returns
*/
const Sticker = ({ decal }: { decal: DecalSticker }) => {
const sticker = useTexture(decal.url);
const [scale, setScale] = useState(0.05);
const setDecalDragging = useModelStore((state) => state.setDecalDragging);
const setActiveDecal = useModelStore((state) => state.setActiveDecal);
return (
<Decal
position={decal.postion}
scale={scale}
onPointerDown={(ev) => {
ev.stopPropagation();
setActiveDecal(decal.id)
setDecalDragging(true);
}}
onPointerUp={(ev) => {
ev.stopPropagation();
setDecalDragging(false);
}}
// onWheel={(ev) => {
// ev.stopPropagation()
// setDecalDragging(true)
// console.log(ev);
// // setScale((state) => state += 1)
// }}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
rotation={Math.PI}
>
<meshPhysicalMaterial
transparent
polygonOffset
polygonOffsetFactor={-10}
map={sticker}
map-flipY={false}
map-anisotropy={16}
iridescence={1}
iridescenceIOR={1}
iridescenceThicknessRange={[0, 1400]}
roughness={1}
clearcoat={0.5}
metalness={0.75}
toneMapped={false}
/>
</Decal>
);
const sticker = useTexture(decal.url);
const setDecalDragging = useModelStore((state) => state.setDecalDragging);
const setDecalScale = useModelStore((state) => state.setDecalScale);
const setActiveDecal = useModelStore((state) => state.setActiveDecal);
// useInterval(() => {
// setRotation(state => state += 0.001 * Math.PI)
// }, 10)
return (
<Decal
debug={true}
position={decal.postion}
scale={decal.scale}
onPointerDown={(ev) => {
ev.stopPropagation();
setActiveDecal(decal.id);
setDecalDragging(true);
}}
onPointerUp={(ev) => {
ev.stopPropagation();
setDecalDragging(false);
}}
onPointerEnter={(_ev) => {
// ev.ctrlKey
}}
onWheel={(ev) => {
ev.stopPropagation();
// setDecalDragging(true);
// setDecals()
if (!ev.altKey) return;
if (ev.deltaY > 0) {
// setScale(state => state += 0.01)
setDecalScale(decal.id, (decal.scale -= 0.01));
} else if (ev.deltaY < 0) {
setDecalScale(decal.id, (decal.scale += 0.01));
// setScale(state => state -= 0.01)
}
}}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
rotation={Math.PI}
// rotation={new Euler(Math.PI / 2, 0, 0)}
// rotation={[Math.PI * 1, Math.PI * 1, Math.PI * 1]}
>
<meshPhysicalMaterial
transparent
polygonOffset
polygonOffsetFactor={-10}
map={sticker}
map-flipY={false}
map-anisotropy={16}
iridescence={1}
iridescenceIOR={1}
iridescenceThicknessRange={[0, 1400]}
roughness={1}
clearcoat={0.5}
metalness={0.75}
toneMapped={false}
/>
</Decal>
);
};
export default Sticker;

View File

@ -1,11 +1,11 @@
import useModelStore, { StickerType } from "@/store/useModelStore";
import { AddIcon, DeleteIcon, EditIcon } from "@chakra-ui/icons";
import {
Button,
FormControl,
FormLabel,
IconButton,
Input,
Button,
FormControl,
FormLabel,
IconButton,
Input,
} from "@chakra-ui/react";
import { useState } from "react";
import { fabric } from "fabric";
@ -13,126 +13,131 @@ import { Vector3 } from "three";
import { v4 } from "uuid";
const TextLabelPanel = () => {
const decals = useModelStore((state) => state.decals);
const setDecals = useModelStore((state) => state.setDecals);
const setActiveDecal = useModelStore((state) => state.setActiveDecal);
const activeDecal = useModelStore((state) => state.activeDecal);
const [editing, setEditing] = useState(false);
const [text, setText] = useState("");
const [color, setColor] = useState("#ffffff");
return (
<>
{editing ? (
<div className={"rounded bg-white p-4"}>
<h2 className="font-bold"></h2>
<FormControl>
<FormLabel></FormLabel>
<Input
value={text}
onInput={(ev) => {
setText(() => (ev.target as HTMLInputElement).value);
}}
/>
</FormControl>
<FormControl>
<FormLabel></FormLabel>
<Input
type="color"
value={color}
onInput={(ev) => {
setColor(() => (ev.target as HTMLInputElement).value);
}}
/>
</FormControl>
<div className={"mt-2 flex justify-center"}>
<Button
colorScheme="messenger"
size={"sm"}
onClick={() => {
const canvas = document.createElement("canvas");
const fabricText = new fabric.Text(text, {
fill: color,
// fontFamily: 'sans-serif'
});
const fabricCanvas = new fabric.Canvas(canvas, {
width: fabricText.width,
height: fabricText.height,
});
const decals = useModelStore((state) => state.decals);
const setDecals = useModelStore((state) => state.setDecals);
const setActiveDecal = useModelStore((state) => state.setActiveDecal);
const activeModel = useModelStore((state) => state.activeModel);
const activeDecal = useModelStore((state) => state.activeDecal);
const [editing, setEditing] = useState(false);
const [text, setText] = useState("");
const [color, setColor] = useState("#ffffff");
return (
<>
{editing ? (
<div className={"rounded bg-white p-4"}>
<h2 className="font-bold"></h2>
<FormControl>
<FormLabel></FormLabel>
<Input
value={text}
onInput={(ev) => {
setText(() => (ev.target as HTMLInputElement).value);
}}
/>
</FormControl>
<FormControl>
<FormLabel></FormLabel>
<Input
type="color"
value={color}
onInput={(ev) => {
setColor(() => (ev.target as HTMLInputElement).value);
}}
/>
</FormControl>
<div className={"mt-2 flex justify-center"}>
<Button
colorScheme="messenger"
size={"sm"}
onClick={() => {
const canvas = document.createElement("canvas");
const fabricText = new fabric.Text(text, {
fill: color,
// fontFamily: 'sans-serif'
});
const fabricCanvas = new fabric.Canvas(canvas, {
width: fabricText.width,
height: fabricText.height,
});
fabricCanvas.add(fabricText);
const id = v4();
setActiveDecal(id);
setDecals([
...decals,
{
id,
postion: new Vector3(0, 0, 0),
text,
url: fabricCanvas.toDataURL(),
type: StickerType.text,
},
]);
setText(() => "");
setColor(() => "#ffffff");
setEditing(() => false);
}}
>
</Button>
</div>
</div>
) : (
<ul>
{decals
.filter((el) => el.type === StickerType.text)
.map((text) => (
<li
onClick={() => {
setActiveDecal(text.id);
}}
key={text.id}
className={`mb-2 flex h-12 w-full cursor-pointer items-center justify-between rounded bg-white px-2 shadow ${activeDecal === text.id ? "border-2 border-green-100 bg-blue-200" : ""
}`}
>
<span>{text.text}</span>
<div className={"right"}>
<IconButton
onClick={(ev) => {
ev.stopPropagation();
}}
size={"xs"}
icon={<EditIcon />}
aria-label={""}
/>
<IconButton
className={"ml-1"}
onClick={(ev) => {
ev.stopPropagation();
setDecals(decals.filter(el => el.id !== text.id))
}}
size={"xs"}
icon={<DeleteIcon />}
aria-label={""}
colorScheme="red"
/>
</div>
</li>
))}
</ul>
)}
<div className="mt-4 flex justify-center">
<Button
colorScheme="messenger"
onClick={() => {
setEditing(() => true);
fabricCanvas.add(fabricText);
const id = v4();
setActiveDecal(id);
setDecals([
...decals,
{
id,
postion: new Vector3(0, 0, 0),
text,
url: fabricCanvas.toDataURL(),
type: StickerType.text,
scale: activeModel ? 1.5 : 0.05,
},
]);
setText(() => "");
setColor(() => "#ffffff");
setEditing(() => false);
}}
>
</Button>
</div>
</div>
) : (
<ul>
{decals
.filter((el) => el.type === StickerType.text)
.map((text) => (
<li
onClick={() => {
setActiveDecal(text.id);
}}
key={text.id}
className={`mb-2 flex h-12 w-full cursor-pointer items-center justify-between rounded bg-white px-2 shadow ${
activeDecal === text.id
? "border-2 border-green-100 bg-blue-200"
: ""
}`}
>
<span>{text.text}</span>
<div className={"right"}>
<IconButton
onClick={(ev) => {
ev.stopPropagation();
}}
leftIcon={<AddIcon />}
>
</Button>
</div>
</>
);
size={"xs"}
icon={<EditIcon />}
aria-label={""}
/>
<IconButton
className={"ml-1"}
onClick={(ev) => {
ev.stopPropagation();
setDecals(decals.filter((el) => el.id !== text.id));
}}
size={"xs"}
icon={<DeleteIcon />}
aria-label={""}
colorScheme="red"
/>
</div>
</li>
))}
</ul>
)}
<div className="mt-4 flex justify-center">
<Button
colorScheme="messenger"
onClick={() => {
setEditing(() => true);
}}
leftIcon={<AddIcon />}
>
</Button>
</div>
</>
);
};
export default TextLabelPanel;

View File

@ -6,7 +6,7 @@ import { models } from "@/constant/models.ts";
import { useGLTF } from "@react-three/drei/core/useGLTF";
import { textures } from "@/constant/textures.ts";
import Sticker from "./Sticker";
import { useWhyDidYouUpdate } from "ahooks";
import { useState, useEffect } from "react";
const CapModel = () => {
const activeModel = useModelStore((state) => state.activeModel);
@ -14,9 +14,33 @@ const CapModel = () => {
const decalDragging = useModelStore((state) => state.decalDragging);
const activeDecal = useModelStore((state) => state.activeDecal);
const setDecalPositon = useModelStore((state) => state.setDecalPositon);
useWhyDidYouUpdate('useWhyDidYouUpdateComponent', {
activeDecal, decalDragging, activeTextures, activeModel
});
const [isAltDown, setIsAltDown] = useState(false);
useEffect(() => {
function handleKeyDown(event: any) {
if (event.key === "Alt") {
setIsAltDown(true);
}
}
function handleKeyUp(event: any) {
if (event.key === "Alt") {
setIsAltDown(false);
}
}
// 监听键盘按下和释放事件
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
// 组件卸载时移除事件监听器
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
}, []);
const { nodes } = useGLTF(models[activeModel].path);
return (
<>
@ -46,6 +70,7 @@ const CapModel = () => {
key={keyName}
onDoubleClick={(ev) => {
ev.stopPropagation();
if (activeDecal) {
const { x, y, z } = ev.point;
const pos = new Vector3(
@ -53,6 +78,18 @@ const CapModel = () => {
y / models[activeModel].scale,
z / models[activeModel].scale,
);
// console.log(ev.point.transformDirection(ev.intersections[0].object.matrixWorld));
const normal = ev.face?.normal;
// normal?.transformDirection(ev.intersections[0].object.matrixWorld)
// normal?.applyQuaternion(ev.object.quaternion)
normal?.normalize();
console.log(normal);
console.log(normal?.angleTo(new Vector3(0, 1, 0)));
// const y_a = normal?.angleTo(new Vector3(0, 1, 0))
// const x_a = normal?.angleTo(new Vector3(1, 0, 0))
// const z_a = normal?.angleTo(new Vector3(0, 0, 1))
// console.log(x_a, y_a, z_a);
setDecalPositon(activeDecal, pos);
}
}}
@ -80,13 +117,11 @@ const CapModel = () => {
}
})}
</group>
<OrbitControls enabled={!decalDragging} />
<OrbitControls enabled={!decalDragging} enableZoom={!isAltDown} />
</>
);
};
export const Stickers = () => {
const decals = useModelStore((state) => state.decals);
@ -96,19 +131,19 @@ export const Stickers = () => {
<Sticker decal={decal} key={decal.id} />
))}
</>
)
}
);
};
const ThreeScene = () => {
// const hdr = useTexture("/venice_sunset_1k.hdr")
return (
<Canvas>
<ambientLight></ambientLight>
<pointLight position={[10, 10, 10]}></pointLight>
<perspectiveCamera />
<CapModel />
<Environment preset={"city"} background={false} />
<axesHelper />
<Environment files={"/venice_sunset_1k.hdr"} background={false} />
</Canvas>
);
};

View File

@ -2,7 +2,7 @@ import { create } from "zustand";
import { models } from "@/constant/models.ts";
import { Vector3 } from "three";
import { devtools } from "zustand/middleware";
import { v4 as uuidv4 } from "uuid";
// import { v4 as uuidv4 } from "uuid";
export enum StickerType {
text,
logo,
@ -13,6 +13,7 @@ export interface DecalSticker {
postion: Vector3;
text?: string;
url: string;
scale: number;
type: StickerType;
}
@ -32,10 +33,14 @@ interface ModelState {
decalDragging: boolean;
setDecalDragging: (enable: boolean) => void;
decalZooming: boolean;
setDecalZooming: (enable: boolean) => void;
decals: DecalSticker[];
activeDecal?: string;
setDecalPositon: (id: string, postion: Vector3) => void;
setDecals: (decals: DecalSticker[]) => void;
setDecalScale: (id: string, scale: number) => void;
setActiveDecal: (decalId: string) => void;
}
@ -44,7 +49,18 @@ const useModelStore = create<ModelState>()(
modelLoading: 0,
setModelLoading: (progress: number) =>
set(() => ({ modelLoading: progress })),
setDecalScale: (id: string, scale: number) =>
set((state) => {
const _decals = state.decals.map((el) => {
if (el.id === id) {
el.scale = scale;
}
return el;
});
return {
decals: _decals,
};
}),
activeModel: 0,
setActiveModel: (index: number) =>
set(() => ({
@ -71,14 +87,11 @@ const useModelStore = create<ModelState>()(
setDecalDragging: (enable: boolean) =>
set(() => ({ decalDragging: enable })),
decals: [
{
id: uuidv4(),
url: "/textures/archlogo.png",
postion: new Vector3(0, 0, 0),
type: StickerType.logo,
},
],
// 是否正在缩放sticker
decalZooming: false,
setDecalZooming: (enable: boolean) => set(() => ({ decalZooming: enable })),
decals: [],
activeDecal: undefined,
setDecalPositon: (id: string, postion: Vector3) =>
set((state) => {
@ -100,10 +113,17 @@ const useModelStore = create<ModelState>()(
set(() => ({
decals,
})),
setActiveDecal: (decalId: string) =>
set(() => ({
activeDecal: decalId,
})),
// // @ts-ignore
// setDecalScale: (id: string, scale: number) =>
// set(() => ({
// activeArea: 0
// })),
})),
);