diff --git a/hooks/tk-maya_actions.py b/hooks/tk-maya_actions.py
index 5f3acb7..10a3e6b 100644
--- a/hooks/tk-maya_actions.py
+++ b/hooks/tk-maya_actions.py
@@ -230,7 +230,19 @@ def generate_actions(self, sg_publish_data, actions, ui_area):
"name": "build_new_scene",
"params": None,
"caption": "Build New Scene",
- "description": "This will create a new scene in the current project.",
+ "description": (
+ "Create a new Maya scene for this task in the "
+ "current Flow AM project.
"
+ "When this task's pipeline step depends on an upstream "
+ "step - configured through the "
+ "pipeline_step_dependencies setting (for "
+ "example Rig and Texture depend on Model) - the "
+ "upstream step's published Maya scene is automatically "
+ "referenced into the new scene.
"
+ "If that upstream step has not been published yet, you "
+ "are warned and can still choose to build an empty "
+ "scene."
+ ),
}
)
diff --git a/python/tk_multi_loader/dialog.py b/python/tk_multi_loader/dialog.py
index 8ced20f..38ec223 100644
--- a/python/tk_multi_loader/dialog.py
+++ b/python/tk_multi_loader/dialog.py
@@ -2245,6 +2245,12 @@ def on_action_click(act):
)
action = QtGui.QAction(entity_action["caption"], view)
+ description = entity_action.get("description")
+ if description:
+ # QMenu does not show action tooltips on its own, so drive it
+ # from the hovered signal like the built-in actions above.
+ action.setToolTip(description)
+ action.hovered.connect(partial(action_hovered, action))
action.triggered.connect(partial(on_action_click, act=entity_action))
view.addAction(action)
self._dynamic_widgets.append(action)
diff --git a/python/tk_multi_loader/flowam/flowam_actions.py b/python/tk_multi_loader/flowam/flowam_actions.py
index c5e082f..fef423b 100644
--- a/python/tk_multi_loader/flowam/flowam_actions.py
+++ b/python/tk_multi_loader/flowam/flowam_actions.py
@@ -32,8 +32,12 @@
download_revision,
open_draft,
)
-from .reference import copy_reference_link, reference_revision
-from .step_validation import find_unpublished_upstream_step
+from .reference import (
+ copy_reference_link,
+ reference_published_workfile,
+ reference_revision,
+)
+from .step_validation import find_unpublished_upstream_step, find_upstream_workfile
class FlowAMActions:
@@ -176,7 +180,11 @@ def _on_build_scene_dialog_accepted(
am_project_id=flow_am_id,
create_mode=dialog.build,
source_path=template_path,
- prep_scene_callback=functools.partial(self._prep_scene, sg_publish_data),
+ )
+ # Bound after construction so the callback can reference create_inputs
+ # (which needs the fully built object to resolve the upstream scene).
+ create_inputs.prep_scene_callback = functools.partial(
+ self._prepare_build_scene, create_inputs, sg_publish_data
)
if not self._confirm_upstream_step_published(create_inputs):
@@ -262,6 +270,77 @@ def _prep_scene(self, sg_publish_data: dict) -> None:
# TDs can override this method to add custom scene prep logic
pass
+ def _prepare_build_scene(
+ self, create_inputs: CreateInputs, sg_publish_data: dict
+ ) -> None:
+ """
+ Prep callback run while a new scene is being built.
+
+ This fires after the host has created/loaded the scene and before it is
+ saved into the draft, which is exactly when the upstream reference must
+ exist so it gets baked into the built scene. Standard referencing runs
+ first, then the TD-overridable prep hook.
+
+ :param create_inputs: Inputs describing the scene being built.
+ :param sg_publish_data: FPTR data dictionary for the task being built from.
+ """
+ self._reference_upstream_step(create_inputs)
+ self._prep_scene(sg_publish_data)
+
+ def _reference_upstream_step(self, create_inputs: CreateInputs) -> None:
+ """
+ Reference the previous pipeline step's published Maya scene into the
+ scene being built.
+
+ This is the referencing counterpart of
+ `_confirm_upstream_step_published`. It is a no-op unless the current host
+ is Maya (only the Maya scene publish is referenced) and the upstream step
+ actually has a published workfile. When nothing is published - for
+ instance when the artist chose to build an empty scene from that warning
+ - there is simply nothing to reference.
+
+ A referencing failure is surfaced as a warning but never aborts the
+ build: the artist still gets their new scene, just without the reference.
+
+ :param create_inputs: Inputs describing the scene being built.
+ """
+ host = getattr(sgtk.platform.current_engine(), "flow_host", None)
+ workfile_type = getattr(host, "WORKFILE_TYPE", "")
+ if workfile_type != MAYA_WORKFILE_TYPE:
+ return
+
+ workfile = find_upstream_workfile(
+ am_project_id=create_inputs.am_project_id,
+ sg_entity_type=create_inputs.sg_entity_type,
+ sg_entity_name=create_inputs.sg_entity_name,
+ sg_pipeline_step=create_inputs.sg_pipeline_step,
+ workfile_type=workfile_type,
+ step_dependencies=self._app.get_setting("pipeline_step_dependencies", {}),
+ )
+ if workfile is None:
+ return
+
+ try:
+ file_path = reference_published_workfile(workfile.revision_id)
+ except exceptions.FlowError as exc:
+ message = (
+ f"Could not reference the previous step's published scene for "
+ f'"{create_inputs.sg_entity_name}". The new scene was built '
+ f"without it. ({exc})"
+ )
+ self._app.log_error(message)
+ QtGui.QMessageBox.warning(
+ self._get_dialog_parent(),
+ "Reference failed",
+ message,
+ )
+ return
+
+ self._app.log_info(
+ f"Referenced the previous step's published scene into the new "
+ f'"{create_inputs.sg_pipeline_step}" scene: {file_path}'
+ )
+
def _discard_draft(self, sg_publish_data: dict) -> None:
"""
Discard the local draft for the given PublishedFile.
diff --git a/python/tk_multi_loader/flowam/reference.py b/python/tk_multi_loader/flowam/reference.py
index bc76d67..41a909c 100644
--- a/python/tk_multi_loader/flowam/reference.py
+++ b/python/tk_multi_loader/flowam/reference.py
@@ -89,6 +89,70 @@ def reference_revision(revision_id: str) -> str:
return depdata.file_path
+def reference_published_workfile(revision_id: str) -> str:
+ """Reference a published workfile's source into the current scene.
+
+ Intended for the "Build New Scene" flow, where a downstream step's fresh
+ scene should open with the upstream step's published output already
+ referenced in. It intentionally mirrors :func:`reference_revision` but omits
+ its ``flow_draft_id`` guard: at build time the scene is brand new and has no
+ asset/draft context yet, which is exactly the state that guard rejects.
+
+ Args:
+ revision_id: The id of the asset revision to be referenced.
+ This can also be a version id.
+
+ Returns:
+ File path of referenced file.
+
+ Raises:
+ CreateReferenceError
+ """
+ engine = sgtk.platform.current_engine()
+
+ if not hasattr(engine.flow_host, "create_reference"):
+ msg = "Referencing is not supported in current execution."
+ raise CreateReferenceError(input_id=revision_id, details=msg)
+
+ try:
+ if objects.FlowVersion.is_version_id(revision_id):
+ input_type = "version"
+ revision = objects.FlowVersion(revision_id).revision
+ else:
+ input_type = "revision"
+ revision = objects.FlowRevision.get_revision(revision_id)
+ except exceptions.FlowError as exc:
+ msg = f"Could not retrieve {input_type} object."
+ raise CreateReferenceError(input_id=revision_id, details=msg) from exc
+
+ # Fetch source component of revision
+ revision.fetch(component_purpose=globals.SOURCE_PURPOSE, fetch_dependencies=True)
+
+ # Get path to source path of revision in local storage
+ file_path = revision.get_storage_component_path(
+ component_purpose=globals.SOURCE_PURPOSE
+ )
+ if file_path is None:
+ msg = "Revision does not have a source component to be referenced."
+ raise CreateReferenceError(input_id=revision_id, details=msg)
+ file_seq_comp = revision.find_component(
+ type_id=schema.get_schema_id(globals.FILE_SEQ_TYPE)
+ )
+ if not file_seq_comp and not os.path.exists(file_path):
+ msg = f"Source file does not exist in storage: {file_path}. "
+ msg += "Fetching the revision was not successful!"
+ raise CreateReferenceError(input_id=revision_id, details=msg)
+ elif file_seq_comp:
+ # Return a file path with embedded frame padding
+ file_path = utils.cleanpath(
+ revision.get_storage_dir(), file_seq_comp.properties["fileFormat"]
+ )
+
+ # Create reference
+ depdata = engine.flow_host.create_reference(file_path, namespace=revision.name)
+ return depdata.file_path
+
+
def copy_reference_link(revision_id: str) -> str:
"""Copy the reference link (file path) to the source component
the of given revision to application clipboard.
diff --git a/python/tk_multi_loader/flowam/step_validation.py b/python/tk_multi_loader/flowam/step_validation.py
index 2b032a4..cd6c214 100644
--- a/python/tk_multi_loader/flowam/step_validation.py
+++ b/python/tk_multi_loader/flowam/step_validation.py
@@ -124,21 +124,13 @@ def has_published_workfile(
return True
try:
- node = objects.FlowProject(am_project_id)
- # Walk down to the "root asset" grouping the workfiles of this step:
- # Assets///. See get_or_create_workfile_parent()
- # in tk-core's tank/flowam/create.py for the hierarchy this mirrors.
- for name in (
- root_folder_name,
- sg_entity_name,
- pipeline_step,
- sg_entity_name,
- ):
- node = node.find_child(name)
- if node is None:
- return False
-
- return bool(node.find_children(type_id=workfile_type_id))
+ workfile = _find_workfile_asset(
+ am_project_id=am_project_id,
+ root_folder_name=root_folder_name,
+ sg_entity_name=sg_entity_name,
+ pipeline_step=pipeline_step,
+ workfile_type_id=workfile_type_id,
+ )
except exceptions.FlowError as exc:
logger.warning(
f'Could not verify whether pipeline step "{pipeline_step}" has a '
@@ -147,6 +139,99 @@ def has_published_workfile(
)
return True
+ return workfile is not None
+
+
+def find_upstream_workfile(
+ am_project_id: str,
+ sg_entity_type: str,
+ sg_entity_name: str,
+ sg_pipeline_step: str,
+ workfile_type: str,
+ step_dependencies: Dict[str, str],
+) -> Optional[objects.FlowAsset]:
+ """Return the upstream step's published workfile asset for the entity.
+
+ This is the referencing counterpart of
+ :func:`find_unpublished_upstream_step`. Where that helper answers "should we
+ warn?", this one answers "what should we reference?". Every unresolved case -
+ no configured upstream, an unsupported entity type, an unresolved workfile
+ schema id, nothing published, or a Flow AM query error - yields ``None`` so
+ the caller simply skips referencing rather than surfacing an error while
+ building a scene.
+
+ :param am_project_id: Id of the Flow AM project holding the asset.
+ :param sg_entity_type: FPTR entity type of the asset, e.g. ``"Asset"``.
+ :param sg_entity_name: FPTR entity name of the asset.
+ :param sg_pipeline_step: Step the new scene is being built for.
+ :param workfile_type: Schema type name of the workfile to reference.
+ :param step_dependencies: Mapping of step name to upstream step name.
+ :returns: The upstream workfile ``FlowAsset``, or ``None``.
+ """
+ upstream_step = get_upstream_step(sg_pipeline_step, step_dependencies)
+ if not upstream_step:
+ return None
+
+ root_folder_name = _get_root_folder_name(sg_entity_type)
+ if not root_folder_name:
+ return None
+
+ workfile_type_id = schema.get_schema_id(workfile_type)
+ if not workfile_type_id:
+ logger.warning(
+ f'Could not resolve the schema id for workfile type "{workfile_type}". '
+ f'Skipping referencing of pipeline step "{upstream_step}".'
+ )
+ return None
+
+ try:
+ return _find_workfile_asset(
+ am_project_id=am_project_id,
+ root_folder_name=root_folder_name,
+ sg_entity_name=sg_entity_name,
+ pipeline_step=upstream_step,
+ workfile_type_id=workfile_type_id,
+ )
+ except exceptions.FlowError as exc:
+ logger.warning(
+ f"Could not resolve a published workfile for pipeline step "
+ f'"{upstream_step}" of "{sg_entity_name}". Skipping referencing. ({exc})'
+ )
+ return None
+
+
+def _find_workfile_asset(
+ am_project_id: str,
+ root_folder_name: str,
+ sg_entity_name: str,
+ pipeline_step: str,
+ workfile_type_id: str,
+) -> Optional[objects.FlowAsset]:
+ """Return the workfile asset published under *pipeline_step* for the entity.
+
+ Walks down to the "root asset" that groups the workfiles of a step -
+ ``///`` - and returns its first
+ workfile-typed child. See ``get_or_create_workfile_parent()`` in tk-core's
+ ``tank/flowam/create.py`` for the hierarchy this mirrors.
+
+ :param am_project_id: Id of the Flow AM project holding the asset.
+ :param root_folder_name: Name of the project's top-level folder.
+ :param sg_entity_name: FPTR entity name of the asset.
+ :param pipeline_step: Step to look under.
+ :param workfile_type_id: Resolved schema id of the workfile type.
+ :returns: The workfile ``FlowAsset``, or ``None`` when the hierarchy is
+ incomplete or the step has no published workfile.
+ :raises exceptions.FlowError: If a Flow AM query fails.
+ """
+ node = objects.FlowProject(am_project_id)
+ for name in (root_folder_name, sg_entity_name, pipeline_step, sg_entity_name):
+ node = node.find_child(name)
+ if node is None:
+ return None
+
+ workfiles = node.find_children(type_id=workfile_type_id)
+ return workfiles[0] if workfiles else None
+
def _get_root_folder_name(sg_entity_type: str) -> Optional[str]:
"""Return the name of the top-level folder holding assets of *sg_entity_type*.
diff --git a/tests/test_step_validation.py b/tests/test_step_validation.py
index ad154b2..5fa659e 100644
--- a/tests/test_step_validation.py
+++ b/tests/test_step_validation.py
@@ -47,9 +47,10 @@
class StubWorkfile:
"""Stand-in for a workfile asset carrying a single schema type."""
- def __init__(self, name, type_id):
+ def __init__(self, name, type_id, revision_id="rev-1"):
self.name = name
self.type_id = type_id
+ self.revision_id = revision_id
class StubNode:
@@ -79,7 +80,13 @@ def build_project(steps, root_folder="Assets", entity_name=ENTITY_NAME):
step_nodes = []
for step_name, is_published in steps.items():
workfiles = (
- [StubWorkfile(f"{entity_name} - MAYA", MAYA_TYPE_ID)]
+ [
+ StubWorkfile(
+ f"{entity_name} - MAYA",
+ MAYA_TYPE_ID,
+ revision_id=f"rev-{step_name}",
+ )
+ ]
if is_published
else []
)
@@ -201,3 +208,62 @@ def raise_error(_project_id):
step_validation, "objects", types.SimpleNamespace(FlowProject=raise_error)
)
assert find_unpublished() is None
+
+
+def find_upstream(step="Surfacing", entity_type="Asset", dependencies=None):
+ """Call the referencing resolver with the common set of arguments."""
+ return step_validation.find_upstream_workfile(
+ am_project_id="am-project-1",
+ sg_entity_type=entity_type,
+ sg_entity_name=ENTITY_NAME,
+ sg_pipeline_step=step,
+ workfile_type=MAYA_TYPE,
+ step_dependencies=(
+ {"Surfacing": "Model"} if dependencies is None else dependencies
+ ),
+ )
+
+
+def test_find_upstream_workfile_returns_published_asset(flow_am):
+ """The upstream step's published workfile asset is returned for referencing."""
+ flow_am(build_project({"Model": True}))
+ workfile = find_upstream()
+ assert workfile is not None
+ assert workfile.revision_id == "rev-Model"
+
+
+def test_find_upstream_workfile_none_when_unpublished(flow_am):
+ """Nothing to reference when the upstream step has no publish."""
+ flow_am(build_project({"Model": False}))
+ assert find_upstream() is None
+
+
+def test_find_upstream_workfile_none_without_configured_upstream(flow_am):
+ """Steps absent from the mapping resolve no reference."""
+ flow_am(build_project({"Model": True}))
+ assert find_upstream(dependencies={}) is None
+
+
+def test_find_upstream_workfile_none_for_unsupported_entity(flow_am):
+ """An entity type with no Flow AM folder resolves no reference."""
+ flow_am(build_project({"Model": True}))
+ assert find_upstream(entity_type="CustomEntity01") is None
+
+
+def test_find_upstream_workfile_none_when_type_unresolved(flow_am):
+ """An unresolved schema id would match every child, so skip referencing."""
+ flow_am(build_project({"Model": True}), resolve_type_id=False)
+ assert find_upstream() is None
+
+
+def test_find_upstream_workfile_none_on_flow_am_error(monkeypatch, flow_am):
+ """A Flow AM outage skips referencing rather than surfacing an error."""
+
+ def raise_error(_project_id):
+ raise step_validation.exceptions.FlowError("simulated Flow AM outage")
+
+ flow_am(build_project({"Model": True}))
+ monkeypatch.setattr(
+ step_validation, "objects", types.SimpleNamespace(FlowProject=raise_error)
+ )
+ assert find_upstream() is None