pyjallib 0.1.17__py3-none-any.whl → 0.1.19__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyjallib/__init__.py +5 -4
- pyjallib/exceptions.py +75 -0
- pyjallib/logger.py +152 -65
- pyjallib/max/__init__.py +8 -0
- pyjallib/max/autoClavicle.py +17 -5
- pyjallib/max/bone.py +21 -1
- pyjallib/max/boneChain.py +2 -0
- pyjallib/max/constraint.py +27 -2
- pyjallib/max/elbow.py +105 -0
- pyjallib/max/groinBone.py +2 -0
- pyjallib/max/header.py +121 -113
- pyjallib/max/hip.py +2 -0
- pyjallib/max/inguinal.py +117 -0
- pyjallib/max/kneeBone.py +2 -0
- pyjallib/max/macro/jal_macro_bone.py +221 -8
- pyjallib/max/macro/jal_macro_constraint.py +30 -0
- pyjallib/max/mirror.py +20 -13
- pyjallib/max/shoulder.py +173 -0
- pyjallib/max/twistBone.py +22 -19
- pyjallib/max/volumeBone.py +12 -1
- pyjallib/max/wrist.py +113 -0
- pyjallib/nameToPath.py +78 -61
- pyjallib/perforce.py +101 -171
- pyjallib/ue5/inUnreal/animationImporter.py +111 -19
- pyjallib/ue5/inUnreal/baseImporter.py +10 -7
- pyjallib/ue5/inUnreal/skeletalMeshImporter.py +1 -1
- pyjallib/ue5/inUnreal/skeletonImporter.py +1 -1
- pyjallib/ue5/logger.py +152 -111
- pyjallib/ue5/templateProcessor.py +35 -2
- pyjallib/ue5/templates/__init__.py +6 -3
- pyjallib/ue5/templates/batchAnimImportTemplate.py +21 -0
- {pyjallib-0.1.17.dist-info → pyjallib-0.1.19.dist-info}/METADATA +1 -1
- {pyjallib-0.1.17.dist-info → pyjallib-0.1.19.dist-info}/RECORD +34 -28
- {pyjallib-0.1.17.dist-info → pyjallib-0.1.19.dist-info}/WHEEL +0 -0
@@ -26,7 +26,31 @@ class AnimationImporter(BaseImporter):
|
|
26
26
|
def asset_type(self) -> str:
|
27
27
|
return "Animation"
|
28
28
|
|
29
|
-
def
|
29
|
+
def _create_batch_import_description(self, inFbxFiles: list[str], inAssetFullPaths: list[str]) -> str:
|
30
|
+
"""
|
31
|
+
배치 임포트용 간결한 디스크립션 생성
|
32
|
+
|
33
|
+
Args:
|
34
|
+
inFbxFiles (list[str]): 임포트된 FBX 파일 목록
|
35
|
+
inAssetFullPaths (list[str]): 임포트된 에셋 전체 경로 목록
|
36
|
+
|
37
|
+
Returns:
|
38
|
+
str: 간결한 디스크립션
|
39
|
+
"""
|
40
|
+
totalCount = len(inFbxFiles)
|
41
|
+
|
42
|
+
if totalCount <= 3:
|
43
|
+
# 3개 이하면 모든 경로 표시
|
44
|
+
fbxList = ", ".join(inFbxFiles)
|
45
|
+
assetList = ", ".join(inAssetFullPaths)
|
46
|
+
return f"Animation Batch Import ({totalCount} files): {fbxList} -> {assetList}"
|
47
|
+
else:
|
48
|
+
# 3개 초과면 처음 3개만 표시하고 나머지는 개수로 표시
|
49
|
+
fbxList = ", ".join(inFbxFiles[:3]) + f" ... (and {totalCount - 3} more)"
|
50
|
+
assetList = ", ".join(inAssetFullPaths[:3]) + f" ... (and {totalCount - 3} more)"
|
51
|
+
return f"Animation Batch Import ({totalCount} files): {fbxList} -> {assetList}"
|
52
|
+
|
53
|
+
def create_import_task(self, inFbxFile: str, inDestinationPath: str, inFbxSkeletonPath: str):
|
30
54
|
"""애니메이션 임포트를 위한 태스크 생성 - 스켈레톤 필수 지정"""
|
31
55
|
ue5_logger.debug(f"애니메이션 임포트 태스크 생성 시작: {inFbxFile}")
|
32
56
|
|
@@ -75,7 +99,7 @@ class AnimationImporter(BaseImporter):
|
|
75
99
|
if unreal.Paths.file_exists(assetFullPath):
|
76
100
|
unreal.SourceControl.check_out_or_add_file(assetFullPath, silent=True)
|
77
101
|
|
78
|
-
task = self.
|
102
|
+
task = self.create_import_task(inFbxFile, destinationPath, inFbxSkeletonPath)
|
79
103
|
|
80
104
|
ue5_logger.info(f"애니메이션 임포트 실행: {inFbxFile} -> {destinationPath}/{assetName}")
|
81
105
|
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
@@ -86,29 +110,97 @@ class AnimationImporter(BaseImporter):
|
|
86
110
|
ue5_logger.error(error_msg)
|
87
111
|
raise ValueError(error_msg)
|
88
112
|
|
89
|
-
|
90
|
-
|
91
|
-
for asset in result:
|
92
|
-
if isinstance(asset, unreal.AnimSequence):
|
93
|
-
importedAnimation = asset
|
94
|
-
break
|
95
|
-
|
96
|
-
if importedAnimation is None:
|
97
|
-
error_msg = f"애니메이션 에셋을 찾을 수 없음: {inFbxFile}"
|
98
|
-
ue5_logger.error(error_msg)
|
99
|
-
raise ValueError(error_msg)
|
100
|
-
|
101
|
-
importedObjectPaths = self.get_dirty_deps(assetFullPath)
|
113
|
+
importedObjectPaths = task.imported_object_paths
|
114
|
+
refObjectPaths = self.get_dirty_deps(assetFullPath)
|
102
115
|
|
103
|
-
|
104
|
-
|
116
|
+
allImportRelatedPaths = list(dict.fromkeys(importedObjectPaths + refObjectPaths))
|
117
|
+
for assetPath in allImportRelatedPaths:
|
118
|
+
unreal.SourceControl.check_out_or_add_file(assetPath, silent=True)
|
105
119
|
|
106
120
|
checkInDescription = f"Animation Imported by {inFbxFile} to {assetFullPath}"
|
107
121
|
if inDescription is not None:
|
108
122
|
checkInDescription = inDescription
|
109
123
|
|
110
|
-
|
124
|
+
allImportAbsPaths = []
|
125
|
+
for assetPath in allImportRelatedPaths:
|
126
|
+
assetObj = unreal.EditorAssetLibrary.load_asset(assetPath)
|
127
|
+
if assetObj is not None:
|
128
|
+
absPath = unreal.SystemLibrary.get_system_path(assetObj)
|
129
|
+
allImportAbsPaths.append(absPath)
|
130
|
+
|
131
|
+
unreal.SourceControl.check_in_files(allImportAbsPaths, checkInDescription, silent=True)
|
111
132
|
|
112
133
|
ue5_logger.info(f"애니메이션 임포트 성공: {inFbxFile} -> {len(result)}개 객체 생성")
|
113
134
|
return self._create_result_dict(inFbxFile, destinationPath, assetName, True)
|
114
|
-
|
135
|
+
|
136
|
+
def import_animations(self, inFbxFiles: list[str], inFbxSkeletonPaths: list[str], inAssetNames: list[str] = None, inDescription: str = None):
|
137
|
+
ue5_logger.info(f"애니메이션 임포트 시작: {inFbxFiles}")
|
138
|
+
|
139
|
+
if len(inFbxFiles) != len(inFbxSkeletonPaths):
|
140
|
+
error_msg = "애니메이션 임포트에는 파일과 스켈레톤이 같은 개수여야 합니다"
|
141
|
+
ue5_logger.error(error_msg)
|
142
|
+
raise ValueError(error_msg)
|
143
|
+
|
144
|
+
if inAssetNames is not None and len(inFbxFiles) != len(inAssetNames):
|
145
|
+
error_msg = "애니메이션 임포트에는 파일과 에셋 이름이 같은 개수여야 합니다"
|
146
|
+
ue5_logger.error(error_msg)
|
147
|
+
raise ValueError(error_msg)
|
148
|
+
|
149
|
+
destinationPaths = []
|
150
|
+
assetNames = []
|
151
|
+
assetFullPaths = []
|
152
|
+
tasks = []
|
153
|
+
for index, fbxFile in enumerate(inFbxFiles):
|
154
|
+
cusAssetName = None
|
155
|
+
if inAssetNames is not None:
|
156
|
+
cusAssetName = inAssetNames[index]
|
157
|
+
destinationPath, assetName = self._prepare_import_paths(fbxFile, cusAssetName)
|
158
|
+
|
159
|
+
destinationPaths.append(destinationPath)
|
160
|
+
assetNames.append(assetName)
|
161
|
+
assetFullPath = f"{destinationPath}/{assetName}"
|
162
|
+
assetFullPaths.append(assetFullPath)
|
163
|
+
|
164
|
+
if unreal.Paths.file_exists(assetFullPath):
|
165
|
+
unreal.SourceControl.check_out_or_add_file(assetFullPath, silent=True)
|
166
|
+
|
167
|
+
task = self.create_import_task(fbxFile, destinationPath, inFbxSkeletonPaths[index])
|
168
|
+
tasks.append(task)
|
169
|
+
|
170
|
+
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks(tasks)
|
171
|
+
|
172
|
+
batchImportedAssetPaths = []
|
173
|
+
batchImporteAbsPaths = []
|
174
|
+
for index, task in enumerate(tasks):
|
175
|
+
result = task.get_objects()
|
176
|
+
if len(result) == 0:
|
177
|
+
error_msg = f"애니메이션 임포트 실패: {inFbxFiles[index]}"
|
178
|
+
ue5_logger.error(error_msg)
|
179
|
+
raise ValueError(error_msg)
|
180
|
+
|
181
|
+
importedObjectPaths = task.imported_object_paths
|
182
|
+
refObjectPaths = self.get_dirty_deps(assetFullPaths[index])
|
183
|
+
|
184
|
+
|
185
|
+
allImportRelatedPaths = list(dict.fromkeys(importedObjectPaths + refObjectPaths))
|
186
|
+
for assetPath in allImportRelatedPaths:
|
187
|
+
unreal.SourceControl.check_out_or_add_file(assetPath, silent=True)
|
188
|
+
batchImportedAssetPaths.append(assetPath)
|
189
|
+
|
190
|
+
batchImportedAssetPaths = list(dict.fromkeys(batchImportedAssetPaths))
|
191
|
+
for assetPath in batchImportedAssetPaths:
|
192
|
+
assetObj = unreal.EditorAssetLibrary.load_asset(assetPath)
|
193
|
+
if assetObj is not None:
|
194
|
+
absPath = unreal.SystemLibrary.get_system_path(assetObj)
|
195
|
+
batchImporteAbsPaths.append(absPath)
|
196
|
+
|
197
|
+
# 배치 임포트용 간결한 디스크립션 생성
|
198
|
+
if inDescription is not None:
|
199
|
+
checkInDescription = inDescription
|
200
|
+
else:
|
201
|
+
checkInDescription = self._create_batch_import_description(inFbxFiles, assetFullPaths)
|
202
|
+
|
203
|
+
checkinResult = unreal.SourceControl.check_in_files(batchImporteAbsPaths, checkInDescription, silent=True)
|
204
|
+
ue5_logger.info(f"배치 임포트 체크인 결과: {checkinResult}")
|
205
|
+
|
206
|
+
ue5_logger.info(f"애니메이션 임포트 완료: {inFbxFiles}")
|
@@ -153,7 +153,7 @@ class BaseImporter(ABC):
|
|
153
153
|
return destinationPath, assetName
|
154
154
|
|
155
155
|
@abstractmethod
|
156
|
-
def
|
156
|
+
def create_import_task(self, inFbxFile: str, inDestinationPath: str):
|
157
157
|
"""임포트 태스크를 생성하는 추상 메서드 - 각 임포터에서 구현"""
|
158
158
|
pass
|
159
159
|
|
@@ -163,6 +163,8 @@ class BaseImporter(ABC):
|
|
163
163
|
assetRegistry = unreal.AssetRegistryHelpers.get_asset_registry()
|
164
164
|
assetData = unreal.EditorAssetLibrary.find_asset_data(inAssetPath)
|
165
165
|
|
166
|
+
ue5_logger.error(f"assetData: {assetData.asset_name}")
|
167
|
+
|
166
168
|
depPackages = assetRegistry.get_dependencies(
|
167
169
|
assetData.package_name,
|
168
170
|
unreal.AssetRegistryDependencyOptions(
|
@@ -174,11 +176,12 @@ class BaseImporter(ABC):
|
|
174
176
|
)
|
175
177
|
)
|
176
178
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
if
|
182
|
-
|
179
|
+
if depPackages is not None:
|
180
|
+
for dep in depPackages:
|
181
|
+
depPathStart = str(dep).split('/')[1]
|
182
|
+
assetPathStart = str(assetData.package_name).split('/')[1]
|
183
|
+
if depPathStart == assetPathStart:
|
184
|
+
if unreal.EditorAssetLibrary.save_asset(dep, only_if_is_dirty=True):
|
185
|
+
returnList.append(dep)
|
183
186
|
|
184
187
|
return returnList
|
@@ -25,7 +25,7 @@ class SkeletalMeshImporter(BaseImporter):
|
|
25
25
|
def asset_type(self) -> str:
|
26
26
|
return "SkeletalMesh"
|
27
27
|
|
28
|
-
def
|
28
|
+
def create_import_task(self, inFbxFile: str, inDestinationPath: str, inFbxSkeletonPath: str):
|
29
29
|
"""스켈레탈 메시 임포트를 위한 태스크 생성 - 스켈레톤 필수 지정"""
|
30
30
|
ue5_logger.debug(f"스켈레탈 메시 임포트 태스크 생성 시작: {inFbxFile}")
|
31
31
|
|
@@ -26,7 +26,7 @@ class SkeletonImporter(BaseImporter):
|
|
26
26
|
def asset_type(self) -> str:
|
27
27
|
return "Skeleton"
|
28
28
|
|
29
|
-
def
|
29
|
+
def create_import_task(self, inFbxFile: str, inDestinationPath: str):
|
30
30
|
"""스켈레톤 임포트를 위한 태스크 생성 - 새 스켈레톤 생성"""
|
31
31
|
ue5_logger.debug(f"스켈레톤 임포트 태스크 생성 시작: {inFbxFile}")
|
32
32
|
|
pyjallib/ue5/logger.py
CHANGED
@@ -3,14 +3,17 @@
|
|
3
3
|
|
4
4
|
"""
|
5
5
|
UE5 모듈 로깅 설정 모듈
|
6
|
-
|
6
|
+
메인 Logger 클래스를 상속하여 UE5 전용 기능을 추가합니다.
|
7
7
|
"""
|
8
8
|
|
9
9
|
import logging
|
10
|
-
import os
|
11
10
|
from pathlib import Path
|
11
|
+
from typing import Optional
|
12
12
|
from datetime import datetime
|
13
13
|
|
14
|
+
from pyjallib.logger import Logger
|
15
|
+
|
16
|
+
|
14
17
|
class UE5LogHandler(logging.Handler):
|
15
18
|
"""UE5 전용 로그 핸들러 - UE5의 로그 시스템과 호환되도록 설계"""
|
16
19
|
|
@@ -20,75 +23,158 @@ class UE5LogHandler(logging.Handler):
|
|
20
23
|
# UE5의 unreal.log 함수 사용
|
21
24
|
import unreal
|
22
25
|
|
26
|
+
# 메시지 포맷팅
|
27
|
+
message = self.format(record) if self.formatter else record.getMessage()
|
28
|
+
|
23
29
|
# 로그 레벨에 따라 적절한 UE5 로그 함수 호출
|
24
30
|
if record.levelno >= logging.ERROR:
|
25
|
-
unreal.log_error(f"[PyJalLib] {
|
31
|
+
unreal.log_error(f"[PyJalLib] {message}")
|
26
32
|
elif record.levelno >= logging.WARNING:
|
27
|
-
unreal.log_warning(f"[PyJalLib] {
|
33
|
+
unreal.log_warning(f"[PyJalLib] {message}")
|
28
34
|
elif record.levelno >= logging.INFO:
|
29
|
-
unreal.log(f"[PyJalLib] {
|
35
|
+
unreal.log(f"[PyJalLib] {message}")
|
30
36
|
else: # DEBUG
|
31
|
-
unreal.log(f"[PyJalLib-DEBUG] {
|
37
|
+
unreal.log(f"[PyJalLib-DEBUG] {message}")
|
32
38
|
|
33
39
|
except ImportError:
|
34
40
|
# unreal 모듈이 없는 경우 표준 출력 사용
|
35
|
-
|
41
|
+
message = self.format(record) if self.formatter else record.getMessage()
|
42
|
+
print(f"[PyJalLib] {message}")
|
36
43
|
except Exception:
|
37
44
|
# 모든 예외를 무시하여 로깅 실패가 애플리케이션을 중단하지 않도록 함
|
38
45
|
pass
|
39
46
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
47
|
+
|
48
|
+
class UE5Logger(Logger):
|
49
|
+
"""UE5 전용 로거 클래스 - 메인 Logger 클래스를 상속하여 UE5 기능을 추가"""
|
50
|
+
|
51
|
+
def __init__(self, inLogPath: Optional[str] = None, inLogFileName: Optional[str] = None,
|
52
|
+
inEnableConsole: bool = True, inEnableUE5: bool = True):
|
53
|
+
"""UE5 로거 인스턴스 초기화
|
54
|
+
|
55
|
+
Args:
|
56
|
+
inLogPath (str, optional): 로그 파일 저장 경로.
|
57
|
+
None인 경우 기본 경로 사용 (Documents/PyJalLib/logs)
|
58
|
+
inLogFileName (str, optional): 로그 파일명 (확장자 제외).
|
59
|
+
None인 경우 기본값 "ue5_module" 사용
|
60
|
+
실제 파일명은 "YYYYMMDD_파일명.log" 형식으로 생성
|
61
|
+
inEnableConsole (bool): 콘솔 출력 활성화 여부 (기본값: True)
|
62
|
+
inEnableUE5 (bool): UE5 출력 활성화 여부 (기본값: True)
|
63
|
+
"""
|
64
|
+
# UE5 기본 파일명 설정
|
65
|
+
if inLogFileName is None:
|
66
|
+
inLogFileName = "ue5_module"
|
67
|
+
|
68
|
+
# 부모 클래스 초기화
|
69
|
+
super().__init__(inLogPath, inLogFileName, inEnableConsole)
|
70
|
+
|
71
|
+
# UE5 핸들러 설정
|
72
|
+
self._enableUE5 = inEnableUE5
|
73
|
+
if self._enableUE5:
|
74
|
+
self._add_ue5_handler()
|
75
|
+
|
76
|
+
def set_ue5_log_level(self, inLevel: str):
|
77
|
+
"""
|
78
|
+
UE5 출력의 로깅 레벨을 설정합니다.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
inLevel (str): 로깅 레벨 ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
|
82
|
+
"""
|
83
|
+
level_map = {
|
84
|
+
'DEBUG': logging.DEBUG,
|
85
|
+
'INFO': logging.INFO,
|
86
|
+
'WARNING': logging.WARNING,
|
87
|
+
'ERROR': logging.ERROR,
|
88
|
+
'CRITICAL': logging.CRITICAL
|
89
|
+
}
|
90
|
+
|
91
|
+
if inLevel.upper() not in level_map:
|
92
|
+
self.warning(f"잘못된 로깅 레벨: {inLevel}. 기본값 INFO로 설정합니다.")
|
93
|
+
inLevel = 'INFO'
|
94
|
+
|
95
|
+
# UE5 핸들러 찾기
|
96
|
+
for handler in self._logger.handlers:
|
97
|
+
if isinstance(handler, UE5LogHandler):
|
98
|
+
handler.setLevel(level_map[inLevel.upper()])
|
99
|
+
self.info(f"UE5 로깅 레벨이 {inLevel.upper()}로 설정되었습니다.")
|
100
|
+
return
|
101
|
+
|
102
|
+
self.warning("UE5 핸들러를 찾을 수 없습니다.")
|
103
|
+
|
104
|
+
def enable_ue5_output(self, inEnable: bool = True):
|
105
|
+
"""
|
106
|
+
UE5 출력을 활성화/비활성화합니다.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
inEnable (bool): UE5 출력 활성화 여부
|
110
|
+
"""
|
111
|
+
if inEnable and not self._enableUE5:
|
112
|
+
# UE5 핸들러 추가
|
113
|
+
self._add_ue5_handler()
|
114
|
+
self._enableUE5 = True
|
115
|
+
self.info("UE5 출력이 활성화되었습니다.")
|
116
|
+
elif not inEnable and self._enableUE5:
|
117
|
+
# UE5 핸들러 제거
|
118
|
+
self._remove_ue5_handler()
|
119
|
+
self._enableUE5 = False
|
120
|
+
self.info("UE5 출력이 비활성화되었습니다.")
|
121
|
+
|
122
|
+
def _add_ue5_handler(self):
|
123
|
+
"""UE5 핸들러를 로거에 추가"""
|
124
|
+
try:
|
125
|
+
ue5_handler = UE5LogHandler()
|
126
|
+
ue5_handler.setLevel(logging.INFO) # UE5에서는 INFO 이상만 표시
|
127
|
+
ue5_handler.setFormatter(self._get_formatter())
|
128
|
+
self._logger.addHandler(ue5_handler)
|
129
|
+
except Exception:
|
130
|
+
# UE5 핸들러 생성 실패 시 무시
|
131
|
+
pass
|
86
132
|
|
87
|
-
|
133
|
+
def _remove_ue5_handler(self):
|
134
|
+
"""UE5 핸들러를 로거에서 제거"""
|
135
|
+
for handler in self._logger.handlers[:]:
|
136
|
+
if isinstance(handler, UE5LogHandler):
|
137
|
+
self._logger.removeHandler(handler)
|
138
|
+
try:
|
139
|
+
handler.close()
|
140
|
+
except Exception:
|
141
|
+
pass
|
142
|
+
|
143
|
+
def _log_separator(self, inMessage: str) -> None:
|
144
|
+
"""구분선 메시지를 모든 핸들러에 직접 출력 (UE5 핸들러 포함)"""
|
145
|
+
# 구분선은 INFO 레벨로 출력하되, 특별한 포맷 사용
|
146
|
+
separator_record = logging.LogRecord(
|
147
|
+
name=self._logger.name,
|
148
|
+
level=logging.INFO,
|
149
|
+
pathname="",
|
150
|
+
lineno=0,
|
151
|
+
msg=inMessage,
|
152
|
+
args=(),
|
153
|
+
exc_info=None
|
154
|
+
)
|
155
|
+
|
156
|
+
# 각 핸들러에 직접 전송 (포맷터 우회)
|
157
|
+
for handler in self._logger.handlers:
|
158
|
+
try:
|
159
|
+
# 핸들러 레벨 확인
|
160
|
+
if handler.level <= logging.INFO:
|
161
|
+
if isinstance(handler, UE5LogHandler):
|
162
|
+
# UE5 핸들러의 경우 직접 emit 호출
|
163
|
+
handler.emit(separator_record)
|
164
|
+
elif hasattr(handler, 'stream'):
|
165
|
+
# 구분선만 특별한 포맷으로 출력
|
166
|
+
handler.stream.write(inMessage + "\n")
|
167
|
+
if hasattr(handler, 'flush'):
|
168
|
+
handler.flush()
|
169
|
+
except Exception:
|
170
|
+
# 핸들러 오류 시 무시
|
171
|
+
pass
|
172
|
+
|
88
173
|
|
89
|
-
#
|
90
|
-
ue5_logger =
|
174
|
+
# 편의를 위한 전역 UE5 로거 인스턴스
|
175
|
+
ue5_logger = UE5Logger()
|
91
176
|
|
177
|
+
# 호환성을 위한 기존 함수들
|
92
178
|
def set_log_level(inLevel: str):
|
93
179
|
"""
|
94
180
|
UE5 모듈의 로깅 레벨을 설정합니다.
|
@@ -108,7 +194,7 @@ def set_log_level(inLevel: str):
|
|
108
194
|
ue5_logger.warning(f"잘못된 로깅 레벨: {inLevel}. 기본값 INFO로 설정합니다.")
|
109
195
|
inLevel = 'INFO'
|
110
196
|
|
111
|
-
ue5_logger.setLevel(level_map[inLevel.upper()])
|
197
|
+
ue5_logger._logger.setLevel(level_map[inLevel.upper()])
|
112
198
|
ue5_logger.info(f"로깅 레벨이 {inLevel.upper()}로 설정되었습니다.")
|
113
199
|
|
114
200
|
def set_ue5_log_level(inLevel: str):
|
@@ -118,26 +204,7 @@ def set_ue5_log_level(inLevel: str):
|
|
118
204
|
Args:
|
119
205
|
inLevel (str): 로깅 레벨 ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
|
120
206
|
"""
|
121
|
-
|
122
|
-
'DEBUG': logging.DEBUG,
|
123
|
-
'INFO': logging.INFO,
|
124
|
-
'WARNING': logging.WARNING,
|
125
|
-
'ERROR': logging.ERROR,
|
126
|
-
'CRITICAL': logging.CRITICAL
|
127
|
-
}
|
128
|
-
|
129
|
-
if inLevel.upper() not in level_map:
|
130
|
-
ue5_logger.warning(f"잘못된 로깅 레벨: {inLevel}. 기본값 INFO로 설정합니다.")
|
131
|
-
inLevel = 'INFO'
|
132
|
-
|
133
|
-
# UE5 핸들러 찾기
|
134
|
-
for handler in ue5_logger.handlers:
|
135
|
-
if isinstance(handler, UE5LogHandler):
|
136
|
-
handler.setLevel(level_map[inLevel.upper()])
|
137
|
-
ue5_logger.info(f"UE5 로깅 레벨이 {inLevel.upper()}로 설정되었습니다.")
|
138
|
-
return
|
139
|
-
|
140
|
-
ue5_logger.warning("UE5 핸들러를 찾을 수 없습니다.")
|
207
|
+
ue5_logger.set_ue5_log_level(inLevel)
|
141
208
|
|
142
209
|
def get_log_file_path():
|
143
210
|
"""
|
@@ -149,7 +216,7 @@ def get_log_file_path():
|
|
149
216
|
documents_path = Path.home() / "Documents"
|
150
217
|
log_folder = documents_path / "PyJalLib" / "logs"
|
151
218
|
current_date = datetime.now().strftime("%Y%m%d")
|
152
|
-
log_filename = f"
|
219
|
+
log_filename = f"{current_date}_ue5_module.log"
|
153
220
|
return str(log_folder / log_filename)
|
154
221
|
|
155
222
|
def set_log_file_path(inLogFolder: str = None, inLogFilename: str = None):
|
@@ -160,41 +227,15 @@ def set_log_file_path(inLogFolder: str = None, inLogFilename: str = None):
|
|
160
227
|
inLogFolder (str, optional): 로그 폴더 경로. None인 경우 기본 Documents/PyJalLib/logs 사용
|
161
228
|
inLogFilename (str, optional): 로그 파일명. None인 경우 기본 날짜 기반 파일명 사용
|
162
229
|
"""
|
163
|
-
#
|
164
|
-
|
165
|
-
documents_path = Path.home() / "Documents"
|
166
|
-
inLogFolder = str(documents_path / "PyJalLib" / "logs")
|
167
|
-
|
168
|
-
if inLogFilename is None:
|
169
|
-
current_date = datetime.now().strftime("%Y%m%d")
|
170
|
-
inLogFilename = f"ue5_module_{current_date}.log"
|
171
|
-
|
172
|
-
# 경로 생성
|
173
|
-
log_folder = Path(inLogFolder)
|
174
|
-
log_folder.mkdir(parents=True, exist_ok=True)
|
175
|
-
log_file_path = log_folder / inLogFilename
|
176
|
-
|
177
|
-
# 기존 파일 핸들러 제거
|
178
|
-
for handler in ue5_logger.handlers[:]:
|
179
|
-
if isinstance(handler, logging.FileHandler):
|
180
|
-
ue5_logger.removeHandler(handler)
|
181
|
-
handler.close()
|
182
|
-
|
183
|
-
# 새로운 파일 핸들러 생성
|
184
|
-
new_file_handler = logging.FileHandler(log_file_path, encoding='utf-8')
|
185
|
-
new_file_handler.setLevel(logging.DEBUG)
|
186
|
-
|
187
|
-
# 포맷터 설정 (기존과 동일)
|
188
|
-
formatter = logging.Formatter(
|
189
|
-
'%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s',
|
190
|
-
datefmt='%Y-%m-%d %H:%M:%S'
|
191
|
-
)
|
192
|
-
new_file_handler.setFormatter(formatter)
|
230
|
+
# 새로운 UE5Logger 인스턴스 생성
|
231
|
+
global ue5_logger
|
193
232
|
|
194
|
-
#
|
195
|
-
ue5_logger.
|
233
|
+
# 기존 로거 정리
|
234
|
+
ue5_logger.close()
|
196
235
|
|
197
|
-
|
236
|
+
# 새로운 로거 생성
|
237
|
+
ue5_logger = UE5Logger(inLogPath=inLogFolder, inLogFileName=inLogFilename)
|
238
|
+
ue5_logger.info(f"로그 파일 경로가 변경되었습니다.")
|
198
239
|
|
199
240
|
# 로깅 설정 완료 메시지
|
200
241
|
ue5_logger.info("UE5 모듈 로깅 시스템 초기화 완료")
|
@@ -13,7 +13,8 @@ from .templates import (
|
|
13
13
|
get_available_templates,
|
14
14
|
ANIM_IMPORT_TEMPLATE,
|
15
15
|
SKELETON_IMPORT_TEMPLATE,
|
16
|
-
SKELETAL_MESH_IMPORT_TEMPLATE
|
16
|
+
SKELETAL_MESH_IMPORT_TEMPLATE,
|
17
|
+
BATCH_ANIM_IMPORT_TEMPLATE
|
17
18
|
)
|
18
19
|
|
19
20
|
|
@@ -197,7 +198,39 @@ class TemplateProcessor:
|
|
197
198
|
inOutputPath = self.get_default_output_path(SKELETAL_MESH_IMPORT_TEMPLATE, "skeletalMeshImportScript")
|
198
199
|
|
199
200
|
return self.process_template(template_path, inOutputPath, inTemplateData)
|
200
|
-
|
201
|
+
|
202
|
+
def process_batch_anim_import_template(self,
|
203
|
+
inTemplateData: Dict[str, Any],
|
204
|
+
inOutputPath: Optional[str] = None) -> str:
|
205
|
+
"""
|
206
|
+
배치 애니메이션 임포트 전용 템플릿 처리
|
207
|
+
|
208
|
+
Args:
|
209
|
+
inTemplateData (Dict[str, Any]): 템플릿 데이터
|
210
|
+
필수 키:
|
211
|
+
- inExtPackagePath: 외부 패키지 경로
|
212
|
+
- inAnimFbxPaths: 애니메이션 FBX 경로들 (리스트)
|
213
|
+
- inSkeletonFbxPaths: 스켈레톤 FBX 경로들 (리스트)
|
214
|
+
- inContentRootPrefix: Content 루트 경로
|
215
|
+
- inFbxRootPrefix: FBX 루트 경로
|
216
|
+
inOutputPath (Optional[str]): 출력 파일 경로. None인 경우 기본 경로 사용
|
217
|
+
|
218
|
+
Returns:
|
219
|
+
str: 처리된 템플릿 내용
|
220
|
+
"""
|
221
|
+
# 필수 키 검증
|
222
|
+
required_keys = ['inExtPackagePath', 'inAnimFbxPaths', 'inSkeletonFbxPaths',
|
223
|
+
'inContentRootPrefix', 'inFbxRootPrefix']
|
224
|
+
if not self.validate_template_data(BATCH_ANIM_IMPORT_TEMPLATE, inTemplateData, required_keys):
|
225
|
+
raise ValueError(f"배치 애니메이션 템플릿에 필요한 키가 누락되었습니다: {required_keys}")
|
226
|
+
|
227
|
+
template_path = get_template_path(BATCH_ANIM_IMPORT_TEMPLATE)
|
228
|
+
|
229
|
+
if inOutputPath is None:
|
230
|
+
inOutputPath = self.get_default_output_path(BATCH_ANIM_IMPORT_TEMPLATE, "batchAnimImportScript")
|
231
|
+
|
232
|
+
return self.process_template(template_path, inOutputPath, inTemplateData)
|
233
|
+
|
201
234
|
# === 유틸리티 메서드 ===
|
202
235
|
def validate_template_data(self, template_type: str, template_data: Dict[str, Any], required_keys: list = None) -> bool:
|
203
236
|
"""
|
@@ -14,12 +14,14 @@ from typing import Dict, Optional
|
|
14
14
|
ANIM_IMPORT_TEMPLATE = "animImport"
|
15
15
|
SKELETON_IMPORT_TEMPLATE = "skeletonImport"
|
16
16
|
SKELETAL_MESH_IMPORT_TEMPLATE = "skeletalMeshImport"
|
17
|
+
BATCH_ANIM_IMPORT_TEMPLATE = "batchAnimImport"
|
17
18
|
|
18
19
|
# 템플릿 파일 매핑
|
19
20
|
_TEMPLATE_FILE_MAP = {
|
20
21
|
ANIM_IMPORT_TEMPLATE: "animImportTemplate.py",
|
21
22
|
SKELETON_IMPORT_TEMPLATE: "skeletonImportTemplate.py",
|
22
|
-
SKELETAL_MESH_IMPORT_TEMPLATE: "skeletalMeshImportTemplate.py"
|
23
|
+
SKELETAL_MESH_IMPORT_TEMPLATE: "skeletalMeshImportTemplate.py",
|
24
|
+
BATCH_ANIM_IMPORT_TEMPLATE: "batchAnimImportTemplate.py"
|
23
25
|
}
|
24
26
|
|
25
27
|
def get_template_path(template_name: str) -> str:
|
@@ -27,7 +29,7 @@ def get_template_path(template_name: str) -> str:
|
|
27
29
|
템플릿 이름으로 템플릿 파일 경로 반환
|
28
30
|
|
29
31
|
Args:
|
30
|
-
template_name (str): 'animImport', 'skeletonImport', 'skeletalMeshImport' 중 하나
|
32
|
+
template_name (str): 'animImport', 'skeletonImport', 'skeletalMeshImport', 'batchAnimImport' 중 하나
|
31
33
|
|
32
34
|
Returns:
|
33
35
|
str: 템플릿 파일의 절대 경로
|
@@ -102,5 +104,6 @@ __all__ = [
|
|
102
104
|
'validate_template_name',
|
103
105
|
'ANIM_IMPORT_TEMPLATE',
|
104
106
|
'SKELETON_IMPORT_TEMPLATE',
|
105
|
-
'SKELETAL_MESH_IMPORT_TEMPLATE'
|
107
|
+
'SKELETAL_MESH_IMPORT_TEMPLATE',
|
108
|
+
'BATCH_ANIM_IMPORT_TEMPLATE'
|
106
109
|
]
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import sys
|
2
|
+
import os
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
# 프로젝트 루트 디렉토리 추가 (PyJalLib 디렉토리)
|
6
|
+
extPackagePath = r'{inExtPackagePath}'
|
7
|
+
|
8
|
+
if extPackagePath not in sys.path:
|
9
|
+
sys.path.insert(0, extPackagePath)
|
10
|
+
|
11
|
+
from pyjallib.ue5.inUnreal.animationImporter import AnimationImporter
|
12
|
+
|
13
|
+
fbxPaths = {inAnimFbxPaths}
|
14
|
+
skeletonPaths = {inSkeletonFbxPaths}
|
15
|
+
|
16
|
+
contentRootPrefix = r'{inContentRootPrefix}'
|
17
|
+
fbxRootPrefix = r'{inFbxRootPrefix}'
|
18
|
+
|
19
|
+
animImporter = AnimationImporter(inContentRootPrefix=contentRootPrefix, inFbxRootPrefix=fbxRootPrefix)
|
20
|
+
|
21
|
+
result = animImporter.import_animations(fbxPaths, skeletonPaths)
|