import { ArrowLeft, ChevronDown, ChevronUp, Copy, Download, FileCode2, Pencil, Plus, RotateCcw, Trash2, Upload, } from 'lucide-react' import { motion } from 'motion/react' import { type CSSProperties } from 'react' import { Accordion } from '@/components/ui/accordion' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, } from '@/components/ui/context-menu' import { Field, FieldTitle } from '@/components/ui/field' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Separator } from '@/components/ui/separator' import { Switch } from '@/components/ui/switch' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { ControlSection, ExportSection, InspectorCard, PanelTitle, SnapshotPreview, StatePlayer, } from '@/app/components/common' import { ColorField, LinkButton, NumericField } from '@/app/components/controls' import { formatSeconds, scaleSurface, type Mode, type Side, type SnapshotFormat, } from '@/app/studio-utils' import { SequenceWorkspace } from '@/features/animation/components/SequenceWorkspace' import { findExpressionIndex, groupSequences } from '@/features/animation/sequences' import { defaultAvatarEyes } from '@/features/avatar/avatars' import { bodyPrimitiveTypes, MAX_BODY_NODES } from '@/features/avatar/body' import { ExpressionCard, ExpressionPreview, ExpressionWorkspace, SurfaceThumbnail, } from '@/features/avatar/components/ExpressionWorkspace' import { defaultExpression } from '@/features/avatar/presets' import { surfaceLabels, surfacePresets } from '@/features/avatar/surfaces' import { type SnapshotBackground } from '@/features/export/snapshotExporter' import type { StudioController } from '@/features/studio/useStudioController' export function StudioInspector({ controller }: { controller: StudioController }) { const { activateAvatar, activeAvatar, activeAvatarEyes, activeAvatarId, activeExpression, activeSequence, activeSequenceLabel, activeState, addBodyNode, avatarDragOrigin, avatarDragPreview, avatars, avatarsRef, blink, bodyEditing, bodyNodes, cancelAvatarEditing, cancelAvatarMove, cancelExpressionEditing, cancelExpressionMove, cancelSequenceEditing, cancelStateMove, commitAvatarMove, commitExpressionMove, commitStateMove, createNewAvatar, deleteSelectedBodyNode, downloadAvatarExport, downloadStudioProject, draggedAvatarId, draggedExpressionId, draggedStateId, draggingAvatarId, draggingExpressionId, draggingStateId, duplicateAvatar, duplicateExpression, duplicateSelectedBodyNode, duplicateSequenceEditing, duplicateState, editing, editorPageOpen, exportAnimationIdSet, exportFormat, expression, expressionById, expressionDragOrigin, expressionDragPreview, expressions, focusAvatarName, language, launchSequence, linked, mode, openExpressionEditor, openSequenceEditor, pauseState, playbackStatus, playbackVisual, prepareStudioProjectImport, previewAvatarMove, previewExpressionDraft, previewExpressionMove, previewStateMove, projectImportError, projectImportRef, reduceMotion, renameActiveAvatar, renderedColors, renderedScene, saveAvatarEditing, saveEditing, saveSequenceEditing, selectBodyNode, selectedBodyNode, selectedBodyNodeId, selectedExportAnimations, selectedSequenceStepId, selectedState, sequenceEditing, sequences, setDeleteAvatarOpen, setDeleteExpressionOpen, setDeleteSequenceOpen, setDraggingAvatarId, setDraggingExpressionId, setDraggingStateId, setExportAnimationIds, setExportFormat, setFocusAvatarName, setLinked, setMode, setSelectedSequenceStepId, setSequenceEditing, setSnapshotBackground, setSnapshotColorFrom, setSnapshotColorTo, setSnapshotFormat, setSnapshotSize, setSpringSpeed, setStatePlayerExpanded, showWire, snapshotBackground, snapshotColorFrom, snapshotColorTo, snapshotFormat, snapshotSize, springSpeed, springSpeedRef, stateDragOrigin, stateDragPreview, statePlayerExpanded, statePlaying, stopState, surface, t, toggleExportAnimation, toggleStatePlayback, transitionToExpression, updateAvatarColors, updateAvatarEyeDimension, updateAvatarEyePosition, updateAvatarEyeSize, updateAvatarEyes, updateDimension, updateHighlight, updateImmediate, updateNodeVector, updateSelectedBodyNode, updateSize, updateSpacing, updateSurface, updateWireVisibility, workspaceBackButtonRef, } = controller return (
{sequenceEditing && !editing && ( setSequenceEditing(current => (current ? { ...current, draft } : current)) } onPreviewStep={step => { const expressionIndex = findExpressionIndex(expressions, step.expressionId) const preset = expressions[expressionIndex] if (preset) transitionToExpression(preset, expressionIndex, step) }} onEditExpression={openExpressionEditor} onPlay={() => launchSequence(sequenceEditing.draft, false, false)} onPause={() => pauseState(false)} onStop={() => stopState(false)} playing={statePlaying} active={activeState === sequenceEditing.draft.id} onCancel={cancelSequenceEditing} onSave={saveSequenceEditing} onDuplicate={duplicateSequenceEditing} onDelete={() => setDeleteSequenceOpen(true)} /> )} {editing && ( duplicateExpression(editing.index, editing.draft, true)} onDelete={() => setDeleteExpressionOpen(true)} /> )} {!sequenceEditing && !editing && (bodyEditing ? (

{t('Construction du corps')}

renameActiveAvatar(event.currentTarget.value)} onFocus={event => { if (focusAvatarName) event.currentTarget.select() }} onBlur={event => { if (!event.currentTarget.value.trim()) renameActiveAvatar('Unknown') setFocusAvatarName(false) }} />

{t('Choisis la forme principale puis assemble les primitives autour d’elle.')}

) : ( <>
Avatars {t('Double-clic pour modifier')}
{avatars.map(avatar => ( { avatarDragOrigin.current = avatarsRef.current avatarDragPreview.current = avatarsRef.current draggedAvatarId.current = avatar.id setDraggingAvatarId(avatar.id) event.dataTransfer.effectAllowed = 'move' }} onDragEnter={() => previewAvatarMove(avatar.id)} onDragOver={event => { event.preventDefault() event.dataTransfer.dropEffect = 'move' }} onDrop={event => { event.preventDefault() commitAvatarMove(avatar.id) }} onDragEnd={cancelAvatarMove} onClick={() => activateAvatar(avatar.id, false, true)} onDoubleClick={() => { setFocusAvatarName(false) activateAvatar(avatar.id, true) }} > {avatar.name} } /> { setFocusAvatarName(false) activateAvatar(avatar.id, true) }} > {t('Modifier')} duplicateAvatar(avatar)}> {t('Dupliquer')} { activateAvatar(avatar.id, false, true) setDeleteAvatarOpen(true) }} > {t('Supprimer')} ))}
setMode(value as Mode)}> {t('Pose')} {t('Expressions')} {t('Animations')} {t('Exporter')} ))} {!editing && mode === 'manual' && (
{bodyEditing && ( <>
{bodyNodes.map(node => ( ))}
{t('Ajouter une forme')} · {bodyNodes.length}/{MAX_BODY_NODES}
{bodyPrimitiveTypes.map(type => ( ))}
{selectedBodyNode && (
{selectedBodyNode.name}

{t('Gizmo local')} {t( 'Glisse un axe pour déplacer la forme, ou un anneau pour la faire tourner.' )}

updateSelectedBodyNode(node => ({ ...node, surface: scaleSurface(node.surface, size, { width: 10, height: 10, depth: 10, }), })) } /> {(['width', 'height', 'depth'] as const).map(dimension => ( updateSelectedBodyNode(node => ({ ...node, surface: { ...node.surface, [dimension]: value }, })) } /> ))} {(selectedBodyNode.surface.type === 'cube' || selectedBodyNode.surface.type === 'diamond' || selectedBodyNode.surface.type === 'cylinder') && ( updateSelectedBodyNode(node => ({ ...node, surface: { ...node.surface, roundness }, })) } /> )} {(selectedBodyNode.surface.type === 'cylinder' || selectedBodyNode.surface.type === 'cone') && ( updateSelectedBodyNode(node => ({ ...node, surface: { ...node.surface, morphRoundness }, })) } /> )} {selectedBodyNode.surface.type === 'cone' && ( <> updateSelectedBodyNode(node => ({ ...node, surface: { ...node.surface, tipRoundness }, })) } /> updateSelectedBodyNode(node => ({ ...node, surface: { ...node.surface, baseRoundness }, })) } /> )}

