Welcome to mirror list, hosted at ThFree Co, Russian Federation.

functions.py « utils « operators « power_sequencer - git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: c4552c2dd8b7e1822ebd4e4c3ac93e4f13683ed5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
from math import floor, sqrt
from operator import attrgetter

import bpy

from .global_settings import SequenceTypes

max_channel = 32
min_channel = 1


def calculate_distance(x1, y1, x2, y2):
    return sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)


def convert_duration_to_frames(context, duration):
    return round(duration * context.scene.render.fps / context.scene.render.fps_base)


def find_linked(context, sequences, selected_sequences):
    """
    Takes a list of sequences and returns a list of all the sequences
    and effects that are linked in time
    Args:
        - sequences: a list of sequences
    Returns a list of all the linked sequences, but not the sequences passed to the function
    """
    start, end = get_frame_range(sequences, selected_sequences)
    sequences_in_range = [s for s in sequences if is_in_range(context, s, start, end)]
    effects = (s for s in sequences_in_range if s.type in SequenceTypes.EFFECT)
    selected_effects = (s for s in sequences if s.type in SequenceTypes.EFFECT)

    linked_sequences = []

    # Filter down to effects that have at least one of seq as input and
    # Append input sequences that aren't in the source list to linked_sequences
    for e in effects:
        if not hasattr(e, "input_2"):
            continue
        for s in sequences:
            if e.input_2 == s:
                linked_sequences.append(e)
                if e.input_1 not in sequences:
                    linked_sequences.append(e.input_1)
            elif e.input_1 == s:
                linked_sequences.append(e)
                if e.input_2 not in sequences:
                    linked_sequences.append(e.input_2)

    # Find inputs of selected effects that are not selected
    for e in selected_effects:
        try:
            if e.input_1 not in sequences:
                linked_sequences.append(e.input_1)
            if e.input_count == 2:
                if e.input_2 not in sequences:
                    linked_sequences.append(e.input_2)
        except AttributeError:
            continue

    return linked_sequences


def find_neighboring_markers(context, frame=None):
    """Returns a tuple containing the closest marker to the left and to the right of the frame"""
    markers = context.scene.timeline_markers

    if not (frame and markers):
        return None, None

    markers = sorted(markers, key=attrgetter("frame"))

    previous_marker, next_marker = None, None
    for m in markers:
        previous_marker = m if m.frame < frame else previous_marker
        if m.frame > frame:
            next_marker = m
            break

    return previous_marker, next_marker


def find_sequences_after(context, sequence):
    """
    Finds the strips following the sequences passed to the function
    Args:
        - Sequences, the sequences to check
    Returns all the strips after the sequence in the current context
    """
    return [s for s in context.sequences if s.frame_final_start > sequence.frame_final_start]


def find_snap_candidate(context, frame=0):
    """
    Returns the cut frame closest to the `frame` argument
    """
    snap_candidate = 1000000

    for s in context.sequences:
        start_to_frame = frame - s.frame_final_start
        end_to_frame = frame - s.frame_final_end

        distance_to_start = abs(start_to_frame)
        distance_to_end = abs(end_to_frame)

        candidate = (
            frame - start_to_frame
            if min(distance_to_start, distance_to_end) == distance_to_start
            else frame - end_to_frame
        )

        if abs(frame - candidate) < abs(frame - snap_candidate):
            snap_candidate = candidate

    return snap_candidate


def find_strips_mouse(context, frame, channel, select_linked=False):
    """
    Finds a list of sequences to select based on the frame and channel the mouse cursor is at
    Args:
        - frame: the frame the mouse or cursor is on
        - channel: the channel the mouse is hovering
        - select_linked: find and append the sequences linked in time if True
    Returns the sequence(s) under the mouse cursor as a list
    Returns an empty list if nothing found
    """
    sequences = [
        s
        for s in context.sequences
        if not s.lock and s.channel == channel and s.frame_final_start <= frame <= s.frame_final_end
    ]
    if select_linked:
        linked_strips = [
            s
            for s in context.sequences
            if s.frame_final_start == sequences[0].frame_final_start
            and s.frame_final_end == sequences[0].frame_final_end
        ]
        sequences.extend(linked_strips)
    return sequences


