pyjallib 0.1.12__py3-none-any.whl → 0.1.14__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.
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ ToolManager 모듈 - 3DS Max에서 실행되는 도구들을 관리
6
+ 도구 인스턴스 생성, 닫기 등을 담당
7
+ """
8
+
9
+ from PySide2 import QtWidgets, QtCore
10
+ import gc
11
+
12
+ class ToolManager:
13
+ def __init__(self):
14
+ self.tools = {} # {tool_class_name: [instances]} 형태로 관리
15
+
16
+ def register_tool(self, tool_instance):
17
+ """도구 인스턴스를 등록합니다"""
18
+ class_name = tool_instance.__class__.__name__
19
+
20
+ if class_name not in self.tools:
21
+ self.tools[class_name] = []
22
+
23
+ self.tools[class_name].append(tool_instance)
24
+
25
+ def close_tool_by_type(self, tool_class):
26
+ """특정 유형의 도구를 모두 닫습니다"""
27
+ class_name = tool_class.__name__
28
+
29
+ if class_name not in self.tools:
30
+ return
31
+
32
+ # 해당 클래스의 모든 인스턴스 정리
33
+ for tool in self.tools[class_name]:
34
+ try:
35
+ if hasattr(tool, 'close'):
36
+ tool.close()
37
+ if hasattr(tool, 'deleteLater'):
38
+ tool.deleteLater()
39
+ except (RuntimeError, AttributeError) as e:
40
+ print(f"도구 닫기 오류: {e}")
41
+
42
+ # 목록 비우기
43
+ self.tools[class_name] = []
44
+
45
+ # 추가적으로 QApplication.allWidgets()를 통한 검사
46
+ try:
47
+ window_title = None
48
+ if hasattr(tool_class, 'windowTitle'):
49
+ window_title = tool_class.windowTitle
50
+
51
+ for widget in QtWidgets.QApplication.allWidgets():
52
+ if (isinstance(widget, QtWidgets.QDialog) and
53
+ ((window_title and hasattr(widget, 'windowTitle') and widget.windowTitle() == window_title) or
54
+ widget.__class__.__name__ == class_name)):
55
+ try:
56
+ widget.close()
57
+ widget.deleteLater()
58
+ except:
59
+ pass
60
+ except Exception as e:
61
+ print(f"위젯 검색 오류: {e}")
62
+
63
+ # 가비지 컬렉션 수행
64
+ gc.collect()
65
+
66
+ def show_tool(self, tool_class, **kwargs):
67
+ """
68
+ 도구를 표시합니다. 중복 실행을 방지하고 항상 새 인스턴스를 생성합니다.
69
+
70
+ Args:
71
+ tool_class: 도구 클래스
72
+ **kwargs: 도구 클래스 생성자에 전달할 인자들
73
+
74
+ Returns:
75
+ 새로 생성된 도구 인스턴스
76
+ """
77
+ # 기존 인스턴스 모두 정리
78
+ self.close_tool_by_type(tool_class)
79
+
80
+ # 약간의 지연을 두어 정리 완료를 기다림
81
+ QtCore.QTimer.singleShot(50, lambda: None)
82
+
83
+ # 새 인스턴스 생성
84
+ tool_instance = tool_class(**kwargs)
85
+
86
+ # 도구 등록
87
+ self.register_tool(tool_instance)
88
+
89
+ # 도구 표시
90
+ tool_instance.show()
91
+
92
+ return tool_instance
pyjallib/max/twistBone.py CHANGED
@@ -258,6 +258,9 @@ class TwistBone:
258
258
  twistBoneRotListController.setActive(twistBoneRotListController.count)
259
259
  twistBoneRotListController.weight[0] = 100.0
260
260
 
261
+ # 첫 번째 트위스트 본을 boneChainArray에 추가
262
+ boneChainArray.append(twistBone)
263
+
261
264
  if twistNum > 1:
262
265
  lastBone = self.bone.create_nub_bone(boneName, 2)
263
266
  lastBone.name = self.name.replace_name_part("Index", boneName, str(twistNum))
@@ -320,7 +323,8 @@ class TwistBone:
320
323
  if not inBoneChain or inBoneChain.is_empty():
321
324
  return None
322
325
 
323
- # 기존 객체 삭제
326
+ # 기존 객체 삭제 (delete_all 대신 delete 사용)
327
+ # delete는 bones와 helpers만 삭제하고 sourceBones와 parameters는 유지함
324
328
  inBoneChain.delete()
325
329
 
326
330
  # BoneChain에서 필요한 정보 추출
@@ -275,7 +275,8 @@ class VolumeBone: # Updated class name to match the new file name
275
275
  if not inBoneChain or inBoneChain.is_empty():
276
276
  return None
277
277
 
278
- # 기존 객체 삭제
278
+ # 기존 객체 삭제 (delete_all 대신 delete 사용)
279
+ # delete는 bones와 helpers만 삭제하고 sourceBones와 parameters는 유지함
279
280
  inBoneChain.delete()
280
281
 
281
282
  # BoneChain에서 필요한 정보 추출
pyjallib/perforce.py CHANGED
@@ -16,10 +16,14 @@ from pathlib import Path
16
16
 
17
17
  # 로깅 설정
18
18
  logger = logging.getLogger(__name__)
19
- logger.setLevel(logging.DEBUG)
19
+
20
+ # 기본 로그 레벨은 ERROR로 설정 (디버그 모드는 생성자에서 설정)
21
+ logger.setLevel(logging.ERROR)
22
+
20
23
  # 사용자 문서 폴더 내 로그 파일 저장
21
24
  log_path = os.path.join(Path.home() / "Documents", 'Perforce.log')
22
25
  file_handler = logging.FileHandler(log_path, encoding='utf-8')
26
+ file_handler.setLevel(logging.ERROR) # 기본적으로 ERROR 레벨만 기록
23
27
  file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
24
28
  logger.addHandler(file_handler)
25
29
 
@@ -27,8 +31,19 @@ logger.addHandler(file_handler)
27
31
  class Perforce:
28
32
  """P4Python을 사용하여 Perforce 작업을 수행하는 클래스."""
29
33
 
30
- def __init__(self):
31
- """Perforce 인스턴스를 초기화합니다."""
34
+ def __init__(self, debug_mode: bool = False):
35
+ """Perforce 인스턴스를 초기화합니다.
36
+
37
+ Args:
38
+ debug_mode (bool): True로 설정하면 DEBUG 레벨 로그를 활성화합니다.
39
+ 기본값은 False (ERROR 레벨만 기록)
40
+ """
41
+ # 디버그 모드에 따라 로그 레벨 설정
42
+ if debug_mode:
43
+ logger.setLevel(logging.DEBUG)
44
+ file_handler.setLevel(logging.DEBUG)
45
+ logger.debug("디버그 모드가 활성화되었습니다.")
46
+
32
47
  self.p4 = P4()
33
48
  self.connected = False
34
49
  self.workspaceRoot = r""
@@ -575,11 +590,13 @@ class Perforce:
575
590
 
576
591
  return all_success
577
592
 
578
- def submit_change_list(self, change_list_number: int) -> bool:
593
+ def submit_change_list(self, change_list_number: int, auto_revert_unchanged: bool = True) -> bool:
579
594
  """체인지 리스트를 제출합니다.
