clean branch-scoped code quality issues
This commit is contained in:
@@ -44,12 +44,12 @@ This document describes the code that exists today in the repository.
|
|||||||
## Editor System
|
## Editor System
|
||||||
|
|
||||||
- `src/pages/editor/EditorPage.tsx` is the route-level editor page for `/editor`.
|
- `src/pages/editor/EditorPage.tsx` is the route-level editor page for `/editor`.
|
||||||
- `src/features/editor/components/EditorControls.tsx` renders the HTML editor control panel.
|
- `src/components/editor/EditorControls.tsx` renders the HTML editor control panel.
|
||||||
- `src/features/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, shortcuts, and map rendering.
|
- `src/components/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, shortcuts, and map rendering.
|
||||||
- `src/features/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
- `src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
||||||
- `src/features/editor/controls/FlyController.tsx` provides player-style editor navigation.
|
- `src/controls/editor/FlyController.tsx` provides player-style editor navigation.
|
||||||
- `src/features/editor/hooks/useEditorSceneData.ts` loads scene data and handles folder upload fallback.
|
- `src/hooks/editor/useEditorSceneData.ts` loads scene data and handles folder upload fallback.
|
||||||
- `src/features/editor/hooks/useEditorHistory.ts` owns editor undo and redo state.
|
- `src/hooks/editor/useEditorHistory.ts` owns editor undo and redo state.
|
||||||
- `src/utils/editor/loadEditorScene.ts` handles editor-only folder upload parsing.
|
- `src/utils/editor/loadEditorScene.ts` handles editor-only folder upload parsing.
|
||||||
- `src/utils/loadMapSceneData.ts` is shared by the game scene and editor to load `public/map.json` and resolve model URLs.
|
- `src/utils/loadMapSceneData.ts` is shared by the game scene and editor to load `public/map.json` and resolve model URLs.
|
||||||
- `src/types/editor.ts` contains the shared `MapNode`, `SceneData`, and `TransformMode` types.
|
- `src/types/editor.ts` contains the shared `MapNode`, `SceneData`, and `TransformMode` types.
|
||||||
@@ -63,9 +63,9 @@ This document describes the code that exists today in the repository.
|
|||||||
|
|
||||||
## Current Limitations
|
## Current Limitations
|
||||||
|
|
||||||
- The repository is still a prototype, not the full intended game runtime.
|
- The repository is a prototype, not the full intended game runtime.
|
||||||
- `src/world/debug/TestScene.tsx` is still part of the active scene composition.
|
- `src/world/debug/TestScene.tsx` is part of the active scene composition.
|
||||||
- There is no central gameplay orchestrator such as `GameManager` yet.
|
- There is no central gameplay orchestrator such as `GameManager`.
|
||||||
- Missions, zones, cinematics, and dialogue systems are not implemented.
|
- Missions, zones, cinematics, and dialogue systems are not implemented.
|
||||||
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack.
|
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack.
|
||||||
- Editor save-to-server is implemented as a Vite dev-server plugin, not a production backend API.
|
- Editor save-to-server is implemented as a Vite dev-server plugin, not a production backend API.
|
||||||
|
|||||||
+20
-19
@@ -20,18 +20,19 @@ src/
|
|||||||
├── pages/
|
├── pages/
|
||||||
│ └── editor/
|
│ └── editor/
|
||||||
│ └── EditorPage.tsx
|
│ └── EditorPage.tsx
|
||||||
├── features/
|
├── components/
|
||||||
│ └── editor/
|
│ └── editor/
|
||||||
│ ├── components/
|
│ ├── EditorControls.tsx
|
||||||
│ │ └── EditorControls.tsx
|
│ └── scene/
|
||||||
│ ├── controls/
|
│ ├── EditorMap.tsx
|
||||||
│ │ └── FlyController.tsx
|
│ └── EditorScene.tsx
|
||||||
│ ├── hooks/
|
├── controls/
|
||||||
│ │ ├── useEditorHistory.ts
|
│ └── editor/
|
||||||
│ │ └── useEditorSceneData.ts
|
│ └── FlyController.tsx
|
||||||
│ ├── scene/
|
├── hooks/
|
||||||
│ │ ├── EditorMap.tsx
|
│ └── editor/
|
||||||
│ │ └── EditorScene.tsx
|
│ ├── useEditorHistory.ts
|
||||||
|
│ └── useEditorSceneData.ts
|
||||||
├── types/
|
├── types/
|
||||||
│ └── editor.ts
|
│ └── editor.ts
|
||||||
└── utils/
|
└── utils/
|
||||||
@@ -44,17 +45,17 @@ src/
|
|||||||
|
|
||||||
`src/pages/editor/EditorPage.tsx` is the route-level composition component. It owns route-specific state such as selected object, hovered object, transform mode, and player-mode toggle.
|
`src/pages/editor/EditorPage.tsx` is the route-level composition component. It owns route-specific state such as selected object, hovered object, transform mode, and player-mode toggle.
|
||||||
|
|
||||||
`src/features/editor/hooks/useEditorSceneData.ts` loads the default map data and handles folder uploads.
|
`src/hooks/editor/useEditorSceneData.ts` loads the default map data and handles folder uploads.
|
||||||
|
|
||||||
`src/features/editor/hooks/useEditorHistory.ts` owns editor undo and redo history.
|
`src/hooks/editor/useEditorHistory.ts` owns editor undo and redo history.
|
||||||
|
|
||||||
`src/features/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, keyboard shortcuts, and `EditorMap`.
|
`src/components/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, keyboard shortcuts, and `EditorMap`.
|
||||||
|
|
||||||
`src/features/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
`src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
||||||
|
|
||||||
`src/features/editor/components/EditorControls.tsx` renders the HTML control panel outside the canvas.
|
`src/components/editor/EditorControls.tsx` renders the HTML control panel outside the canvas.
|
||||||
|
|
||||||
`src/features/editor/controls/FlyController.tsx` provides editor movement controls for player-style navigation.
|
`src/controls/editor/FlyController.tsx` provides editor movement controls for player-style navigation.
|
||||||
|
|
||||||
`src/utils/loadMapSceneData.ts` is shared by the game map and editor. It loads `/map.json` and resolves available `public/models/{name}/model.gltf` files.
|
`src/utils/loadMapSceneData.ts` is shared by the game map and editor. It loads `/map.json` and resolves available `public/models/{name}/model.gltf` files.
|
||||||
|
|
||||||
@@ -138,6 +139,6 @@ Editor styles are in `src/index.css` under the `/* Editor page */` section. Clas
|
|||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
- Uploaded model object URLs are not currently revoked after replacement or unmount.
|
- Uploaded model object URLs are not currently revoked after replacement or unmount.
|
||||||
- Large `map.json` files may need virtualization, culling, or LOD support later.
|
- Large `map.json` files are not virtualized, culled, or LOD-managed.
|
||||||
- There is no snap-to-grid, duplication, material editing, or object creation workflow yet.
|
- There is no snap-to-grid, duplication, material editing, or object creation workflow.
|
||||||
- Save to Server is a Vite dev-server helper, not a production backend API.
|
- Save to Server is a Vite dev-server helper, not a production backend API.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ This document describes the intended medium-term architecture for the project.
|
|||||||
## Relationship To The Current Code
|
## Relationship To The Current Code
|
||||||
|
|
||||||
- `docs/technical/architecture.md` is the source of truth for what exists now.
|
- `docs/technical/architecture.md` is the source of truth for what exists now.
|
||||||
- This document is intentionally aspirational.
|
- This document describes intended direction, not implemented behavior.
|
||||||
- If this document conflicts with the current implementation, the current implementation wins.
|
- If this document conflicts with the current implementation, the current implementation wins.
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
@@ -40,12 +40,12 @@ This document describes the intended medium-term architecture for the project.
|
|||||||
- performance overlay
|
- performance overlay
|
||||||
- scene helpers
|
- scene helpers
|
||||||
- free camera and calibration controls
|
- free camera and calibration controls
|
||||||
- temporary test scenes used during development
|
- debug test scenes used during development
|
||||||
|
|
||||||
### UI Layer
|
### UI Layer
|
||||||
|
|
||||||
- `src/components/ui/` should contain player-facing HTML overlays.
|
- `src/components/ui/` should contain player-facing HTML overlays.
|
||||||
- Expected future examples:
|
- Candidate examples:
|
||||||
- crosshair
|
- crosshair
|
||||||
- loading flow
|
- loading flow
|
||||||
- mission HUD
|
- mission HUD
|
||||||
@@ -54,7 +54,7 @@ This document describes the intended medium-term architecture for the project.
|
|||||||
### Gameplay Layer
|
### Gameplay Layer
|
||||||
|
|
||||||
- As the project grows, gameplay state can move toward a clearer orchestration layer.
|
- As the project grows, gameplay state can move toward a clearer orchestration layer.
|
||||||
- Likely future concerns:
|
- Likely concerns:
|
||||||
- missions
|
- missions
|
||||||
- zones
|
- zones
|
||||||
- cinematics
|
- cinematics
|
||||||
@@ -67,4 +67,4 @@ This document describes the intended medium-term architecture for the project.
|
|||||||
- Prefer direct, working code over speculative scaffolding.
|
- Prefer direct, working code over speculative scaffolding.
|
||||||
- Shared types should stay close to their domain until they have multiple real consumers.
|
- Shared types should stay close to their domain until they have multiple real consumers.
|
||||||
- Avoid creating new managers or service layers without an active runtime need.
|
- Avoid creating new managers or service layers without an active runtime need.
|
||||||
- Debug-only runtime paths should be clearly marked and easy to remove later.
|
- Debug-only runtime paths should be clearly marked and easy to remove when obsolete.
|
||||||
|
|||||||
+2
-2
@@ -72,12 +72,12 @@ This is useful for checking numeric transform values before saving or exporting.
|
|||||||
|
|
||||||
`Save to server` is available only during local development. It writes the edited map back to `public/map.json` through the Vite dev-server endpoint.
|
`Save to server` is available only during local development. It writes the edited map back to `public/map.json` through the Vite dev-server endpoint.
|
||||||
|
|
||||||
The button is hidden in production builds because production persistence is not implemented yet.
|
The button is hidden in production builds because production persistence is not implemented.
|
||||||
|
|
||||||
## Current Limitations
|
## Current Limitations
|
||||||
|
|
||||||
- The editor only modifies existing nodes.
|
- The editor only modifies existing nodes.
|
||||||
- It does not create or delete objects yet.
|
- It does not create or delete objects.
|
||||||
- It does not edit model files or textures.
|
- It does not edit model files or textures.
|
||||||
- It does not provide production persistence.
|
- It does not provide production persistence.
|
||||||
- Fallback cubes indicate missing models; they are editor placeholders, not exported assets.
|
- Fallback cubes indicate missing models; they are editor placeholders, not exported assets.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
|||||||
import { InteractionManager } from "@/stateManager/InteractionManager";
|
import { InteractionManager } from "@/stateManager/InteractionManager";
|
||||||
import { INTERACTION_RADIUS } from "@/data/interaction/interactionConfig";
|
import { INTERACTION_RADIUS } from "@/data/interaction/interactionConfig";
|
||||||
import type { Vector3Tuple } from "@/types/3d";
|
import type { Vector3Tuple } from "@/types/3d";
|
||||||
import type { InteractableHandle, InteractableKind } from "@/types/interaction";
|
import type { InteractableHandle } from "@/types/interaction";
|
||||||
|
|
||||||
interface InteractableObjectBaseProps {
|
interface InteractableObjectBaseProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -37,46 +37,67 @@ type InteractableObjectProps =
|
|||||||
| TriggerInteractableObjectProps
|
| TriggerInteractableObjectProps
|
||||||
| GrabInteractableObjectProps;
|
| GrabInteractableObjectProps;
|
||||||
|
|
||||||
type MutableInteractableHandle = {
|
|
||||||
kind: InteractableKind;
|
|
||||||
label: string;
|
|
||||||
onPress: () => void;
|
|
||||||
onRelease?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _cameraPos = new THREE.Vector3();
|
const _cameraPos = new THREE.Vector3();
|
||||||
const _cameraDir = new THREE.Vector3();
|
const _cameraDir = new THREE.Vector3();
|
||||||
const _objectPos = new THREE.Vector3();
|
const _objectPos = new THREE.Vector3();
|
||||||
const _raycaster = new THREE.Raycaster();
|
const _raycaster = new THREE.Raycaster();
|
||||||
|
|
||||||
|
function createInteractableHandle(
|
||||||
|
props: InteractableObjectProps,
|
||||||
|
): InteractableHandle {
|
||||||
|
if (props.kind === "grab") {
|
||||||
|
return {
|
||||||
|
kind: props.kind,
|
||||||
|
label: props.label,
|
||||||
|
onPress: props.onPress,
|
||||||
|
onRelease: props.onRelease,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: props.kind,
|
||||||
|
label: props.label,
|
||||||
|
onPress: props.onPress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function InteractableObject(
|
export function InteractableObject(
|
||||||
props: InteractableObjectProps,
|
props: InteractableObjectProps,
|
||||||
): React.JSX.Element {
|
): React.JSX.Element {
|
||||||
const { kind, label, position, bodyRef, onPress, children } = props;
|
const { kind, label, position, bodyRef, onPress, children } = props;
|
||||||
const onRelease = props.kind === "grab" ? props.onRelease : undefined;
|
const onRelease = props.kind === "grab" ? props.onRelease : null;
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const debugSphereRef = useRef<THREE.Mesh>(null);
|
const debugSphereRef = useRef<THREE.Mesh>(null);
|
||||||
|
|
||||||
const handle = useRef<InteractableHandle>(
|
const handle = useRef<InteractableHandle>(createInteractableHandle(props));
|
||||||
props.kind === "grab"
|
|
||||||
? { kind: props.kind, label, onPress, onRelease: props.onRelease }
|
|
||||||
: { kind: props.kind, label, onPress },
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const current = handle.current as MutableInteractableHandle;
|
const currentHandle = handle.current;
|
||||||
current.kind = kind;
|
|
||||||
current.label = label;
|
if (currentHandle.kind === kind) {
|
||||||
current.onPress = onPress;
|
currentHandle.label = label;
|
||||||
|
currentHandle.onPress = onPress;
|
||||||
|
|
||||||
|
if (currentHandle.kind === "grab") {
|
||||||
|
if (!onRelease) return;
|
||||||
|
currentHandle.onRelease = onRelease;
|
||||||
|
}
|
||||||
|
|
||||||
if (kind === "grab" && onRelease) {
|
|
||||||
current.onRelease = onRelease;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete current.onRelease;
|
if (kind === "grab") {
|
||||||
return undefined;
|
if (!onRelease) return;
|
||||||
|
handle.current = { kind, label, onPress, onRelease };
|
||||||
|
} else {
|
||||||
|
handle.current = { kind, label, onPress };
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = InteractionManager.getInstance();
|
||||||
|
if (manager.getState().focused === currentHandle) {
|
||||||
|
manager.setFocused(handle.current);
|
||||||
|
}
|
||||||
}, [kind, label, onPress, onRelease]);
|
}, [kind, label, onPress, onRelease]);
|
||||||
|
|
||||||
const setupInteractionDebugFolder = useCallback((folder: GUI) => {
|
const setupInteractionDebugFolder = useCallback((folder: GUI) => {
|
||||||
|
|||||||
@@ -31,6 +31,20 @@ interface EditorControlsProps {
|
|||||||
isPlayerMode?: boolean;
|
isPlayerMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TRANSFORM_OPTIONS = [
|
||||||
|
{ mode: "translate", label: "Translate", shortcut: "T", Icon: Move3D },
|
||||||
|
{ mode: "rotate", label: "Rotate", shortcut: "R", Icon: RotateCw },
|
||||||
|
{ mode: "scale", label: "Scale", shortcut: "S", Icon: Expand },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const EDITOR_SHORTCUTS = [
|
||||||
|
["Click", "Select object"],
|
||||||
|
["T / R / S", "Transform mode"],
|
||||||
|
["Ctrl Z / Y", "Undo / redo"],
|
||||||
|
["Esc", "Deselect"],
|
||||||
|
["WASD", "Move when locked"],
|
||||||
|
] as const;
|
||||||
|
|
||||||
export function EditorControls({
|
export function EditorControls({
|
||||||
transformMode,
|
transformMode,
|
||||||
onTransformModeChange,
|
onTransformModeChange,
|
||||||
@@ -69,33 +83,18 @@ export function EditorControls({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="editor-transform-buttons">
|
<div className="editor-transform-buttons">
|
||||||
|
{TRANSFORM_OPTIONS.map(({ mode, label, shortcut, Icon }) => (
|
||||||
<button
|
<button
|
||||||
className={`editor-transform-button ${transformMode === "translate" ? "active" : ""}`}
|
key={mode}
|
||||||
onClick={() => onTransformModeChange("translate")}
|
className={`editor-transform-button ${transformMode === mode ? "active" : ""}`}
|
||||||
aria-pressed={transformMode === "translate"}
|
onClick={() => onTransformModeChange(mode)}
|
||||||
|
aria-pressed={transformMode === mode}
|
||||||
>
|
>
|
||||||
<Move3D size={16} aria-hidden="true" />
|
<Icon size={16} aria-hidden="true" />
|
||||||
<span>Translate</span>
|
<span>{label}</span>
|
||||||
<kbd>T</kbd>
|
<kbd>{shortcut}</kbd>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`editor-transform-button ${transformMode === "rotate" ? "active" : ""}`}
|
|
||||||
onClick={() => onTransformModeChange("rotate")}
|
|
||||||
aria-pressed={transformMode === "rotate"}
|
|
||||||
>
|
|
||||||
<RotateCw size={16} aria-hidden="true" />
|
|
||||||
<span>Rotate</span>
|
|
||||||
<kbd>R</kbd>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`editor-transform-button ${transformMode === "scale" ? "active" : ""}`}
|
|
||||||
onClick={() => onTransformModeChange("scale")}
|
|
||||||
aria-pressed={transformMode === "scale"}
|
|
||||||
>
|
|
||||||
<Expand size={16} aria-hidden="true" />
|
|
||||||
<span>Scale</span>
|
|
||||||
<kbd>S</kbd>
|
|
||||||
</button>
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="editor-history-buttons">
|
<div className="editor-history-buttons">
|
||||||
@@ -203,26 +202,12 @@ export function EditorControls({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dl className="editor-shortcuts-list">
|
<dl className="editor-shortcuts-list">
|
||||||
<div>
|
{EDITOR_SHORTCUTS.map(([keys, description]) => (
|
||||||
<dt>Click</dt>
|
<div key={keys}>
|
||||||
<dd>Select object</dd>
|
<dt>{keys}</dt>
|
||||||
</div>
|
<dd>{description}</dd>
|
||||||
<div>
|
|
||||||
<dt>T / R / S</dt>
|
|
||||||
<dd>Transform mode</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Ctrl Z / Y</dt>
|
|
||||||
<dd>Undo / redo</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Esc</dt>
|
|
||||||
<dd>Deselect</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>WASD</dt>
|
|
||||||
<dd>Move when locked</dd>
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ interface EditorNodeCommonProps {
|
|||||||
onHoverNode: (index: number | null) => void;
|
onHoverNode: (index: number | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EditorNodePointerHandlers {
|
||||||
|
onClick: (event: ThreeEvent<MouseEvent>) => void;
|
||||||
|
onPointerEnter: (event: ThreeEvent<PointerEvent>) => void;
|
||||||
|
onPointerLeave: (event: ThreeEvent<PointerEvent>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
function applyNodeTransform(object: THREE.Object3D, node: MapNode): void {
|
function applyNodeTransform(object: THREE.Object3D, node: MapNode): void {
|
||||||
object.position.set(...node.position);
|
object.position.set(...node.position);
|
||||||
object.rotation.set(...node.rotation);
|
object.rotation.set(...node.rotation);
|
||||||
@@ -88,6 +94,36 @@ function cloneHighlightedMaterial(
|
|||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNodeHighlightColor(
|
||||||
|
isSelected: boolean,
|
||||||
|
isHovered: boolean,
|
||||||
|
): string | null {
|
||||||
|
if (isSelected) return "#ffffff";
|
||||||
|
if (isHovered) return "#b8b8b8";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEditorNodePointerHandlers(
|
||||||
|
index: number,
|
||||||
|
onSelectNode: (index: number | null) => void,
|
||||||
|
onHoverNode: (index: number | null) => void,
|
||||||
|
): EditorNodePointerHandlers {
|
||||||
|
return {
|
||||||
|
onClick: (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onSelectNode(index);
|
||||||
|
},
|
||||||
|
onPointerEnter: (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onHoverNode(index);
|
||||||
|
},
|
||||||
|
onPointerLeave: (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onHoverNode(null);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function EditorMap({
|
export function EditorMap({
|
||||||
sceneData,
|
sceneData,
|
||||||
selectedNodeIndex,
|
selectedNodeIndex,
|
||||||
@@ -224,15 +260,16 @@ function EditorModelNode({
|
|||||||
const { scene } = useGLTF(modelUrl);
|
const { scene } = useGLTF(modelUrl);
|
||||||
|
|
||||||
const sceneInstance = useMemo(() => scene.clone(true), [scene]);
|
const sceneInstance = useMemo(() => scene.clone(true), [scene]);
|
||||||
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
|
index,
|
||||||
|
onSelectNode,
|
||||||
|
onHoverNode,
|
||||||
|
);
|
||||||
useRegisteredEditorNode(groupRef, index, node, objectsMapRef);
|
useRegisteredEditorNode(groupRef, index, node, objectsMapRef);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!groupRef.current) return;
|
if (!groupRef.current) return;
|
||||||
const highlightColor = isSelected
|
const highlightColor = getNodeHighlightColor(isSelected, isHovered);
|
||||||
? "#ffffff"
|
|
||||||
: isHovered
|
|
||||||
? "#b8b8b8"
|
|
||||||
: null;
|
|
||||||
|
|
||||||
groupRef.current.traverse((child) => {
|
groupRef.current.traverse((child) => {
|
||||||
if (!(child instanceof THREE.Mesh)) {
|
if (!(child instanceof THREE.Mesh)) {
|
||||||
@@ -288,18 +325,7 @@ function EditorModelNode({
|
|||||||
position={node.position}
|
position={node.position}
|
||||||
rotation={node.rotation}
|
rotation={node.rotation}
|
||||||
scale={node.scale}
|
scale={node.scale}
|
||||||
onClick={(e: ThreeEvent<MouseEvent>) => {
|
{...pointerHandlers}
|
||||||
e.stopPropagation();
|
|
||||||
onSelectNode(index);
|
|
||||||
}}
|
|
||||||
onPointerEnter={(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onHoverNode(index);
|
|
||||||
}}
|
|
||||||
onPointerLeave={(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onHoverNode(null);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -314,9 +340,14 @@ function EditorFallbackNode({
|
|||||||
onHoverNode,
|
onHoverNode,
|
||||||
}: EditorNodeCommonProps) {
|
}: EditorNodeCommonProps) {
|
||||||
const meshRef = useRef<THREE.Mesh>(null);
|
const meshRef = useRef<THREE.Mesh>(null);
|
||||||
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
|
index,
|
||||||
|
onSelectNode,
|
||||||
|
onHoverNode,
|
||||||
|
);
|
||||||
useRegisteredEditorNode(meshRef, index, node, objectsMapRef);
|
useRegisteredEditorNode(meshRef, index, node, objectsMapRef);
|
||||||
|
|
||||||
const color = isSelected ? "#ffffff" : isHovered ? "#b8b8b8" : "#6f6f6f";
|
const color = getNodeHighlightColor(isSelected, isHovered) ?? "#6f6f6f";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
@@ -324,18 +355,7 @@ function EditorFallbackNode({
|
|||||||
position={node.position}
|
position={node.position}
|
||||||
rotation={node.rotation}
|
rotation={node.rotation}
|
||||||
scale={node.scale}
|
scale={node.scale}
|
||||||
onClick={(e: ThreeEvent<MouseEvent>) => {
|
{...pointerHandlers}
|
||||||
e.stopPropagation();
|
|
||||||
onSelectNode(index);
|
|
||||||
}}
|
|
||||||
onPointerEnter={(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onHoverNode(index);
|
|
||||||
}}
|
|
||||||
onPointerLeave={(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onHoverNode(null);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<boxGeometry args={[1, 1, 1]} />
|
<boxGeometry args={[1, 1, 1]} />
|
||||||
<meshStandardMaterial color={color} />
|
<meshStandardMaterial color={color} />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createContext } from "react";
|
|||||||
|
|
||||||
export type DocsLanguage = "en" | "fr";
|
export type DocsLanguage = "en" | "fr";
|
||||||
|
|
||||||
export interface DocsLanguageContextValue {
|
interface DocsLanguageContextValue {
|
||||||
language: DocsLanguage;
|
language: DocsLanguage;
|
||||||
toggleLanguage: () => void;
|
toggleLanguage: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ export const INTERACTION_DEBUG_SPHERE_SEGMENTS = 16;
|
|||||||
export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15";
|
export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15";
|
||||||
export const INTERACTION_DEBUG_SPHERE_OPACITY = 0.25;
|
export const INTERACTION_DEBUG_SPHERE_OPACITY = 0.25;
|
||||||
|
|
||||||
export const MAP_DEBUG_BOX_HELPER_COLOR = 0x00ff88;
|
|
||||||
|
|
||||||
export const DEBUG_CAMERA_DAMPING_FACTOR = 0.05;
|
export const DEBUG_CAMERA_DAMPING_FACTOR = 0.05;
|
||||||
export const DEBUG_CAMERA_MIN_DISTANCE = 100;
|
export const DEBUG_CAMERA_MIN_DISTANCE = 100;
|
||||||
export const DEBUG_CAMERA_MAX_DISTANCE = 1000;
|
export const DEBUG_CAMERA_MAX_DISTANCE = 1000;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export interface DocSection {
|
interface DocSection {
|
||||||
path: string;
|
path: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
meta: string;
|
meta: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocGroup {
|
interface DocGroup {
|
||||||
label: string;
|
label: string;
|
||||||
sections: DocSection[];
|
sections: DocSection[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import { EditorControls } from "@/components/editor/EditorControls";
|
|||||||
import { EditorScene } from "@/components/editor/scene/EditorScene";
|
import { EditorScene } from "@/components/editor/scene/EditorScene";
|
||||||
import { useEditorHistory } from "@/hooks/editor/useEditorHistory";
|
import { useEditorHistory } from "@/hooks/editor/useEditorHistory";
|
||||||
import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData";
|
import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData";
|
||||||
import type { MapNode, TransformMode } from "@/types/editor";
|
import type { MapNode, SceneData, TransformMode } from "@/types/editor";
|
||||||
|
|
||||||
|
const SAVE_ERROR_MESSAGE = "Erreur lors de l'enregistrement";
|
||||||
|
|
||||||
|
function serializeMapNodes(sceneData: SceneData): string {
|
||||||
|
return JSON.stringify(sceneData.mapNodes, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
export function EditorPage(): React.JSX.Element {
|
export function EditorPage(): React.JSX.Element {
|
||||||
const {
|
const {
|
||||||
@@ -46,7 +52,7 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
|
|
||||||
const handleSaveToServer = useCallback(async () => {
|
const handleSaveToServer = useCallback(async () => {
|
||||||
if (!sceneData) return;
|
if (!sceneData) return;
|
||||||
const json = JSON.stringify(sceneData.mapNodes, null, 2);
|
const json = serializeMapNodes(sceneData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/save-map", {
|
const response = await fetch("/api/save-map", {
|
||||||
@@ -58,17 +64,17 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
alert("Map enregistrée avec succès!");
|
alert("Map enregistrée avec succès!");
|
||||||
} else {
|
} else {
|
||||||
alert("Erreur lors de l'enregistrement");
|
alert(SAVE_ERROR_MESSAGE);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error saving map:", err);
|
console.error("Error saving map:", err);
|
||||||
alert("Erreur lors de l'enregistrement");
|
alert(SAVE_ERROR_MESSAGE);
|
||||||
}
|
}
|
||||||
}, [sceneData]);
|
}, [sceneData]);
|
||||||
|
|
||||||
const handleExportJson = useCallback(() => {
|
const handleExportJson = useCallback(() => {
|
||||||
if (!sceneData) return;
|
if (!sceneData) return;
|
||||||
const json = JSON.stringify(sceneData.mapNodes, null, 2);
|
const json = serializeMapNodes(sceneData);
|
||||||
const blob = new Blob([json], { type: "application/json" });
|
const blob = new Blob([json], { type: "application/json" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import {
|
|||||||
PLAYER_EYE_HEIGHT,
|
PLAYER_EYE_HEIGHT,
|
||||||
PLAYER_SPAWN_POSITION_GAME,
|
PLAYER_SPAWN_POSITION_GAME,
|
||||||
} from "@/data/player/playerConfig";
|
} from "@/data/player/playerConfig";
|
||||||
|
import type { Vector3Tuple } from "@/types/3d";
|
||||||
|
|
||||||
const DEBUG_CAMERA_TARGET = [
|
const DEBUG_CAMERA_TARGET: Vector3Tuple = [
|
||||||
PLAYER_SPAWN_POSITION_GAME[0],
|
PLAYER_SPAWN_POSITION_GAME[0],
|
||||||
PLAYER_EYE_HEIGHT,
|
PLAYER_EYE_HEIGHT,
|
||||||
PLAYER_SPAWN_POSITION_GAME[2],
|
PLAYER_SPAWN_POSITION_GAME[2],
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
export function DebugCameraControls(): React.JSX.Element {
|
export function DebugCameraControls(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { MapNode, SceneData } from "@/types/editor";
|
|||||||
|
|
||||||
const MAP_JSON_PATH = "/map.json";
|
const MAP_JSON_PATH = "/map.json";
|
||||||
const MODEL_FILE_NAME = "model.gltf";
|
const MODEL_FILE_NAME = "model.gltf";
|
||||||
|
type ModelEntry = [modelName: string, modelUrl: string];
|
||||||
|
|
||||||
export async function loadMapSceneData(): Promise<SceneData | null> {
|
export async function loadMapSceneData(): Promise<SceneData | null> {
|
||||||
const response = await fetch(MAP_JSON_PATH);
|
const response = await fetch(MAP_JSON_PATH);
|
||||||
@@ -29,7 +30,8 @@ async function loadMapModelUrls(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(modelUrl, { method: "HEAD" });
|
const response = await fetch(modelUrl, { method: "HEAD" });
|
||||||
return response.ok ? ([modelName, modelUrl] as const) : null;
|
const modelEntry: ModelEntry = [modelName, modelUrl];
|
||||||
|
return response.ok ? modelEntry : null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { useThree } from "@react-three/fiber";
|
|
||||||
import { useGLTF } from "@react-three/drei";
|
|
||||||
import * as THREE from "three";
|
|
||||||
import { MAP_DEBUG_BOX_HELPER_COLOR } from "@/data/debug/debugConfig";
|
|
||||||
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
|
||||||
import type { OctreeReadyHandler } from "@/types/3d";
|
|
||||||
import { Debug } from "@/utils/debug/Debug";
|
|
||||||
|
|
||||||
const MAP_PATH = "/models/map/model.gltf";
|
|
||||||
|
|
||||||
interface MapProps {
|
|
||||||
onOctreeReady: OctreeReadyHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Map({ onOctreeReady }: MapProps): React.JSX.Element {
|
|
||||||
const { scene: gltfScene } = useGLTF(MAP_PATH);
|
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
|
||||||
const boxHelpersRef = useRef<THREE.BoxHelper[]>([]);
|
|
||||||
const { scene } = useThree();
|
|
||||||
|
|
||||||
useOctreeGraphNode(groupRef, onOctreeReady);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const debug = Debug.getInstance();
|
|
||||||
if (!debug.active || !groupRef.current) return;
|
|
||||||
|
|
||||||
const helpers: THREE.BoxHelper[] = [];
|
|
||||||
|
|
||||||
groupRef.current.traverse((child) => {
|
|
||||||
if (!(child instanceof THREE.Mesh)) return;
|
|
||||||
const helper = new THREE.BoxHelper(child, MAP_DEBUG_BOX_HELPER_COLOR);
|
|
||||||
scene.add(helper);
|
|
||||||
helpers.push(helper);
|
|
||||||
});
|
|
||||||
|
|
||||||
boxHelpersRef.current = helpers;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
helpers.forEach((h) => {
|
|
||||||
scene.remove(h);
|
|
||||||
h.dispose();
|
|
||||||
});
|
|
||||||
boxHelpersRef.current = [];
|
|
||||||
};
|
|
||||||
}, [scene]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group ref={groupRef}>
|
|
||||||
<primitive object={gltfScene} />
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
useGLTF.preload(MAP_PATH);
|
|
||||||
@@ -54,6 +54,25 @@ const _up = new THREE.Vector3(0, 1, 0);
|
|||||||
const _translateVec = new THREE.Vector3();
|
const _translateVec = new THREE.Vector3();
|
||||||
const _collisionCorrection = new THREE.Vector3();
|
const _collisionCorrection = new THREE.Vector3();
|
||||||
|
|
||||||
|
function setMovementKey(keys: Keys, key: string, pressed: boolean): boolean {
|
||||||
|
switch (key.toLowerCase()) {
|
||||||
|
case MOVE_FORWARD_KEY:
|
||||||
|
keys.forward = pressed;
|
||||||
|
return true;
|
||||||
|
case MOVE_BACKWARD_KEY:
|
||||||
|
keys.backward = pressed;
|
||||||
|
return true;
|
||||||
|
case MOVE_LEFT_KEY:
|
||||||
|
keys.left = pressed;
|
||||||
|
return true;
|
||||||
|
case MOVE_RIGHT_KEY:
|
||||||
|
keys.right = pressed;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function PlayerController({
|
export function PlayerController({
|
||||||
octree,
|
octree,
|
||||||
spawnPosition,
|
spawnPosition,
|
||||||
@@ -89,51 +108,29 @@ export function PlayerController({
|
|||||||
const interaction = InteractionManager.getInstance();
|
const interaction = InteractionManager.getInstance();
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||||
switch (event.key.toLowerCase()) {
|
if (setMovementKey(keys.current, event.key, true)) {
|
||||||
case MOVE_FORWARD_KEY:
|
event.preventDefault();
|
||||||
keys.current.forward = true;
|
return;
|
||||||
break;
|
}
|
||||||
case MOVE_BACKWARD_KEY:
|
|
||||||
keys.current.backward = true;
|
if (event.key === JUMP_KEY) {
|
||||||
break;
|
|
||||||
case MOVE_LEFT_KEY:
|
|
||||||
keys.current.left = true;
|
|
||||||
break;
|
|
||||||
case MOVE_RIGHT_KEY:
|
|
||||||
keys.current.right = true;
|
|
||||||
break;
|
|
||||||
case JUMP_KEY:
|
|
||||||
wantsJump.current = true;
|
wantsJump.current = true;
|
||||||
break;
|
event.preventDefault();
|
||||||
case INTERACT_KEY:
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key.toLowerCase() === INTERACT_KEY) {
|
||||||
if (interaction.getState().focused?.kind === "trigger") {
|
if (interaction.getState().focused?.kind === "trigger") {
|
||||||
interaction.pressInteract();
|
interaction.pressInteract();
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyUp = (event: KeyboardEvent): void => {
|
const handleKeyUp = (event: KeyboardEvent): void => {
|
||||||
switch (event.key.toLowerCase()) {
|
if (setMovementKey(keys.current, event.key, false)) {
|
||||||
case MOVE_FORWARD_KEY:
|
|
||||||
keys.current.forward = false;
|
|
||||||
break;
|
|
||||||
case MOVE_BACKWARD_KEY:
|
|
||||||
keys.current.backward = false;
|
|
||||||
break;
|
|
||||||
case MOVE_LEFT_KEY:
|
|
||||||
keys.current.left = false;
|
|
||||||
break;
|
|
||||||
case MOVE_RIGHT_KEY:
|
|
||||||
keys.current.right = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseDown = (event: MouseEvent): void => {
|
const handleMouseDown = (event: MouseEvent): void => {
|
||||||
|
|||||||
Reference in New Issue
Block a user