- 메인 페이지에서 배경, 말, 주사위면을 커스텀하게 지정하도록 변경.

- 모든 데이터를 로컬에 저장하고, 가져오기 및 내보내기를 할 수 있도록 수정.
This commit is contained in:
kimyu
2026-09-08 01:03:21 +09:00
parent fc6eb5fce1
commit 1b99e3961d
9 changed files with 830 additions and 82 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import { Suspense, useCallback, useLayoutEffect, useRef, useState } from 'react';
import { Suspense, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { Canvas, useFrame, useLoader, useThree } from '@react-three/fiber';
import {
Physics,
@@ -15,25 +15,26 @@ import {
Vector3,
type OrthographicCamera,
} from 'three';
import { getDiceFaceTextureList } from './asset-storage';
const FLOOR_SIZE = 10;
const FLOOR_SIZE = 6;
const FLOOR_HALF = FLOOR_SIZE / 2;
const WALL_THICKNESS = 0.2;
const WALL_HEIGHT = 10;
const WALL_THICKNESS = 0.25;
const WALL_HEIGHT = 8;
const WALL_HALF_T = WALL_THICKNESS / 2;
const WALL_HALF_H = WALL_HEIGHT / 2;
const STILL_SPEED_SQ = 0.05;
const STILL_FRAMES = 18;
// boxGeometry material 순서: +X, -X, +Y, -Y, +Z, -Z
const DICE_FACE_TEXTURES = [
'/dice/2.png', // +X
'/dice/5.png', // -X
'/dice/1.png', // +Y
'/dice/6.png', // -Y
'/dice/3.png', // +Z
'/dice/4.png', // -Z
// boxGeometry material 순서: +X, -X, +Y, -Y, +Z, -Z (기본 경로, 런타임에 localStorage로 교체)
const DEFAULT_DICE_FACE_TEXTURES = [
'/dice/2.png',
'/dice/5.png',
'/dice/1.png',
'/dice/6.png',
'/dice/3.png',
'/dice/4.png',
] as const;
// 결과 판정: 로컬 법선이 월드 +Y에 가장 가까운 면
@@ -99,8 +100,8 @@ function InvisibleWalls() {
);
}
function DiceMesh() {
const textures = useLoader(TextureLoader, [...DICE_FACE_TEXTURES]);
function DiceMesh({ faceUrls }: { faceUrls: string[] }) {
const textures = useLoader(TextureLoader, faceUrls);
useLayoutEffect(() => {
for (const texture of textures) {
@@ -115,7 +116,7 @@ function DiceMesh() {
<boxGeometry args={[1, 1, 1]} />
{textures.map((texture, index) => (
<meshStandardMaterial
key={texture.uuid}
key={`${texture.uuid}-${index}`}
attach={`material-${index}`}
map={texture}
roughness={0.55}
@@ -169,10 +170,12 @@ function RapierDiceExample({
diceRef,
rolling,
onSettled,
faceUrls,
}: {
diceRef: React.RefObject<RapierRigidBody | null>;
rolling: boolean;
onSettled: (value: number) => void;
faceUrls: string[];
}) {
return (
<Canvas
@@ -203,7 +206,7 @@ function RapierDiceExample({
friction={0.6}
>
<Suspense fallback={null}>
<DiceMesh />
<DiceMesh key={faceUrls.join('|')} faceUrls={faceUrls} />
</Suspense>
</RigidBody>
@@ -237,6 +240,11 @@ export default function Dice({
const diceRef = useRef<RapierRigidBody>(null);
const [rolling, setRolling] = useState(false);
const [result, setResult] = useState<number | null>(null);
const [faceUrls, setFaceUrls] = useState<string[]>([...DEFAULT_DICE_FACE_TEXTURES]);
useEffect(() => {
setFaceUrls(getDiceFaceTextureList());
}, []);
const onSettled = useCallback(
(value: number) => {
@@ -252,7 +260,12 @@ export default function Dice({
return (
<div className="relative h-full w-full" style={{ containerType: 'size' }}>
<div className="absolute top-0 left-0 aspect-square w-[min(100%,100cqh)]">
<RapierDiceExample diceRef={diceRef} rolling={rolling} onSettled={onSettled} />
<RapierDiceExample
diceRef={diceRef}
rolling={rolling}
onSettled={onSettled}
faceUrls={faceUrls}
/>
<button
type="button"
className="absolute bottom-2 left-2 z-10 rounded bg-black/70 px-3 py-1 text-sm text-white disabled:opacity-50"