580
595
 
581
596
  Args:
582
597
  change_list_number (int): 제출할 체인지 리스트 번호
598
+ auto_revert_unchanged (bool, optional): 제출 후 변경사항이 없는 체크아웃된 파일들을
599
+ 자동으로 리버트할지 여부. 기본값 True
583
600
 
584
601
  Returns:
585
602
  bool: 제출 성공 시 True, 실패 시 False
@@ -590,6 +607,11 @@ class Perforce:
590
607
  try:
591
608
  self.p4.run_submit("-c", change_list_number)
592
609
  logger.info(f"체인지 리스트 {change_list_number} 제출 성공.")
610
+
611
+ # 제출 후 변경사항이 없는 체크아웃된 파일들을 자동으로 리버트
612
+ if auto_revert_unchanged:
613
+ self._auto_revert_unchanged_files(change_list_number)
614
+
593
615
  return True
594
616
  except P4Exception as e:
595
617
  self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 제출")
@@ -597,6 +619,62 @@ class Perforce:
597
619
  logger.warning(f"체인지 리스트 {change_list_number}에 제출할 파일이 없습니다.")
598
620
  return False
599
621
 
622
+ def _auto_revert_unchanged_files(self, change_list_number: int) -> None:
623
+ """제출 후 변경사항이 없는 체크아웃된 파일들을 자동으로 리버트합니다.
624
+
625
+ Args:
626
+ change_list_number (int): 체인지 리스트 번호
627
+ """
628
+ logger.debug(f"체인지 리스트 {change_list_number}에서 변경사항이 없는 파일들 자동 리버트 시도...")
629
+ try:
630
+ # 체인지 리스트에서 체크아웃된 파일들 가져오기
631
+ opened_files = self.p4.run_opened("-c", change_list_number)
632
+
633
+ if not opened_files:
634
+ logger.debug(f"체인지 리스트 {change_list_number}에 체크아웃된 파일이 없습니다.")
635
+ return
636
+
637
+ unchanged_files = []
638
+ for file_info in opened_files:
639
+ file_path = file_info.get('clientFile', '')
640
+ action = file_info.get('action', '')
641
+
642
+ # edit 액션의 파일만 확인 (add, delete는 변경사항이 있음)
643
+ if action == 'edit':
644
+ try:
645
+ # p4 diff 명령으로 파일의 변경사항 확인
646
+ diff_result = self.p4.run_diff("-sa", file_path)
647
+
648
+ # diff 결과가 비어있으면 변경사항이 없음
649
+ if not diff_result:
650
+ unchanged_files.append(file_path)
651
+ logger.debug(f"파일 '{file_path}'에 변경사항이 없어 리버트 대상으로 추가")
652
+ else:
653
+ logger.debug(f"파일 '{file_path}'에 변경사항이 있어 리버트하지 않음")
654
+
655
+ except P4Exception as e:
656
+ # diff 명령 실패 시에도 리버트 대상으로 추가 (안전하게 처리)
657
+ unchanged_files.append(file_path)
658
+ logger.debug(f"파일 '{file_path}' diff 확인 실패, 리버트 대상으로 추가: {e}")
659
+ else:
660
+ logger.debug(f"파일 '{file_path}'는 {action} 액션이므로 리버트하지 않음")
661
+
662
+ # 변경사항이 없는 파일들을 리버트
663
+ if unchanged_files:
664
+ logger.info(f"체인지 리스트 {change_list_number}에서 변경사항이 없는 파일 {len(unchanged_files)}개 자동 리버트 시도...")
665
+ for file_path in unchanged_files:
666
+ try:
667
+ self.p4.run_revert("-c", change_list_number, file_path)
668
+ logger.info(f"파일 '{file_path}' 자동 리버트 완료")
669
+ except P4Exception as e:
670
+ self._handle_p4_exception(e, f"파일 '{file_path}' 자동 리버트")
671
+ logger.info(f"체인지 리스트 {change_list_number}에서 변경사항이 없는 파일 {len(unchanged_files)}개 자동 리버트 완료")
672
+ else:
673
+ logger.debug(f"체인지 리스트 {change_list_number}에서 변경사항이 없는 파일이 없습니다.")
674
+
675
+ except P4Exception as e:
676
+ self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 자동 리버트 처리")
677
+
600
678
  def revert_change_list(self, change_list_number: int) -> bool:
