Feat/env-manager #1
@@ -3,10 +3,16 @@ import { Play, Plus, RefreshCw, Save, Trash2 } from "lucide-react";
|
||||
import type {
|
||||
CinematicCameraKeyframe,
|
||||
CinematicDefinition,
|
||||
CinematicDialogueCue,
|
||||
CinematicManifest,
|
||||
} from "@/types/cinematics/cinematics";
|
||||
import type {
|
||||
DialogueDefinition,
|
||||
DialogueManifest,
|
||||
} from "@/types/dialogues/dialogues";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import { loadCinematicManifest } from "@/utils/cinematics/loadCinematicManifest";
|
||||
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
|
||||
|
||||
type CinematicPatch = Partial<Omit<CinematicDefinition, "timecode">> & {
|
||||
timecode?: number | undefined;
|
||||
@@ -39,7 +45,20 @@ function createKeyframe(
|
||||
};
|
||||
}
|
||||
|
||||
function getManifestErrors(manifest: CinematicManifest | null): string[] {
|
||||
function createDialogueCue(
|
||||
dialogues: DialogueDefinition[],
|
||||
previousCue: CinematicDialogueCue | null,
|
||||
): CinematicDialogueCue {
|
||||
return {
|
||||
time: previousCue ? previousCue.time + 1 : 0,
|
||||
dialogueId: dialogues[0]?.id ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function getManifestErrors(
|
||||
manifest: CinematicManifest | null,
|
||||
dialogueIds: Set<string>,
|
||||
): string[] {
|
||||
if (!manifest) return ["Manifeste absent."];
|
||||
|
||||
const errors: string[] = [];
|
||||
@@ -82,6 +101,8 @@ function getManifestErrors(manifest: CinematicManifest | null): string[] {
|
||||
|
||||
if (!cue.dialogueId.trim()) {
|
||||
errors.push(`${label}: dialogue cue ${cueIndex + 1} id obligatoire.`);
|
||||
} else if (dialogueIds.size > 0 && !dialogueIds.has(cue.dialogueId)) {
|
||||
errors.push(`${label}: dialogue cue ${cueIndex + 1} dialogue inconnu.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -147,10 +168,15 @@ export function EditorCinematicManifestPanel({
|
||||
onPreviewCinematic,
|
||||
}: EditorCinematicManifestPanelProps): React.JSX.Element {
|
||||
const [manifest, setManifest] = useState<CinematicManifest | null>(null);
|
||||
const [dialogueManifest, setDialogueManifest] =
|
||||
useState<DialogueManifest | null>(null);
|
||||
const [selectedCinematicId, setSelectedCinematicId] = useState("");
|
||||
const [status, setStatus] = useState("Chargement des cinematics...");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const errors = getManifestErrors(manifest);
|
||||
const dialogueIds = new Set(
|
||||
dialogueManifest?.dialogues.map((dialogue) => dialogue.id) ?? [],
|
||||
);
|
||||
const errors = getManifestErrors(manifest, dialogueIds);
|
||||
const selectedCinematic =
|
||||
manifest?.cinematics.find(
|
||||
(cinematic) => cinematic.id === selectedCinematicId,
|
||||
@@ -162,8 +188,12 @@ export function EditorCinematicManifestPanel({
|
||||
setStatus("Chargement des cinematics...");
|
||||
|
||||
try {
|
||||
const loadedManifest = await loadCinematicManifest();
|
||||
const [loadedManifest, loadedDialogueManifest] = await Promise.all([
|
||||
loadCinematicManifest(),
|
||||
loadDialogueManifest(),
|
||||
]);
|
||||
setManifest(loadedManifest);
|
||||
setDialogueManifest(loadedDialogueManifest);
|
||||
setSelectedCinematicId(loadedManifest?.cinematics[0]?.id ?? "");
|
||||
setStatus(
|
||||
loadedManifest
|
||||
@@ -281,14 +311,54 @@ export function EditorCinematicManifestPanel({
|
||||
setStatus("Keyframe supprimee localement.");
|
||||
}
|
||||
|
||||
function updateDialogueCue(
|
||||
cueIndex: number,
|
||||
patch: Partial<CinematicDialogueCue>,
|
||||
): void {
|
||||
if (!selectedCinematic) return;
|
||||
|
||||
const dialogueCues = selectedCinematic.dialogueCues ?? [];
|
||||
updateSelectedCinematic({
|
||||
dialogueCues: dialogueCues.map((cue, index) =>
|
||||
index === cueIndex ? { ...cue, ...patch } : cue,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function handleAddDialogueCue(): void {
|
||||
if (!selectedCinematic) return;
|
||||
|
||||
const dialogueCues = selectedCinematic.dialogueCues ?? [];
|
||||
const previousCue = dialogueCues[dialogueCues.length - 1] ?? null;
|
||||
updateSelectedCinematic({
|
||||
dialogueCues: [
|
||||
...dialogueCues,
|
||||
createDialogueCue(dialogueManifest?.dialogues ?? [], previousCue),
|
||||
],
|
||||
});
|
||||
setStatus("Dialogue cue ajoutee localement.");
|
||||
}
|
||||
|
||||
function handleRemoveDialogueCue(cueIndex: number): void {
|
||||
if (!selectedCinematic) return;
|
||||
|
||||
updateSelectedCinematic({
|
||||
dialogueCues: (selectedCinematic.dialogueCues ?? []).filter(
|
||||
(_cue, index) => index !== cueIndex,
|
||||
),
|
||||
});
|
||||
setStatus("Dialogue cue supprimee localement.");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
void loadCinematicManifest()
|
||||
.then((loadedManifest) => {
|
||||
void Promise.all([loadCinematicManifest(), loadDialogueManifest()])
|
||||
.then(([loadedManifest, loadedDialogueManifest]) => {
|
||||
if (!mounted) return;
|
||||
|
||||
setManifest(loadedManifest);
|
||||
setDialogueManifest(loadedDialogueManifest);
|
||||
setSelectedCinematicId(loadedManifest?.cinematics[0]?.id ?? "");
|
||||
setStatus(
|
||||
loadedManifest
|
||||
@@ -452,6 +522,77 @@ export function EditorCinematicManifestPanel({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="editor-cinematic-dialogue-cues">
|
||||
<div className="editor-cinematic-dialogue-cues-heading">
|
||||
<strong>Dialogue cues</strong>
|
||||
<button type="button" onClick={handleAddDialogueCue}>
|
||||
<Plus size={13} aria-hidden="true" />
|
||||
Add dialogue
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(selectedCinematic.dialogueCues ?? []).length === 0 ? (
|
||||
<p>Aucun dialogue synchronise avec cette cinematic.</p>
|
||||
) : (
|
||||
(selectedCinematic.dialogueCues ?? []).map((cue, cueIndex) => (
|
||||
<div
|
||||
className="editor-cinematic-dialogue-cue"
|
||||
key={`${selectedCinematic.id}-dialogue-${cueIndex}`}
|
||||
>
|
||||
<div className="editor-cinematic-dialogue-cue-heading">
|
||||
<strong>Dialogue {cueIndex + 1}</strong>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveDialogueCue(cueIndex)}
|
||||
>
|
||||
<Trash2 size={13} aria-hidden="true" />
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
Time
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
value={cue.time}
|
||||
onChange={(event) =>
|
||||
updateDialogueCue(cueIndex, {
|
||||
time: Number(event.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Dialogue
|
||||
<select
|
||||
value={cue.dialogueId}
|
||||
onChange={(event) =>
|
||||
updateDialogueCue(cueIndex, {
|
||||
dialogueId: event.target.value,
|
||||
})
|
||||
}
|
||||
>
|
||||
{dialogueManifest?.dialogues.length ? (
|
||||
dialogueManifest.dialogues.map((dialogue) => (
|
||||
<option key={dialogue.id} value={dialogue.id}>
|
||||
{dialogue.id}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value={cue.dialogueId}>
|
||||
{cue.dialogueId || "Aucun dialogue disponible"}
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="editor-cinematic-manifest-preview"
|
||||
type="button"
|
||||
|
||||
+34
-10
@@ -1866,7 +1866,9 @@ canvas {
|
||||
.editor-cinematic-manifest-preview,
|
||||
.editor-cinematic-manifest-delete,
|
||||
.editor-cinematic-keyframes-heading button,
|
||||
.editor-cinematic-keyframe-heading button {
|
||||
.editor-cinematic-keyframe-heading button,
|
||||
.editor-cinematic-dialogue-cues-heading button,
|
||||
.editor-cinematic-dialogue-cue-heading button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1885,21 +1887,25 @@ canvas {
|
||||
.editor-cinematic-manifest-preview:hover,
|
||||
.editor-cinematic-manifest-delete:hover,
|
||||
.editor-cinematic-keyframes-heading button:hover,
|
||||
.editor-cinematic-keyframe-heading button:hover {
|
||||
.editor-cinematic-keyframe-heading button:hover,
|
||||
.editor-cinematic-dialogue-cues-heading button:hover,
|
||||
.editor-cinematic-dialogue-cue-heading button:hover {
|
||||
border-color: #ffffff;
|
||||
background: #202020;
|
||||
}
|
||||
|
||||
.editor-cinematic-manifest-actions button:disabled,
|
||||
.editor-cinematic-manifest-preview:disabled,
|
||||
.editor-cinematic-keyframe-heading button:disabled {
|
||||
.editor-cinematic-keyframe-heading button:disabled,
|
||||
.editor-cinematic-dialogue-cue-heading button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.editor-cinematic-manifest-select,
|
||||
.editor-cinematic-manifest-form label,
|
||||
.editor-cinematic-vector-inputs label {
|
||||
.editor-cinematic-vector-inputs label,
|
||||
.editor-cinematic-dialogue-cue label {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
color: #8d8d8d;
|
||||
@@ -1911,6 +1917,7 @@ canvas {
|
||||
|
||||
.editor-cinematic-manifest-select select,
|
||||
.editor-cinematic-manifest-form input,
|
||||
.editor-cinematic-manifest-form select,
|
||||
.editor-cinematic-vector-inputs input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
@@ -1923,6 +1930,7 @@ canvas {
|
||||
|
||||
.editor-cinematic-manifest-select select:focus,
|
||||
.editor-cinematic-manifest-form input:focus,
|
||||
.editor-cinematic-manifest-form select:focus,
|
||||
.editor-cinematic-vector-inputs input:focus {
|
||||
border-color: #ffffff;
|
||||
outline: none;
|
||||
@@ -1930,7 +1938,9 @@ canvas {
|
||||
|
||||
.editor-cinematic-manifest-form,
|
||||
.editor-cinematic-keyframes,
|
||||
.editor-cinematic-keyframe {
|
||||
.editor-cinematic-keyframe,
|
||||
.editor-cinematic-dialogue-cues,
|
||||
.editor-cinematic-dialogue-cue {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -1942,7 +1952,8 @@ canvas {
|
||||
background: #070707;
|
||||
}
|
||||
|
||||
.editor-cinematic-keyframes {
|
||||
.editor-cinematic-keyframes,
|
||||
.editor-cinematic-dialogue-cues {
|
||||
padding: 10px;
|
||||
border: 1px solid #242424;
|
||||
border-radius: 14px;
|
||||
@@ -1950,7 +1961,9 @@ canvas {
|
||||
}
|
||||
|
||||
.editor-cinematic-keyframes-heading,
|
||||
.editor-cinematic-keyframe-heading {
|
||||
.editor-cinematic-keyframe-heading,
|
||||
.editor-cinematic-dialogue-cues-heading,
|
||||
.editor-cinematic-dialogue-cue-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -1958,13 +1971,16 @@ canvas {
|
||||
}
|
||||
|
||||
.editor-cinematic-keyframes-heading strong,
|
||||
.editor-cinematic-keyframe-heading strong {
|
||||
.editor-cinematic-keyframe-heading strong,
|
||||
.editor-cinematic-dialogue-cues-heading strong,
|
||||
.editor-cinematic-dialogue-cue-heading strong {
|
||||
color: #f2f2f2;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.editor-cinematic-keyframe {
|
||||
.editor-cinematic-keyframe,
|
||||
.editor-cinematic-dialogue-cue {
|
||||
padding: 9px;
|
||||
border: 1px solid #1f1f1f;
|
||||
border-radius: 12px;
|
||||
@@ -1996,11 +2012,19 @@ canvas {
|
||||
color: #bae6fd;
|
||||
}
|
||||
|
||||
.editor-cinematic-keyframe-heading button {
|
||||
.editor-cinematic-keyframe-heading button,
|
||||
.editor-cinematic-dialogue-cue-heading button {
|
||||
padding: 6px 8px;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.editor-cinematic-dialogue-cues p {
|
||||
margin: 0;
|
||||
color: #8d8d8d;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.editor-cinematic-manifest-status {
|
||||
margin: 0;
|
||||
color: #8d8d8d;
|
||||
|
||||
Reference in New Issue
Block a user