def get_frame_range(sequences, get_from_start=False):
    """
    Returns a tuple with the minimum and maximum frames of the
    list of passed sequences.
    Args:
        - sequences, the sequences to use
        - get_from_start, the returned start frame is set to 1 if
        this boolean is True
    """
    start, end = -1, -1
    start = (
        1
        if get_from_start
        else min(sequences, key=attrgetter("frame_final_start")).frame_final_start
    )
    end = max(sequences, key=attrgetter("frame_final_end")).frame_final_end
    return start, end


def get_channel_range(sequences):
    """
    Returns a tuple with the minimum and maximum channels of the
    list of passed sequences.
    """
    start = min(sequences, key=attrgetter("channel")).channel
    end = max(sequences, key=attrgetter("channel")).channel
    return start, end


def get_mouse_frame_and_channel(context, event):
    """
    Convert mouse coordinates from the event, from
    pixels to frame, channel.
    Returns a tuple of frame, channel as integers
    """
    view2d = context.region.view2d
    frame, channel = view2d.region_to_view(event.mouse_region_x, event.mouse_region_y)
    return round(frame), floor(channel)


def is_in_range(context, sequence, start, end):
    """
    Checks if a single sequence's start or end is in the range
    Args:
        - sequence: the sequence to check for
        - start, end: the start and end frames
    Returns True if the sequence is within the range, False otherwise
    """
    s_start = sequence.frame_final_start
    s_end = sequence.frame_final_end
    return start <= s_start <= end or start <= s_end <= end


def set_preview_range(context, start, end):
    """Sets the preview range and timeline render range"""
    if not (start and end) and start != 0:
        raise AttributeError("Missing start or end parameter")

    scene = context.scene

    scene.frame_start = start
    scene.frame_end = end
    scene.frame_preview_start = start
    scene.frame_preview_end = end


def slice_selection(context, sequences, range_block=0):
    """
    Takes a list of sequences and breaks it down
    into multiple lists of connected sequences
    Returns a list of lists of sequences,
    each list corresponding to a block of sequences
    that are connected in time and sorted by frame_final_start
    """
    if not sequences:
        return []

    # Indicates the index number of the lists from the "broken_selection" list
    index = -1
    block_end = 0
    broken_selection = []
    sorted_sequences = sorted(sequences, key=attrgetter("frame_final_start"))

    for s in sorted_sequences:
        if not broken_selection or (block_end + 1 + range_block < s.frame_final_start):
            broken_selection.append([s])
            block_end = s.frame_final_end
            index += 1
            continue
        block_end = max(block_end, s.frame_final_end)
        broken_selection[index].append(s)

    return broken_selection


def trim_strips(context, frame_start, frame_end, to_trim, to_delete=[]):
    """
    Removes the footage and audio between frame_start and frame_end.
    You must pass the list of strips to trim to the function for it to work.
    """
    trim_start = min(frame_start, frame_end)
    trim_end = max(frame_start, frame_end)

    to_trim = [s for s in to_trim if s.type in SequenceTypes.CUTABLE]
    initial_selection = context.selected_sequences

    for s in to_trim:
        strips_in_target_channel = []
        # Cut strip longer than the trim range in three
        is_strip_longer_than_trim_range = (
            s.frame_final_start < trim_start and s.frame_final_end > trim_end
        )
        if is_strip_longer_than_trim_range:
            bpy.ops.sequencer.select_all(action="DESELECT")
            s.select = True
            bpy.ops.sequencer.split(frame=trim_start, type="SOFT", side="RIGHT")
            bpy.ops.sequencer.split(frame=trim_end, type="SOFT", side="LEFT")
            to_delete.append(context.selected_sequences[0])

            for c in context.sequences:
                if c.channel == s.channel:
                    strips_in_target_channel.append(c)

            if s in initial_selection:
                initial_selection.append(strips_in_target_channel[0])
            continue

        # Resize strips that overlap the trim range
        elif s.frame_final_start < trim_end and s.frame_final_end > trim_end:
            s.frame_final_start = trim_end
        elif s.frame_final_end > trim_start and s.frame_final_start < trim_start:
            s.frame_final_end = trim_start

    delete_strips(to_delete)
    for s in initial_selection:
        s.select = True
    return {"FINISHED"}


