diff --git a/.agent/AGENTS.md b/.agent/AGENTS.md index 425e13e..69b879e 100644 --- a/.agent/AGENTS.md +++ b/.agent/AGENTS.md @@ -31,6 +31,7 @@ Scene objects are **never** singleton classes. Managers are **never** React comp - Scene components live in `src/world/` and `src/components/3d/` - UI overlays live in `src/components/ui/` - Managers live in `src/stateManager/` +- Debug tooling lives in `src/debug/` - Hooks live in `src/hooks/` - Static data lives in `src/data/` - Shaders live in `src/shaders/` @@ -54,9 +55,9 @@ import { useGameState } from "@/hooks/useGameState"; ### Debug - Debug panel activates with `?debug` in URL -- All debug logic goes through `Debug.getInstance()` from `src/utils/Debug.ts` +- All debug logic goes through `Debug.getInstance()` from `src/debug/Debug.ts` - Never scatter `if (isDev)` blocks across files -- `r3f-perf` is lazy-loaded only in debug mode via `src/components/3d/DebugPerf.tsx` +- `r3f-perf` is lazy-loaded only in debug mode via `src/debug/DebugPerf.tsx` ## Managers (4 max) diff --git a/.agent/skills/debug.md b/.agent/skills/debug.md index b0e14a3..0e44edd 100644 --- a/.agent/skills/debug.md +++ b/.agent/skills/debug.md @@ -11,7 +11,7 @@ http://localhost:5173?debug ## Debug singleton ```ts -// src/utils/Debug.ts +// src/debug/Debug.ts import GUI from "lil-gui"; export class Debug { @@ -56,14 +56,15 @@ if (debug.active) { r3f-perf is loaded only in debug mode to avoid dependency issues in production: ```tsx -// src/components/3d/DebugPerf.tsx +// src/debug/DebugPerf.tsx import { Suspense, lazy } from "react"; +import { Debug } from "@/debug/Debug"; const Perf = lazy(() => import("r3f-perf").then((m) => ({ default: m.Perf }))); export function DebugPerf() { - const debug = new URLSearchParams(window.location.search).has("debug"); - if (!debug) return null; + const debug = Debug.getInstance(); + if (!debug.active) return null; return ( diff --git a/README.md b/README.md index 8465be0..9c96f46 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,9 @@ la-fabrik/ │ └── src/ ├── world/ # Single persistent 3D world + │ ├── World.tsx # Main scene composition │ ├── Map.tsx # Base map, always mounted - │ ├── Lighting.tsx # Ambient, directional, point lights + │ ├── Lighting.tsx # Ambient, directional, point lights │ ├── Environment.tsx # HDRI, fog, sky │ ├── PostFX.tsx # Bloom, SSAO, chromatic aberration │ ├── zones/ # Spatial zones — LOD per zone @@ -97,12 +98,18 @@ la-fabrik/ │ ├── vertex.glsl │ └── fragment.glsl │ - ├── utils/ - │ ├── Debug.ts # lil-gui panel - │ ├── EventEmitter.ts # Simple pub/sub for manager-to-manager events - │ └── Dispose.ts # traverse() + dispose() helper + ├── debug/ # Dev-only tools and scene inspection + │ ├── Debug.ts # Global lil-gui manager + │ ├── DebugPerf.tsx # r3f-perf overlay mounted in Canvas + │ └── scene/ + │ ├── DebugHelpers.tsx # Grid + axes helpers shown in debug mode + │ └── DebugCameraControls.tsx # Free debug camera for map inspection │ - ├── App.tsx # Canvas + UI superimposed + ├── utils/ + │ ├── EventEmitter.ts # Simple pub/sub for manager-to-manager events + │ └── Dispose.ts # traverse() + dispose() helper + │ + ├── App.tsx # Canvas bootstrap └── main.tsx ``` @@ -116,7 +123,11 @@ npm run dev ``` Open `http://localhost:5173` — standard experience. -Open `http://localhost:5173?debug` — debug panel + r3f-perf overlay. +Open `http://localhost:5173?debug` — debug panel + r3f-perf overlay + free debug camera. + +## 🧭 Conventions + +Coding conventions and generation rules live in `.agent/skills/best-practices.md`. ## 📜 License diff --git a/docs/technical/architecture.md b/docs/technical/architecture.md index bb03d9f..a3bc69b 100644 --- a/docs/technical/architecture.md +++ b/docs/technical/architecture.md @@ -1,5 +1,7 @@ # Architecture Patterns +Coding conventions are maintained in `.agent/skills/best-practices.md`. + The project uses **two complementary patterns**: - **Singleton service classes** for orchestration and side effects @@ -332,17 +334,23 @@ useEffect(() => { --- -## 6. Debug Utility +## 6. Debug System The debug panel can be activated by appending `?debug` to the URL: `http://localhost:5173?debug` -All debug logic is centralized in `Debug.ts`. +All debug logic is centralized in `src/debug/`. Do not scatter debug checks across the codebase. +- `src/debug/Debug.ts` owns the global lil-gui singleton +- `src/debug/DebugPerf.tsx` mounts the perf overlay +- `src/debug/scene/*` contains debug-only R3F helpers such as free camera controls and axes/grid helpers + +`world/` stays focused on product scene components, while `debug/` contains developer tooling. + ```ts -// utils/Debug.ts +// src/debug/Debug.ts import GUI from "lil-gui"; export class Debug { @@ -379,3 +387,28 @@ if (debug.active) { debug.gui!.add(params, "bloomIntensity", 0, 3).name("Bloom"); } ``` + +Debug-only scene helpers should live outside `world/`: + +```tsx +// src/debug/scene/DebugHelpers.tsx +import { Debug } from "@/debug/Debug"; + +export function DebugHelpers(): React.JSX.Element | null { + const debug = Debug.getInstance(); + + if (!debug.active) { + return null; + } + + return ( + <> + + + + ); +} +``` diff --git a/docs/technical/best-practices.md b/docs/technical/best-practices.md deleted file mode 100644 index a70fb3e..0000000 --- a/docs/technical/best-practices.md +++ /dev/null @@ -1,153 +0,0 @@ -# Best Practices - -Generate code that is **simple**, **understandable**, **reviewable**, **scalable**, **optimized**, and **modern**. Follow W3C web standards and platform conventions. - -## Naming Conventions - -### Files - -| Type | Convention | Example | -| ---------- | --------------------------- | -------------------- | -| Components | PascalCase | `WorkshopZone.tsx` | -| Hooks | camelCase with `use` prefix | `useGameState.ts` | -| Managers | PascalCase | `GameManager.ts` | -| Utils | PascalCase | `Dispose.ts` | -| Data | PascalCase | `missions.ts` | -| Shaders | kebab-case | `hologram.vert.glsl` | - -### Variables & Functions - -| Type | Convention | Example | -| ---------------- | -------------------- | ----------------------------------------- | -| Variables | camelCase | `activeZone`, `missionStep` | -| Functions | camelCase | `startMission()`, `setActiveZone()` | -| Constants | UPPER_SNAKE_CASE | `MAX_SPEED`, `DEFAULT_PHASE` | -| React components | PascalCase | `function WorkshopZone()` | -| React hooks | camelCase with `use` | `useGameState()`, `useFrame()` | -| Classes | PascalCase | `class GameManager` | -| Interfaces/Types | PascalCase | `type GameSnapshot`, `interface ZoneData` | - -## Code Style - -### Simplicity First - -```ts -// Good — clear, direct -function getZoneRadius(zone: Zone): number { - return zone.radius; -} - -// Bad — over-abstracted -function getZoneRadius(zone: Zone): number { - return zone[ZoneFields.RADIUS] ?? RADIUS_DEFAULTS[zone.type]?.default ?? 50; -} -``` - -### Early Return - -```ts -// Good -if (!gltf) return null; -if (!visible) return null; - -return ; -``` - -### Avoid Nested Callbacks - -```ts -// Good — flat structure -useEffect(() => { - const mixer = new THREE.AnimationMixer(model); - - return () => mixer.stopAllAction(); -}, [model]); -``` - -## TypeScript Rules - -### Explicit Types for Exports - -```ts -export function useGameState(): GameSnapshot { ... } - -export function setPhase(phase: Phase): void { ... } -``` - -### Never use `any` - -```ts -// Good -const ref = useRef(null); - -// Bad -const ref = useRef(null); -``` - -## Performance - -### useRef for Mutable Values - -```ts -// Good — no re-render -const position = useRef(new THREE.Vector3()); - -// Bad — triggers re-render every frame -const [position, setPosition] = useState(new THREE.Vector3()); -``` - -### Memoize Expensive Computations - -```ts -const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); -``` - -## Scalability - -### Single Responsibility - -```ts -// Good — focused component -export function WorkshopZone() { - // Only handles workshop zone logic -} - -// Bad — does everything -export function WorkshopZone() { - // Handles zone logic + audio + cinematics + missions -} -``` - -### Constants for Magic Numbers - -```ts -const DEFAULT_CAMERA_DISTANCE = 5; -const ZONE_DETECTION_RADIUS = 20; -``` - -## Accessibility (W3C) - -### Semantic HTML - -```tsx -// Good - - -// Bad -
- -
-``` - -## Rules - -1. **Simplicity** — Every line of code must be justified. -2. **Readability** — Code is read 10x more than it's written. -3. **Reviewability** — PRs should be understandable in < 5 minutes. -4. **Scalability** — Architecture should support growth without refactoring. -5. **Performance** — Don't optimize prematurely, but don't introduce obvious bottlenecks. -6. **Modern** — Use ES2022+ features, TypeScript strict mode, React hooks. -7. **W3C** — Follow web standards: semantic HTML, ARIA, keyboard navigation. -8. **No Over-Engineering** — Avoid patterns that add complexity without benefit. diff --git a/public/models/map/blocking/model.glb b/public/models/map/blocking/model.glb new file mode 100644 index 0000000..98c45cc --- /dev/null +++ b/public/models/map/blocking/model.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73f7be151055fc5e3cd51b6b2f0db717d442c8ce1645dae8400ac5644e8e4d45 +size 2067524 diff --git a/public/models/map/blocking/model.gltf b/public/models/map/blocking/model.gltf index e13b317..3070180 100644 --- a/public/models/map/blocking/model.gltf +++ b/public/models/map/blocking/model.gltf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c98eb2de597f5e0fd60e2caff3e1889c761bfdcb9d23488d31700cb850cdd2ea -size 2800791 +oid sha256:86174fe3ea442f7a13b86d40d96d315f1ae57d894daa69537f4266eb08fec513 +size 2839228 diff --git a/src/App.css b/src/App.css deleted file mode 100644 index f460279..0000000 --- a/src/App.css +++ /dev/null @@ -1,184 +0,0 @@ -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ""; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} diff --git a/src/App.tsx b/src/App.tsx index 9331cb0..dcc621e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,120 +1,13 @@ -import { useState } from "react"; -import reactLogo from "./assets/react.svg"; -import viteLogo from "./assets/vite.svg"; -import heroImg from "./assets/hero.png"; -import "./App.css"; - -function App() { - const [count, setCount] = useState(0); +import { Canvas } from "@react-three/fiber"; +import { DebugPerf } from "@/debug/DebugPerf"; +import { World } from "@/world/World"; +function App(): React.JSX.Element { return ( - <> -
-
- - React logo - Vite logo -
-
-

Get started

-

- Edit src/App.tsx and save to test HMR -

-
- -
- -
- -
-
- -

Documentation

-

Your questions, answered

- -
-
- -

Connect with us

-

Join the Vite community

- -
-
- -
-
- + + + + ); } diff --git a/src/assets/hero.png b/src/assets/hero.png deleted file mode 100644 index cc51a3d..0000000 Binary files a/src/assets/hero.png and /dev/null differ diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/src/debug/Debug.ts b/src/debug/Debug.ts new file mode 100644 index 0000000..0951f79 --- /dev/null +++ b/src/debug/Debug.ts @@ -0,0 +1,48 @@ +import GUI from "lil-gui"; + +export class Debug { + private static instance: Debug | null = null; + + public readonly active: boolean; + private readonly gui: GUI | null; + private readonly folders = new Map(); + + static getInstance(): Debug { + if (!Debug.instance) { + Debug.instance = new Debug(); + } + return Debug.instance; + } + + private constructor() { + this.active = new URLSearchParams(window.location.search).has("debug"); + if (this.active) { + this.gui = new GUI({ title: "La-Fabrik Debug" }); + } else { + this.gui = null; + } + } + + createFolder(name: string): GUI | null { + if (!this.gui) { + return null; + } + + const existingFolder = this.folders.get(name); + + if (existingFolder) { + return existingFolder; + } + + const folder = this.gui.addFolder(name); + this.folders.set(name, folder); + + return folder; + } + + destroy(): void { + this.folders.clear(); + this.gui?.destroy(); + Debug.instance = null; + } +} diff --git a/src/debug/DebugPerf.tsx b/src/debug/DebugPerf.tsx new file mode 100644 index 0000000..36efac7 --- /dev/null +++ b/src/debug/DebugPerf.tsx @@ -0,0 +1,18 @@ +import { Suspense, lazy } from "react"; +import { Debug } from "@/debug/Debug"; + +const Perf = lazy(() => import("r3f-perf").then((m) => ({ default: m.Perf }))); + +export function DebugPerf(): React.JSX.Element | null { + const debug = Debug.getInstance(); + + if (!debug.active) { + return null; + } + + return ( + + + + ); +} diff --git a/src/debug/scene/DebugCameraControls.tsx b/src/debug/scene/DebugCameraControls.tsx new file mode 100644 index 0000000..f3fcaa0 --- /dev/null +++ b/src/debug/scene/DebugCameraControls.tsx @@ -0,0 +1,68 @@ +import { useEffect, useRef } from "react"; +import { OrbitControls } from "@react-three/drei"; +import type { OrbitControls as OrbitControlsImpl } from "three-stdlib"; +import { Debug } from "@/debug/Debug"; + +export function DebugCameraControls(): React.JSX.Element { + const controls = useRef(null); + + useEffect(() => { + const debug = Debug.getInstance(); + + if (!debug.active || !controls.current) { + return undefined; + } + + const folder = debug.createFolder("Camera"); + + if (!folder) { + return undefined; + } + + const target = controls.current.target; + const cameraState = { + targetX: target.x, + targetY: target.y, + targetZ: target.z, + }; + + const syncTarget = (): void => { + if (!controls.current) { + return; + } + + controls.current.target.set( + cameraState.targetX, + cameraState.targetY, + cameraState.targetZ, + ); + controls.current.update(); + }; + + folder + .add(cameraState, "targetX", -100, 100, 0.1) + .name("Target X") + .onChange(syncTarget); + folder + .add(cameraState, "targetY", -20, 50, 0.1) + .name("Target Y") + .onChange(syncTarget); + folder + .add(cameraState, "targetZ", -100, 100, 0.1) + .name("Target Z") + .onChange(syncTarget); + + return undefined; + }, []); + + return ( + + ); +} diff --git a/src/debug/scene/DebugHelpers.tsx b/src/debug/scene/DebugHelpers.tsx new file mode 100644 index 0000000..3b41fc3 --- /dev/null +++ b/src/debug/scene/DebugHelpers.tsx @@ -0,0 +1,19 @@ +import { Debug } from "@/debug/Debug"; + +export function DebugHelpers(): React.JSX.Element | null { + const debug = Debug.getInstance(); + + if (!debug.active) { + return null; + } + + return ( + <> + + + + ); +} diff --git a/src/index.css b/src/index.css index aad5cb1..faca6d2 100644 --- a/src/index.css +++ b/src/index.css @@ -1,111 +1,27 @@ :root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; - - --sans: system-ui, "Segoe UI", Roboto, sans-serif; - --heading: system-ui, "Segoe UI", Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } -} - -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } + color-scheme: dark; + font-family: Inter; } +html, +body, #root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; - box-sizing: border-box; + margin: 0; + width: 100vw; + height: 100vh; } body { - margin: 0; + overflow: hidden; } -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); +button, +input, +textarea, +select { + font: inherit; } -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); +canvas { + display: block; } diff --git a/src/main.tsx b/src/main.tsx index eff7ccc..ef474bf 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,7 +1,7 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import "./index.css"; import App from "./App.tsx"; +import "./index.css"; createRoot(document.getElementById("root")!).render( diff --git a/src/utils/Debug.ts b/src/utils/Debug.ts deleted file mode 100644 index a635599..0000000 --- a/src/utils/Debug.ts +++ /dev/null @@ -1 +0,0 @@ -// src/utils/Debug.ts diff --git a/src/utils/DebugPerf.tsx b/src/utils/DebugPerf.tsx deleted file mode 100644 index 7a3082e..0000000 --- a/src/utils/DebugPerf.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Suspense, lazy } from "react"; -const Perf = lazy(() => import("r3f-perf").then((m) => ({ default: m.Perf }))); -export function DebugPerf() { - const debug = new URLSearchParams(window.location.search).has("debug"); - if (!debug) return null; - return ( - - - - ); -} diff --git a/src/world/Environment.tsx b/src/world/Environment.tsx index b5a44ae..7b8af01 100644 --- a/src/world/Environment.tsx +++ b/src/world/Environment.tsx @@ -1 +1,3 @@ -// src/world/Environment.tsx +export function Environment(): React.JSX.Element { + return ; +} diff --git a/src/world/Lighting.tsx b/src/world/Lighting.tsx index be7da99..2870010 100644 --- a/src/world/Lighting.tsx +++ b/src/world/Lighting.tsx @@ -1 +1,12 @@ -// src/world/Lighting.tsx +export function Lighting(): React.JSX.Element { + return ( + <> + + + ); +} diff --git a/src/world/Map.tsx b/src/world/Map.tsx index 10bd803..773c9cf 100644 --- a/src/world/Map.tsx +++ b/src/world/Map.tsx @@ -1 +1,101 @@ -// src/world/Map.tsx +// # route path src/world/Map.tsx +import { useEffect, useLayoutEffect, useMemo, useRef } from "react"; +import { useFrame } from "@react-three/fiber"; +import { useGLTF } from "@react-three/drei"; +import * as THREE from "three"; +import { Debug } from "@/debug/Debug"; + +const MODEL_PATH = "/models/map/blocking/model.glb"; + +type MapDebugState = { + positionX: number; + positionY: number; + positionZ: number; + rotationY: number; + scale: number; +}; + +const DEFAULT_DEBUG_STATE: MapDebugState = { + positionX: 0, + positionY: 0, + positionZ: 0, + rotationY: 0, + scale: 1, +}; + +function centerModel(model: THREE.Object3D): number { + model.updateMatrixWorld(true); + + const bounds = new THREE.Box3().setFromObject(model); + const center = bounds.getCenter(new THREE.Vector3()); + const size = bounds.getSize(new THREE.Vector3()); + + model.position.set(-center.x, -bounds.min.y, -center.z); + + return size.length() > 0 && size.length() < 10 ? 5 : 1; +} + +export function Map(): React.JSX.Element { + const root = useRef(null); + const debugState = useRef({ ...DEFAULT_DEBUG_STATE }); + const { scene } = useGLTF(MODEL_PATH); + const model = useMemo(() => scene.clone(true), [scene]); + + useLayoutEffect(() => { + debugState.current.scale = centerModel(model); + }, [model]); + + useEffect(() => { + const debug = Debug.getInstance(); + + if (!debug.active) { + return undefined; + } + + const folder = debug.createFolder("Map"); + + if (!folder) { + return undefined; + } + + folder + .add(debugState.current, "positionX", -100, 100, 0.1) + .name("Position X"); + folder + .add(debugState.current, "positionY", -20, 50, 0.1) + .name("Position Y"); + folder + .add(debugState.current, "positionZ", -100, 100, 0.1) + .name("Position Z"); + folder + .add(debugState.current, "rotationY", -Math.PI, Math.PI, 0.01) + .name("Rotation Y"); + folder.add(debugState.current, "scale", 0.1, 10, 0.01).name("Scale"); + + return undefined; + }, []); + + useFrame(() => { + const currentRoot = root.current; + + if (!currentRoot) { + return; + } + + currentRoot.position.set( + debugState.current.positionX, + debugState.current.positionY, + debugState.current.positionZ, + ); + currentRoot.rotation.y = debugState.current.rotationY; + currentRoot.scale.setScalar(debugState.current.scale); + }); + + return ( + + + + ); +} + +useGLTF.preload(MODEL_PATH); diff --git a/src/world/World.tsx b/src/world/World.tsx new file mode 100644 index 0000000..78e2d58 --- /dev/null +++ b/src/world/World.tsx @@ -0,0 +1,20 @@ +import { Suspense } from "react"; +import { DebugCameraControls } from "@/debug/scene/DebugCameraControls"; +import { DebugHelpers } from "@/debug/scene/DebugHelpers"; +import { Environment } from "@/world/Environment"; +import { Lighting } from "@/world/Lighting"; +import { Map } from "@/world/Map"; + +export function World(): React.JSX.Element { + return ( + <> + + + + + + + + + ); +} diff --git a/tsconfig.app.json b/tsconfig.app.json index 1d29c88..5e4f9b6 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -6,9 +6,14 @@ "module": "esnext", "types": ["vite/client"], "skipLibCheck": true, + "ignoreDeprecations": "6.0", /* Bundler mode */ "moduleResolution": "bundler", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "moduleDetection": "force", diff --git a/vite.config.ts b/vite.config.ts index 0e43ae8..d779ded 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,13 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; +import path from "node:path"; // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, });