601
679
  """체인지 리스트를 되돌리고 삭제합니다.
602
680
 
@@ -771,6 +849,40 @@ class Perforce:
771
849
  self._handle_p4_exception(e, f"파일/폴더 업데이트 필요 여부 확인 ({processed_paths})")
772
850
  return False
773
851
 
852
+ def is_file_in_perforce(self, file_path: str) -> bool:
853
+ """파일이 Perforce에 속하는지 확인합니다.
854
+
855
+ Args:
856
+ file_path (str): 확인할 파일 경로
857
+
858
+ Returns:
859
+ bool: 파일이 Perforce에 속하면 True, 아니면 False
860
+ """
861
+ if not self._is_connected():
862
+ return False
863
+
864
+ logger.debug(f"파일 '{file_path}'가 Perforce에 속하는지 확인 중...")
865
+ try:
866
+ # p4 files 명령으로 파일 정보 조회
867
+ file_info = self.p4.run_files(file_path)
868
+
869
+ # 파일 정보가 있고, 'no such file(s)' 오류가 없는 경우
870
+ if file_info and not any("no such file(s)" in str(err).lower() for err in self.p4.errors):
871
+ logger.info(f"파일 '{file_path}'가 Perforce에 존재합니다.")
872
+ return True
873
+ else:
874
+ logger.info(f"파일 '{file_path}'가 Perforce에 존재하지 않습니다.")
875
+ return False
876
+
877
+ except P4Exception as e:
878
+ # 파일이 존재하지 않는 경우는 일반적인 상황이므로 경고 레벨로 로깅
879
+ if any("no such file(s)" in err.lower() for err in self.p4.errors):
880
+ logger.info(f"파일 '{file_path}'가 Perforce에 존재하지 않습니다.")
881
+ return False
882
+ else:
883
+ self._handle_p4_exception(e, f"파일 '{file_path}' Perforce 존재 여부 확인")
884
+ return False
885
+
774
886
  def sync_files(self, file_paths: list) -> bool:
775
887
  """파일이나 폴더를 동기화합니다.