{t('Position locale')}

{(['X', 'Y', 'Z'] as const).map((axis, index) => ( updateNodeVector('position', index as 0 | 1 | 2, value) } /> ))}

{t('Rotation locale')}

{(['X', 'Y', 'Z'] as const).map((axis, index) => ( updateNodeVector('rotation', index as 0 | 1 | 2, value) } /> ))}
)}
{bodyPrimitiveTypes.map(type => { const previewSurface = type === surface.type ? surface : surfacePresets[type] return ( ) })}
updateSurface( scaleSurface(surface, size, { width: 120, height: 120, depth: 100 }) ) } /> updateSurface({ ...surface, width })} /> updateSurface({ ...surface, height })} /> updateSurface({ ...surface, depth })} /> {(surface.type === 'cube' || surface.type === 'diamond') && ( updateHighlight(active ? 'head' : null)} onChange={roundness => updateSurface({ ...surface, roundness })} /> )} {surface.type === 'cylinder' && ( updateHighlight(active ? 'head' : null)} onChange={roundness => updateSurface({ ...surface, roundness })} /> )} {(surface.type === 'cylinder' || surface.type === 'cone') && ( updateHighlight(active ? 'head' : null)} onChange={morphRoundness => updateSurface({ ...surface, morphRoundness })} /> )} {surface.type === 'cone' && ( <> updateHighlight(active ? 'head' : null)} onChange={tipRoundness => updateSurface({ ...surface, tipRoundness })} /> updateHighlight(active ? 'head' : null)} onChange={baseRoundness => updateSurface({ ...surface, baseRoundness })} /> )}
updateAvatarColors({ body })} />

