Skip to content

load_sequence

FusionLoadSequence

Bases: LoaderPlugin

Load image sequence into Fusion

Source code in client/ayon_fusion/plugins/load/load_sequence.py
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
class FusionLoadSequence(load.LoaderPlugin):
    """Load image sequence into Fusion"""

    product_types = {
        "imagesequence",
        "review",
        "render",
        "plate",
        "image",
        "online",
    }
    representations = {"*"}
    extensions = set(
        ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)
    )

    label = "Load sequence"
    order = -10
    icon = "code-fork"
    color = "orange"

    def load(self, context, name, namespace, data):
        # Fallback to folder name when namespace is None
        if namespace is None:
            namespace = context["folder"]["name"]

        # Use the first file for now
        path = self.filepath_from_context(context)

        # Create the Loader with the filename path set
        comp = get_current_comp()
        with comp_lock_and_undo_chunk(comp, "Create Loader"):
            args = (-32768, -32768)
            tool = comp.AddTool("Loader", *args)
            tool["Clip"] = comp.ReverseMapPath(path)
            tool.SetAttrs({"TOOLB_NameSet": True, "TOOLS_Name": name})

            # Set global in point to start frame (if in version.data)
            start = self._get_start(context["version"], tool)
            loader_shift(tool, start, relative=False)

            imprint_container(
                tool,
                name=name,
                namespace=namespace,
                context=context,
                loader=self.__class__.__name__,
            )

    def switch(self, container, context):
        self.update(container, context)

    def update(self, container, context):
        """Update the Loader's path

        Fusion automatically tries to reset some variables when changing
        the loader's path to a new file. These automatic changes are to its
        inputs:
            - ClipTimeStart: Fusion reset to 0 if duration changes
              - We keep the trim in as close as possible to the previous value.
                When there are less frames then the amount of trim we reduce
                it accordingly.

            - ClipTimeEnd: Fusion reset to 0 if duration changes
              - We keep the trim out as close as possible to the previous value
                within new amount of frames after trim in (ClipTimeStart) has
                been set.

            - GlobalIn: Fusion reset to comp's global in if duration changes
              - We change it to the "frameStart"

            - GlobalEnd: Fusion resets to globalIn + length if duration changes
              - We do the same like Fusion - allow fusion to take control.

            - HoldFirstFrame: Fusion resets this to 0
              - We preserve the value.

            - HoldLastFrame: Fusion resets this to 0
              - We preserve the value.

            - Reverse: Fusion resets to disabled if "Loop" is not enabled.
              - We preserve the value.

            - Depth: Fusion resets to "Format"
              - We preserve the value.

            - KeyCode: Fusion resets to ""
              - We preserve the value.

            - TimeCodeOffset: Fusion resets to 0
              - We preserve the value.

        """

        tool = container["_tool"]
        assert tool.ID == "Loader", "Must be Loader"
        comp = tool.Comp()

        repre_entity = context["representation"]
        path = self.filepath_from_context(context)

        # Get start frame from version data
        start = self._get_start(context["version"], tool)

        with comp_lock_and_undo_chunk(comp, "Update Loader"):
            # Update the loader's path whilst preserving some values
            with preserve_trim(tool, log=self.log):
                with preserve_inputs(
                    tool,
                    inputs=(
                        "HoldFirstFrame",
                        "HoldLastFrame",
                        "Reverse",
                        "Depth",
                        "KeyCode",
                        "TimeCodeOffset",
                    ),
                ):
                    tool["Clip"] = comp.ReverseMapPath(path)
                    tool.SetAttrs(
                        {
                            "TOOLB_NameSet": True,
                            "TOOLS_Name": repre_entity["context"]["product"][
                                "name"
                            ],
                        }
                    )

            # Set the global in to the start frame of the sequence
            global_in_changed = loader_shift(tool, start, relative=False)
            if global_in_changed:
                # Log this change to the user
                self.log.debug(
                    "Changed '%s' global in: %d" % (tool.Name, start)
                )

            # Update the imprinted representation
            tool.SetData("avalon.representation", repre_entity["id"])

    def remove(self, container):
        tool = container["_tool"]
        assert tool.ID == "Loader", "Must be Loader"
        comp = tool.Comp()

        with comp_lock_and_undo_chunk(comp, "Remove Loader"):
            tool.Delete()

    def _get_start(self, version_entity, tool):
        """Return real start frame of published files (incl. handles)"""
        attributes = version_entity["attrib"]

        # Get start frame directly with handle if it's in data
        start = attributes.get("frameStartHandle")
        if start is not None:
            return start

        # Get frame start without handles
        start = attributes.get("frameStart")
        if start is None:
            self.log.warning(
                "Missing start frame for version "
                "assuming starts at frame 0 for: "
                "{}".format(tool.Name)
            )
            return 0

        # Use `handleStart` if the data is available
        handle_start = attributes.get("handleStart")
        if handle_start:
            start -= handle_start

        return start

