2026-04-15 06:04:59 +02:00
import {
useEffect ,
useRef ,
useState ,
type KeyboardEvent as ReactKeyboardEvent ,
2026-04-15 06:10:35 +02:00
type MouseEvent as ReactMouseEvent
2026-04-15 06:04:59 +02:00
} from "react" ;
2026-04-14 03:04:05 +02:00
import {
HOURS_PER_DAY ,
formatTimeOfDayHours
} from "../document/project-time-settings" ;
2026-04-15 00:20:24 +02:00
import type { ProjectDialogueLibrary } from "../dialogues/project-dialogues" ;
2026-04-14 03:04:05 +02:00
import { formatControlEffectValue , getControlTargetRefKey } from "../controls/control-surface" ;
2026-04-15 00:20:24 +02:00
import { ProjectSequencesPanel } from "./ProjectSequencesPanel" ;
2026-04-14 03:04:05 +02:00
import {
getProjectScheduleTimelineSegments ,
type ProjectScheduler ,
2026-04-14 23:20:38 +02:00
type ProjectScheduleRoutine
2026-04-14 23:14:31 +02:00
} from "../sequencer/project-sequencer" ;
2026-04-14 03:04:05 +02:00
import {
getProjectScheduleEffectOptionId ,
getProjectScheduleTargetOptionForRoutine ,
listProjectScheduleEffectOptions ,
type ProjectScheduleEffectOptionId ,
type ProjectScheduleTargetOption
2026-04-14 23:14:31 +02:00
} from "../sequencer/project-sequencer-control-options" ;
2026-04-14 23:22:10 +02:00
import {
findHeldSequenceControlEffect ,
2026-04-14 23:41:48 +02:00
getProjectScheduleRoutineHeldSteps ,
2026-04-15 02:09:03 +02:00
getProjectSequenceImpulseSteps ,
2026-04-14 23:41:48 +02:00
getProjectSequenceHeldSteps
2026-04-14 23:22:10 +02:00
} from "../sequencer/project-sequence-steps" ;
2026-04-14 23:41:48 +02:00
import {
getProjectSequences ,
type ProjectSequenceLibrary
} from "../sequencer/project-sequences" ;
2026-04-14 03:04:05 +02:00
2026-04-15 06:04:59 +02:00
const MINUTES_PER_DAY = HOURS_PER_DAY * 60 ;
const MINIMUM_SEQUENCE_PLACEMENT_DURATION_MINUTES = 1 ;
interface RoutineDragState {
routineId : string ;
mode : "move" | "resize-start" | "resize-end" ;
originStartMinutes : number ;
originEndMinutes : number ;
originTargetKey : string ;
pointerStartX : number ;
trackWidth : number ;
draftStartMinutes : number ;
draftEndMinutes : number ;
draftTargetKey : string ;
}
2026-04-14 23:14:31 +02:00
interface ProjectSequencerPaneProps {
2026-04-15 00:20:24 +02:00
mode : "timeline" | "sequence" ;
onSetMode ( mode : "timeline" | "sequence" ) : void ;
2026-04-14 03:04:05 +02:00
targetOptions : ProjectScheduleTargetOption [ ] ;
2026-04-15 01:29:05 +02:00
teleportTargetOptions : Array < {
entityId : string ;
label : string ;
} > ;
2026-04-15 02:08:50 +02:00
sceneTransitionTargetOptions : Array < {
targetKey : string ;
label : string ;
} > ;
2026-04-15 01:29:05 +02:00
visibilityTargetOptions : Array < {
2026-04-15 01:41:34 +02:00
targetKey : string ;
label : string ;
} > ;
modelAnimationTargetOptions : Array < {
targetKey : string ;
label : string ;
} > ;
soundTargetOptions : Array < {
targetKey : string ;
2026-04-15 01:29:05 +02:00
label : string ;
} > ;
2026-04-14 03:04:05 +02:00
scheduler : ProjectScheduler ;
2026-04-14 23:41:48 +02:00
sequences : ProjectSequenceLibrary ;
2026-04-15 00:20:24 +02:00
dialogues : ProjectDialogueLibrary ;
2026-04-14 03:04:05 +02:00
selectedRoutineId : string | null ;
2026-04-15 00:20:24 +02:00
selectedSequenceId : string | null ;
2026-04-14 03:04:05 +02:00
onSelectRoutine ( routineId : string | null ) : void ;
2026-04-15 00:20:24 +02:00
onSelectSequence ( sequenceId : string | null ) : void ;
2026-04-14 03:04:05 +02:00
onAddRoutine ( targetKey : string ) : void ;
2026-04-15 00:20:24 +02:00
onAddSequence ( ) : void ;
2026-04-14 03:04:05 +02:00
onDeleteRoutine ( routineId : string ) : void ;
2026-04-15 00:20:24 +02:00
onDeleteSequence ( sequenceId : string ) : void ;
2026-04-14 03:04:05 +02:00
onClose ( ) : void ;
2026-04-15 06:20:48 +02:00
onCreateRoutineSequence ( routineId : string ) : void ;
2026-04-14 03:04:05 +02:00
onSetRoutineTarget ( routineId : string , targetKey : string ) : void ;
onSetRoutineTitle ( routineId : string , title : string ) : void ;
onSetRoutineEnabled ( routineId : string , enabled : boolean ) : void ;
onSetRoutineStartHour ( routineId : string , startHour : number ) : void ;
onSetRoutineEndHour ( routineId : string , endHour : number ) : void ;
onSetRoutinePriority ( routineId : string , priority : number ) : void ;
2026-04-14 23:41:48 +02:00
onSetRoutineSequenceId ( routineId : string , sequenceId : string | null ) : void ;
2026-04-14 03:04:05 +02:00
onSetRoutineEffectOption (
routineId : string ,
effectOptionId : ProjectScheduleEffectOptionId
) : void ;
onSetRoutineNumericValue ( routineId : string , value : number ) : void ;
onSetRoutineColorValue ( routineId : string , colorHex : string ) : void ;
onSetRoutineAnimationClip ( routineId : string , clipName : string ) : void ;
onSetRoutineAnimationLoop ( routineId : string , loop : boolean ) : void ;
2026-04-14 13:48:11 +02:00
onSetActorRoutinePresence ( routineId : string , active : boolean ) : void ;
onSetActorRoutineAnimationClip (
routineId : string ,
clipName : string | null
) : void ;
onSetActorRoutineAnimationLoop ( routineId : string , loop : boolean ) : void ;
onSetActorRoutinePath ( routineId : string , pathId : string | null ) : void ;
onSetActorRoutinePathSpeed ( routineId : string , speed : number ) : void ;
onSetActorRoutinePathLoop ( routineId : string , loop : boolean ) : void ;
2026-04-15 00:20:24 +02:00
onSetSequenceTitle ( sequenceId : string , title : string ) : void ;
onAddHeldControlStep ( sequenceId : string , targetKey : string ) : void ;
onAddImpulseControlStep ( sequenceId : string , targetKey : string ) : void ;
onAddDialogueStep ( sequenceId : string , dialogueId : string ) : void ;
2026-04-15 01:29:05 +02:00
onAddTeleportStep ( sequenceId : string , targetEntityId : string ) : void ;
2026-04-15 02:08:50 +02:00
onAddSceneTransitionStep ( sequenceId : string , targetKey : string ) : void ;
2026-04-15 01:41:34 +02:00
onAddVisibilityStep ( sequenceId : string , targetKey : string ) : void ;
onAddPlayAnimationStep ( sequenceId : string , targetKey : string ) : void ;
onAddStopAnimationStep ( sequenceId : string , targetKey : string ) : void ;
onAddPlaySoundStep ( sequenceId : string , targetKey : string ) : void ;
onAddStopSoundStep ( sequenceId : string , targetKey : string ) : void ;
2026-04-15 00:20:24 +02:00
onDeleteStep ( sequenceId : string , stepIndex : number ) : void ;
onSetControlStepTarget (
sequenceId : string ,
stepIndex : number ,
targetKey : string
) : void ;
onSetControlStepEffectOption (
sequenceId : string ,
stepIndex : number ,
effectOptionId : ProjectScheduleEffectOptionId
) : void ;
onSetControlStepNumericValue (
sequenceId : string ,
stepIndex : number ,
value : number
) : void ;
onSetControlStepColorValue (
sequenceId : string ,
stepIndex : number ,
colorHex : string
) : void ;
onSetControlStepAnimationClip (
sequenceId : string ,
stepIndex : number ,
clipName : string
) : void ;
onSetControlStepAnimationLoop (
sequenceId : string ,
stepIndex : number ,
loop : boolean
) : void ;
2026-04-15 06:20:48 +02:00
onSetControlStepPathId (
sequenceId : string ,
stepIndex : number ,
pathId : string
) : void ;
onSetControlStepPathSpeed (
sequenceId : string ,
stepIndex : number ,
speed : number
) : void ;
onSetControlStepPathLoop (
sequenceId : string ,
stepIndex : number ,
loop : boolean
) : void ;
2026-04-15 00:20:24 +02:00
onSetDialogueStepDialogueId (
sequenceId : string ,
stepIndex : number ,
dialogueId : string
) : void ;
2026-04-15 01:29:05 +02:00
onSetTeleportStepTarget (
sequenceId : string ,
stepIndex : number ,
targetEntityId : string
) : void ;
2026-04-15 02:08:50 +02:00
onSetSceneTransitionStepTarget (
sequenceId : string ,
stepIndex : number ,
targetKey : string
) : void ;
2026-04-15 01:29:05 +02:00
onSetVisibilityStepTarget (
sequenceId : string ,
stepIndex : number ,
2026-04-15 01:41:34 +02:00
targetKey : string
2026-04-15 01:29:05 +02:00
) : void ;
onSetVisibilityStepMode (
sequenceId : string ,
stepIndex : number ,
mode : "toggle" | "show" | "hide"
) : void ;
2026-04-14 03:04:05 +02:00
}
function handleCommitOnEnter (
event : ReactKeyboardEvent < HTMLInputElement > ,
commit : ( ) = > void
) {
if ( event . key !== "Enter" ) {
return ;
}
event . currentTarget . blur ( ) ;
commit ( ) ;
}
2026-04-15 00:51:36 +02:00
function parseTimeOfDayInputHours ( value : string , label : string ) : number {
const match = /^(?<hours>\d{1,2}):(?<minutes>\d{2})$/ . exec ( value . trim ( ) ) ;
if ( match ? . groups === undefined ) {
throw new Error ( ` ${ label } must use HH:MM. ` ) ;
}
const hours = Number ( match . groups . hours ) ;
const minutes = Number ( match . groups . minutes ) ;
if (
! Number . isFinite ( hours ) ||
! Number . isFinite ( minutes ) ||
hours < 0 ||
hours >= HOURS_PER_DAY ||
minutes < 0 ||
minutes >= 60
) {
throw new Error ( ` ${ label } must be a valid time of day. ` ) ;
}
return hours + minutes / 60 ;
}
2026-04-15 06:04:59 +02:00
function normalizeMinuteOfDay ( minute : number ) : number {
const wrappedMinute = minute % MINUTES_PER_DAY ;
return wrappedMinute < 0 ? wrappedMinute + MINUTES_PER_DAY : wrappedMinute ;
}
function convertHoursToMinuteOfDay ( hours : number ) : number {
return normalizeMinuteOfDay ( Math . round ( hours * 60 ) ) ;
}
function convertMinuteOfDayToHours ( minute : number ) : number {
return normalizeMinuteOfDay ( minute ) / 60 ;
}
function getMinuteDistance ( startMinutes : number , endMinutes : number ) : number {
return endMinutes > startMinutes
? endMinutes - startMinutes
: MINUTES_PER_DAY - startMinutes + endMinutes ;
}
function resolveRoutineDragState (
dragState : RoutineDragState ,
clientX : number ,
clientY : number
) : RoutineDragState {
2026-04-15 06:09:41 +02:00
const resolvedClientX = Number . isFinite ( clientX )
? clientX
: dragState . pointerStartX ;
const resolvedClientY = Number . isFinite ( clientY ) ? clientY : 0 ;
2026-04-15 06:04:59 +02:00
const deltaMinutes = Math . round (
2026-04-15 06:09:41 +02:00
( ( resolvedClientX - dragState . pointerStartX ) / dragState . trackWidth ) *
MINUTES_PER_DAY
2026-04-15 06:04:59 +02:00
) ;
switch ( dragState . mode ) {
case "move" : {
const durationMinutes = getMinuteDistance (
dragState . originStartMinutes ,
dragState . originEndMinutes
) ;
const draftStartMinutes = normalizeMinuteOfDay (
dragState . originStartMinutes + deltaMinutes
) ;
const draftEndMinutes = normalizeMinuteOfDay (
draftStartMinutes + durationMinutes
) ;
2026-04-15 06:09:41 +02:00
const pointerTargetElement = document . elementFromPoint (
resolvedClientX ,
resolvedClientY
) ;
2026-04-15 06:04:59 +02:00
const draftTargetKey =
pointerTargetElement instanceof HTMLElement
? pointerTargetElement
. closest < HTMLElement > ( "[data-sequencer-target-key]" )
? . dataset . sequencerTargetKey ? ? dragState . originTargetKey
: dragState . originTargetKey ;
return {
. . . dragState ,
draftStartMinutes ,
draftEndMinutes ,
draftTargetKey
} ;
}
case "resize-start" : {
let draftStartMinutes = normalizeMinuteOfDay (
dragState . originStartMinutes + deltaMinutes
) ;
if (
getMinuteDistance ( draftStartMinutes , dragState . originEndMinutes ) <
MINIMUM_SEQUENCE_PLACEMENT_DURATION_MINUTES
) {
draftStartMinutes = normalizeMinuteOfDay (
dragState . originEndMinutes - MINIMUM_SEQUENCE_PLACEMENT_DURATION_MINUTES
) ;
}
return {
. . . dragState ,
draftStartMinutes
} ;
}
case "resize-end" : {
let draftEndMinutes = normalizeMinuteOfDay (
dragState . originEndMinutes + deltaMinutes
) ;
if (
getMinuteDistance ( dragState . originStartMinutes , draftEndMinutes ) <
MINIMUM_SEQUENCE_PLACEMENT_DURATION_MINUTES
) {
draftEndMinutes = normalizeMinuteOfDay (
dragState . originStartMinutes + MINIMUM_SEQUENCE_PLACEMENT_DURATION_MINUTES
) ;
}
return {
. . . dragState ,
draftEndMinutes
} ;
}
}
}
2026-04-14 23:41:48 +02:00
function getRoutineSummary (
routine : ProjectScheduleRoutine ,
sequences : ProjectSequenceLibrary
) : string {
2026-04-14 13:48:11 +02:00
const summaryParts = [
` ${ formatTimeOfDayHours ( routine . startHour ) } - ${ formatTimeOfDayHours ( routine . endHour ) } `
] ;
2026-04-14 23:41:48 +02:00
const heldSteps = getProjectScheduleRoutineHeldSteps ( routine , sequences ) ;
2026-04-14 13:48:11 +02:00
if ( routine . target . kind === "actor" ) {
2026-04-14 23:22:10 +02:00
const presenceEffect = findHeldSequenceControlEffect (
heldSteps ,
2026-04-14 13:48:11 +02:00
"setActorPresence"
) ;
2026-04-14 23:22:10 +02:00
const animationEffect = findHeldSequenceControlEffect (
heldSteps ,
2026-04-14 13:48:11 +02:00
"playActorAnimation"
) ;
2026-04-14 23:22:10 +02:00
const pathEffect = findHeldSequenceControlEffect (
heldSteps ,
"followActorPath"
) ;
2026-04-14 13:48:11 +02:00
summaryParts . push ( presenceEffect ? . active === false ? "Hidden" : "Present" ) ;
if ( animationEffect !== null ) {
summaryParts . push ( formatControlEffectValue ( animationEffect ) ) ;
}
if ( pathEffect !== null ) {
summaryParts . push ( formatControlEffectValue ( pathEffect ) ) ;
}
} else {
2026-04-14 23:43:09 +02:00
const effect = heldSteps [ 0 ] ;
2026-04-15 02:09:03 +02:00
if ( routine . target . kind === "global" && routine . sequenceId !== null ) {
summaryParts . push ( "Impulse Sequence" ) ;
} else if ( effect ? . type === "controlEffect" ) {
2026-04-14 23:43:09 +02:00
summaryParts . push ( formatControlEffectValue ( effect . effect ) ) ;
}
2026-04-14 13:48:11 +02:00
}
summaryParts . push ( ` P ${ routine . priority } ` ) ;
return summaryParts . join ( " · " ) ;
2026-04-14 03:04:05 +02:00
}
2026-04-14 23:41:48 +02:00
function isRoutineEffectInactive (
routine : ProjectScheduleRoutine ,
sequences : ProjectSequenceLibrary
) : boolean {
const heldSteps = getProjectScheduleRoutineHeldSteps ( routine , sequences ) ;
2026-04-14 23:22:10 +02:00
2026-04-14 13:48:11 +02:00
if ( routine . target . kind === "actor" ) {
return (
2026-04-14 23:22:10 +02:00
findHeldSequenceControlEffect ( heldSteps , "setActorPresence" ) ? . active === false
2026-04-14 13:48:11 +02:00
) ;
}
2026-04-14 23:43:09 +02:00
const effect =
heldSteps [ 0 ] ? . type === "controlEffect" ? heldSteps [ 0 ] . effect : undefined ;
2026-04-14 13:48:11 +02:00
if ( effect === undefined ) {
return false ;
}
switch ( effect . type ) {
2026-04-14 03:04:05 +02:00
case "stopModelAnimation" :
case "stopSound" :
return true ;
case "setModelInstanceVisible" :
2026-04-14 13:48:11 +02:00
return ! effect . visible ;
2026-04-14 03:04:05 +02:00
case "setInteractionEnabled" :
case "setLightEnabled" :
2026-04-14 13:48:11 +02:00
return ! effect . enabled ;
2026-04-14 03:04:05 +02:00
default :
return false ;
}
}
function getRoutineNumericValue ( routine : ProjectScheduleRoutine ) : number | null {
2026-04-14 13:48:11 +02:00
const effect = routine . effects [ 0 ] ;
if ( effect === undefined ) {
return null ;
}
switch ( effect . type ) {
2026-04-14 03:04:05 +02:00
case "setSoundVolume" :
2026-04-14 13:48:11 +02:00
return effect . volume ;
2026-04-14 03:04:05 +02:00
case "setLightIntensity" :
case "setAmbientLightIntensity" :
case "setSunLightIntensity" :
2026-04-14 13:48:11 +02:00
return effect . intensity ;
2026-04-14 03:04:05 +02:00
default :
return null ;
}
}
function getRoutineColorValue ( routine : ProjectScheduleRoutine ) : string | null {
2026-04-14 13:48:11 +02:00
const effect = routine . effects [ 0 ] ;
if ( effect === undefined ) {
return null ;
}
switch ( effect . type ) {
2026-04-14 03:04:05 +02:00
case "setLightColor" :
case "setAmbientLightColor" :
case "setSunLightColor" :
2026-04-14 13:48:11 +02:00
return effect . colorHex ;
2026-04-14 03:04:05 +02:00
default :
return null ;
}
}
2026-04-14 23:14:31 +02:00
export function ProjectSequencerPane ( {
2026-04-15 00:20:41 +02:00
mode ,
onSetMode ,
2026-04-14 03:04:05 +02:00
targetOptions ,
2026-04-15 01:29:05 +02:00
teleportTargetOptions ,
2026-04-15 02:08:50 +02:00
sceneTransitionTargetOptions ,
2026-04-15 01:29:05 +02:00
visibilityTargetOptions ,
2026-04-15 01:41:34 +02:00
modelAnimationTargetOptions ,
soundTargetOptions ,
2026-04-14 03:04:05 +02:00
scheduler ,
2026-04-14 23:42:08 +02:00
sequences ,
2026-04-15 00:20:41 +02:00
dialogues ,
2026-04-14 03:04:05 +02:00
selectedRoutineId ,
2026-04-15 00:20:41 +02:00
selectedSequenceId ,
2026-04-14 03:04:05 +02:00
onSelectRoutine ,
2026-04-15 00:20:41 +02:00
onSelectSequence ,
2026-04-14 03:04:05 +02:00
onAddRoutine ,
2026-04-15 00:20:41 +02:00
onAddSequence ,
2026-04-14 03:04:05 +02:00
onDeleteRoutine ,
2026-04-15 00:20:41 +02:00
onDeleteSequence ,
2026-04-14 03:04:05 +02:00
onClose ,
2026-04-15 06:20:48 +02:00
onCreateRoutineSequence ,
2026-04-15 06:06:50 +02:00
onSetRoutineTarget ,
2026-04-14 03:04:05 +02:00
onSetRoutineTitle ,
onSetRoutineEnabled ,
onSetRoutineStartHour ,
onSetRoutineEndHour ,
onSetRoutinePriority ,
2026-04-14 23:42:08 +02:00
onSetRoutineSequenceId ,
2026-04-14 03:04:05 +02:00
onSetRoutineEffectOption ,
onSetRoutineNumericValue ,
onSetRoutineColorValue ,
onSetRoutineAnimationClip ,
2026-04-14 13:48:25 +02:00
onSetRoutineAnimationLoop ,
onSetActorRoutinePresence ,
onSetActorRoutineAnimationClip ,
onSetActorRoutineAnimationLoop ,
onSetActorRoutinePath ,
onSetActorRoutinePathSpeed ,
2026-04-15 00:20:41 +02:00
onSetActorRoutinePathLoop ,
onSetSequenceTitle ,
onAddHeldControlStep ,
onAddImpulseControlStep ,
onAddDialogueStep ,
2026-04-15 01:29:05 +02:00
onAddTeleportStep ,
2026-04-15 02:08:50 +02:00
onAddSceneTransitionStep ,
2026-04-15 01:29:05 +02:00
onAddVisibilityStep ,
2026-04-15 01:41:34 +02:00
onAddPlayAnimationStep ,
onAddStopAnimationStep ,
onAddPlaySoundStep ,
onAddStopSoundStep ,
2026-04-15 00:20:41 +02:00
onDeleteStep ,
onSetControlStepTarget ,
onSetControlStepEffectOption ,
onSetControlStepNumericValue ,
onSetControlStepColorValue ,
onSetControlStepAnimationClip ,
onSetControlStepAnimationLoop ,
2026-04-15 06:20:48 +02:00
onSetControlStepPathId ,
onSetControlStepPathSpeed ,
onSetControlStepPathLoop ,
2026-04-15 01:29:05 +02:00
onSetDialogueStepDialogueId ,
onSetTeleportStepTarget ,
2026-04-15 02:08:50 +02:00
onSetSceneTransitionStepTarget ,
2026-04-15 01:29:05 +02:00
onSetVisibilityStepTarget ,
onSetVisibilityStepMode
2026-04-14 23:14:31 +02:00
} : ProjectSequencerPaneProps ) {
2026-04-15 06:05:18 +02:00
const [ routineDragState , setRoutineDragState ] = useState < RoutineDragState | null > (
null
) ;
const routineDragStateRef = useRef < RoutineDragState | null > ( null ) ;
2026-04-15 06:08:38 +02:00
const routineDragCleanupRef = useRef < ( ( ) = > void ) | null > ( null ) ;
2026-04-15 06:05:18 +02:00
useEffect ( ( ) = > {
routineDragStateRef . current = routineDragState ;
} , [ routineDragState ] ) ;
useEffect ( ( ) = > {
2026-04-15 06:08:38 +02:00
return ( ) = > {
routineDragCleanupRef . current ? . ( ) ;
} ;
} , [ ] ) ;
const beginRoutineDrag = (
2026-04-15 06:10:35 +02:00
event : ReactMouseEvent < HTMLElement > ,
2026-04-15 06:08:38 +02:00
routine : ProjectScheduleRoutine ,
mode : RoutineDragState [ "mode" ]
) = > {
const trackElement = event . currentTarget . closest < HTMLElement > (
"[data-sequencer-track='true']"
) ;
if ( trackElement === null ) {
return ;
}
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
onSelectRoutine ( routine . id ) ;
const nextDragState : RoutineDragState = {
routineId : routine.id ,
mode ,
originStartMinutes : convertHoursToMinuteOfDay ( routine . startHour ) ,
originEndMinutes : convertHoursToMinuteOfDay ( routine . endHour ) ,
originTargetKey : getControlTargetRefKey ( routine . target ) ,
2026-04-15 06:09:41 +02:00
pointerStartX : Number.isFinite ( event . clientX ) ? event.clientX : 0 ,
2026-04-15 06:08:38 +02:00
trackWidth : Math.max ( trackElement . getBoundingClientRect ( ) . width , 1 ) ,
draftStartMinutes : convertHoursToMinuteOfDay ( routine . startHour ) ,
draftEndMinutes : convertHoursToMinuteOfDay ( routine . endHour ) ,
draftTargetKey : getControlTargetRefKey ( routine . target )
} ;
routineDragCleanupRef . current ? . ( ) ;
2026-04-15 06:05:18 +02:00
const previousUserSelect = document . body . style . userSelect ;
document . body . style . userSelect = "none" ;
2026-04-15 06:10:35 +02:00
const handlePointerMove = ( pointerEvent : MouseEvent ) = > {
2026-04-15 06:08:38 +02:00
const currentDragState = routineDragStateRef . current ;
2026-04-15 06:05:18 +02:00
2026-04-15 06:08:38 +02:00
if ( currentDragState === null ) {
return ;
}
const updatedDragState = resolveRoutineDragState (
currentDragState ,
pointerEvent . clientX ,
pointerEvent . clientY
) ;
routineDragStateRef . current = updatedDragState ;
setRoutineDragState ( updatedDragState ) ;
2026-04-15 06:05:18 +02:00
} ;
const handlePointerUp = ( ) = > {
const finalDragState = routineDragStateRef . current ;
document . body . style . userSelect = previousUserSelect ;
2026-04-15 06:10:35 +02:00
window . removeEventListener ( "mousemove" , handlePointerMove ) ;
window . removeEventListener ( "mouseup" , handlePointerUp ) ;
2026-04-15 06:08:38 +02:00
routineDragCleanupRef . current = null ;
2026-04-15 06:05:18 +02:00
setRoutineDragState ( null ) ;
routineDragStateRef . current = null ;
if ( finalDragState === null ) {
return ;
}
if ( finalDragState . draftTargetKey !== finalDragState . originTargetKey ) {
onSetRoutineTarget ( finalDragState . routineId , finalDragState . draftTargetKey ) ;
}
if ( finalDragState . draftStartMinutes !== finalDragState . originStartMinutes ) {
onSetRoutineStartHour (
finalDragState . routineId ,
convertMinuteOfDayToHours ( finalDragState . draftStartMinutes )
) ;
}
if ( finalDragState . draftEndMinutes !== finalDragState . originEndMinutes ) {
onSetRoutineEndHour (
finalDragState . routineId ,
convertMinuteOfDayToHours ( finalDragState . draftEndMinutes )
) ;
}
} ;
2026-04-15 06:08:38 +02:00
routineDragCleanupRef . current = ( ) = > {
2026-04-15 06:05:18 +02:00
document . body . style . userSelect = previousUserSelect ;
2026-04-15 06:10:35 +02:00
window . removeEventListener ( "mousemove" , handlePointerMove ) ;
window . removeEventListener ( "mouseup" , handlePointerUp ) ;
2026-04-15 06:08:38 +02:00
routineDragCleanupRef . current = null ;
2026-04-15 06:05:18 +02:00
} ;
2026-04-15 06:10:35 +02:00
window . addEventListener ( "mousemove" , handlePointerMove ) ;
window . addEventListener ( "mouseup" , handlePointerUp ) ;
2026-04-15 06:05:18 +02:00
routineDragStateRef . current = nextDragState ;
setRoutineDragState ( nextDragState ) ;
} ;
const getRenderedRoutineTargetKey = ( routine : ProjectScheduleRoutine ) : string = >
routineDragState ? . routineId === routine . id
? routineDragState . draftTargetKey
: getControlTargetRefKey ( routine . target ) ;
const getRenderedRoutine = (
routine : ProjectScheduleRoutine
) : ProjectScheduleRoutine = >
routineDragState ? . routineId === routine . id
? {
. . . routine ,
startHour : convertMinuteOfDayToHours ( routineDragState . draftStartMinutes ) ,
endHour : convertMinuteOfDayToHours ( routineDragState . draftEndMinutes )
}
: routine ;
2026-04-14 03:04:05 +02:00
const selectedRoutine =
selectedRoutineId === null ? null : scheduler . routines [ selectedRoutineId ] ? ? null ;
2026-04-14 23:42:08 +02:00
const selectedRoutineHeldSteps =
selectedRoutine === null
? [ ]
: getProjectScheduleRoutineHeldSteps ( selectedRoutine , sequences ) ;
2026-04-14 03:04:05 +02:00
const selectedTargetOption =
selectedRoutine === null
? null
: getProjectScheduleTargetOptionForRoutine (
targetOptions ,
selectedRoutine . target
) ;
2026-04-14 23:42:08 +02:00
const compatibleHeldSequences =
selectedRoutine === null
? [ ]
2026-04-15 02:08:50 +02:00
: selectedRoutine . target . kind === "global"
? getProjectSequences ( sequences ) . filter (
( sequence ) = > getProjectSequenceImpulseSteps ( sequence ) . length > 0
)
: getProjectSequences ( sequences ) . filter ( ( sequence ) = > {
2026-04-14 23:42:08 +02:00
const heldSteps = getProjectSequenceHeldSteps ( sequence ) ;
if ( heldSteps . length === 0 ) {
return false ;
}
return heldSteps . every (
( step ) = >
step . type === "controlEffect" &&
getControlTargetRefKey ( step . effect . target ) ===
getControlTargetRefKey ( selectedRoutine . target )
) ;
} ) ;
2026-04-15 06:21:06 +02:00
const selectedAttachedSequence =
selectedRoutine ? . sequenceId === null || selectedRoutine ? . sequenceId === undefined
? null
: sequences . sequences [ selectedRoutine . sequenceId ] ? ? null ;
2026-04-14 03:04:05 +02:00
const hourTicks = Array . from ( { length : HOURS_PER_DAY } , ( _ , hour ) = > hour ) ;
return (
2026-04-14 23:14:31 +02:00
< section className = "schedule-pane" data-testid = "project-sequencer-pane" >
2026-04-14 03:04:05 +02:00
< div className = "schedule-pane__header" >
< div >
2026-04-14 23:14:31 +02:00
< div className = "label" > Sequencer < / div >
2026-04-14 03:04:05 +02:00
< div className = "schedule-pane__summary" >
2026-04-15 00:20:41 +02:00
{ mode === "timeline"
2026-04-15 01:13:32 +02:00
? "Place sequences over global project time."
: "Compose reusable sequences from engine effects for timeline and interaction playback." }
2026-04-14 03:04:05 +02:00
< / div >
< / div >
< div className = "schedule-pane__actions" >
2026-04-15 01:19:23 +02:00
{ mode !== "timeline" ? (
< button
className = "toolbar__button toolbar__button--compact"
type = "button"
onClick = { ( ) = > onSetMode ( "timeline" ) }
>
Timeline
< / button >
) : null }
{ mode !== "sequence" ? (
< button
className = "toolbar__button toolbar__button--compact"
type = "button"
onClick = { ( ) = > onSetMode ( "sequence" ) }
>
Sequence Editor
< / button >
) : null }
2026-04-14 03:04:05 +02:00
< button
className = "toolbar__button toolbar__button--compact"
type = "button"
2026-04-15 00:20:41 +02:00
disabled = { mode === "timeline" ? targetOptions . length === 0 : false }
onClick = { ( ) = > {
if ( mode === "timeline" ) {
onAddRoutine ( selectedTargetOption ? . key ? ? targetOptions [ 0 ] ? . key ? ? "" ) ;
return ;
}
onAddSequence ( ) ;
} }
2026-04-14 03:04:05 +02:00
>
2026-04-15 01:17:30 +02:00
{ mode === "timeline" ? "Add Sequence" : "Create Sequence" }
2026-04-14 03:04:05 +02:00
< / button >
< button
className = "toolbar__button toolbar__button--compact"
type = "button"
onClick = { onClose }
>
Close
< / button >
< / div >
< / div >
< div className = "schedule-pane__body" >
2026-04-15 00:20:41 +02:00
{ mode === "sequence" ? (
< div className = "schedule-pane__editor" style = { { width : "100%" } } >
< ProjectSequencesPanel
sequences = { sequences }
dialogues = { dialogues }
targetOptions = { targetOptions }
2026-04-15 01:29:05 +02:00
teleportTargetOptions = { teleportTargetOptions }
2026-04-15 02:08:50 +02:00
sceneTransitionTargetOptions = { sceneTransitionTargetOptions }
2026-04-15 01:29:05 +02:00
visibilityTargetOptions = { visibilityTargetOptions }
2026-04-15 01:41:34 +02:00
modelAnimationTargetOptions = { modelAnimationTargetOptions }
soundTargetOptions = { soundTargetOptions }
2026-04-15 00:20:41 +02:00
selectedSequenceId = { selectedSequenceId }
onSelectSequence = { onSelectSequence }
onAddSequence = { onAddSequence }
onDeleteSequence = { onDeleteSequence }
onSetSequenceTitle = { onSetSequenceTitle }
onAddHeldControlStep = { onAddHeldControlStep }
onAddImpulseControlStep = { onAddImpulseControlStep }
onAddDialogueStep = { onAddDialogueStep }
2026-04-15 01:29:05 +02:00
onAddTeleportStep = { onAddTeleportStep }
2026-04-15 02:08:50 +02:00
onAddSceneTransitionStep = { onAddSceneTransitionStep }
2026-04-15 01:29:05 +02:00
onAddVisibilityStep = { onAddVisibilityStep }
2026-04-15 01:41:34 +02:00
onAddPlayAnimationStep = { onAddPlayAnimationStep }
onAddStopAnimationStep = { onAddStopAnimationStep }
onAddPlaySoundStep = { onAddPlaySoundStep }
onAddStopSoundStep = { onAddStopSoundStep }
2026-04-15 00:20:41 +02:00
onDeleteStep = { onDeleteStep }
onSetControlStepTarget = { onSetControlStepTarget }
onSetControlStepEffectOption = { onSetControlStepEffectOption }
onSetControlStepNumericValue = { onSetControlStepNumericValue }
onSetControlStepColorValue = { onSetControlStepColorValue }
onSetControlStepAnimationClip = { onSetControlStepAnimationClip }
onSetControlStepAnimationLoop = { onSetControlStepAnimationLoop }
2026-04-15 06:20:48 +02:00
onSetControlStepPathId = { onSetControlStepPathId }
onSetControlStepPathSpeed = { onSetControlStepPathSpeed }
onSetControlStepPathLoop = { onSetControlStepPathLoop }
2026-04-15 00:20:41 +02:00
onSetDialogueStepDialogueId = { onSetDialogueStepDialogueId }
2026-04-15 01:29:05 +02:00
onSetTeleportStepTarget = { onSetTeleportStepTarget }
2026-04-15 02:08:50 +02:00
onSetSceneTransitionStepTarget = { onSetSceneTransitionStepTarget }
2026-04-15 01:29:05 +02:00
onSetVisibilityStepTarget = { onSetVisibilityStepTarget }
onSetVisibilityStepMode = { onSetVisibilityStepMode }
2026-04-15 00:20:41 +02:00
/ >
< / div >
) : (
2026-04-15 00:24:20 +02:00
< >
2026-04-14 03:04:05 +02:00
< div className = "schedule-pane__timeline" >
< div className = "schedule-ruler" >
< div className = "schedule-ruler__label" > Targets < / div >
< div className = "schedule-ruler__track" >
{ hourTicks . map ( ( hour ) = > (
< div key = { hour } className = "schedule-ruler__tick" >
< span > { String ( hour ) . padStart ( 2 , "0" ) } < / span >
< / div >
) ) }
< / div >
< / div >
{ targetOptions . length === 0 ? (
< div className = "schedule-pane__empty" >
2026-04-14 23:14:31 +02:00
No sequencer - addressable control targets are authored in this
2026-04-14 03:04:05 +02:00
project yet .
< / div >
) : (
targetOptions . map ( ( targetOption ) = > {
const routines = Object . values ( scheduler . routines )
. filter (
2026-04-15 06:05:27 +02:00
( routine ) = > getRenderedRoutineTargetKey ( routine ) === targetOption . key
2026-04-14 03:04:05 +02:00
)
2026-04-15 06:05:41 +02:00
. sort (
( left , right ) = >
getRenderedRoutine ( left ) . startHour -
getRenderedRoutine ( right ) . startHour
) ;
2026-04-14 03:04:05 +02:00
return (
< div key = { targetOption . key } className = "schedule-row" >
< div className = "schedule-row__label" >
< button
className = "schedule-row__add"
type = "button"
onClick = { ( ) = > onAddRoutine ( targetOption . key ) }
>
+
< / button >
< div className = "schedule-row__meta" >
< div className = "schedule-row__title" > { targetOption . label } < / div >
< div className = "schedule-row__subtitle" >
{ targetOption . groupLabel } · { targetOption . subtitle }
< / div >
< / div >
< / div >
2026-04-15 06:05:27 +02:00
< div
className = "schedule-row__track"
data - sequencer - target - key = { targetOption . key }
data - sequencer - track = "true"
>
2026-04-14 03:04:05 +02:00
< div className = "schedule-row__grid" / >
2026-04-15 06:05:27 +02:00
{ routines . map ( ( routine ) = > {
const renderedRoutine = getRenderedRoutine ( routine ) ;
return getProjectScheduleTimelineSegments ( renderedRoutine ) . map (
2026-04-14 03:04:05 +02:00
( segment ) = > (
< button
key = { segment . key }
className = { ` schedule-block ${
selectedRoutineId === routine . id
? "schedule-block--selected"
: ""
} $ {
2026-04-14 23:42:16 +02:00
isRoutineEffectInactive ( routine , sequences )
2026-04-14 03:04:05 +02:00
? "schedule-block--inactive"
: ""
} $ {
routine . enabled ? "" : "schedule-block--disabled"
2026-04-15 06:05:27 +02:00
} $ {
routineDragState ? . routineId === routine . id
? "schedule-block--dragging"
: ""
2026-04-14 03:04:05 +02:00
} ` .trim()}
type = "button"
2026-04-14 23:42:16 +02:00
title = { ` ${ routine . title } · ${ getRoutineSummary ( routine , sequences ) } ` }
2026-04-14 03:04:05 +02:00
style = { {
left : ` ${ ( segment . startHour / HOURS_PER_DAY ) * 100 } % ` ,
width : ` ${ ( ( segment . endHour - segment . startHour ) / HOURS_PER_DAY ) * 100 } % `
} }
2026-04-15 06:10:45 +02:00
onMouseDown = { ( event ) = >
2026-04-15 06:05:27 +02:00
beginRoutineDrag ( event , routine , "move" )
}
2026-04-14 03:04:05 +02:00
onClick = { ( ) = > onSelectRoutine ( routine . id ) }
>
2026-04-15 06:05:27 +02:00
< span
className = "schedule-block__resize-handle schedule-block__resize-handle--start"
aria - label = { ` Resize start of ${ routine . title } ` }
2026-04-15 06:10:45 +02:00
onMouseDown = { ( event ) = >
2026-04-15 06:05:27 +02:00
beginRoutineDrag ( event , routine , "resize-start" )
}
/ >
2026-04-14 03:04:05 +02:00
< span className = "schedule-block__title" >
{ routine . title }
< / span >
2026-04-15 06:05:27 +02:00
< span
className = "schedule-block__resize-handle schedule-block__resize-handle--end"
aria - label = { ` Resize end of ${ routine . title } ` }
2026-04-15 06:10:45 +02:00
onMouseDown = { ( event ) = >
2026-04-15 06:05:27 +02:00
beginRoutineDrag ( event , routine , "resize-end" )
}
/ >
2026-04-14 03:04:05 +02:00
< / button >
)
2026-04-15 06:05:27 +02:00
) ;
} ) }
2026-04-14 03:04:05 +02:00
< / div >
< / div >
) ;
} )
) }
< / div >
< aside className = "schedule-pane__editor" >
{ selectedRoutine === null || selectedTargetOption === null ? (
< div className = "schedule-pane__empty" >
2026-04-15 01:13:32 +02:00
Select a sequence placement or create a new one .
2026-04-14 03:04:05 +02:00
< / div >
) : (
< >
< div className = "form-section" >
2026-04-15 01:13:32 +02:00
< div className = "label" > Sequence Placement < / div >
2026-04-14 03:04:05 +02:00
< label className = "form-field" >
< span className = "label" > Title < / span >
< input
key = { ` ${ selectedRoutine . id } -title ` }
className = "text-input"
type = "text"
defaultValue = { selectedRoutine . title }
onBlur = { ( event ) = >
onSetRoutineTitle (
selectedRoutine . id ,
event . currentTarget . value
)
}
onKeyDown = { ( event ) = >
handleCommitOnEnter ( event , ( ) = >
onSetRoutineTitle (
selectedRoutine . id ,
event . currentTarget . value
)
)
}
/ >
< / label >
2026-04-14 23:42:32 +02:00
< label className = "form-field" >
2026-04-15 00:21:47 +02:00
< span className = "label" > Sequence < / span >
2026-04-14 23:42:32 +02:00
< select
className = "select-input"
value = { selectedRoutine . sequenceId ? ? "" }
onChange = { ( event ) = >
onSetRoutineSequenceId (
selectedRoutine . id ,
event . currentTarget . value === ""
? null
: event . currentTarget . value
)
}
>
2026-04-15 01:13:32 +02:00
< option value = "" > This Placement ( inline effects ) < / option >
2026-04-14 23:42:32 +02:00
{ compatibleHeldSequences . map ( ( sequence ) = > (
< option key = { sequence . id } value = { sequence . id } >
{ sequence . title }
< / option >
) ) }
< / select >
< / label >
2026-04-15 00:21:56 +02:00
< div className = "inline-actions" >
< button
className = "toolbar__button toolbar__button--compact"
type = "button"
onClick = { ( ) = > {
onSelectSequence ( selectedRoutine . sequenceId ? ? selectedSequenceId ) ;
onSetMode ( "sequence" ) ;
} }
>
Edit Sequences
< / button >
< / div >
2026-04-14 23:42:32 +02:00
{ selectedRoutine . sequenceId !== null ? (
< div className = "material-summary" >
2026-04-15 01:13:32 +02:00
This placement resolves held effects from the selected
sequence . Inline effects stay preserved as fallback , but are
not edited while a sequence is attached .
2026-04-14 23:42:32 +02:00
< / div >
) : null }
{ selectedRoutine . sequenceId === null &&
2026-04-15 00:07:52 +02:00
compatibleHeldSequences . length === 0 ? (
< div className = "material-summary" >
2026-04-15 02:09:03 +02:00
{ selectedRoutine . target . kind === "global"
? "No compatible impulse sequence is authored yet for this project event lane. Use Sequence Editor to author one-shot effects like scene transitions, dialogue starts, or teleports."
: "No compatible sequence with held effects is authored for this target yet. Use Sequence Editor to author effects, or keep this placement on its own inline effects." }
2026-04-15 00:07:52 +02:00
< / div >
) : null }
{ selectedRoutine . sequenceId === null &&
2026-04-14 23:42:32 +02:00
selectedRoutine . target . kind === "actor" ? (
2026-04-14 13:48:31 +02:00
< >
< label className = "form-field" >
< span className = "label" > Presence < / span >
< select
className = "select-input"
value = {
selectedActorPresenceEffect ? . active === false
? "hidden"
: "present"
}
onChange = { ( event ) = >
onSetActorRoutinePresence (
selectedRoutine . id ,
event . currentTarget . value !== "hidden"
)
}
>
< option value = "present" > Present < / option >
< option value = "hidden" > Hidden < / option >
< / select >
< / label >
< / >
2026-04-15 05:26:02 +02:00
) : selectedRoutine . target . kind === "global" ? (
< div className = "material-summary" >
Project event placements run attached sequences only . Add or
pick a sequence with impulse effects like scene transitions ,
dialogue starts , teleports , or other one - shot engine events .
< / div >
2026-04-14 23:42:32 +02:00
) : selectedRoutine . sequenceId === null ? (
2026-04-14 13:48:31 +02:00
< label className = "form-field" >
< span className = "label" > Effect < / span >
< select
className = "select-input"
value = { selectedEffectOptionId ? ? "" }
onChange = { ( event ) = >
onSetRoutineEffectOption (
selectedRoutine . id ,
event . currentTarget . value as ProjectScheduleEffectOptionId
)
}
>
{ selectedEffectOptions . map ( ( effectOption ) = > (
< option key = { effectOption . id } value = { effectOption . id } >
{ effectOption . label }
< / option >
) ) }
< / select >
< / label >
2026-04-14 23:42:32 +02:00
) : null }
2026-04-14 03:04:05 +02:00
< label className = "form-field form-field--inline" >
< input
type = "checkbox"
checked = { selectedRoutine . enabled }
onChange = { ( event ) = >
onSetRoutineEnabled (
selectedRoutine . id ,
event . currentTarget . checked
)
}
/ >
< span className = "label" > Enabled < / span >
< / label >
< / div >
< div className = "form-section" >
< div className = "label" > Window < / div >
< div className = "vector-inputs vector-inputs--two" >
< label className = "form-field" >
< span className = "label" > Start < / span >
< input
key = { ` ${ selectedRoutine . id } -start ` }
className = "text-input"
2026-04-15 00:52:41 +02:00
type = "time"
step = "60"
defaultValue = { formatTimeOfDayHours ( selectedRoutine . startHour ) }
2026-04-14 03:04:05 +02:00
onBlur = { ( event ) = >
onSetRoutineStartHour (
selectedRoutine . id ,
2026-04-15 00:52:41 +02:00
parseTimeOfDayInputHours (
event . currentTarget . value ,
"Clip start"
)
2026-04-14 03:04:05 +02:00
)
}
onKeyDown = { ( event ) = >
handleCommitOnEnter ( event , ( ) = >
onSetRoutineStartHour (
selectedRoutine . id ,
2026-04-15 00:52:41 +02:00
parseTimeOfDayInputHours (
event . currentTarget . value ,
"Clip start"
)
2026-04-14 03:04:05 +02:00
)
)
}
/ >
< / label >
< label className = "form-field" >
< span className = "label" > End < / span >
< input
key = { ` ${ selectedRoutine . id } -end ` }
className = "text-input"
2026-04-15 00:52:41 +02:00
type = "time"
step = "60"
defaultValue = { formatTimeOfDayHours ( selectedRoutine . endHour ) }
2026-04-14 03:04:05 +02:00
onBlur = { ( event ) = >
onSetRoutineEndHour (
selectedRoutine . id ,
2026-04-15 00:52:41 +02:00
parseTimeOfDayInputHours (
event . currentTarget . value ,
"Clip end"
)
2026-04-14 03:04:05 +02:00
)
}
onKeyDown = { ( event ) = >
handleCommitOnEnter ( event , ( ) = >
onSetRoutineEndHour (
selectedRoutine . id ,
2026-04-15 00:52:41 +02:00
parseTimeOfDayInputHours (
event . currentTarget . value ,
"Clip end"
)
2026-04-14 03:04:05 +02:00
)
)
}
/ >
< / label >
< / div >
< label className = "form-field" >
< span className = "label" > Priority < / span >
< input
key = { ` ${ selectedRoutine . id } -priority ` }
className = "text-input"
type = "number"
step = "1"
defaultValue = { selectedRoutine . priority }
onBlur = { ( event ) = >
onSetRoutinePriority (
selectedRoutine . id ,
Number ( event . currentTarget . value )
)
}
onKeyDown = { ( event ) = >
handleCommitOnEnter ( event , ( ) = >
onSetRoutinePriority (
selectedRoutine . id ,
Number ( event . currentTarget . value )
)
)
}
/ >
< / label >
< / div >
2026-04-14 23:42:59 +02:00
{ selectedRoutine . sequenceId === null &&
selectedRoutine . target . kind === "actor" ? (
2026-04-14 13:49:09 +02:00
< >
< div className = "form-section" >
< div className = "label" > Animation < / div >
< label className = "form-field" >
< span className = "label" > Clip < / span >
< select
className = "select-input"
value = { selectedActorAnimationEffect ? . clipName ? ? "" }
onChange = { ( event ) = >
onSetActorRoutineAnimationClip (
2026-04-14 03:04:05 +02:00
selectedRoutine . id ,
2026-04-14 13:49:09 +02:00
event . currentTarget . value === ""
? null
: event . currentTarget . value
2026-04-14 03:04:05 +02:00
)
2026-04-14 13:49:09 +02:00
}
>
< option value = "" > None < / option >
{ ( selectedTargetOption . defaults . actorAnimationClipNames ? ? [ ] ) . map (
( clipName ) = > (
< option key = { clipName } value = { clipName } >
{ clipName }
< / option >
)
) }
< / select >
< / label >
< label className = "form-field form-field--inline" >
< input
type = "checkbox"
checked = { selectedActorAnimationEffect ? . loop !== false }
disabled = { selectedActorAnimationEffect === null }
onChange = { ( event ) = >
onSetActorRoutineAnimationLoop (
selectedRoutine . id ,
event . currentTarget . checked
)
}
/ >
< span className = "label" > Loop < / span >
< / label >
{ ( selectedTargetOption . defaults . actorAnimationClipNames ? ? [ ] )
. length === 0 ? (
< div className = "schedule-pane__summary" >
Animation clips are available only when this actor has one
uniquely bound NPC model with imported clips .
< / div >
) : null }
< / div >
2026-04-14 03:04:05 +02:00
2026-04-14 13:49:09 +02:00
< div className = "form-section" >
< div className = "label" > Path < / div >
< label className = "form-field" >
< span className = "label" > Path < / span >
< select
className = "select-input"
value = { selectedActorPathEffect ? . pathId ? ? "" }
onChange = { ( event ) = >
onSetActorRoutinePath (
selectedRoutine . id ,
event . currentTarget . value === ""
? null
: event . currentTarget . value
)
}
>
< option value = "" > None < / option >
{ ( selectedTargetOption . defaults . actorPathOptions ? ? [ ] ) . map (
( pathOption ) = > (
< option key = { pathOption . pathId } value = { pathOption . pathId } >
{ pathOption . label }
< / option >
)
) }
< / select >
< / label >
< label className = "form-field" >
< span className = "label" > Speed < / span >
< input
key = { ` ${ selectedRoutine . id } -actor-path-speed ` }
className = "text-input"
type = "number"
min = "0.01"
step = "0.1"
defaultValue = {
selectedActorPathEffect ? . speed ? ?
selectedTargetOption . defaults . actorPathSpeed ? ?
1
}
disabled = { selectedActorPathEffect === null }
onBlur = { ( event ) = >
onSetActorRoutinePathSpeed (
selectedRoutine . id ,
Number ( event . currentTarget . value )
)
}
onKeyDown = { ( event ) = >
handleCommitOnEnter ( event , ( ) = >
onSetActorRoutinePathSpeed (
selectedRoutine . id ,
Number ( event . currentTarget . value )
)
)
}
/ >
< / label >
< label className = "form-field form-field--inline" >
< input
type = "checkbox"
checked = { selectedActorPathEffect ? . loop ? ? false }
disabled = { selectedActorPathEffect === null }
onChange = { ( event ) = >
onSetActorRoutinePathLoop (
selectedRoutine . id ,
event . currentTarget . checked
)
}
/ >
< span className = "label" > Loop < / span >
< / label >
{ ( selectedTargetOption . defaults . actorPathOptions ? ? [ ] ) . length ===
0 ? (
< div className = "schedule-pane__summary" >
Paths are available only when this actor has one uniquely
bound NPC usage in a scene with enabled authored paths .
< / div >
) : null }
< / div >
< / >
2026-04-15 05:26:02 +02:00
) : selectedRoutine . target . kind === "global" ? null : selectedRoutine . sequenceId === null ? (
2026-04-14 13:49:09 +02:00
< >
{ selectedEffectOptions . find (
( effectOption ) = > effectOption . id === selectedEffectOptionId
) ? . valueKind === "number" ? (
< div className = "form-section" >
< div className = "label" > Value < / div >
< label className = "form-field" >
< span className = "label" >
{ selectedEffectOptions . find (
( effectOption ) = >
effectOption . id === selectedEffectOptionId
) ? . valueLabel ? ? "Value" }
< / span >
< input
key = { ` ${ selectedRoutine . id } -numeric ` }
className = "text-input"
type = "number"
min = {
selectedEffectOptions . find (
( effectOption ) = >
effectOption . id === selectedEffectOptionId
) ? . min ? ? 0
}
step = {
selectedEffectOptions . find (
( effectOption ) = >
effectOption . id === selectedEffectOptionId
) ? . step ? ? 0.1
}
defaultValue = { getRoutineNumericValue ( selectedRoutine ) ? ? 0 }
onBlur = { ( event ) = >
onSetRoutineNumericValue (
selectedRoutine . id ,
Number ( event . currentTarget . value )
)
}
onKeyDown = { ( event ) = >
handleCommitOnEnter ( event , ( ) = >
onSetRoutineNumericValue (
selectedRoutine . id ,
Number ( event . currentTarget . value )
)
)
}
/ >
< / label >
< / div >
) : null }
{ selectedEffectOptions . find (
( effectOption ) = > effectOption . id === selectedEffectOptionId
) ? . valueKind === "color" ? (
< div className = "form-section" >
< div className = "label" > Value < / div >
< label className = "form-field" >
< span className = "label" >
{ selectedEffectOptions . find (
( effectOption ) = >
effectOption . id === selectedEffectOptionId
) ? . valueLabel ? ? "Color" }
< / span >
< input
className = "color-input"
type = "color"
value = { getRoutineColorValue ( selectedRoutine ) ? ? "#ffffff" }
onChange = { ( event ) = >
onSetRoutineColorValue (
selectedRoutine . id ,
event . currentTarget . value
)
}
/ >
< / label >
< / div >
) : null }
{ selectedEffectOptions . find (
( effectOption ) = > effectOption . id === selectedEffectOptionId
) ? . valueKind === "animation" ? (
< div className = "form-section" >
< div className = "label" > Animation < / div >
< label className = "form-field" >
< span className = "label" > Clip < / span >
< select
className = "select-input"
value = {
selectedRoutine . effects [ 0 ] ? . type === "playModelAnimation"
? selectedRoutine . effects [ 0 ] . clipName
: selectedTargetOption . defaults . animationClipNames ? . [ 0 ] ? ?
""
}
onChange = { ( event ) = >
onSetRoutineAnimationClip (
selectedRoutine . id ,
event . currentTarget . value
)
}
>
{ ( selectedTargetOption . defaults . animationClipNames ? ? [ ] ) . map (
( clipName ) = > (
< option key = { clipName } value = { clipName } >
{ clipName }
< / option >
)
) }
< / select >
< / label >
< label className = "form-field form-field--inline" >
< input
type = "checkbox"
checked = {
selectedRoutine . effects [ 0 ] ? . type ===
"playModelAnimation"
? selectedRoutine . effects [ 0 ] . loop !== false
: true
}
onChange = { ( event ) = >
onSetRoutineAnimationLoop (
selectedRoutine . id ,
event . currentTarget . checked
)
}
/ >
< span className = "label" > Loop < / span >
< / label >
< / div >
) : null }
< / >
2026-04-14 23:42:59 +02:00
) : null }
2026-04-14 03:04:05 +02:00
< div className = "form-section" >
< div className = "label" > Details < / div >
< div className = "schedule-pane__summary" >
{ selectedTargetOption . groupLabel } · { selectedTargetOption . label }
< / div >
< div className = "schedule-pane__summary" >
2026-04-14 23:42:16 +02:00
{ getRoutineSummary ( selectedRoutine , sequences ) }
2026-04-14 03:04:05 +02:00
< / div >
< / div >
< div className = "form-section" >
< button
className = "toolbar__button toolbar__button--compact"
type = "button"
onClick = { ( ) = > onDeleteRoutine ( selectedRoutine . id ) }
>
2026-04-15 00:23:56 +02:00
Delete Clip
2026-04-14 03:04:05 +02:00
< / button >
< / div >
< / >
) }
< / aside >
2026-04-15 00:24:20 +02:00
< / >
2026-04-15 00:20:41 +02:00
) }
2026-04-14 03:04:05 +02:00
< / div >
< / section >
) ;
}
2026-04-14 23:14:31 +02:00
export const ProjectSchedulePane = ProjectSequencerPane ;