# Copyright (c) 2023 Autodesk.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the ShotGrid Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the ShotGrid Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Autodesk.
import os
import sgtk
import shutil
import subprocess
import tempfile
logger = sgtk.platform.get_logger(__name__)
[docs]class LMVTranslator:
"""A class to translate files to be consumed by the Flow Production Tracking 3D LMV Viewer."""
def __init__(self, path, tk, context):
"""
Class constructor.
:param path: Path to the source file we want to perform operations on.
"""
self.__source_path = path
self.__tk = tk
self.__context = context
self.__output_directory = None
self.__svf_path = None
################################################################################################
# static methods
[docs] def get_translators_by_file_type():
"""
Return a mapping of file types to translator engine.
The translator engine is the name of the engine that has the necessary tools to
translate the file type.
"""
return {
".wire": "tk-alias",
".CATPart": "tk-alias",
".jt": "tk-alias",
".igs": "tk-alias",
".stp": "tk-alias",
".fbx": "tk-alias",
".vpb": "tk-vred",
}
[docs] def get_translator_relative_paths():
"""
Return a mapping of translator engine to the relative path of the translator executable.
The path return is relative to the engine's software executable location.
"""
return {
"tk-alias": os.path.join("LMVExtractor", "atf_lmv_extractor.exe"),
"tk-vred": os.path.join("LMV", "viewing-vpb-lmv.exe"),
}
[docs] def find_translator_path(tk, context, engine_name, translator_executable_path):
"""
Find the translator executable path relative to the engine's software location.
:param context: The Toolkit context used to create an engine launcher that can
determine the engine software location.
:type context: sgtk.Context
:param engine_name: The name of the engine.
:type engine_name: str
:param extractor_path: The relative file path from the engine's software location, to
the thumbnail extractor executable.
:type extractor_path: str
:return: The thumbnail extractor executable path relative to the engine's software
location.
:rtype: str
"""
# Create the engine laucnher in order to discover the engine's software location
launcher = sgtk.platform.create_engine_launcher(tk, context, engine_name)
software_versions = launcher.scan_software()
if not software_versions:
return None
# Iterate through the software versions starting from the latest version, and return
# the first thumbnail extractor executable path found
for software_version in reversed(software_versions):
software_exe_path = software_version.path
root_dir = os.path.dirname(software_exe_path)
translator_path = os.path.join(root_dir, translator_executable_path)
if os.path.exists(translator_path):
return translator_path
# No translator executable path found
return None
################################################################################################
# properties
@property
def source_path(self):
"""
Path of the file used as source for all the translations.
:returns: The file path as a string
"""
return self.__source_path
@property
def output_directory(self):
"""
Path to the directory where all the translated files will be stored.
:returns: The directory path as a string
"""
return self.__output_directory
################################################################################################
# public methods
[docs] def translate(self, output_directory=None):
"""
Run the translation to convert the source file to a bunch of files needed by the 3D Viewer.
:param output_directory: Path to the directory we want to translate the file to. If no path is supplied, a
temporary one will be used
:param use_framework_translator: True will use the translator shipped with the framework, else
False (default) will use a translator based on the type of file to
translate and the current engine running.
:returns: The path to the directory where all the translated files have been written.
"""
self.__output_directory = output_directory
translator_path = self.get_translator_path()
logger.debug(
"Using LMV Tanslator: {translator}".format(translator=translator_path)
)
if self.output_directory is None:
# generate all the files and folders needed for the translation
self.__output_directory = tempfile.mkdtemp(prefix="lmv_")
output_path = os.path.join(
self.output_directory, os.path.basename(self.source_path)
)
index_file_path = os.path.join(self.output_directory, "index.json")
open(index_file_path, "w").close()
# copy the source file to the temporary location and run the translation
logger.debug("Copying source file to temporary folder")
shutil.copyfile(self.source_path, output_path)
logger.debug("Running translation process")
cmd = [translator_path, index_file_path, output_path]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p_output, _ = p.communicate()
if p.returncode != 0:
raise Exception(p_output)
return self.output_directory
[docs] def package(self, svf_file_name=None, thumbnail_path=None):
"""
Package all the translated files into a zip file and extract the LMV thumbnail if needed
:param svf_file_name: If supplied, rename the svf file according to the given name
:param thumbnail_path: If supplied, use this thumbnail as LMV thumbnail. Otherwise, try to extract the thumbnail
from the source file
:return: The path to the zip file and the path to the thumbnail shipped with the LMV file
"""
if not self.output_directory or not os.path.isdir(self.output_directory):
raise Exception(
"Couldn't package the LMV files: no file seems to have been created"
)
output_dir_path = os.path.join(self.output_directory, "output")
# rename the svf file if needed
if svf_file_name:
logger.debug("Renaming SVF file")
source_path = self.__get_svf_path()
target_path = os.path.join(
output_dir_path, "1", "{}.svf".format(svf_file_name)
)
if os.path.isfile(target_path):
raise Exception(
"Couldn't rename svf file: target path %s already exists"
% target_path
)
os.rename(source_path, target_path)
self.__svf_path = target_path
else:
svf_file_name = os.path.splitext(os.path.basename(self.source_path)[0])
if thumbnail_path:
images_dir_path = os.path.join(output_dir_path, "images")
if not os.path.exists(images_dir_path):
os.makedirs(images_dir_path)
package_thumbnail_path = os.path.join(
images_dir_path, "{}.jpg".format(svf_file_name)
)
shutil.copyfile(thumbnail_path, package_thumbnail_path)
else:
package_thumbnail_path = None
# zip the package
logger.debug("Making archive from LMV files")
zip_path = shutil.make_archive(
base_name=os.path.join(self.output_directory, svf_file_name),
format="zip",
root_dir=output_dir_path,
)
return zip_path, package_thumbnail_path
[docs] def get_translator_path(self):
"""
Get the path to the translator we have to use according to the file extension
:return: The path to the translator.
:rtype: str
"""
_, ext = os.path.splitext(self.source_path)
current_engine = sgtk.platform.current_engine()
translator_engine = LMVTranslator.get_translators_by_file_type().get(ext)
if not translator_engine:
raise Exception("LMV translation does not support file type: {ext}")
translator_relative_path = LMVTranslator.get_translator_relative_paths().get(
translator_engine
)
if not translator_relative_path:
raise Exception(
"Mising translator information for engine: {translator_engine}"
)
# First try a shortcut to get the translator executable path from the current engine
if current_engine.name == translator_engine and hasattr(
current_engine, "executable_path"
):
root_dir = os.path.dirname(current_engine.executable_path)
translator_path = os.path.join(root_dir, translator_relative_path)
if os.path.exists(translator_path):
return translator_path
# Did not find translator from current engine. Check for local installations of
# the engine's DCC to find the translator
translator_path = LMVTranslator.find_translator_path(
self.__tk,
self.__context,
translator_engine,
translator_relative_path,
)
if not os.path.exists(translator_path):
raise Exception("Couldn't find translator for Alias.")
return translator_path
########################################################################################
# private methods
def __get_svf_path(self):
"""
Get the SFV file path according to the output directory
:return: The path to the SFV file
"""
if not self.__svf_path:
svf_file_name = "{}.svf".format(
os.path.splitext(os.path.basename(self.source_path))[0]
)
svf_path = os.path.join(self.output_directory, "output", "1", svf_file_name)
if not os.path.isfile(svf_path):
raise Exception("Couldn't find svf file %s" % svf_path)
self.__svf_path = svf_path
return self.__svf_path