{t( 'Définis l’identité du regard de cet avatar. Les poses s’ajoutent ensuite à cette base.' )}

{(['width', 'height', 'size'] as const).map(dimension => (

{t( { width: 'Largeur', height: 'Hauteur', size: 'Taille proportionnelle', }[dimension] )}

setLinked(current => ({ ...current, [dimension]: !current[dimension], })) } />
{(['Left', 'Right'] as Side[]).map(side => { const width = activeAvatarEyes[`width${side}`] const height = activeAvatarEyes[`height${side}`] const value = dimension === 'width' ? width : dimension === 'height' ? height : Math.max(width, height) return ( updateHighlight( active ? linked[dimension] ? 'both' : side === 'Left' ? 'left' : 'right' : null ) } onChange={next => dimension === 'size' ? updateAvatarEyeSize(side, next) : updateAvatarEyeDimension(side, dimension, next) } /> ) })}
))}

{t('Position et espacement')}

{t('Coordonnées propres à l’avatar, indépendantes des poses.')}

setLinked(current => ({ ...current, position: !current.position })) } />
{(['Left', 'Right'] as Side[]).map(side => (

{t(side === 'Left' ? 'Œil gauche' : 'Œil droit')}

updateHighlight( active ? linked.position ? 'both' : side === 'Left' ? 'left' : 'right' : null ) } onChange={value => updateAvatarEyePosition(side, 'X', value)} /> updateHighlight( active ? linked.position ? 'both' : side === 'Left' ? 'left' : 'right' : null ) } onChange={value => updateAvatarEyePosition(side, 'Y', value)} />
))}
updateHighlight(active ? 'both' : null)} onChange={spacing => updateAvatarEyes({ spacing })} />
setLinked(current => ({ ...current, rotation: !current.rotation })) } />
updateHighlight(active ? (linked.rotation ? 'both' : 'left') : null) } onChange={leftAngle => updateAvatarEyes({ leftAngle, ...(linked.rotation ? { rightAngle: -leftAngle } : {}), }) } /> updateHighlight(active ? (linked.rotation ? 'both' : 'right') : null) } onChange={rightAngle => updateAvatarEyes({ rightAngle, ...(linked.rotation ? { leftAngle: -rightAngle } : {}), }) } />
updateAvatarColors({ eyes })} />
)} {!bodyEditing && ( <> updateImmediate({ ...expression, bodyColor })} /> {expression.bodyColor && ( )} updateHighlight(active ? 'head' : null)} onChange={value => updateImmediate({ ...expression, headX: value })} /> updateHighlight(active ? 'head' : null)} onChange={value => updateImmediate({ ...expression, headY: value })} /> updateHighlight(active ? 'head' : null)} onChange={value => updateImmediate({ ...expression, headZ: value })} /> updateImmediate({ ...expression, eyeColor })} /> {expression.eyeColor && ( )} {(['width', 'height', 'size'] as const).map(dimension => (

{t( { width: 'Largeur', height: 'Hauteur', size: 'Taille proportionnelle', }[dimension] )}

setLinked(current => ({ ...current, [dimension]: !current[dimension], })) } />
{(['Left', 'Right'] as Side[]).map(side => { const width = expression[`width${side}`] const height = expression[`height${side}`] const value = dimension === 'width' ? width : dimension === 'height' ? height : Math.max(width, height) return ( updateHighlight( active ? linked[dimension] ? 'both' : side === 'Left' ? 'left' : 'right' : null ) } onChange={next => dimension === 'size' ? updateSize(side, next) : updateDimension(side, dimension, next) } /> ) })}
))}

{t('Œil gauche')}

updateHighlight(active ? 'left' : null)} onChange={value => updateImmediate({ ...expression, positionXLeft: value })} /> updateHighlight(active ? 'left' : null)} onChange={value => updateImmediate({ ...expression, positionYLeft: value })} />

{t('Œil droit')}