update(container, context)

Update the Loader's path

Fusion automatically tries to reset some variables when changing the loader's path to a new file. These automatic changes are to its inputs: - ClipTimeStart: Fusion reset to 0 if duration changes - We keep the trim in as close as possible to the previous value. When there are less frames then the amount of trim we reduce it accordingly.

- ClipTimeEnd: Fusion reset to 0 if duration changes
  - We keep the trim out as close as possible to the previous value
    within new amount of frames after trim in (ClipTimeStart) has
    been set.

- GlobalIn: Fusion reset to comp's global in if duration changes
  - We change it to the "frameStart"

- GlobalEnd: Fusion resets to globalIn + length if duration changes
  - We do the same like Fusion - allow fusion to take control.

- HoldFirstFrame: Fusion resets this to 0
  - We preserve the value.

- HoldLastFrame: Fusion resets this to 0
  - We preserve the value.

- Reverse: Fusion resets to disabled if "Loop" is not enabled.
  - We preserve the value.

- Depth: Fusion resets to "Format"
  - We preserve the value.

- KeyCode: Fusion resets to ""
  - We preserve the value.

- TimeCodeOffset: Fusion resets to 0
  - We preserve the value.
Source code in client/ayon_fusion/plugins/load/load_sequence.py
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
def update(self, container, context):
    """Update the Loader's path

    Fusion automatically tries to reset some variables when changing
    the loader's path to a new file. These automatic changes are to its
    inputs:
        - ClipTimeStart: Fusion reset to 0 if duration changes
          - We keep the trim in as close as possible to the previous value.
            When there are less frames then the amount of trim we reduce
            it accordingly.

        - ClipTimeEnd: Fusion reset to 0 if duration changes
          - We keep the trim out as close as possible to the previous value
            within new amount of frames after trim in (ClipTimeStart) has
            been set.

        - GlobalIn: Fusion reset to comp's global in if duration changes
          - We change it to the "frameStart"

        - GlobalEnd: Fusion resets to globalIn + length if duration changes
          - We do the same like Fusion - allow fusion to take control.

        - HoldFirstFrame: Fusion resets this to 0
          - We preserve the value.

        - HoldLastFrame: Fusion resets this to 0
          - We preserve the value.

        - Reverse: Fusion resets to disabled if "Loop" is not enabled.
          - We preserve the value.

        - Depth: Fusion resets to "Format"
          - We preserve the value.

        - KeyCode: Fusion resets to ""
          - We preserve the value.

        - TimeCodeOffset: Fusion resets to 0
          - We preserve the value.

    """

    tool = container["_tool"]
    assert tool.ID == "Loader", "Must be Loader"
    comp = tool.Comp()

    repre_entity = context["representation"]
    path = self.filepath_from_context(context)

    # Get start frame from version data
    start = self._get_start(context["version"], tool)

    with comp_lock_and_undo_chunk(comp, "Update Loader"):
        # Update the loader's path whilst preserving some values
        with preserve_trim(tool, log=self.log):
            with preserve_inputs(
                tool,
                inputs=(
                    "HoldFirstFrame",
                    "HoldLastFrame",
                    "Reverse",
                    "Depth",
                    "KeyCode",
                    "TimeCodeOffset",
                ),
            ):
                tool["Clip"] = comp.ReverseMapPath(path)
                tool.SetAttrs(
                    {
                        "TOOLB_NameSet": True,
                        "TOOLS_Name": repre_entity["context"]["product"][
                            "name"
                        ],
                    }
                )

        # Set the global in to the start frame of the sequence
        global_in_changed = loader_shift(tool, start, relative=False)
        if global_in_changed:
            # Log this change to the user
            self.log.debug(
                "Changed '%s' global in: %d" % (tool.Name, start)
            )

        # Update the imprinted representation
        tool.SetData("avalon.representation", repre_entity["id"])

