From 863a92ef58000390badcf4b1c8dae42773d3b7a8 Mon Sep 17 00:00:00 2001 From: danielbui78 Date: Fri, 20 May 2022 22:34:19 -0400 Subject: [PATCH] Daz Bridge Library v2.0 (#1) * Merged latest changes from David Vodhanel's DazToUnreal Bridge, including Environment Transfer Hierarchy and InstanceNode Fixes * Updated README.md and API Documentation * QA script updates and fixes --- .gitignore | 4 + CMakeLists.txt | 271 ++ DzBridge Script Documentation and Example.dsa | 278 ++ How To Use QA Test Scripts.md | 37 + QA Script Documentation and Examples.dsa | 118 + README.md | 43 + Test/Localize-JobPool.dsa | 149 + Test/QA Manual Test Cases.md | 311 ++ Test/QA-Test-Scene-01.duf | Bin 0 -> 117286 bytes Test/Results/Readme.MD | 1 + Test/Results/TestResults_DzBridgeAction.json | 505 +++ Test/Results/TestResults_DzBridgeAction.txt | 96 + ...sults_DzBridgeNameSpaceDzBridgeDialog.json | 139 + ...esults_DzBridgeNameSpaceDzBridgeDialog.txt | 35 + ...NameSpaceDzBridgeMorphSelectionDialog.json | 127 + ...eNameSpaceDzBridgeMorphSelectionDialog.txt | 30 + ...dgeNameSpaceDzBridgeSubdivisionDialog.json | 67 + ...idgeNameSpaceDzBridgeSubdivisionDialog.txt | 20 + Test/RunAllTests.dsa | 54 + Test/UnitTests/CMakeLists.txt | 21 + Test/UnitTests/RunUnitTests.dsa | 29 + Test/UnitTests/UnitTest.cpp | 152 + Test/UnitTests/UnitTest_DzBridgeAction.cpp | 848 +++++ Test/UnitTests/UnitTest_DzBridgeAction.h | 101 + Test/UnitTests/UnitTest_DzBridgeDialog.cpp | 205 ++ Test/UnitTests/UnitTest_DzBridgeDialog.h | 40 + .../UnitTest_DzBridgeMorphSelectionDialog.cpp | 188 ++ .../UnitTest_DzBridgeMorphSelectionDialog.h | 39 + .../UnitTest_DzBridgeSubdivisionDialog.cpp | 110 + .../UnitTest_DzBridgeSubdivisionDialog.h | 28 + Test/testcases/QA_Utility_Functions.dsa | 361 +++ Test/testcases/_TC1001.dsa | 57 + Test/testcases/test_runner.dsa | 108 + include/CMakeLists.txt | 17 + include/DzBridgeAction.h | 277 ++ include/DzBridgeDialog.h | 97 + include/DzBridgeMorphSelectionDialog.h | 185 ++ include/DzBridgeSubdivisionDialog.h | 85 + include/OpenFBXInterface.h | 52 + include/OpenSubdivInterface.h | 121 + include/UnitTest.h | 93 + include/common_version.h | 10 + include/dzbridge.h | 103 + src/CMakeLists.txt | 117 + src/DzBridgeAction.cpp | 2771 +++++++++++++++++ src/DzBridgeAction_Scriptable.cpp | 220 ++ src/DzBridgeAction_Scriptable.h | 29 + src/DzBridgeDialog.cpp | 310 ++ src/DzBridgeDialog_Scriptable.cpp | 9 + src/DzBridgeDialog_Scriptable.h | 13 + src/DzBridgeMorphSelectionDialog.cpp | 1038 ++++++ ...zBridgeMorphSelectionDialog_Scriptable.cpp | 9 + src/DzBridgeMorphSelectionDialog_Scriptable.h | 12 + src/DzBridgeSubdivisionDialog.cpp | 359 +++ src/DzBridgeSubdivisionDialog_Scriptable.cpp | 9 + src/DzBridgeSubdivisionDialog_Scriptable.h | 11 + src/OpenFBXInterface.cpp | 141 + src/OpenSubdivInterface.cpp | 401 +++ src/pluginmain.cpp | 47 + 59 files changed, 11108 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 DzBridge Script Documentation and Example.dsa create mode 100644 How To Use QA Test Scripts.md create mode 100644 QA Script Documentation and Examples.dsa create mode 100644 README.md create mode 100644 Test/Localize-JobPool.dsa create mode 100644 Test/QA Manual Test Cases.md create mode 100644 Test/QA-Test-Scene-01.duf create mode 100644 Test/Results/Readme.MD create mode 100644 Test/Results/TestResults_DzBridgeAction.json create mode 100644 Test/Results/TestResults_DzBridgeAction.txt create mode 100644 Test/Results/TestResults_DzBridgeNameSpaceDzBridgeDialog.json create mode 100644 Test/Results/TestResults_DzBridgeNameSpaceDzBridgeDialog.txt create mode 100644 Test/Results/TestResults_DzBridgeNameSpaceDzBridgeMorphSelectionDialog.json create mode 100644 Test/Results/TestResults_DzBridgeNameSpaceDzBridgeMorphSelectionDialog.txt create mode 100644 Test/Results/TestResults_DzBridgeNameSpaceDzBridgeSubdivisionDialog.json create mode 100644 Test/Results/TestResults_DzBridgeNameSpaceDzBridgeSubdivisionDialog.txt create mode 100644 Test/RunAllTests.dsa create mode 100644 Test/UnitTests/CMakeLists.txt create mode 100644 Test/UnitTests/RunUnitTests.dsa create mode 100644 Test/UnitTests/UnitTest.cpp create mode 100644 Test/UnitTests/UnitTest_DzBridgeAction.cpp create mode 100644 Test/UnitTests/UnitTest_DzBridgeAction.h create mode 100644 Test/UnitTests/UnitTest_DzBridgeDialog.cpp create mode 100644 Test/UnitTests/UnitTest_DzBridgeDialog.h create mode 100644 Test/UnitTests/UnitTest_DzBridgeMorphSelectionDialog.cpp create mode 100644 Test/UnitTests/UnitTest_DzBridgeMorphSelectionDialog.h create mode 100644 Test/UnitTests/UnitTest_DzBridgeSubdivisionDialog.cpp create mode 100644 Test/UnitTests/UnitTest_DzBridgeSubdivisionDialog.h create mode 100644 Test/testcases/QA_Utility_Functions.dsa create mode 100644 Test/testcases/_TC1001.dsa create mode 100644 Test/testcases/test_runner.dsa create mode 100644 include/CMakeLists.txt create mode 100644 include/DzBridgeAction.h create mode 100644 include/DzBridgeDialog.h create mode 100644 include/DzBridgeMorphSelectionDialog.h create mode 100644 include/DzBridgeSubdivisionDialog.h create mode 100644 include/OpenFBXInterface.h create mode 100644 include/OpenSubdivInterface.h create mode 100644 include/UnitTest.h create mode 100644 include/common_version.h create mode 100644 include/dzbridge.h create mode 100644 src/CMakeLists.txt create mode 100644 src/DzBridgeAction.cpp create mode 100644 src/DzBridgeAction_Scriptable.cpp create mode 100644 src/DzBridgeAction_Scriptable.h create mode 100644 src/DzBridgeDialog.cpp create mode 100644 src/DzBridgeDialog_Scriptable.cpp create mode 100644 src/DzBridgeDialog_Scriptable.h create mode 100644 src/DzBridgeMorphSelectionDialog.cpp create mode 100644 src/DzBridgeMorphSelectionDialog_Scriptable.cpp create mode 100644 src/DzBridgeMorphSelectionDialog_Scriptable.h create mode 100644 src/DzBridgeSubdivisionDialog.cpp create mode 100644 src/DzBridgeSubdivisionDialog_Scriptable.cpp create mode 100644 src/DzBridgeSubdivisionDialog_Scriptable.h create mode 100644 src/OpenFBXInterface.cpp create mode 100644 src/OpenSubdivInterface.cpp create mode 100644 src/pluginmain.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..156972f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.vs +out +CMakeSettings.json +.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ec877f4 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,271 @@ +#********************************************************************** +# Copyright (C) 2002-2020 Daz 3D, Inc. All Rights Reserved. +# +# This file is part of the DAZ Studio SDK. +# +# This file may be used only in accordance with the DAZ Studio SDK +# license provided with the DAZ Studio SDK. +# +# The contents of this file may not be disclosed to third parties, +# copied or duplicated in any form, in whole or in part, without the +# prior written permission of Daz 3D, Inc, except as explicitly +# allowed in the DAZ Studio SDK license. +# +# See http://www.daz3d.com to contact DAZ 3D or for more +# information about the DAZ Studio SDK. +#********************************************************************** + +cmake_minimum_required(VERSION 3.4.0) + +#if (NOT EXISTS ${CMAKE_BINARY_DIR}/CMakeCache.txt) +# if (NOT CMAKE_BUILD_TYPE) +# set(CMAKE_BUILD_TYPE "Release" CACHE STRING "" FORCE) +# endif() +#endif() +#set(CMAKE_CONFIGURATION_TYPES +# "Release" +# "MinSizeRel" +# "RelWithDebInfo" +# "Debug" +#) +set(CMAKE_CONFIGURATION_TYPES ${CMAKE_CONFIGURATION_TYPES} "MinSizeRel;RelWithDebInfo;Debug;Release") +set(CMAKE_BUILD_TYPE "Release" CACHE STRING "" FORCE) + + +if(APPLE) + set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE STRING "" FORCE) + if(NOT CMAKE_OSX_ARCHITECTURES) + message( FATAL_ERROR "Mac needs CMAKE_OSX_ARCHITECTURES, set to i386 or x86_64" ) + return() + endif() +endif(APPLE) + +project("DzBridgeProject") +set(FBX_SDK_DIR "" CACHE PATH "Path to FBX SDK" ) +set(OPENSUBDIV_DIR "" CACHE PATH "Path to Opensubdiv folder" ) +set(DAZ_STUDIO_EXE_DIR "" CACHE PATH "Path to DAZ Studio, needs to be installed to a writeable location" ) + +set_property(GLOBAL PROPERTY USE_FOLDERS ON) + +if(WIN32) + set(DZ_LIB_SUFFIX ".lib") + set(DZ_BIN_SUFFIX ".dll") + set(DZ_LIB_PREFIX "") + set(UTIL_EXT ".exe") + if(CMAKE_SIZEOF_VOID_P EQUAL 4) + set(DZ_PLATFORM x86) + set(DZ_MIXED_PLATFORM Win32) + set(DZ_OS_PLATFORM Win32) + elseif(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(DZ_PLATFORM x64) + set(DZ_MIXED_PLATFORM x64) + set(DZ_OS_PLATFORM Win64) + else() + message(FATAL_ERROR "Unknown architecture") + endif() +elseif(APPLE) + set(DZ_LIB_SUFFIX ".dylib") + set(DZ_BIN_SUFFIX ".dylib") + set(DZ_LIB_PREFIX "lib") + set(UTIL_EXT "") + if(CMAKE_SIZEOF_VOID_P EQUAL 4) + set(DZ_PLATFORM x86) + set(DZ_MIXED_PLATFORM Mac32) + set(DZ_OS_PLATFORM Mac32) + elseif(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(DZ_PLATFORM x64) + set(DZ_MIXED_PLATFORM Mac64) + set(DZ_OS_PLATFORM Mac64) + else() + message(FATAL_ERROR "Unknown architecture") + endif() + set(CMAKE_MACOSX_RPATH TRUE) + set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) + SET(CMAKE_CXX_FLAGS "-std=gnu++11 ${CMAKE_CXX_FLAGS}") +else() + message(FATAL_ERROR "Unknown architecture") +endif(WIN32) + +if(NOT USE_DZBRIDGE_SUBMODULE) + +option(INSTALL_QA_PLUGIN "Build QA-Test Plugin (INSTALL_QA_PLUGIN)" OFF) + +set(DAZ_SDK_DIR_DEFAULT "") +set(DAZ_SDK_CORE_RELATIVE_PATH "lib/${DZ_MIXED_PLATFORM}/${DZ_LIB_PREFIX}dzcore${DZ_LIB_SUFFIX}") +if(NOT DAZ_SDK_DIR) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/DAZStudio4.5+ SDK/${DAZ_SDK_CORE_RELATIVE_PATH}") + set( DAZ_SDK_DIR_DEFAULT "${CMAKE_CURRENT_LIST_DIR}/DAZStudio4.5+ SDK" ) + endif() +endif() + +set(DAZ_SDK_DIR ${DAZ_SDK_DIR_DEFAULT} CACHE PATH "Path to root of the DAZ Studio SDK" ) + +if(NOT DAZ_SDK_DIR) + message(FATAL_ERROR "Missing path to DAZ Studio SDK") + return() +endif() + +set(QT_BINARY_DIR_DEFAULT "" CACHE PATH "Path to directory with QT binaries") +if(NOT QT_BINARY_DIR_DEFAULT) + if(EXISTS "${DAZ_SDK_DIR}/bin/${DZ_MIXED_PLATFORM}/qmake${UTIL_EXT}") + set( QT_BINARY_DIR_DEFAULT "${DAZ_SDK_DIR}/bin/${DZ_MIXED_PLATFORM}" ) + endif() +endif() + +if(NOT QT_BINARY_DIR_DEFAULT) + message(FATAL_ERROR "Missing path QT binaries. Check QT_BINARY_DIR_DEFAULT path") + return() +endif() + +find_package(OpenGL REQUIRED) + +#we only have release libraries for dzcore/qt so make sure even in debug they we use MD and undef debug +if(WIN32) + add_compile_options( "/MD" "/U_DEBUG" ) +endif() + +# Set dzcore as import target +set(DZ_SDK_INCLUDE "${DAZ_SDK_DIR}/include" CACHE FILEPATH "path to daz sdk includes" ) +set(DAZ_SDK_LIB "${DAZ_SDK_DIR}/${DAZ_SDK_CORE_RELATIVE_PATH}" CACHE FILEPATH "path to dzcore" ) +if(NOT EXISTS ${DAZ_SDK_LIB}) + message(FATAL_ERROR "The library dzcore could not be located. Check the path for DAZ_SDK_DIR.") + return() +endif() + +add_library(dzcore SHARED IMPORTED) +if(WIN32) + set_property(TARGET dzcore APPEND PROPERTY IMPORTED_IMPLIB ${DAZ_SDK_LIB}) +else() + set_property(TARGET dzcore APPEND PROPERTY IMPORTED_LOCATION ${DAZ_SDK_LIB}) +endif(WIN32) +set_property(TARGET dzcore APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${DZ_SDK_INCLUDE}" ) + +# Set dpc as import target +set(DAZ_SDK_DPC_EXE "${DAZ_SDK_DIR}/bin/${DZ_MIXED_PLATFORM}/dpc${UTIL_EXT}" CACHE FILEPATH "path to sdk dpc" ) +if(NOT EXISTS ${DAZ_SDK_DPC_EXE}) + message(FATAL_ERROR "The executable dpc could not be located. Check the path for DAZ_SDK_DIR.") + return() +endif() +add_executable(dpc IMPORTED) +set_property(TARGET dpc APPEND PROPERTY IMPORTED_LOCATION ${DAZ_SDK_DPC_EXE}) + +# Setup Qt from the DAZ SDK +if(WIN32) + set(DAZ_SDK_QTCORE_LIBRARY "${DAZ_SDK_DIR}/lib/${DZ_MIXED_PLATFORM}/QtCore4.lib") +elseif(APPLE) + set(DAZ_SDK_QTCORE_LIBRARY "${DAZ_SDK_DIR}/lib/${DZ_MIXED_PLATFORM}/QtCore.framework") +endif() + +set(QT_BINARY_DIR_DEFAULT "${DAZ_SDK_DIR}/bin/${DZ_MIXED_PLATFORM}") +set(QT_IMPORTS_DIR "${DAZ_SDK_DIR}/lib/${DZ_MIXED_PLATFORM}") + +set(QT_QTCORE_LIBRARY_RELEASE ${DAZ_SDK_QTCORE_LIBRARY}) +#set(QT_BINARY_DIR "${QT_BINARY_DIR_DEFAULT}") +#set(QT_QMAKE_EXECUTABLE "${QT_BINARY_DIR_DEFAULT}/qmake${UTIL_EXT}") +set(QT_QMAKE_EXECUTABLE "${DAZ_SDK_DIR}/bin/${DZ_MIXED_PLATFORM}/qmake${UTIL_EXT}") +set(QT_BINARY_DIR "${DAZ_SDK_DIR}/bin/${DZ_MIXED_PLATFORM}") +set(QT_HEADERS_DIR "${DAZ_SDK_DIR}/include") +set(QT_QTCORE_INCLUDE_DIR "${DAZ_SDK_DIR}/include/QtCore") + +# the qt find module needs this folder but our build does not so just fake it +file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/DUMMY_MKSPECS/default" ) +set(QT_MKSPECS_DIR "${CMAKE_CURRENT_BINARY_DIR}/DUMMY_MKSPECS") + +find_package(Qt4 4.8.1 REQUIRED QtCore QtGui QtScript QtOpenGL QtNetwork QtSql QtXml) + +set(DZSDK_QT_CORE_TARGET Qt4::QtCore) +set(DZSDK_QT_GUI_TARGET Qt4::QtGui) +set(DZSDK_QT_SCRIPT_TARGET Qt4::QtScript) +set(DZSDK_QT_OPENGL_TARGET Qt4::QtOpenGL) +set(DZSDK_QT_NETWORK_TARGET Qt4::QtNetwork) +set(DZSDK_QT_SQL_TARGET Qt4::QtSql) +set(DZSDK_QT_XML_TARGET Qt4::QtXml) + +list(APPEND CMAKE_AUTOMOC_MOC_OPTIONS -i) + + +############################ +# FBX SETTINGS +############################ +IF(NOT WIN32) + set(FBX_ARCH "x64") + SET(CMAKE_CXX_FLAGS "-m64 ${CMAKE_CXX_FLAGS}") + SET(CMAKE_C_FLAGS "-m64 ${CMAKE_C_FLAGS}") + SET(FBX_TMP_TARGET_LIBS ${FBX_TMP_TARGET_LIBS} dl pthread) + SET(CMAKE_CXX_FLAGS "-D_NDEBUG -Os ${CMAKE_CXX_FLAGS}") + SET(CMAKE_C_FLAGS "-D_NDEBUG -Os ${CMAKE_C_FLAGS}") + SET(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall") + SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall") + + IF(APPLE) + set(FBX_LINKER_FLAGS "-lz -lxml2 -liconv") + IF(NOT FBX_CLANG) + SET(FBX_LINKER_FLAGS "-framework Carbon -framework SystemConfiguration ${FBX_LINKER_FLAGS}") + ELSE(NOT FBX_CLANG) + SET(FBX_LINKER_FLAGS "-framework CoreFoundation -framework SystemConfiguration ${FBX_LINKER_FLAGS}") + ENDIF(NOT FBX_CLANG) + SET(FBX_TMP_TARGET_LIBS ${FBX_TMP_TARGET_LIBS} iconv) + ENDIF() + +# SET(CMAKE_CXX_FLAGS "-std=c++11 -stdlib\=libstdc++ ${CMAKE_CXX_FLAGS}") + SET(CMAKE_CXX_FLAGS "-std=gnu++11 ${CMAKE_CXX_FLAGS}") + +ENDIF(NOT WIN32) + +#set(FBX_SDK_DIR "" CACHE PATH "Path to FBX SDK" ) +if(NOT FBX_SDK_DIR) + message(FATAL_ERROR "Missing path to FBX SDK folder") + return() +endif() + +set(FBX_SDK_INCLUDE "${FBX_SDK_DIR}/include" CACHE PATH "Path to FBX SDK Includes" ) +if(WIN32) + set(FBX_PLATFORM "vs2017/x64/release") + set(FBX_SDK_LIB "${FBX_SDK_DIR}/lib/${FBX_PLATFORM}/libfbxsdk-md.lib" CACHE FILEPATH "Path to FBX SDK static library (libfbx-md.lib)" ) + set(FBX_SDK_XMLLIB "${FBX_SDK_DIR}/lib/${FBX_PLATFORM}/libxml2-md.lib" CACHE FILEPATH "Path to FBX SDK XML library (libxml2-md.lib)" ) + set(FBX_IMPORT_LIBS + ${FBX_SDK_LIB} + ${FBX_SDK_XMLLIB} + ${FBX_LINKER_FLAGS}) +elseif(APPLE) +# set(FBX_PLATFORM "clang/libstdcpp/release") + set(FBX_PLATFORM "clang/release") + set(FBX_SDK_LIB "${FBX_SDK_DIR}/lib/${FBX_PLATFORM}/libfbxsdk.a" CACHE FILEPATH "Path to FBX SDK static library (libfbxsdk.a)" ) + set(FBX_IMPORT_LIBS + ${FBX_SDK_LIB} + ${FBX_LINKER_FLAGS}) +endif() + +############################ +# Opensubdiv SETTINGS +############################ +#set(OPENSUBDIV_DIR "" CACHE PATH "Path to Opensubdiv folder" ) +if(NOT OPENSUBDIV_DIR) + message(FATAL_ERROR "Missing path to Opensubdiv folder") + return() +endif() +set(OPENSUBDIV_INCLUDE "${OPENSUBDIV_DIR}" CACHE PATH "Path to Opensubdiv include folder (usually same as root folder)" ) +if(WIN32) + set(OPENSUBDIV_LIB "${OPENSUBDIV_DIR}/build/lib/Release/osdCPU.lib" CACHE FILEPATH "Path to Opensubdiv CPU static library (osdCPU.lib)" ) +elseif(APPLE) + set(OPENSUBDIV_LIB "${OPENSUBDIV_DIR}/build/lib/Release/libosdCPU.a" CACHE FILEPATH "Path to Opensubdiv CPU static library (libosdCPU.a)" ) +endif() + + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) + + +# if building a plugin and you want the compiled result placed in the Daz Studio ./plugins directory +if(INSTALL_QA_PLUGIN) + if(DAZ_STUDIO_EXE_DIR) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${DAZ_STUDIO_EXE_DIR}/plugins) + endif() +endif() + +endif(NOT USE_DZBRIDGE_SUBMODULE) + +add_subdirectory("include") +add_subdirectory("Test/UnitTests") +add_subdirectory("src") diff --git a/DzBridge Script Documentation and Example.dsa b/DzBridge Script Documentation and Example.dsa new file mode 100644 index 0000000..7d45a39 --- /dev/null +++ b/DzBridge Script Documentation and Example.dsa @@ -0,0 +1,278 @@ +// DAZ Studio version 4.16.0.3 filetype DAZ Script + +// DzBridge Script Documentation and Examples +var oBridge = new DzBridgeScriptableAction(); +var oBridge = new DzBridgeUnityAction(); +var oBridge = new DzUnrealAction(); + +///////////////////////////////////// +// +// Properties +// +///////////////////////////////////// + +// NOTE: Each property also has get/set methods for better error checking. + +// (integer) nNonInteractiveMode +// 0 == interactive mode (default) +// 1 == noninteractive mode (script) +oBridge.nNonInteractiveMode; +oBridge.getNonInteractiveMode(); +oBridge.setNonInteractiveMode(0); + +// (QString) sAssetType +// "SkeletalMesh" == rigged mesh, figure +// "StaticMesh" == unrigged mesh, prop +// "Animation" == animation sequence(s) +// "Pose" == single frame +// "Environment" == one or more instances of props +oBridge.sAssetType; +oBridge.getAssetType(); +oBridge.setAssetType("SkeletalMesh"); + +// (QString) sExportFilename +// Filename stem to be used for DTU file +// Default value is the Daz Scene Label string for selected node +oBridge.sExportFilename; +oBridge.getExportFilename(); +oBridge.setExportFilename(""); + +// (QString) sRootFolder +// Destination root folder for export +// For Unreal, Blender bridges, this is an intermediate folder before the final import routine +// For Unity bridge, this is the final destination within the Unity Project's Asset Folder +oBridge.sRootFolder; +oBridge.getRootFolder(); +oBridge.setRootFolder(""); + +// (QString) sProductName +// metadata which is used in batch conversion mode to export the Daz Store Product name +// not used in interactive mode +oBridge.sProductName; +oBridge.getProductName(); +oBridge.setProductName(""); + +// (QString) sProductComponentName +// metadata which is used in batch conversion mode to export the Daz Store Product name +// not used in interactive mode +oBridge.sProductComponentName; +oBridge.getProductComponentName(); +oBridge.setProductComponentName(""); + +// (QStringList) aMorphList +// List of morphs to be exported +// these are internal morph names and not the friendly labels +oBridge.aMorphList; +oBridge.getMorphList(); +oBridge.setMorphList([]); + +// (boolean) bUseRelativePaths +// false == export DTU file with absolute file paths to Daz Asset Library files (default) +// true == export DTU file with file paths relative to the root of the Daz Asset Library +// Enable this option to make DTU files which are independent of installation paths. +// Must be used in combination with Localize-Jobpool.dsa script to convert back to absolute +// paths for Target Software. +oBridge.bUseRelativePaths; +oBridge.getUseRelativePaths(); +oBridge.setUseRelativePaths(false); + +// (boolean) bUndoNormalMaps +// true == undo changes to materials, aka remove generated normal maps, after export (default) +// false == keep changes to materials, added normal maps +oBridge.bUndoNormalMaps; +oBridge.getUndoNormalMaps(); +oBridge.setUndoNormalMaps(true); + +// (QString) sExportFbx +// Override filename for exported FBX +// If empty/blank string, defaults to sExportFilename +oBridge.sExportFbx; +oBridge.getExportFbx(); +oBridge.setExportFbx(""); + +// (DzBasicDialog) wBridgeDialog +// widget to main Bridge Dialog window +// NOTE: returned object is type DzBasicDialog and not type DzBridgeDialog, so member +// functions of DzBridgeDialog will not be accessible. +oBridge.wBridgeDialog; +oBridge.getBridgeDialog(); +oBridge.setBridgeDialog(null); + +// (DzBasicDialog) wSubdivisionDialog +// widget to Subdivision Dialog window +// NOTE: returned object is type DzBasicDialog and not type DzBridgeSubdivisionDialog, +// so member functions of DzBridgeSubdivisionDialog will not be accessible. +oBridge.wSubdivisionDialog; +oBridge.getSubdivisionDialog(); +oBridge.setSubdivisionDialog(null); + +// (DzBasicDialog) wMorphSelectionDialog +// widget to Morph Selection Dialog window +// NOTE: returned object is type DzBasicDialog and not type DzBridgeMorphSelectionDialog, +// so member functions of DzBridgeMorphSelectionDialog will not be accessible. +oBridge.wMorphSelectionDialog; +oBridge.getMorphSelectionDialog(); +oBridge.setMorphSelectionDialog(null); + +///////////////////////////////////// +// +// Methods +// +///////////////////////////////////// + +// (void) readGui(DzBridgeDialog arg) +// Transfer UI data from a Bridge Dialog window to internal Bridge member variables. +// Use prior to calling export operation. +oBridge.readGui(null); + +// (void) exportHD(DzProgress arg = null) +// Starts export process, including pre-processing Scene data and exporting FBX and +// DTU files. Will also produce high-definition (HD) fbx with baked subdivision and +// automatically fixes missing bone weights in HD fbx file. +oBridge.exportHD(); + +// (void) resetToDefaults() +// Reset bridge options to default values. No arguments or return value. +oBridge.resetToDefaults(); + +// (QString) cleanString(QString arg) +// Returns a copy of with only alphanumeric characters and underscore ("_"). +// In other words, all non alphanumeric characters except underscore are removed, +// including space and hyphen. +// Safe for filesystem names and most programming language identifier names. +oBridge.cleanString(""); + +// (QString) getMD5(QString arg) +// Returns string result of MD5 hash function applied to contents of a file specific +// by filepath. +oBridge.getMD5("c:/temp/tempfile.txt"); + +// (QStringList) getAvailableMorphs(DzNode arg) +// Returns a list of all morphs which can be applied to . +// The returned values can be used to export morphs via oBridge.aMorphList +oBridge.getAvailableMorphs(null); + +// (QStringList) getActiveMorphs(DzNode arg) +// Returns a list of all active morphs for (morph strength > zero). +// The returned values can be used to export morphs via oBridge.aMorphList +oBridge.getActiveMorphs(null); + +// (QImage) makeNormalMapFromHeightMap(QString heightMapFilename, double normalStrength) +// Return a new QImage with a normalmap texture generated from the heightmap passed in +// and modulated by normalStrength. +oBridge.makeNormalMapFromHeightMap("", 1.0); + +///////////////////////////////////// +// +// Dialog Classes +// +///////////////////////////////////// + +// class DzBridgeDialog +var oBridgeDialog = new DzBridgeDialog(); +dzwidget = oBridgeDialog.wAssetNameEdit; +dzwidget = oBridgeDialog.wAssetTypeCombo +dzwidget = oBridgeDialog.wMorphsEnabledCheckBox; +dzwidget = oBridgeDialog.wSubdivisionEnabledCheckBox; +dzwidget = oBridgeDialog.wAdvancedSettingsGroupBox; +oBridgeDialog.resetToDefaults(); +oBridge.setBridgeDialog(oBridgeDialog); + +// NOTE: the following classes MUST have a Scene node selected, otherwise they will crash +if (Scene.getPrimarySelection()) +{ + // class DzBridgeMorphSelectionDialog + // DzBridgeMorphSelectionDialog.Get(QWidget parent) + // retrieves or instantiates a singleton widget for this class + var oMorphDialog = new DzBridgeMorphSelectionDialog().Get(oBridgeDialog); + // (void) DzBridgeMorphSelectionDialog.PrepareDialog() + // Populates morph selection widgets + oMorphDialog.PrepareDialog(); + // (QString) DzBridgeMorphSelectionDialog.GetMorphString() + // returns selection string to be used with FbxExporter + sMorphString = oMorphDialog.GetMorphString(); + // (QString) DzBridgeMorphSelectionDialog.GetMorphCSVString() + // returns CSV version of morph selection string + sCsvString = oMorphDialog.GetMorphCSVString(); + // (boolean) DzBridgeMorphSelectionDialog.IsAutoJCMEnabled() + // returns true if AutoJCM checkbox is enabled + bEnableJCM = oMorphDialog.IsAutoJCMEnabled(); + // (QString) DzBridgeMorphSelectionDialog.GetMorphLabelFromName(QString arg) + // returns friendly label when passed internal morph name via + oMorphDialog.GetMorphLabelFromName(""); + oBridge.setMorphSelectionDialog(oMorphDialog); + + // class DzBridgeSubdivisionDialog + // DzBridgeSubdivisionDialog.Get(QWidget parent) + // retrieves or instantiates a singleton widget for this class + var oSubdivisionDialog = new DzBridgeSubdivisionDialog().Get(oBridgeDialog); + // (void) DzBridgeSubdivisionDialog.PrepareDialog() + // Populates subdivision level widgets + oSubdivisionDialog.PrepareDialog(); + // (void) DzBridgeSubdivisionDialog.LockSubdivisionProperties(bool bSubdivisionEnabled) + // Locks subdivision properties, used during exportHD() method to control + // mesh definition level. + // If bSubdivisionEnabled is true, then all models are locked at their current + // subdivision level. + // If bSubdivisionEnabled is false, then all models are locked at subdivision level zero. + oSubdivisionDialog.LockSubdivisionProperties(false); + // (void) oSubdivisionDialog.UnlockSubdivisionProperties() + // Unlocks subdivision properties, returning them to settings prior to calling + // LockSubdivisionProperties() + oSubdivisionDialog.UnlockSubdivisionProperties(); + // (DzNode) oSubdivisionDialog.FindObject(DzNode parentNode, QString name) + // Recursively searches for a child node within that has name == + // Returns null if no match is found. + oSubdivisionDialog.FindObject(Scene.getPrimarySelection(), ""); + // (bool) setSubdivisionLevelByNode(DzNode node, int level) + // Recursively searches for child nodes with the same name as using the + // primary scene selection as a root node. Then sets that child node's subdivision + // level to . + // Returns true if successful. Returns false if no match is found or if is + // greater than the maximum level available in the Subdivision Level dropdown. + oSubdivisionDialog.setSubdivisionLevelByNode(DzNode(), 0); + oBridge.setSubdivisionDialog(oSubdivisionDialog); + +} + + +///////////////////////////////////// +// +// Currently not accessible from default Daz Script Interpreter +// +///////////////////////////////////// + +// (bool) copyFile(QFile file, QString dst, bool bReplace = true, bool bCompareFiles = true) +// Copies a file to a destination filepath. +// If bReplace is true, then destination file is removed prior to performing copy. +// Note: on most platforms, the copy operation will fail if destination file exists. +// If bCompare is also true, then destination file is removed only if MD5 comparison of +// the source and destination files are different. +// Returns true if copy operation is performed and false if any copy fails or if any ot the +// optional copy conditions is not met. +// NOTE: currently not practical for scripting because QFile is not registered in default +// DazScript interpreter + +// (void) upgradeToHD(QString baseFilePath, QString hdFilePath, QString outFilePath, std::map* pLookupTable) +// Performs insertion of missing bone weight data for baked subdivision fbx files. +// NOTE: currently not practical for scripting due to std::map argument. +//oBridge.upgradeToHD("", "", "", null); + +// NOTE: Following methods are currently not usable due to DzJsonWriter not registered +// (void) writeDTUHeader(DzJsonWriter writer) +// (void) writeAllMaterials(DzNode* Node, DzJsonWriter& Writer, QTextStream* CVSStream = nullptr, bool bRecursive = false) +// (void) startMaterialBlock(DzNode* Node, DzJsonWriter& Writer, QTextStream* pCVSStream, DzMaterial* Material) +// (void) finishMaterialBlock(DzJsonWriter& Writer) +// (void) writeAllMorphs(DzJsonWriter& Writer) +// (void) writeMorphProperties(DzJsonWriter& writer, const QString& key, const QString& value) +// (void) writeMorphJointLinkInfo(DzJsonWriter& writer, const JointLinkInfo& linkInfo) +// (void) writeAllSubdivisions(DzJsonWriter& Writer) +// (void) writeSubdivisionProperties(DzJsonWriter& writer, const QString& Name, int targetValue) +// (void) writeAllDforceInfo(DzNode* Node, DzJsonWriter& Writer, QTextStream* pCVSStream = nullptr, bool bRecursive = false) +// (void) writeDforceMaterialProperties(DzJsonWriter& Writer, DzMaterial* Material, DzShape* Shape) +// (void) writeDforceModifiers(const QList& dforceModifierList, DzJsonWriter& Writer, DzShape* Shape) +// (void) writeEnvironment(DzJsonWriter& writer); +// (void) writeInstances(DzNode* Node, DzJsonWriter& Writer, QMap& WritenInstances, QList& ExportedGeometry, QUuid ParentID = QUuid()) +// (void) writeInstance(DzNode* Node, DzJsonWriter& Writer, QUuid ParentID) +// (void) writeAllPoses(DzJsonWriter& writer) +// (void) writeWeightMaps(DzNode Node, DzJsonWriter Stream) diff --git a/How To Use QA Test Scripts.md b/How To Use QA Test Scripts.md new file mode 100644 index 0000000..36f797c --- /dev/null +++ b/How To Use QA Test Scripts.md @@ -0,0 +1,37 @@ +# How To Use QA Test Scripts # + +## Special Note on Test Reports +The QA Report Files generated by the UnitTest and TestCase scripts have been designed and formatted so that the QA Reports will only change when there is a change in a test result. Specifically, there are no user-names or timestamps or absolute filepaths in the test results. This allows Github to conveniently track the history of test results with source-code changes, and allows developers and QA testers to notified by Github or their git client when there are any changes and the exact test that changed its result. + +## Run all UnitTests and automated Test Cases for all bridge projects +1. Start Daz Studio. +2. Open the Script IDE Pane. +3. Load "Test/RunAllTests.dsa". +4. Configure "sCommonPath" on line 10 to point to the correct absolute path containing the RunAllTests.dsa script. +5. Configure "Global_sOutputPath" on line 13 to point to the absolute path of the "Test/Results/" folder. +6. Make sure the correct absolute path is specified for DzUnreal and DzUnity scripts. Comment out any sections you do not wish to run. +7. Run the script by clicking "Execute" or pressing . + +## Run only UnitTests +1. Start Daz Studio. +2. Open the Script IDE Pane. +3. Load "Test/UnitTests/RunUnitTests.dsa". +4. Configure the "sIncludePath" to point to the correct absolute path containing the RunUnitTests.dsa script. +5. Configure the "sOutputPath" to pont to the absolute path of the "Test/Results" folder. +6. Run the script. + +## Run all Automated Test Cases for a specific bridge project +1. Start Daz Studio. +2. Open the Script IDE Pane. +3. Load "/Test/TestCases/test_runner.dsa" for whatever desired bridge project, such as DazToUnreal or DazToUnity. +4. Configure "sIncludePath" to point to the correct absolute path containing the test_runner.dsa script. +5. Configure "Global_sOutputPath" to point to the absolute path of the "/Test/Results/" folder. +6. Run the script. + +## Run a single Automated Test Case for a specific bridge project +1. Start Daz Studio. +2. Open the Script IDE Pane. +3. Load "/Test/TestCases/test_runner--single.dsa" for whatever desired bridge project, such as DazToUnreal or DazToUnity. +4. Configure "sIncludePath" to point to the correct absolute path containing the test_runner--single.dsa script. +5. Configure "Global_sOutputPath" to point to the absolute path of the "/Test/Results/" folder. +6. Copy-Paste or uncomment the line for the desired Test Case(s) you wish to run. diff --git a/QA Script Documentation and Examples.dsa b/QA Script Documentation and Examples.dsa new file mode 100644 index 0000000..d929164 --- /dev/null +++ b/QA Script Documentation and Examples.dsa @@ -0,0 +1,118 @@ +// DAZ Studio version 4.16.0.3 filetype DAZ Script + +// QA Script Documentation and Examples + +// NOTE: Please read the top section in "How To Use QA Test Scripts.md". You will +// notice that QA Reports in the "/Test/Results" folder do not use timestampes, +// usernames, or absolute filepaths. This is to remove all changes in the QA +// Report files from test to test unless the result changes from PASS to FAIL. +// The benefit is that github can then track the history of test result changes +// with source-code changes and will easily identify when and where a test fails. +// However, if there is a test failure, it's recommended that as much details as +// needed are written into the JSON reports to help debug the failure. + + +// 1. define "Global_sOutputPath" to a folder for test results. +Global_sOutputPath = "C:/MyProject/Test/Results/"; + +// 2. Include the "Test/TestCases/QA_Utility_Functions.dsa" +include("C:/GitHub/dzbridge-common/Test/TestCases/QA_Utility_Functions.dsa"); + +// 3. Use clearLog() to initialize the temporary log file so it is ready to +// receive output from printToLog(). +clearLog(); + +// 4. Use printToLog(string) instead of print(string) for all output intended to +// be written to the QA test log file under the current test. +// NOTE: Like print(), each call to printToLog() will automatically add a new line ("\n"). +printToLog("Starting TestCase01..."); +printToLog("TestCase01, part 1: file format OK"); +printToLog("TestCase01, part 2: file format OK"); +printToLog("TestCase01 completed successfully."); + + +// 5. Use logToJson(sTestName, bResult) to create a new JSON entry in the JSON Test +// results file. Calling logToJson() will automatically clear the log file via clearLog(). +// NOTE: Instead of bResult, you can pass a function call or expression. The return value +// of that function will be logged in the JSON test entry and also passed back as the return +// value of logToJson(). +function TestCase01_Function(args) { return true; } +args = "test_data"; +bReturnValue_of_TestCase01_Function = logToJson("TestCase01", TestCase01_Function(args)); + +// NOTE: The filename for the JSON Test Results file is stored in the +// QA_Utility_Functions.dsa, as sJsonFile. +// Line 5 of QA_Utility_Functioins.dsa: +var sJsonFile = sOutputPath + "/" + "TestCase_Results.json" + + +// (bool) Run_Exporter(sExportFilename, sAssetType, sRootFolder, sExportFolder, +// sProductName, sComponentName, arrayMorphList) +// Convenience function that creates a DzBridge object, configures it, and exports the +// currently selected scene node. +// filename stem to use for DTU file +// this can be left empty, and the bridge will automatically use the name of the +// filename of the loaded scene file, or the label of the selected scene node. +// "SkeletalMesh", "StaticMesh", "Animation", "Pose", "Environment" +// this can be left empty and the bridge will automatically choose a compatible type. +// destination root folder +// destination subfolder within the root folder. If empty, the bridge +// will default to using as the subfolder name. +// optional metadata for Daz Store Product Name, ex: "Kent Hair" +// optional metadata for name of component within the product, +// ex: "Light Brown Texture" +// list of morph names to be exported with FBX file. These are internal +// names and not the friendly labels. If empty, then no morphs will be exported. +// NOTE: see TC06.dsa for example of morph export. +Run_Exporter("victoria8", "", "C:/MyDocs/DestinationRoot/", "", "", "", []); + +// (bool) Run_Exporter2(oBridge, sExportFilename, sAssetType, sRootFolder, +// sExportFolder, sProductName, sComponentName, arrayMorphList) +// Like Run_Exporter(), but you can pass in your own pre-configured DazBridge object. +// NOTE: See TC07.dsa for an example with custom-configured bridge and subdivision dialogs. +oBridge = new DzBridgeScriptableAction(); +Run_Exporter2(oBridge, "", "", "C:/Root/", "", "", "", []); + +/////////////////////////////////////// +// Validation Convenience Functions +/////////////////////////////////////// + +// (bool) Validate_DTU_file(sDTUFilename) +// Validate that the contents of are in valid JSON file format. +// Returns true if valid. +bReturnVal = Validate_DTU_file("exportpath/exported.dtu"); + +// (bool) Validate_FBX_file(sFbxFilename) +// Validates that the contents of are in valid FBX file format. +// Returns true if valid. +bReturnVal = Validate_FBX_file("exportpath/exported.fbx"); + +// (bool) Validate_Image_Format(sImageFilename) +// Validates that the contents of are in a valid image file format +// using QImage. +// Returns true if valid. +bReturnVal = Validate_Image_Format("exportpath/exported.png"); + +// (bool) Validate_NormalMaps(arrNormalMapList, sDTUpath) +// Validates a list of images in /ExportTextures/ are valid. +// should be filenames without paths +// is the main export folder. The function will check for inside +// "/ExportTextures/" for the each filename in +// NOTE: /ExportTextures/ must contain the exact files that are listed +// in and all files must end with "_nm.png". If there are missing +// files or extra files, then the Validate_NormalMaps() function will return false. +// If any files are not valid QImage files, the function will return false. +aNormalMapList = ["image1_nm.png", "image2_nm.png", "image3_nm.png"]; +bReturnVal = Validate_NormalMaps(aNormalMapList, "pathToDTU"); + +// (bool) Validate_LIE_Textures(nNumLIETextures, sNameFilters, sDTUpath) +// Validate a number of images in the ExportTextures folder. +// is the number of images in the /ExportTextures folder +// is a filename search expression, example: "d*.png" or "*.jpg" +// is the export folder which contains an "ExportTextures" subfolder. +// NOTE: /ExportTextures/ must contain a set of image files which match +// the search string that has a count of . +// Each image file in the match set must also be valid QImage files. If any of these +// conditions do not match, the function returns false. +// NOTE: see TC10.dsa for an example +bReturnVal = Validate_LIE_Textures(5, "d*.png", "pathToDTU"); diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8e09ad --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Daz Bridge Library + +# Table of Contents +1. About the Daz Bridge Library +2. How to Install +3. How to Build +4. How to QA Test +5. How to Modify and Develop +6. How to Use with Daz Scripts + + +# 1. About the Daz Bridge Library +Daz Bridge Library is a multipurpose C++ library and Script framework containing classes for: +1. Writing C++ and DazScript Plugins to export Daz assets to external software packages, +2. Performing common conversion-related operations such as Normal Map Generation, Texture Baking and Mesh Subdivision, +3. Automating Quality Assurance tests including automated unit-tests and test-cases, and file format validation. + +This repository can be added to existing or new projects as as a git submodule. The library itself can be linked as a static or shared library or built into a stand-alone scriptable plugin. + +# 2. How to Install +The stand-alone, scriptable plugin can be copied to the Daz Studio plugins folder (example: "\Daz 3D\Applications\64-bit\DAZ 3D\DAZStudio4\plugins"). Daz Studio can then be started, and the plugin accessed via scripts written in the IDE, or pressing F3 and adding the Bridge->"Daz Scriptable Bridge" Action to your main menu or toolbar. Script API documentation and examples can be found in the Scripting section below. + +# 3. How to Build +Requirements: Daz Studio 4.5+ SDK, Qt 4.8.1, Autodesk Fbx SDK, Pixar OpenSubdiv Library, CMake, C++ development environment + +Download or clone the Daz Bridge Library github repository to your local machine. Use CMake to configure the project files. If using the CMake gui, you will be prompted for folder paths to dependencies: Daz SDK, Qt 4.8.1, Fbx SDK and OpenSubdiv during the Configure process. + +# 4. How to QA Test +The Test folder contains a `QA Manual Test Cases.md` document with instructions for performaing manual tests. The Test folder also contains subfolders for UnitTests, TestCases and Results. To run automated Test Cases, run Daz Studio and load the `Test/testcases/test_runner.dsa` script, configure the sIncludePath on line 4, then execute the script. Results will be written to report files stored in the `Test/Reports` subfolder. + +To run UnitTests, you must first build special Debug versions of the DzBridge-Unity and DzBridge Static sub-projects with Visual Studio configured for C++ Code Generation: Enable C++ Exceptions: Yes with SEH Exceptions (/EHa). This enables the memory exception handling features which are used during null pointer argument tests of the UnitTests. Once the special Debug version of DazToUnity dll is built and installed, run Daz Studio and load the `Test/UnitTests/RunUnitTests.dsa` script. Configure the sIncludePath and sOutputPath on lines 4 and 5, then execute the script. Several UI dialog prompts will appear on screen as part of the UnitTests of their related functions. Just click OK or Cancel to advance through them. Results will be written to report files stored in the `Test/Reports` subfolder. + +Finally, there is a "How To Use QA Test Scripts.md" file with instructions for performing a full automated test-suite of UnitTests and TestCases for Daz Bridge Library, DazToUnity and DazToUnreal Bridges. See the section below for "How to Use with Daz Scripts" for information on writing your own automated QA scripts. + +Special Note: The QA Report Files generated by the UnitTest and TestCase scripts have been designed and formatted so that the QA Reports will only change when there is a change in a test result. This allows Github to conveniently track the history of test results with source-code changes, and allows developers and QA testers to notified by Github or their git client when there are any changes and the exact test that changed its result. + +# 5. How to Modify and Develop +The "src" folder contains C++ classes for interactive GUI and scripted conversions. The `pluginmain.cpp`, and the multiple files named `DzBridge***_Scriptable.cpp/.h` build a stand-alone plugin and also serve as an example of how to create a custom Bridge plugin for external software. The "include" folder contains header files which can be added to external projects for static and shared linkage with the Daz Bridge Library. + +The Daz Bridge Library uses a default namespace named "DzBridgeNameSpace". If you static-link with a Daz Studio plugin or make modifications to the existing Daz Bridge Library source-code, it is recommended that you change the namespace to a unique name. This ensures that there are no C++ Namespace collisions when other plugins based on the Daz Bridge Library are also loaded in Daz Studio. In order to link and share C++ classes between this plugin and the Daz Bridge Library, a custom `CPP_PLUGIN_DEFINITION()` macro is used instead of the standard DZ_PLUGIN_DEFINITION macro and usual .DEF file. NOTE: Use of the DZ_PLUGIN_DEFINITION macro and DEF file use will disable C++ class export in the Visual Studio compiler. + +# 6. How to Use with Daz Scripts +The `DzBridge Script Documentation and Example.dsa` serves as both an example script as well as API documentation for how to use the scriptable plugin, starting with how to instantiate a Bridge object, configuring conversion settings and output folder, etc. Additional practical scripting examples for loading assets and exporting them can be found in the script test-cases for DazToUnity and DazToUnreal Bridges. The `QA Script Documentation and Examples.dsa` serves as documentation, API and example script for using the Daz Bridge Library to write automation tests, including output file format validation. diff --git a/Test/Localize-JobPool.dsa b/Test/Localize-JobPool.dsa new file mode 100644 index 0000000..c6ad59f --- /dev/null +++ b/Test/Localize-JobPool.dsa @@ -0,0 +1,149 @@ +// DAZ Studio version 4.16.0.3 filetype DAZ Script + +///////////////////////////// +// Localize-JobPool +///////////////////////////// +// +// Converts all DTU files in a jobpool to Aboslute Paths. +// +function RelativeToAbsolute( sDtuPath ) { + + var oContentMgr = App.getContentMgr(); + print("Reading DTU file: [" + sDtuPath + "]"); + var oFile = new DzFile( sDtuPath ); + if ( !oFile.exists() ) { + print("DTU file does not exist: " + sDtuPath); + return false; + } + if ( !oFile.open(DzFile.ReadOnly) ) { + print("Unable to open DTU file for reading: " + sDtuPath); + return false; + } + var sDtuContents = oFile.read(); + oFile.close(); + if (!sDtuContents) { + print("Error reading DTU file: " + sDtuPath); + return false; + } + var oDTU = {} + try { + oDTU = JSON.parse(sDtuContents); + } + catch (e) { + oDTU = false; + } + if (!oDTU) { + print("Error parsing DTU file. May be invalid JSON format: " + sDtuPath); + return false; + } + + var arrKeys = Object.keys(oDTU); +// for (i = 0; i < arrKeys.length; i++) +// { +// print("Key[" + i + "]: " + arrKeys[i]); +// } + + var MaterialList = oDTU["Materials"]; + // pre def for memory handling + var matObj; + var propList; + var property; + var sTextureName; + var sFullPath; + + if (MaterialList == undefined) { + print("Empty materials list"); + return false; + } + +// print ("Material Keys: " + Object.keys(MaterialList[0])) + for (var i = 0; i < MaterialList.length; i += 1) { + matObj = MaterialList[i]; + if (matObj == undefined) { + print("Matobj @ index="+i+" undefined"); + continue; + } + + print("Material[" + i + "]: " + matObj["Material Name"]) + propList = matObj["Properties"]; + if (propList == undefined) { + print("propList @ index="+i+" undefined"); + continue; + } + for (var j = 0; j < propList.length; j += 1) { + property = propList[j]; + if (property == undefined) { + print("property @ index="+j+" undefined"); + continue; + } + if (property["Texture"].length > 0) { + //print ("Name: " + property["Name"]); + //print ("Texture: " + property["Texture"]); + sTextureName = property["Texture"]; + sFullPath = oContentMgr.findFile(sTextureName); + //print ("sFullPath: " + sFullPath); + if (sTextureName != sFullPath && sFullPath.length > 0) { + print("Texture renamed: " + sFullPath); + property["Texture"] = sFullPath; + } + } + } + } + + var newContents = JSON.stringify(oDTU, null, "\t"); + oFile.open(DzFile.WriteOnly); + oFile.write(newContents); + oFile.close(); + return true; +} + +function ProcessJobFile ( sJobFile ) { + print("Reading jobfile: " + sJobFile); + var oFile = new DzFile( sJobFile ); + if ( !oFile.exists() ) { + print("Jobpool file does not exist."); + return false; + } + if ( !oFile.open(DzFile.ReadOnly) ) { + print("Unable to open jobpool file for reading."); + return false; + } + + var aLines = oFile.readLines(); + var sJobLine; + for (var index=0; index\n"); + return; + } + + print("DEBUG: num args:" + nArgCount + ", arglist is: " + aArgList); + + for (var i = 0; i < nArgCount; i += 1) { + sJobfile = aArgList[i]; + if (ProcessJobFile(sJobfile) == false) { + print("Convert jobpool failed: " + sJobfile); + } + } +} + +main(); diff --git a/Test/QA Manual Test Cases.md b/Test/QA Manual Test Cases.md new file mode 100644 index 0000000..6477569 --- /dev/null +++ b/Test/QA Manual Test Cases.md @@ -0,0 +1,311 @@ +# QA Manual Test Cases # + +## TC1. Load and Export Genesis 8 Basic Female to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 8 Basic Female. +6. Select File->Send To->Daz To Unreal. +7. Confirm Asset Name is "Genesis8Female" in the Daz To Unreal dialog window. +8. Click "Accept". +9. Confirm Unreal Engine has successfully generated a new "Genesis8Female" asset in the Content Browser Pane. +10. Confirm that a "Genesis8Female" subfolder was generated in the Intermediate Folder, with "Genesis8Female.dtu" and "Genesis8Female.fbx" files. +11. Confirm normal maps are valid images by opening each one in an image viewer. +12. In Daz Studio, go to the Surfaces pane and click the "Editor" tab. +13. Select "Genesis 8 Female", type "normal" in the search/filter bar text box which is found next to the magnifying glass icon. +14. Confirm that pane shows "(16): Normal Map" property with "Choose Map" text displayed. + +## TC2. Load and Export Additional Genesis 8.1 Basic Female to Unreal +1. Continue from previous Daz Studio and Unreal Engine session (test case 1). +2. Deselect "Genesis 8 Female" from Scene. +3. Load Genesis 8.1 Basic Female. +4. Confirm new Scene node is created named "Genesis 8.1 Female". +5. Select File->Send To->Daz To Unreal. +6. Confirm Asset Name is "Genesis81Female" in the Daz To Unreal dialog window. +7. Click "Accept". +8. Confirm UnrealEngine has successfully generated a new "Genesis81Female" asset in the Content Browser Pane. +9. Confirm that a "Genesis81Female" subfolder was generated in the Intermediate Folder, with "Genesis81Female.dtu" and "Genesis81Female.fbx" files. +10. Confirm that "ExportTextures" subfolder is present in the "Genesis8Female" folder, with 5 normal map files for: body, face, head, arms, legs. +11. Confirm normal maps are valid images by opening each one in an image viewer. +12. In Daz Studio, go to the Surfaces pane and click the "Editor" tab. +13. Open the "Genesis 8.1 Female" tree and select "Skin-Lips-Nails", type "normal" in the search/filter bar text box which is found next to the magnifying glass icon. +14. Confirm that pane shows "(10): Normal Map" property with "Choose Map" text displayed. + +## TC3. Load and Export Genesis 8.1 Basic Female with Custom Scene Node Label +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 8.1 Basic Female. +6. Select the Genesis 8.1 Basic Female from the Scene Pane. +7. Rename the node to "CustomSceneLabel". +8. Select File->Send To->Daz To Unreal. +9. Confirm Asset Name is "CustomSceneLabel" in the Daz To Unreal dialog window. +10. Click "Accept". +11. Confirm Unreal Engine has successfully generated a new "CustomSceneLabel" asset in the Content Browser Pane. +12. Confirm that a "CustomSceneLabel" subfolder was generated in the Intermediate Folder, with "CustomSceneLabel.dtu" and "CustomSceneLabel.fbx" files. + +## TC4. Load and Export Genesis 8.1 Basic Female with Custom Asset Name to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 8.1 Basic Female. +6. Select File->Send To->Daz To Unreal. +7. Confirm Asset Name is "Genesis81Female" in the Daz To Unreal dialog window. +8. Change Asset Name to "CustomAssetName". +9. Click "Accept". +10. Confirm Unreal Engine has successfully generated a new "CustomAssetName" asset in the Content Browser Pane. +11. Confirm that a "CustomAssetName" subfolder was generated in the Intermediate Folder, with "CustomAssetName.dtu" and "CustomAssetName.fbx" files. + +## TC5. Load and Export Genesis 8.1 Basic Female with Custom Intermediate Folder to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 8.1 Basic Female. +6. Select File->Send To->Daz To Unreal. +7. Confirm Intermediate Folder is "C:/Users//Documents/DazToUnreal" in the Daz To Unreal dialog window. +8. Change Intermediate Folder to "C:/CustomRoot". +9. Click "Accept". +10. Confirm Unreal Engine has successfully generated a new "Genesis81F emale" asset in the Content Browser Pane. +11. Confirm there is a "C:/CustomRoot" folder with "Genesis81Female" subfolder containing "Genesis81Female.dtu" and "Genesis81Female.fbx". + +## TC6. Load and Export Genesis 8.1 Basic Female with Enable Morphs to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 8.1 Basic Female. +6. Select File->Send To->Daz To Unreal. +7. Check "Enable Morphs", then Click "Choose Morphs". +8. Select a Morph such as "Genesis 8.1 Female -> Actor -> Bodybuilder" from the left and middle panes. Then click "Add For Export" so that it appears in the right pane. +9. Click "Accept" for the Morph Selection dialog. +10. Click "Accept" for the Daz To Unreal dialog. +11. Confirm Unreal Engine has successfully generated a new "Genesis81Female" asset in the Content Browser Pane. +12. Double-click the "Genesis81Female" asset to show the asset viewer window. +13. Confirm that the exported morph appears in the "Morph Target Preview" pane on the right side of the asset viewer window. +14. Confirm that moving the slider to 1.0 fully applies the morph. +15. Confirm that moving the slider to 0.0 fully removes the morph. + +## TC7. Load and Export Genesis 8.1 Basic Female with Enable Subdivisions to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 8.1 Basic Female. +6. Select File->Send To->Daz To Unreal. +7. Check "Enable Subdivision", then Click "Choose Subdivisions". +8. Select the drop-down for Genesis 8.1 Female, and change to Subdivision Level 2. +9. Click "Accept" for the Subdivision Levels dialog. +10. Click "Accept" for the Daz To Unreal dialog. +11. Confirm Unreal Engine has successfully generated a new "Genesis81Female" asset in the Content Browser Pane. +12. Double-click the "Genesis81Female" asset to show the asset viewer window. +13. Confirm that the Vertices info printed in the top left corner of the preview window shows 271,418 instead of 19,775. +14. Confirm that a "Genesis81Female" subfolder was generated in the Intermediate Folder, with "Genesis81Female.dtu", "Genesis81Female.fbx", "Genesis81Female_base.fbx" and "Genesis81Female_HD.fbx" files. + +## TC8. Load and Export Custom Scene File to Unreal +1. Download “QA-Test-Scene-01.duf” +2. Start Daz Studio. +3. Confirm correct version number of pre-release Daz Studio bridge plugin. +4. Start Unreal Engine. +5. Confirm correct version number of pre-release UnrealEngine bridge plugin. +6. Select File->Open... and load "QA-Test-Scene-01.duf". +7. Select File->Send To->Daz To Unreal. +8. Confirm the Asset Name is "QATestScene01". +9. Click "Accept". +10. Confirm Unreal Engine has successfully generated a new "QATestScene01" asset in the Content Browser Pane. +11. Confirm that a "QATestScene01" subfolder was generated in the Intermediate Folder, with "QATestScene01.dtu" and "QATestScene01.fbx" files. + +## TC9. Load and Export Victoria 8.1 to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Victoria 8.1. +6. Select File->Send To->Daz To Unreal. +7. Confirm the Asset Name is "Victoria81". +8. Click "Accept". +9. Confirm Unreal Engine has successfully generated a new "Victoria81" asset in the Content Browser Pane. +10. Confirm that a "Victoria81" subfolder was generated in the Intermediate Folder, with "Victoria81.dtu" and "Victoria81.fbx" files. +11. Confirm that "ExportTextures" subfolder is NOT present in the "Victoria81" folder. + +## TC10. Load and Export Victoria 8.1 with "Victoria 8.1 Tattoo All - Add" to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Victoria 8.1. +6. Open the Materials section and load "Victoria 8.1 Tattoo All - Add" onto Victoria 8.1. +7. Wait for the L.I.E. textures to be baked and updated in the Viewport. This can take a few minutes. +8. Select File->Send To->Daz To Unreal. +9. Confirm the Asset Name is "Victoria81". +10. Click "Accept". +11. Confirm Unreal Engine has successfully generated a new "Victoria81" asset in the Content Browser Pane. +12. Double-click the "Victoria81" asset to show the asset viewer window. +13. Confirm that the asset has full body tattoos visible in the asset viewer. +14. Confirm that a "Victoria81" subfolder was generated in the Intermediate Folder, with "Victoria81.dtu" and "Victoria81.fbx" files, and additional "ExportTextures" folder with 8 PNG texture files (d10.png to d17.png). + +## TC11. Load and Export Genesis 8 Basic Male to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 8 Basic Male. +6. Select File->Send To->Daz To Unreal. +7. Confirm Asset Name is "Genesis8Male" in the Daz To Unreal dialog window. +8. Click "Accept". +9. Confirm Unreal Engine has successfully generated a new "Genesis8Male" asset in the Content Browser Pane. +10. Confirm that a "Genesis8Male" subfolder was generated in the Intermediate Folder, with "Genesis8Male.dtu" and "Genesis8Male.fbx" files. + +## TC12. Load and Export Genesis 8.1 Basic Male to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 8.1 Basic Male. +6. Select File->Send To->Daz To Unreal. +7. Confirm Asset Name is "Genesis81Male" in the Daz To Unreal dialog window. +8. Click "Accept". +9. Confirm Unreal Engine has successfully generated a new "Genesis81Male" asset in the Content Browser Pane. +10. Confirm that a "Genesis81Male" subfolder was generated in the Intermediate Folder, with "Genesis81Male.dtu" and "Genesis81Male.fbx" files. + +## TC13. Load and Export Genesis 3 Basic Female to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 3 Basic Female. +6. Select File->Send To->Daz To Unreal. +7. Confirm Asset Name is "Genesis3Female" in the Daz To Unreal dialog window. +8. Click "Accept". +9. Confirm Unreal Engine has successfully generated a new "Genesis3Female" asset in the Content Browser Pane. +10. Confirm that a "Genesis3Female" subfolder was generated in the Intermediate Folder, with "Genesis3Female.dtu" and "Genesis3Female.fbx" files. + +## TC14. Load and Export Genesis 3 Basic Male to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 3 Basic Male. +6. Select File->Send To->Daz To Unreal. +7. Confirm Asset Name is "Genesis3Male" in the Daz To Unreal dialog window. +8. Click "Accept". +9. Confirm Unreal Engine has successfully generated a new "Genesis3Male" asset in the Content Browser Pane. +10. Confirm that a "Genesis3Male" subfolder was generated in the Intermediate Folder, with "Genesis3Male.dtu" and "Genesis3Male.fbx" files. + +## TC15. Load and Export Genesis 2 Basic Female to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 2 Basic Female. +6. Select File->Send To->Daz To Unreal. +7. Confirm Asset Name is "Genesis2Female" in the Daz To Unreal dialog window. +8. Click "Accept". +9. Confirm Unreal Engine has successfully generated a new "Genesis2Female" asset in the Content Browser Pane. +10. Confirm that a "Genesis2Female" subfolder was generated in the Intermediate Folder, with "Genesis2Female.dtu" and "Genesis2Female.fbx" files. + +## TC16. Load and Export Genesis 2 Basic Male to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 2 Basic Male. +6. Select File->Send To->Daz To Unreal. +7. Confirm Asset Name is "Genesis2Male" in the Daz To Unreal dialog window. +8. Click "Accept". +9. Confirm Unreal Engine has successfully generated a new "Genesis2Male" asset in the Content Browser Pane. +10. Confirm that a "Genesis2Male" subfolder was generated in the Intermediate Folder, with "Genesis2Male.dtu" and "Genesis2Male.fbx" files. + +## TC17. Load and Export Moonshine Drive-In Movie Theatre Set to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load “!Pre_DriveIn.duf” Environment Set from the Moonshine’s Drive-In Movie Theatre product package. +6. Select File->Send To->Daz To Unreal. +7. Confirm Asset Name is “MSD_Ground” in the Daz To Unreal dialog window. +8. Change Asset Type to “Environment”. +9. Click “Accept”. +10. Confirm Unreal Engine has successfully generated multiple assets in the Content Browser Pane. +11. Confirm Unreal Engine has generated a “MSD_Ground” map level in the main Level Editor window, which has all assets placed in similar position as the Daz Studio Viewport pane. +12. Confirm that a “MSD_Ground” subfolder was generated in the Intermediate Folder, with multiple “DTU” and “FBX” files generated, including “MSD_Ground.dtu”. + +## TC18. Load and Export Genesis 8 Basic Female with Pose to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 8 Basic Female. +6. Select File->Send To->Daz To Unreal. +7. Confirm Asset Name is "Genesis8Female" in the Daz To Unreal dialog window. +8. Click "Accept". +9. Confirm Unreal Engine has successfully generated a new "Genesis8Female" asset in the Content Browser Pane. +10. Return to Daz Studio and apply “Basic Pose Standing A.duf” to the selected Genesis 8 Female figure. +11. Select File->Send To->Daz To Unreal. +12. Change Asset Name to “BasicPoseStandingA”. +13. Change Asset Type to “Pose”. +14. Click "Accept". +15. Confirm Unreal Engine now has a new Pose Asset named “BasicPoseStandingA” in the Pose folder. + +## TC19. Load and Export Genesis 8 Basic Female with Aniblock Animation to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 8 Basic Female. +6. Select File->Send To->Daz To Unreal. +7. Confirm Asset Name is "Genesis8Female" in the Daz To Unreal dialog window. +8. Click "Accept". +9. Confirm Unreal Engine has successfully generated a new "Genesis8Female" asset in the Content Browser Pane. +10. Return to Daz Studio and apply “bow-g8f.ds” to the selected Genesis 8 Female figure. +11. Open the aniMate2 Pane, right-click in an empty part of the pane and select “Bake To Studio Keyframes”. +12. Click “Yes” to continue. +13. Open the Timeline Pane and confirm that it is populated with multiple keyframes. +14. Select File->Send To->Daz To Unreal. +15. Change Asset Name to “BowG8F”. +16. Change Asset Type to “Animation”. +17. Click “Accept”. +18. Confirm Unreal Engine now has a new Animation Asset named “BowG8F” in the Animation folder. + +## TC20. Load and Export Genesis 8 Basic Female with hair, clothing and props to Unreal +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unreal Engine. +4. Confirm correct version number of pre-release UnrealEngine bridge plugin. +5. Load Genesis 8 Basic Female. +6. Apply “Toulouse Hair.duf” to Genesis 8 Female. +7. Apply “Shadow Thief !!Outfit.duf” from the “Shadow Thief Outfit for Genesis 8 Female(s)” product. +8. Apply “Dark Fantasy Long Blade Hande Left.duf” for Genesis 8 Female from the “Dark Fantasy Weapons for Genesis 3 and 8 Female” product. +9. Apply “Dark Fantasy Long Blade Hande Right.duf” for Genesis 8 Female from the “Dark Fantasy Weapons for Genesis 3 and 8 Female” product. +10. Select File->Send To->Daz To Unreal. +11. Confirm Asset Name is "Genesis8Female" in the Daz To Unreal dialog window. +12. Click "Accept". +13. Confirm Unreal Engine has successfully generated a new "Genesis8Female" asset in the Content Browser Pane. +14. Double-click the asset in the Content Browser Pane to view the new asset and confirm that hair, clothing and props are transferred correctly to Unreal Engine. + +## TC21. Open Daz Bridge Dialog with Empty Scene +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Select File->Send To->Daz Bridges. +4. Confirm either pop-up notification to select figure or prop, or Daz Bridge dialog window. +5. If Daz Bridge dialog appears, confirm Asset Name, Asset Type, Morphs and Subdivision options are disabled. +6. Click “Accept”. +7. Confirm pop-up notification to select figure or prop. + +## TC22. Install Target Bridge Software (Unity) via Daz Bridge Plugin (Daz To Unity plugin) +1. Start Daz Studio. +2. Confirm correct version number of pre-release Daz Studio bridge plugin. +3. Start Unity Editor with a new HDRP Project. +4. Load Genesis 8 Basic Female. +5. Select File->Send To->Daz To Unity. +6. Confirm Asset Name is "Genesis8Female" in the Daz To Unity dialog window. +7. Select Unity Assets Folder for the newly created project. +8. Confirm that “Install Unity Files” is automatically enabled with checkmark. +9. Click “Accept”. +10. Switch to Unity Editor and confirm Unity Package Import Window is loaded with DazToUnity installer unity package. +11. Click “Import”. +12. Confirm creation of “Assets/Daz3D” folder in Project Pane of Unity Editor. +13. Confirm correct version number of pre-release Unity bridge plugin. diff --git a/Test/QA-Test-Scene-01.duf b/Test/QA-Test-Scene-01.duf new file mode 100644 index 0000000000000000000000000000000000000000..f66bafb3b3d4538aeda6e2b5faf3a0851cb110f2 GIT binary patch literal 117286 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%0A*R(B~c>=LXxS}MDfy|D2cJ;~-d+77WVO9pk z#a3r*ew{HKjf>;C{K_vug(lgUh`Be9i>6LWncW5GXfM8-bb*Jt(fh6%CicNrxpFN8_9-eK2i+T^uiJ;Z3*i;UZfmeLQIhJ}v9+kS11XB9YC> zsK#eQtomZ%FPy>b%oP>}l}5G{1ITq=&!Bf)PtnPh4ILXw>!c5}e-IXe;XS!QM0bDs z)a!y_Yu1R7JgeJ&!qcXG4hAb;b%-xcKu23;EKuOT{?t|YM zKQQV}dy&LrMKRT{%HzoO*g;PNS59L0H^dU=aA5i;#G(^P6&}0+LahFLcVN%GLv8x_ z2!;Fw;{E<~umeTFdYj8n6oK>6tdAj#>o_pq7A5jIz9{4>rqku3wGzL_)JFh!U?cg$ z_eJac#x7o@nju$K;B2xUqoCkpXlkxsdqNsU8^dB13)StzDg~Q|Fn8yu%L;`OOeR{) zsLwFs4w>w<;#$u;iy{`+_8w5s4mVG)j$zZ&A_YP0l>j=!EV`(!Ox&s1$8q;ETMfg6 ziEx`(O>uW(^egJI+!3+JvX_fZ%tEM0=#aVOc3B-OZDslB!$dn~@-_FdB_odfD!qNF z3nyc-JGcU1Ez2EGM`)LMXVUfAC6)2y*N1gfYcTtt0iUG6*cG!l^iq$ByFG7dNs=y` zWLPX@K@xP>b~(5>aWgXs9X};Y^qy*4P9=dHYtF7W98>IEk$UF) z%owf))j~}X>|4v2v*laF*A(O*VY zAv8v^+#*gain{#je8_Dz+@-yJD)%-rzuG3C$N;m1^g%D8yxaf8S?|L4F#^~l!vG`#WRb?5cg1#)pxDI+UZWMPn@X=rX>Snapx z8)bZ)nSN&p<1?<~qOpdT{4xg@kKgI4b2P`$BMilr$B{j!^?`kNU^WY?fv}T(UzdBJ z$Q1mK%f(s`l}gf*G*5rA3cYO5Z{81>cRh8xr>y zH7s^~UY>nNbG;mYH{2O0u3g>e9$%12*Kuo7iO<0rRMm?Eg}9h%b-Q!?R_k7tg@8Si z=ihnOXO@5CS)0Fj)MSZ;NnJuzsy z8Ke|H?)*%Bcn(SlO>v^MWPep4`{gw=`kjgHF8a(nLv9zZ2 z2>4q=$A}Lcfu&)t(8wJP=DrO}mvXO#cT7($swW(C;JRTZZaZv@#m0Lat#~RuMixz2 z8;3dB2n%>BfUCh<$}bFGipBRwmi84O>ay9|kH2?;?)4AAoTOHJ<)d%^1PQ2}AJy{k zxuU}u#weu^2vl;}Ir`*6L?ll*ve^qJfX#PepM5)@->~bo_#SIrOKNqZpqu1&0O4iV zukMbZrIY#K4Zhi$j?@mF5kG#ATSM&z`}xX&Oml)P3?;gL5HZKI0r&`nA$b)Bm^`sf5uv~HgBPZx=GMd2n9n9=N|xckb35SQi#deJ9O0= zBP6D)EFMIExZDQz0Brt=ygb1I_xpmmyzhvX1Gxo)u7$QAw`i-$)YuT-CB8B*+kY@G z<%b5@JZu2ZYj!Jqtk*9sB&5NgAtskUBScftOV+IH;=b4bu3B`|?ViP7Du-^{2l!#e z@vvH8U~2-d216qEnDps8QK5xX%6bpdvONarTyOYzF`wg)}=cAC2qkxhq z9ZHCWm30t{q1+ZPPJxYqGGa`y^89Sftwz!3CZ_ksG^4?i7mwe*KsFOo{P<2q7lt{F zjss_z&Nm|2bzFl5x5-07PIK@KrB0tY4)4WRkKrvba|GHH{3GPPeNpx$ush_vRcTky zH<^K4Q0^G#tsYFt*bCqr!f@;ZG7T48R{|$RLS>k&(DQ;3HqWIx^^G7Qx>``XM#8CZ7gW%D)2k=6xN*U#thcy680&_!5A9}_VkE!^ z=XL{rwi9S5f;}-L>M`UunMbg-9Yj*TPjmptjAqlQYh?ob>fZb{|#%A*qqp?*)wcG zW>1B0V-#Sp(SN4%Rh-FmSCStBL(;lY3Wez0IKPfqaYb*B0!VkGG^K!LL=|bTQRP~G zSB@rKGNh#J9ckzv6C20l*Xt*4si*XfG3O`y-@ZdLAM>R62iFSVY zH^xP{AsLx;&CQjf%WQ%FSv_D{2Gr+pl>XghxLJ5N?`Io^=WE=e+y4I`E`*x30`tWYNLq|6I!Cz5>T5oTM}(i$E)*jIwaED_%ZHT!7 z9EK+XjOkUW#ChK5id`--x(vE4kHm9Ko$F2Zdg4VJ7Bh(XqAY=T3IfP6n9u@U?g!o3wcvujiK2d zotz(;_r1cl(pGps-@PDTuNNwcBiIHfS8-@$(gu7@yT+NzejeV6eW<2uql&>pbvCSI zv2GQckg>?{C7w^FAqO;$^03`EF{~T%UWS4R!TWF$G`rad_@wd0&u=74laALffZN4z z*bX#r6{JgzFd?9ulTeO~h#3yauBwE>UAP0@`0sbq`x0MKf9+W8g*!FfIpr5!3*AL& zm+tNCQtEA5lzgM~NWp0ZCtR+M{yzXO+0Ihu-@vOl-Jt##@cKx^CAzWhfY?fY8EmdD z-vjQA;#hhP#Fpn4nHGy!4~~%+!&UDFa&82ntRc)_tMI%}s-&vm@O`T!dn)$0yzLPA z4G;@Ti|9|MHF9A!wm*+V;LIC)naKtsP7dP_OPae5q5ITWV^#yE%3S<7xs~+OcmtAr zVW+D73Y)9%b3d2;Ox!~YmwGFNC=0NaBR#&Vh74$561rnaCS>-&#=f5Kr`PQwf(3`W z&s|FJY2KE=T!Cc%jvpwq3{E|)P|2{VFZ3yt)+&Gz=)k1GxI2+CWR&xOKXPjsvgH}@ zQ$&n2FF(dUA9R~KY9-tv{w7{Ybf=chJ!06EwMK9zGUEY=iYZnzrqF-F@NBb1t#q>F zvilc&1h+MuibheaV&k+{+2J1_43i{|MkBAG{d)z4-@+C1R*^@!GW@1r)&HVjxaI#& zzv}+bFS_T9bU8WlK~i!KZazG`C_Q=x6g3RW9CS?NW8^M&$*nql@po|jwY zdq-}o_oaE714N-s^2kc}x#oC2lB&>jH7&C7Mk#}J%MYLJ2aBGb1Q;T$MI5dr&P!0) z7JSovL$o7Q^ye^^X2Y;iT{ip-z=3`E>?`;p_!szEqjM&W zjkmPRgZf+_f!7cF37bq*NB7ZDBRwJLn|)ts9nP-a7DPP9XiDqn(sSEnK4WZLdJj?~ zBzB&=cub1G$aRXr4oM8SM+zR;Ge)2UHf;~j*T&W7^44b6FfLmB{3)wbmz(~z5+6Q6 z$*#EDD%PL^XDBfZzDaZ|d5DhNrf6p%kHZW2hKd=-7BaNKbpSvHCgN+}x{sG>PbmQ) zE3*hso9gz7_Y?8Dfke&5%pQ*c&OX~-4_OBt9Kg3?pA4T!@j*?xH@|GBsSWKg<|(0^ z1TD&IA&Pr_;Kox?s&3f=PVv!j$}0+TGnqPhY|vo)s1L5Tr)U0fL*0W%{Zc5$3wi>;$ww z`N(sYsq>G~r{{uIHqsSu!(Y@niMp+=Cl{3dyL}W zJv7ZG%b>}0QLK%;y#Aiu>~%qV68}pSjm6vU(e;>79NF_|}w5BY#!cHcH)S-fWWl15(x2uCbK473Vwf!Bs~C zJb1v>ftc)b6hsR{X#+cLt`_vTB!-*U@nS%+-Q>@Z^TAufQXtYMj>+jc46?N0l~zrL zEsAfSd%r1Jq4eW)BDZgLd_I*l$p2~|C(tWaXJa}=T`nQwJ{Am_0yl?2MTR|!^wT=^ z><798Q(3EQfJ_wlY?T!D&&2XeTcE-9R-OQ*+SLnaaA5!%U!)_B`Y9i>hN1m2XEhU> zRu1W!-s20?(Q#ngHm79#GZR9PDDTo?+3U3239 z0lSc#p_rwnM%My_|0rCz=!fq#)79QON z?NZl8DhUTo%ef4=L;rNP%#h9-C_eK#zI(B4QGm@z(*$ZN4_UG)?AGSsV<}(TMEM$% zF=qdY#CN%5J!a~Mx{hWvqVDUL;bEsg>m;K?uxPsP&R-u}*78=9v?yST!em9d#m$LzG>oJkZC#(C2K^r3iblr*mOluXj8q-}`-hdEyRiSy#9p z;3w|52ttY4vP3SJLQ3tqwx>)fOFcQc*!8c9NyGK`LbiHv^C@hgqh{i%s`+ZdUm{c{Cyf3tUrUup;T3l=d@@BJ7^~PH-#$8$6(2-Eqn0%AsxCOLE5KTe z7s0k4)UZLPrQlwqk!uhVhh?h)OlU^z< z36OZP{vuvr7P3SM_=%ucKl;P5a^eGeHxHnDDBRqXWnN4%RPthfbFbN0Z|Jr5nOo$h zgxxf^bhzxt^wzF`@n5m{A>j4wH^Nw9_xTOLNfeNK?HomX_!swzZh;4KuT>Af5)(4? z8FY);bpTPLks_05QjyX*7>AxJz-~->cQ@SnbUq>S?1!6+3+ZEu!C|wWSOW1qW#WN@ znQ?Rl+R@@HIC0~kixSODXtOXFEJEF&+miDwK+U@ z-No{dBpD0rdsso-W<#WRBDMdrw?^3qFM$459iTPz-~6rT-~JY|7RKNHR(kGRgF4+> z=^Ozr5@eoh;3~~mLk*-#{^nfb*8YvJg|<`HycGDBRN;)Gw04V!4;w%1sgj>UB4oLi zBbH(U6~mpAX=YvZn?6PBp%h*=8Wyrs>R!Le@zAR~--qb*!`1O{J)g(No$PWMF*a*A zCYEG&BPawOV_ab@PSNd!i!qawb`;;|_WwR-v}%w%rwC_Fn&~X5xS;wVG=x$fg?R`K zFDtyKUTM@IEee=4Rl0@&(iF$Z_%lU_B>SlVH8Eb7IM46j4Qh~wcSc6b-bT~uQ<#8v ze9T$NY?pr_V?{#c+Ow_BbC0J3dViZo=MX($&c2)cmOjL% z@&J^mG?g>=&hnMhToc`#`WxRx^0(k7Y0Hh4ZerPNA)#sR_KpcP=vGjDTQRH_DNPcF zVKvQa{Bt_lkzzkq{Knds#n84hRFw(7EK-_(YR5`7~9-K6RAd<&yo}x4d?PO0jySoBIq^r@DT~K&BKXGH6<-3`cRcsyzS?a za_DR88}hXT1%sQ4okWU1hbALUYnDN%r_r6wb$4Y(bPp!>bmkPUL!AzHx1Bdb@Hcd` zrJ_Q-uWLAGmsqQBUWm1ST{ql%1nPEymAD@8MvSl@`bWKAG`pf(h%Pv9);6B0V^pz# zFR%OeM^^A$6WAn{7EVQmuLb{Map^9A#fJy>8)H-$TX{ubqeMrG5y*>sHmCHCAszNU z0>XWS;LdYxrSpii-)qf5&6m6M;pQ`E|@rj3f01WAxd=AH54>^)J1vsI2-Bh6tu5 zg$2OcjJ&10_C-q@Ci4E-IYDZt`jv2v)r8m?Tt)&37b}Y{Z1kMsmgWg<^2SiI-p>S9 z%_!5sNocOCyN!E8U$14Y)XMFym8Iw=+;e(<94eR!xh8DtVagw*K065rqUhJON zJp|Nr7`(&NsYpGvlQ)c+_rP7hhAz#}94Dm;H$7A!i<2FUG>G1AYjSXmD7MnUS9TY3 zt>eJv!J_~)Pd7BNgPIiE#PY9o`Ge0mg-wjEjp;w3=5>#v zHqlfZT1$gLLsTW)s5_%tt!T5(>6Es~VLX3+Cd~?c)81={s^sqy|8z@O29{a~8lt(r zXRiUr3=ba7hAXR5VP5}yr`G$MaV_QjVO;AHLeS9&yu5>>d87XH7(=XW&yD60C?A1< z3m@}<_!r<}qI*#K_|9G4&!}!U0~8>DtYft>)^oBRG@EqdkYN_4%H%gB=h0OSm_ecv z19$XQ$1OTUQ{gFDIp3P1L`a-!VaZQOq~Ah>MTVb<=9`ct#DB@&lX&Vn6`aE?FF8Z^fG04L!It5ZmcIK-dkn;VSwEtjF-rBxE<--HDwny zV~9SL%zQGWoX*Ny1dj}G^5d<6DKoGYznK7=N_J8(Bwn1N>|U-wx5yXif(S5%-4bNZg)L6YWYHt_ z`RW1{`M#BR)=>PsdRRg1O>34~G$05ugErC7{H|KV!X^=*2;Iu_qfXN<@>3k>v{#%7 zc8lB^Wa_1N7TU4<;Dmhe@h)K`vmzU4ZMr~xFhy4oD0GGVC3Ib5e7V;ZA@Y|6Ws_el z^l)=35GYyu6kAnk2cimKwxUFy#~zuzwrpxCviG%jVWxrUd=*1XoF2pCmi>ru~i@+(uB#3va*Z=H(8~cEJ zgBw-JHS*<#gAf~0MiL{}Uv#(-9Sec6;)GR9{Jj>9&J38im1}EY%U^nzg97fSNzzw} zNaq@9?8vW55g$Uqd;#ohkt9;|5B7BdU|&I5K?dAq-Vb@&y^sL0OT?4yp-RFSd&0Ds z3d$aPV~*yHc990q?z*@rWw$)VnC? zUXm%N+cp~|hjL+gGakvZ{A?IJae@A;=T4InCK9IARGqz(tF0Y~E9K6nkCAeTR`l&= z8%Mat$+wipO$IT5Pr*j``Kuo+KB$EeT zh24ejMn=;PDd3k{T=Om~f9^f0T(4xLWWGBbL?^ex~(&=3hXi;P%I=f{QRS0;1BNE>TjLvP$$HK9L zjYruOeTi0JM{^ck*m||3;r>(yxQAFys)oJG$PUMH4$$9tj}wRvFhQ>>XW4WQ32Ri zHR(qbCN5Okvo0;AfGJ|a7{$=gMxIZqX>~q?;er-jsp|n6@O3x5-~bPdfn)_9koUsb zn$(OG@)QEW&ATQ|JDP^v%@8nkHlmmc)(L9QmxRc??t_gJt=!Lv*=_TKXlt-es#qu5 z*g1NYOg*xGP4x(MajnRlRS*AZSkH3VCDJBs7LH?n4+r64Ait+)ntY4}yqvQQU8esP z4fx|9ix^IJ&u&nk*^iWw6k~%C!t26esjD66^LdZ{k(tAb#e`iZdgpwV8hgrws2JBP zx94$N$hX5}t#l=T%TVx==m_VFHVp6@f8yQ3nO51yF#Vym)^p&s5l5hYNv`?`IdyI=?-1KxbL`ut6@W8+N9f>nwFib~N3 z78^bxW||^=G*npCSz3Qc)D0kt!NjZiOAB|lCvYRkSg(H_GO3Na5hC&RWWgiE#ysvB zI&haT#Hw|t;(zITmD%F<;5>Sqk;2u!!eb=mz{u#+Ci-k~|E+55^IMTv`}?ZczVMoz zo_15I35$b-VQlL8B!VHf1hXxfyV1xmfZAQ{Ju+MYEDW0C*E}rfp z#Kl8H`|D?^rTZ6!b<5>V6e1_0ZI-mt0zBF0aqoTZ=Yf1|DNnbbwJ{j+C#g1No!$=D zt7K!MuQqSghY7PUE>Bunnh`Y%4y9xGKd?(fWb;j$A`Zg3fBRI8S|7?-Q;aT1bv@052k z8{H36;}0!5I{EyLJ)zCPxLu+R(+R7%AO_P5-HsG5km|b$H|izWeLm;G7504H%ID;S z^Ugy+-HU~5A@{-pS2f!i|IW0ii?mic0SdNw{9y}+`eyouc1jK?fNlOEfW5at3IGaV z9)Ag7H2?uDq=UYG5_P@<1|REMgEdKCec544~o=&XBgD1 zM1`0a5o{YMf?=_t;^&Hr%0Kqnc4SH_!r*@$x<&n&Of`4jO#)4aEkPLy)WC)-VATg? zUNkUc-^A<$*P!m2ubx-rXPQKXbwBqWKU$ytJbmOD_C2^O&ai7W?LB_cz(%u~5wgDS zzTfE>i014rks=>VS-}i+shK3zpo4lZULXCDW4pFk3}&FXf*pd!*RZeY34+2oGS~b| zWLOt}1r36?GNT3!ih|e|!#;U;*UV=x1%M64?$6^Pi?kZh-n%ICIW(l)Z-1BSW0XQr zjOE60vAac_sIe>?9Yo+>vNeLY1T=465ZM+0k=54$5gCOx z+H{D)LB$y~tCi#M;AF>&?;v!b`w<%5Z$FIg@V6h<{x?62I*h>7qX0$@wg}S6u(Lph z*G9Fo;l93r@aT<`Kv+7mC6b>lavmNY6UhY*UAX~{@^lWrq99`QxD8E={E~LvUrreL zrA8{S@5MkpE}1bH5QZKzk0dtOQ_$4FT(3zl|_J-HQ=e_c{;asRzdE+r zEmy&DW(YLGGDyN$D?eYn(`|!vqjSCY%Foa}^j0BXoR6KG(=JeCX(Sb=?92^-d>=`2 zK2b%G*hud8-8o6D!b~^Z*kDXFKr#>v&Uteln)4y05->-cmfR#yR{>jQr^!eg8K1MM@VLOXa zW5uygk*tphI1{0!=#UBi3ovv3pTG>x?GIqam-XMk%>BQCnTsJ1n2oys1G_4WR#_S{2MU)PV5N;W|n4X?CoUBDi#q?7b0c2)dGKwU>SB~nBraaEuAfBqEo%!wCj~6z``JN#vl~D<-3$v!5 znOaEOM3)lv3@yZeAaJIKH5Q4~*7==RqijDxQzk;$$_UyIuxU#M-W_{LGVIu=NiC#x zQx7xT40UasC1v49qj;%Bfm$b@<~#gB$C6@yih}Ac3jU#mx#X$-aQj0G8v|%z5X$qZ zcglX>LiK!<74_NXMJ^KJTN%_@8-#I>BYCg-oVOQ8{ZLK#u_%da1wMcgf1Myw2ZAxm z-3S8!7>ho=1GR^dJRGs`2Ro2rm3%UPZQb-e?puAjj${xJlN|yvS=kFFGhE(ksc)0s z&Rn-p)NcxB*5ocCLUoSQ*Y5pszFil~zxH9T(y1*^8HOTNh1WRUzxC4`!*)c09$pf` z9Sz_Bz|U&o!#$%5G3tK^VLQKsFenFY_dkU&KGe~PKw4%av%wxY9{#E@^klq05LqA) zkxdslG};PKufBOfWUPN6vWLsdYa2Q2!V!T_y0U#Nu&N#{6%^+nj)`RUBz|*5MBZPZ z2iVUtYqaqy(_j)5Rzqn8eVcUe1>>K9-;$yiiqQld87RuZLCk62D1#)P%Xb{Z|Dcjd#9uv=*kGK6SBMU*gO;Q z_@NIennMjR`gfRoCqob0hGO8X@uo=7{H5Fr3DXmJ?MU_{0!Ehg3yHPp4jXRR`9}PI zTQ>fO33iAyY>mx&kc@wp@#nHJ1BM5Mm(D4ixIi145|6uV_1agvVFeiYvc^9)`iO#& z@z!RkW(PB1mVnF-P=`>qP}VYO!Ue0oB_ELzjJyeBxEpS}$h`qGXBb{XGTWVD%ELLW z%ez18drjKtLrm_>)|5$#+2@YIg?!9L@Z~hKaG^@1VU;|d0&WK3LrUnBsOnCq;cp-o zOvv~r5R--0IQ;k8kHs;U2?emVSa^=oMrknyq$f5S$OP-F=Y8DPu*FV@#2AaGxs zgZaz)8vS3^*W2$xEMLkgztMK`eemJGW)WaH3XqlTHerv{sI708M1++d$UEiv5m%gz zhiRCr4)r1O%LSIP`6xoJP^(|=Ep{&mGh71)-V|q_WVF(lC->+EV}~E-Vf>cXP9$*E zIL*O~q9~Hows7D8^XU;Vu{TLX4c$Du-CuW?i3K6(noJLMm`7{nPoEG@5T5#^(#OLk z!)?pzc`&Q_=3AkM<={`o9W2Ei9+Rs_xNcWo_QptbjR^9RU=Mw6EAqICREwY+IB#h+`8I&BC<{MC4s{|HOAiO{bl7B6^B4;;|aJHDru0 z&QTR?cNs&R*2Uq|9rq2gsk-iW2rT8*R~HWuFUuGBuqL&_m8^J+h1vWMP&VcK@1QKh zye6IZ-!Pe^?DN@`6&voR2kt73L0s6d&u&OFM})S>^zqZkd)pnIDl|&~G|R%FeZm7; zVXTZ#0BClv7-0DV%?8F=@5|G|m)hv(_~M5k$obDfdkkbG&G2b+$oCA039<;bRO$GG zXJ1F~wC-}+7*NKHzh#r>NV-tfJ|F|HrkM2qjsq<%I8OB8#HT5umLhaPKf3I_9BUEv zI?X^_+-0LDL8B+;c)LX{3$^p6CO6<<^B0LMly)-%7}E{oSrwN}?m>oyqWA)Ue0bv> z2+ElT=zgt7jxbmon(T@V0J>kf4bWxo=C3_aN=d$XK`^xkjV`s&o#5WYA|d6@ob0pC z2e6F@5UBGU>IY*vd_%ubVIA>B58eqy8ljtT}7qe1VX#Xc1GX#5U*CN`a0Y*R+yGDTSebxxJLOov{ z?Prs}nRl9%V^62G>qxUz<=QV-rFJ^kOfm`I`5?SF^5wiXk%SOnf(ZlO#R5&R!h(jg zK4^S6RND(SWV6+gGWy5(@5#s3uF>#S&2iS^;e%v)pYwTXPd+6r$XQBwyy^HpL@zgm zhN0tllJ~8C7cJ3&;yOqGL|-WFg9^ohbC>;cHZL{wgPmAu93k zv;yIeU-@Ux2E>bk6#iy|*??>Fh^gx+u)+AEJO&HTki-PDff70NOdd?eAwBW^CS+Iu zLiTvmb7Jr|`OWu9J|?`l2pTca_et{9QLroPW>WNBB(a<^-y;JrD`FoXXz~Rj`=sD{qWP{IUwj|2 zn6%%`)f{eb2_WK^V0CYS>5$SM5&PmvN05+^ttmJSX1L7$A3E5+3Y*$xj#43ioFgXE z+eE|?${mw6(g#pkd`a;m;aOTh51$jqYsz_Yu%!eD$OH$zdYbFBcISenrrW*#95eQq z^SMCpEWANaloBx06U_g>e_d4?tbSCk8ayL(4PmPLpL|Rl$j7eZv`F8TS~s{U;5c7j zOJY8`GqAnG{qUXRsmw8=w0kG#zfjpbgs{Z+0i(4I!@`9PtpAk66o5NOhkxuKe@S8n zSMdu;V~YNjn|tK~pHRM2y0J3AdP4tA5K9+0;a<;^Wvxf|))8Y$?J+1Ly94wUwrh<~ zV$i>QptjH1=*bGbi9YEdd6GQle;_d>5ZoLf5<`dFwS7Ti zn?)$qC~BBwR|Je?`=!jUlMy^6e{gVA{v_XTdzEhs`2&X8(7r}M8>*7~fIF_-&+{Rp zUn%>`Tch}<@x}wJuWS{VR&k|HnBdm)*%?Fx8AL?x-$Tdqu!Xj34WvU^zXv?b{a)KRJaLuyJ8WP~p5RBzy29IirsgKYQg9v)`VFwSfhmPMtAVwVg561Nva5 zD4Q@rfg;|MJ;!SA~!4xHuGqymWBVl+5C>(xdGiSw~KB=3HJ zudF;Epl80BG*z$>_UL*IsNSn9)RB-uQtCBAb=jB$JRFNAiUguqu^mXv+ag|Sh8YV2 zQysWWlVc)!uX+6wKgnQG`gdCMJ%IV7q@o~ZOy+=YWl!q5F(!dJS z;!K_j`CW(9`Pf?nmknsaZ`!8>pV#(8p`nJV-;W#K;6MOmGM%ZJKJNiX5s?K)?@3#6 z;rVmCs4w7Y{Hy&;Z*AFJ59J%R;#@MQWX*&i)6@A6<&c_>9O5Oz3cv*JL2o|XCx`zN zhJ88u4a1y)FpLNgy9dHBN4d*&aSV%(96DmZWA|fMpNv21FZmO#T^SQcqMMJ9GKZlU z116M3Os_B!E%5$?VySyfoo$wiPpih3k?4J_5SVNP8EwjCygn>UNCCOSSn;e$z2=`*--CWiZ1CLTQQ>6^f_1zA!eM z6TEkSP@@e-$zSXwPEZ#cQMa2m!`(eEuh%GCKBpBv0u=C%e4T|{^9CT6-VFVJU@|nA z(z<=(%)BEa1ZXegsW?V;TNK``H~jPlEz6YDhjuGgQ6%v0D7|_WIMn0|@YwxDp7h&* z#Uvp;U@jA3`1zGy6Pn0LL9+Ui-`R5P%5=a@$?VNRzuieKlku#5Q+xp$WFe zSlX9C+S2-_+o$vnLm!3OB(^}vf6z{Cd0r^k?;Ubh6nNY=I~6>?Isi zW9UhQ^~%Vx9aMISYWFKP)K|;Pu8Xy;emrBfeAMlc6FAXf5Q5FK%&zs$5Qkb2MlZS` z2k%8basN%=SW;1#tpNuxmJGuItXYD(uxCWx1{k50KGGReBkl6Tq% zwC`tu?fZx<2`-#;z`4SqL56*Ita0B6ODJ57WQu$-68Cm3r1m;M{vJ}N!+>CzJfEh(X`UJG&cRnv z$A?YrMe0XYT|~)o!qP{SQf&Hc`jZac^me7$DubZfynf?FCnIY{RPILE8j7Og208oS zs|-N@-YID5MqCKRK$jT3$1!|iXVO|;(kD#w+^LqLkDH({(A2KEIwmCY4kF&PEGNlG zhvbLkm)m|x{@H;z!LTe&Ovit&CjsSqj{R{})I9T03E?@Pzlz- z%BX9dOh$l%0h;1u(C~qvP>Fs)!vyfXR5FUs@dq;m!3IW=^S2WI~Dw z73^rDG=eR9UI?<@;?g|wOeq<-nKHrK;iZ9uaI3^rs~{f-_fzi(es#2uX3>?2A5gx}Lp+oGf?y7}GIz<6iOs%Bib-^3AX!@}zweGv zG*O9I%mCHqcKSr=yPhK4fn;pKh0$3ZD`W4SE0NEmD?!U4Q^fLw54UHtYvJYHw0^Wj z6>4NC@esP2ZOu50i`ySs3Dc1D=Yn$CvdcFsAR*-Y=^d)HTapZ|yNUplOFy57EUp9q zjyZZ>vg-sbFv2_i*ge0#rpoWZg(obi)?3iIS(rjPc619`EOB}OI%SdZv6g+@gDd1# zQMY9*k#yLsb@v0Iru&`viMEON3EX2pFj%FAxk9M7U>SrdB0~qn*?I!&myrQOa_CU< z>SlyI*2y{3ES47awzj3jz|+y*5aa#14U2fg{*g1P=2qC0kvKIF^35YxcgUTwGb&~@ z=ISWLXp#)vq+|Rl+?!V25i^ePx5lyTCR~0a90%hV`UlFO0Y$E_k&r@*=bI@7gy@Qb zIOL&K>aZHtB7L`M&1Z=*z7}dz*wKz~U-ICJfKVk-3u2~HSIP>W}-g@ z_Y%C<#5(s8;QhYU@4ju#@-AvX@+mvQIm zA~4y*6_58jZ!RYTPMh_jte)zd~TZz!M>VW-~r1$@(|BOdBIv%Ps^^%9p7%@br>M_TDkLQ}PO6 z!+N%dW&`|RRE~1fiC4KbDMq2F#=D{M#JKqZ7yMoKjdj*9uZ&jW#Vd0`Qs;e6ljShG z;fSQw{xzjkkl;bZ?$4AGVV-)(jil8s`5DM}4+$~g0heECe6 z78~KQ^)Da=h(s3VFvQgw;*#@U`d}Q_5<$&_+%pMIprEuUD)bRbnW)Ujc?}wfRb0BO zpWbh*>#w7XVhMd7aFy3fL^Wa2`}YOq&^(Z(LAJjvC}jx#$Aa?zhovDWW+8=|4s3+c zi0t8LuhL$y$OoaF8-#3{Pe@>LQgV7uaZZ}+oQ4Sm6+UPd0Vb0Nz{w=2Q3bn6aS!R0 zbm!M&DR*TC_Dv0fEF#2MMXR}PQDtSt)`X!+*g8f6IRn|^Qdc*-m!px%Jz#5DiE0rVCqz;mF91o zZ16`iWSSjmaIm(0eM3-?ER4)$QtZ6S0}O^;mH~rdn#*-#h%dpg0V0g+Q08|LIV)PH zaO6R$zE<^K9_fnMJF$2<)$gRNVD{R)K#V8djA={lMaCtE1KP~l2e(Iv68oC^RhUhr z8Huj}e^ptBA`k!y8C;z)i}p%{CGH+s1`O}U#J`X-_pIzHyU!8Kdd_1}cHCc%rGS_W zC08r#HZLV8Rt@c4aJM6CQ5Z*a-fPYM@z~nNj85eW$PHgOsB~%4R}hX~GGVW6;Wpfe z^InxWGstjrv@?$fhWy@360H8!2m|gVi~ih8dd*j-0oRgpz_sKDz*>?s8Z_C87uipW zMWN38XJzuB>cl4^!)7~R80-_HrKFe#Fbw86lhFhSgXPXB55I)LMyS0b6ZFv`+x@(f zY{ldzCdBAIbt|!tBNv-~Q~Lm|O*puSd1(}9P2*6$@x2yE$HxCDk-1vuPO>JTBY*vI z0a6PR)yFoL)gf?_T)-sh^(zcU9hKxC{VNRi&F2$yAsUOMFQ@J*t%Kv>=$rBMyT%nS z#|r0E1*)1JN|xhb5sA&$pqbt!HpMxMvO6IuAyU>Q5MTAha+0!#<**Mz&BMUhy(iz> ziWci_g}M$XyQSu=YQy~Y$lN$TL(>(eWch!*y=Wx6BfNcIo*Eicrnzzah6~=Ajn3C~ znypS-z8BGOe#5)UNq#rjf1sy4YhU8s5P*+G0{PgXcOWSTkdHYW29GA(NSZUb=qZJp zXPZoHL>jO}pahCIS+d9LkGwe*V>w&_#g@(s6cZ%H#xX%x3?OaaH@>wpE^$NE=~Pl2 zgbdMIr)BBI7DOOMMj%~?3t@Nq47J_4H;K%!qCa5?aLD4~1&v7!nI@x45I&Nhp|j$LYk+w*Qc7i>?)@Thh-1*M)LVuXhi9y=%9%G%fb$r zCZ;Q0dWfKkaJ_C-i9GA1-dd{15SYQ(@0$bdGMe9ZSsHjraW^S#c+Fuh*6n7N=oHf) z9Rd!l^q1BHi)1Ou1y9P<(IS zM@lTsjW5J@QhXW$SmCO@m(6Vp6+0pUbb&?ZhSBWmt{fS4Z~SNo6DnQ?maAcaf#q0! zPr9x8&bwBSGEAdp@v5aTW>d~)%F9^)>Ng@CD$L7>nM|d)mpo7|kZ~VW0&kN+C?0uLox2-Tn~5oe zz=5!GX`~(co}F$KC8vanVM&H^e1VP?J4tZ$*fMxe5#Jws2oKD_c$MwMUi)bf{ONf7 zmr54tgLq9m;XOGt4miDBlAa zKVHAo264T>=t(jI_Q{na`+Pq1d}o4oD{{bq@n&)06gSalornyK$KmTQDU7c)8=>w# zZuC0W%h@-hnwJHw$&?b#M=TNsZzIx_7;q?qroCw_x92CZ=;>QLbNWUf_dCzTogp^& zHrWnIK@Q`XP*}ltR!Y&jIyWU{qpnyz@8CbC&GLat(oN15xSy4S%GX8N^Gl?x;3b`S zTF`jKwYHXi&}8(4#&Ppm9vx|-a*Ejsp^M2W8N&)7mJg#-89Ys8=_enVBzc+LGTya-d*bVDszaKlhUuoKY@ykiPq>54)cGIJJgp z4}u^@M+b0vkZ%A3N?<5*Fg8A;GKH7rWJU!yr}{Jf7#+s9sd!F3x( zAw%3P)p7fM-Acw)#txB(=yGRx8?0*(F5NYB(^UhFhZQ$2^M+?L7;nFYgV~CV!}9tt z@TeWjq%eY&p+8cejEN7!(Z(^oIA2I{FgUJ-=t^-I%Ep0AIwFBWJ0&wRLn(Sg zzF1$1!0Z=9=xU)c5-1J8`eH5oGy4Td;U}3$L}02n*|8ST&DQ6$LW%*6;T= zz0ef4c!QX}%1TGR*j^}a0k&82_pNWf{MMPg_F5J{VSMEik8n&3Wl-SJ8=XWmG;MTG zYI2>l4Zpa&dO=wSn#~Hcs1dNjN0YIgh8{^%>0LkUi@Gj5er(Y^6z*kTP$5FSj31p) z|1o|nBrI4i>6(apyGccAe8UkIy8l=2>#Gmj4+miHYj^=$Wviq1&gkxxH5|kblX+vt z)u0ug*>VSZkX3wQMH`>wW+Lu)>Fa#%uhN&S;@?YO9_=Q+a+*uS1eKoYC0TPH36OkY zg+BLaXK;rEdvN~ntoGLACRh+oew4~~a^;vv7{~nDLZdC^aqq0O-0q%yBm4Yq_U-q5 zZy1`|7nRF7_v0vC)F>nZ1NvyM=$a0hokNe@FhT=scMSP*{VBZk+QPjO6;S5tQi^?@ z#kYsF5bBP}%q_2i(5pZ!HbS1B@a%sXWV{;3ZyL%Q#)soP4k522CkV1b_ZjsAVtwH0pc zEmiPUl2+d-I8(3UzE3UMK{6UIqw1_ zE<%fcN?h>9p(KeAZOhtZAMM}pTN`Tw^)1OnQGy5Ky6(Z{83H*=mDH3h$n0*9R%=_+ zZ=jZH} z9V{%ZZ|>N1hsh2-XvHZ&J~+$yL6`2M-#t{^h2c5) z&oup&BaQFh)AUv&3da3>7-Nstb3_J77&jsMmfp+TC`XZ2@{TKnO#+mxJ5B{m0rJA*Ma3h*FTzi8ChPVmj7e;A`_HX|b- z=k<{EaT1vGiW(H4Zx^-0I|vsT{l%h6fh-EgjD;$m;F{8Y_mmbbO6CTg4{Rf~T-`9@ zi}_j9P%F`;|3dU(8NR1Vb86bGpg1lf*=O2ShA|TX`;0SlB{uuc7|``hT`E&qTR5+7 z)UrF>BGHzj61iA8D2^o;CkGw@LD5Ohi;EH*?J0%{DPx^qIeJpU9Jv6!bn!p5EshS= z%0Fvff*Pmzy&5uJ_-SK&hq3o@z05Y{e{iOq^QL76^H_1m)51~OPv0a%nZ;~Z^D@i; zn??bMlruH#b4yV=e$9#1GFnQ%^ozK=QXpkw0#K$qp%kZGhZo93@um~7XOtd#=>K!i z=tI7H&ufmlHjp^|L@SQ&aZ>h*^xqp^SVPcje`|R0&+Zw)iM#z$wq~JU8eWpWkW+}G zcG927=?Q)GA+U?>SHr85?6sD|uPFVZ(Yt%Xg@I$(-5Z8oOgkF>4%L9`r+N-WGT3()F-~nnBz7v>hy~Zbdi3s4?W$bzTb$^k2}2Jo zg#&*t(sN4xD$;A~diBVM>(Bx>j5z3k4Wk?a;+XjEEpWq_QQX}yv1-KOTm17`Eti?Wr^6r|fWr1CY{;Gxzs1`dC*Z*BuI2>N)G z%ggB<7}L-85L+L!E#60vTJl>&xr2$AMp5!N#GAw)w7?s7M^o9$ZJE5(lpb#m(G}+2 z(=ACV@Et%2V9KtGF}qU_Ok(Zu(aiN6mW+o$H zf$X)cJU@x1T7%YT`Ta;T+XdwR-tfXh7c|k)U~NWsp6u6e+)u#VJqHb*4erGRMXU+;%*7|%yVY!r zBMC3XCX4{}_RYrInC7O0f9x1bkbpbJg_j*8(#F5-7!MZoJnaA;m-y@d%Z^cTsJgQp zWw^%+JHkzLG%3%vEVJ~>=To(xD;&xjX6iL{=|mkqsp6x zpI(QZm}b%~ukhlhzA?BFr6xq+m`?uOZYWB_Y}xPbNCscliV~P$c{kRFWEZ4&*6j14 zW!ue@hDVOUi9vM%PojUbXPwPskq}z6ViZ7@0s{pTbdGt~B~ev;qM`9wk9qA0y57|X zhyspfsrSmeEK;HC!3e_E8XEWZsC(}OD2qbxMOpjz-xKG$W^g}%21(|JFq)P+qz#QW z$^l?fSEK63S5$^4->{Zmor!NG4^VI<)|B>&fy98lT~^R&2(;(%*!U~J6->$Kith5w zS}A+;REcSL0{5@{eAZqlR$L1^LQDiGOF<&a7qsS2=D1^2a!fS6tZGhmwdwLXI$7SkLsO$^d-)+q}CgVxla8qQTtp+ajdnBZ(qLcYT? zZwGi+;6G^Af*>YWlD0kLW&+CfNl~EELcj*_xGdCO1=T-49r_4JFrX|@1*nIMV48TR;g#cU><`p>kqEjjTps6kJ8A^jE#8wmX{WBl0?Lgc9!Q&{>M+Wxs zRl4o$Rc8k1Ys;bXicVJnVqfw+@sB!g zicNQy$MH@?=s zk3;b#DCR%qsnZoQC(^8$SjZ~?50KC8uD*sKRP4e==EAkDU{?z)^eq0+gBVFg8L%#i zdw>xtZ1pqG-^vpDrm=p~70p6?(CSncFg|2mV{I0=f*Ec^j}@h&^4oNCTkcwby)rP+ zKZtc}sH@bRYlLNDm|@^8A$`io$K*00)M#~I#*si#44-eJw#}fomR5Sz7Z4fULQN>^ z3YYxIHSemNL>OhoNboO1(=urhVn$BCTlqnT_7K_$x<$=OR$^25gV!i zo3{Qqpt#gkB-b@WQ5wm=;jgl1OdTYN+_P__{w)oCX{*&bi%(d!cO1 zsX|aS^kaZ4S;QZlsIQJF--iodI#i2;cTs9dx@4jft8)F_=H1Q@o{m>sca$(Z3B{y7 z@<>#vqv@ekvqn(~o!R#_k*{k_EKJTo0XG&;zyu%VxQ|6WZZ^)W*!qiyKC;qu%?oHWh?faM$fLS*qnL&MfsAl#DvP^wX~XLdd9;7!=G}hh<~=)t40Bb&4FqX84w20C zgTcA@5|<8OX|YeQd`N;9 zI!~rA>BT-RT&Z;`Cerv)Ws@mYTD2G_A5nX2gNnM<>RF#U*Q$(F5kX>Up26Et7o69= z#W5{pwR%HaOosso{T1NPdPKMY{nEOnhyG*MIcg%hpXVy^)gvXg#Q>Bvt&GV8t&YcZ z3r>N+I*NgyAq?Cs1}O}&Fu6}mNce75pALuYS=TiW*;jwt&09vRbk5xZBcgXWlZ3ab z`no}+elA0Ys>T*5tnAHUV*l<9Z(~7(f9*w`NFDxL{{qrldV(34>~sZ>U}6K~YUt6D zDvmq825*btnfqP?^p(U{jWjHmrH4LLPY&&b?I z2F2vAqgVHcDy4okIViDQp2rx*S_44b)c>lXD$TkAUFrD4GV*PP!VQUshdOCGGl}7bIYfp1!25=f^yl zA0Hc29!hZu2F4~JMlXlT|3eHBgN&=!tFSmnS+*f1a2@cuumDadg7bGgQKh+WkL7z{ z!mxaD#qxdv1g~DdzG;MwCOl?!IA>=&b2HB^pV_f&bB=*K73Xs`UjJ zt#A4jtzYG%>?e+G%o%V;o$x?hG9#*k(DS!w*p2(CqWYs_ia~sG7cfRVYIn9V&Zeft z*&ajEiJ(-go3&!IUs3QnZl%qlY`4;|Kz6H#1_tXh8t&vjW-GMbqcLGoR1A94BptI6 zmW<-Z*ZOT+dC321TQSh{ zMAi!Eja@a_%l}YD_k0caDHbxCEV!zjO`eqICM6_8_VR1kX+KCgGFXy3ojv1-5m{^J z&n(!WHt-K@emXJo3$NewX{ii0`a_Su3AQo1HRRJ$2b~mWj=sJ%!wZ9kJdU3}a9J#> zk_HkeEg9PN9&Gjed+{1MtBUcr=?pyrSu{qQNHP>=*0GH|sFV0!)b@WXl z)7qCO^dGyV=D`>;DK9xLav6xI<|$Yj1+avI_=I3lY(^5%Y#WrDcWjvgE;t_#F%wM= z8u1k%gomB5Z+YYzgDqeo2zHPP^52 z)Yz=ETA94DB>hw^5}vhDpL)uMG2N=oin`tlc~SVs;c;MmF|y^R|8ANwuLxc$lZ6OK za77BS{z`Bm_VWg10}@=lNq;7|(2f8JE>v2vBbgXd?K%U>#ceAE(YOe~1ka(q7oDmv z>z7WY(3dX0evxt208>x0tkpdM*kM>L2+4hKsI{s*=#!KRI48eleD&Eh^&$+SdV=!< z{N^ATu)_6y<5ZQ%_kiy1J5wR>_@Xoo)z5NiVKBcPWjXnR)s8f%ug|No`Nt_)e#0N@ z@H6a1rs{_ztgxF&03sC(K&0{%8RnA-SAL&K`my)ORclb5dlyqY zLNS!9j(})+p%G$ZKB)WPluV@!m01@j7}FC>M6){#HyGqYuFOhxCb_J7p>!K*yVzGv)cT~{9ZdWe__`}iI8@!c3Bs65N# zS1JX3?sc7i4|73y{@xiLu(sX|ES9@!X&uL^CH@VOU1e1VfEsSX7GN z>5Z8=#WG`i`v9obaio>o!~^@Ww^Kus=j+=D2l+Hwyg~UU>*|LKpO(>u%dJwR(ERCq zQb9^xp9S;tN2{=x5zt2!Rh}Kjp8TvTF7eg&m!9SKDM|$W; zzXykbCs4-|%^ZE7NpiRJ{cDs2EVi&=D;zt^tlaif!)*hK-4*l&n!!(4u6N8Iy8_;r z^@OQU=mwB5h~b$j8`D(PSc6?NjK5}*w$C>o94Yepm=rT~gt%)=gf!mW@pO9b;3O5A z*Ez5enkZWB-`Yx04WIGaFH8T~kZKbE&m;^Kh*9$Le@Zw^5kp?syQ`|fpqh*1aY72G zVyd;ZU7Unfc1o%>{3v-0X~qv8Lv2W336b&p(P%WrrRmHQ^?nqwDF~E)PHCU~Bp{d2 z&q@?My%Qq~|5)h83Jt5Q@q12wZ^X!q+sh_rQ*Zp- z@y;rTaPo@VrC(OP^}PxH>Ulgp4H!6Hu8CDS4^3+h&ZRu#!yMqTEbukTBuH}I3Wh4s zS9?PLECavK1iibVfz+m_5W9|S^ojJt4Pty6flGxyzFZ@k81#L3FRpxU~+uJx!9*x2Ki5Gm?r5oq;o?AFyGIa$D%|OSh1N2_xpaZr2(j2N| z^ZAX6=q5j;I82+g-S|FqypYP*EzEAMvUH9dAg#SXj? zOcb^$W{sZiNE(JIT({nKG3x0DO2aD1IFaXzd z{d`;RpX0D?;%25Sho%iU)tIHFLqnyR`$MT6PNYArEu1R&{Y?C+|MR|&`71)glDobT z#mank7!lxwA!uyyMAz<)XuKvC{UeI2d5owj5S)HK@pA)$R8P;-{%w%U^^IabG<80? zF=)xV?jcxjE1q0&mC&vC8|UZoRqAzEJ}G;&OBi^bA@wR?lPEK@u|JIIzm zz<1aHdz&FW$D8)b*BO7^5PC~S${M zz*W?Y8%4kl5(6#88af(=?<5;Zk(3RTKIR@ezlR)zREaNmT0#n}NLTU_vmm5WP+Z<|lZY@b_d>#+ZXEJ`s*yhH;S%RxGp@f0;nuUZgakHWQ# zbkEcIP#5ZLOO=l_NtqG3@k0o7BKU216eX=gzU@PSp8)9y67qfi^ia@Xqw(|px|zEt zjYv~*i%{@Is@)HKRSf>1DyB;shW3CI#~it!`A7g9=FI-TM*7s!IMR}W00uOBG%q2R$pP2Ry6ma712}$tyLJo8YWax1eA+o5c&@m&*R1pbwHEw z^q!DOLS&O~RKwzMfRIgK6hlCMiGVo+eB=UMz#YN^J)m z--m-wkaV~h`3b(2iRkC~nS0R9XOr$2(XFWd;wqadVUQdYR@Zr9f=ndj__Vz6w3Dt7 zLyA$@W2<~r(dw@KvElx1%oBfE{?aAKxoFo3e(1+7@tP*1_$-uoRw!kXD0o>jC5)&p z**s56CkW!Q6dV##sx8#Uaf%Q>F@4fav;e_3vwYHztX70oZvBs>W}zE%;IOVPPVQz6 zhBqLRsxqvMM)b(d>H=`u0W>=*keXMXARQ{5e$=oAF!8!LWu^|wNHNI!T(MFlYvGTu zt7EXeqN5MqQ^c|T62_uJdsol)r@9}Pq-tt~`8xs&E7U0MCD}mHC{pd)%Pli%EHmP3 z`BN5ECeh3sQ^+vmuA;cVJU{x3TVG)!8sMX_l&RU2OLF6pWECrB3;8gKm+({A-OKQD z>?2KR4$Ab@lElp`xgQ1y8Xfi9G`bSQJ6wa@trmC2yg!yD06)gLbQX~MNkty^7@kYi zTqXfWX`W%^nfQt#Tt-!PNN$SQs%vYSR+pm#`eG}1AxTc*|4?;~!JP)p-i~eCwyl56 zjcwbuZF6ILV|!!Uw(Vq3p6AqCb>8nYQ(gDW)ZN!~_pcZ0O-EIg$#*Nm!QJ!U;5$Kq zj^FqrP%=ori#R(Zm5WgW$g8bhm^uQ?5hFpUf|vv!?=X?gHVLtX3x1ym2#d}X_9dlSiv7ci||q9|V0UTc(d zt5uf6$_f;RrNJQH+0S7N_-Eig*zm}vow8T~xWhXOCbrT_+EIzyKYea-YrrDPLHI{t zxLr~<(*N9MZZu{J>mN3-RzX|*tk^d@;=$W;cCrvYt|9m~_=vCo_M~8xpw;V0w?YoG zoo%*8zF_=%IhH?SEfu!2Gmi3nvz*Pi&yp}B_4!qX6A4wN(tWCku-8HlxV18*vSztE zt2zKOk)tTcuf4RR>=@o6EM4MH;~als`C!Ek!Bd2BFBtG)O~gxp7N?qV&4-4?#8^$c z!@yx_%}W3qyR2Of&a8gS4n{P7S|Bl8caw`e2^NS&k}+yrbH4_}I(M`Fv;eL%Io$Sh z29DK7M=%SHWiLH>OlY0w7nq5o0G=51F)3VIGL*Nu+C(82HZD_p6+6e80qBL36x;|H z^KU@Rvwi2#%d#^CS%($A>)ulQ18DUIeT1!DPN5H~0dl03O&$r9eO8*U4lY8=pL&dtPj8wQe`N+6S*L=%IQi4w{4s`30(fLe!6hQ@??*5GBEG4itoH zA!pYR9x1s5rweww76`yW94N;FlHt2XX`CuigkH zNYnIVlnczh{B?^*hZ2v7@R@qPWh8od2)qU#iVXgOoj#c6!8)85Dn@u1tgm3JK31t1 zGsSQ(e`aE;@Q=FoN{R0G+|(rmjcsmW0~6OqyzJNJv;*k4H-tz>vQZNI$D{)j@2GNu zhIaAHBSv@Pw8V5+RSxU8{z2kQ%T2Bq7@i9N^HS%BXIu870sp`AnbI3|SdH}m_Lrd% z99TQVsTNdCL%?{N1|s}_t01yZ#5;24Yi_Ih|JaZ#9XPiP>V3N$tBthsn{M)mZY74x zHjVJwQE1eC=}3sUF3x3ezc!4%s5YyTSzVWINM!1sGzY;$WJRhD6uv1}BCz0Re&5gh z-R0kOeMe|3E?lNjgxBr?i=e+<`9`lmcbF13jIElkE*NGlYVP@DxmsMACRGY6MFRR>&=5XI zc7OKCx!T0bkl9icU#dKYVfav=8z}o(4wOCM492q01Are#snONq9Jyb@g%3q-cs3;4 zFgu=V7pHSh1t33?K%(F_rlH_oVUOj4PWd7s$5|uWa8K{_whWFil&Bi}%SDob%x5b4 z3!5(JW-z0$ic1v)T=AEZgJD?**d9mF3I~`j+YrCQ%z%hR%r{YGnhY7L7!X0BPd!E) zQLck}791q6Fq8jh#W!8cXVF{PzhF|ytHq#)iOx4UJF^FvK^g)z6xHO)N49YFboYrV zyfaJMhiL95II2Q}PrUv?!aj23vqPZS1VW0|QQl}{pW#E`xD>%JUlodS87KtezdXVS zjf#6a?2+Qsij6dFnD1*2Lb7p0n-q$7W<`o9@U7vVEumUz3#LMLng={!jq8sh<^jx9LINm%= zQ5kbCgDg=H4u~*y#P9T>O8#Kbgk1_B3rDGzZC9hhzbHl~kU6s8{E~EGCZzrYp9{J~ zQL7e-qV`U0z;jOO=O$}SyQ%l#QSZm9f2j+*JRI+y{ULw;$_STw$;_U4d4ce|?N8bE zkWq?QbAW8nQT&qnxVmr@$9_qyiZMib{ja1Fq%S23(fJ+o8;HFy(JmWSqG-;OZ7|)$ zIM)f53?xHigu_cx30$XI2P<+ifJ`jbjs!|1Z!wB-dCUUvSn;sSXmPFgSg{s8%NpIL z_uma$m;ho+p|zU&^Tc+jfB9XRO1f|yiqr5~l#E>L6l#5U0+~7hxS^*cH8f=AVH1fH zraIfOfRMN&t|RlsaLIjHMrxFNWi^W@E4{hLS1okV5*;|u2C3MydMVwP2C3$>wcCb7 z0t5T3`EB?pm_;d0e2Q*A562w$e_t{3MmTYM#5|jJKn|?qEB2b}cu$>-!P^iRL23mu z;%#ulatzvmJq^fq*bl+u7%l9>{Qe3q+MeVJ1o{iG0JHo~m_vUH&cIF?`0Nj*(5 zTF6rvzk`-0Ao9io!tY#)eNebKqpv?>n$9-7ufFgEfR*IF!u=mByGHGZ@I2E#HkxGI(adT?%)n2PeaXYL^N z+`4j|apHOTRHu3#wlOJalVo1t@R)d-VbBAu2)iTEaAhiphMMx3=3;5zQYu;s4v-Kf$UuNVlvobr5WWTRdY4G{&Rs&aXS*)`O?s0B%;z^32) zhhTTWXA#T-XRV_@N$&xq&-9jOs2V8Y)ty4o#H?^TV7N~THzQgd=RiWCicAO<$n*#m zRRjnX95PH9L$K8ylIIF$xJrhV%Qz{NdtzZU#7GOz^C5ebi)DB|27^#N{6wf-^c2Y^ z(TesVgNZJpXYUjywBp0g9g)9uV)=c&xr>`Z4e*pmweXbG`SFx&8N%7x2cU@^#JM>7 z+5mqQB=wo4?KR(}W$yzX(QTY1_18w;8wd$={@R3)dbCxbL{bqoFj2J5{qaM^;puB4ckdP01fRKf7qIdAI z^}J6Zy&3AbsV3X<20)gAxBe-F3Jg$_#RBozGIz-CHUbYP#)%lkAhN!t_@G(B`JgYTNX2=PRm0&2$VWPp? zx+|;{rnr#c_CNZ8O&e}cXcA8o^e}ecdD(%$sBz9hqH$L6#3TZr_2FqqK*VVOKV_uL8>&k6RfUKg*?(EHqF} zJ4mZT(~N9s6QS{b)tDzmY35!@x>;*};MhomJ}lUP2na@X)rFPWZHh{0GDO%CA;L9vo|5FHAj7U?Izpat^RxQU)P=8`)gS;4faUBQ=wUqy5@sZ? z&9*usbqKq?o$Y==7`ZMu73Tlr%~#LpHuEBi`J&tJP0(=-|3Tn)ESa?|Kc>}2>w^fh zkl=GoWPlL4Lpp$m+~>Z%1tkF8!5%*-iRBNL_Y$be*0?>M#E`r8hjWha{-_PB=b?1BN zmp}?EEWyZ0OvRgt;Djk!>w9b_#=#4Qig$GcT3+~(mMEjgQR#Zxw0NU$11&zLwD@vI52hW@ z@S-=cf|M^NqJqT?gn~s^!2%WMOoc3K;1%BTMKo4W;j4%fkpxiJp~Mm5J0EYS!9S+C z?p%e8n3RQvGdG(6_OhI^_F|ei^Q8@q>V}kRU+$y1nfRzP-f(SA_273W-ahJ>#rly& zsErlffc9qL8pk$dC;UPS{-1GE)bxtw^DL$rdsqO)nc$p zQ=}f+R@;)L4m&5g3V!s)UIuMdU=X z@1d$jVW@jl@hQ5(nRk$HiRSz~Nx`h05LcLo5+Ph%<2OFN^|fy2OhD2V@ckCW;!PIC zweJ?iIJ!8Ne&A^0T)z3YhND^dj(%uT!M<2$4EuTA`=8GyqEhZ>cE$Pyom6Op3R0&; zRti%TW&PwT1)b%%^uNFtbrIs94t(7ug1?+-VQAn6mJ@LFFB+m5*zK0|*{*@bAU1)i z!@w~z@$dX>5&<)VB;R1n2(WKOOIGo(wQdZ2A%#g-d!xl+O=HCj|MkQGhKWqoD1&pE zMB^S^?B|I5?`TDVap56fS7Xuh_X}%%f@!i=%i*NSIP(OjR5vUqTSR6rr$}Ddov%OU_6TcTpoQ7TSb^y=AKTkvdTs1Fa{t z;0RKjC1t^;N2YM@<>-uo2)m|Le5XVaP1@&eRNO?L2yC@_Y&-5nuIcO3akJW!!-!zb_kJN(l$q|wBje5g}Nn>3J z4G_75Hu%MpU`C->*pUiwnj-j`c^G0*3FiL(2*mWT8IYxglhw=}_9Y+(;-pUOqH?5* za%E1W+D@q7p9}Hvy1DUIrMMc9#Iu{&D#)X@vyXd!%gwtaSr0Q4_CfZTa)ls%%GDH0 zmiDJZ#*6o=gE;JOuknNs*JTE=75%^*hqm*Gnrl>nw=Nna{ss!_Cl*XMn|I zPL1W>9l#pGnyF3L+sPVmfP=g*2{ z2jjpD;^Qy9+F!fI-p9=ap=yn0)0I`BmC{39^U4&zM(yVp>XZf+#rg7+gpYBQltyVY zTBkB5rSow?g5LjdE+?RP!WaCx?C$7Gv=)kNfmC__J9@0wU(hCh4wqC1hukvz{I{e} zWsMKSNwN_j1@U5O3Q>W{jD3i5<5Hmznkc_b>c&#o4C!5rwU-$NXIaw4D3HeD5Gtxt za6nR-t0u0(?un#&@H_65N$tp`dNI~dwB6(e3*G{eDev^bsOuLEKjYg%dOa%fAJoVO zy!k1=?YP6&%}ydLbrh@>ee};tV|v6$f?Wt?5w#=@BDPiK@YaBr{%#4 zFmJucy;imDd%imk8Ix;%QMd!f!HoenQ91PGN_k9y*&X$5V;wuA$SF^XjI=K-XVAbL zk>a!YFqwJcM3@P#>_MY{pr5LEe8xX5b84}z++=y3F#TRK-BuBwid=9J*I zb^cepoir6>e?~(I(N%96W6k12%-A_FV?Wj!{c z0{5Q;(sI49-tzDBwjD26LSJ7s+@76kcCLOVMnu~^J-cUvncrT^A3sfY`^LlPGj-&l zz=T4nBVJN!#Z+2ag?)$Q%IT*@Kz&e;s(rd0rfLd^nTf~2hB+EI6Ra*5<0kx*t#ts>;}~GifkKK zWs*s)KcD4J#M{Z05#4VKHVJR+RL@~l66n?SZF=?N5RdfVAfx79pP$F5OKMnKX&`8b zq;ngvt2_6dt5HJ6X+52Z)>lmkIOap}_wrSTc7KTyUc1Hf5DQ8#l+B~-sy+q2PT2IQ z5ab9^!$?zXYBr;o_cycIxGs&onfEP876Gx zFNvI;Ye$+>sKKk|08Wxc6D^no1qbO-??1tB4Ex5*6~bi;Q<1h zjR9JdGv`e7Zk0=4JIHg89Zoa;gH7oM5@DDR{Z7Eh(-Kdc<^*vx^K*O`?~``;pC|S?$Is<${^}q6rpUef}F5m ztNS;H)Bvvn3(WQB;=Ah?iR+1O+c?+p(7cuoG@rlVE2j2NruS+^DI>W|4AR(4$l+)* zmz|}0E%Rn7ZH~mEZju;mn*0l6R$8BBu#y?VbH^vafI!LaFb0@wbHSZlwT3Q8{GAI@ z2^=i2fwMBUVe>$)6*z^0XAZ?7l8pT+t>3Vqi))WLfHltspHR<=*&`*jCo;FdbCjEJ z`-)}*wp~ja1lYn`2ivlH?zMO=au@vR#vyhSglU#a12EwOLGTE|z0wD;sjd*k55T0nX*`BvK5KxpbY+(Ko!{ zbIU=|i-^91pSBGbK~6omX9I6dYRmps-@>y<)fJz#g^#gS*8r|FC#eOYI?2RtDZ5QI z7KpxoWNtMiraj|RfHOydi=AdcQ^N_!d?fZkZJL^x8#B>=RvT;(h!hXoaiYB<7uxGQ zqVdk0u@eY)<+zWl26}F>ikX8uDI@5{3E5hbDZ zzBU9dI(Xv0qmTtEA1FmBiUj+Usl`ut|SDcOSmgCzTAq;eBu0lIm=kUobRHf2>=@O zK*APZoF9m9OAZ-`s4p=F#b2|gB*i-K7y@q}fG7*ZTb%$}YXkRLD_Qomr~mREsJ#K+ z6frE#>$@{mM`>$0{pM@7TKMvGk%!qdx={Cf#eiX}dL=Ec)&PDc12LLKmF69OAS8(6 z``0NHve_ts$nwt@wCe4~dT}?J^)bx-9~U#f(MLS!)AaQ}Sfuffn}pXNQ>7HH75l-i zS2OSo1n{hY^RqjwezF&cFgJH)6F|MCh;Uo?7(W)vVUQxMWP0nc`#LI&-GETvZa)IY z=eqIgb9-Zr6y0u%_IK`r-=tYE!{JjJb{vw#E1lA2Q?wL>gJ4i?b>4x z1ktoMz;vscL+o5C5xq|Y|4oAQV+`US?8`I zy*~8fA8L?g$*u%Z z{G5!4d8fKoZ0}l-Gfo7cy^tL~fR+iuBrdhS#(fiZyJbj{8~Sb#)#Dz#8*=ib7-AOo zXl`=roEz-E)&3%ppPs*;fDGKkQ<$=QD9l|xUcxD3;a)BB(&j0JX-4k4PY%os8gN`s z=$1f!5?3V5S#QOszySlU)elSa+7B za;Vc;PagUy5ooizNoj|=YjE6hH)t*hA=e}wl}TTb2eOsjGGfKKcyVKPWrIT;k^wf$ z#;&4Vb~v#RDs5VuqL%s8jW_kZw6FVj=zrGx%8GNzwWx1@*70FJ!IdCPeua8HtF7Ia zPgYQ0^Gr1t51b+b@!tSN!)g@lB_5I6oC0=hAsmjJ817=I$h>OD7;ITBauq=SIiNmh%zz4whR26M9owJ4R z3?#SYb>|$37uOYvECsWp55);BwOwf5s26!eFo;tPWcLA~#wJLh;Rf*!f!bv~l5)K5 z<^r42<>KDQ|IoJN}Y?h(m^DISmE=*;C;2A2ZSx%1BMH~`fKWmyZ0$%b{kUnk^ ziQ&ZCAfX!*Lb^m)`o9MN83vP{-Y$Pb#tE{Z$^RH2s*t_|tB`#-?u2EA7!oO*@eDch5iRh5^Lfh1K;JPHd8A{vYg8aD z{tY{5mlXqMpAlLa)Fa=yjt zDV&P?{HMVV8>gZGA&LOkbBr{gY@5BFtaI34U3x&jh>jr57SAJVMYWs#3%X0G&gMqqQ zbf3ZafNhH2)&Ab}c~$+A*ZuD!-Tc$v{DmqnYX=O z8q}C@oqU6wg#K_oZ!dr3gD;U%Q`!@`6Qn+sYN(XuXe%-zcu_%Nf{1Zb(INO>8gIMV z`4U7BK+y{RBP{o)Z$P8G`XIRC4p^Wx?-}xQkI1m0bG_8xKm@@O+OHyzF_Kk&K>nh5 zqXl3zg%Uht`EP_}o(d2@jwVDVCVOxQ(EN;GrjThMPmUZC=K6_(>fzevIs*Z zorM>8>X}ab+kdonWx@w%)prtKHBj3tS3JYK+PjF`w(h23DNBUEL}86LeS=_KBq&(~ z53N6&wP1Y*xl$*#xwWoNcVm7FG~!{UR-6ls#k7l8WO?vZ#k#vqOj*Z zHDR$Pgd>cSRMn>vRPw z2(`HtiTd;>4`+ik$>=V*{Jd2+ownYU?MYqR;?b+%$`kK zvgk3@tK|H6yE(jc?eFotCD~4YJzUCglxZIr9lTu91-A~O7wR;A8gl2=T9$tt#bV{H ztEn&iomzTg(Kl%~!QBoN@7AJoPPnsj?bWGOTzP5iQP%0mnm!SuJXpSLbroRW^?H^C zG5czHIyP(Rq5=B$a`olZrE6#>fc(A09?O4~^yJWi$UoEdeOXO3a&)|KLFj;#baeB2 zFp5apvT?3_0E?b;00H-D%CmE8j_Y^=qBtUxJ-_ z40dxjcdJWS$q1LB(1=-rL~%z5n^P6$xxDPG%;|Y@BKI zbdzVjG3&?`(4V>{JjVW41LabB{10)>ML^sC>w7_er)PPp%BFkR#VC^^@BVV~TxMYh zJxV%5M@%uts9glEc2$Jh3VNQoH{?nJj*)V3>vE4TiL6qdZJ+s`_*@2SrD^v=&3&^4 zr<{;MVr%%H{6wgkzi0M@N)gF)1(Vy*AY0}3P9dk67|!5U7EJj5VU1weK?p@de4*gK z*HPY$aGN;QBIx43$z*geHXjYcAE3P%mv+K-%nvFaipA6 zBB1u4SM2b9UM@Ced3O0I^d1MRU<1bwWDYRqKVb6_>ale0bOiuNoCgtS<{_a&-tOU+ zP+DR?Q3*3WCX8b$#7g4D#I<|~HAA=ySlS@D`{|)ys~KavSFPn-QrJG)b2~1h7NT-c z=@99x(eWJy3Wm*0hWJ>0U{XtqQ*6jH5)k^@(#`Kw^*m_{)`a-9OC5Yi(P=F;b_;D8 z-bR);l1_?+e6pj0iXiTbD5-KIVT9{6#aQ3XjiEn1rNe^FOCYm@Pw_dZJ zv{OWzT}65zmUnS_*6vezbL9#-S>ySZ%a49Xss?r-CTvt*Rc_?iVzqzkJsIlpNf%ZE zxz}1ZtrgR6%Wta)Zx~)eTNo=x1z0eUa}KRfmjs?!K;w$BzIB1}Tqzg^m-3%G#DoUW zEw<6hDXdy_TCD_uRvg#Q*kYkSPrJr;*K=>UFF^^&_KoEGZ=1eiu08GGC|@iM9?Ghr zQ8inj_=Iy->+L)V7nH0(Kjd+ok0JHTWb_3`OFb2!xQl|YPI^kd;b)E#=%*IlTu6?+ zn{;_)V47L)xf!&qPiEG>A|sdvKpIcJJ(u83*FpngP^Aaw+EY7l+e6OklnFaZFEX=w zkHuooT0O|efHCF5W4&kA_+=-cQ|vTS{;(ntXdqS(jzU84oQYnNm*EU#M$+2p>l~S1j*b-3*R-i*n@JYJX{&4)!bwerTAqX zMir{IOC?7`FCMg}ykz1vmJMz{-f!3izF4I6IV2-xiWgm7fV`CMa#AaC>k!`e+6PXD9&)RdL$5w9 zklDdJIwgncTt1f%eEI?C-^+4{w*HXF6IGnqWGds?XpXhh@=Zk*of_s%21xp&Q2IVJ zm8G)g_40yD>ZH$%}PLS>X_GuYX zve__KZ#QnO>i-!D@$+_I+)CJ6&?n5?liujUvR%V7fQQ$D;p;(RB@JVMe6b0F(=p#< zf2xIRtppaO1tQde)KOujq_TI%ESDjlmZe50*v*R~+45^a9{M&X1x&gsfI!qkku*Zb zT`CVsj2MID_xR{YQh>IJTlp5G(h7>woD@x%-wxP;xLpLI0GhwOwb@^dfOmD;4S>2E z@Cw`z@+J8__3a?@c;V|UGy3Qv22#Y~oN4D6X+k*5-Vzb7E{CO+3hO!b#Rn=2%;|+T zxAWP73XY4&aFheE>hZ-~y24){u`FdDk%KyEFCA*mK|ncL*r#B=6e4?MVt=SxuuXrE zj=L#@J!5GJW|=}NOM_+d#I_{NW^(}Mhe)4<$;u5niKO6Zv_T7ick)&hAn~^PyUL7R zYvg>gHY6gN1Ijv@$!}rct41)qF4b?;lE1-d7nqM~d3CNjh@=)i>jT^?4B6i`4P7fV zE%4O9I-}X^VNq^@?G|Kb5mT)LWWs-}pd5O0Zk*(5V&mY$RNN0`CC#s<$fV$wn1`Z} zHio~ql_ubqNRmRKx4d8#&6wGchF@z7MQ@OqO4`2-S}XOM`{T7nAs}}n)1}ed zra)rBI|+2c!o4lZP4T&l*)L-zIa#lUvPXOhW^Z}6jT+Q^Nn-Wq0U zs7f@Oldmwxul!Mp8hoGp%aOa%P|F^M)<1xLTg{csYKa!kg)TOR3%V@0Uh)~9S2)N z`!2bl)_s$oJq#`8GsiFaDuDy(5+0c-TKs!LtWjn3=|EcLw#kwvqPftL!61qd0MS#G zY?BWK#2i6Iu1?EOSUSI zLA3=T?G!l5@RIumpPnGZSb3jvSUiRXxL;SH`+2b81^$TBn_cA_gXP1_Gd8~6tNR>^ z7k54NH4riBOSD)C)U)M#Eq{*Vossh{jl(Qv$S4VT#)xyp<0-VLEfQtJ8L?_78}kkP zgu=>=t%0}L!ej%K~>vS*-{d$fcLDIs*#lTku3r({lOSdYim)0 z&2HQm&?_H(vgA(c?rqj{79H}m3nOoiOem)?$VDDK`0qqacC^HxStQ^W2Gl8fB*T$) z>fEDHIxN{R$mz~Xv2Y+np}$M;Ld57Z8r`h0;)k%3sBjE%4P@$7T<2rv?!{hglUV9m z2>P*q=({-c0&%L=PL2+6&04~yU{Z0OA4$SNE6vAYkr(BVluG1IcBLf_B_>Zy2 z$p+Az*v|z%YW+=Q$j_3BqrK?{lPa+*an08_6kmyuMflh4m>CMB~Qk zFCxq|!ownx?B|~Fv&raC8^xHAQd<&p${f7W|+#`#aSI|9?@N+roK_&*E}u)6Xap z;@d13VvQa~mzsgi-Ht$CGy?jdP+ih?TVAp*gaM~aq;imkv#Lau?7WD!j>HAEWF>?J zW+kg2k*;@Z(qj7Um($!$PDO?xyE@C@x$4C{pydfb@>ymi?K#jA&-QC!HRBtB^|UmC zmQ4Xa)I;)@M{@L?Nk%bm4h!MV)%ZXKGz{2n5A|7uid;xLt^y zC08f5mVBzmn&y02gsS!|JhfJ>BZ#IE(loT5sw!(3kiP7M9|6>YYE3Lm_RlVQb}iYV zU{(1&$@vs3tB4pcYVRWw84eIW8dpeUKOVysI~d_Ia0t5(4ShG2SsMu?FH|n&E^%n{ zhr(gaYGCnyS^*f$+~Fj;qW3N+=RagTl`=WxEUmCGcq9hjoH5@~f>FFj!5!IV0YbOH zg%~~^Zp!8H)CTHwvrmO_e^?CO zg}KaH{QZ-ve5Nzz=~Kk5Tkm+mZ1d#sKmKQV)k*?Aj#uu%b?mWdZ)wyj-gy~jAu3-< zA~{vuaE_Kuz`bk4pZ>{`AobO8^-gJJ?rvlbAdMHuF>os@qpf%YNFm>v7W;qBZ;nqV zRWlN5{IVc5R0>veXh@CvJ*8cZ;s^$r0%2e$_-6gtmwr4mUT3NyIKf)e1k+$0AMIFI z`ih$GGz0!;>wl-tpAmB}P=1MpL~+i!yi#bJRIR~qw3n;9aOkfcip`eg_)(T`7~>q7`$cqN}ur3OH3S?2nV9lKcT zWC-T{HkFk34~^KO^AF%e_sy;Pf6zPe_p^#1Ev)m0c4sgDOS?Be*?;dCj%QXpd6>V| z-v1YO1AcHmkRTAO%zCZe#m<7>+>#gM?Ba)nc=>xofUZ8LE=WZoGO@w6Y*MDKRvgKS&b2yb3lj6Mb z;%FaZ=fjGBO3>UI;u}#F)o3HgllP-*3>`52TrMetK0vuTR2so>)bOXvkH&P3N_v97 zKv5YviWz$b63Te9j)oq=@ZCz1Q;Prh?4x3H*f5)y=i^nL-{)J5ptd6D%cN;G86eUU zcD8%6p^;JZ7ix6Ri$Z{UOtr;v+#_GM`3u{TUFpTy!_wuqVeZ{dl#sdA{l5gclg6i& z8^N!qE9jecwHVk2>)Mqjmj;uLnWUl!7NEVh zxXA5`V2Ad~6rYX}yZb-c!t&Tw)I!rq$BFD$KnZ)z-bFY@k`!?3jXWYl2bO!P_rCh< zNjtNlv@dzl7no6;?xYAqCE=$!(x$27`kHo58ayKs18+XvC|^e)UEa?iw5Ek^V$ZYc zjz!3{V0840!d^o?dI@^-yIMJl6LAeC)v$*8fE^`!YOEzfR?qgZWU$VQpB+6o*RNq0 zaq8bzt6I2qcE=DC1KJI=V@>O8%usETY2P7pxO$<7p>O^&BK6p$Rnf@bP=#gRhv+E$ z@UT_R4-cCY5;_guDz02E$5Zv|+RYpEk|fJMb#Yjt!C4XiB|RL?#Om`jAV`WD1FkLC zla^`3026q+Jt4GKNBrqY`0vS5`?KKU_sTo6Uq+48#efZa7ONI38P;ep^7&F6lb9n1 z51u?x2D^tqIVDqfvOIz-AyKy4hHL#>)sat}aazZ*GqWGG=}AXeMory*$)Oy11ioD^ zQNtfw(Lubn`b(+zR8Mzl{&S$e1fyuo}Sg*sDJoMrC#F`NpkI=> zqGS0=>yot16ZJ=h$&$)E(sSNH;2!ca3~zraBGy3-4YYBelm8TyaYyryftnOaQ>Oew z*)r6?ZPg9l;o~!T8==$QIfKT~pgEODiBPedcQG2@?L(}ixM9^aZS=Dje!D6I@??`X zP?A#M_u#^@O@G_W9%#*nG-9+Xt+<8y&LiAX_3HU#pZI6}Zfy%S0V$cYm~_oy;i5Tq zx>s_@|70j5&Ev_hq{xH^bCo_!Jg@j_qskpAOTAqbVjTYo`8rwMm#7CT~evU+v%i{Pe=I|bbi z)9?lom6B@deYaLmZ=%O3Y5Z&V&Z~Bkd_?|1VVjav=bT<%lAow09^x&j9h=A9HY)}T z)Gim71QqT_7s%sW8zL%B7hdZ8Np6cVETpq14@s+gPvCqCX_!~iFz7)>Q+Itd62{XN z2(9@f9qsy1?xk8XrcYHk_41S{0QDth?AI8eAP*o!#lJWkEnBQdNv;?WYYc@3O?X51$#YcbooV!4Kx(D`a z>n_v_-pVf)RGD?xm}Vt=8u$Ey61+}Bdy#Xl5~(rzrlOYTbX6JcHA-hlD)CB%>l(k1 zNSXav?@>%UUQcdYo%Y^DXmH@y6I9|c$jMH{-ugb>MFV5r=-e_viFgyr>i81gKr%~z z&!qV+pcdZxf*OURWYJTbeP6i8`Ch&6*A2BqIsU66%nc<_9pI5@`Zt8a8zs|Xfm;c3 z+q(2^Y+So>xx zRwsPI5EHAOyc(J2lv4GsU~^C&=6Z#R9ib)wp2xKrsAVi>T@CDkSo`fK3aS8bt8nE; zl7d92^0eJv_>;6kA;5kE5>kF5WlQ~d?WB19Clu8?twxoi8dX+kM5X=O;ipIqJ2t_H zmatk}5WSj#Jvw~wT+97WwQC?BVKN6uuU`_{U9BwD%PlGZ?}|evT!dD0uo*Gbf-8vt zon}m9)Y4X*3FmrMOu&y_i+J#jcBO{pR;1+~z+qPqbg9%R%hl!9T*0CsW#pJM8=Eu) z0gbb!$b|YTh4Sl1&(+IjPjrDd3baa*hOXZ?qO?v0rFWU_qe`0~p?u^8~{ z1OX2~Qum|mDO1ZT&RA;JqE3H@bNjiQ^0O7nGvb;Ij>UJML%tSw<5)Osw|WMnC3Y;) z0@wy%tvgSZ2CS+Lf|ttdh#fP&fPwuF9J3I}9ACQz^JTO9&AWS%A$~SbD$FS!hQVQ^ zlzgy&0!lJ$dYuSntZ%2??kSM4Ar<8Z&v8xiI>>`d1R@ehD_4-PbHk3%g?AR2SwqHx z#ocmbAtpw5YtfGgHZ0~|2@Zrrz(PdL>5(5M{%sM-sCIWT5t5v=6^7I)HU|0<4)JnG z24i6hBox8^I(5(p5|;WC6d@@mF-u3QtAAQ*s0jYsF6bEAdWh%>QTqmnRKTGdM>Mdp z1f+wk9_Nd!zGJaNf#?Y_>1FtE6agj1$Vp0q$?ykuodd2Tx&QUFLADZw8it_4%Hifw zyq)fad!jn^`AAA3yyCEfhln*e0s>NiB_u(5sE96*y@8Ar6w?qyUQ&1|^55DsDkUK5U zSO>f6@#TZnY~=5m8eVUz(N3zEA?k_g7+6PCGZ*iW_I2eInshDC;Xfd5v6-#Ow<>^v zK?Px{lEzPI@`8JAC7d9*SxKC5Sd4~#y8Xbevj-F&Fwajwd9mXA>?c#}6P>VKfY^?| zgzsp|J)tHGjPGXdN0WE#k8Fc-jf&7-4cXB{w?&9@FEIIws9De$5eF++YN=o)4`EjAUZ2z8T8#T~ z>K1H|m#w*c)6A5zPe8sHlH0v*OS;XYDb5SWub|p9ek{|lCDd4CU?5O3qfCl9h;CD} zotDmNsN5E18~x~QS#xZiY*#&?74?NiB%Vplr2D<@Q~>gDJk& z#<7^z3bJ*AF1HM{A<{4xR|cDfuUb)^t>oWP4a_M9;f0t$;SdM#gi@_i3p$|A8{U_K zzQ9r8pM!BvBp%PNrnV^iu!%)&dMZSssp7LyM@@|y*Mme2AvfiB@{Ge8M@6AQ$VtN9 z6{E09-;j7)Shd{`5LB3j8l#j^F^hm0doasV$5S^L{vEZbd%>Ni03WM4@R0q$?g_8L zBMMz-isK<;v^owYMzmpTE-Vm>+7fvvBxAXZYeoU>7}%TK&F+wbKMku^Uz~*fU{*q- zQhLp1C!J+ziqs2MG4N(b`(-08#Tr3s(qD^)Q0XjvU*L~iQnYxwCCwhx5(ekj9X|JL zEWP@X4S(6FtwN&&W^paa(-VR8G^84xaM6*M5Cm1i@+({gsIeUB{Mfu8do~plx+8L) zO}Hw=e|&aydd-e!@v@3S^Tgdi#ic%r_RQd^($$m*^@T`kO&nXf4|IcSo=eWyxR93z zbPKHj3gryW1@*03j$t_2FzJ5+m=tI0?r?p7e1|0qr`j}0vQDrNvXE3OgTGnRL0cqv zkOo~Cu}?UH5PVnH8By zFfMA$c_(SKm@o$`k-<@ssgUIq1)Z|HDl#Y|u%S{P5TTS4c68>Ouzn!C0n5jZvWD#l zovsaBikT>ZYhKjg3K4JguLWJi7$5-UP~g(|5!F;zR9j1#juu}7P4LEXYS z(ajAY^}=ji)aVwow`O?HkHYu7%K*be1=gC%2kmxGhOD!6AG0)|@$vbNVo`M38K>oZd(4*Qk8=21Tmp8N9S(dNP zdjHw`g^E!BAJ?NkL!la6Is5SK`}fPu`tFBpI-jlX)2ry}+t*h&voAx5!h!sF{(hN1 z+^wF!p8ow`Sp;uCM~|n7TIdHN~1T~{dAsvlP&LM zx9hvf~kI&B5Umr656u0x`bY0y2>`c-2PgZYnFUE|d4exrB z=SV+)rCay|i(22$&hqT@Y`NeE@RyLU_c>o~9{BQe`ZB#gFZ1~g*CO{-3BUZ?Z1Hlo zxS1_f`L9E9k)P_S_*VS2J>%~Ad9&Zv`IO$DNhN9Z%=n%wyz|v6Td(obEBRfYolQ6E z<<(-D1FzZNv#ZZQ5bF?)w9YqK@l{LR=F98domS|>SXv`}65s6#Q$YYtUxFT~o>jcV z)&2BA_jxX># zSLk+-IhS`+NH#(8vnNcqV~ROjESS@Emc#wBYQcQDy!l$5y?>y~@>_q#DwM7BuPojF zASbD3iQ+XtJg9fZBgoz48CLDv4Z?7Y)vLUqsTEE3Ruau#)UK|V^QGK8Lqw~!?X`GH z4N zvAUv{alMceq$L#!E1M$S$9%e2%{SNC;`*z@AnLW1g4Dr7nEpjFM&J$=B*ZEBB#siQ z439qt{C}EA{2H}z>GJZ@WfLAIR(ASBPO(Cd#-&}~>6`g-wffq7H~pIB*-h`y+3fS3 z(lhyf`%@Lfp%%|al6oh%)-#<#e}I>p`-k4;8d~yaRft;rx`TfBMWVhvK1@A7{bgY6 zKQ13IHKyxRQAcgo27di^;90rFGAaJuq3cqyV$U*3m!)7lZkgl}q#$@&Cihw<>6^)A zGJ0AjS#&DVvwB)44^g=aE)~n9$tHZ(e~x8Regax60)BulER)4g=VdbBPw&Vwxfk#U zEtBwHT6va9x_sI)$s;^1lLzf43wU`qX_8w=wWr-AHmOg$$vrI-QdEfDq{*hIW%4vV zA57K*>h?dz{SvM@F z2IgqV>|WE)=3jjE-FsRBMi=YZZE;{>{osR^0JsSHHcNE*v;}}ic#{7I$^WO#k~#}| zI(j?t7*}eGPn#t^feE-<474!Tdi2)%?NY5rZ<71rc1rKE&%R}nE}ync@(54Mq<@*T z{ZrUGg}#^EAw7Agkn(AGg*+X*9m=DB3eS}4X|&xrLD{mTGDf~LTiushwKP`$?BBab zO7$<_4)|d^$FJGt^7?Pt+G9Y3AGByYZE@7YJgty^$#xalSQkE=PaDanjbtc)V5v+$ zZ6u#IlG+>GvN#^DDE!kpr?3Urvtsh~(^g9{#M4T7&`QZ4O>QX7GlVAx6-2Uja6|cY z{`j`r5)0|UD`CK&-L{?4v49`6B!1XD%;vsD zQ8b*kEEZ!tEsXw!vBHE1+v}Q}&L4j;g-``v;g3)YA43GKjod=8J6VIVTnkr>&F482=PY&h4j$_E zFJQC4{~;;3?@!5O%(oLJ_Cba)YicXu+OVT+AJyE|v+McgPZo?277Q|tWIp@0XgE!r z7h}jt@Vhoy9I@j$Xj(t*dAhpiDI2wmwSuhPa&G*yEf7!k?NF(a>$GoQUeA#`-=pk{ zhSOGuVhlM0o>qswvU>vVr_)c8mQtgA+CChaPTkL>R%{>YbQGnZ{j_%ov3K~m%&JaK zWYcLYgc{{(jqrQOcr1QTVdzf*b_WO8RrBa6483pF8u|S!kJXs->S(60IPph!z;U#Y zngN5*zs;sMo{bY-K5bFt5uO%A|AJUyQloDs<0;}Nr%}vj3cs6m@B4@8^=z$+ z7+NaE8NOha;BME zwftH2V4OBNhIx4Du z<{?mc7qxTMWH@`6PrqK3T3JrXx#*UFaF-SlSpt8q^> zk`JRMMA-a*qQvwI6kIpU4cK@&yFnRx=a+q+&K6eR&(r(+sdeRn-VxBE(lK2>yPPeq z6%fCPhCk14P<RrB8HSh!Ad7O(v>OAB(ufD&%Rbv)p&gR*Dwpi<0;g}op6j}*b zDl(l<=e+{A;!8jPd&@8D`*ja`0X3{4x?e!i>e2pl4&dawp&fTB*Yj+#xwo52PUlgf z@8CWa=BkcNaM5_QeCMh+`mkKBdniGFovl{A%j*}|biiHk|V*O~ry`s?)jAwLk_h!?73zs@)g`8t9>kjB0L-c0A%s`dW3n37K5 zf4xu3<=k#6p(phNrlC1?^oR!tlk)prUcP;Azk;Gx`R)7~j(9btnkk{f%Vjw7mEOJL zwM+Y8Pf$=m`K({-)pGGA%Ri$aJ=Wsc@`Tre=5KoTKf^wC#ro=$RbD96O0^32eR-iF zI`m$AL9Fg+v<-~CezybX)1xw0zx9VayO~|DdmpFZha<4neDe_Zu-6A|%nF1})8;*7 z@%!w4nOg>`ZEMwGKQHRX!+>-r1-JJ`=r3BBnrmx$tyi*5D)T}+C`s+Op7(@cxtVQb zp1?xxZsSks#c*&F@YP5)eVr|kog>TJDmB#mbxv}qH@~DUdhab1ZMK+HEG#8W@UYpH z*2RmCi3N+(1q8hp^M|`>#dmL+nsh04*u7WNhgG3{N@o~XV3gmyBrgK5yO!_Qof(Fya6gaMybsg$UGH7C_zWAVsbAtPl*+abjn8Z0 z@vA8im|YhWO&tdmlS02?X^rrGL}!%<_Z{CZa6GZNCW8veo%`9}%NmgRJ$-~$WUzF0 z-_J0#_HaMp2N*2r)PjD)yXCLG9n#*vK-%^tO3jG8H?usuoqsL%N|Tq~2}l-^I0Ucr#yfKlxmX?E8-POV~Gk&T$M=d{vK(J4i@0@S>i*r};rW zqHKqhF0pr@v=ICO@3Q%x`RO88QIED{_!r)R(H#}y{|GSLy+HslZjfUwFSz{eD?Wn zy%2|sQ|F{2_)n@umAQPtmRBDK(?aX9^_ah9uQjT+Nul0X92l(NVbG)fgmaQ-zOYZP zDT&Wo$f92M`%f_P;e4Y~mp$_C7J#p;TFRe_n&6qnV{RsaV4ciQz;!R!8`^S^f~mGu;&$=ulhZWY`_Ix{-!t6 z`FweM+aoK&YvhYQT>U%rhr#2gCJ|lwP1sNV&VEy+90{^^*0RvJs7OS5m+;H3-fH*+ z~{HkqZQd< z$pH5fPTX*ytb`|k@gf@4p|J4}fc337)N9~(80Wy@^v>dEzzj+*n@zw`%g-Pxy}c*T zc2X{T4cP%;Myka!4ZAMnCfO#|h5Vz1saNq}MG8WR_)>Xarc~lj?n#Uj4O|L!Dt5 z?iY5<9PA*Ln}jyNh>Ri6i+GqTctuQ~&k+6KMuG7NG8AmRyt-yT5)pF58l$nKYga~M#p7#y_vepm6Uiyta_@c$4;Ho|J>{-~?f?0z%nlU(=LoEc5k zacwwMhr&vZB#3Hz{bre8XMYrziXUqZvsXmX4G+@0R4-PxBFdvC=d+lG$%?}$X|Vhz zpMG&|Zu;Alr`cAQmg18G7vps7fR}kOqaE?eXDB=Do%$D%amsb-YyBykFDb?y(&>J7 zfTwiH8a{|G46W(Z*Lu5{v4SN04|-cdPWFM(kRqqPnD7mKxx@($#2=0Y*#mp_cSIq; z{oFQ6XE# +#include +#include +#include + +UnitTest::UnitTest() +{ + m_testObject = nullptr; +} + +bool UnitTest::writeAllTestResults(QString outputPath) +{ + if (outputPath == "") + { + outputPath = dzApp->getDocumentsPath(); + } + QString sClassName = m_testObject ? m_testObject->metaObject()->className() : this->metaObject()->className(); + QString sFileNameStem = "TestResults_" + cleanString(sClassName); + + QFile jsonFile(outputPath + "/" + sFileNameStem + ".json"); + jsonFile.open(QIODevice::WriteOnly); + DzJsonWriter writer(&jsonFile); + // write JSON header + writer.startObject(true); + writer.addMember("UnitTest Results Version", 1); + writer.addMember("Class Name", sClassName); + writer.addMember("Number of UnitTests", m_testResultList.count()); + writer.startMemberArray("Detailed Test Results", true); + // write detailed unittests output + for (auto testResult_iter = m_testResultList.begin(); testResult_iter != m_testResultList.end(); testResult_iter++) + { + UnitTest::TestResult* testResult = *testResult_iter; + if (testResult) + { + writer.startObject(true); + writer.addMember("UnitTest ID", testResult->nId); + writer.addMember("Method Name", testResult->sName); + writer.addMember("Test Result", testResult->bResult); + if (testResult->aLog) + { + QString testResult_buffer = testResult->aLog->join("\n"); + // format test result header + // output buffer + writer.addMember("Test Log", testResult_buffer); + } + writer.finishObject(); + } + } + writer.finishArray(); + writer.finishArray(); + writer.finishObject(); + jsonFile.close(); + + // write unittests summary + QFile textOutput(outputPath + "/" + sFileNameStem + ".txt"); + textOutput.open(QIODevice::WriteOnly); + // write test details + textOutput.write("======= UnitTest Details =======\n"); + textOutput.write("\n"); + int nMethodNameSpacing = 0; + for (auto testResult_iter = m_testResultList.begin(); testResult_iter != m_testResultList.end(); testResult_iter++) + { + UnitTest::TestResult* testResult = *testResult_iter; + if (testResult) + { + // calculate spacing for method name print out + if (nMethodNameSpacing < testResult->sName.length()) + nMethodNameSpacing = testResult->sName.length(); + QString sTestResult_buffer = ""; + if (testResult->aLog) + sTestResult_buffer = testResult->aLog->join("\n"); + if (sTestResult_buffer.isEmpty() != true) + { + textOutput.write( QString("[%1] %2:\n%3\n\n").arg(testResult->nId,2).arg(testResult->sName).arg(sTestResult_buffer).toLocal8Bit()); + } + } + } + textOutput.write("\n"); + + // write summary header + textOutput.write("======= UnitTest Summary =======\n"); + textOutput.write("\n"); + textOutput.write( QString("Class Name: " + sClassName + "\n").toLocal8Bit() ); + textOutput.write( QString("Number of UnitTests: %1\n").arg(m_testResultList.count()).toLocal8Bit() ); + textOutput.write("\n"); +// textOutput.write("--------------------------------\n"); + // print each unittest result line + for (auto testResult_iter = m_testResultList.begin(); testResult_iter != m_testResultList.end(); testResult_iter++) + { + UnitTest::TestResult* testResult = *testResult_iter; + if (testResult) + { + // print test result line + QString sResultString = "*** FAILED ***"; + if (testResult->bResult) + sResultString = "PASSED"; + QString sMethodName = QString(testResult->sName + ":").leftJustified(nMethodNameSpacing+2, '.'); + QString sTestResult_line = QString("[%1] %2[ %3 ]\n").arg(testResult->nId,2).arg(sMethodName).arg(sResultString); + textOutput.write( sTestResult_line.toLocal8Bit() ); + } + } +// textOutput.write("--------------------------------\n"); + textOutput.write("\n"); + textOutput.write("End of Report.\n"); + textOutput.close(); + + return true; +} + +bool UnitTest::convertTestResutlsToXls() +{ + return false; +} + +bool UnitTest::convertTestResultsToHtml() +{ + return false; +} + +QObject* UnitTest::getTestObject() +{ + return m_testObject; +} + +UnitTest::TestResult* UnitTest::createTestResult(QString methodName) +{ + UnitTest::TestResult* test = new UnitTest::TestResult(); + test->nId = m_testResultList.count(); + test->sName = methodName; + test->aLog = new QStringList(); + test->bResult = false; + + m_testResultList.append(test); + + return test; +} + +bool UnitTest::logToTestResult(UnitTest::TestResult* testResult, QString text) +{ + if (!testResult || !testResult->aLog) + return false; + + testResult->aLog->append(text); + + return true; +} + +#include "moc_UnitTest.cpp" +#endif \ No newline at end of file diff --git a/Test/UnitTests/UnitTest_DzBridgeAction.cpp b/Test/UnitTests/UnitTest_DzBridgeAction.cpp new file mode 100644 index 0000000..148f242 --- /dev/null +++ b/Test/UnitTests/UnitTest_DzBridgeAction.cpp @@ -0,0 +1,848 @@ +#ifdef UNITTEST_DZBRIDGE + +#include "UnitTest_DzBridgeAction.h" +#include "DzBridgeAction_Scriptable.h" +#include "dzprogress.h" + +#include "dzbridge.h" + +using namespace DzBridgeNameSpace; + +/// +/// Constructor +/// +UnitTest_DzBridgeAction::UnitTest_DzBridgeAction() +{ + m_testObject = (QObject*) new ::DzBridgeAction(); +} + +/// +/// This class also tests DzBridgeAction indirectly since DzBridgeAction is an abstract base class. +/// +/// +bool UnitTest_DzBridgeAction::runUnitTests() +{ + if (!m_testObject) + { + return false; + } + + RUNTEST(_DzBridgeAction); + RUNTEST(resetToDefaults); + RUNTEST(cleanString); + RUNTEST(getAvailableMorphs); + RUNTEST(getActiveMorphs); + RUNTEST(makeNormalMapFromHeightMap); + RUNTEST(preProcessScene); + RUNTEST(renameDuplicateMaterial); + RUNTEST(undoRenameDuplicateMaterials); + RUNTEST(generateMissingNormalMap); + RUNTEST(undoGenerateMissingNormalMaps); + RUNTEST(getActionGroup); + RUNTEST(getDefaultMenuPath); + RUNTEST(exportAsset); + RUNTEST(exportNode); + RUNTEST(writeConfiguration); + RUNTEST(setExportOptions); + RUNTEST(readGuiRootFolder); + RUNTEST(writeDtuHeader); + RUNTEST(startMaterialBlock); + RUNTEST(finishMaterialBlock); + RUNTEST(writeAllMaterials); + RUNTEST(writeMaterialProperty); + RUNTEST(writeAllMorphs); + RUNTEST(writeMorphProperties); + RUNTEST(writeMorphJointLinkInfo); + RUNTEST(writeAllSubdivisions); + RUNTEST(writeSubdivisionProperties); + RUNTEST(writeAllDforceInfo); + RUNTEST(writeDforceMaterialProperties); + RUNTEST(writeDforceModifiers); + RUNTEST(writeEnvironment); + RUNTEST(writeInstances); + RUNTEST(writeInstance); + RUNTEST(writeAllPoses); + RUNTEST(renameDuplicateMaterials2); + RUNTEST(undoRenameDuplicateMaterials2); + RUNTEST(getScenePropList); + RUNTEST(disconnectNode); + RUNTEST(reconnectNodes); + RUNTEST(disconnectOverrideControllers); + RUNTEST(reconnectOverrideControllers); + RUNTEST(checkIfPoseExportIsDestructive); + RUNTEST(unlockTransform); + RUNTEST(getBridgeDialog); + RUNTEST(setBridgeDialog); + RUNTEST(getSubdivisionDialog); + RUNTEST(setSubdivisionDialog); + RUNTEST(getMorphSelectionDialog); + RUNTEST(setMorphSelectionDialog); + RUNTEST(getAssetType); + RUNTEST(setAssetType); + RUNTEST(getExportFilename); + RUNTEST(setExportFilename); + RUNTEST(getExportFolder); + RUNTEST(setExportFolder); + RUNTEST(getRootFolder); + RUNTEST(setRootFolder); + RUNTEST(getProductName); + RUNTEST(setProductName); + RUNTEST(getProductComponentName); + RUNTEST(setProductComponentName); + RUNTEST(getMorphList); + RUNTEST(setMorphList); + RUNTEST(getUseRelativePaths); + RUNTEST(setUseRelativePaths); + RUNTEST(isTemporaryFile); + RUNTEST(exportAssetWithDtu); + RUNTEST(writePropertyTexture); + RUNTEST(makeUniqueFilename); + RUNTEST(getUndoNormalMaps); + RUNTEST(setUndoNormalMaps); + RUNTEST(getNonInteractiveMode); + RUNTEST(setNonInteractiveMode); + RUNTEST(getExportFbx); + RUNTEST(setExportFbx); + RUNTEST(readGui); + RUNTEST(exportHD); + RUNTEST(upgradeToHD); + RUNTEST(writeWeightMaps); + RUNTEST(metaInvokeMethod); + RUNTEST(copyFile); + RUNTEST(getMD5); + + + return true; +} + +bool UnitTest_DzBridgeAction::_DzBridgeAction(UnitTest::TestResult *testResult) +{ + bool bResult = true; + LOGTEST_TEXT("DzBridgeAction is an abstract class. Can not test constructor."); + +/** + // Can not build because constructor for abstract class + DzBridgeAction *testObj = new DzBridgeAction(); + if (!testObj) + LOGTEST_FAILED(""); + else + LOGTEST_PASSED(""); +*/ + + return bResult; +} + +bool UnitTest_DzBridgeAction::resetToDefaults(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->resetToDefaults()); + return true; +} + +bool UnitTest_DzBridgeAction::cleanString(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->cleanString(nullptr)); + + TRY_METHODCALL_CUSTOM(qobject_cast(m_testObject)->cleanString(""), "C++ exception with empty string test."); + + return bResult; + +} + +bool UnitTest_DzBridgeAction::getAvailableMorphs(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getAvailableMorphs(new DzNode())); + + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->getAvailableMorphs(nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getActiveMorphs(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getActiveMorphs(new DzNode())); + + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->getActiveMorphs(nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::makeNormalMapFromHeightMap(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->makeNormalMapFromHeightMap(nullptr, 0.0)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::preProcessScene(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->preProcessScene(new DzNode())); + + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->preProcessScene(nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::renameDuplicateMaterial(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->renameDuplicateMaterial(nullptr, nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::undoRenameDuplicateMaterials(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->undoRenameDuplicateMaterials()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::generateMissingNormalMap(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->generateMissingNormalMap(nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::undoGenerateMissingNormalMaps(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->undoGenerateMissingNormalMaps()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getActionGroup(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getActionGroup()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getDefaultMenuPath(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getDefaultMenuPath()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::exportAsset(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->exportAsset()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::exportNode(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->exportNode(new DzNode())); + + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->exportNode(nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeConfiguration(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->writeConfiguration()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::setExportOptions(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzFileIOSettings arg; + TRY_METHODCALL(qobject_cast(m_testObject)->setExportOptions(arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::readGuiRootFolder(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->readGuiRootFolder()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeDtuHeader(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL(qobject_cast(m_testObject)->writeDTUHeader(arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::startMaterialBlock(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->startMaterialBlock(nullptr, arg, nullptr, nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::finishMaterialBlock(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL(qobject_cast(m_testObject)->finishMaterialBlock(arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeAllMaterials(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->writeAllMaterials(nullptr, arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeMaterialProperty(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->writeMaterialProperty(nullptr, arg, nullptr, nullptr, nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeAllMorphs(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL(qobject_cast(m_testObject)->writeAllMorphs(arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeMorphProperties(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL(qobject_cast(m_testObject)->writeMorphProperties(arg, "", "")); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeMorphJointLinkInfo(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg1(nullptr); + JointLinkInfo arg2; + TRY_METHODCALL(qobject_cast(m_testObject)->writeMorphJointLinkInfo(arg1, arg2)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeAllSubdivisions(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL(qobject_cast(m_testObject)->writeAllSubdivisions(arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeSubdivisionProperties(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL(qobject_cast(m_testObject)->writeSubdivisionProperties(arg, "", 0)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeAllDforceInfo(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->writeAllDforceInfo(nullptr, arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeDforceMaterialProperties(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->writeDforceMaterialProperties(arg, nullptr, nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeDforceModifiers(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg1(nullptr); + DzModifierList arg2; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->writeDforceModifiers(arg2, arg1, nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeEnvironment(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL(qobject_cast(m_testObject)->writeEnvironment(arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeInstances(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg1(nullptr); + QMap arg2; + QList arg3; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->writeInstances(nullptr, arg1, arg2, arg3)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeInstance(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->writeInstance(nullptr, arg, 0)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeAllPoses(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL(qobject_cast(m_testObject)->writeAllPoses(arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::renameDuplicateMaterials2(UnitTest::TestResult* testResult) +{ + bool bResult = true; + QList arg1; + QMap arg2; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->renameDuplicateMaterials(nullptr, arg1, arg2)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::undoRenameDuplicateMaterials2(UnitTest::TestResult* testResult) +{ + bool bResult = true; + QList arg1; + QMap arg2; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->undoRenameDuplicateMaterials(nullptr, arg1, arg2)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getScenePropList(UnitTest::TestResult* testResult) +{ + bool bResult = true; + QMap arg; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->getScenePropList(nullptr, arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::disconnectNode(UnitTest::TestResult* testResult) +{ + bool bResult = true; + QList arg; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->disconnectNode(nullptr, arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::reconnectNodes(UnitTest::TestResult* testResult) +{ + bool bResult = true; + QList arg; + TRY_METHODCALL(qobject_cast(m_testObject)->reconnectNodes(arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::disconnectOverrideControllers(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->disconnectOverrideControllers()); + + return bResult; + +} + +bool UnitTest_DzBridgeAction::reconnectOverrideControllers(UnitTest::TestResult* testResult) +{ + bool bResult = true; + QList arg; + TRY_METHODCALL(qobject_cast(m_testObject)->reconnectOverrideControllers(arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::checkIfPoseExportIsDestructive(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->checkIfPoseExportIsDestructive()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::unlockTransform(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->unlockTranform(nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getBridgeDialog(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getBridgeDialog()); + + return bResult; + +} + +bool UnitTest_DzBridgeAction::setBridgeDialog(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->setBridgeDialog(nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getSubdivisionDialog(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getSubdivisionDialog()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::setSubdivisionDialog(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->setSubdivisionDialog(nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getMorphSelectionDialog(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getMorphSelectionDialog()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::setMorphSelectionDialog(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->setMorphSelectionDialog(nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getAssetType(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getAssetType()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::setAssetType(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->setAssetType("")); + + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->setAssetType(nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getExportFilename(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getExportFilename()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::setExportFilename(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->setExportFilename("")); + + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->setExportFilename(nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getExportFolder(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getExportFolder()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::setExportFolder(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->setExportFolder("")); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getRootFolder(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getRootFolder()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::setRootFolder(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->setRootFolder("")); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getProductName(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getProductName()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::setProductName(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->setProductName("")); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getProductComponentName(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getProductComponentName()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::setProductComponentName(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->setProductComponentName("")); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getMorphList(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getMorphList()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::setMorphList(UnitTest::TestResult* testResult) +{ + bool bResult = true; + QStringList arg; + TRY_METHODCALL(qobject_cast(m_testObject)->setMorphList(arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getUseRelativePaths(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getUseRelativePaths()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::setUseRelativePaths(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->setUseRelativePaths(0)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::isTemporaryFile(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->isTemporaryFile("")); + + return bResult; +} + +bool UnitTest_DzBridgeAction::exportAssetWithDtu(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->exportAssetWithDtu("")); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writePropertyTexture(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL(qobject_cast(m_testObject)->writePropertyTexture(arg,"",0,"","")); + + return bResult; +} + +bool UnitTest_DzBridgeAction::makeUniqueFilename(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->makeUniqueFilename("")); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getUndoNormalMaps(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getUndoNormalMaps()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::setUndoNormalMaps(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->setUndoNormalMaps(0)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getNonInteractiveMode(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getNonInteractiveMode()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::setNonInteractiveMode(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->setNonInteractiveMode(0)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getExportFbx(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getExportFbx()); + + return bResult; +} + +bool UnitTest_DzBridgeAction::setExportFbx(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->setExportFbx("")); + + return bResult; +} + +bool UnitTest_DzBridgeAction::readGui(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->readGui(nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::exportHD(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzProgress* arg = new DzProgress("", 0, true, true); + arg->enable(false); + TRY_METHODCALL(qobject_cast(m_testObject)->exportHD(arg)); + arg->finish(); + + return bResult; +} + +bool UnitTest_DzBridgeAction::upgradeToHD(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->setNonInteractiveMode(true)); + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->upgradeToHD("","", "", nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::writeWeightMaps(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->writeWeightMaps(nullptr, arg)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::metaInvokeMethod(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->metaInvokeMethod(nullptr, nullptr, nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::copyFile(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->copyFile(nullptr, nullptr)); + + return bResult; +} + +bool UnitTest_DzBridgeAction::getMD5(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getMD5("")); + + return bResult; +} + + + + +#include "moc_UnitTest_DzBridgeAction.cpp" +#endif \ No newline at end of file diff --git a/Test/UnitTests/UnitTest_DzBridgeAction.h b/Test/UnitTests/UnitTest_DzBridgeAction.h new file mode 100644 index 0000000..c46a760 --- /dev/null +++ b/Test/UnitTests/UnitTest_DzBridgeAction.h @@ -0,0 +1,101 @@ +#pragma once +#ifdef UNITTEST_DZBRIDGE + +#include +#include "UnitTest.h" + +class UnitTest_DzBridgeAction : public UnitTest { + Q_OBJECT +public: + UnitTest_DzBridgeAction(); + + bool runUnitTests(); + +private: + bool _DzBridgeAction(UnitTest::TestResult* testResult); + bool resetToDefaults(UnitTest::TestResult* testResult); + bool cleanString(UnitTest::TestResult* testResult); + bool getAvailableMorphs(UnitTest::TestResult* testResult); + bool getActiveMorphs(UnitTest::TestResult* testResult); + bool makeNormalMapFromHeightMap(UnitTest::TestResult* testResult); + bool preProcessScene(UnitTest::TestResult* testResult); + bool renameDuplicateMaterial(UnitTest::TestResult* testResult); + bool undoRenameDuplicateMaterials(UnitTest::TestResult* testResult); + bool generateMissingNormalMap(UnitTest::TestResult* testResult); + bool undoGenerateMissingNormalMaps(UnitTest::TestResult* testResult); + bool getActionGroup(UnitTest::TestResult* testResult); + bool getDefaultMenuPath(UnitTest::TestResult* testResult); + bool exportAsset(UnitTest::TestResult* testResult); + bool exportNode(UnitTest::TestResult* testResult); + bool writeConfiguration(UnitTest::TestResult* testResult); + bool setExportOptions(UnitTest::TestResult* testResult); + bool readGuiRootFolder(UnitTest::TestResult* testResult); + bool writeDtuHeader(UnitTest::TestResult* testResult); + bool startMaterialBlock(UnitTest::TestResult* testResult); + bool finishMaterialBlock(UnitTest::TestResult* testResult); + bool writeAllMaterials(UnitTest::TestResult* testResult); + bool writeMaterialProperty(UnitTest::TestResult* testResult); + bool writeAllMorphs(UnitTest::TestResult* testResult); + bool writeMorphProperties(UnitTest::TestResult* testResult); + bool writeMorphJointLinkInfo(UnitTest::TestResult* testResult); + bool writeAllSubdivisions(UnitTest::TestResult* testResult); + bool writeSubdivisionProperties(UnitTest::TestResult* testResult); + bool writeAllDforceInfo(UnitTest::TestResult* testResult); + bool writeDforceMaterialProperties(UnitTest::TestResult* testResult); + bool writeDforceModifiers(UnitTest::TestResult* testResult); + bool writeEnvironment(UnitTest::TestResult* testResult); + bool writeInstances(UnitTest::TestResult* testResult); + bool writeInstance(UnitTest::TestResult* testResult); + bool writeAllPoses(UnitTest::TestResult* testResult); + bool renameDuplicateMaterials2(UnitTest::TestResult* testResult); + bool undoRenameDuplicateMaterials2(UnitTest::TestResult* testResult); + bool getScenePropList(UnitTest::TestResult* testResult); + bool disconnectNode(UnitTest::TestResult* testResult); + bool reconnectNodes(UnitTest::TestResult* testResult); + bool disconnectOverrideControllers(UnitTest::TestResult* testResult); + bool reconnectOverrideControllers(UnitTest::TestResult* testResult); + bool checkIfPoseExportIsDestructive(UnitTest::TestResult* testResult); + bool unlockTransform(UnitTest::TestResult* testResult); + bool getBridgeDialog(UnitTest::TestResult* testResult); + bool setBridgeDialog(UnitTest::TestResult* testResult); + bool getSubdivisionDialog(UnitTest::TestResult* testResult); + bool setSubdivisionDialog(UnitTest::TestResult* testResult); + bool getMorphSelectionDialog(UnitTest::TestResult* testResult); + bool setMorphSelectionDialog(UnitTest::TestResult* testResult); + bool getAssetType(UnitTest::TestResult* testResult); + bool setAssetType(UnitTest::TestResult* testResult); + bool getExportFilename(UnitTest::TestResult* testResult); + bool setExportFilename(UnitTest::TestResult* testResult); + bool getExportFolder(UnitTest::TestResult* testResult); + bool setExportFolder(UnitTest::TestResult* testResult); + bool getRootFolder(UnitTest::TestResult* testResult); + bool setRootFolder(UnitTest::TestResult* testResult); + bool getProductName(UnitTest::TestResult* testResult); + bool setProductName(UnitTest::TestResult* testResult); + bool getProductComponentName(UnitTest::TestResult* testResult); + bool setProductComponentName(UnitTest::TestResult* testResult); + bool getMorphList(UnitTest::TestResult* testResult); + bool setMorphList(UnitTest::TestResult* testResult); + bool getUseRelativePaths(UnitTest::TestResult* testResult); + bool setUseRelativePaths(UnitTest::TestResult* testResult); + bool isTemporaryFile(UnitTest::TestResult* testResult); + bool exportAssetWithDtu(UnitTest::TestResult* testResult); + bool writePropertyTexture(UnitTest::TestResult* testResult); + bool makeUniqueFilename(UnitTest::TestResult* testResult); + bool getUndoNormalMaps(UnitTest::TestResult* testResult); + bool setUndoNormalMaps(UnitTest::TestResult* testResult); + bool getNonInteractiveMode(UnitTest::TestResult* testResult); + bool setNonInteractiveMode(UnitTest::TestResult* testResult); + bool getExportFbx(UnitTest::TestResult* testResult); + bool setExportFbx(UnitTest::TestResult* testResult); + bool readGui(UnitTest::TestResult* testResult); + bool exportHD(UnitTest::TestResult* testResult); + bool upgradeToHD(UnitTest::TestResult* testResult); + bool writeWeightMaps(UnitTest::TestResult* testResult); + bool metaInvokeMethod(UnitTest::TestResult* testResult); + bool copyFile(UnitTest::TestResult* testResult); + bool getMD5(UnitTest::TestResult* testResult); + +}; + +#endif \ No newline at end of file diff --git a/Test/UnitTests/UnitTest_DzBridgeDialog.cpp b/Test/UnitTests/UnitTest_DzBridgeDialog.cpp new file mode 100644 index 0000000..cf3456c --- /dev/null +++ b/Test/UnitTests/UnitTest_DzBridgeDialog.cpp @@ -0,0 +1,205 @@ +#ifdef UNITTEST_DZBRIDGE + +#include "UnitTest_DzBridgeDialog.h" +#include "DzBridgeDialog.h" + +#include "dzbridge.h" +using namespace DzBridgeNameSpace; + +UnitTest_DzBridgeDialog::UnitTest_DzBridgeDialog() +{ + m_testObject = (QObject*) new DzBridgeDialog(); +} + +bool UnitTest_DzBridgeDialog::runUnitTests() +{ + DzBridgeDialog* testObject = new DzBridgeDialog(); + + if (!testObject) + { + return false; + } + + RUNTEST(getAssetNameEdit); + RUNTEST(getAssetTypeCombo); + RUNTEST(getMorphsEnabledCheckBox); + RUNTEST(getSubdivisionEnabledCheckBox); + RUNTEST(getAdvancedSettingsGroupBox); + RUNTEST(getFbxVersionCombo); + RUNTEST(getShowFbxDialogCheckBox); + RUNTEST(_DzBridgeDialog); + RUNTEST(GetMorphString); + RUNTEST(GetMorphMapping); + RUNTEST(resetToDefaults); + RUNTEST(loadSavedSettings); + RUNTEST(Accepted); + RUNTEST(handleSceneSelectionChanged); + RUNTEST(HandleChooseMorphsButton); + RUNTEST(HandleMorphsCheckBoxChange); + RUNTEST(HandleChooseSubdivisionsButton); + RUNTEST(HandleFBXVersionChange); + RUNTEST(HandleShowFbxDialogCheckBoxChange); + RUNTEST(HandleExportMaterialPropertyCSVCheckBoxChange); + RUNTEST(HandleShowAdvancedSettingsCheckBoxChange); + RUNTEST(refreshAsset); + + return true; +} + +bool UnitTest_DzBridgeDialog::getAssetNameEdit(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getAssetNameEdit()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::getAssetTypeCombo(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getAssetTypeCombo()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::getMorphsEnabledCheckBox(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getMorphsEnabledCheckBox()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::getSubdivisionEnabledCheckBox(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getSubdivisionEnabledCheckBox()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::getAdvancedSettingsGroupBox(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getAdvancedSettingsGroupBox()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::getFbxVersionCombo(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getFbxVersionCombo()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::getShowFbxDialogCheckBox(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getShowFbxDialogCheckBox()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::_DzBridgeDialog(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(new DzBridgeDialog()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::GetMorphString(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->GetMorphString()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::GetMorphMapping(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->GetMorphMapping()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::resetToDefaults(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->resetToDefaults()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::loadSavedSettings(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->loadSavedSettings()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::Accepted(UnitTest::TestResult* testResult) +{ + bool bResult = true; + LOGTEST_TEXT("Accepted is Qt framework GUI method. Skipping UnitTest..."); +// TRY_METHODCALL(qobject_cast(m_testObject)->Accepted()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::handleSceneSelectionChanged(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->handleSceneSelectionChanged()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::HandleChooseMorphsButton(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleChooseMorphsButton()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::HandleMorphsCheckBoxChange(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleMorphsCheckBoxChange(0)); + return bResult; +} + +bool UnitTest_DzBridgeDialog::HandleChooseSubdivisionsButton(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleChooseSubdivisionsButton()); + return bResult; +} + +bool UnitTest_DzBridgeDialog::HandleFBXVersionChange(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleFBXVersionChange(0)); + return bResult; +} + +bool UnitTest_DzBridgeDialog::HandleShowFbxDialogCheckBoxChange(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleShowFbxDialogCheckBoxChange(0)); + return bResult; +} + +bool UnitTest_DzBridgeDialog::HandleExportMaterialPropertyCSVCheckBoxChange(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleExportMaterialPropertyCSVCheckBoxChange(0)); + return bResult; +} + +bool UnitTest_DzBridgeDialog::HandleShowAdvancedSettingsCheckBoxChange(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleShowAdvancedSettingsCheckBoxChange(0)); + return bResult; +} + +bool UnitTest_DzBridgeDialog::refreshAsset(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->refreshAsset()); + return bResult; +} + +#include "moc_UnitTest_DzBridgeDialog.cpp" +#endif \ No newline at end of file diff --git a/Test/UnitTests/UnitTest_DzBridgeDialog.h b/Test/UnitTests/UnitTest_DzBridgeDialog.h new file mode 100644 index 0000000..0d8f2fa --- /dev/null +++ b/Test/UnitTests/UnitTest_DzBridgeDialog.h @@ -0,0 +1,40 @@ +#pragma once +#ifdef UNITTEST_DZBRIDGE + +#include +#include "UnitTest.h" + +class UnitTest_DzBridgeDialog : public UnitTest { + Q_OBJECT +public: + UnitTest_DzBridgeDialog(); + bool runUnitTests(); + +private: + bool getAssetNameEdit(UnitTest::TestResult* testResult); + bool getAssetTypeCombo(UnitTest::TestResult* testResult); + bool getMorphsEnabledCheckBox(UnitTest::TestResult* testResult); + bool getSubdivisionEnabledCheckBox(UnitTest::TestResult* testResult); + bool getAdvancedSettingsGroupBox(UnitTest::TestResult* testResult); + bool getFbxVersionCombo(UnitTest::TestResult* testResult); + bool getShowFbxDialogCheckBox(UnitTest::TestResult* testResult); + bool _DzBridgeDialog(UnitTest::TestResult* testResult); + bool GetMorphString(UnitTest::TestResult* testResult); + bool GetMorphMapping(UnitTest::TestResult* testResult); + bool resetToDefaults(UnitTest::TestResult* testResult); + bool loadSavedSettings(UnitTest::TestResult* testResult); + bool Accepted(UnitTest::TestResult* testResult); + bool handleSceneSelectionChanged(UnitTest::TestResult* testResult); + bool HandleChooseMorphsButton(UnitTest::TestResult* testResult); + bool HandleMorphsCheckBoxChange(UnitTest::TestResult* testResult); + bool HandleChooseSubdivisionsButton(UnitTest::TestResult* testResult); + bool HandleFBXVersionChange(UnitTest::TestResult* testResult); + bool HandleShowFbxDialogCheckBoxChange(UnitTest::TestResult* testResult); + bool HandleExportMaterialPropertyCSVCheckBoxChange(UnitTest::TestResult* testResult); + bool HandleShowAdvancedSettingsCheckBoxChange(UnitTest::TestResult* testResult); + bool refreshAsset(UnitTest::TestResult* testResult); + +}; + + +#endif \ No newline at end of file diff --git a/Test/UnitTests/UnitTest_DzBridgeMorphSelectionDialog.cpp b/Test/UnitTests/UnitTest_DzBridgeMorphSelectionDialog.cpp new file mode 100644 index 0000000..3bae0b2 --- /dev/null +++ b/Test/UnitTests/UnitTest_DzBridgeMorphSelectionDialog.cpp @@ -0,0 +1,188 @@ +#ifdef UNITTEST_DZBRIDGE + +#include "UnitTest_DzBridgeMorphSelectionDialog.h" +#include "DzBridgeMorphSelectionDialog.h" + +#include "dzbridge.h" +using namespace DzBridgeNameSpace; + +UnitTest_DzBridgeMorphSelectionDialog::UnitTest_DzBridgeMorphSelectionDialog() +{ + m_testObject = (QObject*) new DzBridgeMorphSelectionDialog(); +} + +bool UnitTest_DzBridgeMorphSelectionDialog::runUnitTests() +{ + DzBridgeMorphSelectionDialog* testObject = new DzBridgeMorphSelectionDialog(); + + if (!testObject) + { + return false; + } + + RUNTEST(_DzBridgeMorphSelectionDialog); + RUNTEST(PrepareDialog); + RUNTEST(GetMorphString); + RUNTEST(GetMorphCSVString); + RUNTEST(GetMorphRenaming); + RUNTEST(IsAutoJCMEnabled); + RUNTEST(GetActiveJointControlledMorphs); + RUNTEST(GetMorphLabelFromName); + RUNTEST(FilterChanged); + RUNTEST(ItemSelectionChanged); + RUNTEST(HandleAddMorphsButton); + RUNTEST(HandleRemoveMorphsButton); + RUNTEST(HandleSavePreset); + RUNTEST(HandlePresetChanged); + RUNTEST(HandleArmJCMMorphsButton); + RUNTEST(HandleLegJCMMorphsButton); + RUNTEST(HandleTorsoJCMMorphsButton); + RUNTEST(HandleARKitGenesis81MorphsButton); + RUNTEST(HandleFaceFXGenesis8Button); + RUNTEST(HandleAutoJCMCheckBoxChange); + + return true; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::_DzBridgeMorphSelectionDialog(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(new DzBridgeMorphSelectionDialog()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::PrepareDialog(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->PrepareDialog()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::GetMorphString(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->GetMorphString()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::GetMorphCSVString(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->GetMorphCSVString()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::GetMorphRenaming(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->GetMorphRenaming()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::IsAutoJCMEnabled(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->IsAutoJCMEnabled()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::GetActiveJointControlledMorphs(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->GetActiveJointControlledMorphs()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::GetMorphLabelFromName(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->GetMorphLabelFromName("")); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::FilterChanged(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->FilterChanged("")); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::ItemSelectionChanged(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->ItemSelectionChanged()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::HandleAddMorphsButton(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleAddMorphsButton()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::HandleRemoveMorphsButton(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleRemoveMorphsButton()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::HandleSavePreset(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleSavePreset()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::HandlePresetChanged(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandlePresetChanged("")); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::HandleArmJCMMorphsButton(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleArmJCMMorphsButton()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::HandleLegJCMMorphsButton(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleLegJCMMorphsButton()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::HandleTorsoJCMMorphsButton(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleTorsoJCMMorphsButton()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::HandleARKitGenesis81MorphsButton(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleARKitGenesis81MorphsButton()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::HandleFaceFXGenesis8Button(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleFaceFXGenesis8Button()); + return bResult; +} + +bool UnitTest_DzBridgeMorphSelectionDialog::HandleAutoJCMCheckBoxChange(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleAutoJCMCheckBoxChange(false)); + return bResult; +} + +#include "moc_UnitTest_DzBridgeMorphSelectionDialog.cpp" +#endif \ No newline at end of file diff --git a/Test/UnitTests/UnitTest_DzBridgeMorphSelectionDialog.h b/Test/UnitTests/UnitTest_DzBridgeMorphSelectionDialog.h new file mode 100644 index 0000000..95ddf58 --- /dev/null +++ b/Test/UnitTests/UnitTest_DzBridgeMorphSelectionDialog.h @@ -0,0 +1,39 @@ +#pragma once +#ifdef UNITTEST_DZBRIDGE + +#include +#include "UnitTest.h" + +class UnitTest_DzBridgeMorphSelectionDialog : public UnitTest { + Q_OBJECT +public: + UnitTest_DzBridgeMorphSelectionDialog(); + bool runUnitTests(); + +private: + bool _DzBridgeMorphSelectionDialog(UnitTest::TestResult* testResult); + bool PrepareDialog(UnitTest::TestResult* testResult); + bool GetMorphString(UnitTest::TestResult* testResult); + bool GetMorphCSVString(UnitTest::TestResult* testResult); + bool GetMorphRenaming(UnitTest::TestResult* testResult); + bool IsAutoJCMEnabled(UnitTest::TestResult* testResult); + bool GetActiveJointControlledMorphs(UnitTest::TestResult* testResult); + bool GetMorphLabelFromName(UnitTest::TestResult* testResult); + bool FilterChanged(UnitTest::TestResult* testResult); + bool ItemSelectionChanged(UnitTest::TestResult* testResult); + bool HandleAddMorphsButton(UnitTest::TestResult* testResult); + bool HandleRemoveMorphsButton(UnitTest::TestResult* testResult); + bool HandleSavePreset(UnitTest::TestResult* testResult); + bool HandlePresetChanged(UnitTest::TestResult* testResult); + bool HandleArmJCMMorphsButton(UnitTest::TestResult* testResult); + bool HandleLegJCMMorphsButton(UnitTest::TestResult* testResult); + bool HandleTorsoJCMMorphsButton(UnitTest::TestResult* testResult); + bool HandleARKitGenesis81MorphsButton(UnitTest::TestResult* testResult); + bool HandleFaceFXGenesis8Button(UnitTest::TestResult* testResult); + bool HandleAutoJCMCheckBoxChange(UnitTest::TestResult* testResult); + + +}; + + +#endif \ No newline at end of file diff --git a/Test/UnitTests/UnitTest_DzBridgeSubdivisionDialog.cpp b/Test/UnitTests/UnitTest_DzBridgeSubdivisionDialog.cpp new file mode 100644 index 0000000..e8f3d0a --- /dev/null +++ b/Test/UnitTests/UnitTest_DzBridgeSubdivisionDialog.cpp @@ -0,0 +1,110 @@ +#ifdef UNITTEST_DZBRIDGE + +#include "UnitTest_DzBridgeSubdivisionDialog.h" +#include "DzBridgeSubdivisionDialog.h" + +#include "dzbridge.h" +using namespace DzBridgeNameSpace; + +UnitTest_DzBridgeSubdivisionDialog::UnitTest_DzBridgeSubdivisionDialog() +{ + m_testObject = (QObject*) new DzBridgeSubdivisionDialog(); +} + +bool UnitTest_DzBridgeSubdivisionDialog::runUnitTests() +{ + DzBridgeSubdivisionDialog* testObject = new DzBridgeSubdivisionDialog(); + + if (!testObject) + { + return false; + } + + RUNTEST(_DzBridgeSubdivisionDialog); + RUNTEST(getSubdivisionCombos); + RUNTEST(PrepareDialog); + RUNTEST(LockSubdivisionProperties); + RUNTEST(WriteSubdivisions); + RUNTEST(FindObject); + RUNTEST(setSubdivisionLevelByNode); + RUNTEST(UnlockSubdivisionProperties); + RUNTEST(GetLookupTable); + RUNTEST(HandleSubdivisionLevelChanged); + + return true; +} + +bool UnitTest_DzBridgeSubdivisionDialog::_DzBridgeSubdivisionDialog(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(new DzBridgeSubdivisionDialog()); + return bResult; +} + +bool UnitTest_DzBridgeSubdivisionDialog::getSubdivisionCombos(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->getSubdivisionCombos()); + return bResult; +} + +bool UnitTest_DzBridgeSubdivisionDialog::PrepareDialog(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->PrepareDialog()); + return bResult; +} + +bool UnitTest_DzBridgeSubdivisionDialog::LockSubdivisionProperties(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->LockSubdivisionProperties(false)); + return bResult; +} + +bool UnitTest_DzBridgeSubdivisionDialog::WriteSubdivisions(UnitTest::TestResult* testResult) +{ + bool bResult = true; + DzJsonWriter arg(nullptr); + TRY_METHODCALL(qobject_cast(m_testObject)->WriteSubdivisions(arg)); + return bResult; +} + +bool UnitTest_DzBridgeSubdivisionDialog::FindObject(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->FindObject(nullptr, "")); + return bResult; +} + +bool UnitTest_DzBridgeSubdivisionDialog::setSubdivisionLevelByNode(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL_NULLPTR(qobject_cast(m_testObject)->setSubdivisionLevelByNode(nullptr, 0)); + return bResult; +} + +bool UnitTest_DzBridgeSubdivisionDialog::UnlockSubdivisionProperties(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->UnlockSubdivisionProperties()); + return bResult; +} + +bool UnitTest_DzBridgeSubdivisionDialog::GetLookupTable(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->GetLookupTable()); + return bResult; +} + +bool UnitTest_DzBridgeSubdivisionDialog::HandleSubdivisionLevelChanged(UnitTest::TestResult* testResult) +{ + bool bResult = true; + TRY_METHODCALL(qobject_cast(m_testObject)->HandleSubdivisionLevelChanged("")); + return bResult; +} + + +#include "moc_UnitTest_DzBridgeSubdivisionDialog.cpp" +#endif \ No newline at end of file diff --git a/Test/UnitTests/UnitTest_DzBridgeSubdivisionDialog.h b/Test/UnitTests/UnitTest_DzBridgeSubdivisionDialog.h new file mode 100644 index 0000000..adb9137 --- /dev/null +++ b/Test/UnitTests/UnitTest_DzBridgeSubdivisionDialog.h @@ -0,0 +1,28 @@ +#pragma once +#ifdef UNITTEST_DZBRIDGE + +#include +#include "UnitTest.h" + +class UnitTest_DzBridgeSubdivisionDialog : public UnitTest { + Q_OBJECT +public: + UnitTest_DzBridgeSubdivisionDialog(); + bool runUnitTests(); + +private: + bool _DzBridgeSubdivisionDialog(UnitTest::TestResult* testResult); + bool getSubdivisionCombos(UnitTest::TestResult* testResult); + bool PrepareDialog(UnitTest::TestResult* testResult); + bool LockSubdivisionProperties(UnitTest::TestResult* testResult); + bool WriteSubdivisions(UnitTest::TestResult* testResult); + bool FindObject(UnitTest::TestResult* testResult); + bool setSubdivisionLevelByNode(UnitTest::TestResult* testResult); + bool UnlockSubdivisionProperties(UnitTest::TestResult* testResult); + bool GetLookupTable(UnitTest::TestResult* testResult); + bool HandleSubdivisionLevelChanged(UnitTest::TestResult* testResult); + +}; + + +#endif \ No newline at end of file diff --git a/Test/testcases/QA_Utility_Functions.dsa b/Test/testcases/QA_Utility_Functions.dsa new file mode 100644 index 0000000..5f11f51 --- /dev/null +++ b/Test/testcases/QA_Utility_Functions.dsa @@ -0,0 +1,361 @@ +// DAZ Studio version 4.16.0.3 filetype DAZ Script + +var sOutputPath = Global_sOutputPath; +var sLogFile = sOutputPath + "/" + "temp_log.txt"; +var sJsonFile = sOutputPath + "/" + "TestCase_Results.json" +var sReportFile = sOutputPath + "/" + "TestCase_Results.txt" + +function writeLogToReport() +{ + var oFile = new DzFile(sReportFile); + oFile.open( DzFile.WriteOnly ); + oFile.write(readLogText()); + oFile.close(); +} + +function clearLog() +{ + var oFile = new DzFile(sLogFile); + oFile.open( DzFile.WriteOnly ); + oFile.write(""); + oFile.close(); +} + +function clearJson() +{ + var oFile = new DzFile(sJsonFile); + oFile.open( DzFile.WriteOnly ); + oFile.write(""); + oFile.close(); +} + +function printToLog(sText) +{ + print(sText); + var oFile = new DzFile(sLogFile); + oFile.open( DzFile.Append ); + oFile.write(sText + "\n"); + oFile.close(); +} + +function logTxt(logText) +{ + var file = new DzFile(sLogFile); + file.open( DzFile.Append ); + file.write(logText); + file.close(); +} + +function readLogText() +{ + var file = new DzFile(sLogFile); + file.open( DzFile.ReadOnly); + return file.read(); +} + +function logToJson(testCase, result) +{ + var TestCase = testCase; + var file = new DzFile(sJsonFile); + file.open( DzFile.Append); + file.write( + JSON.stringify({ + "TestCase ID": TestCase, + "Test Result": result, + "Time": Date(), + "Test Log":readLogText() + }, null, "\t")); + file.write("\n"); + file.close(); + clearLog(); + + return result; +} + +///////////////////////////////////// +// Validation functions +///////////////////////////////////// +function Validate_DTU_file( sDtuFilename ) +{ +/* + var oFile = new DzFile( sDtuFilename ); + if ( !oFile.exists() ) + { + printToLog("DTU File not found [FAILED]."); + return false; + } + + oFile.open( DzFile.ReadOnly ); + if (!oFile) + { + printToLog("Unable to open DTU file [FAILED]."); + return false; + } + + var sDTU_contents = oFile.read(); + if (!sDTU_contents) + { + printToLog("Unable to read file [FAILED]."); + return false; + } + + var oDTU = {}; + try + { + oDTU = JSON.parse(sDTU_contents); + } + catch (e) + { + oDTU = false; + } +*/ + var oDTU = Load_DTU_file(sDtuFilename); + + if (!oDTU) + { + printToLog("DTU: Invalid JSON format [FAILED]."); + return false; + } + else + { + printToLog("DTU: Valid JSON format [OK]."); + return true; + } + return false; +} + +function Load_DTU_file (sDtuFilename) +{ + var oFile = new DzFile( sDtuFilename ); + if ( !oFile.exists() ) + { + printToLog("DTU File not found [FAILED]."); + return false; + } + + oFile.open( DzFile.ReadOnly ); + if (!oFile) + { + printToLog("Unable to open DTU file [FAILED]."); + return false; + } + + var sDTU_contents = oFile.read(); + if (!sDTU_contents) + { + printToLog("Unable to read file [FAILED]."); + return false; + } + + var oDTU = {}; + try + { + oDTU = JSON.parse(sDTU_contents); + } + catch (e) + { + oDTU = false; + } + + return oDTU; +} + +function Validate_FBX_file (sFbxFilename) +{ + var oFBXi = new OpenFBXInterface(); + var result = oFBXi.LoadScene(sFbxFilename); + if (result) + { + printToLog("FBX fileformat check passed: valid FBX file"); + return true; + } + else + { + printToLog("FBX fileformat check failed: invalid FBX file"); + return false; + } + return false; +} + +function Validate_NormalMaps (arrNormalMapList, sDTUpath) +{ + var sExportTexturesFolder = sDTUpath + "/" + "exporttextures"; + //printToLog(sExportTexturesFolder) + var oFolderInfo = new DzDir( sExportTexturesFolder ); + if ( !oFolderInfo.exists() ) + { + printToLog("Normal map folder not found [FAILED]."); + return false; + } + else + { + printToLog("Generated Normal map folder found:"); + } + oFolderInfo.setNameFilters(["*_nm.png"]); + var numFiles = oFolderInfo.count(); + if (numFiles != arrNormalMapList.length) + { + printToLog("Incorrect number of normal maps found [FAILED]."); + return false; + } + for (i=0; i < arrNormalMapList.length ; i++) + { + sNormalMapPath = sExportTexturesFolder + "/" + arrNormalMapList[i]; + var oFileInfo = new DzFileInfo(sNormalMapPath); + if (oFileInfo.exists()) + { + printToLog("Generated normal map found: " + arrNormalMapList[i] + ":"); + if (Validate_Image_Format(sNormalMapPath) == false) + { + return false; + } + } + else + { + printToLog("Normal map not found: " + arrNormalMapList[i] + " [FAILED]"); + return false; + } + } + + ///////////////////////////////////////////// + // TODO: Check undoGenerateMissingNormalMaps() + // Check All Surfaces to Verify that + // added Normal Maps were properly + // removed. + ///////////////////////////////////////////// + + return true; +} + +function Validate_Image_Format(sImagePath) +{ + var img = new QImage; + if (img.load(sImagePath) == true) + { + printToLog(" - valid image format. [OK]"); + return true; + } + else + { + printToLog(" - invalid format. [FAILED]") + return false; + } + + print ("UNKNOWN ERROR: Validate_Image_Format( \"" + sImagePath + "\" )"); + return false; +} + +function Validate_LIE_Textures(nNumLIETextures, sNameFilters, sDTUpath) +{ + var sExportTexturesFolder = sDTUpath + "/" + "exporttextures"; + var oTextureFolder = DzDir(sExportTexturesFolder); + if (oTextureFolder.exists() == false) + { + printToLog("ExportTextures subfolder was NOT generated [FAILED]."); + return false; + } + + oTextureFolder.setNameFilters(sNameFilters); + var oFileList = oTextureFolder.entryList(); + + if (oFileList.length != nNumLIETextures) + { + printToLog("Incorrect number of LIE textures found [FAILED]."); + return false; + } + for (var i=0; i < oFileList.length; i++) + { + var sTexturePath = sExportTexturesFolder + "/" + oFileList[i]; + printToLog("LIE texture found: " + oFileList[i] + ":"); + if (Validate_Image_Format(sTexturePath) == false) + { + return false; + } + } + + return true; +} + +///////////////////////////////////// +// Run Test Case function +///////////////////////////////////// +function Run_Exporter(sExportFilename, sAssetType, sRootFolder, sExportFolder, sProductName, sComponentName, arrayMorphList) +{ +// var oBridge = new DzUnrealAction(); +// var oBridge = new DzUnityAction(); + var oBridge = new DzBridgeAction(); + oBridge.resetToDefaults(); + + var sResultString = Run_Exporter2(oBridge, sExportFilename, sAssetType, sRootFolder, sExportFolder, sProductName, sComponentName, arrayMorphList); + + return sResultString; +} + +// Run_Exporter2(): +// Description: Like Run_Exporter() but does not call resetToDefaults() and does not initialize DzUnrealAction() +function Run_Exporter2(oBridge, sExportFilename, sAssetType, sRootFolder, sExportFolder, sProductName, sComponentName, arrayMorphList) +{ + var obj = oBridge; + obj.setNonInteractiveMode(1); + + // obj.UseRelativePaths: set true for relative Daz Library runtime paths + // set false for absolute filepath + // Default to true to work with interactive mode + obj.setUseRelativePaths(false); + // obj.UseRelativePaths = true; + + // obj.ExportFilename: filename stem for DTU and FBX file + // Can not have spaces or hyphen. Underscore is OK + // Leave blank to default to sanitized Scene Node Label + if (sExportFilename != "") + { + obj.setExportFilename(sExportFilename); + } + + // obj.AssetType: "SkeletalMesh" [DEFAULT], "StaticMesh", "Animation", "Environment", "Pose" + if (sAssetType != "") + { + obj.setAssetType(sAssetType); + } + + // obj.RootFolder: path to destination root + // If folder or path doesn't exist, it will be created + if (sRootFolder != "") + { + obj.setRootFolder(sRootFolder); + } + + // obj.ExportFolder: name folder containing DTU/FBX + // Leave blank to default to sanitized Scene Node Label like ExportFilename + if (sExportFolder != "") + { + obj.setExportFolder(sExportFolder); + } + + // obj.ProductName: Daz Store Product Name (or anything you want), can have spaces & special characters + if (sProductName != "") + { + obj.setProductName(sProductName); + } + + // obj.ProductComponentName: Friendly Name for component within Product + // Put frienldy pose name or material name here + if (sComponentName != "") + { + obj.setProductComponentName(sComponentName); + } + + // obj.MorphList: String array of morphs to convert into blendshapes within FBX. + // Leave empty if you do not want to export any blendshapes. + //obj.MorphList = ["CTRLVictoria8_1", "FHMVictoria8_1", "FBMVictoria8_1"] + if (arrayMorphList.length > 0) + { + obj.setMorphList(arrayMorphList); + } + + obj.executeAction() + + var sReturnString = obj.getRootFolder() + "/" + obj.getExportFolder() + "/" + obj.getExportFilename() + ".dtu" + + return sReturnString; +} diff --git a/Test/testcases/_TC1001.dsa b/Test/testcases/_TC1001.dsa new file mode 100644 index 0000000..966b11d --- /dev/null +++ b/Test/testcases/_TC1001.dsa @@ -0,0 +1,57 @@ +// Script-Only Test Case +function Run_TestCase_1001(sTestAsset) +{ + sExportFilename = "CustomAsset" + sAssetType = "SkeletalMesh" + sRootFolder = "C:/CustomRoot" + sExportFolder = "CustomFolder" + sProductName = "" + sComponentName = "" + arrayMorphList = [] + + // For next dzbridge version +// bGenerateNormalMaps = true + + printToLog("Running Test Case 1001:") + + Scene.clear() + var oContentMgr = App.getContentMgr() + + var sFullPath = oContentMgr.findFile(sTestAsset) + oContentMgr.openFile(sFullPath) + + var sReturnString = Run_Exporter(sExportFilename, sAssetType, sRootFolder, sExportFolder, sProductName, sComponentName, arrayMorphList) + var sDtuFilename = sReturnString + + // check for expected root folder, export folder and export filename (TC5.11) + if (sDtuFilename.lower().find("/customroot/customfolder/customasset.dtu") == -1) + { + printToLog("Test Case 1001 FAILED: Incorrect output DTU filename"); + return false; + } + + printToLog("Exported DTU = " + sDtuFilename); + if (Validate_DTU_file(sDtuFilename) == false) + { + return false; + } + + var sFbxFilename = sDtuFilename; + sFbxFilename = sFbxFilename.replace(".dtu",".fbx"); + printToLog("Exported FBX = " + sFbxFilename); + + if (Validate_FBX_file(sFbxFilename) == false) + { + return false; + } + + // var arrNormalMapList = ["G8_1FBaseBodyMapB_1003_nm.png", "G8_1FBaseFaceMapB_1001_nm.png", "G8_1FBaseHeadMapB_1002_nm.png", + // "G8FBaseArmsMapB_1004_nm.png", "G8FBaseLegsMapB_1003_nm.png"] + // var sDTUpath = DzFileInfo(sDtuFilename).path() + // if (Validate_NormalMaps(arrNormalMapList, sDTUpath) == false) + // { + // return false; + // } + + return true; +} diff --git a/Test/testcases/test_runner.dsa b/Test/testcases/test_runner.dsa new file mode 100644 index 0000000..cadd059 --- /dev/null +++ b/Test/testcases/test_runner.dsa @@ -0,0 +1,108 @@ +// DAZ Studio version 4.16.0.3 filetype DAZ Script +var includeDir_oFILE = new DzFile( getScriptFileName() ); +//var sIncludePath = includeDir_oFILE.path(); +var sIncludePath = "C:/GitHub/DazBridgeUtils-daz3d/Test/TestCases/" + +Global_sOutputPath = "C:/GitHub/DazBridgeUtils-daz3d/Test/Results/"; + +include(sIncludePath + "_TC1001.dsa") +/* +include(sIncludePath + "TC01.dsa") +include(sIncludePath + "TC02.dsa") +include(sIncludePath + "TC03.dsa") +include(sIncludePath + "TC04.dsa") +include(sIncludePath + "TC05.dsa") +include(sIncludePath + "TC06.dsa") +include(sIncludePath + "TC07.dsa") +include(sIncludePath + "TC08.dsa") +include(sIncludePath + "TC09.dsa") +include(sIncludePath + "TC10.dsa") +include(sIncludePath + "TC11.dsa") +include(sIncludePath + "TC12.dsa") +include(sIncludePath + "TC13.dsa") +include(sIncludePath + "TC14.dsa") +include(sIncludePath + "TC15.dsa") +include(sIncludePath + "TC16.dsa") +include(sIncludePath + "TC17.dsa") +include(sIncludePath + "TC18.dsa") +include(sIncludePath + "TC19b.dsa") +include(sIncludePath + "TC20.dsa") +*/ + +include(sIncludePath + "QA_Utility_Functions.dsa") + +function main() +{ + var aTCResults = new Array(1); + + clearLog(); + clearJson(); + var i=0; + aTCResults[i] = logToJson("TC1001", Run_TestCase_1001("/people/genesis 8 female/genesis 8.1 basic female.duf")); + i++; +/* + aTCResults[i] = logToJson("TC01", Run_TestCase_01("/people/genesis 8 female/genesis 8 basic female.duf")); + i++; + aTCResults[i] = logToJson("TC02", Run_TestCase_02("/people/genesis 8 female/genesis 8.1 basic female.duf")); + i++; + aTCResults[i] = logToJson("TC03", Run_TestCase_03("/people/genesis 8 female/genesis 8.1 basic female.duf")); + i++; + aTCResults[i] = logToJson("TC04", Run_TestCase_04("/people/genesis 8 female/genesis 8.1 basic female.duf")); + i++; + aTCResults[i] = logToJson("TC05", Run_TestCase_05("/people/genesis 8 female/genesis 8.1 basic female.duf")); + i++; + aTCResults[i] = logToJson("TC06", Run_TestCase_06("/people/genesis 8 female/genesis 8.1 basic female.duf")); + i++; + aTCResults[i] = logToJson("TC07", Run_TestCase_07("/people/genesis 8 female/genesis 8.1 basic female.duf")); + i++; + var sTestScene = DzFileInfo(DzFileInfo(sIncludePath).path()).path() + "/" + "QA-Test-Scene-01.duf" ; + aTCResults[i] = logToJson("TC08", Run_TestCase_08(sTestScene)); + i++; + aTCResults[i] = logToJson("TC09", Run_TestCase_09("/people/genesis 8 female/characters/victoria 8.1.duf")); + i++; + aTCResults[i] = logToJson("TC10", Run_TestCase_10("/people/genesis 8 female/characters/victoria 8.1.duf")); + i++; + aTCResults[i] = logToJson("TC11", Run_TestCase_11("/people/genesis 8 male/genesis 8 basic male.duf")); + i++; + aTCResults[i] = logToJson("TC12", Run_TestCase_12("people/genesis 8 male/genesis 8.1 basic male.duf")); + i++; + aTCResults[i] = logToJson("TC13", Run_TestCase_13("people/genesis 3 female/genesis 3 female.duf")); + i++; + aTCResults[i] = logToJson("TC14", Run_TestCase_14("people/genesis 3 male/genesis 3 male.duf")); + i++; + aTCResults[i] = logToJson("TC15", Run_TestCase_15("people/genesis 2 female/genesis 2 base female.duf")); + i++; + aTCResults[i] = logToJson("TC16", Run_TestCase_16("people/genesis 2 male/genesis 2 base male.duf")); + i++; + aTCResults[i] = logToJson("TC17", Run_TestCase_17("/Environments/Architecture/MS_DriveIn/Scenes/!Pre_DriveIn.duf")); + i++; + aTCResults[i] = logToJson("TC18", Run_TestCase_18()); + i++; + aTCResults[i] = logToJson("TC19", Run_TestCase_19()); + i++; + aTCResults[i] = logToJson("TC20", Run_TestCase_20()); +*/ + + clearLog(); + printToLog("\n"); + printToLog("======================="); + printToLog("Automated Test Results:"); + printToLog("======================="); + printToLog("\n"); + for (var i=0; i < aTCResults.length; i++) + { + if (aTCResults[i]) + { + printToLog("Test Case " + (i+1) + ": PASSED"); + } + else + { + printToLog("Test Case " + (i+1) + ": **FAILED**"); + } + } + writeLogToReport(); + clearLog(); + +} + +main(); diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt new file mode 100644 index 0000000..265b1f8 --- /dev/null +++ b/include/CMakeLists.txt @@ -0,0 +1,17 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(COMMON_LIB_INCLUDE_DIR ${COMMON_LIB_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) +set(COMMON_LIB_INCLUDE_DIR ${COMMON_LIB_INCLUDE_DIR} PARENT_SCOPE) + +include_directories(${COMMON_LIB_INCLUDE_DIR}) +set(LIB_HEADERS + ${CMAKE_CURRENT_SOURCE_DIR}/dzbridge.h + ${CMAKE_CURRENT_SOURCE_DIR}/DzBridgeAction.h + ${CMAKE_CURRENT_SOURCE_DIR}/DzBridgeMorphSelectionDialog.h + ${CMAKE_CURRENT_SOURCE_DIR}/DzBridgeSubdivisionDialog.h + ${CMAKE_CURRENT_SOURCE_DIR}/DzBridgeDialog.h + ${CMAKE_CURRENT_SOURCE_DIR}/common_version.h + ${CMAKE_CURRENT_SOURCE_DIR}/UnitTest.h +) +set(LIB_HEADERS ${LIB_HEADERS} PARENT_SCOPE) \ No newline at end of file diff --git a/include/DzBridgeAction.h b/include/DzBridgeAction.h new file mode 100644 index 0000000..82da1c9 --- /dev/null +++ b/include/DzBridgeAction.h @@ -0,0 +1,277 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include "QtCore/qfile.h" +#include "QtCore/qtextstream.h" + +#include "DzBridgeMorphSelectionDialog.h" + +class DzProgress; +class DzGeometry; + +class UnitTest_DzBridgeAction; + +#include "dzbridge.h" +namespace DzBridgeNameSpace +{ + class DzBridgeDialog; + class DzBridgeMorphSelectionDialog; + class DzBridgeSubdivisionDialog; + + /// + /// Abstract base class that manages exporting of assets to Target Software via FBX/DTU + /// intermediate files. Manages destination filepaths, morphs, subdivisions, animations, etc. + /// + /// Usage: + /// Subclass and implement executeAction() to open m_bridgeDialog. Implement readGuiRootFolder() + /// to read and return any custom UI widget containing destination root folder. Implement + /// writeConfiguration() to manage DTU file generation. Implement setExportOptions() to override + /// FBX generation options. + /// + /// See also: + /// DzBridgeScriptableAction.h for Daz Script usage. + /// + class CPP_Export DzBridgeAction : public DzAction { + Q_OBJECT + Q_PROPERTY(int nNonInteractiveMode READ getNonInteractiveMode WRITE setNonInteractiveMode) + Q_PROPERTY(QString sAssetType READ getAssetType WRITE setAssetType) + Q_PROPERTY(QString sExportFilename READ getExportFilename WRITE setExportFilename) + Q_PROPERTY(QString sExportFolder READ getExportFolder WRITE setExportFolder) + Q_PROPERTY(QString sRootFolder READ getRootFolder WRITE setRootFolder) + Q_PROPERTY(QString sProductName READ getProductName WRITE setProductName) + Q_PROPERTY(QString sProductComponentName READ getProductComponentName WRITE setProductComponentName) + Q_PROPERTY(QStringList aMorphList READ getMorphList WRITE setMorphList) + Q_PROPERTY(bool bUseRelativePaths READ getUseRelativePaths WRITE setUseRelativePaths) + Q_PROPERTY(bool bUndoNormalMaps READ getUndoNormalMaps WRITE setUndoNormalMaps) + Q_PROPERTY(QString sExportFbx READ getExportFbx WRITE setExportFbx) + Q_PROPERTY(DzBasicDialog* wBridgeDialog READ getBridgeDialog WRITE setBridgeDialog) + Q_PROPERTY(DzBasicDialog* wSubdivisionDialog READ getSubdivisionDialog WRITE setSubdivisionDialog) + Q_PROPERTY(DzBasicDialog* wMorphSelectionDialog READ getMorphSelectionDialog WRITE setMorphSelectionDialog) + + public: + + DzBridgeAction(const QString& text = QString::null, const QString& desc = QString::null); + virtual ~DzBridgeAction(); + + Q_INVOKABLE void resetToDefaults(); + Q_INVOKABLE QString cleanString(QString argString) { return argString.remove(QRegExp("[^A-Za-z0-9_]")); }; + Q_INVOKABLE QStringList getAvailableMorphs(DzNode* Node); + Q_INVOKABLE QStringList getActiveMorphs(DzNode* Node); + + // Normal Map Handling + Q_INVOKABLE QImage makeNormalMapFromHeightMap(QString heightMapFilename, double normalStrength); + // Pre-Process Scene data to workaround FbxExporter issues, called by Export() before FbxExport operation. + virtual bool preProcessScene(DzNode* parentNode = nullptr); + // Undo changes made by preProcessScene(), called by Export() after FbxExport operation. + virtual bool undoPreProcessScene(); + bool renameDuplicateMaterial(DzMaterial* material, QList* existingMaterialNameList); + bool undoRenameDuplicateMaterials(); + bool generateMissingNormalMap(DzMaterial* material); + bool undoGenerateMissingNormalMaps(); + + protected: + // Struct to remember attachment info + struct AttachmentInfo + { + DzNode* Parent; + DzNode* Child; + }; + + DzBridgeDialog* m_bridgeDialog; + DzBridgeSubdivisionDialog* m_subdivisionDialog; + DzBridgeMorphSelectionDialog* m_morphSelectionDialog; + + int m_nNonInteractiveMode; + QString m_sAssetName; // Exported filename without extension + QString m_sRootFolder; // The destination Root Folder + QString m_sDestinationPath; // Path to destination files: + "/" + + "/" + QString m_sDestinationFBX; // Path to destination fbx file: + + ".fbx"; + QString m_sAssetType; // Asset Types: "SkeletalMesh", "StaticMesh", "Animation", "Pose", "Environment" + QString m_sMorphSelectionRule; // Selection Rule used by FbxExporter to choose morphs to export + QString m_sFbxVersion; // FBX file format version to export + QMap m_mMorphNameToLabel; // Internal name to Friendly label + QList m_aPoseList; // Control Pose names + QMap m_imgPropertyTable_NormalMapStrength; // Image Property to Normal Map Strength + + // Used only by script system + QString m_sExportSubfolder; // Destination subfolder within Root Folder for exporting. [Default is ] + QString m_sProductName; // Daz Store Product Name, can contain spaces and special characters + QString m_sProductComponentName; // Friendly name of Component of Daz Store Product, can contain spaces and special characters + QStringList m_aMorphListOverride; // overrides Morph Selection Dialog + bool m_bUseRelativePaths; // use relative paths in DTU instead of absolute paths + bool m_bGenerateNormalMaps; // generate normal maps from height maps + bool m_bUndoNormalMaps; // remove generated normal maps after export + QString m_sExportFbx; // override filename of exported fbx + + bool m_bEnableMorphs; // enable morph export + bool m_EnableSubdivisions; // enable subdivision baking + bool m_bExportingBaseMesh; + bool m_bShowFbxOptions; + bool m_bExportMaterialPropertiesCSV; + DzNode* m_pSelectedNode; + + virtual QString getActionGroup() const { return tr("Bridges"); } + virtual QString getDefaultMenuPath() const { return tr("&File/Send To"); } + + virtual void exportAsset(); + virtual void exportNode(DzNode* Node); + + virtual void writeConfiguration() = 0; + virtual void setExportOptions(DzFileIOSettings& ExportOptions) = 0; + virtual QString readGuiRootFolder() = 0; + + Q_INVOKABLE virtual void writeDTUHeader(DzJsonWriter& writer); + + Q_INVOKABLE virtual void writeAllMaterials(DzNode* Node, DzJsonWriter& Writer, QTextStream* CVSStream = nullptr, bool bRecursive = false); + Q_INVOKABLE virtual void startMaterialBlock(DzNode* Node, DzJsonWriter& Writer, QTextStream* pCVSStream, DzMaterial* Material); + Q_INVOKABLE virtual void finishMaterialBlock(DzJsonWriter& Writer); + Q_INVOKABLE virtual void writeMaterialProperty(DzNode* Node, DzJsonWriter& Writer, QTextStream* pCVSStream, DzMaterial* Material, DzProperty* Property); + + Q_INVOKABLE virtual void writeAllMorphs(DzJsonWriter& Writer); + Q_INVOKABLE virtual void writeMorphProperties(DzJsonWriter& writer, const QString& key, const QString& value); + Q_INVOKABLE virtual void writeMorphJointLinkInfo(DzJsonWriter& writer, const JointLinkInfo& linkInfo); + + Q_INVOKABLE virtual void writeAllSubdivisions(DzJsonWriter& Writer); + Q_INVOKABLE virtual void writeSubdivisionProperties(DzJsonWriter& writer, const QString& Name, int targetValue); + + Q_INVOKABLE virtual void writeAllDforceInfo(DzNode* Node, DzJsonWriter& Writer, QTextStream* pCVSStream = nullptr, bool bRecursive = false); + Q_INVOKABLE virtual void writeDforceMaterialProperties(DzJsonWriter& Writer, DzMaterial* Material, DzShape* Shape); + Q_INVOKABLE virtual void writeDforceModifiers(const QList& dforceModifierList, DzJsonWriter& Writer, DzShape* Shape); + + Q_INVOKABLE virtual void writeEnvironment(DzJsonWriter& writer); + Q_INVOKABLE virtual void writeInstances(DzNode* Node, DzJsonWriter& Writer, QMap& WritenInstances, QList& ExportedGeometry, QUuid ParentID = QUuid()); + Q_INVOKABLE virtual QUuid writeInstance(DzNode* Node, DzJsonWriter& Writer, QUuid ParentID); + + Q_INVOKABLE virtual void writeAllPoses(DzJsonWriter& writer); + + // Used to find all the unique props in a scene for Environment export + void getScenePropList(DzNode* Node, QMap& Types); + + // During Environment export props need to get disconnected as they are exported. + void disconnectNode(DzNode* Node, QList& AttachmentList); + void reconnectNodes(QList& AttachmentList); + + // During Skeletal Mesh Export Disconnect Override Controllers + QList disconnectOverrideControllers(); + void reconnectOverrideControllers(QList& DisconnetedControllers); + QList m_ControllersToDisconnect; + + // For Pose exports check if writing to the timeline will alter existing keys + bool checkIfPoseExportIsDestructive(); + + // Need to be able to move asset instances to origin during environment export + void unlockTranform(DzNode* NodeToUnlock); + + // Getter/Setter methods + Q_INVOKABLE DzBridgeDialog* getBridgeDialog() { return m_bridgeDialog; } + Q_INVOKABLE bool setBridgeDialog(DzBasicDialog* arg_dlg); + Q_INVOKABLE DzBridgeSubdivisionDialog* getSubdivisionDialog() { return m_subdivisionDialog; } + Q_INVOKABLE bool setSubdivisionDialog(DzBasicDialog* arg_dlg); + Q_INVOKABLE DzBridgeMorphSelectionDialog* getMorphSelectionDialog() { return m_morphSelectionDialog; } + Q_INVOKABLE bool setMorphSelectionDialog(DzBasicDialog* arg_dlg); + + Q_INVOKABLE QString getAssetType() { return this->m_sAssetType; }; + Q_INVOKABLE void setAssetType(QString arg_AssetType) { this->m_sAssetType = arg_AssetType; }; + Q_INVOKABLE QString getExportFilename() { return this->m_sAssetName; }; + Q_INVOKABLE void setExportFilename(QString arg_Filename) { this->m_sAssetName = arg_Filename; }; + + Q_INVOKABLE QString getExportFolder() { return this->m_sExportSubfolder; }; + Q_INVOKABLE void setExportFolder(QString arg_Folder) { this->m_sExportSubfolder = arg_Folder; }; + Q_INVOKABLE QString getRootFolder() { return this->m_sRootFolder; }; + Q_INVOKABLE void setRootFolder(QString arg_Root) { this->m_sRootFolder = arg_Root; }; + + Q_INVOKABLE QString getProductName() { return this->m_sProductName; }; + Q_INVOKABLE void setProductName(QString arg_ProductName) { this->m_sProductName = arg_ProductName; }; + Q_INVOKABLE QString getProductComponentName() { return this->m_sProductComponentName; }; + Q_INVOKABLE void setProductComponentName(QString arg_ProductComponentName) { this->m_sProductComponentName = arg_ProductComponentName; }; + + Q_INVOKABLE QStringList getMorphList() { return m_aMorphListOverride; }; + Q_INVOKABLE void setMorphList(QStringList arg_MorphList) { this->m_aMorphListOverride = arg_MorphList; }; + + Q_INVOKABLE bool getUseRelativePaths() { return this->m_bUseRelativePaths; }; + Q_INVOKABLE void setUseRelativePaths(bool arg_UseRelativePaths) { this->m_bUseRelativePaths = arg_UseRelativePaths; }; + + bool isTemporaryFile(QString sFilename); + QString exportAssetWithDtu(QString sFilename, QString sAssetMaterialName = ""); + void writePropertyTexture(DzJsonWriter& Writer, QString sName, QString sValue, QString sType, QString sTexture); + void writePropertyTexture(DzJsonWriter& Writer, QString sName, double dValue, QString sType, QString sTexture); + QString makeUniqueFilename(QString sFilename); + + Q_INVOKABLE bool getUndoNormalMaps() { return this->m_bUndoNormalMaps; }; + Q_INVOKABLE void setUndoNormalMaps(bool arg_UndoNormalMaps) { this->m_bUndoNormalMaps = arg_UndoNormalMaps; }; + + Q_INVOKABLE int getNonInteractiveMode() { return this->m_nNonInteractiveMode; }; + Q_INVOKABLE void setNonInteractiveMode(int arg_Mode) { this->m_nNonInteractiveMode = arg_Mode; }; + + Q_INVOKABLE QString getExportFbx() { return this->m_sExportFbx; }; + Q_INVOKABLE void setExportFbx(QString arg_FbxName) { this->m_sExportFbx = arg_FbxName; }; + + Q_INVOKABLE void readGui(DzBridgeDialog*); + Q_INVOKABLE void exportHD(DzProgress* exportProgress = nullptr); + Q_INVOKABLE bool upgradeToHD(QString baseFilePath, QString hdFilePath, QString outFilePath, std::map* pLookupTable); + Q_INVOKABLE void writeWeightMaps(DzNode* Node, DzJsonWriter& Stream); + + Q_INVOKABLE bool metaInvokeMethod(QObject* object, const char* methodSig, void** returnPtr); + Q_INVOKABLE bool copyFile(QFile* file, QString* dst, bool replace = true, bool compareFiles = true); + Q_INVOKABLE QString getMD5(const QString& path); + + private: + class MaterialGroupExportOrderMetaData + { + public: + int materialIndex; + int vertex_offset; + int vertex_count; + + MaterialGroupExportOrderMetaData(int a_index, int a_offset) + { + materialIndex = a_index; + vertex_offset = a_offset; + vertex_count = -1; + } + + bool operator< (MaterialGroupExportOrderMetaData b) const + { + if (vertex_offset < b.vertex_offset) + { + return true; + } + else + { + return false; + } + } + + }; + + // Undo data structures + QMap m_undoTable_DuplicateMaterialRename; + QMap m_undoTable_GenerateMissingNormalMap; + + // NormalMap utility methods + double getPixelIntensity(const QRgb& pixel); + uint8_t getNormalMapComponent(double pX); + int getIntClamp(int x, int low, int high); + QRgb getSafePixel(const QImage& img, int x, int y); + bool isNormalMapMissing(DzMaterial* material); + bool isHeightMapPresent(DzMaterial* material); + QString getHeightMapFilename(DzMaterial* material); + double getHeightMapStrength(DzMaterial* material); + + DzWeightMapPtr getWeightMapPtr(DzNode* Node); + + // Need to temporarily rename surfaces if there is a name collision + void renameDuplicateMaterials(DzNode* Node, QList& MaterialNames, QMap& OriginalMaterialNames); + void undoRenameDuplicateMaterials(DzNode* Node, QList& MaterialNames, QMap& OriginalMaterialNames); + +#ifdef UNITTEST_DZBRIDGE + friend class ::UnitTest_DzBridgeAction; +#endif + + }; + +} \ No newline at end of file diff --git a/include/DzBridgeDialog.h b/include/DzBridgeDialog.h new file mode 100644 index 0000000..9d9d1b0 --- /dev/null +++ b/include/DzBridgeDialog.h @@ -0,0 +1,97 @@ +#pragma once +#include "dzbasicdialog.h" +#include +#include + +class QPushButton; +class QLineEdit; +class QCheckBox; +class QComboBox; +class QGroupBox; +class QFormLayout; + +class UnitTest_DzBridgeDialog; + +#include "dzbridge.h" + +namespace DzBridgeNameSpace +{ + class CPP_Export DzBridgeDialog : public DzBasicDialog { + Q_OBJECT + Q_PROPERTY(QWidget* wAssetNameEdit READ getAssetNameEdit) + Q_PROPERTY(QWidget* wAssetTypeCombo READ getAssetTypeCombo) + Q_PROPERTY(QWidget* wMorphsEnabledCheckBox READ getMorphsEnabledCheckBox) + Q_PROPERTY(QWidget* wSubdivisionEnabledCheckBox READ getSubdivisionEnabledCheckBox) + Q_PROPERTY(QWidget* wAdvancedSettingsGroupBox READ getAdvancedSettingsGroupBox) + Q_PROPERTY(QWidget* wFbxVersionCombo READ getFbxVersionCombo) + Q_PROPERTY(QWidget* wShowFbxDialogCheckBox READ getShowFbxDialogCheckBox) + Q_PROPERTY(QWidget* wEnableNormalMapGenerationCheckBox READ getEnableNormalMapGenerationCheckBox) + public: + Q_INVOKABLE QLineEdit* getAssetNameEdit() { return assetNameEdit; } + Q_INVOKABLE QComboBox* getAssetTypeCombo() { return assetTypeCombo; } + Q_INVOKABLE QCheckBox* getMorphsEnabledCheckBox() { return morphsEnabledCheckBox; } + Q_INVOKABLE QCheckBox* getSubdivisionEnabledCheckBox() { return subdivisionEnabledCheckBox; } + Q_INVOKABLE QGroupBox* getAdvancedSettingsGroupBox() { return advancedSettingsGroupBox; } + Q_INVOKABLE QComboBox* getFbxVersionCombo() { return fbxVersionCombo; } + Q_INVOKABLE QCheckBox* getShowFbxDialogCheckBox() { return showFbxDialogCheckBox; } + Q_INVOKABLE QCheckBox* getEnableNormalMapGenerationCheckBox() { return enableNormalMapGenerationCheckBox; } + + /** Constructor **/ + DzBridgeDialog(QWidget* parent = nullptr, const QString& windowTitle = ""); + + /** Destructor **/ + virtual ~DzBridgeDialog() {} + + // Pass so the DazTRoUnrealAction can access it from the morph dialog + Q_INVOKABLE QString GetMorphString(); + // Pass so the DazTRoUnrealAction can access it from the morph dialog + Q_INVOKABLE QMap GetMorphMapping() { return morphMapping; } + Q_INVOKABLE virtual void resetToDefaults(); + Q_INVOKABLE virtual bool loadSavedSettings(); + + void Accepted(); + + protected slots: + void handleSceneSelectionChanged(); + void HandleChooseMorphsButton(); + void HandleMorphsCheckBoxChange(int state); + void HandleChooseSubdivisionsButton(); + void HandleSubdivisionCheckBoxChange(int state); + void HandleFBXVersionChange(const QString& fbxVersion); + void HandleShowFbxDialogCheckBoxChange(int state); + void HandleExportMaterialPropertyCSVCheckBoxChange(int state); + void HandleShowAdvancedSettingsCheckBoxChange(bool checked); + void HandleEnableNormalMapGenerationCheckBoxChange(int state); + + protected: + QSettings* settings; + + void refreshAsset(); + + // These are clumsy leftovers from before the dialog were singletons + QString morphString; + QMap morphMapping; + + QFormLayout* mainLayout; + QFormLayout* advancedLayout; + QLineEdit* assetNameEdit; + // QLineEdit* projectEdit; + // QPushButton* projectButton; + QComboBox* assetTypeCombo; + QPushButton* morphsButton; + QCheckBox* morphsEnabledCheckBox; + QPushButton* subdivisionButton; + QCheckBox* subdivisionEnabledCheckBox; + QGroupBox* advancedSettingsGroupBox; + QWidget* advancedWidget; + QComboBox* fbxVersionCombo; + QCheckBox* showFbxDialogCheckBox; + QCheckBox* enableNormalMapGenerationCheckBox; + +#ifdef UNITTEST_DZBRIDGE + friend class ::UnitTest_DzBridgeDialog; +#endif + + }; + +} diff --git a/include/DzBridgeMorphSelectionDialog.h b/include/DzBridgeMorphSelectionDialog.h new file mode 100644 index 0000000..789d172 --- /dev/null +++ b/include/DzBridgeMorphSelectionDialog.h @@ -0,0 +1,185 @@ +#pragma once +#include "dzbasicdialog.h" +#include +#include +#include +#include +#include "dznode.h" + +class QListWidget; +class QListWidgetItem; +class QTreeWidget; +class QTreeWidgetItem; +class QLineEdit; +class QComboBox; +class QCheckBox; + +#include "dzbridge.h" +namespace DzBridgeNameSpace +{ + struct MorphInfo { + QString Name; + QString Label; + QString Type; + QString Path; + + inline bool operator==(MorphInfo other) + { + if (Name == other.Name) + { + return true; + } + return false; + } + + MorphInfo() + { + Name = QString(); + Label = QString(); + Type = QString(); + Path = QString(); + } + }; + + struct JointLinkKey + { + int Angle; + int Value; + }; + + struct JointLinkInfo + { + QString Bone; + QString Axis; + QString Morph; + double Scalar; + double Alpha; + QList Keys; + }; + + class CPP_Export DzBridgeMorphSelectionDialog : public DzBasicDialog { + Q_OBJECT + public: + + DzBridgeMorphSelectionDialog(QWidget* parent = nullptr); + virtual ~DzBridgeMorphSelectionDialog() {} + + // Setup the dialog + Q_INVOKABLE void PrepareDialog(); + + // Singleton access + Q_INVOKABLE static DzBridgeMorphSelectionDialog* Get(QWidget* Parent) + { + if (singleton == nullptr) + { + singleton = new DzBridgeMorphSelectionDialog(Parent); + } + else + { + singleton->PrepareDialog(); + } + return singleton; + } + + // Get the morph string in the format for the Daz FBX Export + Q_INVOKABLE QString GetMorphString(); + + // Get the morph string in the format used for presets + Q_INVOKABLE QString GetMorphCSVString(); + + // Get the morph string in an internal name = friendly name format + // Used to rename them to the friendly name in Unreal + Q_INVOKABLE QMap GetMorphRenaming(); + + Q_INVOKABLE bool IsAutoJCMEnabled() { return autoJCMCheckBox->isChecked(); } + + // Recursive function for finding all active JCM morphs for a node + Q_INVOKABLE QList GetActiveJointControlledMorphs(DzNode* Node = nullptr); + + // Retrieve label based on morph name + // DB Dec-21-2021, Created for scripting. + Q_INVOKABLE QString GetMorphLabelFromName(QString morphName); + + public slots: + void FilterChanged(const QString& filter); + void ItemSelectionChanged(); + void HandleAddMorphsButton(); + void HandleRemoveMorphsButton(); + void HandleSavePreset(); + void HandlePresetChanged(const QString& presetName); + void HandleArmJCMMorphsButton(); + void HandleLegJCMMorphsButton(); + void HandleTorsoJCMMorphsButton(); + void HandleARKitGenesis81MorphsButton(); + void HandleFaceFXGenesis8Button(); + void HandleAutoJCMCheckBoxChange(bool checked); + + private: + + // Refresh the list of possible presets from disk + void RefreshPresetsCombo(); + + // Recursive function for finding all morphs for a node + QStringList GetAvailableMorphs(DzNode* Node); + + // + QList GetJointControlledMorphInfo(DzProperty* property); + + void UpdateMorphsTree(); + + // Returns the tree node for the morph name (with path) + // Builds out the structure of the tree as needed. + QTreeWidgetItem* FindTreeItem(QTreeWidgetItem* parent, QString name); + + void SavePresetFile(QString filePath); + + // Updates selectedInTree to have all the morphs for the nodes + // selected in the left tree + void SelectMorphsInNode(QTreeWidgetItem* item); + + // Rebuild the right box that lists all the morphs that will export. + void RefreshExportMorphList(); + + // Morphs currently selected in the left tree box + QList selectedInTree; + + // List of morphs moved to the export box + QList morphsToExport; + + // Store off the presetsFolder path at dialog setup + QString presetsFolder; + + static DzBridgeMorphSelectionDialog* singleton; + + // A list of all found morphs. + QStringList morphList; + + // Mapping of morph name to label + QMap morphNameMapping; + + // Mapping of morph name to MorphInfo + //TODO: morphNameMapping (and others) should be replaced by this + QMap morphs; + + // List of morphs (recursive) under each tree node + // For convenience populating the middle box. + QMap> morphsForNode; + + // Force the size of the dialog + QSize minimumSizeHint() const override; + + // Widgets the dialog will access after construction + QListWidget* morphListWidget; + QListWidget* morphExportListWidget; + QTreeWidget* morphTreeWidget; + QLineEdit* filterEdit; + QComboBox* presetCombo; + + QTreeWidgetItem* fullBodyMorphTreeItem; + QTreeWidgetItem* charactersTreeItem; + QCheckBox* autoJCMCheckBox; + + QSettings* settings; + }; + +} \ No newline at end of file diff --git a/include/DzBridgeSubdivisionDialog.h b/include/DzBridgeSubdivisionDialog.h new file mode 100644 index 0000000..793b11e --- /dev/null +++ b/include/DzBridgeSubdivisionDialog.h @@ -0,0 +1,85 @@ +#pragma once +#include "dzbasicdialog.h" +#include +#include +#include +#include "dznode.h" +#include + +class QListWidget; +class QListWidgetItem; +class QTreeWidget; +class QTreeWidgetItem; +class QLineEdit; +class QComboBox; +class QGridLayout; + +#include "dzbridge.h" +namespace DzBridgeNameSpace +{ + class CPP_Export DzBridgeSubdivisionDialog : public DzBasicDialog { + Q_OBJECT + Q_PROPERTY(QObjectList aSubdivisionCombos READ getSubdivisionCombos) + public: + Q_INVOKABLE QObjectList getSubdivisionCombos(); + + /** Constructor **/ + DzBridgeSubdivisionDialog(QWidget* parent = nullptr); + + Q_INVOKABLE void PrepareDialog(); + + /** Destructor **/ + virtual ~DzBridgeSubdivisionDialog() {} + + Q_INVOKABLE static DzBridgeSubdivisionDialog* Get(QWidget* Parent) + { + if (singleton == nullptr) + { + singleton = new DzBridgeSubdivisionDialog(Parent); + } + else + { + singleton->PrepareDialog(); + } + return singleton; + } + + QGridLayout* subdivisionItemsGrid; + + Q_INVOKABLE void LockSubdivisionProperties(bool subdivisionEnabled); + Q_INVOKABLE void WriteSubdivisions(DzJsonWriter& Writer); + Q_INVOKABLE DzNode* FindObject(DzNode* Node, QString Name); + + Q_INVOKABLE bool setSubdivisionLevelByNode(DzNode* Node, int level); + + Q_INVOKABLE void UnlockSubdivisionProperties(); + std::map* GetLookupTable(); + + public slots: + void HandleSubdivisionLevelChanged(const QString& text); + + private: + void CreateList(DzNode* Node); + + void SavePresetFile(QString filePath); + + QSize minimumSizeHint() const override; + + QString presetsFolder; + + QList SubdivisionCombos; + + QMap SubdivisionLevels; + + static DzBridgeSubdivisionDialog* singleton; + + struct UndoData + { + bool originalLockState; + double originalValue; + }; + QMap UndoSubdivisionOverrides; + + }; + +} \ No newline at end of file diff --git a/include/OpenFBXInterface.h b/include/OpenFBXInterface.h new file mode 100644 index 0000000..a5d8476 --- /dev/null +++ b/include/OpenFBXInterface.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#ifdef __APPLE__ +#define USING_LIBSTDCPP 1 +#endif +#include + + +// FBX Interface class based upon AutoDesk FBX SDK +class OpenFBXInterface : public QObject +{ + Q_OBJECT + +public: + static OpenFBXInterface* GetInterface() + { + if (singleton == nullptr) + { + singleton = new OpenFBXInterface(); + } + return singleton; + } + + OpenFBXInterface(); + ~OpenFBXInterface(); + + Q_INVOKABLE bool LoadScene(FbxScene* pScene, QString sFilename); + Q_INVOKABLE bool SaveScene(FbxScene* pScene, QString sFilename, int nFileFormat = -1, bool bEmbedMedia = false); + Q_INVOKABLE FbxScene* CreateScene(QString sSceneName); + + Q_INVOKABLE bool LoadScene(QString sFilename) { return LoadScene(m_DefaultScene, sFilename); }; + Q_INVOKABLE bool SaveScene(QString sFilename, int nFileFormat = -1, bool bEmbedMedia = false) { return SaveScene(m_DefaultScene, sFilename, nFileFormat, bEmbedMedia); }; + + Q_INVOKABLE FbxManager* GetManager() { return m_fbxManager; } + Q_INVOKABLE FbxIOSettings* GetSettigns() { return m_fbxIOSettings; } + Q_INVOKABLE QString GetErrorString() { return m_ErrorString; } + Q_INVOKABLE int GetErrorCode() { return m_ErrorCode; } + +protected: + static OpenFBXInterface* singleton; + + FbxManager* m_fbxManager; + FbxIOSettings* m_fbxIOSettings; + FbxScene* m_DefaultScene; + + QString m_ErrorString; + int m_ErrorCode; + +}; \ No newline at end of file diff --git a/include/OpenSubdivInterface.h b/include/OpenSubdivInterface.h new file mode 100644 index 0000000..7b394b0 --- /dev/null +++ b/include/OpenSubdivInterface.h @@ -0,0 +1,121 @@ +#pragma once + +#include + +class OpenSubdivInterface +{ + +}; + +// utility class for subdividing FbxScene +class SubdivideFbxScene +{ +public: + SubdivideFbxScene(FbxScene* pScene, std::map* pLookupTable) + { + m_Scene = pScene; + m_SubDLevel_NameLookup = pLookupTable; + } + virtual ~SubdivideFbxScene() {}; + + bool ProcessScene(); + bool SaveClustersToScene(FbxScene* pDestScene); + +protected: + bool ProcessNode(FbxNode* pNode); + FbxMesh* SubdivideMesh(FbxNode* pNode, FbxMesh* pMesh, int subdLevel); + bool SaveClustersToNode(FbxScene* pDestScene, FbxNode* pNode); + FbxMesh* SaveClustersToMesh(FbxScene* pDestScene, FbxNode* pNode, FbxMesh* pMesh); + + FbxScene* m_Scene; + std::map* m_SubDLevel_NameLookup; + std::map m_FbxMesh_NameLookup; + + struct Vertex { + + // Minimal required interface ---------------------- + Vertex() { } + + Vertex(Vertex const& src) { + _position[0] = src._position[0]; + _position[1] = src._position[1]; + _position[2] = src._position[2]; + } + + void Clear(void* = 0) { + _position[0] = _position[1] = _position[2] = 0.0f; + } + + void AddWithWeight(Vertex const& src, float weight) { + _position[0] += weight * src._position[0]; + _position[1] += weight * src._position[1]; + _position[2] += weight * src._position[2]; + } + + // Public interface ------------------------------------ + void SetPosition(float x, float y, float z) { + _position[0] = x; + _position[1] = y; + _position[2] = z; + } + + void SetPosition(double x, double y, double z) { + _position[0] = (float)x; + _position[1] = (float)y; + _position[2] = (float)z; + } + + void SetVector(FbxVector4 vec) { + _position[0] = (float)vec[0]; + _position[1] = (float)vec[1]; + _position[2] = (float)vec[2]; + } + + const float* GetPosition() const { + return _position; + } + + FbxVector4 GetVector() { + return FbxVector4(_position[0], _position[1], _position[2], 1.0f); + } + + private: + float _position[3]; + }; + + typedef Vertex VertexPosition; + + struct SkinWeight { + + // Minimal required interface ---------------------- + SkinWeight() { + _weight = 0.0f; + } + + SkinWeight(SkinWeight const& src) { + _weight = src._weight; + } + + void Clear(void* = 0) { + _weight = 0.0f; + } + + void AddWithWeight(SkinWeight const& src, float weight) { + _weight += weight * src._weight; + } + + // Public interface ------------------------------------ + void SetWeight(float weight) { + _weight = weight; + } + + const float GetWeight() const { + return _weight; + } + + private: + float _weight; + }; + +}; + diff --git a/include/UnitTest.h b/include/UnitTest.h new file mode 100644 index 0000000..b19b88f --- /dev/null +++ b/include/UnitTest.h @@ -0,0 +1,93 @@ +/***************************************************************** +* +* For Visual Studio: In order to catch memory access violations, +* you must set following option in Properties -> C/C++ -> Code Generation: +* Enable C++ Exceptions: "Yes With SEH Exceptions" +* +******************************************************************/ + +#pragma once + +#ifdef UNITTEST_DZBRIDGE + +#include +#include + +#undef CPP_Export +#define CPP_Export Q_DECL_IMPORT +#ifdef DZ_BRIDGE_SHARED + #undef CPP_Export + #define CPP_Export Q_DECL_EXPORT +#elif DZ_BRIDGE_STATIC + #undef CPP_Export + #define CPP_Export +#endif + +#define DECLARE_TEST(method_name) \ +bool method_name(UnitTest::TestResult* testResult); + +#define RUNTEST RUNTEST_1ARG + +#define RUNTEST_0ARG(method_name) \ +UnitTest::TestResult *method_name ## _testResult = createTestResult(#method_name); \ +method_name ## _testResult->bResult = method_name(); + +#define RUNTEST_1ARG(method_name) \ +UnitTest::TestResult *method_name ## _testResult = createTestResult(#method_name); \ +method_name ## _testResult->bResult = method_name(method_name ## _testResult); + +#define LOGTEST_TEXT(text) \ +logToTestResult(testResult, QString(text)); + +#define LOGTEST_FAILED(text) \ +LOGTEST_TEXT(testResult->sName + ": failed. " + text); + +#define LOGTEST_PASSED(text) \ +LOGTEST_TEXT(testResult->sName + ": passed. " + text); + +#define TRY_METHODCALL(method_call) \ +try { method_call; } \ +catch (...) { LOGTEST_TEXT("C++ exception caught."); bResult = false; } + +#define TRY_METHODCALL_CUSTOM(method_call, error_string) \ +try { method_call; } \ +catch (...) { LOGTEST_TEXT(error_string); bResult = false; } + +#define TRY_METHODCALL_NULLPTR(method_call) \ +try { method_call; } \ +catch (...) { LOGTEST_TEXT("C++ exception caught. Failed nullptr C++ exception test."); bResult = false; } + +class QStringList; + +class CPP_Export UnitTest : public QObject { + Q_OBJECT +public: + Q_INVOKABLE virtual bool runUnitTests()=0; + + UnitTest(); + + Q_INVOKABLE bool writeAllTestResults(QString outputPath=""); + Q_INVOKABLE bool convertTestResutlsToXls(); + Q_INVOKABLE bool convertTestResultsToHtml(); + Q_INVOKABLE QObject* getTestObject(); + Q_INVOKABLE QString cleanString(QString argString) { return argString.remove(QRegExp("[^A-Za-z0-9_]")); }; + +protected: + struct TestResult { + int nId; + QString sName; + QStringList* aLog; + bool bResult; + }; + + UnitTest::TestResult* createTestResult(QString methodName); + bool logToTestResult(UnitTest::TestResult *testResult, QString text); + + QObject* m_testObject; + +private: + QList m_testResultList; + +}; + +#endif diff --git a/include/common_version.h b/include/common_version.h new file mode 100644 index 0000000..e21819a --- /dev/null +++ b/include/common_version.h @@ -0,0 +1,10 @@ +#pragma once +#include "dzversion.h" + +// Version number for Common +#define COMMON_MAJOR 2 +#define COMMON_MINOR 0 +#define COMMON_REV 15 +#define COMMON_BUILD 2 + +#define COMMON_VERSION DZ_MAKE_VERSION( COMMON_MAJOR, COMMON_MINOR, COMMON_REV, COMMON_BUILD ) diff --git a/include/dzbridge.h b/include/dzbridge.h new file mode 100644 index 0000000..0efddb0 --- /dev/null +++ b/include/dzbridge.h @@ -0,0 +1,103 @@ +#pragma once + +////////////////////////////////////////////// +// +// Define Daz Bridge Namespace +// +// NOTE: If you are want to static link the Daz Bridge Common library into +// a Daz Plugin, you **MUST** edit the DZ_BRIDGE_NAMESPACE macro so that it +// defines a unique namespace so that your version of the Common Library +// can co-exist with Common Library from other Daz Plugins. I recommend that +// you paste your Plugin's GUID onto the end of your namespace. See example +// below. +// +////////////////////////////////////////////// + +#define DZ_BRIDGE_NAMESPACE DzBridgeNameSpace +//#define DZ_BRIDGE_NAMESPACE DzBridgeStatic_71fb72024b4947baa82a4780e3819776 + + +///////////////////////////////////////////// +// +// Define C++ compatible DLL and Plugin Macros +// +// NOTE: The order to export C++ classes in a Windows DLL, you should use +// the CPP_Export macro below. Additionally, you must **NOT** use .DEF file +// method of exporting DLL functions. The current Microsoft does not allow +// exporting of C++ class data when using DEF files. Alternative plugin +// macros are supplied below that do not rely on DEF files for export. +///////////////////////////////////////////// + +#undef CPP_Export +#define CPP_Export Q_DECL_IMPORT +#ifdef DZ_BRIDGE_SHARED + #undef CPP_Export + #define CPP_Export Q_DECL_EXPORT +#elif DZ_BRIDGE_STATIC + #undef CPP_Export + #define CPP_Export +#endif + +#ifdef __APPLE__ +#define CPP_PLUGIN_DEFINITION DZ_PLUGIN_DEFINITION +#else +#define CPP_PLUGIN_DEFINITION( pluginName ) \ +BOOL WINAPI DllMain( HINSTANCE hinstDLL, ULONG fdwReason, LPVOID lpvReserved ) \ +{ \ + switch( fdwReason ) { \ + case DLL_PROCESS_ATTACH: \ + break; \ + case DLL_THREAD_ATTACH: \ + break; \ + case DLL_THREAD_DETACH: \ + break; \ + case DLL_PROCESS_DETACH: \ + break; \ + } \ + return TRUE; \ +} \ + \ +static DzPlugin s_pluginDef( pluginName ); \ +extern "C" __declspec(dllexport) DzVersion getSDKVersion() { return DZ_SDK_VERSION; } \ +extern "C" __declspec(dllexport) DzPlugin *getPluginDefinition() { return &s_pluginDef; } +#endif + +////////////////////////////////////////////// +// +// Fixed DZ_PLUGIN_CUSTOM_CLASS macro +// +// The original DazSDK macros are missing the proper factory functions to handle arguments +// for custom classes. The following alternative macros fix this. +// +////////////////////////////////////////////// + +#include +#include + +static QWidget* getParentFromArgs(const QVariantList& args) +{ + if (args.count() < 1) + return nullptr; + + QWidget* parent = nullptr; + QVariant qvar = args[0]; + QObject* obj = qvar.value(); + if (obj) + parent = qobject_cast(obj); + + return parent; +} + +#define NEW_PLUGIN_CUSTOM_CLASS_GUID( typeName, guid ) \ +DZ_PLUGIN_CUSTOM_CLASS_GUID( typeName, guid ) \ + \ +QObject* typeName ## Factory::createInstance(const QVariantList& args) const \ +{ \ + QWidget* parent = getParentFromArgs(args); \ + return qobject_cast(new typeName(parent)); \ +} \ +QObject* typeName ## Factory::createInstance() const \ +{ \ + return qobject_cast(new typeName(nullptr)); \ +} + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..31dfff3 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,117 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(COMMON_LIB_INCLUDE_DIR ${COMMON_LIB_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) +set(COMMON_LIB_INCLUDE_DIR ${COMMON_LIB_INCLUDE_DIR} PARENT_SCOPE) + +include_directories(${COMMON_LIB_INCLUDE_DIR}) + +if(WIN32) + set(OS_SOURCES "") +elseif(APPLE) + set(OS_SOURCES "") +endif() + +set(OFBXI_SOURCES + OpenFBXInterface.cpp + ../include/OpenFBXInterface.h +) + +set(OSDI_SOURCES + OpenSubdivInterface.cpp + ../include/OpenSubdivInterface.h +) + +set(LIB_SRCS + ${LIB_HEADERS} + DzBridgeAction.cpp + DzBridgeMorphSelectionDialog.cpp + DzBridgeSubdivisionDialog.cpp + DzBridgeDialog.cpp + ${QA_SRCS} +) + +add_library(dzbridge-static + STATIC + ${LIB_SRCS} + ${OS_SOURCES} + ${OFBXI_SOURCES} + ${OSDI_SOURCES} +) + +target_include_directories(dzbridge-static + PUBLIC + ${FBX_SDK_INCLUDE} + ${OPENSUBDIV_INCLUDE} +) + +target_link_libraries(dzbridge-static + PRIVATE + dzcore + ${DZSDK_QT_CORE_TARGET} + ${DZSDK_QT_GUI_TARGET} + ${DZSDK_QT_SCRIPT_TARGET} + ${DZSDK_QT_NETWORK_TARGET} +) + +set_target_properties (dzbridge-static + PROPERTIES + FOLDER "" + PROJECT_LABEL "DzBridge Static" +) + +if (NOT USE_DZBRIDGE_SUBMODULE) + +add_library(dzbridge-shared + SHARED + ${LIB_SRCS} + ${OS_SOURCES} + ${OFBXI_SOURCES} + ${OSDI_SOURCES} + pluginmain.cpp + DzBridgeAction_Scriptable.h + DzBridgeAction_Scriptable.cpp + DzBridgeMorphSelectionDialog_Scriptable.h + DzBridgeMorphSelectionDialog_Scriptable.cpp + DzBridgeSubdivisionDialog_Scriptable.h + DzBridgeSubdivisionDialog_Scriptable.cpp + DzBridgeDialog_Scriptable.h + DzBridgeDialog_Scriptable.cpp +) + +target_include_directories(dzbridge-shared + PUBLIC + ${FBX_SDK_INCLUDE} + ${OPENSUBDIV_INCLUDE} +) + +target_link_libraries(dzbridge-shared + PRIVATE + dzcore + ${DZSDK_QT_CORE_TARGET} + ${DZSDK_QT_GUI_TARGET} + ${DZSDK_QT_SCRIPT_TARGET} + ${DZSDK_QT_NETWORK_TARGET} + ${FBX_IMPORT_LIBS} + ${OPENSUBDIV_LIB} +) + +set_target_properties (dzbridge-shared + PROPERTIES + FOLDER "" + PROJECT_LABEL "DzBridge Shared" +) + +target_compile_definitions(dzbridge-shared + PUBLIC + DZ_BRIDGE_SHARED + $<$:UNITTEST_DZBRIDGE> +) + +endif(NOT USE_DZBRIDGE_SUBMODULE) + +target_compile_definitions(dzbridge-static + PUBLIC + DZ_BRIDGE_STATIC + $<$:UNITTEST_DZBRIDGE> +) diff --git a/src/DzBridgeAction.cpp b/src/DzBridgeAction.cpp new file mode 100644 index 0000000..f666bd8 --- /dev/null +++ b/src/DzBridgeAction.cpp @@ -0,0 +1,2771 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dzgeometry.h" +#include "dzweightmap.h" +#include "dzfacetshape.h" +#include "dzfacetmesh.h" +#include "dzfacegroup.h" +#include "dzmaterial.h" + +#include "dzgroupnode.h" +#include "dzinstancenode.h" + +#include +#include +#include +#include +#include +#include +#include "QtCore/qmetaobject.h" + +#include "DzBridgeAction.h" +#include "DzBridgeDialog.h" +#include "DzBridgeSubdivisionDialog.h" +#include "DzBridgeMorphSelectionDialog.h" + +using namespace DzBridgeNameSpace; + +/// +/// Initializes general export data and settings. +/// +DzBridgeAction::DzBridgeAction(const QString& text, const QString& desc) : + DzAction(text, desc) +{ + resetToDefaults(); + m_bridgeDialog = nullptr; + m_subdivisionDialog = nullptr; + m_morphSelectionDialog = nullptr; + m_bGenerateNormalMaps = false; + m_pSelectedNode = nullptr; + +#ifdef _DEBUG + m_bUndoNormalMaps = false; +#else + m_bUndoNormalMaps = true; +#endif + +} + +DzBridgeAction::~DzBridgeAction() +{ +} + +/// +/// Resets export settings to default values. +/// +void DzBridgeAction::resetToDefaults() +{ + m_bEnableMorphs = false; + m_EnableSubdivisions = false; + m_bShowFbxOptions = false; + m_bExportMaterialPropertiesCSV = false; + m_ControllersToDisconnect.clear(); + m_ControllersToDisconnect.append("facs_bs_MouthClose_div2"); + + // Reset all dialog settings and script-exposed properties to Hardcoded Defaults + // Ignore saved settings, QSettings, etc. + DzNode* selection = dzScene->getPrimarySelection(); + DzFigure* figure = qobject_cast(selection); + if (selection) + { + if (dzScene->getFilename().length() > 0) + { + QFileInfo fileInfo = QFileInfo(dzScene->getFilename()); + m_sAssetName = fileInfo.baseName().remove(QRegExp("[^A-Za-z0-9_]")); + } + else + { + m_sAssetName = this->cleanString(selection->getLabel()); + } + } + else + { + m_sAssetName = ""; + } + if (figure) + { + m_sAssetType = "SkeletalMesh"; + } + else + { + m_sAssetType = "StaticMesh"; + } + + m_sProductName = ""; + m_sProductComponentName = ""; + m_aMorphListOverride.clear(); + m_bUseRelativePaths = false; + m_bUndoNormalMaps = true; + m_nNonInteractiveMode = 0; + m_undoTable_DuplicateMaterialRename.clear(); + m_undoTable_GenerateMissingNormalMap.clear(); + m_sExportFbx = ""; + +} + +/// +/// Performs multiple pre-processing procedures prior to exporting FBX and generating DTU. +/// +/// Usage: Usually called from executeAction() prior to calling Export() or ExportHD(). +/// See Also: undoPreProcessScene() +/// +/// The "root" node from which to start processing of all +/// children. If null, then scene primary selection is used. +/// true if procedure was successful +bool DzBridgeAction::preProcessScene(DzNode* parentNode) +{ + DzProgress preProcessProgress = DzProgress("Daz Bridge Pre-Processing...", 0, false, true); + + /////////////////////// + // Create JobPool + // Iterate through all children of "parentNode", create jobpool of nodes to process later + // Nodes are added to nodeJobList in breadth-first order (parent,children,grandchildren) + /////////////////////// + DzNodeList nodeJobList; + DzNodeList tempQueue; + DzNode *node_ptr = parentNode; + if (node_ptr == nullptr) + node_ptr = dzScene->getPrimarySelection(); + if (node_ptr == nullptr) + return false; + + tempQueue.append(node_ptr); + while (!tempQueue.isEmpty()) + { + node_ptr = tempQueue.first(); + tempQueue.removeFirst(); + nodeJobList.append(node_ptr); + DzNodeListIterator Iterator = node_ptr->nodeChildrenIterator(); + while (Iterator.hasNext()) + { + DzNode* tempChild = Iterator.next(); + tempQueue.append(tempChild); + } + } + + /////////////////////// + // Process JobPool (DzNodeList nodeJobList) + /////////////////////// + QList existingMaterialNameList; + for (int i = 0; i < nodeJobList.length(); i++) + { + DzNode *node = nodeJobList[i]; + DzObject* object = node->getObject(); + DzShape* shape = object ? object->getCurrentShape() : NULL; + if (shape) + { + for (int i = 0; i < shape->getNumMaterials(); i++) + { + DzMaterial* material = shape->getMaterial(i); + if (material) + { + ////////////////// + // Rename Duplicate Material + ///////////////// + renameDuplicateMaterial(material, &existingMaterialNameList); + + ///////////////// + // Generate Missing Normal Maps + ///////////////// + if (m_bGenerateNormalMaps) + generateMissingNormalMap(material); + } + } + } + } + + preProcessProgress.finish(); + + return true; +} + +/// +/// Generate Normal Map texture for for use in Target Software that doesn't support HeightMap. +/// Called by preProcessScene() for each exported material. Checks material for existing +/// HeightMap texture but missing NormalMap texture before generating NormalMap. Exports HeightMap +/// strength to NormalMap strength in DTU file. +/// +/// Note: Must call undoGenerateMissingNormalMaps() to undo insertion of NormalMaps into materials. +/// +/// See Also: makeNormalMapFromHeightMap(), m_undoTable_GenerateMissingNormalMap, +/// preProcessScene(), undoPreProcessScene(). +/// +/// true if normalmap was generated +bool DzBridgeAction::generateMissingNormalMap(DzMaterial* material) +{ + if (material == nullptr) + return false; + + bool bNormalMapWasGenerated = false; + + // Check if normal map missing + if (isNormalMapMissing(material)) + { + // Check if height map present + if (isHeightMapPresent(material)) + { + // Generate normal map from height map + QString heightMapFilename = getHeightMapFilename(material); + if (heightMapFilename != "") + { + // Retrieve Normap Map property + QString propertyName = "normal map"; + DzProperty* normalMapProp = material->findProperty(propertyName, false); + if (normalMapProp) + { + DzImageProperty* imageProp = qobject_cast(normalMapProp); + DzNumericProperty* numericProp = qobject_cast(normalMapProp); + + // calculate normal map strength based on height map strength + double conversionFactor = 0.5; + QString shaderName = material->getMaterialName().toLower(); + if (shaderName.contains("aoa_subsurface")) + { + conversionFactor = 3.0; + } + else if (shaderName.contains("omubersurface")) + { + double bumpMin = -0.1; + double bumpMax = 0.1; + DzNumericProperty *bumpMinProp = qobject_cast(material->findProperty("bump minimum", false)); + DzNumericProperty *bumpMaxProp = qobject_cast(material->findProperty("bump maximum", false)); + if (bumpMinProp) + { + bumpMin = bumpMinProp->getDoubleValue(); + } + if (bumpMaxProp) + { + bumpMax = bumpMaxProp->getDoubleValue(); + } + double range = bumpMax - bumpMin; + conversionFactor = range * 25; + } + double heightStrength = getHeightMapStrength(material); + double normalStrength = heightStrength * conversionFactor; + double bakeStrength = 1.0; + // If not numeric property, then save normal map strength to external + // value so it can be added into the DTU file on export. + if (!numericProp && imageProp) + { + // normalStrengthTable + m_imgPropertyTable_NormalMapStrength.insert(imageProp, normalStrength); + } + + // create normalMap filename + QString tempPath; + QFileInfo fileInfo = QFileInfo(heightMapFilename); + //QString normalMapFilename = fileInfo.completeBaseName() + "_nm." + fileInfo.suffix(); + QString normalMapFilename = fileInfo.completeBaseName() + "_nm." + "png"; + QString normalMapSavePath = dzApp->getTempPath() + "/" + normalMapFilename; + QFileInfo normalMapInfo = QFileInfo(normalMapSavePath); + + // Generate Temp Normal Map if does not already exist. + // If it does exist then assume it was generated for previous material + // and re-use it for this material. + if (!normalMapInfo.exists()) + { + QImage normalMap = makeNormalMapFromHeightMap(heightMapFilename, bakeStrength); + QString progressString = "Saving Normal Map: " + normalMapSavePath; + DzProgress saveProgress = DzProgress(progressString, 2, false, true); + saveProgress.step(); + normalMap.save(normalMapSavePath, 0, 75); + saveProgress.step(); + saveProgress.finish(); + } + + // Insert generated NormalMap into Daz material + if (numericProp) + { + numericProp->setMap(normalMapSavePath); + numericProp->setDoubleValue(normalStrength); + } + else if (imageProp) + { + imageProp->setValue(normalMapSavePath); + } + + if (m_bUndoNormalMaps) + { + // Add to Undo Table + m_undoTable_GenerateMissingNormalMap.insert(material, normalMapProp); + } + + bNormalMapWasGenerated = true; + } + } + } + } + + return bNormalMapWasGenerated; +} + +/// +/// Revert changes to materials made by GenerateMissingNormalMaps(). +/// Called by undoPreProcessScene() after Export() or exportHD(). +/// +/// true if the undo process completed successfully. +bool DzBridgeAction::undoGenerateMissingNormalMaps() +{ + QMap::iterator iter; + for (iter = m_undoTable_GenerateMissingNormalMap.begin(); iter != m_undoTable_GenerateMissingNormalMap.end(); ++iter) + { + DzProperty* normalMapProp = iter.value(); + DzNumericProperty* numericProp = qobject_cast(normalMapProp); + DzImageProperty* imageProp = qobject_cast(normalMapProp); + if (numericProp) + { + numericProp->setDoubleValue(numericProp->getDoubleDefaultValue()); + numericProp->setMap(nullptr); + } + else if (imageProp) + { + imageProp->setValue(nullptr); + } + } + m_undoTable_GenerateMissingNormalMap.clear(); + + return true; +} + +/// +/// Retrieve HeightMap Strength from material's "bump strength" property. +/// Called by generateMissingNormalMap(). +/// +/// value of heightmap strength if it exists, +/// or 0.0 if it does not exist +double DzBridgeAction::getHeightMapStrength(DzMaterial* material) +{ + if (material == nullptr) + return false; + + QString propertyName = "bump strength"; + DzProperty* heightMapProp = material->findProperty(propertyName, false); + + if (heightMapProp) + { + // DEBUG + QString propertyName = heightMapProp->getName(); + QString propertyLabel = heightMapProp->getLabel(); + + // normal map property preseet + DzNumericProperty* numericProp = qobject_cast(heightMapProp); + + if (numericProp) + { + double heightStrength = numericProp->getDoubleValue(); + return heightStrength; + } + + } + + return 0.0; + +} + + +/// filename stored in material's "bump strength" property if it exists, +/// QString("") if it does not. +QString DzBridgeAction::getHeightMapFilename(DzMaterial* material) +{ + if (material == nullptr) + return QString(""); + + QString propertyName = "bump strength"; + DzProperty* heightMapProp = material->findProperty(propertyName, false); + + if (heightMapProp) + { + // DEBUG + QString propertyName = heightMapProp->getName(); + QString propertyLabel = heightMapProp->getLabel(); + + // normal map property preseet + DzImageProperty* imageProp = qobject_cast(heightMapProp); + DzNumericProperty* numericProp = qobject_cast(heightMapProp); + + if (imageProp) + { + if (imageProp->getValue()) + { + QString heightMapFilename = imageProp->getValue()->getFilename(); + return heightMapFilename; + } + } + else if (numericProp) + { + if (numericProp->getMapValue()) + { + QString heightMapFilename = numericProp->getMapValue()->getFilename(); + return heightMapFilename; + } + } + + // Bump Map property present, missing mapvalue + return ""; + } + + // DEBUG + QString materialName = material->getName(); + QString materialLabel = material->getLabel(); + + return ""; +} + +/// true if "normal map" property exists AND property does not have filename set +bool DzBridgeAction::isNormalMapMissing(DzMaterial* material) +{ + if (material == nullptr) + return false; + + QString propertyName = "normal map"; + DzProperty* normalMapProp = material->findProperty(propertyName, false); + + if (normalMapProp) + { + // DEBUG + QString propertyName = normalMapProp->getName(); + QString propertyLabel = normalMapProp->getLabel(); + + // normal map property preseet + DzImageProperty* imageProp = qobject_cast(normalMapProp); + DzNumericProperty* numericProp = qobject_cast(normalMapProp); + + if (imageProp) + { + if (imageProp->getValue()) + { + QString normalMapFilename = imageProp->getValue()->getFilename(); + if (normalMapFilename == "") + { + // image-type normal map property present, value is empty string + return true; + } + else + { + return false; + } + } + else + { + // image property is missing texture + return true; + } + } + else if (numericProp) + { + if (numericProp->getMapValue()) + { + QString normalMapFilename = numericProp->getMapValue()->getFilename(); + if (normalMapFilename == "") + { + // numeric-type normal map property present, map value is empty string + return true; + } + else + { + return false; + } + } + else + { + // numeric-type normal map property present, no map value + return true; + } + } + + // normal map property exists... + return false; + } + +#ifdef _DEBUG + QString materialName = material->getName(); + QString materialLabel = material->getLabel(); + QString shaderName = material->getMaterialName(); +#endif + + // normal map property does not exist + return false; +} + +/// true if material has a "bump strength" property +bool DzBridgeAction::isHeightMapPresent(DzMaterial* material) +{ + QString propertyName = "bump strength"; + DzProperty* heightMapProp = material->findProperty(propertyName, false); + + if (heightMapProp) + { + return true; + } + + return false; +} + +/// +/// Undo changes performed by preProcessScene(). +/// +/// true if all undo procedures are successful +bool DzBridgeAction::undoPreProcessScene() +{ + bool bResult = true; + + if (undoRenameDuplicateMaterials() == false) + { + bResult = false; + } + + // Undo Inject Normal Maps + if (undoGenerateMissingNormalMaps() == false) + { + bResult = false; + } + + return bResult; +} + +/// +/// Rename material if its name is already used by existing material(s) to +/// prevent collisions in Target Software. Called by preProcessScene(). +/// See also: undoRenameMaterialDuplicateMaterials(). +/// +/// true if successful +bool DzBridgeAction::renameDuplicateMaterial(DzMaterial *material, QList* existingMaterialNameList) +{ + if (material == nullptr) + return false; + + int nameIndex = 0; + QString newMaterialName = material->getName(); + while (existingMaterialNameList->contains(newMaterialName)) + { + newMaterialName = material->getName() + QString("_%1").arg(++nameIndex); + } + if (newMaterialName != material->getName()) + { + // Add to Undo Table + m_undoTable_DuplicateMaterialRename.insert(material, material->getName()); + material->setName(newMaterialName); + } + existingMaterialNameList->append(newMaterialName); + + return true; +} + +/// +/// Undo any materials modified by renameDuplicaetMaterials(). +/// +/// true if successful +bool DzBridgeAction::undoRenameDuplicateMaterials() +{ + QMap::iterator iter; + for (iter = m_undoTable_DuplicateMaterialRename.begin(); iter != m_undoTable_DuplicateMaterialRename.end(); ++iter) + { + iter.key()->setName(iter.value()); + } + m_undoTable_DuplicateMaterialRename.clear(); + + return true; + +} + +/// +/// Convenience method to export base mesh and HD mesh as needed. +/// Performs weightmap fix on subdivided mesh. +/// See also: upgradeToHD(), Export(). +/// +/// if null, exportHD will handle UI progress updates +void DzBridgeAction::exportHD(DzProgress* exportProgress) +{ + if (m_subdivisionDialog == nullptr) + return; + + bool bLocalDzProgress = false; + if (!exportProgress) + { + bLocalDzProgress = true; + exportProgress = new DzProgress(tr("DazBridge: Exporting FBX/DTU"), 8, false, true); + exportProgress->setCloseOnFinish(true); + exportProgress->enable(true); + exportProgress->step(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + } + + if (m_EnableSubdivisions) + { + if (exportProgress) + { + exportProgress->setInfo(tr("Exporting Base Mesh...")); + exportProgress->step(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + } + m_subdivisionDialog->LockSubdivisionProperties(false); + m_bExportingBaseMesh = true; + exportAsset(); + m_subdivisionDialog->UnlockSubdivisionProperties(); + if (exportProgress) + { + exportProgress->setInfo(tr("Base mesh exported.")); + exportProgress->step(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + } + + } + + if (exportProgress) + { + exportProgress->step(); + if (m_EnableSubdivisions) + exportProgress->setInfo(tr("Exporting HD Mesh...")); + else + exportProgress->setInfo(tr("Exporting Mesh...")); + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + } + m_subdivisionDialog->LockSubdivisionProperties(m_EnableSubdivisions); + m_bExportingBaseMesh = false; + exportAsset(); + if (exportProgress) + { + exportProgress->step(); + exportProgress->setInfo(tr("Mesh exported.")); + } + + if (m_EnableSubdivisions) + { + if (exportProgress) + { + exportProgress->step(); + exportProgress->setInfo(tr("Fixing weightmaps on HD Mesh...")); + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + } + + std::map* pLookupTable = m_subdivisionDialog->GetLookupTable(); + QString BaseCharacterFBX = m_sDestinationPath + m_sAssetName + "_base.fbx"; + // DB 2021-10-02: Upgrade HD + if (upgradeToHD(BaseCharacterFBX, m_sDestinationFBX, m_sDestinationFBX, pLookupTable) == false) + { + if (m_nNonInteractiveMode == 0) QMessageBox::warning(0, tr("Error"), + tr("There was an error during the Subdivision Surface refinement operation, the exported Daz model may not work correctly."), QMessageBox::Ok); + } + else + { + // remove intermediate base character fbx + // Sanity Check + if (QFile::exists(BaseCharacterFBX)) + { +// QFile::remove(BaseCharacterFBX); + } + } + delete(pLookupTable); + if (exportProgress) + { + exportProgress->step(); + exportProgress->setInfo(tr("HD weightmaps fixed.")); + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + } + + } + + // DB 2021-09-02: Unlock and Undo subdivision changes + m_subdivisionDialog->UnlockSubdivisionProperties(); + if (exportProgress) + { + exportProgress->step(); + exportProgress->setInfo(tr("Mesh export complete.")); + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + } + + if (bLocalDzProgress) + { + exportProgress->finish(); + // 2022-02-13 (DB): Generic messagebox "Export Complete" + if (m_nNonInteractiveMode == 0) + { + QMessageBox::information(0, "DazBridge", + tr("Export phase from Daz Studio complete. Please switch to Unity to begin Import phase."), QMessageBox::Ok); + } + + } + +} + +void DzBridgeAction::exportAsset() +{ + // FBX Export + m_pSelectedNode = dzScene->getPrimarySelection(); + if (m_pSelectedNode == nullptr) + return; + + QMap PropToInstance; + if (m_sAssetType == "Environment") + { + // Store off the original export information + QString OriginalCharacterName = m_sAssetName; + DzNode* OriginalSelection = m_pSelectedNode; + + // Find all the different types of props in the scene + getScenePropList(m_pSelectedNode, PropToInstance); + QMap::iterator iter; + for (iter = PropToInstance.begin(); iter != PropToInstance.end(); ++iter) + { + // Override the export info for exporting this prop + m_sAssetType = "StaticMesh"; + m_sAssetName = iter.key(); + m_sAssetName = m_sAssetName.remove(QRegExp("[^A-Za-z0-9_]")); + m_sDestinationPath = m_sRootFolder + "/" + m_sAssetName + "/"; + m_sDestinationFBX = m_sDestinationPath + m_sAssetName + ".fbx"; + DzNode* Node = iter.value(); + + // If this is a figure, send it as a skeletal mesh + if (DzSkeleton* Skeleton = Node->getSkeleton()) + { + if (DzFigure* Figure = qobject_cast(Skeleton)) + { + m_sAssetType = "SkeletalMesh"; + } + } + + //Unlock the transform controls so the node can be moved to root + unlockTranform(Node); + + // Disconnect the asset being sent from everything else + QList AttachmentList; + disconnectNode(Node, AttachmentList); + + // Set the selection so this will be the exported asset + m_pSelectedNode = Node; + + // Store the current transform and zero it out. + DzVec3 Location; + DzQuat Rotation; + DzMatrix3 Scale; + + Node->getWSTransform(Location, Rotation, Scale); + Node->setWSTransform(DzVec3(0.0f, 0.0f, 0.0f), DzQuat(), DzMatrix3(true)); + + // Export + exportNode(Node); + + // Put the item back where it was + Node->setWSTransform(Location, Rotation, Scale); + + // Reconnect all the nodes + reconnectNodes(AttachmentList); + } + + // After the props have been exported, export the environment + m_sAssetName = OriginalCharacterName; + m_sDestinationPath = m_sRootFolder + "/" + m_sExportSubfolder + "/"; + // use original export fbx filestem, if exists + if (m_sExportFbx == "") m_sExportFbx = m_sAssetName; + m_sDestinationFBX = m_sDestinationPath + m_sExportFbx + ".fbx"; + m_pSelectedNode = OriginalSelection; + m_sAssetType = "Environment"; + exportNode(m_pSelectedNode); + } + else if (m_sAssetType == "Pose") + { + if (checkIfPoseExportIsDestructive()) + { + if (QMessageBox::question(0, tr("Continue"), + tr("Proceeding will delete keyed values on some properties. Continue?"), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) + { + return; + } + } + + m_aPoseList.clear(); + DzNode* Selection = dzScene->getPrimarySelection(); + int poseIndex = 0; + DzNumericProperty* previousProperty = nullptr; + for (int index = 0; index < Selection->getNumProperties(); index++) + { + DzProperty* property = Selection->getProperty(index); + DzNumericProperty* numericProperty = qobject_cast(property); + QString propName = property->getName(); + if (numericProperty) + { + QString propName = property->getName(); + if (m_mMorphNameToLabel.contains(propName)) + { + poseIndex++; + numericProperty->setDoubleValue(0.0f, 0.0f); + for (int frame = 0; frame < m_mMorphNameToLabel.count() + 1; frame++) + { + numericProperty->setDoubleValue(dzScene->getTimeStep() * double(frame), 0.0f); + } + numericProperty->setDoubleValue(dzScene->getTimeStep() * double(poseIndex),1.0f); + m_aPoseList.append(propName); + } + } + } + + DzObject* Object = Selection->getObject(); + if (Object) + { + for (int index = 0; index < Object->getNumModifiers(); index++) + { + DzModifier* modifier = Object->getModifier(index); + DzMorph* mod = qobject_cast(modifier); + if (mod) + { + for (int propindex = 0; propindex < modifier->getNumProperties(); propindex++) + { + DzProperty* property = modifier->getProperty(propindex); + QString propName = property->getName(); + QString propLabel = property->getLabel(); + DzNumericProperty* numericProperty = qobject_cast(property); + if (numericProperty) + { + QString propName = property->getName(); + //qDebug() << propName; + if (m_mMorphNameToLabel.contains(modifier->getName())) + { + poseIndex++; + numericProperty->setDoubleValue(0.0f, 0.0f); + for (int frame = 0; frame < m_mMorphNameToLabel.count() + 1; frame++) + { + numericProperty->setDoubleValue(dzScene->getTimeStep() * double(frame), 0.0f); + } + numericProperty->setDoubleValue(dzScene->getTimeStep() * double(poseIndex), 1.0f); + m_aPoseList.append(modifier->getName()); + } + } + } + + } + + } + } + + dzScene->setAnimRange(DzTimeRange(0, poseIndex * dzScene->getTimeStep())); + dzScene->setPlayRange(DzTimeRange(0, poseIndex * dzScene->getTimeStep())); + + exportNode(Selection); + } + else if (m_sAssetType == "SkeletalMesh") + { + QList DisconnectedModifiers = disconnectOverrideControllers(); + DzNode* Selection = dzScene->getPrimarySelection(); + exportNode(Selection); + reconnectOverrideControllers(DisconnectedModifiers); + } + else + { + DzNode* Selection = dzScene->getPrimarySelection(); + exportNode(Selection); + } +} + +void DzBridgeAction::disconnectNode(DzNode* Node, QList& AttachmentList) +{ + if (Node == nullptr) + return; + + AttachmentInfo ParentAttachment; + if (Node->getNodeParent()) + { + // Don't disconnect a figures bones + if (DzBone* Bone = qobject_cast(Node)) + { + + } + else + { + ParentAttachment.Parent = Node->getNodeParent(); + ParentAttachment.Child = Node; + AttachmentList.append(ParentAttachment); + Node->getNodeParent()->removeNodeChild(Node); + } + } + + QList ChildNodes; + for (int ChildIndex = Node->getNumNodeChildren() - 1; ChildIndex >= 0; ChildIndex--) + { + DzNode* ChildNode = Node->getNodeChild(ChildIndex); + if (DzBone* Bone = qobject_cast(ChildNode)) + { + + } + else + { + DzNode* ChildNode = Node->getNodeChild(ChildIndex); + AttachmentInfo ChildAttachment; + ChildAttachment.Parent = Node; + ChildAttachment.Child = ChildNode; + AttachmentList.append(ChildAttachment); + Node->removeNodeChild(ChildNode); + } + disconnectNode(ChildNode, AttachmentList); + } +} + +void DzBridgeAction::reconnectNodes(QList& AttachmentList) +{ + foreach(AttachmentInfo Attachment, AttachmentList) + { + Attachment.Parent->addNodeChild(Attachment.Child); + } +} + + +void DzBridgeAction::exportNode(DzNode* Node) +{ + if (Node == nullptr) + return; + + dzScene->selectAllNodes(false); + dzScene->setPrimarySelection(Node); + + if (m_sAssetType == "Environment") + { + QDir dir; + dir.mkpath(m_sDestinationPath); + writeConfiguration(); + return; + } + + DzExportMgr* ExportManager = dzApp->getExportMgr(); + DzExporter* Exporter = ExportManager->findExporterByClassName("DzFbxExporter"); + + if (Exporter) + { + DzFileIOSettings ExportOptions; + ExportOptions.setBoolValue("doSelected", true); + ExportOptions.setBoolValue("doVisible", false); + if (m_sAssetType == "SkeletalMesh" || m_sAssetType == "StaticMesh" || m_sAssetType == "Environment") + { + ExportOptions.setBoolValue("doFigures", true); + ExportOptions.setBoolValue("doProps", true); + } + else + { + ExportOptions.setBoolValue("doFigures", true); + ExportOptions.setBoolValue("doProps", false); + } + ExportOptions.setBoolValue("doLights", false); + ExportOptions.setBoolValue("doCameras", false); + if (m_sAssetType == "Animation") + { + ExportOptions.setBoolValue("doAnims", true); + } + else + { + ExportOptions.setBoolValue("doAnims", false); + } + if ((m_sAssetType == "Animation" || m_sAssetType == "SkeletalMesh") && m_bEnableMorphs && m_sMorphSelectionRule != "") + { + ExportOptions.setBoolValue("doMorphs", true); + ExportOptions.setStringValue("rules", m_sMorphSelectionRule); + } + else + { + ExportOptions.setBoolValue("doMorphs", false); + ExportOptions.setStringValue("rules", ""); + } + + ExportOptions.setStringValue("format", m_sFbxVersion); + ExportOptions.setIntValue("RunSilent", !m_bShowFbxOptions); + + ExportOptions.setBoolValue("doEmbed", false); + ExportOptions.setBoolValue("doCopyTextures", false); + ExportOptions.setBoolValue("doDiffuseOpacity", false); + ExportOptions.setBoolValue("doMergeClothing", true); + ExportOptions.setBoolValue("doStaticClothing", false); + ExportOptions.setBoolValue("degradedSkinning", true); + ExportOptions.setBoolValue("degradedScaling", true); + ExportOptions.setBoolValue("doSubD", false); + ExportOptions.setBoolValue("doCollapseUVTiles", false); + + // get the top level node for things like clothing so we don't get dupe material names + DzNode* Parent = Node; + if (m_sAssetType != "Environment") + { + while (Parent->getNodeParent() != NULL) + { + Parent = Parent->getNodeParent(); + } + } + + preProcessScene(Parent); + + QDir dir; + dir.mkpath(m_sDestinationPath); + + setExportOptions(ExportOptions); + + if (m_EnableSubdivisions && m_bExportingBaseMesh) + { + QString CharacterBaseFBX = this->m_sDestinationFBX; + CharacterBaseFBX.replace(".fbx", "_base.fbx"); + Exporter->writeFile(CharacterBaseFBX, &ExportOptions); + } + else + { + Exporter->writeFile(m_sDestinationFBX, &ExportOptions); + writeConfiguration(); + } + + undoPreProcessScene(); + } +} + +// If there are duplicate material names, save off the original and rename one +void DzBridgeAction::renameDuplicateMaterials(DzNode* Node, QList& MaterialNames, QMap& OriginalMaterialNames) +{ + if (Node == nullptr) + return; + + DzObject* Object = Node->getObject(); + DzShape* Shape = Object ? Object->getCurrentShape() : NULL; + + if (Shape) + { + for (int i = 0; i < Shape->getNumMaterials(); i++) + { + DzMaterial* Material = Shape->getMaterial(i); + if (Material) + { + OriginalMaterialNames.insert(Material, Material->getName()); + while (MaterialNames.contains(Material->getName())) + { + Material->setName(Material->getName() + "_1"); + } + MaterialNames.append(Material->getName()); + } + } + } + DzNodeListIterator Iterator = Node->nodeChildrenIterator(); + while (Iterator.hasNext()) + { + DzNode* Child = Iterator.next(); + renameDuplicateMaterials(Child, MaterialNames, OriginalMaterialNames); + } +} + +// Restore the original material names +void DzBridgeAction::undoRenameDuplicateMaterials(DzNode* Node, QList& MaterialNames, QMap& OriginalMaterialNames) +{ + QMap::iterator iter; + for (iter = OriginalMaterialNames.begin(); iter != OriginalMaterialNames.end(); ++iter) + { + iter.key()->setName(iter.value()); + } +} + +void DzBridgeAction::getScenePropList(DzNode* Node, QMap& Types) +{ + if (Node == nullptr) + return; + + DzObject* Object = Node->getObject(); + DzShape* Shape = Object ? Object->getCurrentShape() : NULL; + DzGeometry* Geometry = Shape ? Shape->getGeometry() : NULL; + DzSkeleton* Skeleton = Node->getSkeleton(); + DzFigure* Figure = Skeleton ? qobject_cast(Skeleton) : NULL; + //QString AssetId = Node->getAssetId(); + //IDzSceneAsset::m_sAssetType Type = Node->getAssetType(); + + // Use the FileName to generate a name for the prop to be exported + QString Path = Node->getAssetFileInfo().getUri().getFilePath(); + QFile File(Path); + QString FileName = File.fileName(); + QStringList Items = FileName.split("/"); + QStringList Parts = Items[Items.count() - 1].split("."); + QString AssetID = Node->getAssetUri().getId(); + QString Name = AssetID.remove(QRegExp("[^A-Za-z0-9_]")); + + if (Figure) + { + QString FigureAssetId = Figure->getAssetId(); + if (!Types.contains(Name)) + { + Types.insert(Name, Node); + } + } + else if (Geometry) + { + if (!Types.contains(Name)) + { + Types.insert(Name, Node); + } + } + + // Looks through the child nodes for more props + for (int ChildIndex = 0; ChildIndex < Node->getNumNodeChildren(); ChildIndex++) + { + DzNode* ChildNode = Node->getNodeChild(ChildIndex); + getScenePropList(ChildNode, Types); + } +} + +QList DzBridgeAction::disconnectOverrideControllers() +{ + QList ModifiedList; + ModifiedList.clear(); + + DzNode* Selection = dzScene->getPrimarySelection(); + if (Selection == nullptr) + return ModifiedList; + + int poseIndex = 0; + DzNumericProperty* previousProperty = nullptr; + for (int index = 0; index < Selection->getNumProperties(); index++) + { + DzProperty* property = Selection->getProperty(index); + DzNumericProperty* numericProperty = qobject_cast(property); + QString propName = property->getName(); + if (numericProperty && !numericProperty->isOverridingControllers()) + { + QString propName = property->getName(); + if (m_mMorphNameToLabel.contains(propName) && m_ControllersToDisconnect.contains(propName)) + { + numericProperty->setOverrideControllers(true); + ModifiedList.append(propName); + } + } + } + + DzObject* Object = Selection->getObject(); + if (Object) + { + for (int index = 0; index < Object->getNumModifiers(); index++) + { + DzModifier* modifier = Object->getModifier(index); + DzMorph* mod = qobject_cast(modifier); + if (mod) + { + for (int propindex = 0; propindex < modifier->getNumProperties(); propindex++) + { + DzProperty* property = modifier->getProperty(propindex); + QString propName = property->getName(); + QString propLabel = property->getLabel(); + DzNumericProperty* numericProperty = qobject_cast(property); + if (numericProperty && !numericProperty->isOverridingControllers()) + { + QString propName = property->getName(); + if (m_mMorphNameToLabel.contains(modifier->getName()) && m_ControllersToDisconnect.contains(modifier->getName())) + { + numericProperty->setOverrideControllers(true); + ModifiedList.append(modifier->getName()); + } + } + } + + } + + } + } + + return ModifiedList; +} + +void DzBridgeAction::reconnectOverrideControllers(QList& DisconnetedControllers) +{ + DzNode* Selection = dzScene->getPrimarySelection(); + if (Selection == nullptr) + return; + + int poseIndex = 0; + DzNumericProperty* previousProperty = nullptr; + for (int index = 0; index < Selection->getNumProperties(); index++) + { + DzProperty* property = Selection->getProperty(index); + DzNumericProperty* numericProperty = qobject_cast(property); + QString propName = property->getName(); + if (numericProperty && numericProperty->isOverridingControllers()) + { + QString propName = property->getName(); + if (DisconnetedControllers.contains(propName)) + { + numericProperty->setOverrideControllers(false); + } + } + } + + DzObject* Object = Selection->getObject(); + if (Object) + { + for (int index = 0; index < Object->getNumModifiers(); index++) + { + DzModifier* modifier = Object->getModifier(index); + DzMorph* mod = qobject_cast(modifier); + if (mod) + { + for (int propindex = 0; propindex < modifier->getNumProperties(); propindex++) + { + DzProperty* property = modifier->getProperty(propindex); + QString propName = property->getName(); + QString propLabel = property->getLabel(); + DzNumericProperty* numericProperty = qobject_cast(property); + if (numericProperty && numericProperty->isOverridingControllers()) + { + QString propName = property->getName(); + if (DisconnetedControllers.contains(modifier->getName())) + { + numericProperty->setOverrideControllers(false); + } + } + } + + } + + } + } +} + +bool DzBridgeAction::checkIfPoseExportIsDestructive() +{ + DzNode* Selection = dzScene->getPrimarySelection(); + if (Selection == nullptr) + return false; + + int poseIndex = 0; + DzNumericProperty* previousProperty = nullptr; + for (int index = 0; index < Selection->getNumProperties(); index++) + { + DzProperty* property = Selection->getProperty(index); + DzNumericProperty* numericProperty = qobject_cast(property); + QString propName = property->getName(); + if (numericProperty) + { + QString propName = property->getName(); + if (m_mMorphNameToLabel.contains(propName)) + { + if (!(numericProperty->getKeyRange().getEnd() == 0.0f && numericProperty->getDoubleValue(0.0f) == 0.0f)) return true; + } + } + } + + DzObject* Object = Selection->getObject(); + if (Object) + { + for (int index = 0; index < Object->getNumModifiers(); index++) + { + DzModifier* modifier = Object->getModifier(index); + DzMorph* mod = qobject_cast(modifier); + if (mod) + { + for (int propindex = 0; propindex < modifier->getNumProperties(); propindex++) + { + DzProperty* property = modifier->getProperty(propindex); + QString propName = property->getName(); + QString propLabel = property->getLabel(); + DzNumericProperty* numericProperty = qobject_cast(property); + if (numericProperty) + { + QString propName = property->getName(); + if (m_mMorphNameToLabel.contains(modifier->getName())) + { + if (!(numericProperty->getKeyRange().getEnd() == 0.0f && numericProperty->getDoubleValue(0.0f) == 0.0f)) return true; + } + } + } + + } + + } + } + + return false; +} + +void DzBridgeAction::unlockTranform(DzNode* NodeToUnlock) +{ + if (NodeToUnlock == nullptr) + return; + + DzFloatProperty* Property = nullptr; + Property = NodeToUnlock->getXPosControl(); + Property->lock(false); + Property = NodeToUnlock->getYPosControl(); + Property->lock(false); + Property = NodeToUnlock->getZPosControl(); + Property->lock(false); + + Property = NodeToUnlock->getXRotControl(); + Property->lock(false); + Property = NodeToUnlock->getYRotControl(); + Property->lock(false); + Property = NodeToUnlock->getZRotControl(); + Property->lock(false); + + Property = NodeToUnlock->getXScaleControl(); + Property->lock(false); + Property = NodeToUnlock->getYScaleControl(); + Property->lock(false); + Property = NodeToUnlock->getZScaleControl(); + Property->lock(false); +} + +bool DzBridgeAction::isTemporaryFile(QString sFilename) +{ + QString cleanedFilename = sFilename.toLower().replace("\\", "/"); + QString cleanedTempPath = dzApp->getTempPath().toLower().replace("\\", "/"); + + if (cleanedFilename.contains(cleanedTempPath)) + { + return true; + } + + return false; +} + +QString DzBridgeAction::exportAssetWithDtu(QString sFilename, QString sAssetMaterialName) +{ + if (sFilename.isEmpty()) + return sFilename; + + QString cleanedFilename = sFilename.toLower().replace("\\", "/"); + QString cleanedTempPath = dzApp->getTempPath().toLower().replace("\\", "/"); + QString cleanedAssetMaterialName = sAssetMaterialName; + cleanedAssetMaterialName.remove(QRegExp("[^A-Za-z0-9_]")); + + QString exportPath = this->m_sRootFolder.replace("\\","/") + "/" + this->m_sExportSubfolder.replace("\\", "/"); + QString fileStem = QFileInfo(sFilename).fileName(); + + exportPath += "/ExportTextures/"; + QDir().mkpath(exportPath); +// QString exportFilename = exportPath + cleanedAssetMaterialName + "_" + fileStem; + QString exportFilename = exportPath + fileStem; + + exportFilename = makeUniqueFilename(exportFilename); + + if (QFile(sFilename).copy(exportFilename) == true) + { + return exportFilename; + } + + // copy method may fail if file already exists, + // if exists and same file size, then proceed as if successful + if ( QFileInfo(exportFilename).exists() && + QFileInfo(sFilename).size() == QFileInfo(exportFilename).size()) + { + return exportFilename; + } + + // return original source string if failed + return sFilename; + +} + +QString DzBridgeAction::makeUniqueFilename(QString sFilename) +{ + if (QFileInfo(sFilename).exists() != true) + return sFilename; + + QString newFilename = sFilename; + int duplicate_count = 0; + + while ( + QFileInfo(newFilename).exists() && + QFileInfo(sFilename).size() != QFileInfo(newFilename).size() + ) + { + newFilename = sFilename + QString("_%i").arg(duplicate_count++); + } + + return newFilename; + +} + +void DzBridgeAction::writePropertyTexture(DzJsonWriter& Writer, QString sName, QString sValue, QString sType, QString sTexture) +{ + Writer.startObject(true); + Writer.addMember("Name", sName); + Writer.addMember("Value", sValue); + Writer.addMember("Data Type", sType); + Writer.addMember("Texture", sTexture); + Writer.finishObject(); + +} + +void DzBridgeAction::writePropertyTexture(DzJsonWriter& Writer, QString sName, double dValue, QString sType, QString sTexture) +{ + Writer.startObject(true); + Writer.addMember("Name", sName); + Writer.addMember("Value", dValue); + Writer.addMember("Data Type", sType); + Writer.addMember("Texture", sTexture); + Writer.finishObject(); + +} + +void DzBridgeAction::writeDTUHeader(DzJsonWriter& writer) +{ + QString sAssetId = ""; + + if (m_pSelectedNode) + { + sAssetId = m_pSelectedNode->getAssetId(); + } + + writer.addMember("DTU Version", 3); + writer.addMember("Asset Name", m_sAssetName); + writer.addMember("Asset Type", m_sAssetType); + writer.addMember("Asset Id", sAssetId); + writer.addMember("FBX File", m_sDestinationFBX); + QString CharacterBaseFBX = m_sDestinationFBX; + CharacterBaseFBX.replace(".fbx", "_base.fbx"); + writer.addMember("Base FBX File", CharacterBaseFBX); + QString CharacterHDFBX = m_sDestinationFBX; + CharacterHDFBX.replace(".fbx", "_HD.fbx"); + writer.addMember("HD FBX File", CharacterHDFBX); + writer.addMember("Import Folder", m_sDestinationPath); + // DB Dec-21-2021: additional metadata + writer.addMember("Product Name", m_sProductName); + writer.addMember("Product Component Name", m_sProductComponentName); + +} + +// Write out all the surface properties +void DzBridgeAction::writeAllMaterials(DzNode* Node, DzJsonWriter& Writer, QTextStream* pCVSStream, bool bRecursive) +{ + if (Node == nullptr) + return; + + if (!bRecursive) + Writer.startMemberArray("Materials", true); + + DzObject* Object = Node->getObject(); + DzShape* Shape = Object ? Object->getCurrentShape() : nullptr; + + if (Shape) + { + for (int i = 0; i < Shape->getNumMaterials(); i++) + { + DzMaterial* Material = Shape->getMaterial(i); + if (Material) + { + auto propertyList = Material->propertyListIterator(); + startMaterialBlock(Node, Writer, pCVSStream, Material); + while (propertyList.hasNext()) + { + writeMaterialProperty(Node, Writer, pCVSStream, Material, propertyList.next()); + } + finishMaterialBlock(Writer); + } + } + } + + DzNodeListIterator Iterator = Node->nodeChildrenIterator(); + while (Iterator.hasNext()) + { + DzNode* Child = Iterator.next(); + writeAllMaterials(Child, Writer, pCVSStream, true); + } + + if (!bRecursive) + Writer.finishArray(); +} + +void DzBridgeAction::startMaterialBlock(DzNode* Node, DzJsonWriter& Writer, QTextStream* pCVSStream, DzMaterial* Material) +{ + if (Node == nullptr || Material == nullptr) + return; + + Writer.startObject(true); + Writer.addMember("Version", 3); + Writer.addMember("Asset Name", Node->getLabel()); + Writer.addMember("Material Name", Material->getName()); + Writer.addMember("Material Type", Material->getMaterialName()); + + DzPresentation* presentation = Node->getPresentation(); + if (presentation) + { + const QString presentationType = presentation->getType(); + Writer.addMember("Value", presentationType); + } + else + { + Writer.addMember("Value", QString("Unknown")); + } + + Writer.startMemberArray("Properties", true); + // Presentation node is stored as first element in Property array for compatibility with UE plugin's basematerial search algorithm + if (presentation) + { + const QString presentationType = presentation->getType(); + Writer.startObject(true); + Writer.addMember("Name", QString("Asset Type")); + Writer.addMember("Value", presentationType); + Writer.addMember("Data Type", QString("String")); + Writer.addMember("Texture", QString("")); + Writer.finishObject(); + + if (m_bExportMaterialPropertiesCSV && pCVSStream) + { + *pCVSStream << "2, " << Node->getLabel() << ", " << Material->getName() << ", " << Material->getMaterialName() << ", " << "Asset Type" << ", " << presentationType << ", " << "String" << ", " << "" << endl; + } + } +} + +void DzBridgeAction::finishMaterialBlock(DzJsonWriter& Writer) +{ + // replace with Section Stack + Writer.finishArray(); + Writer.finishObject(); + +} + +void DzBridgeAction::writeMaterialProperty(DzNode* Node, DzJsonWriter& Writer, QTextStream* pCVSStream, DzMaterial* Material, DzProperty* Property) +{ + if (Node == nullptr || Material == nullptr || Property == nullptr) + return; + + QString Name = Property->getName(); + QString TextureName = ""; + QString dtuPropType = ""; + QString dtuPropValue = ""; + double dtuPropNumericValue = 0.0; + bool bUseNumeric = false; + + DzImageProperty* ImageProperty = qobject_cast(Property); + DzNumericProperty* NumericProperty = qobject_cast(Property); + DzColorProperty* ColorProperty = qobject_cast(Property); + if (ImageProperty) + { + if (ImageProperty->getValue()) + { + TextureName = ImageProperty->getValue()->getFilename(); + } + dtuPropValue = Material->getDiffuseColor().name(); + dtuPropType = QString("Texture"); + + // Check if this is a Normal Map with Strength stored in lookup table + if (m_imgPropertyTable_NormalMapStrength.contains(ImageProperty)) + { + dtuPropType = QString("Double"); + dtuPropNumericValue = m_imgPropertyTable_NormalMapStrength[ImageProperty]; + bUseNumeric = true; + } + } + // DzColorProperty is subclass of DzNumericProperty + else if (ColorProperty) + { + if (ColorProperty->getMapValue()) + { + TextureName = ColorProperty->getMapValue()->getFilename(); + } + dtuPropValue = ColorProperty->getColorValue().name(); + dtuPropType = QString("Color"); + } + else if (NumericProperty) + { + if (NumericProperty->getMapValue()) + { + TextureName = NumericProperty->getMapValue()->getFilename(); + } + dtuPropType = QString("Double"); + dtuPropNumericValue = NumericProperty->getDoubleValue(); + bUseNumeric = true; + } + else + { + // unsupported property type + return; + } + + QString dtuTextureName = TextureName; + if (TextureName != "") + { + if (this->m_bUseRelativePaths) + { + dtuTextureName = dzApp->getContentMgr()->getRelativePath(TextureName, true); + } + if (isTemporaryFile(TextureName)) + { + dtuTextureName = exportAssetWithDtu(TextureName, Node->getLabel() + "_" + Material->getName()); + } + } + if (bUseNumeric) + writePropertyTexture(Writer, Name, dtuPropNumericValue, dtuPropType, dtuTextureName); + else + writePropertyTexture(Writer, Name, dtuPropValue, dtuPropType, dtuTextureName); + + if (m_bExportMaterialPropertiesCSV && pCVSStream) + { + if (bUseNumeric) + *pCVSStream << "2, " << Node->getLabel() << ", " << Material->getName() << ", " << Material->getMaterialName() << ", " << Name << ", " << dtuPropNumericValue << ", " << dtuPropType << ", " << TextureName << endl; + else + *pCVSStream << "2, " << Node->getLabel() << ", " << Material->getName() << ", " << Material->getMaterialName() << ", " << Name << ", " << dtuPropValue << ", " << dtuPropType << ", " << TextureName << endl; + } + return; + +} + +void DzBridgeAction::writeAllMorphs(DzJsonWriter& writer) +{ + writer.startMemberArray("Morphs", true); + if (m_bEnableMorphs) + { + for (QMap::iterator i = m_mMorphNameToLabel.begin(); i != m_mMorphNameToLabel.end(); ++i) + { + writeMorphProperties(writer, i.key(), i.value()); + } + } + writer.finishArray(); + + if (m_bEnableMorphs) + { + if (m_morphSelectionDialog->IsAutoJCMEnabled()) + { + writer.startMemberArray("JointLinks", true); + QList JointLinks = m_morphSelectionDialog->GetActiveJointControlledMorphs(m_pSelectedNode); + foreach(JointLinkInfo linkInfo, JointLinks) + { + writeMorphJointLinkInfo(writer, linkInfo); + } + writer.finishArray(); + } + } + +} + +void DzBridgeAction::writeMorphProperties(DzJsonWriter& writer, const QString& key, const QString& value) +{ + writer.startObject(true); + writer.addMember("Name", key); + writer.addMember("Label", value); + writer.finishObject(); +} + +void DzBridgeAction::writeMorphJointLinkInfo(DzJsonWriter& writer, const JointLinkInfo& linkInfo) +{ + writer.startObject(true); + writer.addMember("Bone", linkInfo.Bone); + writer.addMember("Axis", linkInfo.Axis); + writer.addMember("Morph", linkInfo.Morph); + writer.addMember("Scalar", linkInfo.Scalar); + writer.addMember("Alpha", linkInfo.Alpha); + if (linkInfo.Keys.count() > 0) + { + writer.startMemberArray("Keys", true); + foreach(JointLinkKey key, linkInfo.Keys) + { + writer.startObject(true); + writer.addMember("Angle", key.Angle); + writer.addMember("Value", key.Value); + writer.finishObject(); + } + writer.finishArray(); + } + writer.finishObject(); +} + +void DzBridgeAction::writeAllSubdivisions(DzJsonWriter& writer) +{ + writer.startMemberArray("Subdivisions", true); + if (m_EnableSubdivisions) + { + //stream << "Version, Object, Subdivision" << endl; + QObjectList objList = m_subdivisionDialog->getSubdivisionCombos(); + foreach(QObject* obj, objList) + { + QComboBox* combo = (QComboBox*) obj; + QString Name = combo->property("Object").toString() + ".Shape"; + int targetValue = combo->currentText().toInt(); + + writeSubdivisionProperties(writer, Name, targetValue); + //stream << "1, " << Name << ", " << targetValue << endl; + } + + } + + writer.finishArray(); + +} + +void DzBridgeAction::writeSubdivisionProperties(DzJsonWriter& writer, const QString& Name, int targetValue) +{ + writer.startObject(true); + writer.addMember("Version", 1); + writer.addMember("Asset Name", Name); + writer.addMember("Value", targetValue); + writer.finishObject(); +} + +void DzBridgeAction::writeAllDforceInfo(DzNode* Node, DzJsonWriter& Writer, QTextStream* pCVSStream, bool bRecursive) +{ + if (Node == nullptr) + return; + + if (!bRecursive) + Writer.startMemberArray("dForce", true); + + DzObject* Object = Node->getObject(); + DzShape* Shape = Object ? Object->getCurrentShape() : nullptr; + + bool bDForceSettingsAvailable = false; + if (Shape) + { + QList dforceModifierList; + DzModifierIterator modIter = Object->modifierIterator(); + int modifierCount = 0; + while (modIter.hasNext()) + { + DzModifier* modifier = modIter.next(); + QString mod_Class = modifier->className(); + if (mod_Class.toLower().contains("dforce")) + { + bDForceSettingsAvailable = true; + dforceModifierList.append(modifier); + modifierCount++; + } + } + + if (bDForceSettingsAvailable) + { + Writer.startObject(true); + Writer.addMember("Version", 4); + Writer.addMember("Asset Name", Node->getLabel()); + Writer.addMember("Modifier Count", modifierCount); + Writer.addMember("Material Count", Shape->getNumMaterials()); + writeDforceModifiers(dforceModifierList, Writer, Shape); + + Writer.startMemberArray("dForce-Materials", true); + for (int i = 0; i < Shape->getNumMaterials(); i++) + { + DzMaterial* Material = Shape->getMaterial(i); + if (Material) + { + Writer.startObject(true); + Writer.addMember("Version", 3); + Writer.addMember("Asset Name", Node->getLabel()); + Writer.addMember("Material Name", Material->getName()); + Writer.addMember("Material Type", Material->getMaterialName()); + DzPresentation* presentation = Node->getPresentation(); + if (presentation != nullptr) + { + const QString presentationType = presentation->getType(); + Writer.addMember("Value", presentationType); + } + else + { + Writer.addMember("Value", QString("Unknown")); + } + writeDforceMaterialProperties(Writer, Material, Shape); + Writer.finishObject(); + } + } + Writer.finishArray(); + Writer.finishObject(); + } + } + + DzNodeListIterator Iterator = Node->nodeChildrenIterator(); + while (Iterator.hasNext()) + { + DzNode* Child = Iterator.next(); + writeAllDforceInfo(Child, Writer, pCVSStream, true); + } + + if (!bRecursive) + { + if (m_sAssetType == "SkeletalMesh") + { + bool ExportDForce = true; + Writer.startMemberArray("dForce-WeightMaps", true); + if (ExportDForce) + { + writeWeightMaps(m_pSelectedNode, Writer); + } + Writer.finishArray(); + } + Writer.finishArray(); + } +} + +void DzBridgeAction::writeDforceModifiers(const QList& dforceModifierList, DzJsonWriter& Writer, DzShape* Shape) +{ + Writer.startMemberArray("DForce-Modifiers", true); + + foreach(auto modifier, dforceModifierList) + { + Writer.startObject(true); + Writer.addMember("Modifier Name", modifier->getName()); + Writer.addMember("Modifier Class", modifier->className()); + ///////////////////////////////////// + // TODO: DUMP MODIFIER PROPERTIES + ///////////////////////////////////// + Writer.finishObject(); + } + + Writer.finishArray(); +} + +void DzBridgeAction::writeDforceMaterialProperties(DzJsonWriter& Writer, DzMaterial* Material, DzShape* Shape) +{ + if (Material == nullptr || Shape == nullptr) + return; + + Writer.startMemberArray("Properties", true); + + DzElement* elSimulationSettingsProvider; + bool ret = false; + int methodIndex = -1; + methodIndex = Shape->metaObject()->indexOfMethod(QMetaObject::normalizedSignature("findSimulationSettingsProvider(QString)")); + if (methodIndex != -1) + { + QMetaMethod method = Shape->metaObject()->method(methodIndex); + QGenericReturnArgument returnArgument( + method.typeName(), + &elSimulationSettingsProvider + ); + ret = method.invoke(Shape, returnArgument, Q_ARG(QString, Material->getName())); + if (elSimulationSettingsProvider) + { + int numProperties = elSimulationSettingsProvider->getNumProperties(); + DzPropertyListIterator propIter = elSimulationSettingsProvider->propertyListIterator(); + QString propString = ""; + int propIndex = 0; + while (propIter.hasNext()) + { + DzProperty* Property = propIter.next(); + DzNumericProperty* NumericProperty = qobject_cast(Property); + if (NumericProperty) + { + QString Name = Property->getName(); + QString TextureName = ""; + if (NumericProperty->getMapValue()) + { + TextureName = NumericProperty->getMapValue()->getFilename(); + } + Writer.startObject(true); + Writer.addMember("Name", Name); + Writer.addMember("Value", QString::number(NumericProperty->getDoubleValue())); + Writer.addMember("Data Type", QString("Double")); + Writer.addMember("Texture", TextureName); + Writer.finishObject(); + } + } + + } + + } + + Writer.finishArray(); +} + +void DzBridgeAction::writeAllPoses(DzJsonWriter& writer) +{ + writer.startMemberArray("Poses", true); + for (QList::iterator i = m_aPoseList.begin(); i != m_aPoseList.end(); ++i) + { + writer.startObject(true); + writer.addMember("Name", *i); + writer.addMember("Label", m_mMorphNameToLabel[*i]); + writer.finishObject(); + } + writer.finishArray(); +} + +void DzBridgeAction::writeEnvironment(DzJsonWriter& writer) +{ + writer.startMemberArray("Instances", true); + QMap WritingInstances; + QList ExportedGeometry; + writeInstances(m_pSelectedNode, writer, WritingInstances, ExportedGeometry); + writer.finishArray(); +} + +void DzBridgeAction::writeInstances(DzNode* Node, DzJsonWriter& Writer, QMap& WritenInstances, QList& ExportedGeometry, QUuid ParentID) +{ + if (Node == nullptr) + return; + + DzObject* Object = Node->getObject(); + DzShape* Shape = Object ? Object->getCurrentShape() : NULL; + DzGeometry* Geometry = Shape ? Shape->getGeometry() : NULL; + DzBone* Bone = qobject_cast(Node); + DzGroupNode* GroupNode = qobject_cast(Node); + DzInstanceNode* InstanceNode = qobject_cast(Node); + + if (Bone == nullptr && Geometry) + { + ExportedGeometry.append(Geometry); + ParentID = writeInstance(Node, Writer, ParentID); + } + + if (GroupNode) + { + ParentID = writeInstance(Node, Writer, ParentID); + } + + if (InstanceNode) + { + ParentID = writeInstance(Node, Writer, ParentID); + } + + for (int ChildIndex = 0; ChildIndex < Node->getNumNodeChildren(); ChildIndex++) + { + DzNode* ChildNode = Node->getNodeChild(ChildIndex); + writeInstances(ChildNode, Writer, WritenInstances, ExportedGeometry, ParentID); + } +} + +QUuid DzBridgeAction::writeInstance(DzNode* Node, DzJsonWriter& Writer, QUuid ParentID) +{ + if (Node == nullptr) +#ifdef __APPLE__ + return 0; +#else + return false; +#endif + + QString Path = Node->getAssetFileInfo().getUri().getFilePath(); + QFile File(Path); + QString AssetID = Node->getAssetUri().getId(); + QString Name = AssetID.remove(QRegExp("[^A-Za-z0-9_]")); + QUuid Uid = QUuid::createUuid(); + + // Group Node needs an empty InstanceAsset + DzGroupNode* GroupNode = qobject_cast(Node); + if (GroupNode) + { + Name = ""; + } + + // Instance Node needs an InstanceAsset + DzInstanceNode* InstanceNode = qobject_cast(Node); + if (InstanceNode) + { + AssetID = InstanceNode->getTarget()->getAssetUri().getId(); + Name = AssetID.remove(QRegExp("[^A-Za-z0-9_]")); + } + + Writer.startObject(true); + Writer.addMember("Version", 1); + Writer.addMember("InstanceLabel", Node->getLabel()); + Writer.addMember("InstanceAsset", Name); + Writer.addMember("ParentID", ParentID.toString()); + Writer.addMember("Guid", Uid.toString()); + Writer.addMember("TranslationX", Node->getWSPos().m_x); + Writer.addMember("TranslationY", Node->getWSPos().m_y); + Writer.addMember("TranslationZ", Node->getWSPos().m_z); + + DzQuat RotationQuat = Node->getWSRot(); + DzVec3 Rotation; + RotationQuat.getValue(Node->getRotationOrder(), Rotation); + Writer.addMember("RotationX", Rotation.m_x); + Writer.addMember("RotationY", Rotation.m_y); + Writer.addMember("RotationZ", Rotation.m_z); + + DzMatrix3 Scale = Node->getWSScale(); + + Writer.addMember("ScaleX", Scale.row(0).length()); + Writer.addMember("ScaleY", Scale.row(1).length()); + Writer.addMember("ScaleZ", Scale.row(2).length()); + Writer.finishObject(); + + return Uid; +} + +void DzBridgeAction::readGui(DzBridgeDialog* BridgeDialog) +{ + if (BridgeDialog == nullptr) + return; + + // Collect the values from the dialog fields + if (m_sAssetName == "" || m_nNonInteractiveMode == 0) m_sAssetName = BridgeDialog->getAssetNameEdit()->text(); + if (m_sRootFolder == "" || m_nNonInteractiveMode == 0) m_sRootFolder = readGuiRootFolder(); + if (m_sExportSubfolder == "" || m_nNonInteractiveMode == 0) m_sExportSubfolder = m_sAssetName; + m_sDestinationPath = m_sRootFolder + "/" + m_sExportSubfolder + "/"; + if (m_sExportFbx == "" || m_nNonInteractiveMode == 0) m_sExportFbx = m_sAssetName; + m_sDestinationFBX = m_sDestinationPath + m_sExportFbx + ".fbx"; + + if (m_nNonInteractiveMode == 0) + { + // TODO: consider removing once findData( ) method above is completely implemented + m_sAssetType = cleanString(BridgeDialog->getAssetTypeCombo()->currentText()); + + m_sMorphSelectionRule = BridgeDialog->GetMorphString(); + m_mMorphNameToLabel = BridgeDialog->GetMorphMapping(); + m_bEnableMorphs = BridgeDialog->getMorphsEnabledCheckBox()->isChecked(); + } + + m_EnableSubdivisions = BridgeDialog->getSubdivisionEnabledCheckBox()->isChecked(); + m_bShowFbxOptions = BridgeDialog->getShowFbxDialogCheckBox()->isChecked(); + if (m_subdivisionDialog == nullptr) + { + m_subdivisionDialog = DzBridgeSubdivisionDialog::Get(BridgeDialog); + } + if (m_morphSelectionDialog == nullptr) + { + m_morphSelectionDialog = DzBridgeMorphSelectionDialog::Get(BridgeDialog); + } + m_sFbxVersion = BridgeDialog->getFbxVersionCombo()->currentText(); + m_bGenerateNormalMaps = BridgeDialog->getEnableNormalMapGenerationCheckBox()->isChecked(); + +} + +// ------------------------------------------------ +// PixelIntensity +// ------------------------------------------------ +double DzBridgeAction::getPixelIntensity(const QRgb& pixel) +{ + const double r = double(qRed(pixel)); + const double g = double(qGreen(pixel)); + const double b = double(qBlue(pixel)); + const double average = (r + g + b) / 3.0; + return average / 255.0; +} + +// ------------------------------------------------ +// MapComponent +// ------------------------------------------------ +uint8_t DzBridgeAction::getNormalMapComponent(double pX) +{ + return (pX + 1.0) * (255.0 / 2.0); +} + +// ------------------------------------------------ +// intclamp +// ------------------------------------------------ +int DzBridgeAction::getIntClamp(int x, int low, int high) +{ + if (x < low) { return low; } + else if (x > high) { return high; } + return x; +} + +// ------------------------------------------------ +// map_component +// ------------------------------------------------ +QRgb DzBridgeAction::getSafePixel(const QImage& img, int x, int y) +{ + int ix = this->getIntClamp(x, 0, img.size().width() - 1); + int iy = this->getIntClamp(y, 0, img.size().height() - 1); + return img.pixel(ix, iy); +} + +// ------------------------------------------------ +// makeNormalMapFromBumpMap +// ------------------------------------------------ +QImage DzBridgeAction::makeNormalMapFromHeightMap(QString heightMapFilename, double normalStrength) +{ + // load qimage + QImage image; + image.load(heightMapFilename); + int imageWidth = image.size().width(); + int imageHeight = image.size().height(); + + QImage result = QImage(imageWidth, imageHeight, QImage::Format_ARGB32_Premultiplied); + QRect rect = result.rect(); + int r1 = rect.top(); + int r2 = rect.bottom(); + int c1 = rect.left(); + int c2 = rect.right(); + + QFileInfo fileInfo = QFileInfo(heightMapFilename); + QString progressString = QString("Generating Normal Map: %1 (%2 x %3)").arg(fileInfo.fileName()).arg(imageWidth).arg(imageHeight); + + DzProgress progress = DzProgress(progressString, 100, false, true); + float step = ((float)(r2 - r1)) / 100.0; + float current = 0; + + // row loop + for (int row = r1; row <= r2; row++) { + current++; + if (current >= step) { + progress.step(); + current = 0; + } + + // col loop + for (int col = c1; col <= c2; col++) { + + // skip blank pixels to speed conversion + QRgb rgbMask = image.pixel(col, row); + int mask = QColor(rgbMask).value(); + if (mask == 0 || mask == 255) + { + const QColor color = QColor(128, 127, 255); + result.setPixel(col, row, color.rgb()); + continue; + } + + // Pixel Picker + const QRgb topLeft = this->getSafePixel(image, col - 1, row - 1); + const QRgb top = this->getSafePixel(image, col, row - 1); + const QRgb topRight = this->getSafePixel(image, col + 1, row - 1); + const QRgb right = this->getSafePixel(image, col + 1, row); + const QRgb bottomRight = this->getSafePixel(image, col + 1, row + 1); + const QRgb bottom = this->getSafePixel(image, col, row + 1); + const QRgb bottomLeft = this->getSafePixel(image, col - 1, row + 1); + const QRgb left = this->getSafePixel(image, col - 1, row); + + // calculating pixel intensities + const double tl = this->getPixelIntensity(topLeft); + const double t = this->getPixelIntensity(top); + const double tr = this->getPixelIntensity(topRight); + const double r = this->getPixelIntensity(right); + const double br = this->getPixelIntensity(bottomRight); + const double b = this->getPixelIntensity(bottom); + const double bl = this->getPixelIntensity(bottomLeft); + const double l = this->getPixelIntensity(left); + + //// skip edge cases to reduce seam + //if (tl == 0 || t == 0 || tr == 0 || r == 0 || br == 0 || b == 0 || bl == 0 || l == 0) + //{ + // const QColor color = QColor(128, 127, 255); + // result.setPixel(col, row, color.rgb()); + // continue; + //} + + // Sobel filter + const double dX = (tr + 2.0 * r + br) - (tl + 2.0 * l + bl); + const double dY = 1.0 / normalStrength; + const double dZ = (bl + 2.0 * b + br) - (tl + 2.0 * t + tr); + const DzVec3 vec = DzVec3(dX, dY, dZ).normalized(); + + // DS uses Y as up, not Z, Normalmaps uses Z + const QColor color = QColor(this->getNormalMapComponent(vec.m_x), this->getNormalMapComponent(vec.m_z), this->getNormalMapComponent(vec.m_y)); + result.setPixel(col, row, color.rgb()); + } + } + + return result; +} + +QStringList DzBridgeAction::getAvailableMorphs(DzNode* Node) +{ + QStringList newMorphList; + newMorphList.clear(); + + if (Node == nullptr) + return newMorphList; + + DzObject* Object = Node->getObject(); + DzShape* Shape = Object ? Object->getCurrentShape() : NULL; + + for (int index = 0; index < Node->getNumProperties(); index++) + { + DzProperty* property = Node->getProperty(index); + QString propName = property->getName(); + QString propLabel = property->getLabel(); + DzPresentation* presentation = property->getPresentation(); + if (presentation && presentation->getType() == "Modifier/Shape") + { + newMorphList.append(propName); + } + } + + if (Object) + { + for (int index = 0; index < Object->getNumModifiers(); index++) + { + DzModifier* modifier = Object->getModifier(index); + QString modName = modifier->getName(); + QString modLabel = modifier->getLabel(); + DzMorph* mod = qobject_cast(modifier); + if (mod) + { + for (int propindex = 0; propindex < modifier->getNumProperties(); propindex++) + { + DzProperty* property = modifier->getProperty(propindex); + QString propName = property->getName(); + QString propLabel = property->getLabel(); + DzPresentation* presentation = property->getPresentation(); + if (presentation) + { + newMorphList.append(modName); + } + } + } + } + } + + return newMorphList; +} + +QStringList DzBridgeAction::getActiveMorphs(DzNode* Node) +{ + QStringList newMorphList; + newMorphList.clear(); + + if (Node == nullptr) + return newMorphList; + + DzObject* Object = Node->getObject(); + DzShape* Shape = Object ? Object->getCurrentShape() : NULL; + + for (int index = 0; index < Node->getNumProperties(); index++) + { + DzProperty* property = Node->getProperty(index); + QString propName = property->getName(); + QString propLabel = property->getLabel(); + DzPresentation* presentation = property->getPresentation(); + if (presentation && presentation->getType() == "Modifier/Shape") + { + DzNumericProperty *numericProp = qobject_cast(property); + if (numericProp->getDoubleValue() > 0) + { + newMorphList.append(propName); + } + } + } + + if (Object) + { + for (int index = 0; index < Object->getNumModifiers(); index++) + { + DzModifier* modifier = Object->getModifier(index); + QString modName = modifier->getName(); + QString modLabel = modifier->getLabel(); + DzMorph* mod = qobject_cast(modifier); + if (mod) + { + for (int propindex = 0; propindex < modifier->getNumProperties(); propindex++) + { + DzProperty* property = modifier->getProperty(propindex); + QString propName = property->getName(); + QString propLabel = property->getLabel(); + DzPresentation* presentation = property->getPresentation(); + if (presentation) + { + DzNumericProperty* numericProp = qobject_cast(property); + if (numericProp->getDoubleValue() > 0) + { + newMorphList.append(modName); + } + } + } + } + } + } + + return newMorphList; +} + +bool DzBridgeAction::setBridgeDialog(DzBasicDialog* arg_dlg) +{ + m_bridgeDialog = qobject_cast(arg_dlg); + + if (m_bridgeDialog == nullptr) + { + if (arg_dlg == nullptr) + return true; + + if (arg_dlg->inherits("DzBridgeDialog")) + { + m_bridgeDialog = (DzBridgeDialog*)arg_dlg; + // WARNING + printf("WARNING: DzBridge version mismatch detected! Crashes may occur."); + } + + // return false to signal version mismatch + return false; + } + + return true; +} + +bool DzBridgeAction::setSubdivisionDialog(DzBasicDialog* arg_dlg) +{ + m_subdivisionDialog = qobject_cast(arg_dlg); + + if (m_subdivisionDialog == nullptr) + { + if (arg_dlg == nullptr) + return true; + + if (arg_dlg->inherits("DzBridgeSubdivisionDialog")) + { + m_subdivisionDialog = (DzBridgeSubdivisionDialog*)arg_dlg; + // WARNING + printf("WARNING: DzBridge version mismatch detected! Crashes may occur."); + } + + // return false to signal version mismatch + return false; + } + + return true; +} + +bool DzBridgeAction::setMorphSelectionDialog(DzBasicDialog* arg_dlg) +{ + m_morphSelectionDialog = qobject_cast(arg_dlg); + + if (m_morphSelectionDialog == nullptr) + { + if (arg_dlg == nullptr) + return true; + + if (arg_dlg->inherits("DzBridgeMorphSelectionDialog")) + { + m_morphSelectionDialog = (DzBridgeMorphSelectionDialog*)arg_dlg; + // WARNING + printf("WARNING: DzBridge version mismatch detected! Crashes may occur."); + } + + // return false to signal version mismatch + return false; + } + + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// START: DFORCE WEIGHTMAPS +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Write weightmaps - recursively traverse parent/children, and export all associated weightmaps +void DzBridgeAction::writeWeightMaps(DzNode* Node, DzJsonWriter& Writer) +{ + if (Node == nullptr) + return; + + DzObject* Object = Node->getObject(); + DzShape* Shape = Object ? Object->getCurrentShape() : NULL; + + bool bDForceSettingsAvailable = false; + + if (Shape && Shape->inherits("DzFacetShape")) + { + DzModifier* dforceModifier; + DzModifierIterator modIter = Object->modifierIterator(); + while (modIter.hasNext()) + { + DzModifier* modifier = modIter.next(); + QString mod_Class = modifier->className(); + if (mod_Class.toLower().contains("dforce")) + { + bDForceSettingsAvailable = true; + dforceModifier = modifier; + break; + } + } + + if (bDForceSettingsAvailable) + { + /////////////////////////////////////////////// + // Method for obtaining weightmaps, grab directly from dForce Modifier Node + /////////////////////////////////////////////// + int methodIndex = dforceModifier->metaObject()->indexOfMethod(QMetaObject::normalizedSignature("getInfluenceWeights()")); + if (methodIndex != -1) + { + QMetaMethod method = dforceModifier->metaObject()->method(methodIndex); + DzWeightMap* weightMap; + QGenericReturnArgument returnArg( + method.typeName(), + &weightMap + ); + int result = method.invoke((QObject*)dforceModifier, returnArg); + if (result != -1) + { + if (weightMap) + { + int numVerts = Shape->getAssemblyGeometry()->getNumVertices(); + unsigned short* daz_weights = weightMap->getWeights(); + int byte_length = numVerts * sizeof(unsigned short); + + char* buffer = new char[byte_length]; + unsigned short* unity_weights = (unsigned short*)buffer; + + // load material groups to remap weights to unity's vertex order + DzFacetMesh* facetMesh = dynamic_cast(Shape)->getFacetMesh(); + if (facetMesh) + { + // sanity check + if (numVerts != facetMesh->getNumVertices()) + { + // throw error if needed + dzApp->log("DazBridge: ERROR Exporting Weight Map to file."); + return; + } + int numMaterials = facetMesh->getNumMaterialGroups(); + std::list exportQueue; + DzFacet* facetPtr = facetMesh->getFacetsPtr(); + + // generate export order queue + // first, populate export queue with materialgroups + for (int i = 0; i < numMaterials; i++) + { + DzMaterialFaceGroup* materialGroup = facetMesh->getMaterialGroup(i); + int numFaces = materialGroup->count(); + const int* indexPtr = materialGroup->getIndicesPtr(); + int offset = facetPtr[indexPtr[0]].m_vertIdx[0]; + int count = -1; + MaterialGroupExportOrderMetaData* metaData = new MaterialGroupExportOrderMetaData(i, offset); + exportQueue.push_back(*metaData); + } + + // sort: uses operator< to order by vertex_offset + exportQueue.sort(); + + ///////////////////////////////////////// + // for building vertex index lookup tables + ///////////////////////////////////////// + int material_vertex_count = 0; + int material_vertex_offset = 0; + int* DazToUnityLookup = new int[numVerts]; + for (int i = 0; i < numVerts; i++) { DazToUnityLookup[i] = -1; } + int* UnityToDazLookup = new int[numVerts]; + for (int i = 0; i < numVerts; i++) { UnityToDazLookup[i] = -1; } + + int unity_weightmap_vertexindex = 0; + // iterate through sorted material groups... + for (std::list::iterator export_iter = exportQueue.begin(); export_iter != exportQueue.end(); export_iter++) + { + // update the vert_offset for each materialGroup + material_vertex_offset = material_vertex_offset + material_vertex_count; + material_vertex_count = 0; + int check_offset = export_iter->vertex_offset; + + // retrieve material group based on sorted material index list + int materialGroupIndex = export_iter->materialIndex; + DzMaterialFaceGroup* materialGroup = facetMesh->getMaterialGroup(materialGroupIndex); + int numFaces = materialGroup->count(); + // pointer for faces in materialGroup + const int* indexPtr = materialGroup->getIndicesPtr(); + + // get each face in materialGroup, then iterate through all vertex indices in the face + // copy out weights into buffer using material group's vertex ordering, but cross-referenced with internal vertex array indices + + // get the i-th index into the index array of faces, then retrieve the j-th index into the vertex index array + // i is 0 to number of faces (aka facets), j is 0 to number of vertices in the face + for (int i = 0; i < numFaces; i++) + { + int vertsPerFacet = (facetPtr->isQuad()) ? 4 : 3; + for (int j = 0; j < vertsPerFacet; j++) + { + // retrieve vertex index into daz internal vertex array (probably a BST in array format) + int vert_index = facetPtr[indexPtr[i]].m_vertIdx[j]; + + /////////////////////////////////// + // NOTE: Since the faces will often share/re-use the same vertex, we need to skip + // any vertex that has already been recorded, since we want ONLY unique vertices + // in weightmap. This is done by creating checking back into a DazToUnity vertex index lookup table + /////////////////////////////////// + // unique vertices will not yet be written and have default -1 value + if (DazToUnityLookup[vert_index] == -1) + { + // This vertex is unique, record into the daztounity lookup table and proceed with other operations + // to be performend on unqiue verts. + DazToUnityLookup[vert_index] = unity_weightmap_vertexindex; + + // use the vertex index to cross-reference to the corresponding weightmap value and copy out to buffer for exporting + // (only do this for unique verts) + unity_weights[unity_weightmap_vertexindex] = weightMap->getWeight(vert_index); + //unity_weights[unity_weightmap_vertexindex] = daz_weights[vert_index]; + + // Create the unity to daz vertex lookup table (only do this for unique verts) + UnityToDazLookup[unity_weightmap_vertexindex] = vert_index; + + // increment the unity weightmap vertex index (only do this for unique verts) + unity_weightmap_vertexindex++; + } + } //for (int j = 0; j < vertsPerFace; j++) + } // for (int i = 0; i < numFaces; i++) + } // for (std::list::iterator export_iter = exportQueue.begin(); export_iter != exportQueue.end(); export_iter++) + + // export to dforce_weightmap file + QString filename = QString("%1.dforce_weightmap.bytes").arg(cleanString(Node->getLabel())); + QFile rawWeight(m_sDestinationPath + filename); + if (rawWeight.open(QIODevice::WriteOnly)) + { + int bytesWritten = rawWeight.write(buffer, byte_length); + if (bytesWritten != byte_length) + { + // write error + QString errString = rawWeight.errorString(); + if (m_nNonInteractiveMode == 0) QMessageBox::warning(0, + tr("Error writing dforce weightmap. Incorrect number of bytes written."), + errString, QMessageBox::Ok); + } + rawWeight.close(); + + // Write entry into DTU for weightmap lookup + Writer.startObject(true); + Writer.addMember("Asset Name", Node->getLabel()); + Writer.addMember("Weightmap Filename", filename); + Writer.finishObject(); + + } + + } // if (facetMesh) /** facetMesh null? */ + } // if (weightMap) /** weightmap null? */ + } // if (result != -1) /** invokeMethod failed? */ + } // if (methodIndex != -1) /** findMethod failed? */ + } // if (bDForceSettingsAvailable) /** no dforce data found */ + } // if (Shape) + + DzNodeListIterator Iterator = Node->nodeChildrenIterator(); + while (Iterator.hasNext()) + { + DzNode* Child = Iterator.next(); + writeWeightMaps(Child, Writer); + } + +} + +// OLD Method for obtaining weightmap, relying on dForce Weight Modifier Node +DzWeightMapPtr DzBridgeAction::getWeightMapPtr(DzNode* Node) +{ + if (Node == nullptr) + return nullptr; + + // 1. check if weightmap modifier present + DzNodeListIterator Iterator = Node->nodeChildrenIterator(); + while (Iterator.hasNext()) + { + DzNode* Child = Iterator.next(); + if (Child->className().contains("DzDForceModifierWeightNode")) + { + QObject* handler; + if (metaInvokeMethod(Child, "getWeightMapHandler()", (void**)&handler)) + { + QObject* weightGroup; + if (metaInvokeMethod(handler, "currentWeightGroup()", (void**)&weightGroup)) + { + QObject* context; + if (metaInvokeMethod(weightGroup, "currentWeightContext()", (void**)&context)) + { + DzWeightMapPtr weightMap; + // DzWeightMapPtr + QMetaMethod metaMethod = context->metaObject()->method(30); // getWeightMap() + QGenericReturnArgument returnArgument( + metaMethod.typeName(), + &weightMap + ); + int result = metaMethod.invoke((QObject*)context, returnArgument); + if (result != -1) + { + return weightMap; + } + } + } + } + } + + } + + return nullptr; + +} + +bool DzBridgeAction::metaInvokeMethod(QObject* object, const char* methodSig, void** returnPtr) +{ + if (object == nullptr) + { + return false; + } + + ////////////////////////////////////////////////////////////////// + // REFERENCE Signatures obtained by QMetaObject->method() query + ////////////////////////////////////////////////////////////////// + // + // DzDForceModifierWeightNode::getWeightMapHandler() = 372 + // + // DzDForceModifierWeightHandler::getWeightGroup(int) = 18 + // DzDForceModifierWeightHandler::currentWeightGroup() = 20 + // + // DzDForceModifierWeightGroup::getWeightMapContext(int) = 19 + // DzDForceModifierWeightGroup::currentWeightContext() = 22 + // + // DzDForceModiferMapContext::getWeightMap() = 30 + ///////////////////////////////////////////////////////////////////////// + + // find the metamethod + const QMetaObject* metaObject = object->metaObject(); + int methodIndex = metaObject->indexOfMethod(QMetaObject::normalizedSignature(methodSig)); + if (methodIndex == -1) + { + // use fuzzy search + // look up all methods, find closest match for methodSig + int searchResult = -1; + QString fuzzySig = QString(QMetaObject::normalizedSignature(methodSig)).toLower().remove("()"); + for (int i = 0; i < metaObject->methodCount(); i++) + { + const char* sig = metaObject->method(i).signature(); + if (QString(sig).toLower().contains(fuzzySig)) + { + searchResult = i; + break; + } + } + if (searchResult == -1) + { + return false; + } + else + { + methodIndex = searchResult; + } + + } + + // invoke metamethod + QMetaMethod metaMethod = metaObject->method(methodIndex); + void* returnVal; + QGenericReturnArgument returnArgument( + metaMethod.typeName(), + &returnVal + ); + int result = metaMethod.invoke((QObject*)object, returnArgument); + if (result) + { + // set returnvalue + *returnPtr = returnVal; + + return true; + } + + return false; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// END: DFORCE WEIGHTMAPS +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// START: SUBDIVISION +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#ifdef __APPLE__ +#define USING_LIBSTDCPP 1 +#endif +#include "OpenFBXInterface.h" +#include "OpenSubdivInterface.h" + + +bool DzBridgeAction::upgradeToHD(QString baseFilePath, QString hdFilePath, QString outFilePath, std::map* pLookupTable) +{ + OpenFBXInterface* openFBX = OpenFBXInterface::GetInterface(); + FbxScene* baseMeshScene = openFBX->CreateScene("Base Mesh Scene"); + if (openFBX->LoadScene(baseMeshScene, baseFilePath.toLocal8Bit().data()) == false) + { + if (m_nNonInteractiveMode == 0) QMessageBox::warning(0, "Error", + "An error occurred while loading the base scene...", QMessageBox::Ok); + printf("\n\nAn error occurred while loading the base scene..."); + return false; + } + SubdivideFbxScene subdivider = SubdivideFbxScene(baseMeshScene, pLookupTable); + subdivider.ProcessScene(); + FbxScene* hdMeshScene = openFBX->CreateScene("HD Mesh Scene"); + if (openFBX->LoadScene(hdMeshScene, hdFilePath.toLocal8Bit().data()) == false) + { + if (m_nNonInteractiveMode == 0) QMessageBox::warning(0, "Error", + "An error occurred while loading the base scene...", QMessageBox::Ok); + printf("\n\nAn error occurred while loading the base scene..."); + return false; + } + subdivider.SaveClustersToScene(hdMeshScene); + if (openFBX->SaveScene(hdMeshScene, outFilePath.toLocal8Bit().data()) == false) + { + if (m_nNonInteractiveMode == 0) QMessageBox::warning(0, "Error", + "An error occurred while saving the scene...", QMessageBox::Ok); + + printf("\n\nAn error occurred while saving the scene..."); + return false; + } + + return true; + +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// END: SUBDIVISION +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +QString DzBridgeAction::getMD5(const QString& path) +{ + auto algo = QCryptographicHash::Md5; + QFile sourceFile(path); + qint64 fileSize = sourceFile.size(); + const qint64 bufferSize = 10240; + + if (sourceFile.open(QIODevice::ReadOnly)) + { + char buffer[bufferSize]; + int bytesRead; + int readSize = qMin(fileSize, bufferSize); + + QCryptographicHash hash(algo); + while (readSize > 0 && (bytesRead = sourceFile.read(buffer, readSize)) > 0) + { + fileSize -= bytesRead; + hash.addData(buffer, bytesRead); + readSize = qMin(fileSize, bufferSize); + } + + sourceFile.close(); + return QString(hash.result().toHex()); + } + return QString(); +} + +bool DzBridgeAction::copyFile(QFile* file, QString* dst, bool replace, bool compareFiles) +{ + if (file == nullptr || dst == nullptr) + return false; + + bool dstExists = QFile::exists(*dst); + + if (replace) + { + if (compareFiles && dstExists) + { + auto srcFileMD5 = getMD5(file->fileName()); + auto dstFileMD5 = getMD5(*dst); + + if (srcFileMD5.length() > 0 && dstFileMD5.length() > 0 && srcFileMD5.compare(dstFileMD5) == 0) + { + return false; + } + } + + if (dstExists) + { + QFile::remove(*dst); + } + } + + auto result = file->copy(*dst); + + if (QFile::exists(*dst)) + { +#if __APPLE__ + QFile::setPermissions(*dst, QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::ReadGroup | QFile::ReadOther); +#else + QFile::setPermissions(*dst, QFile::ReadOther | QFile::WriteOther); +#endif + } + + return result; +} + + +#include "moc_DzBridgeAction.cpp" diff --git a/src/DzBridgeAction_Scriptable.cpp b/src/DzBridgeAction_Scriptable.cpp new file mode 100644 index 0000000..e034a2f --- /dev/null +++ b/src/DzBridgeAction_Scriptable.cpp @@ -0,0 +1,220 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +//#include +#include "idzsceneasset.h" +#include "dzuri.h" + +#include "DzBridgeAction_Scriptable.h" +#include "DzBridgeDialog_Scriptable.h" +#include "DzBridgeMorphSelectionDialog_Scriptable.h" +#include "DzBridgeSubdivisionDialog_Scriptable.h" + +DzBridgeAction::DzBridgeAction() : + DzBridgeNameSpace::DzBridgeAction(tr("Daz &Scriptable Bridge"), tr("Send the selected node to Daz Scriptable Bridge.")) +{ + m_nNonInteractiveMode = 0; + m_sAssetType = QString("SkeletalMesh"); + //Setup Icon + //QString iconName = "icon"; + //QPixmap basePixmap = QPixmap::fromImage(getEmbeddedImage(iconName.toLatin1())); + //QIcon icon; + //icon.addPixmap(basePixmap, QIcon::Normal, QIcon::Off); + //QAction::setIcon(icon); + + m_bGenerateNormalMaps = false; +} + +void DzBridgeAction::executeAction() +{ + // Check if the main window has been created yet. + // If it hasn't, alert the user and exit early. + DzMainWindow* mw = dzApp->getInterface(); + if (!mw) + { + if (m_nNonInteractiveMode == 0) + { + QMessageBox::warning(0, tr("Error"), + tr("The main window has not been created yet."), QMessageBox::Ok); + } + return; + } + + // Create and show the dialog. If the user cancels, exit early, + // otherwise continue on and do the thing that required modal + // input from the user. + if (dzScene->getNumSelectedNodes() != 1) + { + if (m_nNonInteractiveMode == 0) + { + QMessageBox::warning(0, tr("Error"), + tr("Please select one Character or Prop to send."), QMessageBox::Ok); + } + return; + } + + // Create the dialog + if (m_bridgeDialog == nullptr) + { + m_bridgeDialog = new DzBridgeDialog(mw, QString(tr("Daz Scriptable Bridge"))); + } + + // Prepare member variables when not using GUI + if (m_nNonInteractiveMode == 1) + { +// if (m_sRootFolder != "") m_bridgeDialog->getIntermediateFolderEdit()->setText(m_sRootFolder); + + if (m_aMorphListOverride.isEmpty() == false) + { + m_bEnableMorphs = true; + m_sMorphSelectionRule = m_aMorphListOverride.join("\n1\n"); + m_sMorphSelectionRule += "\n1\n.CTRLVS\n2\nAnything\n0"; + if (m_morphSelectionDialog == nullptr) + { + m_morphSelectionDialog = DzBridgeMorphSelectionDialog::Get(m_bridgeDialog); + } + m_mMorphNameToLabel.clear(); + foreach(QString morphName, m_aMorphListOverride) + { + QString label = m_morphSelectionDialog->GetMorphLabelFromName(morphName); + m_mMorphNameToLabel.insert(morphName, label); + } + } + else + { + m_bEnableMorphs = false; + m_sMorphSelectionRule = ""; + m_mMorphNameToLabel.clear(); + } + + } + + // If the Accept button was pressed, start the export + int dialog_choice = -1; + if (m_nNonInteractiveMode == 0) + { + dialog_choice = m_bridgeDialog->exec(); + } + if (m_nNonInteractiveMode == 1 || dialog_choice == QDialog::Accepted) + { + // Read in Common GUI values + readGui(m_bridgeDialog); + + exportHD(); + } +} + +void DzBridgeAction::writeConfiguration() +{ + QString DTUfilename = m_sDestinationPath + m_sAssetName + ".dtu"; + QFile DTUfile(DTUfilename); + DTUfile.open(QIODevice::WriteOnly); + DzJsonWriter writer(&DTUfile); + writer.startObject(true); + + writeDTUHeader(writer); + + if (m_sAssetType.toLower().contains("mesh")) + { + QTextStream *pCVSStream = nullptr; + if (m_bExportMaterialPropertiesCSV) + { + QString filename = m_sDestinationPath + m_sAssetName + "_Maps.csv"; + QFile file(filename); + file.open(QIODevice::WriteOnly); + pCVSStream = new QTextStream(&file); + *pCVSStream << "Version, Object, Material, Type, Color, Opacity, File" << endl; + } + writeAllMaterials(m_pSelectedNode, writer, pCVSStream); + writeAllMorphs(writer); + writeAllSubdivisions(writer); + writeAllDforceInfo(m_pSelectedNode, writer); + } + + if (m_sAssetType == "Pose") + { + writeAllPoses(writer); + } + + if (m_sAssetType == "Environment") + { + writeEnvironment(writer); + } + + writer.finishObject(); + DTUfile.close(); + +} + +// Setup custom FBX export options +void DzBridgeAction::setExportOptions(DzFileIOSettings& ExportOptions) +{ + +} + +// Overrides baseclass implementation with Unreal specific resets +// Resets Default Values but Ignores any saved settings +void DzBridgeAction::resetToDefaults() +{ + DzBridgeNameSpace::DzBridgeAction::resetToDefaults(); + + // Must Instantiate m_bridgeDialog so that we can override any saved states + if (m_bridgeDialog == nullptr) + { + DzMainWindow* mw = dzApp->getInterface(); + m_bridgeDialog = new DzBridgeDialog(mw); + } + m_bridgeDialog->resetToDefaults(); + + if (m_subdivisionDialog != nullptr) + { + foreach(QObject * obj, m_subdivisionDialog->getSubdivisionCombos()) + { + QComboBox* combo = qobject_cast(obj); + if (combo) + combo->setCurrentIndex(0); + } + } + // reset morph selection + //DzBridgeMorphSelectionDialog::Get(nullptr)->PrepareDialog(); + +} + +QString DzBridgeAction::readGuiRootFolder() +{ + QString rootFolder = QDesktopServices::storageLocation(QDesktopServices::DocumentsLocation) + QDir::separator() + "DazBridge"; + + if (m_bridgeDialog) + { + QLineEdit* intermediateFolderEdit = nullptr; + DzBridgeDialog* bridgeDialog = qobject_cast(m_bridgeDialog); + + //if (bridgeDialog) + // intermediateFolderEdit = bridgeDialog->getIntermediateFolderEdit(); + + if (intermediateFolderEdit) + rootFolder = intermediateFolderEdit->text().replace("\\", "/"); + } + + return rootFolder; +} + +#include "moc_DzBridgeAction_Scriptable.cpp" diff --git a/src/DzBridgeAction_Scriptable.h b/src/DzBridgeAction_Scriptable.h new file mode 100644 index 0000000..b0ac60d --- /dev/null +++ b/src/DzBridgeAction_Scriptable.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dzbridge.h" + +class DzBridgeAction : public DzBridgeNameSpace::DzBridgeAction { + Q_OBJECT +public: + DzBridgeAction(); + + Q_INVOKABLE void resetToDefaults(); + QString readGuiRootFolder(); + +protected: + void executeAction(); + Q_INVOKABLE void writeConfiguration(); + void setExportOptions(DzFileIOSettings& ExportOptions); + + virtual QString getDefaultMenuPath() const { return tr(""); } + +}; diff --git a/src/DzBridgeDialog.cpp b/src/DzBridgeDialog.cpp new file mode 100644 index 0000000..f33faf4 --- /dev/null +++ b/src/DzBridgeDialog.cpp @@ -0,0 +1,310 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dzapp.h" +#include "dzscene.h" +#include "dzstyle.h" +#include "dzmainwindow.h" +#include "dzactionmgr.h" +#include "dzaction.h" +#include "dzskeleton.h" + +#include "DzBridgeAction.h" +#include "DzBridgeDialog.h" +#include "DzBridgeMorphSelectionDialog.h" +#include "DzBridgeSubdivisionDialog.h" +#include "common_version.h" + +/***************************** +Local definitions +*****************************/ +#define DAZ_BRIDGE_LIBRARY_NAME "Daz Bridge" + +using namespace DzBridgeNameSpace; + +DzBridgeDialog::DzBridgeDialog(QWidget *parent, const QString &windowTitle) : + DzBasicDialog(parent, DAZ_BRIDGE_LIBRARY_NAME) +{ + assetNameEdit = NULL; +// projectEdit = NULL; +// projectButton = NULL; + assetTypeCombo = NULL; + morphsButton = NULL; + morphsEnabledCheckBox = NULL; + subdivisionButton = NULL; + subdivisionEnabledCheckBox = NULL; + advancedSettingsGroupBox = NULL; + fbxVersionCombo = NULL; + showFbxDialogCheckBox = NULL; + + settings = nullptr; + + // Declarations + int margin = style()->pixelMetric(DZ_PM_GeneralMargin); + int wgtHeight = style()->pixelMetric(DZ_PM_ButtonHeight); + int btnMinWidth = style()->pixelMetric(DZ_PM_ButtonMinWidth); + + // Set the dialog title + int revision = COMMON_REV % 1000; + QString workingTitle; + if (windowTitle != "") + workingTitle = windowTitle + QString(tr(" v%1.%2 Build %3.%4")).arg(COMMON_MAJOR).arg(COMMON_MINOR).arg(revision).arg(COMMON_BUILD); + else + workingTitle = QString(tr("DazBridge v%1.%2 Build %3.%4").arg(COMMON_MAJOR).arg(COMMON_MINOR).arg(revision).arg(COMMON_BUILD)); + setWindowTitle(workingTitle); + layout()->setSizeConstraint(QLayout::SetFixedSize); + mainLayout = new QFormLayout(); + + advancedWidget = new QWidget(); + QHBoxLayout* advancedLayoutOuter = new QHBoxLayout(); + advancedLayoutOuter->addWidget(advancedWidget); + advancedLayout = new QFormLayout(); + advancedWidget->setLayout(advancedLayout); + + // Asset Name + assetNameEdit = new QLineEdit(this); + assetNameEdit->setValidator(new QRegExpValidator(QRegExp("[A-Za-z0-9_]*"), this)); + + // Asset Transfer Type + assetTypeCombo = new QComboBox(this); + assetTypeCombo->addItem("Skeletal Mesh"); + assetTypeCombo->addItem("Static Mesh"); + assetTypeCombo->addItem("Animation"); + assetTypeCombo->addItem("Environment"); + assetTypeCombo->addItem("Pose"); + + // Morphs + QHBoxLayout* morphsLayout = new QHBoxLayout(); + morphsButton = new QPushButton("Choose Morphs", this); + connect(morphsButton, SIGNAL(released()), this, SLOT(HandleChooseMorphsButton())); + morphsEnabledCheckBox = new QCheckBox("", this); + morphsEnabledCheckBox->setMaximumWidth(25); + morphsLayout->addWidget(morphsEnabledCheckBox); + morphsLayout->addWidget(morphsButton); + connect(morphsEnabledCheckBox, SIGNAL(stateChanged(int)), this, SLOT(HandleMorphsCheckBoxChange(int))); + + // Subdivision + QHBoxLayout* subdivisionLayout = new QHBoxLayout(); + subdivisionButton = new QPushButton("Choose Subdivisions", this); + connect(subdivisionButton, SIGNAL(released()), this, SLOT(HandleChooseSubdivisionsButton())); + subdivisionEnabledCheckBox = new QCheckBox("", this); + subdivisionEnabledCheckBox->setMaximumWidth(25); + subdivisionLayout->addWidget(subdivisionEnabledCheckBox); + subdivisionLayout->addWidget(subdivisionButton); + connect(subdivisionEnabledCheckBox, SIGNAL(stateChanged(int)), this, SLOT(HandleSubdivisionCheckBoxChange(int))); + + // FBX Version + fbxVersionCombo = new QComboBox(this); + fbxVersionCombo->addItem("FBX 2014 -- Binary"); + fbxVersionCombo->addItem("FBX 2014 -- Ascii"); + fbxVersionCombo->addItem("FBX 2013 -- Binary"); + fbxVersionCombo->addItem("FBX 2013 -- Ascii"); + fbxVersionCombo->addItem("FBX 2012 -- Binary"); + fbxVersionCombo->addItem("FBX 2012 -- Ascii"); + fbxVersionCombo->addItem("FBX 2011 -- Binary"); + fbxVersionCombo->addItem("FBX 2011 -- Ascii"); + fbxVersionCombo->addItem("FBX 2010 -- Binary"); + fbxVersionCombo->addItem("FBX 2010 -- Ascii"); + fbxVersionCombo->addItem("FBX 2009 -- Binary"); + fbxVersionCombo->addItem("FBX 2009 -- Ascii"); + connect(fbxVersionCombo, SIGNAL(currentIndexChanged(const QString &)), this, SLOT(HandleFBXVersionChange(const QString &))); + + // Show FBX Dialog option + showFbxDialogCheckBox = new QCheckBox("", this); + connect(showFbxDialogCheckBox, SIGNAL(stateChanged(int)), this, SLOT(HandleShowFbxDialogCheckBoxChange(int))); + + // Enable Normal Map Generation checkbox + enableNormalMapGenerationCheckBox = new QCheckBox("", this); + connect(enableNormalMapGenerationCheckBox, SIGNAL(stateChanged(int)), this, SLOT(HandleEnableNormalMapGenerationCheckBoxChange(int))); + + // Add the widget to the basic dialog + mainLayout->addRow("Asset Name", assetNameEdit); + mainLayout->addRow("Asset Type", assetTypeCombo); + mainLayout->addRow("Enable Morphs", morphsLayout); + mainLayout->addRow("Enable Subdivision", subdivisionLayout); + advancedLayout->addRow("FBX Version", fbxVersionCombo); + advancedLayout->addRow("Show FBX Dialog", showFbxDialogCheckBox); + advancedLayout->addRow("Enable Normal Map Generation", enableNormalMapGenerationCheckBox); + + addLayout(mainLayout); + + // Advanced + advancedSettingsGroupBox = new QGroupBox("Advanced Settings", this); + advancedSettingsGroupBox->setLayout(advancedLayoutOuter); + advancedSettingsGroupBox->setCheckable(true); + advancedSettingsGroupBox->setChecked(false); + advancedSettingsGroupBox->setFixedWidth(500); // This is what forces the whole forms width + addWidget(advancedSettingsGroupBox); + advancedWidget->setHidden(true); + connect(advancedSettingsGroupBox, SIGNAL(clicked(bool)), this, SLOT(HandleShowAdvancedSettingsCheckBoxChange(bool))); + + // Help + assetNameEdit->setWhatsThis("This is the name the asset will use in Unreal."); + assetTypeCombo->setWhatsThis("Skeletal Mesh for something with moving parts, like a character\nStatic Mesh for things like props\nAnimation for a character animation."); + fbxVersionCombo->setWhatsThis("The version of FBX to use when exporting assets."); + showFbxDialogCheckBox->setWhatsThis("Checking this will show the FBX Dialog for adjustments before export."); + + connect(dzScene, SIGNAL(nodeSelectionListChanged()), this, SLOT(handleSceneSelectionChanged())); + + // Set Defaults + resetToDefaults(); + +} + +bool DzBridgeDialog::loadSavedSettings() +{ + if (settings == nullptr) + { + return false; + } + + if (!settings->value("MorphsEnabled").isNull()) + { + morphsEnabledCheckBox->setChecked(settings->value("MorphsEnabled").toBool()); + } + if (!settings->value("SubdivisionEnabled").isNull()) + { + subdivisionEnabledCheckBox->setChecked(settings->value("SubdivisionEnabled").toBool()); + } + if (!settings->value("ShowFBXDialog").isNull()) + { + showFbxDialogCheckBox->setChecked(settings->value("ShowFBXDialog").toBool()); + } + if (!settings->value("ShowAdvancedSettings").isNull()) + { + advancedSettingsGroupBox->setChecked(settings->value("ShowAdvancedSettings").toBool()); + advancedWidget->setHidden(!advancedSettingsGroupBox->isChecked()); + } + if (!settings->value("FBXExportVersion").isNull()) + { + int index = fbxVersionCombo->findText(settings->value("FBXExportVersion").toString()); + if (index != -1) + { + fbxVersionCombo->setCurrentIndex(index); + } + } + if (!settings->value("EnableNormalMapGeneration").isNull()) + { + enableNormalMapGenerationCheckBox->setChecked(settings->value("EnableNormalMapGeneration").toBool()); + } + + return true; +} + +void DzBridgeDialog::refreshAsset() +{ + DzNode* Selection = dzScene->getPrimarySelection(); + if (dzScene->getFilename().length() > 0) + { + QFileInfo fileInfo = QFileInfo(dzScene->getFilename()); + assetNameEdit->setText(fileInfo.baseName().remove(QRegExp("[^A-Za-z0-9_]"))); + } + else if (dzScene->getPrimarySelection()) + { + assetNameEdit->setText(Selection->getLabel().remove(QRegExp("[^A-Za-z0-9_]"))); + } + + if (qobject_cast(Selection)) + { + assetTypeCombo->setCurrentIndex(0); + } + else + { + assetTypeCombo->setCurrentIndex(1); + } + +} + +void DzBridgeDialog::resetToDefaults() +{ + // Set Defaults + refreshAsset(); + + subdivisionEnabledCheckBox->setChecked(false); + morphsEnabledCheckBox->setChecked(false); + showFbxDialogCheckBox->setChecked(false); + +} + +void DzBridgeDialog::handleSceneSelectionChanged() +{ + refreshAsset(); +} + +void DzBridgeDialog::HandleChooseMorphsButton() +{ + DzBridgeMorphSelectionDialog *dlg = DzBridgeMorphSelectionDialog::Get(this); + dlg->exec(); + morphString = dlg->GetMorphString(); + morphMapping = dlg->GetMorphRenaming(); +} + +void DzBridgeDialog::HandleChooseSubdivisionsButton() +{ + DzBridgeSubdivisionDialog *dlg = DzBridgeSubdivisionDialog::Get(this); + dlg->exec(); +} + +QString DzBridgeDialog::GetMorphString() +{ + morphMapping = DzBridgeMorphSelectionDialog::Get(this)->GetMorphRenaming(); + return DzBridgeMorphSelectionDialog::Get(this)->GetMorphString(); +} + +void DzBridgeDialog::HandleMorphsCheckBoxChange(int state) +{ + if (settings == nullptr) return; + settings->setValue("MorphsEnabled", state == Qt::Checked); +} + +void DzBridgeDialog::HandleSubdivisionCheckBoxChange(int state) +{ + if (settings == nullptr) return; + settings->setValue("SubdivisionEnabled", state == Qt::Checked); +} + +void DzBridgeDialog::HandleFBXVersionChange(const QString& fbxVersion) +{ + if (settings == nullptr) return; + settings->setValue("FBXExportVersion", fbxVersion); +} +void DzBridgeDialog::HandleShowFbxDialogCheckBoxChange(int state) +{ + if (settings == nullptr) return; + settings->setValue("ShowFBXDialog", state == Qt::Checked); +} +void DzBridgeDialog::HandleExportMaterialPropertyCSVCheckBoxChange(int state) +{ + if (settings == nullptr) return; + settings->setValue("ExportMaterialPropertyCSV", state == Qt::Checked); +} + +void DzBridgeDialog::HandleShowAdvancedSettingsCheckBoxChange(bool checked) +{ + advancedWidget->setHidden(!checked); + + if (settings == nullptr) return; + settings->setValue("ShowAdvancedSettings", checked); +} +void DzBridgeDialog::HandleEnableNormalMapGenerationCheckBoxChange(int state) +{ + if (settings == nullptr) return; + settings->setValue("EnableNormalMapGeneration", state == Qt::Checked); +} + + +#include "moc_DzBridgeDialog.cpp" diff --git a/src/DzBridgeDialog_Scriptable.cpp b/src/DzBridgeDialog_Scriptable.cpp new file mode 100644 index 0000000..cdc05b0 --- /dev/null +++ b/src/DzBridgeDialog_Scriptable.cpp @@ -0,0 +1,9 @@ + +#include "DzBridgeDialog_Scriptable.h" + +/***************************** +Local definitions +*****************************/ +#define DAZ_BRIDGE_LIBRARY_NAME "Daz Bridge" + +#include "moc_DzBridgeDialog_Scriptable.cpp" diff --git a/src/DzBridgeDialog_Scriptable.h b/src/DzBridgeDialog_Scriptable.h new file mode 100644 index 0000000..1ba4694 --- /dev/null +++ b/src/DzBridgeDialog_Scriptable.h @@ -0,0 +1,13 @@ +#pragma once + +#include "DzBridgeDialog.h" + +#include "dzbridge.h" + +class CPP_Export DzBridgeDialog : public DzBridgeNameSpace::DzBridgeDialog { + Q_OBJECT +public: + DzBridgeDialog(QWidget* parent = nullptr, const QString& windowTitle = "") : + DzBridgeNameSpace::DzBridgeDialog(parent, windowTitle) {}; + +}; diff --git a/src/DzBridgeMorphSelectionDialog.cpp b/src/DzBridgeMorphSelectionDialog.cpp new file mode 100644 index 0000000..c5ee2dc --- /dev/null +++ b/src/DzBridgeMorphSelectionDialog.cpp @@ -0,0 +1,1038 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dzapp.h" +#include "dzscene.h" +#include "dzstyle.h" +#include "dzmainwindow.h" +#include "dzactionmgr.h" +#include "dzaction.h" +#include "dzskeleton.h" +#include "dzfigure.h" +#include "dzobject.h" +#include "dzshape.h" +#include "dzmodifier.h" +#include "dzpresentation.h" +#include "dzassetmgr.h" +#include "dzproperty.h" +#include "dzsettings.h" +#include "dzmorph.h" +#include "dzcontroller.h" +#include "dznumericnodeproperty.h" +#include "dzerclink.h" +#include "dzbone.h" +#include "DzBridgeMorphSelectionDialog.h" + +#include "QtGui/qlayout.h" +#include "QtGui/qlineedit.h" + +#include + +/***************************** +Local definitions +*****************************/ +#define DAZ_BRIDGE_LIBRARY_NAME "Daz Bridge" + +using namespace DzBridgeNameSpace; + +CPP_Export DzBridgeMorphSelectionDialog* DzBridgeMorphSelectionDialog::singleton = nullptr; + +// For sorting the lists +class SortingListItem : public QListWidgetItem { + +public: + virtual bool operator< (const QListWidgetItem &otherItem) const + { + if (this->checkState() != otherItem.checkState()) + { + return (this->checkState() == Qt::Checked); + } + return QListWidgetItem::operator<(otherItem); + } +}; + +DzBridgeMorphSelectionDialog::DzBridgeMorphSelectionDialog(QWidget *parent) : + DzBasicDialog(parent, DAZ_BRIDGE_LIBRARY_NAME) +{ + settings = new QSettings("Daz 3D", "DazToUnreal"); + + morphListWidget = NULL; + morphExportListWidget = NULL; + morphTreeWidget = NULL; + filterEdit = NULL; + presetCombo = NULL; + fullBodyMorphTreeItem = NULL; + charactersTreeItem = NULL; + + // Set the dialog title + setWindowTitle(tr("Select Morphs")); + + // Setup folder + presetsFolder = QDesktopServices::storageLocation(QDesktopServices::DocumentsLocation) + QDir::separator() + "DAZ 3D"+ QDir::separator() + "Bridges" + QDir::separator() + "Daz To Unreal" + QDir::separator() + "Presets"; + + + QVBoxLayout* mainLayout = new QVBoxLayout(); + + // Left tree with morph structure + morphTreeWidget = new QTreeWidget(this); + morphTreeWidget->setHeaderHidden(true); + + // Center list showing morhps for selected tree items + morphListWidget = new QListWidget(this); + morphListWidget->setSelectionMode(QAbstractItemView::ExtendedSelection); + + // Right list showing morphs that will export + morphExportListWidget = new QListWidget(this); + morphExportListWidget->setSelectionMode(QAbstractItemView::ExtendedSelection); + + // Quick filter box + QHBoxLayout* filterLayout = new QHBoxLayout(); + filterLayout->addWidget(new QLabel("filter")); + filterEdit = new QLineEdit(); + connect(filterEdit, SIGNAL(textChanged(const QString &)), this, SLOT(FilterChanged(const QString &))); + filterLayout->addWidget(filterEdit); + + // Presets + QHBoxLayout* settingsLayout = new QHBoxLayout(); + presetCombo = new QComboBox(this); + QPushButton* savePresetButton = new QPushButton("Save Preset", this); + connect(savePresetButton, SIGNAL(released()), this, SLOT(HandleSavePreset())); + settingsLayout->addWidget(new QLabel("Choose Preset")); + settingsLayout->addWidget(presetCombo); + settingsLayout->addWidget(savePresetButton); + settingsLayout->addStretch(); + + // All Morphs + QHBoxLayout* morphsLayout = new QHBoxLayout(); + + // Left Tree + QVBoxLayout* treeLayout = new QVBoxLayout(); + treeLayout->addWidget(new QLabel("Morph Groups")); + treeLayout->addWidget(new QLabel("Select to see available morphs")); + treeLayout->addWidget(morphTreeWidget); + + // Buttons for quickly adding certain JCMs + QGroupBox* MorphGroupBox = new QGroupBox("Morph Utilities", this); + MorphGroupBox->setLayout(new QVBoxLayout()); + QGroupBox* JCMGroupBox = new QGroupBox("Add JCMs", this); + JCMGroupBox->setLayout(new QGridLayout()); + QGroupBox* FaceGroupBox = new QGroupBox("Add Expressions", this); + FaceGroupBox->setLayout(new QGridLayout()); + QPushButton* ArmsJCMButton = new QPushButton("Arms"); + QPushButton* LegsJCMButton = new QPushButton("Legs"); + QPushButton* TorsoJCMButton = new QPushButton("Torso"); + QPushButton* ARKit81Button = new QPushButton("ARKit (Genesis8.1)"); + QPushButton* FaceFX8Button = new QPushButton("FaceFX (Genesis8)"); + autoJCMCheckBox = new QCheckBox("Auto JCM"); + autoJCMCheckBox->setChecked(false); + ((QGridLayout*)JCMGroupBox->layout())->addWidget(ArmsJCMButton, 0, 0); + ((QGridLayout*)JCMGroupBox->layout())->addWidget(LegsJCMButton, 0, 1); + ((QGridLayout*)JCMGroupBox->layout())->addWidget(TorsoJCMButton, 0, 2); + ((QGridLayout*)FaceGroupBox->layout())->addWidget(ARKit81Button, 0, 1); + ((QGridLayout*)FaceGroupBox->layout())->addWidget(FaceFX8Button, 0, 2); + MorphGroupBox->layout()->addWidget(JCMGroupBox); + MorphGroupBox->layout()->addWidget(FaceGroupBox); + MorphGroupBox->layout()->addWidget(autoJCMCheckBox); + + if (!settings->value("AutoJCMEnabled").isNull()) + { + autoJCMCheckBox->setChecked(settings->value("AutoJCMEnabled").toBool()); + } + + connect(ArmsJCMButton, SIGNAL(released()), this, SLOT(HandleArmJCMMorphsButton())); + connect(LegsJCMButton, SIGNAL(released()), this, SLOT(HandleLegJCMMorphsButton())); + connect(TorsoJCMButton, SIGNAL(released()), this, SLOT(HandleTorsoJCMMorphsButton())); + connect(ARKit81Button, SIGNAL(released()), this, SLOT(HandleARKitGenesis81MorphsButton())); + connect(FaceFX8Button, SIGNAL(released()), this, SLOT(HandleFaceFXGenesis8Button())); + connect(autoJCMCheckBox, SIGNAL(clicked(bool)), this, SLOT(HandleAutoJCMCheckBoxChange(bool))); + + treeLayout->addWidget(MorphGroupBox); + morphsLayout->addLayout(treeLayout); + + + // Center List of morphs based on tree selection + QVBoxLayout* morphListLayout = new QVBoxLayout(); + morphListLayout->addWidget(new QLabel("Morphs in Group")); + morphListLayout->addWidget(new QLabel("Select and click Add for Export")); + morphListLayout->addLayout(filterLayout); + morphListLayout->addWidget(morphListWidget); + + // Button for adding morphs + QPushButton* addMorphsButton = new QPushButton("Add For Export", this); + connect(addMorphsButton, SIGNAL(released()), this, SLOT(HandleAddMorphsButton())); + morphListLayout->addWidget(addMorphsButton); + morphsLayout->addLayout(morphListLayout); + + // Right List of morphs that will export + QVBoxLayout* selectedListLayout = new QVBoxLayout(); + selectedListLayout->addWidget(new QLabel("Morphs to Export")); + selectedListLayout->addWidget(morphExportListWidget); + + // Button for clearing morphs from export + QPushButton* removeMorphsButton = new QPushButton("Remove From Export", this); + connect(removeMorphsButton, SIGNAL(released()), this, SLOT(HandleRemoveMorphsButton())); + selectedListLayout->addWidget(removeMorphsButton); + morphsLayout->addLayout(selectedListLayout); + + mainLayout->addLayout(settingsLayout); + mainLayout->addLayout(morphsLayout); + + this->addLayout(mainLayout); + resize(QSize(800, 800));//.expandedTo(minimumSizeHint())); + setFixedWidth(width()); + setFixedHeight(height()); + RefreshPresetsCombo(); + +// connect(morphListWidget, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(ItemChanged(QListWidgetItem*))); + + connect(morphTreeWidget, SIGNAL(itemSelectionChanged()), + this, SLOT(ItemSelectionChanged())); + + PrepareDialog(); +} + +QSize DzBridgeMorphSelectionDialog::minimumSizeHint() const +{ + return QSize(800, 800); +} + +// Build out the Left morphs tree based on the current selection +void DzBridgeMorphSelectionDialog::PrepareDialog() +{ + DzNode* Selection = dzScene->getPrimarySelection(); + + // For items like clothing, create the morph list from the character + DzNode* ParentFigureNode = Selection; + while (ParentFigureNode->getNodeParent()) + { + ParentFigureNode = ParentFigureNode->getNodeParent(); + if (DzSkeleton* Skeleton = ParentFigureNode->getSkeleton()) + { + if (DzFigure* Figure = qobject_cast(Skeleton)) + { + Selection = ParentFigureNode; + break; + } + } + } + + morphs.clear(); + morphList = GetAvailableMorphs(Selection); + for (int ChildIndex = 0; ChildIndex < Selection->getNumNodeChildren(); ChildIndex++) + { + DzNode* ChildNode = Selection->getNodeChild(ChildIndex); + morphList.append(GetAvailableMorphs(ChildNode)); + } + + //GetActiveJointControlledMorphs(Selection); + + UpdateMorphsTree(); + HandlePresetChanged("LastUsed.csv"); +} + +// When the filter text is changed, update the center list +void DzBridgeMorphSelectionDialog::FilterChanged(const QString& filter) +{ + morphListWidget->clear(); + QString newFilter = filter; + morphListWidget->clear(); + foreach(MorphInfo morphInfo, selectedInTree) + { + if (newFilter == NULL || newFilter.isEmpty() || morphInfo.Label.contains(newFilter, Qt::CaseInsensitive)) + { + SortingListItem* item = new SortingListItem();// modLabel, morphListWidget); + item->setText(morphInfo.Label); + item->setData(Qt::UserRole, morphInfo.Name); + + morphListWidget->addItem(item); + } + } + + morphListWidget->sortItems(); +} + +// Build a list of availaboe morphs for the node +// TODO: This function evolved a lot as I figured out where to find the morphs. +// There may be dead code in here. +QStringList DzBridgeMorphSelectionDialog::GetAvailableMorphs(DzNode* Node) +{ + QStringList newMorphList; + + DzObject* Object = Node->getObject(); + DzShape* Shape = Object ? Object->getCurrentShape() : NULL; + + for (int index = 0; index < Node->getNumProperties(); index++) + { + DzProperty* property = Node->getProperty(index); + QString propName = property->getName(); + QString propLabel = property->getLabel(); + DzPresentation* presentation = property->getPresentation(); + if (presentation) + { + MorphInfo morphInfo; + morphInfo.Name = propName; + morphInfo.Label = propLabel; + morphInfo.Path = Node->getLabel() + "/" + property->getPath(); + morphInfo.Type = presentation->getType(); + if (!morphs.contains(morphInfo.Name)) + { + morphs.insert(morphInfo.Name, morphInfo); + } + //qDebug() << "Property Name " << propName << " Label " << propLabel << " Presentation Type:" << presentation->getType() << "Path: " << property->getPath(); + //qDebug() << "Path " << property->getGroupOnlyPath(); + } + if (presentation && presentation->getType() == "Modifier/Shape") + { + SortingListItem* item = new SortingListItem();// modLabel, morphListWidget); + item->setText(propLabel); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + if (morphList.contains(propLabel)) + { + item->setCheckState(Qt::Checked); + newMorphList.append(propName); + } + else + { + item->setCheckState(Qt::Unchecked); + } + item->setData(Qt::UserRole, propName); + morphNameMapping.insert(propName, propLabel); + } + } + + if (Object) + { + for (int index = 0; index < Object->getNumModifiers(); index++) + { + DzModifier* modifier = Object->getModifier(index); + QString modName = modifier->getName(); + QString modLabel = modifier->getLabel(); + DzMorph* mod = qobject_cast(modifier); + if (mod) + { + for (int propindex = 0; propindex < modifier->getNumProperties(); propindex++) + { + DzProperty* property = modifier->getProperty(propindex); + QString propName = property->getName(); + QString propLabel = property->getLabel(); + DzPresentation* presentation = property->getPresentation(); + if (presentation) + { + MorphInfo morphInfoProp; + morphInfoProp.Name = modName; + morphInfoProp.Label = propLabel; + morphInfoProp.Path = Node->getLabel() + "/" + property->getPath(); + morphInfoProp.Type = presentation->getType(); + if (!morphs.contains(morphInfoProp.Name)) + { + morphs.insert(morphInfoProp.Name, morphInfoProp); + } + //qDebug() << "Modifier Name " << modName << " Label " << propLabel << " Presentation Type:" << presentation->getType() << " Path: " << property->getPath(); + //qDebug() << "Path " << property->getGroupOnlyPath(); + } + } + + } + + } + } + + return newMorphList; +} + +// Recursive function for finding all active JCM morphs for a node +QList DzBridgeMorphSelectionDialog::GetActiveJointControlledMorphs(DzNode* Node) +{ + QList returnMorphs; + if (autoJCMCheckBox->isChecked()) + { + if (Node == nullptr) + { + Node = dzScene->getPrimarySelection(); + + // For items like clothing, create the morph list from the character + DzNode* ParentFigureNode = Node; + while (ParentFigureNode->getNodeParent()) + { + ParentFigureNode = ParentFigureNode->getNodeParent(); + if (DzSkeleton* Skeleton = ParentFigureNode->getSkeleton()) + { + if (DzFigure* Figure = qobject_cast(Skeleton)) + { + Node = ParentFigureNode; + break; + } + } + } + } + + + DzObject* Object = Node->getObject(); + DzShape* Shape = Object ? Object->getCurrentShape() : NULL; + + for (int index = 0; index < Node->getNumProperties(); index++) + { + DzProperty* property = Node->getProperty(index); + returnMorphs.append(GetJointControlledMorphInfo(property)); + } + + if (Object) + { + for (int index = 0; index < Object->getNumModifiers(); index++) + { + DzModifier* modifier = Object->getModifier(index); + QString modName = modifier->getName(); + QString modLabel = modifier->getLabel(); + DzMorph* mod = qobject_cast(modifier); + if (mod) + { + for (int propindex = 0; propindex < modifier->getNumProperties(); propindex++) + { + DzProperty* property = modifier->getProperty(propindex); + returnMorphs.append(GetJointControlledMorphInfo(property)); + } + + } + + } + } + + } + + return returnMorphs; +} + +QList DzBridgeMorphSelectionDialog::GetJointControlledMorphInfo(DzProperty* property) +{ + QList returnMorphs; + + QString propName = property->getName(); + QString propLabel = property->getLabel(); + DzPresentation* presentation = property->getPresentation(); + if (presentation && presentation->getType() == "Modifier/Corrective") + { + QString linkLabel; + QString linkDescription; + QString linkBone; + QString linkAxis; + QString linkBodyType; + double bodyStrength = 0.0f; + double currentBodyScalar = 0.0f; + double linkScalar = 0.0f; + bool isJCM = false; + QList keys; + QList keysValues; + QList linkKeys; + + for (int ControllerIndex = 0; ControllerIndex < property->getNumControllers(); ControllerIndex++) + { + DzController* controller = property->getController(ControllerIndex); + + DzERCLink* link = qobject_cast(controller); + if (link) + { + double value = link->getScalar(); + QString linkProperty = link->getProperty()->getName(); + QString linkObject = link->getProperty()->getOwner()->getName(); + double currentValue = link->getProperty()->getDoubleValue(); + + DzBone* bone = qobject_cast(link->getProperty()->getOwner()); + if (bone) + { + linkLabel = propLabel; + linkDescription = controller->description(); + linkBone = linkObject; + linkAxis = linkProperty; + linkScalar = value; + isJCM = true; + + if (link->getType() == 6) + { + for (int keyIndex = 0; keyIndex < link->getNumKeyValues(); keyIndex++) + { + JointLinkKey newKey; + newKey.Angle = link->getKey(keyIndex); + newKey.Value = link->getKeyValue(keyIndex); + linkKeys.append(newKey); + keys.append(link->getKey(keyIndex)); + keysValues.append(link->getKeyValue(keyIndex)); + } + } + } + else + { + linkBodyType = linkObject; + bodyStrength = value; + currentBodyScalar = currentValue; + } + } + } + + if (isJCM && currentBodyScalar > 0.0f) + { + JointLinkInfo linkInfo; + linkInfo.Bone = linkBone; + linkInfo.Axis = linkAxis; + linkInfo.Morph = linkLabel; + linkInfo.Scalar = linkScalar; + linkInfo.Alpha = currentBodyScalar; + linkInfo.Keys = linkKeys; + qDebug() << "Label " << linkLabel << " Description " << linkDescription << " Bone " << linkBone << " Axis " << linkAxis << " Alpha " << currentBodyScalar << " Scalar " << linkScalar; + if (!keys.isEmpty()) + { + foreach(double key, keys) + { + qDebug() << key; + } + + foreach(double key, keysValues) + { + qDebug() << key; + } + + } + + if (morphs.contains(linkLabel) && !morphsToExport.contains(morphs[linkLabel])) + { + morphsToExport.append(morphs[linkLabel]); + } + + returnMorphs.append(linkInfo); + + } + } + return returnMorphs; +} + +// Build out the left tree +void DzBridgeMorphSelectionDialog::UpdateMorphsTree() +{ + morphTreeWidget->clear(); + morphsForNode.clear(); + foreach(QString morph, morphs.keys()) + { + QString path = morphs[morph].Path; + QTreeWidgetItem* parentItem = nullptr; + foreach(QString pathPart, path.split("/")) + { + if (pathPart == "") continue; + parentItem = FindTreeItem(parentItem, pathPart); + + if (!morphsForNode.keys().contains(parentItem)) + { + morphsForNode.insert(parentItem, QList()); + } + morphsForNode[parentItem].append(morphs[morph]); + } + } +} + +// This function could be better named. It will find the node matching the property path +// but it will also create the structure of that path in the tree as needed as it searches +QTreeWidgetItem* DzBridgeMorphSelectionDialog::FindTreeItem(QTreeWidgetItem* parent, QString name) +{ + if (parent == nullptr) + { + for(int i = 0; i < morphTreeWidget->topLevelItemCount(); i++) + { + QTreeWidgetItem* item = morphTreeWidget->topLevelItem(i); + if (item->text(0) == name) + { + return item; + } + } + + QTreeWidgetItem* newItem = new QTreeWidgetItem(morphTreeWidget); + newItem->setText(0, name); + newItem->setExpanded(true); + morphTreeWidget->addTopLevelItem(newItem); + return newItem; + } + else + { + for (int i = 0; i < parent->childCount(); i++) + { + QTreeWidgetItem* item = parent->child(i); + if (item->text(0) == name) + { + return item; + } + } + + QTreeWidgetItem* newItem = new QTreeWidgetItem(parent); + newItem->setText(0, name); + newItem->setExpanded(true); + parent->addChild(newItem); + return newItem; + } +} + +// For selection changes in the Left Tree +void DzBridgeMorphSelectionDialog::ItemSelectionChanged() +{ + selectedInTree.clear(); + foreach(QTreeWidgetItem* selectedItem, morphTreeWidget->selectedItems()) + { + SelectMorphsInNode(selectedItem); + } + + FilterChanged(filterEdit->text()); +} + +// Updates the list of selected morphs in the Left Tree +// including any children +void DzBridgeMorphSelectionDialog::SelectMorphsInNode(QTreeWidgetItem* item) +{ + if (morphsForNode.keys().contains(item)) + { + selectedInTree.append(morphsForNode[item]); + } +} + +// Add Morphs for export +void DzBridgeMorphSelectionDialog::HandleAddMorphsButton() +{ + foreach(QListWidgetItem* selectedItem, morphListWidget->selectedItems()) + { + QString morphName = selectedItem->data(Qt::UserRole).toString(); + if (morphs.contains(morphName) && !morphsToExport.contains(morphs[morphName])) + { + morphsToExport.append(morphs[morphName]); + } + } + RefreshExportMorphList(); + RefreshPresetsCombo(); +} + +// Remove morph from export list +void DzBridgeMorphSelectionDialog::HandleRemoveMorphsButton() +{ + foreach(QListWidgetItem* selectedItem, morphExportListWidget->selectedItems()) + { + QString morphName = selectedItem->data(Qt::UserRole).toString(); + if (morphs.keys().contains(morphName)) + { + morphsToExport.removeAll(morphs[morphName]); + } + } + RefreshExportMorphList(); + RefreshPresetsCombo(); +} + +// Brings up a dialgo for choosing a preset name +void DzBridgeMorphSelectionDialog::HandleSavePreset() +{ + QString filters("CSV Files (*.csv)"); + QString defaultFilter("CSV Files (*.csv)"); + QDir dir; + dir.mkpath(presetsFolder); + + QString presetName = QFileDialog::getSaveFileName(this, QString("Save Preset"), + presetsFolder, + filters, + &defaultFilter); + + if (presetName != NULL) + { + SavePresetFile(presetName); + } +} + +// Saves out a preset. If the path isn't supplied, it's saved as the last selection +void DzBridgeMorphSelectionDialog::SavePresetFile(QString filePath) +{ + QDir dir; + dir.mkpath(presetsFolder); + if (filePath == NULL) + { + filePath = presetsFolder + QDir::separator() + "LastUsed.csv"; + } + + QFile file(filePath); + file.open(QIODevice::WriteOnly | QIODevice::Text); + QTextStream out(&file); + out << GetMorphCSVString(); + + // optional, as QFile destructor will already do it: + file.close(); + RefreshPresetsCombo(); + +} + +// Hard coded list of morphs for Genesis 3 and 8 +// It just adds them all, the other functions will ignore any that don't fit the character +void DzBridgeMorphSelectionDialog::HandleArmJCMMorphsButton() +{ + QStringList MorphsToAdd; + + MorphsToAdd.append("pJCMCollarTwist_n30_L"); + MorphsToAdd.append("pJCMCollarTwist_n30_R"); + MorphsToAdd.append("pJCMCollarTwist_p30_L"); + MorphsToAdd.append("pJCMCollarTwist_p30_R"); + MorphsToAdd.append("pJCMCollarUp_55_L"); + MorphsToAdd.append("pJCMCollarUp_55_R"); + MorphsToAdd.append("pJCMCollarUp_50_L"); + MorphsToAdd.append("pJCMCollarUp_50_R"); + MorphsToAdd.append("pJCMForeArmFwd_135_L"); + MorphsToAdd.append("pJCMForeArmFwd_135_R"); + MorphsToAdd.append("pJCMForeArmFwd_75_L"); + MorphsToAdd.append("pJCMForeArmFwd_75_R"); + MorphsToAdd.append("pJCMHandDwn_70_L"); + MorphsToAdd.append("pJCMHandDwn_70_R"); + MorphsToAdd.append("pJCMHandUp_80_L"); + MorphsToAdd.append("pJCMHandUp_80_R"); + MorphsToAdd.append("pJCMShldrDown_40_L"); + MorphsToAdd.append("pJCMShldrDown_40_R"); + MorphsToAdd.append("pJCMShldrDown_75_L"); + MorphsToAdd.append("pJCMShldrDown_75_R"); + MorphsToAdd.append("pJCMShldrDown2_75_L"); + MorphsToAdd.append("pJCMShldrDown2_75_R"); + MorphsToAdd.append("pJCMShldrFront_n110_Bend_n40_L"); + MorphsToAdd.append("pJCMShldrFront_n110_Bend_p90_L"); + MorphsToAdd.append("pJCMShldrFront_p110_Bend_n90_R"); + MorphsToAdd.append("pJCMShldrFront_p110_Bend_p40_R"); + MorphsToAdd.append("pJCMShldrFwdDwn_110_75_L"); + MorphsToAdd.append("pJCMShldrFwdDwn_110_75_R"); + MorphsToAdd.append("pJCMShldrFwd_110_L"); + MorphsToAdd.append("pJCMShldrFwd_110_R"); + MorphsToAdd.append("pJCMShldrFwd_95_L"); + MorphsToAdd.append("pJCMShldrFwd_95_R"); + MorphsToAdd.append("pJCMShldrUp_90_L"); + MorphsToAdd.append("pJCMShldrUp_90_R"); + MorphsToAdd.append("pJCMShldrUp_35_L"); + MorphsToAdd.append("pJCMShldrUp_35_R"); + + // Add the list for export + foreach(QString MorphName, MorphsToAdd) + { + if (morphs.contains(MorphName) && !morphsToExport.contains(morphs[MorphName])) + { + morphsToExport.append(morphs[MorphName]); + } + } + RefreshExportMorphList(); +} + +// Hard coded list of morphs for Genesis 3 and 8 +// It just adds them all, the other functions will ignore any that don't fit the character +void DzBridgeMorphSelectionDialog::HandleLegJCMMorphsButton() +{ + QStringList MorphsToAdd; + + MorphsToAdd.append("pJCMBigToeDown_45_L"); + MorphsToAdd.append("pJCMBigToeDown_45_R"); + MorphsToAdd.append("pJCMFootDwn_75_L"); + MorphsToAdd.append("pJCMFootDwn_75_R"); + MorphsToAdd.append("pJCMFootUp_40_L"); + MorphsToAdd.append("pJCMFootUp_40_R"); + MorphsToAdd.append("pJCMShinBend_155_L"); + MorphsToAdd.append("pJCMShinBend_155_R"); + MorphsToAdd.append("pJCMShinBend_90_L"); + MorphsToAdd.append("pJCMShinBend_90_R"); + MorphsToAdd.append("pJCMThighBack_35_L"); + MorphsToAdd.append("pJCMThighBack_35_R"); + MorphsToAdd.append("pJCMThighFwd_115_L"); + MorphsToAdd.append("pJCMThighFwd_115_R"); + MorphsToAdd.append("pJCMThighFwd_57_L"); + MorphsToAdd.append("pJCMThighFwd_57_R"); + MorphsToAdd.append("pJCMThighSide_85_L"); + MorphsToAdd.append("pJCMThighSide_85_R"); + MorphsToAdd.append("pJCMToesUp_60_L"); + MorphsToAdd.append("pJCMToesUp_60_R"); + + // Add the list for export + foreach(QString MorphName, MorphsToAdd) + { + if (morphs.contains(MorphName) && !morphsToExport.contains(morphs[MorphName])) + { + morphsToExport.append(morphs[MorphName]); + } + } + RefreshExportMorphList(); +} + +// Hard coded list of morphs for Genesis 3 and 8 +// It just adds them all, the other functions will ignore any that don't fit the character +void DzBridgeMorphSelectionDialog::HandleTorsoJCMMorphsButton() +{ + QStringList MorphsToAdd; + + MorphsToAdd.append("pJCMAbdomen2Fwd_40"); + MorphsToAdd.append("pJCMAbdomen2Side_24_L"); + MorphsToAdd.append("pJCMAbdomen2Side_24_R"); + MorphsToAdd.append("pJCMAbdomenFwd_35"); + MorphsToAdd.append("pJCMAbdomenLowerFwd_Navel"); + MorphsToAdd.append("pJCMAbdomenUpperFwd_Navel"); + MorphsToAdd.append("pJCMHeadBack_27"); + MorphsToAdd.append("pJCMHeadFwd_25"); + MorphsToAdd.append("pJCMNeckBack_27"); + MorphsToAdd.append("pJCMNeckFwd_35"); + MorphsToAdd.append("pJCMNeckLowerSide_40_L"); + MorphsToAdd.append("pJCMNeckLowerSide_40_R"); + MorphsToAdd.append("pJCMNeckTwist_22_L"); + MorphsToAdd.append("pJCMNeckTwist_22_R"); + MorphsToAdd.append("pJCMNeckTwist_Reverse"); + MorphsToAdd.append("pJCMPelvisFwd_25"); + MorphsToAdd.append("pJCMChestFwd_35"); + MorphsToAdd.append("pJCMChestSide_20_L"); + MorphsToAdd.append("pJCMChestSide_20_R"); + + // Add the list for export + foreach(QString MorphName, MorphsToAdd) + { + if (morphs.contains(MorphName) && !morphsToExport.contains(morphs[MorphName])) + { + morphsToExport.append(morphs[MorphName]); + } + } + RefreshExportMorphList(); +} + +// Hard coded list of morphs for Genesis 8.1 and ARKit +// It just adds them all, the other functions will ignore any that don't fit the character +void DzBridgeMorphSelectionDialog::HandleARKitGenesis81MorphsButton() +{ + QStringList MorphsToAdd; + + MorphsToAdd.append("facs_jnt_EyeWideLeft"); + MorphsToAdd.append("facs_jnt_EyeWideRight"); + MorphsToAdd.append("facs_jnt_EyeBlinkLeft"); + MorphsToAdd.append("facs_jnt_EyeBlinkRight"); + MorphsToAdd.append("facs_bs_EyeSquintLeft_div2"); + MorphsToAdd.append("facs_bs_EyeSquintRight_div2"); + MorphsToAdd.append("facs_ctrl_EyeLookUpRight"); + MorphsToAdd.append("facs_ctrl_EyeLookUpLeft"); + MorphsToAdd.append("facs_ctrl_EyeLookOutRight"); + MorphsToAdd.append("facs_ctrl_EyeLookOutLeft"); + MorphsToAdd.append("facs_ctrl_EyeLookInRight"); + MorphsToAdd.append("facs_ctrl_EyeLookInLeft"); + MorphsToAdd.append("facs_ctrl_EyeLookDownRight"); + MorphsToAdd.append("facs_ctrl_EyeLookDownLeft"); + MorphsToAdd.append("facs_bs_NoseSneerRight_div2"); + MorphsToAdd.append("facs_bs_NoseSneerLeft_div2"); + MorphsToAdd.append("facs_jnt_JawForward"); + MorphsToAdd.append("facs_jnt_JawLeft"); + MorphsToAdd.append("facs_jnt_JawRight"); + MorphsToAdd.append("facs_jnt_JawOpen"); + MorphsToAdd.append("facs_bs_MouthClose_div2"); + MorphsToAdd.append("facs_bs_MouthFunnel_div2"); + MorphsToAdd.append("facs_bs_MouthPucker_div2"); + MorphsToAdd.append("facs_bs_MouthLeft_div2"); + MorphsToAdd.append("facs_bs_MouthRight_div2"); + MorphsToAdd.append("facs_bs_MouthSmileLeft_div2"); + MorphsToAdd.append("facs_bs_MouthSmileRight_div2"); + MorphsToAdd.append("facs_bs_MouthFrownLeft_div2"); + MorphsToAdd.append("facs_bs_MouthFrownRight_div2"); + MorphsToAdd.append("facs_bs_MouthDimpleLeft_div2"); + MorphsToAdd.append("facs_bs_MouthDimpleRight_div2"); + MorphsToAdd.append("facs_bs_MouthStretchLeft_div2"); + MorphsToAdd.append("facs_bs_MouthStretchRight_div2"); + MorphsToAdd.append("facs_bs_MouthRollLower_div2"); + MorphsToAdd.append("facs_bs_MouthRollUpper_div2"); + MorphsToAdd.append("facs_bs_MouthShrugLower_div2"); + MorphsToAdd.append("facs_bs_MouthShrugUpper_div2"); + MorphsToAdd.append("facs_bs_MouthPressLeft_div2"); + MorphsToAdd.append("facs_bs_MouthPressRight_div2"); + MorphsToAdd.append("facs_bs_MouthLowerDownLeft_div2"); + MorphsToAdd.append("facs_bs_MouthLowerDownRight_div2"); + MorphsToAdd.append("facs_bs_MouthUpperUpLeft_div2"); + MorphsToAdd.append("facs_bs_MouthUpperUpRight_div2"); + MorphsToAdd.append("facs_bs_BrowDownLeft_div2"); + MorphsToAdd.append("facs_bs_BrowDownRight_div2"); + MorphsToAdd.append("facs_ctrl_BrowInnerUp"); + MorphsToAdd.append("facs_bs_BrowOuterUpLeft_div2"); + MorphsToAdd.append("facs_bs_BrowOuterUpRight_div2"); + MorphsToAdd.append("facs_ctrl_CheekPuff"); + MorphsToAdd.append("facs_bs_CheekSquintLeft_div2"); + MorphsToAdd.append("facs_bs_CheekSquintRight_div2"); + MorphsToAdd.append("facs_bs_NoseSneerLeft_div2"); + MorphsToAdd.append("facs_bs_NoseSneerRight_div2"); + MorphsToAdd.append("facs_bs_TongueOut"); + + + // Add the list for export + foreach(QString MorphName, MorphsToAdd) + { + if (morphs.contains(MorphName) && !morphsToExport.contains(morphs[MorphName])) + { + morphsToExport.append(morphs[MorphName]); + } + } + RefreshExportMorphList(); +} + +void DzBridgeMorphSelectionDialog::HandleFaceFXGenesis8Button() +{ + QStringList MorphsToAdd; + + MorphsToAdd.append("eCTRLvSH"); + MorphsToAdd.append("eCTRLvW"); + MorphsToAdd.append("eCTRLvM"); + MorphsToAdd.append("eCTRLvF"); + MorphsToAdd.append("eCTRLMouthOpen"); + MorphsToAdd.append("eCTRLMouthWide-Narrow"); + MorphsToAdd.append("eCTRLTongueIn-Out"); + MorphsToAdd.append("eCTRLTongueUp-Down"); + + // Add the list for export + foreach(QString MorphName, MorphsToAdd) + { + if (morphs.contains(MorphName) && !morphsToExport.contains(morphs[MorphName])) + { + morphsToExport.append(morphs[MorphName]); + } + } + RefreshExportMorphList(); +} + +void DzBridgeMorphSelectionDialog::HandleAutoJCMCheckBoxChange(bool checked) +{ + settings->setValue("AutoJCMEnabled", checked); +} + +// Refresh the Right export list +void DzBridgeMorphSelectionDialog::RefreshExportMorphList() +{ + morphExportListWidget->clear(); + foreach(MorphInfo morphInfo, morphsToExport) + { + SortingListItem* item = new SortingListItem(); + item->setText(morphInfo.Label); + item->setData(Qt::UserRole, morphInfo.Name); + + morphExportListWidget->addItem(item); + } + SavePresetFile(NULL); +} + +// Refresh the list of preset csvs from the files in the folder +void DzBridgeMorphSelectionDialog::RefreshPresetsCombo() +{ + disconnect(presetCombo, SIGNAL(currentIndexChanged(const QString &)), this, SLOT(HandlePresetChanged(const QString &))); + + presetCombo->clear(); + presetCombo->addItem("None"); + + QDirIterator it(presetsFolder, QStringList() << "*.csv", QDir::NoFilter, QDirIterator::NoIteratorFlags); + while (it.hasNext()) + { + QString Path = it.next(); + QString NewPath = Path.right(Path.length() - presetsFolder.length() - 1); + presetCombo->addItem(NewPath); + } + connect(presetCombo, SIGNAL(currentIndexChanged(const QString &)), this, SLOT(HandlePresetChanged(const QString &))); +} + +// Call when the preset combo is changed by the user +void DzBridgeMorphSelectionDialog::HandlePresetChanged(const QString& presetName) +{ + morphsToExport.clear(); + QString PresetFilePath = presetsFolder + QDir::separator() + presetName; + + QFile file(PresetFilePath); + if (!file.open(QIODevice::ReadOnly)) { + // TODO: should be an error dialog + return; + } + + // load the selected csv from disk into the export list on the right + QTextStream InStream(&file); + + while (!InStream.atEnd()) { + QString MorphLine = InStream.readLine(); + if (MorphLine.endsWith("\"Export\"")) + { + QStringList Items = MorphLine.split(","); + QString MorphName = Items[0].replace("\"", ""); + if (morphs.contains(MorphName)) + { + morphsToExport.append(morphs[MorphName]); + } + } + } + + RefreshExportMorphList(); + GetActiveJointControlledMorphs(); + file.close(); +} + +// Get the morph string in the format for the Daz FBX Export +QString DzBridgeMorphSelectionDialog::GetMorphString() +{ + GetActiveJointControlledMorphs(); + + if (morphsToExport.length() == 0) + { + return ""; + } + QStringList morphNamesToExport; + foreach(MorphInfo exportMorph, morphsToExport) + { + morphNamesToExport.append(exportMorph.Name); + } + QString morphString = morphNamesToExport.join("\n1\n"); + morphString += "\n1\n.CTRLVS\n2\nAnything\n0"; + return morphString; +} + +// Get the morph string in the format used for presets +QString DzBridgeMorphSelectionDialog::GetMorphCSVString() +{ + morphList.clear(); + QString morphString; + foreach(MorphInfo exportMorph, morphsToExport) + { + morphList.append(exportMorph.Name); + morphString += "\"" + exportMorph.Name + "\",\"Export\"\n"; + } + morphString += "\".CTRLVS\", \"Ignore\"\n"; + morphString += "\"Anything\", \"Bake\"\n"; + return morphString; +} + +// Get the morph string in an internal name = friendly name format +// Used to rename them to the friendly name in Unreal +QMap DzBridgeMorphSelectionDialog::GetMorphRenaming() +{ + morphNameMapping.clear(); + foreach(MorphInfo exportMorph, morphsToExport) + { + morphNameMapping.insert(exportMorph.Name, exportMorph.Label); + } + + return morphNameMapping; +} + +QString DzBridgeMorphSelectionDialog::GetMorphLabelFromName(QString morphName) +{ + if (morphs.isEmpty()) return QString(); + + if (morphs.contains(morphName)) + { + MorphInfo morph = morphs[morphName]; + return morph.Label; + } + else + { + return QString(); + } + +} + +#include "moc_DzBridgeMorphSelectionDialog.cpp" diff --git a/src/DzBridgeMorphSelectionDialog_Scriptable.cpp b/src/DzBridgeMorphSelectionDialog_Scriptable.cpp new file mode 100644 index 0000000..027c2c2 --- /dev/null +++ b/src/DzBridgeMorphSelectionDialog_Scriptable.cpp @@ -0,0 +1,9 @@ + +#include "DzBridgeMorphSelectionDialog_Scriptable.h" + +/***************************** +Local definitions +*****************************/ +#define DAZ_BRIDGE_LIBRARY_NAME "Daz Bridge" + +#include "moc_DzBridgeMorphSelectionDialog_Scriptable.cpp" diff --git a/src/DzBridgeMorphSelectionDialog_Scriptable.h b/src/DzBridgeMorphSelectionDialog_Scriptable.h new file mode 100644 index 0000000..a7e0a4f --- /dev/null +++ b/src/DzBridgeMorphSelectionDialog_Scriptable.h @@ -0,0 +1,12 @@ +#pragma once + +#include "DzBridgeMorphSelectionDialog.h" +#include "dzbridge.h" + +class CPP_Export DzBridgeMorphSelectionDialog : public DzBridgeNameSpace::DzBridgeMorphSelectionDialog { + Q_OBJECT +public: + DzBridgeMorphSelectionDialog(QWidget* parent = nullptr) : + DzBridgeNameSpace::DzBridgeMorphSelectionDialog(parent) {}; + +}; diff --git a/src/DzBridgeSubdivisionDialog.cpp b/src/DzBridgeSubdivisionDialog.cpp new file mode 100644 index 0000000..de67eb0 --- /dev/null +++ b/src/DzBridgeSubdivisionDialog.cpp @@ -0,0 +1,359 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "QtCore/qfile.h" +#include "QtCore/qtextstream.h" + +#include "dzapp.h" +#include "dzscene.h" +#include "dzstyle.h" +#include "dzmainwindow.h" +#include "dzactionmgr.h" +#include "dzaction.h" +#include "dzskeleton.h" +#include "dzobject.h" +#include "dzshape.h" +#include "dzmodifier.h" +#include "dzpresentation.h" +#include "dzassetmgr.h" +#include "dzproperty.h" +#include "dznumericnodeproperty.h" +#include "dzsettings.h" +#include "dzmorph.h" +#include "dzgeometry.h" + +#include "DzBridgeSubdivisionDialog.h" + +#include "QtGui/qlayout.h" +#include "QtGui/qlineedit.h" + +#include + +/***************************** +Local definitions +*****************************/ +#define DAZ_BRIDGE_LIBRARY_NAME "Daz Bridge" + +using namespace DzBridgeNameSpace; + +CPP_Export DzBridgeSubdivisionDialog* DzBridgeSubdivisionDialog::singleton = nullptr; + +DzBridgeSubdivisionDialog::DzBridgeSubdivisionDialog(QWidget *parent) : + DzBasicDialog(parent, DAZ_BRIDGE_LIBRARY_NAME) +{ + subdivisionItemsGrid = NULL; + //settings = new QSettings("Code Wizards", "DazToUnreal"); + + + + // Set the dialog title + setWindowTitle(tr("Choose Subdivision Levels")); + + // Setup folder + presetsFolder = QDesktopServices::storageLocation(QDesktopServices::DocumentsLocation) + QDir::separator() + "DazToUnreal" + QDir::separator() + "Presets"; + + + QVBoxLayout* mainLayout = new QVBoxLayout(); + mainLayout->addWidget(new QLabel("Subdivision can greatly increase transfer time.")); + + subdivisionItemsGrid = new QGridLayout(); + subdivisionItemsGrid->addWidget(new QLabel("Object Name"), 0, 0); + subdivisionItemsGrid->addWidget(new QLabel("Subdivision Level"), 0, 1); + subdivisionItemsGrid->addWidget(new QLabel("Base Vert Count"), 0, 2); + mainLayout->addLayout(subdivisionItemsGrid); + mainLayout->addStretch(); + + this->addLayout(mainLayout); + resize(QSize(800, 800));//.expandedTo(minimumSizeHint())); + setFixedWidth(width()); + setFixedHeight(height()); + + SubdivisionCombos.clear(); + + PrepareDialog(); +} + +QSize DzBridgeSubdivisionDialog::minimumSizeHint() const +{ + return QSize(800, 800); +} + + +void DzBridgeSubdivisionDialog::PrepareDialog() +{ + /*foreach(QObject* object, this->children()) + { + delete object; + }*/ + + /*if (this->layout()) + { + delete this->layout(); + } + + + QVBoxLayout* mainLayout = new QVBoxLayout(this); + mainLayout->addWidget(new QLabel("Subdivision can greatly increase transfer time."));*/ + + int itemCount = subdivisionItemsGrid->count(); + while(QLayoutItem* item = subdivisionItemsGrid->takeAt(0)) + { + if (QWidget* widget = item->widget()) + { + delete widget; + //delete item; + } + } + + //subdivisionItemsGrid = new QGridLayout(this); + subdivisionItemsGrid->addWidget(new QLabel("Object Name"), 0, 0); + subdivisionItemsGrid->addWidget(new QLabel("Subdivision Level"), 0, 1); + subdivisionItemsGrid->addWidget(new QLabel("Base Vert Count"), 0, 2); + //mainLayout->addLayout(subdivisionItemsGrid); + //mainLayout->addStretch(); + + //this->addLayout(mainLayout); + //resize(QSize(800, 800));//.expandedTo(minimumSizeHint())); + //setFixedWidth(width()); + //setFixedHeight(height()); + + SubdivisionCombos.clear(); + DzNode* Selection = dzScene->getPrimarySelection(); + CreateList(Selection); +} + +void DzBridgeSubdivisionDialog::CreateList(DzNode* Node) +{ + DzObject* Object = Node->getObject(); + if (Object) + { + DzShape* Shape = Object ? Object->getCurrentShape() : NULL; + DzGeometry* Geo = Shape ? Shape->getGeometry() : NULL; + + int row = subdivisionItemsGrid->rowCount(); + subdivisionItemsGrid->addWidget(new QLabel(Node->getLabel()), row, 0); + QComboBox* subdivisionLevelCombo = new QComboBox(this); + subdivisionLevelCombo->setProperty("Object", QVariant(Node->getName())); + subdivisionLevelCombo->addItem("0"); + subdivisionLevelCombo->addItem("1"); + subdivisionLevelCombo->addItem("2"); + subdivisionLevelCombo->addItem("3"); + subdivisionLevelCombo->addItem("4"); + SubdivisionCombos.append(subdivisionLevelCombo); + subdivisionItemsGrid->addWidget(subdivisionLevelCombo, row, 1); + if (SubdivisionLevels.contains(Node->getName())) + { + subdivisionLevelCombo->setCurrentIndex(SubdivisionLevels[Node->getName()]); + } + connect(subdivisionLevelCombo, SIGNAL(currentIndexChanged(const QString &)), this, SLOT(HandleSubdivisionLevelChanged(const QString &))); + + if (Geo) + { + int VertCount = Geo->getNumVertices(); + subdivisionItemsGrid->addWidget(new QLabel(QString::number(VertCount)), row, 2); + + /*for (int index = 0; index < Shape->getNumProperties(); index++) + { + DzProperty* property = Shape->getProperty(index); + QString propName = property->getName();//property->getName(); + QString propLabel = property->getLabel(); + qDebug() << propName << " " << propLabel; + }*/ + } + } + + for (int ChildIndex = 0; ChildIndex < Node->getNumNodeChildren(); ChildIndex++) + { + DzNode* ChildNode = Node->getNodeChild(ChildIndex); + CreateList(ChildNode); + } +} + +void DzBridgeSubdivisionDialog::HandleSubdivisionLevelChanged(const QString& text) +{ + foreach(QComboBox* combo, SubdivisionCombos) + { + QString name = combo->property("Object").toString(); + int targetValue = combo->currentText().toInt(); + SubdivisionLevels[name] = targetValue; + } +} + +DzNode* DzBridgeSubdivisionDialog::FindObject(DzNode* Node, QString Name) +{ + if (Node == nullptr) + return nullptr; + + DzObject* Object = Node->getObject(); + if (Object) + { + if (Node->getName() == Name) return Node; + } + + for (int ChildIndex = 0; ChildIndex < Node->getNumNodeChildren(); ChildIndex++) + { + DzNode* ChildNode = Node->getNodeChild(ChildIndex); + DzNode* FoundNode = FindObject(ChildNode, Name); + if (FoundNode) return FoundNode; + } + return NULL; +} + +bool DzBridgeSubdivisionDialog::setSubdivisionLevelByNode(DzNode* Node, int level) +{ + if (Node == nullptr) + return nullptr; + + DzNode* selection = dzScene->getPrimarySelection(); + QString searchName = Node->getName(); + foreach(QComboBox * combo, SubdivisionCombos) + { + QString name = combo->property("Object").toString(); + if (name == searchName) + { + int maxLevel = combo->count() - 1; + if (level > maxLevel) + return false; + + combo->setCurrentIndex(level); + return true; + } + } + + return false; +} + +void DzBridgeSubdivisionDialog::LockSubdivisionProperties(bool subdivisionEnabled) +{ + DzNode* Selection = dzScene->getPrimarySelection(); + foreach(QComboBox* combo, SubdivisionCombos) + { + QString Name = combo->property("Object").toString(); + DzNode* ObjectNode = FindObject(Selection, Name); + if (ObjectNode) + { + DzObject* Object = ObjectNode->getObject(); + DzShape* Shape = Object ? Object->getCurrentShape() : NULL; + DzGeometry* Geo = Shape ? Shape->getGeometry() : NULL; + if (Geo) + { + int VertCount = Geo->getNumVertices(); + + for (int index = 0; index < Shape->getNumProperties(); index++) + { + DzProperty* property = Shape->getProperty(index); + DzNumericProperty* numericProperty = qobject_cast(property); + QString propName = property->getName(); + if (propName == "SubDIALevel" && numericProperty) + { + // DB 2021-09-02: Record data to Unlock/Undo changes + UndoData undo_data; + undo_data.originalLockState = numericProperty->isLocked(); + undo_data.originalValue = numericProperty->getDoubleValue(); + UndoSubdivisionOverrides.insert(numericProperty, undo_data); + + numericProperty->lock(false); + if (subdivisionEnabled) + { + double targetValue = combo->currentText().toDouble(); + numericProperty->setDoubleValue(targetValue); + } + else + { + numericProperty->setDoubleValue(0.0f); + } + numericProperty->lock(true); + } + //QString propLabel = property->getLabel(); + //qDebug() << propName << " " << propLabel; + } + } + } + } +} + +// DB 2021-09-02: Unlock/Undo Subdivision Property Changes +void DzBridgeSubdivisionDialog::UnlockSubdivisionProperties() +{ + QMap::iterator undoIterator = UndoSubdivisionOverrides.begin(); + while (undoIterator != UndoSubdivisionOverrides.end()) + { + DzProperty* undoKey = undoIterator.key(); + DzNumericProperty* numericProperty = qobject_cast(undoKey); + if (numericProperty) + { + UndoData undo_data = undoIterator.value(); + numericProperty->lock(false); + numericProperty->setDoubleValue(undo_data.originalValue); + numericProperty->lock(undo_data.originalLockState); + } + undoIterator++; + } + + // Clear subdivision map after processing undo + UndoSubdivisionOverrides.clear(); +} + +// DEPRECATED: use DzBridgeAction::writeAllSubdivisions(DzJsonWriter& writer) +void DzBridgeSubdivisionDialog::WriteSubdivisions(DzJsonWriter& Writer) +{ + DzNode* Selection = dzScene->getPrimarySelection(); + + //stream << "Version, Object, Subdivision" << endl; + foreach(QComboBox* combo, SubdivisionCombos) + { + QString Name = combo->property("Object").toString() + ".Shape"; + //DzNode* ObjectNode = FindObject(Selection, Name); + + int targetValue = combo->currentText().toInt(); + Writer.startObject(true); + Writer.addMember("Version", 1); + Writer.addMember("Asset Name", Name); + Writer.addMember("Value", targetValue); + Writer.finishObject(); + //stream << "1, " << Name << ", " << targetValue << endl; + } +} + +QObjectList DzBridgeSubdivisionDialog::getSubdivisionCombos() +{ + QObjectList *returnList = new QObjectList(); + foreach(QComboBox * combo, SubdivisionCombos) + { + returnList->append(qobject_cast(combo)); + } + return *returnList; +} + +std::map* DzBridgeSubdivisionDialog::GetLookupTable() +{ + std::map* pLookupTable = new std::map(); + + foreach(QComboBox * combo, SubdivisionCombos) + { + std::string name(combo->property("Object").toString().toLocal8Bit().data()); + name = name + ".Shape"; + int targetValue = combo->currentText().toInt(); + (*pLookupTable)[name] = targetValue; + + } + + return pLookupTable; +} + +#include "moc_DzBridgeSubdivisionDialog.cpp" diff --git a/src/DzBridgeSubdivisionDialog_Scriptable.cpp b/src/DzBridgeSubdivisionDialog_Scriptable.cpp new file mode 100644 index 0000000..45af01c --- /dev/null +++ b/src/DzBridgeSubdivisionDialog_Scriptable.cpp @@ -0,0 +1,9 @@ + +#include "DzBridgeSubdivisionDialog_Scriptable.h" + +/***************************** +Local definitions +*****************************/ +#define DAZ_BRIDGE_LIBRARY_NAME "Daz Bridge" + +#include "moc_DzBridgeSubdivisionDialog_Scriptable.cpp" diff --git a/src/DzBridgeSubdivisionDialog_Scriptable.h b/src/DzBridgeSubdivisionDialog_Scriptable.h new file mode 100644 index 0000000..a0581c1 --- /dev/null +++ b/src/DzBridgeSubdivisionDialog_Scriptable.h @@ -0,0 +1,11 @@ +#pragma once + +#include "DzBridgeSubdivisionDialog.h" +#include "dzbridge.h" + +class CPP_Export DzBridgeSubdivisionDialog : public DzBridgeNameSpace::DzBridgeSubdivisionDialog { + Q_OBJECT +public: + DzBridgeSubdivisionDialog(QWidget* parent = nullptr) : + DzBridgeNameSpace::DzBridgeSubdivisionDialog(parent) {}; +}; diff --git a/src/OpenFBXInterface.cpp b/src/OpenFBXInterface.cpp new file mode 100644 index 0000000..fdb185e --- /dev/null +++ b/src/OpenFBXInterface.cpp @@ -0,0 +1,141 @@ + +/**************************************************************************************** + Portions of this file is based on source code from Autodesk, + and is used under license below: + + Copyright (C) 2015 Autodesk, Inc. + All rights reserved. + Use of this software is subject to the terms of the Autodesk license agreement + provided at the time of installation or download, or which otherwise accompanies + this software in either electronic or hard copy form. +****************************************************************************************/ + +#include + +#ifdef __APPLE__ +#define USING_LIBSTDCPP 1 +#endif +#include + +#include "OpenFBXInterface.h" + +OpenFBXInterface* OpenFBXInterface::singleton = nullptr; + +// Constructor +OpenFBXInterface::OpenFBXInterface() +{ + // Create FbxManager + m_fbxManager = FbxManager::Create(); + if (!m_fbxManager) + { + throw (std::runtime_error("OpenFBXInterface: could not create FbxManager")); + } + + // Create FbxIOSettings + m_fbxIOSettings = FbxIOSettings::Create(m_fbxManager, IOSROOT); + m_fbxManager->SetIOSettings(m_fbxIOSettings); + + // Initialize Fbx Plugin folder + FbxString appPath = FbxGetApplicationDirectory(); + m_fbxManager->LoadPluginsDirectory(appPath.Buffer()); + + m_ErrorCode = 0; + m_ErrorString = ""; + m_DefaultScene = CreateScene("DefaultScene"); + +} + +// Destructor +OpenFBXInterface::~OpenFBXInterface() +{ + if (m_fbxManager) m_fbxManager->Destroy(); + if (m_fbxIOSettings) m_fbxIOSettings->Destroy(); + if (m_DefaultScene) m_DefaultScene->Destroy(); +} + +bool OpenFBXInterface::SaveScene(FbxScene* pScene, QString sFilename, int nFileFormat, bool bEmbedMedia) +{ + bool bStatus = true; + + // Create FbxExporter + FbxExporter* pExporter = FbxExporter::Create(m_fbxManager, ""); + + // Check if fileformat is invalid + if (nFileFormat < 0 || nFileFormat >= m_fbxManager->GetIOPluginRegistry()->GetWriterFormatCount()) + { + // replace with valid format + nFileFormat = m_fbxManager->GetIOPluginRegistry()->GetNativeWriterFormat(); + } + + m_fbxIOSettings->SetBoolProp(EXP_FBX_MATERIAL, true); + m_fbxIOSettings->SetBoolProp(EXP_FBX_TEXTURE, true); + m_fbxIOSettings->SetBoolProp(EXP_FBX_EMBEDDED, bEmbedMedia); + m_fbxIOSettings->SetBoolProp(EXP_FBX_SHAPE, true); + m_fbxIOSettings->SetBoolProp(EXP_FBX_GOBO, true); + m_fbxIOSettings->SetBoolProp(EXP_FBX_ANIMATION, true); + m_fbxIOSettings->SetBoolProp(EXP_FBX_GLOBAL_SETTINGS, true); + + if (pExporter->Initialize(sFilename.toLocal8Bit().data(), nFileFormat, m_fbxIOSettings) == false) + { + m_ErrorString = QString(pExporter->GetStatus().GetErrorString()); + m_ErrorCode = pExporter->GetStatus().GetCode(); + pExporter->Destroy(); + return false; + } + + bStatus = pExporter->Export(pScene); + if (!bStatus) + { + m_ErrorString = QString(pExporter->GetStatus().GetErrorString()); + m_ErrorCode = pExporter->GetStatus().GetCode(); + } + + pExporter->Destroy(); + + return bStatus; + +} + +bool OpenFBXInterface::LoadScene(FbxScene* pScene, QString sFilename) +{ + bool bStatus = true; + + FbxImporter* pImporter = FbxImporter::Create(m_fbxManager, ""); + + if (pImporter->Initialize(sFilename.toLocal8Bit().data(), -1, m_fbxIOSettings) == false) + { + m_ErrorString = QString(pImporter->GetStatus().GetErrorString()); + m_ErrorCode = pImporter->GetStatus().GetCode(); + pImporter->Destroy(); + return false; + } + + if (pImporter->IsFBX() == false) + { + m_ErrorCode = -1; + m_ErrorString = QString("OpenFBXInterface: loaded scene file has unrecognized FBX file format."); + pImporter->Destroy(); + return false; + } + + bStatus = pImporter->Import(pScene); + if (!bStatus) + { + m_ErrorString = QString(pImporter->GetStatus().GetErrorString()); + m_ErrorCode = pImporter->GetStatus().GetCode(); + } + + pImporter->Destroy(); + return bStatus; + +} + + +FbxScene* OpenFBXInterface::CreateScene(QString sSceneName) +{ + FbxScene* pNewScene = FbxScene::Create(m_fbxManager, sSceneName.toLocal8Bit().data()); + + return pNewScene; +} + +#include "moc_OpenFBXInterface.cpp" diff --git a/src/OpenSubdivInterface.cpp b/src/OpenSubdivInterface.cpp new file mode 100644 index 0000000..32e785b --- /dev/null +++ b/src/OpenSubdivInterface.cpp @@ -0,0 +1,401 @@ +/**************************************************************************************** + Portions of this file are based on source code from + https://github.com/cocktailboy/daz-to-ue4-subd-skin + and is used under license below: + +MIT License + +Copyright(c) 2021 cocktailboy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this softwareand associated documentation files(the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and /or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions : + +The above copyright noticeand this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +****************************************************************************************/ + + +#define _USE_MATH_DEFINES +#include + +#include +#include + +#include +#include + +#include "OpenFBXInterface.h" +#include "OpenSubdivInterface.h" + + +bool SubdivideFbxScene::ProcessScene() +{ + FbxNode* pNode = m_Scene->GetRootNode(); + if (pNode) + { + for (int i = 0; i < pNode->GetChildCount(); i++) + { + if (ProcessNode(pNode->GetChild(i)) == false) + { + return false; + } + } + } + else + { + return false; + } + + return true; +} + +bool SubdivideFbxScene::ProcessNode(FbxNode* pNode) +{ + FbxNodeAttribute::EType nAttributeType; + + if (pNode->GetNodeAttribute() != NULL) + { + nAttributeType = (pNode->GetNodeAttribute()->GetAttributeType()); + + switch (nAttributeType) + { + case FbxNodeAttribute::eMesh: + { + std::string name = pNode->GetName(); + int nodeSubDLevel = -1; + try + { + nodeSubDLevel = m_SubDLevel_NameLookup->at(name); + } + catch (std::out_of_range) + { + nodeSubDLevel = -1; + } + if (nodeSubDLevel > 0) + { + m_FbxMesh_NameLookup[name] = SubdivideMesh(pNode, (FbxMesh*)pNode->GetNodeAttribute(), nodeSubDLevel); + } + break; + } + + case FbxNodeAttribute::eMarker: + break; + case FbxNodeAttribute::eSkeleton: + break; + case FbxNodeAttribute::eNurbs: + break; + case FbxNodeAttribute::ePatch: + break; + case FbxNodeAttribute::eCamera: + break; + case FbxNodeAttribute::eLight: + break; + case FbxNodeAttribute::eLODGroup: + break; + default: + break; + } + } + + for (int i = 0; i < pNode->GetChildCount(); i++) + { + ProcessNode(pNode->GetChild(i)); + } + + return true; +} + +FbxMesh* SubdivideFbxScene::SubdivideMesh(FbxNode* pNode, FbxMesh* pMesh, int subdLevel) +{ + // Get topology from FbxMesh + int numVertices = pMesh->GetControlPointsCount(); + int numFaces = pMesh->GetPolygonCount(); + std::vector numVertsPerFace(numFaces); + std::vector vertIndicesPerFace; + for (int i = 0; i < numFaces; i++) + { + numVertsPerFace[i] = pMesh->GetPolygonSize(i); + for (int j = 0; j < numVertsPerFace[i]; j++) + { + int nControlPointIndex = pMesh->GetPolygonVertex(i,j); + if (nControlPointIndex < 0) + { + // skip invalid index + continue; + } + vertIndicesPerFace.push_back(nControlPointIndex); + } + } + + // OpenSubdiv topology descriptor + typedef OpenSubdiv::Far::TopologyDescriptor Descriptor; + OpenSubdiv::Sdc::SchemeType type = OpenSubdiv::Sdc::SCHEME_CATMARK; + OpenSubdiv::Sdc::Options options; + options.SetVtxBoundaryInterpolation(OpenSubdiv::Sdc::Options::VTX_BOUNDARY_EDGE_ONLY); + + Descriptor desc; + desc.numVertices = numVertices; + desc.numFaces = numFaces; + desc.numVertsPerFace = numVertsPerFace.data(); + desc.vertIndicesPerFace = vertIndicesPerFace.data(); + + // Make topology refiner + OpenSubdiv::Far::TopologyRefiner* refiner = OpenSubdiv::Far::TopologyRefinerFactory::Create( + desc, + OpenSubdiv::Far::TopologyRefinerFactory::Options(type, options) + ); + + // Refine topology + refiner->RefineUniform(OpenSubdiv::Far::TopologyRefiner::UniformOptions(subdLevel)); + + // calculate number of additional vertices + int nCoarseVerts = numVertices; + int nFineVerts = refiner->GetLevel(subdLevel).GetNumVertices(); + int nTotalVerts = refiner->GetNumVerticesTotal(); + int nTempVerts = nTotalVerts - nCoarseVerts - nFineVerts; + + // allocate storage for additional vertices + std::vector tempPosBuffer(nTempVerts); + std::vector finePosBuffer(nFineVerts); + + // get vertex data from source mesh + std::vector coarsePosBuffer(nCoarseVerts); + FbxVector4* nControlPoints = pMesh->GetControlPoints(); + for (int i = 0; i < nCoarseVerts; i++) + { + coarsePosBuffer[i].SetVector(nControlPoints[i]); + } + + VertexPosition *srcPos = &coarsePosBuffer[0]; + VertexPosition *dstPos = &tempPosBuffer[0]; + + // Make Primvar::refiner from topology refiner + OpenSubdiv::Far::PrimvarRefiner primvarRefiner(*refiner); + + // Do intermediate interpolation steps + for (int level = 1; level < subdLevel; ++level) + { + primvarRefiner.Interpolate(level, srcPos, dstPos); + srcPos = dstPos, dstPos += refiner->GetLevel(level).GetNumVertices(); + } + + // Do last Interpolation into buffer for final data + primvarRefiner.Interpolate(subdLevel, srcPos, finePosBuffer); + + // Create FBX mesh to store subdivided data + FbxString meshName = FbxString(pNode->GetName()) + FbxString("_subd"); + FbxMesh* mesh = FbxMesh::Create(m_Scene, meshName); + OpenSubdiv::Far::TopologyLevel topo = refiner->GetLevel(subdLevel); + mesh->InitControlPoints(nFineVerts); + FbxVector4* pControlPoints = mesh->GetControlPoints(); + for (int vert_index = 0; vert_index < nFineVerts; ++vert_index) + { + pControlPoints[vert_index] = finePosBuffer[vert_index].GetVector(); + } + int numSubDFaces = topo.GetNumFaces(); + for (int i = 0; i < numSubDFaces; i++) + { + OpenSubdiv::Far::ConstIndexArray faceVertices = topo.GetFaceVertices(i); + mesh->BeginPolygon(-1, -1, false); + for (int j = 0; j < faceVertices.size(); j++) + { + mesh->AddPolygon(faceVertices[j]); + } + mesh->EndPolygon(); + } + + // Get cluster and skin weight data from FBX geometry links and interplate them + FbxGeometry* pGeometry = pMesh; + int lSkinCount = pGeometry->GetDeformerCount(FbxDeformer::eSkin); + for (int i = 0; i != lSkinCount; ++i) + { + FbxSkin* skin = FbxSkin::Create(m_Scene, ""); + int lClusterCount = ((FbxSkin*)pGeometry->GetDeformer(i, FbxDeformer::eSkin))->GetClusterCount(); + for (int j = 0; j != lClusterCount; ++j) + { + // get cluster data from FBX + FbxCluster* lCluster = ((FbxSkin*)pGeometry->GetDeformer(i, FbxDeformer::eSkin))->GetCluster(j); + const char* lClusterModes[] = { "Normalize", "Additive", "Total1" }; + + int lIndexCount = lCluster->GetControlPointIndicesCount(); + int* lIndices = lCluster->GetControlPointIndices(); + double* lWeights = lCluster->GetControlPointWeights(); + + // populate coarse skin weight buffer + std::vector coarseSkinWeightBuffer(nCoarseVerts); + for (int k = 0; k < lIndexCount; k++) + coarseSkinWeightBuffer[lIndices[k]].SetWeight((float)lWeights[k]); + + // Allocate intermediate and final storage to be populated: + std::vector tempSkinWeightBuffer(nTempVerts); + std::vector fineSkinWeightBuffer(nFineVerts); + + // interpolate skin weights + SkinWeight* src = &coarseSkinWeightBuffer[0]; + SkinWeight* dst = &tempSkinWeightBuffer[0]; + for (int level = 1; level < subdLevel; ++level) + { + primvarRefiner.Interpolate(level, src, dst); + src = dst, dst += refiner->GetLevel(level).GetNumVertices(); + } + + // Interpolate the last level into the separate buffers for our final data: + primvarRefiner.Interpolate(subdLevel, src, fineSkinWeightBuffer); + + // save cluster data + FbxCluster* cluster = FbxCluster::Create(m_Scene, lCluster->GetName()); + cluster->SetLink(lCluster->GetLink()); + cluster->SetLinkMode(lCluster->GetLinkMode()); + for (int k = 0; k < nFineVerts; k++) + { + float weight = fineSkinWeightBuffer[k].GetWeight(); + if (weight > 0.0f) + cluster->AddControlPointIndex(k, weight); + } + + // copy matrix + FbxAMatrix lMatrix; + cluster->SetTransformMatrix(lCluster->GetTransformMatrix(lMatrix)); + cluster->SetTransformLinkMatrix(lCluster->GetTransformLinkMatrix(lMatrix)); + if (lCluster->GetAssociateModel() != NULL) + cluster->SetTransformAssociateModelMatrix(lCluster->GetTransformAssociateModelMatrix(lMatrix)); + skin->AddCluster(cluster); + + } // for each cluster + + mesh->AddDeformer(skin); + } // for each skin + + return mesh; +} + +bool SubdivideFbxScene::SaveClustersToScene(FbxScene* pDestScene) +{ + FbxNode* pNode = pDestScene->GetRootNode(); + if (pNode) + { + for (int i = 0; i < pNode->GetChildCount(); i++) + { + if (SaveClustersToNode(pDestScene, pNode->GetChild(i)) == false) + return false; + } + } + + return true; +} + +bool SubdivideFbxScene::SaveClustersToNode(FbxScene* pDestScene, FbxNode* pNode) +{ + FbxNodeAttribute::EType nAttributeType; + + if (pNode->GetNodeAttribute() != NULL) + { + nAttributeType = (pNode->GetNodeAttribute()->GetAttributeType()); + + switch (nAttributeType) + { + default: + break; + case FbxNodeAttribute::eMarker: + break; + + case FbxNodeAttribute::eSkeleton: + break; + + case FbxNodeAttribute::eMesh: + SaveClustersToMesh(pDestScene, pNode, (FbxMesh*)pNode->GetNodeAttribute()); + break; + + case FbxNodeAttribute::eNurbs: + break; + + case FbxNodeAttribute::ePatch: + break; + + case FbxNodeAttribute::eCamera: + break; + + case FbxNodeAttribute::eLight: + break; + + case FbxNodeAttribute::eLODGroup: + break; + } + } + + for (int i = 0; i < pNode->GetChildCount(); i++) + { + SaveClustersToNode(pDestScene, pNode->GetChild(i)); + } + + return true; +} + +FbxMesh* SubdivideFbxScene::SaveClustersToMesh(FbxScene* pDestScene, FbxNode* pNode, FbxMesh* pMesh) +{ + std::string name = pNode->GetName(); + auto it = m_FbxMesh_NameLookup.find(name); + if (it == m_FbxMesh_NameLookup.end()) + return nullptr; + + FbxMesh* subdMesh = it->second; + + // Get cluster and skin weight data from subd geometry links and save them + FbxGeometry* geometry = pMesh; + FbxGeometry* subdGeometry = subdMesh; + int skinCount = geometry->GetDeformerCount(FbxDeformer::eSkin); + for (int i = 0; i != skinCount; ++i) + { + FbxSkin* skin = (FbxSkin*)geometry->GetDeformer(i, FbxDeformer::eSkin); + FbxSkin* subdSkin = (FbxSkin*)subdGeometry->GetDeformer(i, FbxDeformer::eSkin); + int lClusterCount = subdSkin->GetClusterCount(); + for (int j = 0; j != lClusterCount; ++j) + { + FbxCluster* cluster = skin->GetCluster(j); + FbxCluster* tmp = FbxCluster::Create(pDestScene, cluster->GetName()); + + // set the original cluster data aside to tmp cluster + FbxAMatrix lMatrix; + tmp->SetLink(cluster->GetLink()); + tmp->SetLinkMode(cluster->GetLinkMode()); + tmp->SetTransformMatrix(cluster->GetTransformMatrix(lMatrix)); + tmp->SetTransformLinkMatrix(cluster->GetTransformLinkMatrix(lMatrix)); + if (cluster->GetAssociateModel() != NULL) + tmp->SetTransformAssociateModelMatrix(cluster->GetTransformAssociateModelMatrix(lMatrix)); + + // reset cluster and restore data from tmp + cluster->Reset(); + cluster->SetLink(tmp->GetLink()); + cluster->SetLinkMode(tmp->GetLinkMode()); + cluster->SetTransformMatrix(tmp->GetTransformMatrix(lMatrix)); + cluster->SetTransformLinkMatrix(tmp->GetTransformLinkMatrix(lMatrix)); + if (tmp->GetAssociateModel() != NULL) + cluster->SetTransformAssociateModelMatrix(tmp->GetTransformAssociateModelMatrix(lMatrix)); + + // get interpolated skin weight data from subd cluster and save it to the original cluster + FbxCluster* subdCluster = subdSkin->GetCluster(j); + int lIndexCount = subdCluster->GetControlPointIndicesCount(); + int* lIndices = subdCluster->GetControlPointIndices(); + double* lWeights = subdCluster->GetControlPointWeights(); + for (int k = 0; k < lIndexCount; k++) + { + cluster->AddControlPointIndex(lIndices[k], lWeights[k]); + } + + } // for each cluster + } // for each skin + + return pMesh; +} diff --git a/src/pluginmain.cpp b/src/pluginmain.cpp new file mode 100644 index 0000000..261e6fc --- /dev/null +++ b/src/pluginmain.cpp @@ -0,0 +1,47 @@ +#include "dzplugin.h" +#include "dzapp.h" + +#include "common_version.h" +#include "DzBridgeDialog_Scriptable.h" +#include "DzBridgeMorphSelectionDialog_Scriptable.h" +#include "DzBridgeSubdivisionDialog_Scriptable.h" +#include "OpenFBXInterface.h" +#include "DzBridgeAction_Scriptable.h" + +#include "dzbridge.h" + +CPP_PLUGIN_DEFINITION("Daz Bridges Common Library") + +DZ_PLUGIN_AUTHOR("Daz 3D, Inc"); + +DZ_PLUGIN_VERSION(COMMON_MAJOR, COMMON_MINOR, COMMON_REV, COMMON_BUILD); + +#ifdef _DEBUG +DZ_PLUGIN_DESCRIPTION(QString( +"Pre-Release Daz Bridge Library v%1.%2.%3.%4
\ +Github

" +).arg(COMMON_MAJOR).arg(COMMON_MINOR).arg(COMMON_REV).arg(COMMON_BUILD)); +#else +DZ_PLUGIN_DESCRIPTION(QString( +"This plugin provides the ability to access Daz Bridge functions from Daz Script. \ +Documentation and source code are available on Github

" +)); +#endif + +NEW_PLUGIN_CUSTOM_CLASS_GUID(DzBridgeDialog, c0830510-cea8-419a-b17b-49b3353e3d07); +NEW_PLUGIN_CUSTOM_CLASS_GUID(DzBridgeMorphSelectionDialog, 321916ba-0bcc-45d9-8c7e-ebbe80dea51c); +NEW_PLUGIN_CUSTOM_CLASS_GUID(DzBridgeSubdivisionDialog, a2342e17-db3b-4032-a576-75b5843fa893); +DZ_PLUGIN_CLASS_GUID(OpenFBXInterface, 9aaaf080-28c1-4e0f-a3e9-a0205e91a154); +DZ_PLUGIN_CLASS_GUID(DzBridgeAction, 71fb7202-4b49-47ba-a82a-4780e3819776); + +#ifdef UNITTEST_DZBRIDGE +#include "UnitTest_DzBridgeAction.h" +#include "UnitTest_DzBridgeDialog.h" +#include "UnitTest_DzBridgeMorphSelectionDialog.h" +#include "UnitTest_DzBridgeSubdivisionDialog.h" + +DZ_PLUGIN_CLASS_GUID(UnitTest_DzBridgeAction, 1ae818ba-d745-4db7-afb9-b1cb5e7700db); +DZ_PLUGIN_CLASS_GUID(UnitTest_DzBridgeDialog, 15bdc1cf-fbe6-4085-b729-fcb5e428fe71); +DZ_PLUGIN_CLASS_GUID(UnitTest_DzBridgeMorphSelectionDialog, 8d4ba27a-bb2a-4d69-95da-c8dc1b095bcc); +DZ_PLUGIN_CLASS_GUID(UnitTest_DzBridgeSubdivisionDialog, fc3a8f28-fef2-44ed-ac99-25aadb91e3d5); +#endif