@@ -106,11 +106,151 @@ async function applyUserOperation(
106106 case DefaultUserOperationsTypes . UPDATE_PROPS :
107107 processUpdateProps ( context , mutableIngestRundown , changes . operationTarget , changes )
108108 break
109+ // Handle drag and drop retime operations:
110+ case DefaultUserOperationsTypes . RETIME_PIECE :
111+ processRetimePiece ( context , mutableIngestRundown , changes . operationTarget , changes )
112+ break
109113 default :
110114 context . logWarning ( `Unknown operation: ${ changes . operation . id } ` )
111115 }
112116}
113117
118+ /**
119+ * Process piece retiming operations from the UI.
120+ *
121+ * This function handles drag-and-drop retime operations on pieces in the Sofie Rundown Editor.
122+ * It modifies the underlying ingest data (objectTime and duration) for pieces within parts.
123+ *
124+ * Expected payload formats:
125+ * - { newTime: number, newDuration?: number }
126+ * - { deltaTime: number, deltaDuration?: number }
127+ * - { startTime: number }
128+ * - { timing: { start: number, duration?: number } }
129+ *
130+ * @param context - The ingest data processing context
131+ * @param mutableIngestRundown - The mutable rundown being processed
132+ * @param operationTarget - Target containing partExternalId and pieceExternalId
133+ * @param changes - The user operation change containing timing information
134+ */
135+ function processRetimePiece (
136+ context : IProcessIngestDataContext ,
137+ mutableIngestRundown : BlueprintMutableIngestRundown ,
138+ operationTarget : UserOperationTarget ,
139+ changes : UserOperationChange < BlueprintsUserOperations | DefaultUserOperations >
140+ ) {
141+ // Extract piece timing information from the operation
142+ const operation = changes . operation as any // DefaultUserOperationRetimePiece would be the proper type
143+
144+ context . logDebug ( 'Processing piece retime operation: ' + JSON . stringify ( changes , null , 2 ) )
145+
146+ // Ensure we have the required identifiers
147+ if ( ! operationTarget . partExternalId || ! operationTarget . pieceExternalId ) {
148+ context . logError ( 'Retime piece operation missing part or piece external ID' )
149+ return
150+ }
151+
152+ // Find the part containing the piece
153+ const part = mutableIngestRundown . findPart ( operationTarget . partExternalId )
154+ if ( ! part ?. payload ) {
155+ context . logError ( `Part not found for retime operation: ${ operationTarget . partExternalId } ` )
156+ return
157+ }
158+
159+ // Parse the part payload to access pieces
160+ const partPayload : any = part . payload
161+ if ( ! partPayload . pieces || ! Array . isArray ( partPayload . pieces ) ) {
162+ context . logError ( `Part has no pieces array: ${ operationTarget . partExternalId } ` )
163+ return
164+ }
165+
166+ context . logDebug ( 'Original partPayload: ' + JSON . stringify ( partPayload , null , 2 ) )
167+
168+ // Find the specific piece to retime
169+ const pieceIndex = partPayload . pieces . findIndex ( ( piece : any ) => piece . id === operationTarget . pieceExternalId )
170+ if ( pieceIndex === - 1 ) {
171+ context . logError ( `Piece not found for retime: ${ operationTarget . pieceExternalId } ` )
172+ return
173+ }
174+
175+ const piece = partPayload . pieces [ pieceIndex ]
176+ const originalTime = piece . objectTime || 0
177+
178+ // Extract new timing from operation payload
179+ const payload = operation . payload || { }
180+ const newTime = payload . inPoint / 1000 // Convert milliseconds to seconds for comparison
181+
182+ // Example payload structure for retime operations:
183+ // "payload": {
184+ // "segmentExternalId": "d26d22e2-4f4e-4d31-a0ca-de6f37ff9b3f",
185+ // "partExternalId": "42077925-8d15-4a5d-abeb-a445ccee2984",
186+ // "inPoint": 1061.4136732329084
187+ // }
188+
189+ // Handle different retime operation types
190+ if ( payload . inPoint === undefined ) {
191+ context . logError ( 'Retime piece operation missing inPoint in payload' )
192+ return
193+ }
194+
195+ // Check if there are any unknown values in the payload that need to be handled apart from inPoint, segmentExternalId, partExternalId
196+ const knownKeys = [ 'inPoint' , 'segmentExternalId' , 'partExternalId' ]
197+ const unknownKeys = Object . keys ( payload ) . filter ( ( key ) => ! knownKeys . includes ( key ) )
198+ if ( unknownKeys . length > 0 ) {
199+ context . logWarning ( `Retime piece operation has unknown keys in payload: ${ unknownKeys . join ( ', ' ) } ` )
200+ }
201+
202+ // Check if there are actually changes to apply (now comparing in the same units)
203+ const timeDifference = Math . abs ( newTime - originalTime )
204+ if ( timeDifference < 0.005 ) {
205+ // Less than 5ms difference - consider it unchanged
206+ context . logDebug (
207+ `No significant timing changes needed for piece ${ operationTarget . pieceExternalId } (${ timeDifference } s difference)`
208+ )
209+ return
210+ }
211+
212+ // Apply the retime changes to the ingest data structure
213+ // Note: Ingest pieces use objectTime, not enable.start
214+ const updatedPiece = {
215+ ...piece ,
216+ objectTime : newTime ,
217+ }
218+
219+ // Update the piece in the part payload
220+ partPayload . pieces [ pieceIndex ] = updatedPiece
221+
222+ // Mark the part as modified using replacePayload
223+ part . replacePayload ( partPayload )
224+
225+ // Mark the segment/part as having been edited by the user
226+ // This helps track that the content has been modified from the original ingest
227+ const partAndSegment = mutableIngestRundown . findPartAndSegment ( operationTarget . partExternalId )
228+ if ( partAndSegment ?. segment && partAndSegment ?. part ) {
229+ // Mark both segment and part as user-edited
230+ partAndSegment . segment . setUserEditState ( BlueprintUserOperationTypes . USER_EDITED , true )
231+ partAndSegment . part . setUserEditState ( BlueprintUserOperationTypes . USER_EDITED , true )
232+
233+ // Create a unique user edit state key for this piece retime operation
234+ // This ensures each retime operation creates a new state change, triggering persistence
235+ const pieceRetimeKey = `${ operation . id } _${ operationTarget . pieceExternalId } _${ Date . now ( ) } `
236+
237+ // Store the retime operation as a user edit state
238+ // Each retime gets a unique key to ensure state changes trigger persistence
239+ partAndSegment . segment . setUserEditState ( pieceRetimeKey , true )
240+ partAndSegment . part . setUserEditState ( pieceRetimeKey , true )
241+
242+ // Also mark the specific operation that was performed
243+ partAndSegment . segment . setUserEditState ( operation . id , true )
244+ partAndSegment . part . setUserEditState ( operation . id , true )
245+
246+ // Lock both segment and part to prevent NRCS updates:
247+ partAndSegment . segment . setUserEditState ( BlueprintUserOperationTypes . LOCK_SEGMENT_NRCS_UPDATES , true )
248+ partAndSegment . part . setUserEditState ( BlueprintUserOperationTypes . LOCK_PART_NRCS_UPDATES , true )
249+
250+ context . logDebug ( `Marked segment and part as user-edited and created unique retime state: ${ pieceRetimeKey } ` )
251+ }
252+ }
253+
114254function processUpdateProps (
115255 context : IProcessIngestDataContext ,
116256 mutableIngestRundown : BlueprintMutableIngestRundown ,
0 commit comments