update: edit cinematic dialogue

This commit is contained in:
Tom Boullay
2026-05-11 13:01:56 +02:00
parent 6a4d0f7eb1
commit e5aeab6534
2 changed files with 180 additions and 15 deletions
@@ -3,10 +3,16 @@ import { Play, Plus, RefreshCw, Save, Trash2 } from "lucide-react";
import type { import type {
CinematicCameraKeyframe, CinematicCameraKeyframe,
CinematicDefinition, CinematicDefinition,
CinematicDialogueCue,
CinematicManifest, CinematicManifest,
} from "@/types/cinematics/cinematics"; } from "@/types/cinematics/cinematics";
import type {
DialogueDefinition,
DialogueManifest,
} from "@/types/dialogues/dialogues";
import type { Vector3Tuple } from "@/types/three/three"; import type { Vector3Tuple } from "@/types/three/three";
import { loadCinematicManifest } from "@/utils/cinematics/loadCinematicManifest"; import { loadCinematicManifest } from "@/utils/cinematics/loadCinematicManifest";
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
type CinematicPatch = Partial<Omit<CinematicDefinition, "timecode">> & { type CinematicPatch = Partial<Omit<CinematicDefinition, "timecode">> & {
timecode?: number | undefined; 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."]; if (!manifest) return ["Manifeste absent."];
const errors: string[] = []; const errors: string[] = [];
@@ -82,6 +101,8 @@ function getManifestErrors(manifest: CinematicManifest | null): string[] {
if (!cue.dialogueId.trim()) { if (!cue.dialogueId.trim()) {
errors.push(`${label}: dialogue cue ${cueIndex + 1} id obligatoire.`); 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, onPreviewCinematic,
}: EditorCinematicManifestPanelProps): React.JSX.Element { }: EditorCinematicManifestPanelProps): React.JSX.Element {
const [manifest, setManifest] = useState<CinematicManifest | null>(null); const [manifest, setManifest] = useState<CinematicManifest | null>(null);
const [dialogueManifest, setDialogueManifest] =
useState<DialogueManifest | null>(null);
const [selectedCinematicId, setSelectedCinematicId] = useState(""); const [selectedCinematicId, setSelectedCinematicId] = useState("");
const [status, setStatus] = useState("Chargement des cinematics..."); const [status, setStatus] = useState("Chargement des cinematics...");
const [isSaving, setIsSaving] = useState(false); 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 = const selectedCinematic =
manifest?.cinematics.find( manifest?.cinematics.find(
(cinematic) => cinematic.id === selectedCinematicId, (cinematic) => cinematic.id === selectedCinematicId,
@@ -162,8 +188,12 @@ export function EditorCinematicManifestPanel({
setStatus("Chargement des cinematics..."); setStatus("Chargement des cinematics...");
try { try {
const loadedManifest = await loadCinematicManifest(); const [loadedManifest, loadedDialogueManifest] = await Promise.all([
loadCinematicManifest(),
loadDialogueManifest(),
]);
setManifest(loadedManifest); setManifest(loadedManifest);
setDialogueManifest(loadedDialogueManifest);
setSelectedCinematicId(loadedManifest?.cinematics[0]?.id ?? ""); setSelectedCinematicId(loadedManifest?.cinematics[0]?.id ?? "");
setStatus( setStatus(
loadedManifest loadedManifest
@@ -281,14 +311,54 @@ export function EditorCinematicManifestPanel({
setStatus("Keyframe supprimee localement."); 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(() => { useEffect(() => {
let mounted = true; let mounted = true;
void loadCinematicManifest() void Promise.all([loadCinematicManifest(), loadDialogueManifest()])
.then((loadedManifest) => { .then(([loadedManifest, loadedDialogueManifest]) => {
if (!mounted) return; if (!mounted) return;
setManifest(loadedManifest); setManifest(loadedManifest);
setDialogueManifest(loadedDialogueManifest);
setSelectedCinematicId(loadedManifest?.cinematics[0]?.id ?? ""); setSelectedCinematicId(loadedManifest?.cinematics[0]?.id ?? "");
setStatus( setStatus(
loadedManifest loadedManifest
@@ -452,6 +522,77 @@ export function EditorCinematicManifestPanel({
)} )}
</div> </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 <button
className="editor-cinematic-manifest-preview" className="editor-cinematic-manifest-preview"
type="button" type="button"
+34 -10
View File
@@ -1866,7 +1866,9 @@ canvas {
.editor-cinematic-manifest-preview, .editor-cinematic-manifest-preview,
.editor-cinematic-manifest-delete, .editor-cinematic-manifest-delete,
.editor-cinematic-keyframes-heading button, .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; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -1885,21 +1887,25 @@ canvas {
.editor-cinematic-manifest-preview:hover, .editor-cinematic-manifest-preview:hover,
.editor-cinematic-manifest-delete:hover, .editor-cinematic-manifest-delete:hover,
.editor-cinematic-keyframes-heading button: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; border-color: #ffffff;
background: #202020; background: #202020;
} }
.editor-cinematic-manifest-actions button:disabled, .editor-cinematic-manifest-actions button:disabled,
.editor-cinematic-manifest-preview: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; cursor: not-allowed;
opacity: 0.45; opacity: 0.45;
} }
.editor-cinematic-manifest-select, .editor-cinematic-manifest-select,
.editor-cinematic-manifest-form label, .editor-cinematic-manifest-form label,
.editor-cinematic-vector-inputs label { .editor-cinematic-vector-inputs label,
.editor-cinematic-dialogue-cue label {
display: grid; display: grid;
gap: 5px; gap: 5px;
color: #8d8d8d; color: #8d8d8d;
@@ -1911,6 +1917,7 @@ canvas {
.editor-cinematic-manifest-select select, .editor-cinematic-manifest-select select,
.editor-cinematic-manifest-form input, .editor-cinematic-manifest-form input,
.editor-cinematic-manifest-form select,
.editor-cinematic-vector-inputs input { .editor-cinematic-vector-inputs input {
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
@@ -1923,6 +1930,7 @@ canvas {
.editor-cinematic-manifest-select select:focus, .editor-cinematic-manifest-select select:focus,
.editor-cinematic-manifest-form input:focus, .editor-cinematic-manifest-form input:focus,
.editor-cinematic-manifest-form select:focus,
.editor-cinematic-vector-inputs input:focus { .editor-cinematic-vector-inputs input:focus {
border-color: #ffffff; border-color: #ffffff;
outline: none; outline: none;
@@ -1930,7 +1938,9 @@ canvas {
.editor-cinematic-manifest-form, .editor-cinematic-manifest-form,
.editor-cinematic-keyframes, .editor-cinematic-keyframes,
.editor-cinematic-keyframe { .editor-cinematic-keyframe,
.editor-cinematic-dialogue-cues,
.editor-cinematic-dialogue-cue {
display: grid; display: grid;
gap: 8px; gap: 8px;
} }
@@ -1942,7 +1952,8 @@ canvas {
background: #070707; background: #070707;
} }
.editor-cinematic-keyframes { .editor-cinematic-keyframes,
.editor-cinematic-dialogue-cues {
padding: 10px; padding: 10px;
border: 1px solid #242424; border: 1px solid #242424;
border-radius: 14px; border-radius: 14px;
@@ -1950,7 +1961,9 @@ canvas {
} }
.editor-cinematic-keyframes-heading, .editor-cinematic-keyframes-heading,
.editor-cinematic-keyframe-heading { .editor-cinematic-keyframe-heading,
.editor-cinematic-dialogue-cues-heading,
.editor-cinematic-dialogue-cue-heading {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -1958,13 +1971,16 @@ canvas {
} }
.editor-cinematic-keyframes-heading strong, .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; color: #f2f2f2;
font-size: 0.76rem; font-size: 0.76rem;
font-weight: 800; font-weight: 800;
} }
.editor-cinematic-keyframe { .editor-cinematic-keyframe,
.editor-cinematic-dialogue-cue {
padding: 9px; padding: 9px;
border: 1px solid #1f1f1f; border: 1px solid #1f1f1f;
border-radius: 12px; border-radius: 12px;
@@ -1996,11 +2012,19 @@ canvas {
color: #bae6fd; color: #bae6fd;
} }
.editor-cinematic-keyframe-heading button { .editor-cinematic-keyframe-heading button,
.editor-cinematic-dialogue-cue-heading button {
padding: 6px 8px; padding: 6px 8px;
color: #fca5a5; color: #fca5a5;
} }
.editor-cinematic-dialogue-cues p {
margin: 0;
color: #8d8d8d;
font-size: 0.72rem;
line-height: 1.4;
}
.editor-cinematic-manifest-status { .editor-cinematic-manifest-status {
margin: 0; margin: 0;
color: #8d8d8d; color: #8d8d8d;