Skip to content

load_source

SourceLoader

Bases: SilhouetteLoader

Load media source.

Source code in client/ayon_silhouette/plugins/load/load_source.py
 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
class SourceLoader(plugin.SilhouetteLoader):
    """Load media source."""

    color = "orange"
    product_types = {"*"}
    icon = "code-fork"
    label = "Load Source"
    order = -10
    representations = {"*"}
    extensions = {
        ext.lstrip(".") for ext in VIDEO_EXTENSIONS.union(IMAGE_EXTENSIONS)
    }

    set_session_frame_range_on_load = False
    set_session_frame_range_on_update = False

    @classmethod
    def get_options(cls, contexts):
        return [
            BoolDef(
                "load_all_parts",
                label="Load All Parts",
                default=True,
                tooltip=(
                    "Load all subimages/parts of the media instead of only "
                    "the first subimage. This can be useful for e.g. "
                    "Stereo EXR files."
                ),
            ),
            BoolDef(
                "set_session_frame_range_on_load",
                label="Set Session Frame Range on Load",
                default=cls.set_session_frame_range_on_load
            ),
        ]

    @lib.undo_chunk("Load Source")
    def load(self, context, name=None, namespace=None, options=None):
        project = fx.activeProject()
        if not project:
            raise RuntimeError("No active project found.")

        filepath = self.filepath_from_context(context)

        if options.get(
            "set_session_frame_range_on_load",
            self.set_session_frame_range_on_load
        ):
            self._set_session_frame_range(context)

        # A source file may contain multiple parts, such as a left view
        # and a right view in a single EXR.
        options = options or {}
        load_all_parts = options.get("load_all_parts", True)

        # If the file is not an EXR or SXR, we can only load one part so force
        # disable loading multiple parts.
        if (
            load_all_parts
            and os.path.splitext(filepath)[-1].lower()
            not in SUBIMAGE_EXTENSIONS
        ):
            load_all_parts = False

        # If loading all parts, find the info for the subimages so we can label
        # them correctly.
        info: list[dict[str, Any]] = []
        parts = 1
        if load_all_parts:
            raw_filepath = super().filepath_from_context(context)
            info = get_oiio_info_for_input(raw_filepath, subimages=True)
            parts = len(info)

        for part in range(parts):
            source = fx.Source(filepath, part=part)
            part_name = None
            if parts > 1:
                # Use subimage name as part name
                part_attribs = info[part].get("attribs", {})
                part_name = part_attribs.get(
                    "name", part_attribs.get("oiio:subimage_name", "")
                )

            # Provide a nice label indicating the product
            source.label = self._get_label(context, part_name=part_name)
            project.addItem(source)

            # property.hidden = True  # hide the attribute
            lib.imprint(source, data={
                "name": str(name),
                "namespace": str(namespace),
                "loader": str(self.__class__.__name__),
                "representation": context["representation"]["id"],
            })

    def filepath_from_context(self, context):
        # If the media is a sequence of files we need to load it with the
        # frames in the path as in file.[start-end].ext
        if context["representation"]["context"].get("frame"):
            anatomy = Anatomy(
                project_name=context["project"]["name"],
                project_entity=context["project"]
            )
            representation = context["representation"]
            files = [data["path"] for data in representation["files"]]
            files = [anatomy.fill_root(file) for file in files]

            collections, _remainder = clique.assemble(
                files, patterns=[clique.PATTERNS["frames"]]
            )
            collection = collections[0]
            frames = list(collection.indexes)
            start = str(frames[0]).zfill(collection.padding)
            end = str(frames[-1]).zfill(collection.padding)
            return collection.format(f"{{head}}[{start}-{end}]{{tail}}")

        return super().filepath_from_context(context)

    def _get_label(
        self, context: dict, part_name: Optional[str] = None
    ) -> str:
        """Return product name as label with the part name if provided."""
        label = context["product"]["name"]
        if part_name:
            label = f"{label} [{part_name}]"

        return label

    @lib.undo_chunk("Update Source")
    def update(self, container, context):
        # Update filepath
        item = container["_item"]
        item.property("path").value = self.filepath_from_context(context)

        # Update representation id
        data = lib.read(item)
        data["representation"] = context["representation"]["id"]
        lib.imprint(item, data)

        if self.set_session_frame_range_on_update:
            self._set_session_frame_range(context)

    @lib.undo_chunk("Remove container")
    def remove(self, container):
        """Remove all sub containers"""
        item = container["_item"]
        project = container["_project"]
        project.removeItem(item)

    def switch(self, container, context):
        """Support switch to another representation."""
        self.update(container, context)

    def _set_session_frame_range(self, context: dict):

        # Get the start frame from the loaded product
        lookup_entities = [
            # TODO: Allow taking from representation if it actually contains
            #  more sensible data. Currently it seems to just contain the
            #  task frame ranges by default?
            # context["representation"],
            context["version"]
        ]
        attrs = {"frameStart", "frameEnd", "handleStart", "handleEnd"}
        values = {}
        for attr in attrs:
            for entity in lookup_entities:
                if attr in entity.get("attrib", {}):
                    values[attr] = entity["attrib"][attr]
                    break

        if "frameStart" not in values:
            self.log.warning(
                "No start frame data found, cannot set start frame."
            )
            return

        active_session = fx.activeSession()
        if not active_session:
            self.log.warning("No active session, cannot set frame range.")
            return

        # Set start frame based on start frame with handle
        frame_start = values["frameStart"]
        handle_start = values.get("handleStart", 0)
        frame_start_handle = frame_start - handle_start

        # Set duration based on end frame from start frame
        duration = active_session.duration
        if "frameEnd" in values:
            frame_end = values["frameEnd"]
            handle_end = values.get("handleEnd", 0)
            frame_end_handle = frame_end + handle_end
            duration = (frame_end_handle - frame_start_handle) + 1
        else:
            self.log.warning(
                "No end frame data found, cannot set duration."
            )

        changes = False
        if active_session.startFrame != frame_start_handle:
            print(f"Updating session start frame to: {frame_start_handle}")
            changes = True
        if active_session.duration != duration:
            print(f"Updating session duration to: {duration}")
            changes = True
        if not changes:
            # Do not enforce viewer timeline to 'revert' to the session
            # start/end if there are no session changes, so that we leave the
            # artist's work area intact as much as possible.
            return

        # Set the duration before startFrame otherwise the viewer timeline
        # will not update correctly, see: https://forum.borisfx.com/t/20386
        active_session.duration = duration
        active_session.startFrame = frame_start_handle

remove(container)

Remove all sub containers

Source code in client/ayon_silhouette/plugins/load/load_source.py
162
163
164
165
166
167
@lib.undo_chunk("Remove container")
def remove(self, container):
    """Remove all sub containers"""
    item = container["_item"]
    project = container["_project"]
    project.removeItem(item)

switch(container, context)

Support switch to another representation.

Source code in client/ayon_silhouette/plugins/load/load_source.py
169
170
171
def switch(self, container, context):
    """Support switch to another representation."""
    self.update(container, context)