Merge pull request 'Feat/env-manager2' (#4) from feat/env-manager into develop
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
@@ -47,6 +47,7 @@ Keep the player and map octree outside the Rapier provider until there is a deli
|
|||||||
- `src/managers/AudioManager.ts` provides pooled one-shot playback, looped music playback, category volumes, and optional stereo pan for one-shot sounds.
|
- `src/managers/AudioManager.ts` provides pooled one-shot playback, looped music playback, category volumes, and optional stereo pan for one-shot sounds.
|
||||||
- Supported audio categories are `music`, `sfx`, and `dialogue`.
|
- Supported audio categories are `music`, `sfx`, and `dialogue`.
|
||||||
- Trigger interactions may play SFX directly through `AudioManager`.
|
- Trigger interactions may play SFX directly through `AudioManager`.
|
||||||
|
- Detailed audio documentation lives in `docs/technical/audio.md`.
|
||||||
|
|
||||||
## Settings Menu
|
## Settings Menu
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
# Audio Technical Notes
|
||||||
|
|
||||||
|
This document describes the audio systems that exist in the current codebase.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Audio is currently split into three runtime categories:
|
||||||
|
|
||||||
|
- `music`: looped background music
|
||||||
|
- `dialogue`: spoken dialogue audio linked to subtitles
|
||||||
|
- `sfx`: one-shot interaction and feedback sounds
|
||||||
|
|
||||||
|
The shared runtime service is `src/managers/AudioManager.ts`. User-facing volume settings live in `src/managers/stores/useSettingsStore.ts` and are forwarded to `AudioManager` by category.
|
||||||
|
|
||||||
|
## AudioManager
|
||||||
|
|
||||||
|
`AudioManager` is a singleton side-effect service. It owns browser audio elements, category volumes, pooled one-shot sounds, music playback, and stereo panning for one-shot sounds.
|
||||||
|
|
||||||
|
Supported public methods:
|
||||||
|
|
||||||
|
- `playMusic(path, volume)`: starts or updates a looped music track.
|
||||||
|
- `stopMusic()`: stops the active music track.
|
||||||
|
- `playSound(path, volume, options)`: plays a pooled one-shot sound and returns its `HTMLAudioElement`.
|
||||||
|
- `setCategoryVolume(category, volume)`: updates `music`, `sfx`, or `dialogue` volume.
|
||||||
|
- `getCategoryVolume(category)`: reads the current category volume.
|
||||||
|
- `destroy()`: stops music, clears pools, closes the audio context, and resets the singleton.
|
||||||
|
|
||||||
|
One-shot sounds are pooled by path with a maximum pool size per sound. If every element in a pool is busy, the pool grows until the limit, then recycles an existing element.
|
||||||
|
|
||||||
|
Browser autoplay restrictions are handled in `playMusic()`: if playback is blocked by the browser, the manager waits for a user `pointerdown` or `keydown`, then retries the music.
|
||||||
|
|
||||||
|
## Music
|
||||||
|
|
||||||
|
Runtime music is mounted by `src/world/GameMusic.tsx`.
|
||||||
|
|
||||||
|
Current behavior:
|
||||||
|
|
||||||
|
- `GameMusic` calls `AudioManager.getInstance().playMusic()` on mount.
|
||||||
|
- The current music path is `/sounds/musique/test.mp3`.
|
||||||
|
- The base music volume is `0.33` before category volume is applied.
|
||||||
|
- On unmount, `GameMusic` calls `stopMusic()`.
|
||||||
|
|
||||||
|
Effective music volume is:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
base music volume * settings music volume
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `music` only for long-running looped background tracks. Do not use `playSound()` for music, because one-shot pooling is designed for short overlapping sounds.
|
||||||
|
|
||||||
|
## Sound Effects
|
||||||
|
|
||||||
|
SFX are short one-shot sounds. They should use `AudioManager.playSound()` with the default category or with `{ category: "sfx" }`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
AudioManager.getInstance().playSound("/sounds/sfx/click.mp3", 0.8, {
|
||||||
|
category: "sfx",
|
||||||
|
pan: 0,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful options:
|
||||||
|
|
||||||
|
- `category`: `sfx` or `dialogue`; defaults to `sfx`.
|
||||||
|
- `pan`: stereo panning from `-1` left to `1` right.
|
||||||
|
- `playbackRate`: playback speed multiplier.
|
||||||
|
|
||||||
|
SFX volume is controlled by the settings menu through the `sfx` category volume.
|
||||||
|
|
||||||
|
## Dialogues
|
||||||
|
|
||||||
|
Runtime dialogue data lives under `public/sounds/dialogue/`.
|
||||||
|
|
||||||
|
```txt
|
||||||
|
public/
|
||||||
|
└── sounds/
|
||||||
|
└── dialogue/
|
||||||
|
├── dialogues.json
|
||||||
|
└── subtitles/
|
||||||
|
├── fr/
|
||||||
|
│ ├── narrateur.srt
|
||||||
|
│ ├── fermier.srt
|
||||||
|
│ └── electricienne.srt
|
||||||
|
└── en/
|
||||||
|
├── narrateur.srt
|
||||||
|
├── fermier.srt
|
||||||
|
└── electricienne.srt
|
||||||
|
```
|
||||||
|
|
||||||
|
The dialogue manifest shape is defined in `src/types/dialogues/dialogues.ts`.
|
||||||
|
|
||||||
|
Each dialogue entry contains:
|
||||||
|
|
||||||
|
- `id`: stable dialogue identifier
|
||||||
|
- `voice`: voice group, currently `narrateur`, `fermier`, or `electricienne`
|
||||||
|
- `audio`: runtime audio path
|
||||||
|
- `subtitleCueIndex`: cue number inside that voice/language SRT file
|
||||||
|
- `timecode`: optional global trigger time in seconds from scene start
|
||||||
|
|
||||||
|
Dialogues are played through `src/utils/dialogues/playDialogue.ts`.
|
||||||
|
|
||||||
|
Important functions:
|
||||||
|
|
||||||
|
- `playDialogueById(manifest, dialogueId)`: plays a dialogue from an already loaded manifest.
|
||||||
|
- `queueDialogueById(manifest, dialogueId)`: queues dialogue playback so multiple requests do not overlap.
|
||||||
|
- `playGameplayDialogueById(dialogueId)`: loads the gameplay manifest once and queues a dialogue by ID.
|
||||||
|
- `clearQueuedDialogues()`: resolves pending dialogue requests and clears the queue.
|
||||||
|
|
||||||
|
Dialogue audio uses `AudioManager.playSound()` with `{ category: "dialogue" }`, so it follows the dialogue volume setting.
|
||||||
|
|
||||||
|
## Dialogue And SRT Link
|
||||||
|
|
||||||
|
The subtitle model is one SRT file per voice and language, not one SRT file per dialogue.
|
||||||
|
|
||||||
|
A dialogue chooses its subtitle by combining:
|
||||||
|
|
||||||
|
1. `voice`
|
||||||
|
2. selected subtitle language from settings
|
||||||
|
3. `subtitleCueIndex`
|
||||||
|
|
||||||
|
For example, this dialogue:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "narrateur_bienvenueaaltera",
|
||||||
|
"voice": "narrateur",
|
||||||
|
"audio": "/sounds/dialogue/narrateur/bienvenueaaltera.mp3",
|
||||||
|
"subtitleCueIndex": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
loads cue `1` from:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
public/sounds/dialogue/subtitles/fr/narrateur.srt
|
||||||
|
```
|
||||||
|
|
||||||
|
when the subtitle language is French, or from:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
public/sounds/dialogue/subtitles/en/narrateur.srt
|
||||||
|
```
|
||||||
|
|
||||||
|
when the subtitle language is English.
|
||||||
|
|
||||||
|
If the selected language is missing, the loader falls back to French. Missing English SRT files are warnings during validation, not runtime errors.
|
||||||
|
|
||||||
|
SRT timecodes are relative to the dialogue audio file. They are not relative to the game clock and not relative to a cinematic timeline.
|
||||||
|
|
||||||
|
## Subtitle Runtime
|
||||||
|
|
||||||
|
`playDialogueById()` loads the matching subtitle cue with `loadDialogueSubtitleCue()` before playing the audio.
|
||||||
|
|
||||||
|
While audio plays:
|
||||||
|
|
||||||
|
- `timeupdate` checks `audio.currentTime`
|
||||||
|
- the active subtitle is written to `useSubtitleStore`
|
||||||
|
- `src/components/ui/Subtitles.tsx` renders the current speaker and text
|
||||||
|
- `ended` and `pause` clear the subtitle
|
||||||
|
|
||||||
|
The subtitle overlay respects settings from `useSettingsStore`, including visibility and selected language.
|
||||||
|
|
||||||
|
## Global Timecode Dialogues
|
||||||
|
|
||||||
|
`src/world/GameDialogues.tsx` loads the dialogue manifest and triggers entries that define `timecode`.
|
||||||
|
|
||||||
|
This is useful for simple global scene timing. It should not be used for dialogue that belongs to a cinematic. Cinematic-owned dialogue should be triggered by `dialogueCues` in `public/cinematics.json` instead, otherwise the same dialogue can play twice.
|
||||||
|
|
||||||
|
## Cinematic Dialogue Cues
|
||||||
|
|
||||||
|
`public/cinematics.json` can include `dialogueCues`.
|
||||||
|
|
||||||
|
Each cue contains:
|
||||||
|
|
||||||
|
- `time`: seconds relative to the cinematic start
|
||||||
|
- `dialogueId`: ID from `dialogues.json`
|
||||||
|
|
||||||
|
`src/world/GameCinematics.tsx` uses those cues to play dialogue during camera timelines. This keeps camera movement and dialogue playback synchronized without relying on global scene time.
|
||||||
|
|
||||||
|
## Editor Tooling
|
||||||
|
|
||||||
|
The `/editor` route provides three audio-related tools:
|
||||||
|
|
||||||
|
- `Dialogues`: edits `public/sounds/dialogue/dialogues.json` and previews dialogue playback.
|
||||||
|
- `SRT`: edits one SRT file at a time and validates dialogue assets.
|
||||||
|
- `Cinematics`: links dialogue IDs to cinematic timelines through `dialogueCues`.
|
||||||
|
|
||||||
|
Dev-only Vite endpoints in `vite.config.ts` support local saves:
|
||||||
|
|
||||||
|
- `POST /api/save-dialogues`
|
||||||
|
- `POST /api/save-srt`
|
||||||
|
- `GET /api/validate-dialogues`
|
||||||
|
- `POST /api/save-cinematics`
|
||||||
|
|
||||||
|
These endpoints are local development helpers. They are not production APIs.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
`GET /api/validate-dialogues` validates:
|
||||||
|
|
||||||
|
- manifest shape
|
||||||
|
- referenced dialogue audio files
|
||||||
|
- French SRT files
|
||||||
|
- referenced subtitle cue indexes
|
||||||
|
- optional English SRT files as warnings
|
||||||
|
|
||||||
|
Run validation after adding or renaming dialogue audio, changing cue indexes, or editing SRT files.
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
- There is no production persistence for audio manifests or SRT files.
|
||||||
|
- Dialogue branching is not implemented.
|
||||||
|
- Dialogue interruption and priority rules are minimal; playback is queue-based.
|
||||||
|
- SRT editing is text-based and does not yet provide waveform editing.
|
||||||
|
- Music currently supports one active looped track at a time.
|
||||||
@@ -52,7 +52,7 @@ src/
|
|||||||
|
|
||||||
## Responsibilities
|
## Responsibilities
|
||||||
|
|
||||||
`src/pages/editor/page.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/page.tsx` is the route-level composition component. It owns route-specific state such as selected object, hovered object, transform mode, selection lock, player-mode toggle, cinematic preview requests, and editor scene loading state.
|
||||||
|
|
||||||
`src/hooks/editor/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.
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ src/
|
|||||||
|
|
||||||
`src/components/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/components/editor/EditorControls.tsx` renders the HTML control panel outside the canvas.
|
`src/components/editor/EditorControls.tsx` renders the HTML control panel outside the canvas. The panel is organized into top-level `details` groups: `Editor`, `Cinematics`, `Dialogues`, and `SRT`.
|
||||||
|
|
||||||
`src/components/editor/EditorDialogueManifestPanel.tsx` renders the dialogue manifest editor. It loads `dialogues.json`, edits dialogue entries, previews selected dialogue playback, creates missing French SRT cues, and saves the manifest through a dev-server endpoint.
|
`src/components/editor/EditorDialogueManifestPanel.tsx` renders the dialogue manifest editor. It loads `dialogues.json`, edits dialogue entries, previews selected dialogue playback, creates missing French SRT cues, and saves the manifest through a dev-server endpoint.
|
||||||
|
|
||||||
@@ -122,13 +122,17 @@ If `model.glb` and `model.gltf` are both missing, the editor renders a fallback
|
|||||||
2. `useEditorSceneData` calls `loadMapSceneData()`.
|
2. `useEditorSceneData` calls `loadMapSceneData()`.
|
||||||
3. `loadMapSceneData()` loads `/map.json` and available model URLs.
|
3. `loadMapSceneData()` loads `/map.json` and available model URLs.
|
||||||
4. If `/map.json` is missing, the page displays a folder-upload flow.
|
4. If `/map.json` is missing, the page displays a folder-upload flow.
|
||||||
5. `EditorScene` renders the grid, lights, camera controls, and map nodes.
|
5. `EditorSceneLoadingTracker` uses drei `useProgress()` to update the fullscreen editor loading overlay while models load.
|
||||||
6. `EditorControls` exposes transform mode, history actions, export, save, and selection info.
|
6. `EditorScene` renders the grid, lights, camera controls, and map nodes inside `Suspense`.
|
||||||
|
7. `EditorControls` exposes transform mode, history actions, export, save, JSON preview, selection lock, and the cinematic/dialogue/SRT editors.
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
- Click: select a node.
|
- Click: select a node.
|
||||||
- `Esc`: clear selection.
|
- `Esc`: clear selection.
|
||||||
|
- Click empty space: clear selection.
|
||||||
|
- Selection lock button: prevent object clicks, empty-space clicks, and `Esc` from changing the current selection.
|
||||||
|
- Selection clear button: intentionally clear the current selection even when the lock is active.
|
||||||
- `T`: translate mode.
|
- `T`: translate mode.
|
||||||
- `R`: rotate mode.
|
- `R`: rotate mode.
|
||||||
- `S`: scale mode.
|
- `S`: scale mode.
|
||||||
@@ -147,6 +151,51 @@ The editor supports two output paths:
|
|||||||
|
|
||||||
The dev-only `/api/save-map` endpoint is implemented by the Vite plugin in `vite.config.ts`. It writes to `public/map.json` and enforces a maximum payload size.
|
The dev-only `/api/save-map` endpoint is implemented by the Vite plugin in `vite.config.ts`. It writes to `public/map.json` and enforces a maximum payload size.
|
||||||
|
|
||||||
|
## Editor Loading Overlay
|
||||||
|
|
||||||
|
The editor uses `SceneLoadingOverlay` like the runtime scene. `EditorSceneLoadingTracker` lives in `src/pages/editor/page.tsx` and reads drei `useProgress()` inside the canvas.
|
||||||
|
|
||||||
|
The route tracks two loading phases:
|
||||||
|
|
||||||
|
- map JSON loading through `useEditorSceneData()`
|
||||||
|
- model loading through `useProgress()`
|
||||||
|
|
||||||
|
The overlay is rendered outside the canvas so it remains visible while the R3F scene mounts. The scene itself is wrapped in `Suspense` with a `null` fallback; the visual feedback is handled by the overlay instead of by the canvas fallback.
|
||||||
|
|
||||||
|
## Panel Groups
|
||||||
|
|
||||||
|
`EditorControls` uses the local `EditorPanelGroup` helper to keep the side panel navigable as tools grow.
|
||||||
|
|
||||||
|
Current group order:
|
||||||
|
|
||||||
|
1. `Editor`
|
||||||
|
2. `Cinematics`
|
||||||
|
3. `Dialogues`
|
||||||
|
4. `SRT`
|
||||||
|
|
||||||
|
Inside the `Editor` group, the section order is:
|
||||||
|
|
||||||
|
1. `Shortcuts`
|
||||||
|
2. `Transform`
|
||||||
|
3. `Selection`
|
||||||
|
4. `View`
|
||||||
|
5. `JSON`
|
||||||
|
6. `File`
|
||||||
|
|
||||||
|
The `Shortcuts` group is nested and closed by default to reduce visual noise.
|
||||||
|
|
||||||
|
## Selection Lock
|
||||||
|
|
||||||
|
Selection lock is owned by `EditorPage` through `isSelectionLocked`.
|
||||||
|
|
||||||
|
The state is passed to:
|
||||||
|
|
||||||
|
- `EditorControls`, to render the lock/unlock button
|
||||||
|
- `EditorScene`, to block `Esc` deselection when locked
|
||||||
|
- `EditorMap`, to block object selection and empty-space deselection when locked
|
||||||
|
|
||||||
|
The clear button calls `onClearSelection` directly from `EditorControls`. This is intentionally separate from scene click behavior so the user always has an explicit way to clear the selection.
|
||||||
|
|
||||||
## Dialogue SRT Editing
|
## Dialogue SRT Editing
|
||||||
|
|
||||||
Dialogue subtitle editing is part of the `/editor` side panel.
|
Dialogue subtitle editing is part of the `/editor` side panel.
|
||||||
|
|||||||
+85
-7
@@ -1,12 +1,18 @@
|
|||||||
# Editor User Guide
|
# Editor User Guide
|
||||||
|
|
||||||
The map editor is available at `/editor`. It is a browser-based tool for inspecting and adjusting the objects listed in `public/map.json`.
|
The map editor is available at `/editor`. It is a browser-based tool for editing the runtime map, cinematic manifest, dialogue manifest, and SRT subtitle files without manually jumping between JSON and subtitle files.
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
Use the editor when you need to move, rotate, or scale existing map objects without editing JSON by hand.
|
Use the editor when you need to:
|
||||||
|
|
||||||
The editor reads the same map data as the runtime scene:
|
- move, rotate, or scale objects from `public/map.json`
|
||||||
|
- inspect the raw JSON generated by the editor
|
||||||
|
- preview and edit cinematics from `public/cinematics.json`
|
||||||
|
- create, preview, and validate dialogue entries from `public/sounds/dialogue/dialogues.json`
|
||||||
|
- edit FR/EN SRT subtitle files per voice
|
||||||
|
|
||||||
|
The map editor reads the same map data as the runtime scene:
|
||||||
|
|
||||||
- `public/map.json` contains the object list.
|
- `public/map.json` contains the object list.
|
||||||
- `public/models/{name}/model.glb` contains the matching 3D model for each object name. `model.gltf` is still supported as a fallback during migration.
|
- `public/models/{name}/model.glb` contains the matching 3D model for each object name. `model.gltf` is still supported as a fallback during migration.
|
||||||
@@ -24,7 +30,18 @@ Each entry in `public/map.json` represents one object:
|
|||||||
| `rotation` | Object rotation as `[x, y, z]`, expressed radians |
|
| `rotation` | Object rotation as `[x, y, z]`, expressed radians |
|
||||||
| `scale` | Object scale as `[x, y, z]` |
|
| `scale` | Object scale as `[x, y, z]` |
|
||||||
|
|
||||||
## Editing Workflow
|
## Panel Layout
|
||||||
|
|
||||||
|
The right panel is split into dropdown groups:
|
||||||
|
|
||||||
|
- `Editor`: map transform tools, shortcuts, selection, view mode, JSON preview, and file actions.
|
||||||
|
- `Cinematics`: editor for `public/cinematics.json`.
|
||||||
|
- `Dialogues`: editor for `public/sounds/dialogue/dialogues.json`.
|
||||||
|
- `SRT`: editor for subtitle files in `public/sounds/dialogue/subtitles/`.
|
||||||
|
|
||||||
|
Only the `Editor` group is open by default. Open the other groups when you need audio or cinematic tooling.
|
||||||
|
|
||||||
|
## Map Editing Workflow
|
||||||
|
|
||||||
1. Open `/editor` in the local app.
|
1. Open `/editor` in the local app.
|
||||||
2. Click an object in the scene to select it.
|
2. Click an object in the scene to select it.
|
||||||
@@ -40,6 +57,8 @@ Each entry in `public/map.json` represents one object:
|
|||||||
| -------------------- | -------------------------- |
|
| -------------------- | -------------------------- |
|
||||||
| Select object | Click object |
|
| Select object | Click object |
|
||||||
| Deselect | `Esc` or click empty space |
|
| Deselect | `Esc` or click empty space |
|
||||||
|
| Lock selection | `Lock` button in Selection |
|
||||||
|
| Clear selection | `X` button in Selection |
|
||||||
| Translate mode | `T` |
|
| Translate mode | `T` |
|
||||||
| Rotate mode | `R` |
|
| Rotate mode | `R` |
|
||||||
| Scale mode | `S` |
|
| Scale mode | `S` |
|
||||||
@@ -49,18 +68,34 @@ Each entry in `public/map.json` represents one object:
|
|||||||
| Move up | `Space` |
|
| Move up | `Space` |
|
||||||
| Move down | `Shift` |
|
| Move down | `Shift` |
|
||||||
|
|
||||||
|
## Selection
|
||||||
|
|
||||||
|
The `Selection` section shows the selected object name and its index in `public/map.json`.
|
||||||
|
|
||||||
|
- Click an object to select it.
|
||||||
|
- Click empty space or press `Esc` to clear the selection.
|
||||||
|
- Use the `X` button to clear the selection explicitly.
|
||||||
|
- Use the `Lock` button to protect the current selection while editing.
|
||||||
|
|
||||||
|
When selection is locked:
|
||||||
|
|
||||||
|
- clicking another object does not change the selection
|
||||||
|
- clicking empty space does not clear the selection
|
||||||
|
- pressing `Esc` does not clear the selection
|
||||||
|
- the `X` button still clears the selection intentionally
|
||||||
|
|
||||||
## View Mode
|
## View Mode
|
||||||
|
|
||||||
The `Lock view` action switches the editor into a movement mode closer to the runtime player camera. Use it to navigate larger scenes while keeping the transform tools available.
|
The `Lock view` action switches the editor into a movement mode closer to the runtime player camera. Use it to navigate larger scenes while keeping the transform tools available.
|
||||||
|
|
||||||
## JSON Inspector
|
## JSON Inspector
|
||||||
|
|
||||||
The side panel includes a raw JSON inspector:
|
The `JSON` section shows the raw map data that will be exported or saved:
|
||||||
|
|
||||||
- When no object is selected, it shows the full map node list.
|
- When no object is selected, it shows the full map node list.
|
||||||
- When an object is selected, it highlights the JSON lines for that object.
|
- When an object is selected, it highlights the JSON lines for that object.
|
||||||
|
|
||||||
This is useful for checking numeric transform values before saving or exporting.
|
Use it to verify exact numeric transform values before saving or exporting. The JSON inspector is read-only; transform values are changed through the gizmo in the scene.
|
||||||
|
|
||||||
## Saving Changes
|
## Saving Changes
|
||||||
|
|
||||||
@@ -76,12 +111,27 @@ The button is hidden in production builds because production persistence is not
|
|||||||
|
|
||||||
## Editing Dialogue Subtitles
|
## Editing Dialogue Subtitles
|
||||||
|
|
||||||
The side panel also includes dialogue tools for the dialogue manifest and SRT subtitles.
|
The side panel includes two separate audio text tools:
|
||||||
|
|
||||||
|
- `Dialogues` edits the dialogue manifest, which links dialogue IDs to audio files and SRT cue indexes.
|
||||||
|
- `SRT` edits the actual subtitle text and cue timings.
|
||||||
|
|
||||||
|
The important model is: one dialogue entry points to one cue inside one SRT file. The SRT file is grouped by voice and language, not by dialogue.
|
||||||
|
|
||||||
### Dialogue Manifest
|
### Dialogue Manifest
|
||||||
|
|
||||||
Use the `Dialogues` panel to edit `public/sounds/dialogue/dialogues.json` without opening the JSON file manually.
|
Use the `Dialogues` panel to edit `public/sounds/dialogue/dialogues.json` without opening the JSON file manually.
|
||||||
|
|
||||||
|
Each dialogue entry contains:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| ------------------ | ----------------------------------------------------------------- |
|
||||||
|
| `id` | Unique dialogue ID used by cinematics and runtime triggers |
|
||||||
|
| `voice` | Voice file group: `narrateur`, `fermier`, or `electricienne` |
|
||||||
|
| `audio` | Runtime audio path, usually under `/sounds/dialogue/` |
|
||||||
|
| `subtitleCueIndex` | Cue number inside the selected voice/language SRT file |
|
||||||
|
| `timecode` | Optional global runtime trigger time, in seconds from scene start |
|
||||||
|
|
||||||
Available actions:
|
Available actions:
|
||||||
|
|
||||||
- `Reload` reloads the manifest from disk.
|
- `Reload` reloads the manifest from disk.
|
||||||
@@ -95,6 +145,19 @@ After using `Add`, save the manifest to keep the new dialogue entry. The generat
|
|||||||
|
|
||||||
New dialogue audio paths start as placeholders such as `/sounds/dialogue/new_dialogue_24.mp3`. Replace them with real MP3 paths before validating the final asset set.
|
New dialogue audio paths start as placeholders such as `/sounds/dialogue/new_dialogue_24.mp3`. Replace them with real MP3 paths before validating the final asset set.
|
||||||
|
|
||||||
|
Recommended workflow for a new dialogue:
|
||||||
|
|
||||||
|
1. Open `Dialogues`.
|
||||||
|
2. Click `Add`.
|
||||||
|
3. Choose the correct `voice`.
|
||||||
|
4. Replace the generated `id` with a readable stable ID.
|
||||||
|
5. Replace the placeholder `audio` path with the real MP3 path.
|
||||||
|
6. Check the generated `subtitleCueIndex`.
|
||||||
|
7. Click `Create FR SRT cue` if the cue does not exist yet.
|
||||||
|
8. Click `Save`.
|
||||||
|
9. Open `SRT`, edit the cue text and timings, then save the SRT file.
|
||||||
|
10. Run `Validate` from the SRT panel.
|
||||||
|
|
||||||
### SRT Editor
|
### SRT Editor
|
||||||
|
|
||||||
Use the `SRT` panel to edit one subtitle file at a time.
|
Use the `SRT` panel to edit one subtitle file at a time.
|
||||||
@@ -108,6 +171,8 @@ Use the `SRT` panel to edit one subtitle file at a time.
|
|||||||
|
|
||||||
Each SRT file belongs to one voice, not one dialogue. Cue indexes must match the `subtitleCueIndex` values referenced by the dialogue manifest.
|
Each SRT file belongs to one voice, not one dialogue. Cue indexes must match the `subtitleCueIndex` values referenced by the dialogue manifest.
|
||||||
|
|
||||||
|
SRT timings are relative to the dialogue audio file, not to the global game timeline and not to the cinematic timeline. For example, `00:00:01,000` means one second after that dialogue audio starts.
|
||||||
|
|
||||||
## Validating Dialogue Assets
|
## Validating Dialogue Assets
|
||||||
|
|
||||||
Use `Validate` in the SRT panel to check the dialogue manifest and linked assets.
|
Use `Validate` in the SRT panel to check the dialogue manifest and linked assets.
|
||||||
@@ -143,6 +208,17 @@ Dialogue cues define:
|
|||||||
- `time`: seconds relative to the cinematic start
|
- `time`: seconds relative to the cinematic start
|
||||||
- `dialogueId`: an entry from `public/sounds/dialogue/dialogues.json`
|
- `dialogueId`: an entry from `public/sounds/dialogue/dialogues.json`
|
||||||
|
|
||||||
|
Recommended workflow for a cinematic:
|
||||||
|
|
||||||
|
1. Open `Cinematics`.
|
||||||
|
2. Select an existing cinematic or click `Add`.
|
||||||
|
3. Set a stable `id`.
|
||||||
|
4. Add or adjust camera keyframes.
|
||||||
|
5. Keep keyframe `time` values increasing from start to end.
|
||||||
|
6. Add dialogue cues when a dialogue must start during the camera sequence.
|
||||||
|
7. Click `Preview cinematic` to test the camera path in the editor canvas.
|
||||||
|
8. Click `Save` when the manifest is correct.
|
||||||
|
|
||||||
Available actions:
|
Available actions:
|
||||||
|
|
||||||
- `Reload` reloads the cinematic manifest from disk.
|
- `Reload` reloads the cinematic manifest from disk.
|
||||||
@@ -155,6 +231,8 @@ Available actions:
|
|||||||
|
|
||||||
Cinematic dialogue cues are the preferred way to synchronize a dialogue with a cinematic. Avoid also giving the same dialogue a global `timecode`, or it can be triggered twice.
|
Cinematic dialogue cues are the preferred way to synchronize a dialogue with a cinematic. Avoid also giving the same dialogue a global `timecode`, or it can be triggered twice.
|
||||||
|
|
||||||
|
Use `dialogueCues` when the dialogue belongs to a cinematic. Use a dialogue `timecode` only for simple global scene timing outside a cinematic.
|
||||||
|
|
||||||
## Current Limitations
|
## Current Limitations
|
||||||
|
|
||||||
- The editor only modifies existing nodes.
|
- The editor only modifies existing nodes.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Braces,
|
Braces,
|
||||||
|
ChevronDown,
|
||||||
Download,
|
Download,
|
||||||
Expand,
|
Expand,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
@@ -11,6 +12,8 @@ import {
|
|||||||
RotateCw,
|
RotateCw,
|
||||||
Save,
|
Save,
|
||||||
Undo2,
|
Undo2,
|
||||||
|
Unlock,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { EditorCinematicManifestPanel } from "@/components/editor/EditorCinematicManifestPanel";
|
import { EditorCinematicManifestPanel } from "@/components/editor/EditorCinematicManifestPanel";
|
||||||
import { EditorDialogueManifestPanel } from "@/components/editor/EditorDialogueManifestPanel";
|
import { EditorDialogueManifestPanel } from "@/components/editor/EditorDialogueManifestPanel";
|
||||||
@@ -25,6 +28,9 @@ interface EditorControlsProps {
|
|||||||
mapNodes: MapNode[];
|
mapNodes: MapNode[];
|
||||||
nodesCount: number;
|
nodesCount: number;
|
||||||
selectedNodeName: string | null;
|
selectedNodeName: string | null;
|
||||||
|
isSelectionLocked: boolean;
|
||||||
|
onSelectionLockToggle: () => void;
|
||||||
|
onClearSelection: () => void;
|
||||||
undoCount: number;
|
undoCount: number;
|
||||||
redoCount: number;
|
redoCount: number;
|
||||||
onUndo: () => void;
|
onUndo: () => void;
|
||||||
@@ -50,6 +56,33 @@ const EDITOR_SHORTCUTS = [
|
|||||||
["WASD", "Move when locked"],
|
["WASD", "Move when locked"],
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
interface EditorPanelGroupProps {
|
||||||
|
title: string;
|
||||||
|
summary?: string;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditorPanelGroup({
|
||||||
|
title,
|
||||||
|
summary,
|
||||||
|
defaultOpen = false,
|
||||||
|
children,
|
||||||
|
}: EditorPanelGroupProps): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<details className="editor-panel-group" open={defaultOpen}>
|
||||||
|
<summary className="editor-panel-group-summary">
|
||||||
|
<span>{title}</span>
|
||||||
|
<span className="editor-panel-group-meta">
|
||||||
|
{summary ? <span>{summary}</span> : null}
|
||||||
|
<ChevronDown size={15} aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div className="editor-panel-group-content">{children}</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function EditorControls({
|
export function EditorControls({
|
||||||
transformMode,
|
transformMode,
|
||||||
onTransformModeChange,
|
onTransformModeChange,
|
||||||
@@ -57,6 +90,9 @@ export function EditorControls({
|
|||||||
mapNodes,
|
mapNodes,
|
||||||
nodesCount,
|
nodesCount,
|
||||||
selectedNodeName,
|
selectedNodeName,
|
||||||
|
isSelectionLocked,
|
||||||
|
onSelectionLockToggle,
|
||||||
|
onClearSelection,
|
||||||
undoCount,
|
undoCount,
|
||||||
redoCount,
|
redoCount,
|
||||||
onUndo,
|
onUndo,
|
||||||
@@ -79,6 +115,28 @@ export function EditorControls({
|
|||||||
<p>Select an object, choose a transform mode, then drag the gizmo.</p>
|
<p>Select an object, choose a transform mode, then drag the gizmo.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<EditorPanelGroup title="Editor" summary="Map tools" defaultOpen>
|
||||||
|
<EditorPanelGroup title="Shortcuts" summary="Keys">
|
||||||
|
<section
|
||||||
|
className="editor-control-section"
|
||||||
|
aria-labelledby="shortcuts-heading"
|
||||||
|
>
|
||||||
|
<div className="editor-section-heading">
|
||||||
|
<h3 id="shortcuts-heading">Shortcuts</h3>
|
||||||
|
<Keyboard size={15} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl className="editor-shortcuts-list">
|
||||||
|
{EDITOR_SHORTCUTS.map(([keys, description]) => (
|
||||||
|
<div key={keys}>
|
||||||
|
<dt>{keys}</dt>
|
||||||
|
<dd>{description}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</EditorPanelGroup>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
className="editor-control-section"
|
className="editor-control-section"
|
||||||
aria-labelledby="transform-heading"
|
aria-labelledby="transform-heading"
|
||||||
@@ -127,25 +185,57 @@ export function EditorControls({
|
|||||||
|
|
||||||
<section
|
<section
|
||||||
className="editor-control-section"
|
className="editor-control-section"
|
||||||
aria-labelledby="file-heading"
|
aria-labelledby="selection-heading"
|
||||||
>
|
>
|
||||||
<div className="editor-section-heading">
|
<div className="editor-section-heading">
|
||||||
<h3 id="file-heading">File</h3>
|
<h3 id="selection-heading">Selection</h3>
|
||||||
|
<span>{nodesCount} nodes</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{selectedNodeIndex !== null ? (
|
||||||
|
<div className="editor-selected-info">
|
||||||
|
<Box size={17} aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<strong>
|
||||||
|
{selectedNodeName || `Node ${selectedNodeIndex + 1}`}
|
||||||
|
</strong>
|
||||||
|
<span>
|
||||||
|
Index {selectedNodeIndex + 1} of {nodesCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="editor-selected-actions">
|
||||||
<button
|
<button
|
||||||
className="editor-action-button editor-action-button-primary"
|
type="button"
|
||||||
onClick={onExportJson}
|
onClick={onSelectionLockToggle}
|
||||||
|
aria-pressed={isSelectionLocked}
|
||||||
|
aria-label={
|
||||||
|
isSelectionLocked ? "Unlock selection" : "Lock selection"
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
isSelectionLocked ? "Unlock selection" : "Lock selection"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Download size={16} aria-hidden="true" />
|
{isSelectionLocked ? (
|
||||||
Export JSON
|
<Lock size={14} aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<Unlock size={14} aria-hidden="true" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
{onSaveToServer && (
|
type="button"
|
||||||
<button className="editor-action-button" onClick={onSaveToServer}>
|
onClick={onClearSelection}
|
||||||
<Save size={16} aria-hidden="true" />
|
aria-label="Clear selection"
|
||||||
Save to server
|
title="Clear selection"
|
||||||
|
>
|
||||||
|
<X size={14} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="editor-no-selection">
|
||||||
|
<MousePointer2 size={17} aria-hidden="true" />
|
||||||
|
No object selected
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -170,54 +260,9 @@ export function EditorControls({
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
className="editor-control-section"
|
className="editor-json-section"
|
||||||
aria-labelledby="selection-heading"
|
aria-labelledby="json-heading"
|
||||||
>
|
>
|
||||||
<div className="editor-section-heading">
|
|
||||||
<h3 id="selection-heading">Selection</h3>
|
|
||||||
<span>{nodesCount} nodes</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedNodeIndex !== null ? (
|
|
||||||
<div className="editor-selected-info">
|
|
||||||
<Box size={17} aria-hidden="true" />
|
|
||||||
<div>
|
|
||||||
<strong>
|
|
||||||
{selectedNodeName || `Node ${selectedNodeIndex + 1}`}
|
|
||||||
</strong>
|
|
||||||
<span>
|
|
||||||
Index {selectedNodeIndex + 1} of {nodesCount}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="editor-no-selection">
|
|
||||||
<MousePointer2 size={17} aria-hidden="true" />
|
|
||||||
No object selected
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
|
||||||
className="editor-control-section"
|
|
||||||
aria-labelledby="shortcuts-heading"
|
|
||||||
>
|
|
||||||
<div className="editor-section-heading">
|
|
||||||
<h3 id="shortcuts-heading">Shortcuts</h3>
|
|
||||||
<Keyboard size={15} aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dl className="editor-shortcuts-list">
|
|
||||||
{EDITOR_SHORTCUTS.map(([keys, description]) => (
|
|
||||||
<div key={keys}>
|
|
||||||
<dt>{keys}</dt>
|
|
||||||
<dd>{description}</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="editor-json-section" aria-labelledby="json-heading">
|
|
||||||
<div className="editor-section-heading">
|
<div className="editor-section-heading">
|
||||||
<h3 id="json-heading">JSON</h3>
|
<h3 id="json-heading">JSON</h3>
|
||||||
<span>{jsonPreview.label}</span>
|
<span>{jsonPreview.label}</span>
|
||||||
@@ -243,9 +288,42 @@ export function EditorControls({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<EditorCinematicManifestPanel onPreviewCinematic={onPreviewCinematic} />
|
<section
|
||||||
|
className="editor-control-section"
|
||||||
|
aria-labelledby="file-heading"
|
||||||
|
>
|
||||||
|
<div className="editor-section-heading">
|
||||||
|
<h3 id="file-heading">File</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="editor-action-button editor-action-button-primary"
|
||||||
|
onClick={onExportJson}
|
||||||
|
>
|
||||||
|
<Download size={16} aria-hidden="true" />
|
||||||
|
Export JSON
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{onSaveToServer && (
|
||||||
|
<button className="editor-action-button" onClick={onSaveToServer}>
|
||||||
|
<Save size={16} aria-hidden="true" />
|
||||||
|
Save to server
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</EditorPanelGroup>
|
||||||
|
|
||||||
|
<EditorPanelGroup title="Cinematics" summary="Timeline">
|
||||||
|
<EditorCinematicManifestPanel
|
||||||
|
onPreviewCinematic={onPreviewCinematic}
|
||||||
|
/>
|
||||||
|
</EditorPanelGroup>
|
||||||
|
<EditorPanelGroup title="Dialogues" summary="Manifest">
|
||||||
<EditorDialogueManifestPanel />
|
<EditorDialogueManifestPanel />
|
||||||
|
</EditorPanelGroup>
|
||||||
|
<EditorPanelGroup title="SRT" summary="Subtitles">
|
||||||
<EditorSrtPanel />
|
<EditorSrtPanel />
|
||||||
|
</EditorPanelGroup>
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface EditorMapProps {
|
|||||||
sceneData: SceneData;
|
sceneData: SceneData;
|
||||||
selectedNodeIndex: number | null;
|
selectedNodeIndex: number | null;
|
||||||
onSelectNode: (index: number | null) => void;
|
onSelectNode: (index: number | null) => void;
|
||||||
|
isSelectionLocked: boolean;
|
||||||
hoveredNodeIndex: number | null;
|
hoveredNodeIndex: number | null;
|
||||||
onHoverNode: (index: number | null) => void;
|
onHoverNode: (index: number | null) => void;
|
||||||
transformMode: TransformMode;
|
transformMode: TransformMode;
|
||||||
@@ -28,6 +29,7 @@ interface EditorNodeCommonProps {
|
|||||||
isHovered: boolean;
|
isHovered: boolean;
|
||||||
objectsMapRef: EditorNodeObjectRef;
|
objectsMapRef: EditorNodeObjectRef;
|
||||||
onSelectNode: (index: number | null) => void;
|
onSelectNode: (index: number | null) => void;
|
||||||
|
isSelectionLocked: boolean;
|
||||||
onHoverNode: (index: number | null) => void;
|
onHoverNode: (index: number | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,11 +110,13 @@ function getNodeHighlightColor(
|
|||||||
function createEditorNodePointerHandlers(
|
function createEditorNodePointerHandlers(
|
||||||
index: number,
|
index: number,
|
||||||
onSelectNode: (index: number | null) => void,
|
onSelectNode: (index: number | null) => void,
|
||||||
|
isSelectionLocked: boolean,
|
||||||
onHoverNode: (index: number | null) => void,
|
onHoverNode: (index: number | null) => void,
|
||||||
): EditorNodePointerHandlers {
|
): EditorNodePointerHandlers {
|
||||||
return {
|
return {
|
||||||
onClick: (event) => {
|
onClick: (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
if (isSelectionLocked) return;
|
||||||
onSelectNode(index);
|
onSelectNode(index);
|
||||||
},
|
},
|
||||||
onPointerEnter: (event) => {
|
onPointerEnter: (event) => {
|
||||||
@@ -130,6 +134,7 @@ export function EditorMap({
|
|||||||
sceneData,
|
sceneData,
|
||||||
selectedNodeIndex,
|
selectedNodeIndex,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
|
isSelectionLocked,
|
||||||
hoveredNodeIndex,
|
hoveredNodeIndex,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
transformMode,
|
transformMode,
|
||||||
@@ -192,8 +197,9 @@ export function EditorMap({
|
|||||||
<axesHelper args={[10]} />
|
<axesHelper args={[10]} />
|
||||||
|
|
||||||
<group
|
<group
|
||||||
onClick={(e: ThreeEvent<MouseEvent>) => {
|
onClick={(event: ThreeEvent<MouseEvent>) => {
|
||||||
e.stopPropagation();
|
event.stopPropagation();
|
||||||
|
if (isSelectionLocked) return;
|
||||||
onSelectNode(null);
|
onSelectNode(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -211,6 +217,7 @@ export function EditorMap({
|
|||||||
isHovered={hoveredNodeIndex === index}
|
isHovered={hoveredNodeIndex === index}
|
||||||
objectsMapRef={objectsMapRef}
|
objectsMapRef={objectsMapRef}
|
||||||
onSelectNode={onSelectNode}
|
onSelectNode={onSelectNode}
|
||||||
|
isSelectionLocked={isSelectionLocked}
|
||||||
onHoverNode={onHoverNode}
|
onHoverNode={onHoverNode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -224,6 +231,7 @@ export function EditorMap({
|
|||||||
isHovered={hoveredNodeIndex === index}
|
isHovered={hoveredNodeIndex === index}
|
||||||
objectsMapRef={objectsMapRef}
|
objectsMapRef={objectsMapRef}
|
||||||
onSelectNode={onSelectNode}
|
onSelectNode={onSelectNode}
|
||||||
|
isSelectionLocked={isSelectionLocked}
|
||||||
onHoverNode={onHoverNode}
|
onHoverNode={onHoverNode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -251,6 +259,7 @@ function EditorModelNode({
|
|||||||
isHovered,
|
isHovered,
|
||||||
objectsMapRef,
|
objectsMapRef,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
|
isSelectionLocked,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
}: EditorNodeCommonProps & {
|
}: EditorNodeCommonProps & {
|
||||||
modelUrl: string;
|
modelUrl: string;
|
||||||
@@ -269,6 +278,7 @@ function EditorModelNode({
|
|||||||
const pointerHandlers = createEditorNodePointerHandlers(
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
index,
|
index,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
|
isSelectionLocked,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
);
|
);
|
||||||
useRegisteredEditorNode(groupRef, index, node, objectsMapRef);
|
useRegisteredEditorNode(groupRef, index, node, objectsMapRef);
|
||||||
@@ -343,12 +353,14 @@ function EditorFallbackNode({
|
|||||||
isHovered,
|
isHovered,
|
||||||
objectsMapRef,
|
objectsMapRef,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
|
isSelectionLocked,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
}: EditorNodeCommonProps) {
|
}: EditorNodeCommonProps) {
|
||||||
const meshRef = useRef<THREE.Mesh>(null);
|
const meshRef = useRef<THREE.Mesh>(null);
|
||||||
const pointerHandlers = createEditorNodePointerHandlers(
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
index,
|
index,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
|
isSelectionLocked,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
);
|
);
|
||||||
useRegisteredEditorNode(meshRef, index, node, objectsMapRef);
|
useRegisteredEditorNode(meshRef, index, node, objectsMapRef);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface EditorSceneProps {
|
|||||||
sceneData: SceneData;
|
sceneData: SceneData;
|
||||||
selectedNodeIndex: number | null;
|
selectedNodeIndex: number | null;
|
||||||
onSelectNode: (index: number | null) => void;
|
onSelectNode: (index: number | null) => void;
|
||||||
|
isSelectionLocked: boolean;
|
||||||
hoveredNodeIndex: number | null;
|
hoveredNodeIndex: number | null;
|
||||||
onHoverNode: (index: number | null) => void;
|
onHoverNode: (index: number | null) => void;
|
||||||
transformMode: TransformMode;
|
transformMode: TransformMode;
|
||||||
@@ -35,6 +36,7 @@ export function EditorScene({
|
|||||||
sceneData,
|
sceneData,
|
||||||
selectedNodeIndex,
|
selectedNodeIndex,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
|
isSelectionLocked,
|
||||||
hoveredNodeIndex,
|
hoveredNodeIndex,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
transformMode,
|
transformMode,
|
||||||
@@ -68,7 +70,7 @@ export function EditorScene({
|
|||||||
if (selectedNodeIndex !== null) {
|
if (selectedNodeIndex !== null) {
|
||||||
switch (e.key.toLowerCase()) {
|
switch (e.key.toLowerCase()) {
|
||||||
case "escape":
|
case "escape":
|
||||||
onSelectNode(null);
|
if (!isSelectionLocked) onSelectNode(null);
|
||||||
break;
|
break;
|
||||||
case "t":
|
case "t":
|
||||||
onTransformModeChange("translate");
|
onTransformModeChange("translate");
|
||||||
@@ -85,7 +87,14 @@ export function EditorScene({
|
|||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [selectedNodeIndex, onSelectNode, onTransformModeChange, onUndo, onRedo]);
|
}, [
|
||||||
|
isSelectionLocked,
|
||||||
|
selectedNodeIndex,
|
||||||
|
onSelectNode,
|
||||||
|
onTransformModeChange,
|
||||||
|
onUndo,
|
||||||
|
onRedo,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -113,6 +122,7 @@ export function EditorScene({
|
|||||||
sceneData={sceneData}
|
sceneData={sceneData}
|
||||||
selectedNodeIndex={selectedNodeIndex}
|
selectedNodeIndex={selectedNodeIndex}
|
||||||
onSelectNode={onSelectNode}
|
onSelectNode={onSelectNode}
|
||||||
|
isSelectionLocked={isSelectionLocked}
|
||||||
hoveredNodeIndex={hoveredNodeIndex}
|
hoveredNodeIndex={hoveredNodeIndex}
|
||||||
onHoverNode={onHoverNode}
|
onHoverNode={onHoverNode}
|
||||||
transformMode={transformMode}
|
transformMode={transformMode}
|
||||||
|
|||||||
@@ -38,17 +38,23 @@ export const docGroups: DocGroup[] = [
|
|||||||
subtitle: "Implementation details",
|
subtitle: "Implementation details",
|
||||||
meta: "04",
|
meta: "04",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/docs/audio",
|
||||||
|
title: "Audio Technical Notes",
|
||||||
|
subtitle: "Music, dialogue, SRT, and SFX",
|
||||||
|
meta: "05",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/hand-tracking",
|
path: "/docs/hand-tracking",
|
||||||
title: "Hand Tracking Technical Notes",
|
title: "Hand Tracking Technical Notes",
|
||||||
subtitle: "Webcam interaction pipeline",
|
subtitle: "Webcam interaction pipeline",
|
||||||
meta: "05",
|
meta: "06",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/zustand",
|
path: "/docs/zustand",
|
||||||
title: "Zustand Game State",
|
title: "Zustand Game State",
|
||||||
subtitle: "Progression store",
|
subtitle: "Progression store",
|
||||||
meta: "06",
|
meta: "07",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -59,25 +65,25 @@ export const docGroups: DocGroup[] = [
|
|||||||
path: "/docs/features",
|
path: "/docs/features",
|
||||||
title: "Features",
|
title: "Features",
|
||||||
subtitle: "Implemented scope",
|
subtitle: "Implemented scope",
|
||||||
meta: "07",
|
meta: "08",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/main-feature",
|
path: "/docs/main-feature",
|
||||||
title: "Main Feature",
|
title: "Main Feature",
|
||||||
subtitle: "Repair-game prototype",
|
subtitle: "Repair-game prototype",
|
||||||
meta: "08",
|
meta: "09",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/editor",
|
path: "/docs/editor",
|
||||||
title: "Editor User Guide",
|
title: "Editor User Guide",
|
||||||
subtitle: "Editing workflow",
|
subtitle: "Editing workflow",
|
||||||
meta: "09",
|
meta: "10",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/animation",
|
path: "/docs/animation",
|
||||||
title: "Animation & 3D Model System",
|
title: "Animation & 3D Model System",
|
||||||
subtitle: "Components and usage",
|
subtitle: "Components and usage",
|
||||||
meta: "010",
|
meta: "11",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
+103
-100
@@ -556,142 +556,145 @@ Ce document liste les fonctionnalités présentes dans le code actuel.
|
|||||||
- séparation complète production / debug pour les scènes gameplay
|
- séparation complète production / debug pour les scènes gameplay
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const editorFr = `# Éditeur de carte
|
export const editorFr = `# Guide utilisateur de l'éditeur
|
||||||
|
|
||||||
L'éditeur de carte est disponible sur "/editor". Il permet d'inspecter et d'ajuster les objets déclarés dans "/public/map.json" directement depuis le navigateur.
|
L'éditeur est disponible sur \`/editor\`. Il sert à modifier la carte runtime, les cinématiques, le manifeste de dialogues et les sous-titres SRT sans éditer tous les fichiers à la main.
|
||||||
|
|
||||||
## Ce qui est édité
|
## À quoi il sert
|
||||||
|
|
||||||
L'éditeur travaille sur la liste de nodes stockée dans "/public/map.json".
|
Utilise l'éditeur pour :
|
||||||
|
|
||||||
Chaque node décrit un objet de la scène :
|
- déplacer, tourner ou scaler les objets de \`public/map.json\`
|
||||||
|
- inspecter le JSON généré avant export ou sauvegarde
|
||||||
|
- prévisualiser et modifier \`public/cinematics.json\`
|
||||||
|
- créer, prévisualiser et valider les dialogues de \`public/sounds/dialogue/dialogues.json\`
|
||||||
|
- modifier les fichiers SRT FR/EN par voix
|
||||||
|
|
||||||
- "name" : nom du dossier modèle dans "/public/models/{name}/model.glb", avec fallback vers "model.gltf"
|
## Organisation du panneau
|
||||||
- "type" : catégorie de l'objet
|
|
||||||
- "position" : "[x, y, z]"
|
|
||||||
- "rotation" : "[x, y, z]"
|
|
||||||
- "scale" : "[x, y, z]"
|
|
||||||
|
|
||||||
Les modèles sont chargés depuis "/public/models". Si un modèle manque, l'éditeur affiche un cube gris de remplacement pour que le node reste sélectionnable et déplaçable.
|
Le panneau latéral est divisé en groupes repliables :
|
||||||
|
|
||||||
## Workflow de base
|
- \`Editor\` : transforms, raccourcis, sélection, vue, JSON et actions fichier.
|
||||||
|
- \`Cinematics\` : cinématiques et keyframes caméra.
|
||||||
|
- \`Dialogues\` : manifeste des dialogues.
|
||||||
|
- \`SRT\` : fichiers de sous-titres par voix et langue.
|
||||||
|
|
||||||
1. Ouvrir "/editor".
|
## Carte et transforms
|
||||||
2. Sélectionner un objet dans la vue 3D.
|
|
||||||
3. Choisir un mode de transformation : translation, rotation ou scale.
|
|
||||||
4. Déplacer la gizmo de transformation.
|
|
||||||
5. Utiliser undo ou redo si nécessaire.
|
|
||||||
6. Exporter le JSON mis à jour ou le sauvegarder sur le serveur de dev.
|
|
||||||
|
|
||||||
## Contrôles
|
1. Ouvre \`/editor\`.
|
||||||
|
2. Clique un objet pour le sélectionner.
|
||||||
|
3. Choisis \`Translate\`, \`Rotate\` ou \`Scale\`.
|
||||||
|
4. Déplace la gizmo dans la vue 3D.
|
||||||
|
5. Vérifie le bloc \`JSON\` si tu veux contrôler les valeurs exactes.
|
||||||
|
6. Utilise \`Undo\` ou \`Redo\` si besoin.
|
||||||
|
7. Utilise \`Export JSON\` ou \`Save to server\`.
|
||||||
|
|
||||||
|
Contrôles utiles :
|
||||||
|
|
||||||
| Action | Input |
|
| Action | Input |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Sélectionner un objet | Clic sur l'objet |
|
| Sélectionner | Clic objet |
|
||||||
| Désélectionner | "Esc" ou clic dans le vide |
|
| Désélectionner | \`Esc\` ou clic vide |
|
||||||
| Mode translation | "T" |
|
| Verrouiller la sélection | bouton lock |
|
||||||
| Mode rotation | "R" |
|
| Vider la sélection | bouton \`X\` |
|
||||||
| Mode scale | "S" |
|
| Translate | \`T\` |
|
||||||
| Undo | "Ctrl+Z" |
|
| Rotate | \`R\` |
|
||||||
| Redo | "Ctrl+Y" |
|
| Scale | \`S\` |
|
||||||
| Déplacement en vue verrouillée | "WASD", "ZQSD", flèches |
|
| Undo / redo | \`Ctrl+Z\` / \`Ctrl+Y\` |
|
||||||
| Monter / descendre | "Space", "Shift" |
|
| Déplacement vue verrouillée | \`WASD\`, \`ZQSD\`, flèches |
|
||||||
|
|
||||||
## Actions fichier
|
Quand la sélection est verrouillée, cliquer un autre objet, cliquer dans le vide ou appuyer sur \`Esc\` ne change pas la sélection. Le bouton \`X\` reste le moyen volontaire de la vider.
|
||||||
|
|
||||||
### Export JSON
|
## Inspecteur JSON
|
||||||
|
|
||||||
"Export JSON" télécharge la liste actuelle des nodes sous le nom "map.json". À utiliser pour remplacer manuellement "/public/map.json".
|
Le bloc \`JSON\` affiche le JSON qui sera exporté ou sauvegardé :
|
||||||
|
|
||||||
### Save to server
|
- sans sélection, il affiche toute la liste de nodes
|
||||||
|
- avec une sélection, il affiche les lignes du node sélectionné
|
||||||
|
|
||||||
"Save to server" est disponible uniquement en développement local. L'action écrit la carte modifiée dans "/public/map.json" via l'endpoint du serveur de dev Vite.
|
Cet inspecteur est en lecture seule. Les valeurs changent via la gizmo de transformation.
|
||||||
|
|
||||||
Cette action est masquée dans les builds de production car il n'existe pas encore d'API de persistance production.
|
## Sauvegarde
|
||||||
|
|
||||||
## Éditer les dialogues et sous-titres
|
- \`Export JSON\` télécharge un \`map.json\` local.
|
||||||
|
- \`Save to server\` écrit directement \`public/map.json\` via le serveur Vite local.
|
||||||
|
|
||||||
Le panneau latéral contient aussi des outils pour les dialogues et les sous-titres.
|
Les sauvegardes serveur sont des helpers de développement, pas des APIs de production.
|
||||||
|
|
||||||
### Manifeste dialogues
|
## Cinématiques
|
||||||
|
|
||||||
Le panneau \`Dialogues\` permet d'éditer \`public/sounds/dialogue/dialogues.json\` sans ouvrir le JSON à la main.
|
Le groupe \`Cinematics\` édite \`public/cinematics.json\`.
|
||||||
|
|
||||||
- \`Reload\` recharge le manifeste depuis le disque.
|
Une cinématique contient :
|
||||||
- \`Add\` crée un dialogue local pour la voix courante et assigne le prochain index SRT disponible.
|
|
||||||
- \`Save\` écrit le manifeste via le serveur Vite local.
|
|
||||||
- \`Preview dialogue\` joue le dialogue sélectionné avec les sous-titres dans l'éditeur.
|
|
||||||
- \`Create FR SRT cue\` crée la cue française si elle manque.
|
|
||||||
- \`Delete dialogue\` supprime localement l'entrée sélectionnée.
|
|
||||||
|
|
||||||
Après \`Add\`, il faut cliquer \`Save\` pour conserver le dialogue dans le manifeste. La cue SRT FR est écrite directement, mais le manifeste reste local tant qu'il n'est pas sauvegardé.
|
|
||||||
|
|
||||||
Les nouveaux dialogues utilisent un chemin audio placeholder comme \`/sounds/dialogue/new_dialogue_24.mp3\`. Remplace-le par un vrai MP3 avant validation finale.
|
|
||||||
|
|
||||||
### Éditeur SRT
|
|
||||||
|
|
||||||
1. Choisir une voix : \`narrateur\`, \`fermier\` ou \`electricienne\`.
|
|
||||||
2. Choisir une langue : \`FR\` ou \`EN\`.
|
|
||||||
3. Modifier le texte SRT directement dans la textarea.
|
|
||||||
4. Utiliser la preview audio pour vérifier le dialogue sélectionné.
|
|
||||||
5. Utiliser \`Set start\`, \`Set end\`, \`-100ms\` et \`+100ms\` pour ajuster le timing de la cue sélectionnée avec l'audio.
|
|
||||||
6. Utiliser \`Save SRT\` en développement local, ou \`Export SRT\` pour télécharger le fichier manuellement.
|
|
||||||
|
|
||||||
Chaque fichier SRT appartient à une voix, pas à un dialogue. Les indexes de cue doivent correspondre aux valeurs \`subtitleCueIndex\` référencées par le manifeste de dialogues.
|
|
||||||
|
|
||||||
## Valider les assets de dialogue
|
|
||||||
|
|
||||||
Utilise \`Validate\` dans le panneau SRT pour vérifier le manifeste et les assets liés.
|
|
||||||
|
|
||||||
La validation vérifie :
|
|
||||||
|
|
||||||
- \`public/sounds/dialogue/dialogues.json\`
|
|
||||||
- les fichiers audio de dialogue référencés
|
|
||||||
- les fichiers SRT français
|
|
||||||
- les indexes de cue référencés par le manifeste
|
|
||||||
|
|
||||||
Les fichiers SRT anglais manquants sont des warnings parce que le runtime retombe sur les sous-titres français.
|
|
||||||
|
|
||||||
## Éditer les cinématiques
|
|
||||||
|
|
||||||
Le panneau \`Cinematics\` permet d'éditer \`public/cinematics.json\`.
|
|
||||||
|
|
||||||
Chaque cinématique contient :
|
|
||||||
|
|
||||||
- un \`id\`
|
- un \`id\`
|
||||||
- un \`timecode\` global optionnel
|
- un \`timecode\` global optionnel
|
||||||
- au moins deux keyframes caméra
|
- au moins deux keyframes caméra
|
||||||
- des dialogue cues optionnelles synchronisées avec la timeline
|
- des \`dialogueCues\` optionnelles
|
||||||
|
|
||||||
Les keyframes caméra définissent un temps relatif, une position caméra et une cible de regard. Les dialogue cues définissent un temps relatif et un \`dialogueId\` issu de \`dialogues.json\`.
|
Workflow conseillé :
|
||||||
|
|
||||||
Actions disponibles :
|
1. Sélectionne une cinématique ou clique \`Add\`.
|
||||||
|
2. Donne un \`id\` stable.
|
||||||
|
3. Ajoute ou ajuste les keyframes caméra.
|
||||||
|
4. Garde les temps de keyframes dans l'ordre croissant.
|
||||||
|
5. Ajoute des \`dialogueCues\` si un dialogue doit démarrer pendant la cinématique.
|
||||||
|
6. Clique \`Preview cinematic\` pour tester la caméra.
|
||||||
|
7. Clique \`Save\`.
|
||||||
|
|
||||||
- \`Reload\` recharge le manifeste.
|
Les temps de keyframes et de dialogue cues sont relatifs au début de la cinématique.
|
||||||
- \`Add\` crée une cinématique locale avec deux keyframes.
|
|
||||||
- \`Save\` écrit \`public/cinematics.json\` via le serveur Vite local.
|
|
||||||
- \`Preview cinematic\` joue l'animation caméra dans le canvas éditeur.
|
|
||||||
- \`Add keyframe\` et \`Remove\` modifient le chemin caméra.
|
|
||||||
- \`Add dialogue\` et \`Remove\` modifient les dialogues synchronisés.
|
|
||||||
- \`Delete cinematic\` supprime localement la cinématique sélectionnée.
|
|
||||||
|
|
||||||
Les dialogue cues sont la manière recommandée de synchroniser un dialogue avec une cinématique. Évite de donner aussi un \`timecode\` global au même dialogue dans \`dialogues.json\`, sinon il peut être lancé deux fois.
|
## Dialogues
|
||||||
|
|
||||||
## Inspecteur JSON
|
Le groupe \`Dialogues\` édite \`public/sounds/dialogue/dialogues.json\`.
|
||||||
|
|
||||||
Le panneau latéral affiche le JSON brut de la carte :
|
Chaque dialogue contient :
|
||||||
|
|
||||||
- sans sélection, il affiche toute la liste des nodes
|
- \`id\` : identifiant stable utilisé par les cinématiques et le runtime
|
||||||
- avec un objet sélectionné, il met en évidence les lignes du node sélectionné
|
- \`voice\` : \`narrateur\`, \`fermier\` ou \`electricienne\`
|
||||||
|
- \`audio\` : chemin MP3 runtime
|
||||||
|
- \`subtitleCueIndex\` : numéro de cue dans le SRT de la voix
|
||||||
|
- \`timecode\` : déclenchement global optionnel
|
||||||
|
|
||||||
Utilise-le pour vérifier les valeurs numériques exactes avant export ou sauvegarde.
|
Workflow conseillé pour créer un dialogue :
|
||||||
|
|
||||||
|
1. Clique \`Add\`.
|
||||||
|
2. Choisis la bonne voix.
|
||||||
|
3. Remplace l'\`id\` généré par un ID lisible.
|
||||||
|
4. Remplace le chemin audio placeholder par le vrai MP3.
|
||||||
|
5. Vérifie le \`subtitleCueIndex\`.
|
||||||
|
6. Clique \`Create FR SRT cue\` si la cue manque.
|
||||||
|
7. Clique \`Save\`.
|
||||||
|
8. Passe dans \`SRT\` pour écrire le texte et régler les timings.
|
||||||
|
9. Lance \`Validate\`.
|
||||||
|
|
||||||
|
## SRT
|
||||||
|
|
||||||
|
Le groupe \`SRT\` édite un fichier de sous-titres à la fois.
|
||||||
|
|
||||||
|
1. Choisis une voix.
|
||||||
|
2. Choisis \`FR\` ou \`EN\`.
|
||||||
|
3. Écris le texte SRT.
|
||||||
|
4. Prévisualise l'audio.
|
||||||
|
5. Utilise \`Set start\`, \`Set end\`, \`-100ms\` et \`+100ms\` pour ajuster les timings.
|
||||||
|
6. Clique \`Save SRT\` ou \`Export SRT\`.
|
||||||
|
|
||||||
|
Il y a un fichier SRT par voix et par langue, pas un fichier par dialogue. Les timings SRT sont relatifs au fichier audio du dialogue.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
\`Validate\` vérifie :
|
||||||
|
|
||||||
|
- le manifeste \`dialogues.json\`
|
||||||
|
- les fichiers audio référencés
|
||||||
|
- les fichiers SRT français
|
||||||
|
- les indexes de cues référencés
|
||||||
|
- les SRT anglais en warning si manquants
|
||||||
|
|
||||||
## Limites actuelles
|
## Limites actuelles
|
||||||
|
|
||||||
- L'éditeur modifie uniquement les nodes existants.
|
- L'éditeur modifie les nodes existants mais ne crée pas encore d'objet de carte.
|
||||||
- Il n'y a pas encore d'interface pour créer ou supprimer des objets.
|
- Les sauvegardes serveur sont limitées au développement local.
|
||||||
- La sauvegarde production n'est pas implémentée.
|
- L'éditeur SRT reste textuel, sans waveform.
|
||||||
- Les modèles manquants s'affichent comme cubes de fallback au lieu de bloquer tout l'éditeur.
|
- Les modèles manquants sont représentés par des cubes de fallback.
|
||||||
- La sauvegarde SRT est un helper local du serveur Vite, pas une API backend de production.
|
|
||||||
- Les sauvegardes dialogues et cinématiques sont aussi des helpers locaux du serveur Vite.
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
+89
-1
@@ -1142,6 +1142,61 @@ canvas {
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-panel-group {
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-panel-group-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 13px 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
list-style: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-panel-group-summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-panel-group-summary:hover {
|
||||||
|
color: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-panel-group-meta {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #777777;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-panel-group-meta svg {
|
||||||
|
transition: transform 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-panel-group[open] .editor-panel-group-meta svg {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-panel-group-content > .editor-control-section:first-child,
|
||||||
|
.editor-panel-group-content > .editor-json-section:first-child,
|
||||||
|
.editor-panel-group-content > .editor-cinematic-manifest-section:first-child,
|
||||||
|
.editor-panel-group-content > .editor-dialogue-manifest-section:first-child,
|
||||||
|
.editor-panel-group-content > .editor-srt-section:first-child {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-control-section {
|
.editor-control-section {
|
||||||
padding: 14px 12px;
|
padding: 14px 12px;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.09);
|
border-top: 1px solid rgba(255, 255, 255, 0.09);
|
||||||
@@ -1313,7 +1368,8 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-selected-info {
|
.editor-selected-info {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 17px 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 11px;
|
gap: 11px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
@@ -1323,6 +1379,38 @@ canvas {
|
|||||||
color: #050505;
|
color: #050505;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-selected-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-selected-actions button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 27px;
|
||||||
|
height: 27px;
|
||||||
|
padding: 0;
|
||||||
|
color: #050505;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 9px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background 160ms ease,
|
||||||
|
transform 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-selected-actions button:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.12);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-selected-actions button[aria-pressed="true"] {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #050505;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-selected-info strong,
|
.editor-selected-info strong,
|
||||||
.editor-selected-info span {
|
.editor-selected-info span {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function DocsAnimationPage(): React.JSX.Element {
|
|||||||
<DocsDocument
|
<DocsDocument
|
||||||
content={animation}
|
content={animation}
|
||||||
frContent={animation}
|
frContent={animation}
|
||||||
meta="08"
|
meta="11"
|
||||||
title="Animation & 3D Model System"
|
title="Animation & 3D Model System"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import audio from "../../../../docs/technical/audio.md?raw";
|
||||||
|
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||||
|
|
||||||
|
export function DocsAudioPage(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<DocsDocument
|
||||||
|
content={audio}
|
||||||
|
frContent={audio}
|
||||||
|
meta="05"
|
||||||
|
title="Audio Technical Notes"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ export function DocsEditorPage(): React.JSX.Element {
|
|||||||
<DocsDocument
|
<DocsDocument
|
||||||
content={editor}
|
content={editor}
|
||||||
frContent={editorFr}
|
frContent={editorFr}
|
||||||
meta="09"
|
meta="10"
|
||||||
title="Editor User Guide"
|
title="Editor User Guide"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function DocsFeaturesPage(): React.JSX.Element {
|
|||||||
<DocsDocument
|
<DocsDocument
|
||||||
content={features}
|
content={features}
|
||||||
frContent={featuresFr}
|
frContent={featuresFr}
|
||||||
meta="06"
|
meta="08"
|
||||||
title="Features"
|
title="Features"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function DocsHandTrackingPage(): React.JSX.Element {
|
|||||||
<DocsDocument
|
<DocsDocument
|
||||||
content={handTracking}
|
content={handTracking}
|
||||||
frContent={handTracking}
|
frContent={handTracking}
|
||||||
meta="05"
|
meta="06"
|
||||||
title="Hand Tracking Technical Notes"
|
title="Hand Tracking Technical Notes"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function DocsMainFeaturePage(): React.JSX.Element {
|
|||||||
<DocsDocument
|
<DocsDocument
|
||||||
content={mainFeature}
|
content={mainFeature}
|
||||||
frContent={mainFeature}
|
frContent={mainFeature}
|
||||||
meta="07"
|
meta="09"
|
||||||
title="Main Feature"
|
title="Main Feature"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function DocsZustandPage(): React.JSX.Element {
|
|||||||
<DocsDocument
|
<DocsDocument
|
||||||
content={zustand}
|
content={zustand}
|
||||||
frContent={zustandFr}
|
frContent={zustandFr}
|
||||||
meta="05"
|
meta="07"
|
||||||
title="Zustand Game State"
|
title="Zustand Game State"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
const [transformMode, setTransformMode] =
|
const [transformMode, setTransformMode] =
|
||||||
useState<TransformMode>("translate");
|
useState<TransformMode>("translate");
|
||||||
const [isPlayerMode, setIsPlayerMode] = useState(false);
|
const [isPlayerMode, setIsPlayerMode] = useState(false);
|
||||||
|
const [isSelectionLocked, setIsSelectionLocked] = useState(false);
|
||||||
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
|
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
|
||||||
{
|
{
|
||||||
...INITIAL_SCENE_LOADING_STATE,
|
...INITIAL_SCENE_LOADING_STATE,
|
||||||
@@ -112,6 +113,14 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
setSelectedNodeIndex(index);
|
setSelectedNodeIndex(index);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleClearSelection = useCallback(() => {
|
||||||
|
setSelectedNodeIndex(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSelectionLockToggle = useCallback(() => {
|
||||||
|
setIsSelectionLocked((locked) => !locked);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleHoverNode = useCallback((index: number | null) => {
|
const handleHoverNode = useCallback((index: number | null) => {
|
||||||
setHoveredNodeIndex(index);
|
setHoveredNodeIndex(index);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -246,6 +255,7 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
sceneData={sceneData!}
|
sceneData={sceneData!}
|
||||||
selectedNodeIndex={selectedNodeIndex}
|
selectedNodeIndex={selectedNodeIndex}
|
||||||
onSelectNode={handleSelectNode}
|
onSelectNode={handleSelectNode}
|
||||||
|
isSelectionLocked={isSelectionLocked}
|
||||||
hoveredNodeIndex={hoveredNodeIndex}
|
hoveredNodeIndex={hoveredNodeIndex}
|
||||||
onHoverNode={handleHoverNode}
|
onHoverNode={handleHoverNode}
|
||||||
transformMode={transformMode}
|
transformMode={transformMode}
|
||||||
@@ -276,6 +286,9 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
? sceneData.mapNodes[selectedNodeIndex].name || null
|
? sceneData.mapNodes[selectedNodeIndex].name || null
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
isSelectionLocked={isSelectionLocked}
|
||||||
|
onSelectionLockToggle={handleSelectionLockToggle}
|
||||||
|
onClearSelection={handleClearSelection}
|
||||||
undoCount={undoCount}
|
undoCount={undoCount}
|
||||||
redoCount={redoCount}
|
redoCount={redoCount}
|
||||||
onUndo={handleUndo}
|
onUndo={handleUndo}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { HomePage } from "@/pages/page";
|
|||||||
import { EditorPage } from "@/pages/editor/page";
|
import { EditorPage } from "@/pages/editor/page";
|
||||||
import {
|
import {
|
||||||
DocsAnimationRoute,
|
DocsAnimationRoute,
|
||||||
|
DocsAudioRoute,
|
||||||
DocsArchitectureRoute,
|
DocsArchitectureRoute,
|
||||||
DocsEditorRoute,
|
DocsEditorRoute,
|
||||||
DocsFeaturesRoute,
|
DocsFeaturesRoute,
|
||||||
@@ -47,6 +48,7 @@ const docsChildRoutes = [
|
|||||||
{ path: "architecture", component: DocsArchitectureRoute },
|
{ path: "architecture", component: DocsArchitectureRoute },
|
||||||
{ path: "target-architecture", component: DocsTargetArchitectureRoute },
|
{ path: "target-architecture", component: DocsTargetArchitectureRoute },
|
||||||
{ path: "technical-editor", component: DocsTechnicalEditorRoute },
|
{ path: "technical-editor", component: DocsTechnicalEditorRoute },
|
||||||
|
{ path: "audio", component: DocsAudioRoute },
|
||||||
{ path: "hand-tracking", component: DocsHandTrackingRoute },
|
{ path: "hand-tracking", component: DocsHandTrackingRoute },
|
||||||
{ path: "zustand", component: DocsZustandRoute },
|
{ path: "zustand", component: DocsZustandRoute },
|
||||||
{ path: "features", component: DocsFeaturesRoute },
|
{ path: "features", component: DocsFeaturesRoute },
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ const LazyDocsTechnicalEditorPage = lazyNamed(
|
|||||||
() => import("@/pages/docs/technical-editor/page"),
|
() => import("@/pages/docs/technical-editor/page"),
|
||||||
"DocsTechnicalEditorPage",
|
"DocsTechnicalEditorPage",
|
||||||
);
|
);
|
||||||
|
const LazyDocsAudioPage = lazyNamed(
|
||||||
|
() => import("@/pages/docs/audio/page"),
|
||||||
|
"DocsAudioPage",
|
||||||
|
);
|
||||||
const LazyDocsHandTrackingPage = lazyNamed(
|
const LazyDocsHandTrackingPage = lazyNamed(
|
||||||
() => import("@/pages/docs/hand-tracking/page"),
|
() => import("@/pages/docs/hand-tracking/page"),
|
||||||
"DocsHandTrackingPage",
|
"DocsHandTrackingPage",
|
||||||
@@ -81,6 +85,7 @@ export const DocsTargetArchitectureRoute = createDocsRoute(
|
|||||||
export const DocsTechnicalEditorRoute = createDocsRoute(
|
export const DocsTechnicalEditorRoute = createDocsRoute(
|
||||||
LazyDocsTechnicalEditorPage,
|
LazyDocsTechnicalEditorPage,
|
||||||
);
|
);
|
||||||
|
export const DocsAudioRoute = createDocsRoute(LazyDocsAudioPage);
|
||||||
export const DocsHandTrackingRoute = createDocsRoute(LazyDocsHandTrackingPage);
|
export const DocsHandTrackingRoute = createDocsRoute(LazyDocsHandTrackingPage);
|
||||||
export const DocsZustandRoute = createDocsRoute(LazyDocsZustandPage);
|
export const DocsZustandRoute = createDocsRoute(LazyDocsZustandPage);
|
||||||
export const DocsFeaturesRoute = createDocsRoute(LazyDocsFeaturesPage);
|
export const DocsFeaturesRoute = createDocsRoute(LazyDocsFeaturesPage);
|
||||||
|
|||||||
Reference in New Issue
Block a user