refactor: move editor page and types to conventional folders
This commit is contained in:
+4
-1
@@ -37,4 +37,7 @@ Thumbs.db
|
|||||||
|
|
||||||
# 3D Assets Cache (drei, GLTFJSX)
|
# 3D Assets Cache (drei, GLTFJSX)
|
||||||
.drei/
|
.drei/
|
||||||
.glitchdrei-cache/
|
.glitchdrei-cache/
|
||||||
|
|
||||||
|
.backend/
|
||||||
|
backend/
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
# ✅ Résolution de l'erreur R3F "Div is not part of the THREE namespace!"
|
|
||||||
|
|
||||||
## ❌ Problème initial
|
|
||||||
|
|
||||||
**Erreur** : `Uncaught Error: R3F: Div is not part of the THREE namespace! Did you forget to extend?`
|
|
||||||
|
|
||||||
**Cause** : Le composant `EditorControls` était rendu à l'intérieur du `<Canvas>` de React Three Fiber, alors qu'il contient des éléments HTML (`<div>`, `<button>`, `<h3>`).
|
|
||||||
|
|
||||||
## ✅ Solution implémentée
|
|
||||||
|
|
||||||
### Changement 1 : Réorganisation de la structure des composants
|
|
||||||
|
|
||||||
**Ancienne structure (PROBLÉMATIQUE)**
|
|
||||||
|
|
||||||
```
|
|
||||||
EditorPage.tsx
|
|
||||||
└── Canvas
|
|
||||||
├── EditorViewer
|
|
||||||
│ ├── EditorCamera
|
|
||||||
│ ├── EditorFPSController
|
|
||||||
│ ├── MapViewer
|
|
||||||
│ └── EditorControls ← HTML DANS LE CANVAS !
|
|
||||||
└── Lumières
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nouvelle structure (CORRIGÉE)**
|
|
||||||
|
|
||||||
```
|
|
||||||
EditorPage.tsx
|
|
||||||
├── Canvas (uniquement 3D)
|
|
||||||
│ ├── EditorViewer
|
|
||||||
│ │ ├── EditorCamera
|
|
||||||
│ │ ├── EditorFPSController
|
|
||||||
│ │ └── MapViewer
|
|
||||||
│ └── Lumières
|
|
||||||
└── EditorControls ← HTML EN DEHORS DU CANVAS
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changement 2 : Props drilling pour partager l'état
|
|
||||||
|
|
||||||
**État partagé dans `EditorPage.tsx`** :
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const [selectedNodeIndex, setSelectedNodeIndex] = useState<number | null>(null);
|
|
||||||
const [transformMode, setTransformMode] = useState<TransformMode>("translate");
|
|
||||||
const [undoCount, setUndoCount] = useState(0);
|
|
||||||
const [redoCount, setRedoCount] = useState(0);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Props passés au Canvas (3D)** :
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
<EditorViewer
|
|
||||||
sceneData={sceneData!}
|
|
||||||
selectedNodeIndex={selectedNodeIndex}
|
|
||||||
onSelectNode={handleSelectNode}
|
|
||||||
transformMode={transformMode}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Props passés à EditorControls (HTML)** :
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
<EditorControls
|
|
||||||
transformMode={transformMode}
|
|
||||||
onTransformModeChange={handleTransformModeChange}
|
|
||||||
selectedNodeIndex={selectedNodeIndex}
|
|
||||||
nodesCount={sceneData.mapNodes.length}
|
|
||||||
// ... autres props
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changement 3 : Mise à jour des interfaces
|
|
||||||
|
|
||||||
**`EditorViewer`** : Accepte maintenant les props de l'état
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
interface EditorViewerProps {
|
|
||||||
sceneData: SceneData;
|
|
||||||
selectedNodeIndex: number | null;
|
|
||||||
onSelectNode: (index: number | null) => void;
|
|
||||||
transformMode: TransformMode;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changement 4 : Simplification de `EditorViewer`
|
|
||||||
|
|
||||||
- Retrait de la gestion d'état local
|
|
||||||
- Propagation de `onSelectNode` à `MapViewer`
|
|
||||||
- Suppression de `EditorControls` du return JSX
|
|
||||||
- Focus sur le rendu 3D uniquement
|
|
||||||
|
|
||||||
## 🧪 Résultats
|
|
||||||
|
|
||||||
### ✅ Résolu
|
|
||||||
|
|
||||||
1. **Erreur R3F ** éliminée
|
|
||||||
2. **Séparation claire** : HTML vs 3D
|
|
||||||
3. **État synchronisé** entre UI et scène 3D
|
|
||||||
4. **Build TypeScript** : Pas d'erreurs
|
|
||||||
5. **Serveur dev** : Démarre sur port 5177
|
|
||||||
|
|
||||||
### ⚡ Fonctionnalités préservées
|
|
||||||
|
|
||||||
- ✅ Upload dossier map.json
|
|
||||||
- ✅ Visualisation 3D avec MapNode
|
|
||||||
- ✅ Transformations T/R/S
|
|
||||||
- ✅ Undo/Redo (Ctrl+Z/Y)
|
|
||||||
- ✅ Export JSON
|
|
||||||
- ✅ Mode player FPS
|
|
||||||
- ✅ Sélection objets + highlight
|
|
||||||
- ✅ Navigation jeu ↔ éditeur
|
|
||||||
|
|
||||||
### 📋 Tests recommandés
|
|
||||||
|
|
||||||
1. **Accéder à `/editor`** : Vérifier que l'erreur R3F disparaît
|
|
||||||
2. **Interaction** : Tester sélection d'objets (clic sur cubes)
|
|
||||||
3. **Panels** : Vérifier que EditorControls s'affiche correctement
|
|
||||||
4. **Keyboard shortcuts** : T/R/S, ESC, Ctrl+Z/Y
|
|
||||||
|
|
||||||
### 📁 Fichiers modifiés
|
|
||||||
|
|
||||||
1. `src/components/editor/EditorPage.tsx` → Gestion état + réorganisation
|
|
||||||
2. `src/components/editor/EditorViewer.tsx` → Props drilling + cleanup
|
|
||||||
3. Props updates dans l'arbre de composants
|
|
||||||
|
|
||||||
L'erreur R3F est maintenant résolue avec une architecture propre qui sépare correctement le HTML du 3D ! 🎉
|
|
||||||
+1
-1
@@ -5,7 +5,7 @@ import { Crosshair } from "@/components/ui/Crosshair";
|
|||||||
import { InteractPrompt } from "@/components/ui/InteractPrompt";
|
import { InteractPrompt } from "@/components/ui/InteractPrompt";
|
||||||
import { DebugPerf } from "@/utils/debug/DebugPerf";
|
import { DebugPerf } from "@/utils/debug/DebugPerf";
|
||||||
import { World } from "@/world/World";
|
import { World } from "@/world/World";
|
||||||
import { EditorPage } from "@/components/editor/EditorPage";
|
import { EditorPage } from "@/pages/EditorPage";
|
||||||
|
|
||||||
function App(): React.JSX.Element {
|
function App(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { TransformMode } from "./types";
|
import type { TransformMode } from "@/types/editor";
|
||||||
|
|
||||||
interface EditorControlsProps {
|
interface EditorControlsProps {
|
||||||
transformMode: TransformMode;
|
transformMode: TransformMode;
|
||||||
|
|||||||
@@ -1,335 +0,0 @@
|
|||||||
.editor-container {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-loading,
|
|
||||||
.editor-error {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-loading h2 {
|
|
||||||
font-size: 2rem;
|
|
||||||
color: #ff6600;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-loading p {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-error h2 {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
color: #ff4444;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-error p {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: #ccc;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-section {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 2rem;
|
|
||||||
border: 2px dashed rgba(255, 102, 0, 0.3);
|
|
||||||
max-width: 500px;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-section h3 {
|
|
||||||
color: #ff6600;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-zone {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
border: 2px dashed #ff6600;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: rgba(255, 102, 0, 0.1);
|
|
||||||
color: #ff6600;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-zone:hover {
|
|
||||||
background: rgba(255, 102, 0, 0.2);
|
|
||||||
border-color: #ff8533;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-structure {
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-structure h4 {
|
|
||||||
color: #aaa;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-structure pre {
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #ddd;
|
|
||||||
font-family: "Courier New", monospace;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
background: rgba(255, 102, 0, 0.2);
|
|
||||||
padding: 0.2rem 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #ff8533;
|
|
||||||
font-family: "Courier New", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.editor-error h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-section {
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-zone {
|
|
||||||
padding: 1.5rem 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Editor Controls Styles */
|
|
||||||
.editor-camera-info {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
left: 10px;
|
|
||||||
background: rgba(0, 0, 0, 0.8);
|
|
||||||
color: #00ff00;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid #00ff00;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-controls-panel {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 250px;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(30, 30, 30, 0.95);
|
|
||||||
padding: 20px;
|
|
||||||
color: white;
|
|
||||||
border-left: 2px solid #ff6600;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-controls-panel h3 {
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #ff6600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transform-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transform-button {
|
|
||||||
padding: 12px;
|
|
||||||
background: #333;
|
|
||||||
color: white;
|
|
||||||
border: 1px solid #555;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 13px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transform-button.active {
|
|
||||||
background: #ff6600;
|
|
||||||
color: black;
|
|
||||||
border-color: #ff6600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transform-button:hover {
|
|
||||||
background: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transform-button.active:hover {
|
|
||||||
background: #ff8533;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-button {
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #333;
|
|
||||||
color: #aaa;
|
|
||||||
border: 1px solid #555;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-button:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.export-button {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 12px;
|
|
||||||
background: #ff6600;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.export-button:hover {
|
|
||||||
background: #ff8533;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-button {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 12px;
|
|
||||||
background: #22c55e;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-button:hover {
|
|
||||||
background: #16a34a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset-button {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
background: #444;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset-button:hover {
|
|
||||||
background: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-button {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
background: #444;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-button.active {
|
|
||||||
background: #ff6600;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-button:hover {
|
|
||||||
background: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-button.active:hover {
|
|
||||||
background: #ff8533;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-info {
|
|
||||||
background: rgba(255, 102, 0, 0.1);
|
|
||||||
border: 1px solid #ff6600;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-name {
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-index {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-selection {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border: 1px dashed #555;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 15px;
|
|
||||||
text-align: center;
|
|
||||||
color: #888;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls-help {
|
|
||||||
background: #222;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 15px;
|
|
||||||
border: 1px solid #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls-help p {
|
|
||||||
margin: 4px 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,7 @@ import { OrbitControls } from "@react-three/drei";
|
|||||||
import EditorCamera from "./EditorCamera";
|
import EditorCamera from "./EditorCamera";
|
||||||
import FlyController from "./FlyController";
|
import FlyController from "./FlyController";
|
||||||
import MapViewer from "./MapViewer";
|
import MapViewer from "./MapViewer";
|
||||||
import type { MapNode, TransformMode } from "./types";
|
import type { MapNode, TransformMode, SceneData } from "@/types/editor";
|
||||||
import type { SceneData } from "./types";
|
|
||||||
|
|
||||||
interface EditorViewerProps {
|
interface EditorViewerProps {
|
||||||
sceneData: SceneData;
|
sceneData: SceneData;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useGLTF } from "@react-three/drei";
|
|||||||
import { Grid, TransformControls } from "@react-three/drei";
|
import { Grid, TransformControls } from "@react-three/drei";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
|
||||||
import type { SceneData, MapNode, TransformMode } from "./types";
|
import type { SceneData, MapNode, TransformMode } from "@/types/editor";
|
||||||
|
|
||||||
interface MapViewerProps {
|
interface MapViewerProps {
|
||||||
sceneData: SceneData;
|
sceneData: SceneData;
|
||||||
@@ -171,7 +171,14 @@ function ModelNodeWithRef({
|
|||||||
return () => {
|
return () => {
|
||||||
currentMap.delete(currentIndex);
|
currentMap.delete(currentIndex);
|
||||||
};
|
};
|
||||||
}, [index]);
|
}, [
|
||||||
|
index,
|
||||||
|
node.name,
|
||||||
|
node.position,
|
||||||
|
node.rotation,
|
||||||
|
node.scale,
|
||||||
|
objectsMapRef,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (groupRef.current) {
|
if (groupRef.current) {
|
||||||
@@ -258,7 +265,14 @@ function FallbackNodeWithRef({
|
|||||||
return () => {
|
return () => {
|
||||||
currentMap.delete(currentIndex);
|
currentMap.delete(currentIndex);
|
||||||
};
|
};
|
||||||
}, [index]);
|
}, [
|
||||||
|
index,
|
||||||
|
node.name,
|
||||||
|
node.position,
|
||||||
|
node.rotation,
|
||||||
|
node.scale,
|
||||||
|
objectsMapRef,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (meshRef.current) {
|
if (meshRef.current) {
|
||||||
|
|||||||
@@ -3,14 +3,7 @@ import { useGLTF } from "@react-three/drei";
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
||||||
import type { OctreeReadyHandler } from "@/types/3d";
|
import type { OctreeReadyHandler } from "@/types/3d";
|
||||||
|
import type { MapNode } from "@/types/editor";
|
||||||
interface MapNode {
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
position: [number, number, number];
|
|
||||||
rotation: [number, number, number];
|
|
||||||
scale: [number, number, number];
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAP_JSON_PATH = "/map.json";
|
const MAP_JSON_PATH = "/map.json";
|
||||||
|
|
||||||
@@ -88,32 +81,23 @@ function ModelInstance({ node }: { node: MapNode }): React.JSX.Element {
|
|||||||
const modelPath = `/models/${node.name}/model.gltf`;
|
const modelPath = `/models/${node.name}/model.gltf`;
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const { scene } = useGLTF(modelPath);
|
const { scene } = useGLTF(modelPath);
|
||||||
|
const { position, rotation, scale } = node;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (groupRef.current) {
|
if (groupRef.current) {
|
||||||
groupRef.current.position.set(...node.position);
|
groupRef.current.position.set(...position);
|
||||||
groupRef.current.rotation.set(...node.rotation);
|
groupRef.current.rotation.set(...rotation);
|
||||||
groupRef.current.scale.set(...node.scale);
|
groupRef.current.scale.set(...scale);
|
||||||
}
|
}
|
||||||
}, [
|
}, [position, rotation, scale]);
|
||||||
node.position[0],
|
|
||||||
node.position[1],
|
|
||||||
node.position[2],
|
|
||||||
node.rotation[0],
|
|
||||||
node.rotation[1],
|
|
||||||
node.rotation[2],
|
|
||||||
node.scale[0],
|
|
||||||
node.scale[1],
|
|
||||||
node.scale[2],
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<primitive
|
<primitive
|
||||||
ref={groupRef}
|
ref={groupRef}
|
||||||
object={scene}
|
object={scene}
|
||||||
position={node.position}
|
position={position}
|
||||||
rotation={node.rotation}
|
rotation={rotation}
|
||||||
scale={node.scale}
|
scale={scale}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+336
@@ -79,3 +79,339 @@ canvas {
|
|||||||
color: rgba(255, 255, 255, 0.85);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Editor page */
|
||||||
|
.editor-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-loading,
|
||||||
|
.editor-error {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-loading h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #ff6600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-loading p {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-error h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #ff4444;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-error p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #ccc;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container code {
|
||||||
|
background: rgba(255, 102, 0, 0.2);
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ff8533;
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
border: 2px dashed rgba(255, 102, 0, 0.3);
|
||||||
|
max-width: 500px;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section h3 {
|
||||||
|
color: #ff6600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
border: 2px dashed #ff6600;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 102, 0, 0.1);
|
||||||
|
color: #ff6600;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone:hover {
|
||||||
|
background: rgba(255, 102, 0, 0.2);
|
||||||
|
border-color: #ff8533;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-structure {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-structure h4 {
|
||||||
|
color: #aaa;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-structure pre {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #ddd;
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-camera-info {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
color: #00ff00;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #00ff00;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-controls-panel {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 250px;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(30, 30, 30, 0.95);
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
border-left: 2px solid #ff6600;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-controls-panel h3 {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #ff6600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transform-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transform-button {
|
||||||
|
padding: 12px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transform-button.active {
|
||||||
|
background: #ff6600;
|
||||||
|
color: black;
|
||||||
|
border-color: #ff6600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transform-button:hover {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transform-button.active:hover {
|
||||||
|
background: #ff8533;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
background: #333;
|
||||||
|
color: #aaa;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-button {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #ff6600;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-button:hover {
|
||||||
|
background: #ff8533;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #22c55e;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button:hover {
|
||||||
|
background: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: #444;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: #444;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-button.active {
|
||||||
|
background: #ff6600;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-button.active:hover {
|
||||||
|
background: #ff8533;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-info {
|
||||||
|
background: rgba(255, 102, 0, 0.1);
|
||||||
|
border: 1px solid #ff6600;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-name {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-index {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-selection {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px dashed #555;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
color: #888;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-help {
|
||||||
|
background: #222;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-help p {
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.editor-error h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone {
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import { useEffect, useState, useCallback, useRef } from "react";
|
import { useEffect, useState, useCallback, useRef } from "react";
|
||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import EditorViewer from "./EditorViewer";
|
import EditorViewer from "@/components/editor/EditorViewer";
|
||||||
import EditorControls from "./EditorControls";
|
import EditorControls from "@/components/editor/EditorControls";
|
||||||
import type { TransformMode, MapNode } from "./types";
|
import type {
|
||||||
import type { SceneData } from "./types";
|
TransformMode,
|
||||||
import "./EditorPage.css";
|
MapNode,
|
||||||
|
SceneData,
|
||||||
interface ObjectTransform {
|
ObjectTransform,
|
||||||
uuid: string;
|
} from "@/types/editor";
|
||||||
position: { x: number; y: number; z: number };
|
|
||||||
rotation: { x: number; y: number; z: number };
|
|
||||||
scale: { x: number; y: number; z: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
class HistoryManager {
|
class HistoryManager {
|
||||||
private history: ObjectTransform[][] = [];
|
private history: ObjectTransform[][] = [];
|
||||||
@@ -60,19 +56,6 @@ class HistoryManager {
|
|||||||
getRedoCount(): number {
|
getRedoCount(): number {
|
||||||
return this.history.length - 1 - this.currentIndex;
|
return this.history.length - 1 - this.currentIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
|
||||||
this.history = [];
|
|
||||||
this.currentIndex = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
canUndo(): boolean {
|
|
||||||
return this.currentIndex > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
canRedo(): boolean {
|
|
||||||
return this.currentIndex < this.history.length - 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditorPage(): React.JSX.Element {
|
export function EditorPage(): React.JSX.Element {
|
||||||
@@ -80,7 +63,6 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
const [isMapLoading, setIsMapLoading] = useState<boolean>(true);
|
const [isMapLoading, setIsMapLoading] = useState<boolean>(true);
|
||||||
const [sceneData, setSceneData] = useState<SceneData | null>(null);
|
const [sceneData, setSceneData] = useState<SceneData | null>(null);
|
||||||
|
|
||||||
// État partagé entre Canvas (3D) et EditorControls (HTML)
|
|
||||||
const [selectedNodeIndex, setSelectedNodeIndex] = useState<number | null>(
|
const [selectedNodeIndex, setSelectedNodeIndex] = useState<number | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -91,10 +73,8 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
const [redoCount, setRedoCount] = useState(0);
|
const [redoCount, setRedoCount] = useState(0);
|
||||||
const [isPlayerMode, setIsPlayerMode] = useState(false);
|
const [isPlayerMode, setIsPlayerMode] = useState(false);
|
||||||
|
|
||||||
const historyManagerRef = useCallback(() => new HistoryManager(50), []);
|
const historyManager = useRef<HistoryManager>(new HistoryManager(50));
|
||||||
const historyManager = useRef<HistoryManager>(historyManagerRef());
|
|
||||||
|
|
||||||
// Callbacks partagés
|
|
||||||
const handleSelectNode = useCallback((index: number | null) => {
|
const handleSelectNode = useCallback((index: number | null) => {
|
||||||
setSelectedNodeIndex(index);
|
setSelectedNodeIndex(index);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -195,7 +175,6 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
}, [sceneData]);
|
}, [sceneData]);
|
||||||
|
|
||||||
const handleResetCamera = useCallback(() => {
|
const handleResetCamera = useCallback(() => {
|
||||||
// Logique pour reset camera
|
|
||||||
console.log("Reset camera");
|
console.log("Reset camera");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -203,9 +182,10 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
setIsPlayerMode((prev) => !prev);
|
setIsPlayerMode((prev) => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleTransformStart = useCallback(() => {
|
const createSnapshot = useCallback((): ObjectTransform[] => {
|
||||||
if (!sceneData) return;
|
if (!sceneData) return [];
|
||||||
const snapshot = sceneData.mapNodes.map((node, index) => ({
|
|
||||||
|
return sceneData.mapNodes.map((node, index) => ({
|
||||||
uuid: `node-${index}`,
|
uuid: `node-${index}`,
|
||||||
position: {
|
position: {
|
||||||
x: node.position[0],
|
x: node.position[0],
|
||||||
@@ -219,29 +199,19 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
},
|
},
|
||||||
scale: { x: node.scale[0], y: node.scale[1], z: node.scale[2] },
|
scale: { x: node.scale[0], y: node.scale[1], z: node.scale[2] },
|
||||||
}));
|
}));
|
||||||
historyManager.current.saveSnapshot(snapshot);
|
|
||||||
}, [sceneData]);
|
}, [sceneData]);
|
||||||
|
|
||||||
|
const handleTransformStart = useCallback(() => {
|
||||||
|
if (!sceneData) return;
|
||||||
|
historyManager.current.saveSnapshot(createSnapshot());
|
||||||
|
}, [createSnapshot, sceneData]);
|
||||||
|
|
||||||
const handleTransformEnd = useCallback(() => {
|
const handleTransformEnd = useCallback(() => {
|
||||||
if (!sceneData) return;
|
if (!sceneData) return;
|
||||||
const snapshot = sceneData.mapNodes.map((node, index) => ({
|
historyManager.current.saveSnapshot(createSnapshot());
|
||||||
uuid: `node-${index}`,
|
|
||||||
position: {
|
|
||||||
x: node.position[0],
|
|
||||||
y: node.position[1],
|
|
||||||
z: node.position[2],
|
|
||||||
},
|
|
||||||
rotation: {
|
|
||||||
x: node.rotation[0],
|
|
||||||
y: node.rotation[1],
|
|
||||||
z: node.rotation[2],
|
|
||||||
},
|
|
||||||
scale: { x: node.scale[0], y: node.scale[1], z: node.scale[2] },
|
|
||||||
}));
|
|
||||||
historyManager.current.saveSnapshot(snapshot);
|
|
||||||
setUndoCount(historyManager.current.getUndoCount());
|
setUndoCount(historyManager.current.getUndoCount());
|
||||||
setRedoCount(historyManager.current.getRedoCount());
|
setRedoCount(historyManager.current.getRedoCount());
|
||||||
}, [sceneData]);
|
}, [createSnapshot, sceneData]);
|
||||||
|
|
||||||
const handleNodeTransform = useCallback(
|
const handleNodeTransform = useCallback(
|
||||||
(nodeIndex: number, updatedNode: MapNode) => {
|
(nodeIndex: number, updatedNode: MapNode) => {
|
||||||
@@ -445,7 +415,6 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
{/* EditorControls rendu en dehors du Canvas (HTML overlay) */}
|
|
||||||
{sceneData && (
|
{sceneData && (
|
||||||
<EditorControls
|
<EditorControls
|
||||||
transformMode={transformMode}
|
transformMode={transformMode}
|
||||||
+37
-60
@@ -3,74 +3,51 @@ import react from "@vitejs/plugin-react";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import type { ViteDevServer } from "vite";
|
import type { Plugin } from "vite";
|
||||||
import type { IncomingMessage, ServerResponse } from "http";
|
|
||||||
|
|
||||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||||
|
|
||||||
const MAX_MAP_PAYLOAD_BYTES = 1024 * 1024; // 1MB limit
|
const MAX_MAP_PAYLOAD_BYTES = 1024 * 1024;
|
||||||
|
|
||||||
const saveMapPlugin = () => ({
|
const saveMapPlugin = (): Plugin => ({
|
||||||
name: "save-map-api",
|
name: "save-map-api",
|
||||||
configureServer(server: ViteDevServer) {
|
configureServer(server) {
|
||||||
server.middlewares.use(
|
server.middlewares.use("/api/save-map", async (req, res) => {
|
||||||
"/api/save-map",
|
if (req.method !== "POST") {
|
||||||
async (req: IncomingMessage, res: ServerResponse) => {
|
res.writeHead(405).end(JSON.stringify({ error: "Method not allowed" }));
|
||||||
if (req.method !== "POST") {
|
return;
|
||||||
res.writeHead(405, { "Content-Type": "application/json" });
|
}
|
||||||
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let size = 0;
|
||||||
|
|
||||||
|
for await (const chunk of req) {
|
||||||
|
size += chunk.length;
|
||||||
|
if (size > MAX_MAP_PAYLOAD_BYTES) {
|
||||||
|
res
|
||||||
|
.writeHead(413)
|
||||||
|
.end(JSON.stringify({ error: "Payload too large" }));
|
||||||
|
req.destroy();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
let body = "";
|
try {
|
||||||
let bodySize = 0;
|
const data = JSON.parse(Buffer.concat(chunks).toString());
|
||||||
let requestAborted = false;
|
const mapPath = path.resolve(__dirname, "public/map.json");
|
||||||
|
await fs.promises.writeFile(
|
||||||
req.on("data", (chunk: Buffer) => {
|
mapPath,
|
||||||
if (requestAborted) return;
|
JSON.stringify(data, null, 2),
|
||||||
bodySize += chunk.length;
|
"utf8",
|
||||||
if (bodySize > MAX_MAP_PAYLOAD_BYTES) {
|
);
|
||||||
requestAborted = true;
|
res.writeHead(200).end(JSON.stringify({ success: true }));
|
||||||
res.writeHead(413, { "Content-Type": "application/json" });
|
} catch (err) {
|
||||||
res.end(JSON.stringify({ error: "Payload too large" }));
|
const status = err instanceof SyntaxError ? 400 : 500;
|
||||||
req.destroy();
|
const message = err instanceof Error ? err.message : "Unknown error";
|
||||||
return;
|
res.writeHead(status).end(JSON.stringify({ error: message }));
|
||||||
}
|
}
|
||||||
body += chunk.toString();
|
});
|
||||||
});
|
|
||||||
|
|
||||||
req.on("error", (err: Error) => {
|
|
||||||
if (!res.headersSent) {
|
|
||||||
res.writeHead(400, { "Content-Type": "application/json" });
|
|
||||||
res.end(JSON.stringify({ error: err.message }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on("end", async () => {
|
|
||||||
if (requestAborted) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsedBody = JSON.parse(body);
|
|
||||||
const mapPath = path.resolve(__dirname, "public/map.json");
|
|
||||||
await fs.promises.writeFile(
|
|
||||||
mapPath,
|
|
||||||
JSON.stringify(parsedBody, null, 2),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
res.end(JSON.stringify({ success: true }));
|
|
||||||
} catch (err) {
|
|
||||||
const statusCode = err instanceof SyntaxError ? 400 : 500;
|
|
||||||
res.writeHead(statusCode, { "Content-Type": "application/json" });
|
|
||||||
res.end(
|
|
||||||
JSON.stringify({
|
|
||||||
error: err instanceof Error ? err.message : "Unknown error",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user