updateHighlight(active ? 'right' : null)} onChange={value => updateImmediate({ ...expression, positionXRight: value }) } /> updateHighlight(active ? 'right' : null)} onChange={value => updateImmediate({ ...expression, positionYRight: value }) } />
updateHighlight(active ? 'both' : null)} onChange={updateSpacing} />
setLinked(current => ({ ...current, rotation: !current.rotation })) } />
updateHighlight(active ? (linked.rotation ? 'both' : 'left') : null) } onChange={value => updateImmediate({ ...expression, leftAngle: value, ...(linked.rotation ? { rightAngle: -value } : {}), }) } /> updateHighlight(active ? (linked.rotation ? 'both' : 'right') : null) } onChange={value => updateImmediate({ ...expression, rightAngle: value, ...(linked.rotation ? { leftAngle: -value } : {}), }) } />
updateImmediate({ ...expression, perspective: value })} />
{t('Afficher le maillage')}
)}
)} {!sequenceEditing && !editing && bodyEditing && ( )} {!sequenceEditing && !editing && !bodyEditing && mode === 'expressions' && (

{expressions.length} presets

{t('Expressions')}

{t('Double-clic pour modifier')}
{expressions.map((preset, index) => ( transitionToExpression(preset, index)} onEdit={() => openExpressionEditor(index, preset)} onDuplicate={() => duplicateExpression(index, preset)} onDelete={() => { openExpressionEditor(index, preset) setDeleteExpressionOpen(true) }} draggable onDragStart={event => { expressionDragOrigin.current = expressions expressionDragPreview.current = expressions draggedExpressionId.current = preset.id setDraggingExpressionId(preset.id) event.dataTransfer.effectAllowed = 'move' }} onDragEnter={() => previewExpressionMove(preset.id)} onDragOver={event => { event.preventDefault() event.dataTransfer.dropEffect = 'move' }} onDrop={event => { event.preventDefault() commitExpressionMove(preset.id) }} onDragEnd={cancelExpressionMove} /> ))}
{ springSpeedRef.current = value setSpringSpeed(value) }} />
)} {!sequenceEditing && !editing && !bodyEditing && mode === 'states' && (

{sequences.length} {t('animations')}

{t('Animations')}

{groupSequences(sequences).map(group => (
event.preventDefault()} onDrop={event => { event.preventDefault() commitStateMove(null, group.name) }} > {group.sequences.every(sequence => sequence.builtIn) ? t(group.name) : group.name}
{group.sequences.map(sequence => { const firstStep = sequence.steps[0] const firstExpression = firstStep ? expressionById.get(firstStep.expressionId) : undefined const card = ( ) return ( openSequenceEditor(sequence)}> {t('Modifier')} duplicateState(sequence)}> {t('Dupliquer')} { openSequenceEditor(sequence) setDeleteSequenceOpen(true) }} > {t('Supprimer')} ) })}
))}
)} {!sequenceEditing && !editing && !bodyEditing && mode === 'export' && (
{t('Avatar sélectionné')} {activeAvatar.name}

{selectedExportAnimations.length}/{sequences.length} {t('sélectionnées')}

{t('Animations à exporter')}

{sequences.map(animation => { const firstStep = animation.steps[0] const firstExpression = firstStep ? expressionById.get(firstStep.expressionId) : undefined return ( ) })}
{t('Aperçu du mode photo')} {activeAvatar.name} {snapshotSize} × {snapshotSize} px · {snapshotFormat.toUpperCase()}
{t('Style')} {snapshotBackground !== 'transparent' && (
{(snapshotBackground === 'linear' || snapshotBackground === 'radial') && ( )}
)}
{t('Format d’export')} {t('Choisis le type de fichier généré par le mode photo.')}
{t('Définition')} {t('Dimensions du fichier exporté.')}
{ prepareStudioProjectImport(event.currentTarget.files?.[0]) event.currentTarget.value = '' }} />
{projectImportError && (

{projectImportError}

)}
)} {activeSequence && !editorPageOpen && ( {statePlayerExpanded && (

{t('Détails de l’animation')}

{activeSequenceLabel}

{activeSequence.builtIn ? t(activeSequence.description) : activeSequence.description}

{t('Mode de lecture')} · {t(activeSequence.playbackMode)}
{t('Expressions')} {activeSequence.steps.length} {activeSequence.steps .map(step => formatSeconds(step.holdMs, language)) .join(' · ')}
{t('Premier clignement')} {activeSequence.blink.enabled ? formatSeconds(activeSequence.blink.initialDelayMs, language) : t('Désactivé')} {t('après le lancement')}
{t('Intervalle du clignement')} {formatSeconds(activeSequence.blink.minIntervalMs, language)}– {formatSeconds(activeSequence.blink.maxIntervalMs, language)} {t('tirage aléatoire')}
{t('Durée du clignement')} {activeSequence.blink.durationMs} ms {t('fermeture et ouverture')}
)}
{activeSequence.steps.map((step, position) => { const expressionIndex = findExpressionIndex(expressions, step.expressionId) const preset = expressions[expressionIndex] if (!preset) return null return ( ) })}
)}
) }