def find_closest_surrounding_cuts(context, frame):
    """
    Returns a tuple of (strip_before, strip_after), the two closest sequences around a gap.
    If the frame is in the middle of a strip, both strips may be the same.
    """
    strip_before = max(
        context.sequences,
        key=lambda s: s.frame_final_end
        if s.frame_final_end <= frame
        else s.frame_final_start
        if s.frame_final_start <= frame
        else 0,
    )
    strip_after = min(
        context.sequences,
        key=lambda s: s.frame_final_start
        if s.frame_final_start >= frame
        else s.frame_final_end
        if s.frame_final_end >= frame
        else 1000000,
    )
    return strip_before, strip_after


def find_closest_surrounding_cuts_frames(context, frame):
    before, after = find_closest_surrounding_cuts(context, frame)
    if before == after:
        frame_left, frame_right = before.frame_final_start, before.frame_final_end
    else:
        frame_left, frame_right = before.frame_final_end, after.frame_final_start
    return frame_left, frame_right


def get_sequences_under_cursor(context):
    frame = context.scene.frame_current
    under_cursor = [
        s
        for s in context.sequences
        if s.frame_final_start <= frame and s.frame_final_end >= frame and not s.lock
    ]
    return under_cursor


def ripple_move(context, sequences, duration_frames, delete=False):
    """
    Moves sequences in the list and ripples the change to all sequences after them, in the corresponding channels
    The `duration_frames` can be positive or negative.
    If `delete` is True, deletes every sequence in `sequences`.
    """
    channels = {s.channel for s in sequences}
    first_strip = min(sequences, key=lambda s: s.frame_final_start)
    to_ripple = [
        s
        for s in context.sequences
        if s.channel in channels and s.frame_final_start >= first_strip.frame_final_start
    ]

    if delete:
        delete_strips(sequences)
    else:
        to_ripple = set(to_ripple + sequences)

    move_selection(context, to_ripple, duration_frames, 0)


def find_strips_in_range(frame_start, frame_end, sequences, find_overlapping=True):
    """
    Returns a tuple of two lists: (strips_inside_range, strips_overlapping_range)
    strips_inside_range are strips entirely contained in the frame range.
    strips_overlapping_range are strips that only overlap the frame range.
    Args:
        - frame_start, the start of the frame range
        - frame_end, the end of the frame range
        - sequences (optional): only work with these sequences.
        If it doesn't receive any, the function works with all the sequences in the current context
        - find_overlapping (optional): find and return a list of strips that overlap the
        frame range
    """
    strips_inside_range = []
    strips_overlapping_range = []
    if not sequences:
        sequences = bpy.context.sequences
    for s in sequences:
        if (
            frame_start <= s.frame_final_start <= frame_end
            and frame_start <= s.frame_final_end <= frame_end
        ):
            strips_inside_range.append(s)
        elif find_overlapping:
            if (
                frame_start < s.frame_final_end <= frame_end
                or frame_start <= s.frame_final_start < frame_end
            ):
                strips_overlapping_range.append(s)

        if find_overlapping:
            if s.frame_final_start < frame_start and s.frame_final_end > frame_end:
                strips_overlapping_range.append(s)
    return strips_inside_range, strips_overlapping_range


def delete_strips(to_delete):
    """
    Deletes the list of sequences `to_delete`
    """
    # Effect strips get deleted with their source so we skip them to avoid errors.
    to_delete = [s for s in to_delete if s.type in SequenceTypes.CUTABLE]
    sequences = bpy.context.scene.sequence_editor.sequences
    for s in to_delete:
        sequences.remove(s)


def move_selection(context, sequences, frame_offset, channel_offset=0):
    """
    Offsets the selected `sequences` horizontally and vertically and preserves
    the current selected sequences.
    """
    if not sequences:
        return
    initial_selection = context.selected_sequences
    bpy.ops.sequencer.select_all(action="DESELECT")
    for s in sequences:
        s.select = True
    bpy.ops.transform.seq_slide(value=(frame_offset, channel_offset))
    bpy.ops.sequencer.select_all(action="DESELECT")
    for s in initial_selection:
        s.select = True