update: edit cinematic dialogue
This commit is contained in:
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user