776
888
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyjallib
3
- Version: 0.1.12
3
+ Version: 0.1.14
4
4
  Summary: A utility library for 3D game character development pipelines.
5
5
  Author-email: Dongseok Kim <jalnagakds@gmail.com>
6
6
  Requires-Python: >=3.10
@@ -1,34 +1,38 @@
1
- pyjallib/__init__.py,sha256=4EmbUX3I3DLJ3DpGz7kBAADCFskvffdUEPd3HIu7-9k,509
1
+ pyjallib/__init__.py,sha256=or1aolPeqzUTuj7mEsvepJ3vXibO18C4SA60Ypeahu0,509
2
2
  pyjallib/namePart.py,sha256=lKIiOVkWrtAW-D3nuv--vHmdAnlQeVPaXLYUDhcr8QU,24177
3
3
  pyjallib/nameToPath.py,sha256=aBeezepLYdpv3VYxnQ2c4ZWzz2WjticXjkdbAIlVa1k,4676
4
4
  pyjallib/naming.py,sha256=b2C-P9VWV4Q2StqkizEwABblYOC5g6sXHzN0KpOZ_Ys,37419
5
5
  pyjallib/namingConfig.py,sha256=QGpK5mCnRiclKqNKz3GJ2PeJO8fbVitAEdqWwnwo8oA,34127
6
- pyjallib/perforce.py,sha256=XcF-YG250bh3obOkajPzMNwaJcCmXKIHIlJIEEFw6JE,46459
6
+ pyjallib/perforce.py,sha256=Z3hMO-3RMFIiNX0kxV2VOhu-euM0k5nLEtuu9QWp410,52605
7
7
  pyjallib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  pyjallib/reloadModules.py,sha256=RAEG3IxzJ0TlsjvnZwJt56JOkc2j8voqAnRbfQuZ44g,1151
9
9
  pyjallib/ConfigFiles/namingConfig.json,sha256=Ov4bbVJb6qodPaooU63e11YUMGXXPWFAA4AQq1sLBYU,1486
10
- pyjallib/max/__init__.py,sha256=sLF07So7OcCccPOWixSjIkAhuye37-buK-1I5Hp8F4U,1537
10
+ pyjallib/max/__init__.py,sha256=UCvMt5FFWYQz2-ifPyI4ExwqxpPHx5WJEUDREW-QLwc,1649
11
11
  pyjallib/max/align.py,sha256=HKjCViQCuicGmtvHB6xxVv4BEGEBGtV2gO3NvR_6R2A,5183