loader_shift(loader, frame, relative=True)

Shift global in time by i preserving duration

This moves the loader by i frames preserving global duration. When relative is False it will shift the global in to the start frame.

Parameters:

Name Type Description Default
loader tool

The fusion loader tool.

required
frame int

The amount of frames to move.

required
relative bool

When True the shift is relative, else the shift will change the global in to frame.

True

Returns:

Name Type Description
int

The resulting relative frame change (how much it moved)

Source code in client/ayon_fusion/plugins/load/load_sequence.py
 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
def loader_shift(loader, frame, relative=True):
    """Shift global in time by i preserving duration

    This moves the loader by i frames preserving global duration. When relative
    is False it will shift the global in to the start frame.

    Args:
        loader (tool): The fusion loader tool.
        frame (int): The amount of frames to move.
        relative (bool): When True the shift is relative, else the shift will
            change the global in to frame.

    Returns:
        int: The resulting relative frame change (how much it moved)

    """
    comp = loader.Comp()
    time = comp.TIME_UNDEFINED

    old_in = loader["GlobalIn"][time]
    old_out = loader["GlobalOut"][time]

    if relative:
        shift = frame
    else:
        shift = frame - old_in

    if not shift:
        return 0

    # Shifting global in will try to automatically compensate for the change
    # in the "ClipTimeStart" and "HoldFirstFrame" inputs, so we preserve those
    # input values to "just shift" the clip
    with preserve_inputs(
        loader,
        inputs=[
            "ClipTimeStart",
            "ClipTimeEnd",
            "HoldFirstFrame",
            "HoldLastFrame",
        ],
    ):
        # GlobalIn cannot be set past GlobalOut or vice versa
        # so we must apply them in the order of the shift.
        if shift > 0:
            loader["GlobalOut"][time] = old_out + shift
            loader["GlobalIn"][time] = old_in + shift
        else:
            loader["GlobalIn"][time] = old_in + shift
            loader["GlobalOut"][time] = old_out + shift

    return int(shift)

preserve_inputs(tool, inputs)

Preserve the tool's inputs after context

Source code in client/ayon_fusion/plugins/load/load_sequence.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@contextlib.contextmanager
def preserve_inputs(tool, inputs):
    """Preserve the tool's inputs after context"""

    comp = tool.Comp()

    values = {}
    for name in inputs:
        tool_input = getattr(tool, name)
        value = tool_input[comp.TIME_UNDEFINED]
        values[name] = value

    try:
        yield
    finally:
        for name, value in values.items():
            tool_input = getattr(tool, name)
            tool_input[comp.TIME_UNDEFINED] = value

preserve_trim(loader, log=None)

Preserve the relative trim of the Loader tool.

This tries to preserve the loader's trim (trim in and trim out) after the context by reapplying the "amount" it trims on the clip's length at start and end.

Source code in client/ayon_fusion/plugins/load/load_sequence.py
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
@contextlib.contextmanager
def preserve_trim(loader, log=None):
    """Preserve the relative trim of the Loader tool.

    This tries to preserve the loader's trim (trim in and trim out) after
    the context by reapplying the "amount" it trims on the clip's length at
    start and end.

    """

    # Get original trim as amount of "trimming" from length
    time = loader.Comp().TIME_UNDEFINED
    length = loader.GetAttrs()["TOOLIT_Clip_Length"][1] - 1
    trim_from_start = loader["ClipTimeStart"][time]
    trim_from_end = length - loader["ClipTimeEnd"][time]

    try:
        yield
    finally:
        length = loader.GetAttrs()["TOOLIT_Clip_Length"][1] - 1
        if trim_from_start > length:
            trim_from_start = length
            if log:
                log.warning(
                    "Reducing trim in to %d "
                    "(because of less frames)" % trim_from_start
                )

        remainder = length - trim_from_start
        if trim_from_end > remainder:
            trim_from_end = remainder
            if log:
                log.warning(
                    "Reducing trim in to %d "
                    "(because of less frames)" % trim_from_end
                )

        loader["ClipTimeStart"][time] = trim_from_start
        loader["ClipTimeEnd"][time] = length - trim_from_end