12
- pyjallib/max/anim.py,sha256=QTpR8T047IMpV40wnMMNo080wY9rHMV9k7ISrh4P61I,26083
13
- pyjallib/max/autoClavicle.py,sha256=Iga8bWUhRabfFePObdwGJSshp5Gww1Jv1Hwcul_y0V4,9559
14
- pyjallib/max/bip.py,sha256=kEneUnYph_GQXY1oojmzpJOV9bAYNBKagE0Ht3EGZ7o,30199
15
- pyjallib/max/bone.py,sha256=XVtX6e5UbMcGaOqz5UeoMEpQNMfbyQWDNM-UuS1CCUA,50019
16
- pyjallib/max/boneChain.py,sha256=weuOmGk7Y7i-0QNCr7G2hRPOecb5xmErshqpmXtikI0,5660
12
+ pyjallib/max/anim.py,sha256=scfxLSSXfFAlxgFi_qePrbONYc4mpKB8mXIxtYtFtl0,29032
13
+ pyjallib/max/autoClavicle.py,sha256=m5rs313-qkaHfs9TUr98HYRVFdB25E_fW-YiY4o9zhE,9559
14
+ pyjallib/max/bip.py,sha256=uhnNwt9EOP1ezPEBf7uIAqveBh3afzXbgr2ydJzTo0U,31471
15
+ pyjallib/max/bone.py,sha256=ImFUqdUoKeXD44rYsccJdgLClbqazraGmsv66I-tyGI,52923
16
+ pyjallib/max/boneChain.py,sha256=qTvbnJVuBchOdOBhi8wPxPClQ-5Wbkhc7Y2H47nUjCc,5740
17
17
  pyjallib/max/constraint.py,sha256=93g-X0aZHtZMKXVKr8xMjIhbKou61yc2b3ubQKJquBs,40589
18
- pyjallib/max/groinBone.py,sha256=nLR8rgWl6Vt4qQPZKY_jDgTRNF9RCnst49iR2JdWEMs,9144
19
- pyjallib/max/header.py,sha256=nuNCVfm5bfYMS0KxB8IRR67D30CXXHRUXHfFYkLG0jU,4120
18
+ pyjallib/max/fbxHandler.py,sha256=rVcnxZh5_Cu012wKIFgiGO_XvZF07Oy3jezxUhIpmSo,8703
19
+ pyjallib/max/groinBone.py,sha256=yBexwDTrnXViP8DRACIMnnpJWErbn9RIA61_d3iADCc,9193
20
+ pyjallib/max/header.py,sha256=miL-dCC1z1syugn9_L-DN0HbdPwTdcvJS0e_UlT8H5A,4199
20
21
  pyjallib/max/helper.py,sha256=Na3jFRwLsjHh4rz0Tk_r_CwHQxOA6n8LhDRA9x5xcSk,18018
21
- pyjallib/max/hip.py,sha256=OXMS_bBJUYVKT-aZoJ2YCbbS9eQStwMOkXfA22t-PKw,12696
22
- pyjallib/max/kneeBone.py,sha256=P5vDX1MFMbDy4DEI1LsT05a2Z62rqgr_FqGBxl2yEeY,24397
23
- pyjallib/max/layer.py,sha256=e9Mn8h7xf0oBYST3QIpyBpLMl8qpWTExO9Y6yH6rKc0,8795
22
+ pyjallib/max/hip.py,sha256=RavoZgK7zP2sXDa4A8CXEbHB6g8MQ-604XryZbStx0E,12684
23
+ pyjallib/max/kneeBone.py,sha256=cs5bCZtHxgLf6u80er1rV_PBF_SizqRgcTjYmy1q3IM,25316
24
+ pyjallib/max/layer.py,sha256=luDAzWzXXPiRq7v5lm-xv0Vd8meEEe7HSoX6ZoKpvu0,8825
24
25
  pyjallib/max/link.py,sha256=J3z9nkP8ZxAh9yYhR16tjQFCJTCYZMSB0MGbSHfA7uI,2592
25
26
  pyjallib/max/mirror.py,sha256=TcbfZXSk-VJQstNqAmD6VGCqYBF9bMuJtFTg-6SiGdQ,14505
27
+ pyjallib/max/mocap.py,sha256=tD0yhan8GBYLBZtdpGBlYC-DzdQkT4gsbqeo5lpyQXU,12544
26
28
  pyjallib/max/morph.py,sha256=I8HRYx4NznL6GZL4CbT9iTv05SeaBW_mJJ4PzMxCBkw,12664
27
29
  pyjallib/max/name.py,sha256=DcJt2td-N7vfUGyWazdGTD4-0JW-noa7z5nwc6SHm6I,15337
30
+ pyjallib/max/rootMotion.py,sha256=H9kvh9dx4zLLBYuigGyN0XZsdGAF5d7YRBdaokZmCiw,31399
28
31
  pyjallib/max/select.py,sha256=HMJD2WNX3zVBEeYrj0UX2YXM3fHNItfw6UtQSItNsoU,9487
29
32
  pyjallib/max/skin.py,sha256=5mBzG2wSUxoGlkFeb9Ys8uUxOwuZRGeqUMTI9LiWWZU,41937
30
- pyjallib/max/twistBone.py,sha256=3fs8EzRH-TTt2Oypm4LGoR9QtNcno9Fe1OV5_gA9p4U,17049
31
- pyjallib/max/volumeBone.py,sha256=SfmxxqmozcDinRtfPsjdNOPDMcqDkHJoquaZZOLrn1g,14649
33
+ pyjallib/max/toolManager.py,sha256=ya6beAGzk1_culw4H7lBdr7klnS-Yl8mVGg13A41Q8A,3185
34
+ pyjallib/max/twistBone.py,sha256=VPZLDE5V6KEW-i5gAberggEeR2YRzLwsFfFQBT4AB1U,17295
35
+ pyjallib/max/volumeBone.py,sha256=xt58rhMeA1YQQl6_y3GPikYLJXTfyoASSsnT0Nt285E,14776
32
36
  pyjallib/max/ConfigFiles/3DSMaxNamingConfig.json,sha256=PBUYawCELG0aLNRdTh6j-Yka4eNdmpF4P8iRyp0Ngpg,3717
33
37
  pyjallib/max/ConfigFiles/Default_3DSMaxNamingConfig.json,sha256=PBUYawCELG0aLNRdTh6j-Yka4eNdmpF4P8iRyp0Ngpg,3717
34
38
  pyjallib/max/macro/jal_macro_align.py,sha256=_Iqwskz0i4AVlP_AhDrqHwhLml6zoDWkVAxPF3AKqEQ,4286
@@ -38,6 +42,6 @@ pyjallib/max/macro/jal_macro_helper.py,sha256=hd8e5x56aq7Qt0g-hP5bY0p-njVy8ja77_
38
42
  pyjallib/max/macro/jal_macro_link.py,sha256=E8i3z2xsrQiGDEz4Qoxc75hkpalzS95mOMcIic0J-Fc,1193
39
43
  pyjallib/max/macro/jal_macro_select.py,sha256=jeSFR_mqqudTTrZE1rU6qifJ4g441cYxXWcHPTWh1CU,2289
40
44
  pyjallib/max/ui/Container.py,sha256=QSk3oCqhfiR4aglSSkherRGAyPFqMRUt83L-0ENBz-s,5571
41
- pyjallib-0.1.12.dist-info/METADATA,sha256=0jeCReJS2g2RlmWhefBDeSPwlIoFDturmBEqrw_s3Y8,870
42
- pyjallib-0.1.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
- pyjallib-0.1.12.dist-info/RECORD,,
45
+ pyjallib-0.1.14.dist-info/METADATA,sha256=7Zzp4ny8Umo3ZNETlrKbLIbtYb5aJc_Um78xtTDlOYw,870
46
+ pyjallib-0.1.14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
47
+ pyjallib-0.1.14.dist-info/RECORD,,