pyjallib 0.1.6__py3-none-any.whl → 0.1.8__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 CHANGED
@@ -6,7 +6,7 @@ pyjallib Package
6
6
  Python library for game character development pipeline.
7
7
  """
8
8
 
9
- __version__ = '0.1.6'
9
+ __version__ = '0.1.8'
10
10
 
11
11
  # reload_modules 함수를 패키지 레벨에서 사용 가능하게 함
12
12
  from .namePart import NamePart, NamePartType
@@ -34,8 +34,8 @@
34
34
  {
35
35
  "name": "Type",
36
36
  "predefinedValues": [
37
- "P",
38
37
  "Dum",
38
+ "P",
39
39
  "Exp",
40
40
  "IK",
41
41
  "T",
@@ -57,8 +57,8 @@
57
57
  ],
58
58
  "type": "PREFIX",
59
59
  "descriptions": [
60
- "Parent",
61
60
  "Dummy",
61
+ "Parent",
62
62
  "ExposeTM",
63
63
  "IK",
64
64
  "Target",
pyjallib/max/__init__.py CHANGED
@@ -24,10 +24,14 @@ from .link import Link
24
24
 
25
25
  from .bip import Bip
26
26
  from .skin import Skin
27
+ from .morph import Morph
27
28
 
28
29
  from .twistBone import TwistBone
29
30
  from .groinBone import GroinBone
30
31
  from .autoClavicle import AutoClavicle
32
+ from .volumePreserveBone import VolumePreserveBone
33
+
34
+ from .ui.Container import Container
31
35
 
32
36
  # 모듈 내보내기
33
37
  __all__ = [
@@ -44,7 +48,10 @@ __all__ = [
44
48
  'Link',
45
49
  'Bip',
46
50
  'Skin',
51
+ 'Morph',
47
52
  'TwistBone',
48
53
  'GroinBone',
49
- 'AutoClavicle'
54
+ 'AutoClavicle',
55
+ 'VolumePreserveBone',
56
+ 'Container'
50
57
  ]
@@ -120,3 +120,50 @@ class AutoClavicle:
120
120
  ikGoal.parent = autoClavicleRotHelper
121
121
 
122
122
  return autoClavicleBones
123
+
124
+ def get_bones(self, inClavicle, inUpperArm):
125
+ """
126
+ 자동 쇄골 뼈를 가져옵니다.
127
+
128
+ Args:
129
+ inClavicle: 쇄골 뼈 객체
130
+ inUpperArm: 상완 뼈 객체
131
+
132
+ Returns:
133
+ 자동 쇄골 뼈대 배열
134
+ """
135
+ clavicleChildren = [item for item in self.bone.get_every_children(inClavicle) if rt.classOf(item) == rt.BoneGeometry]
136
+ upperArmChildren = [item for item in self.bone.get_every_children(inUpperArm) if rt.classOf(item) == rt.BoneGeometry]
137
+ returnVal = []
138
+ for item in clavicleChildren:
139
+ if item not in returnVal:
140
+ returnVal.append(item)
141
+ for item in upperArmChildren:
142
+ if item not in returnVal:
143
+ returnVal.append(item)
144
+
145
+ return returnVal
146
+
147
+ def get_helpers(self, inClavicle, inUpperArm):
148
+ """
149
+ 자동 쇄골 헬퍼를 가져옵니다.
150
+
151
+ Args:
152
+ inClavicle: 쇄골 뼈 객체
153
+ inUpperArm: 상완 뼈 객체
154
+
155
+ Returns:
156
+ 자동 쇄골 헬퍼 배열
157
+ """
158
+ clavicleChildren = [item for item in self.bone.get_every_children(inClavicle) if rt.classOf(item) == rt.Point]
159
+ upperArmChildren = [item for item in self.bone.get_every_children(inUpperArm) if rt.classOf(item) == rt.Point]
160
+ returnVal = []
161
+ for item in clavicleChildren:
162
+ if item not in returnVal:
163
+ returnVal.append(item)
164
+ for item in upperArmChildren:
165
+ if item not in returnVal:
166
+ returnVal.append(item)
167
+
168
+ return returnVal
169
+
@@ -212,6 +212,8 @@ class Constraint:
212
212
  """
213
213
  for item in inTargetArray:
214
214
  self.assign_pos_const(inObj, item, keepInit=keepInit)
215
+
216
+ return self.get_pos_const(inObj)
215
217
 
216
218
  def add_target_to_pos_const(self, inObj, inTarget, inWeight):
217
219
  """
@@ -251,8 +253,36 @@ class Constraint:
251
253
  posList = self.assign_pos_list(inObj)
252
254
 
253
255
  # Position_XYZ 컨트롤러 할당
254
- rt.setPropertyController(posList, "Available", rt.Position_XYZ())
256
+ posXYZ = rt.Position_XYZ()
257
+ rt.setPropertyController(posList, "Available", posXYZ)
258
+ posList.setActive(posList.count)
259
+
260
+ return posXYZ
261
+
262
+ def assign_pos_script_controller(self, inObj):
263
+ """
264
+ 객체에 스크립트 기반 위치 컨트롤러를 할당.
265
+
266
+ Args:
267
+ inObj: 컨트롤러를 할당할 객체
268
+
269
+ Returns:
270
+ None
271
+ """
272
+ # 위치 컨트롤러가 리스트 형태가 아니면 변환
273
+ pos_controller = rt.getPropertyController(inObj.controller, "Position")
274
+ if rt.classOf(pos_controller) != rt.Position_list:
275
+ rt.setPropertyController(inObj.controller, "Position", rt.Position_list())
276
+
277
+ # 위치 리스트 컨트롤러 가져오기
278
+ posList = self.assign_pos_list(inObj)
279
+
280
+ # 스크립트 기반 위치 컨트롤러 할당
281
+ scriptPos = rt.Position_Script()
282
+ rt.setPropertyController(posList, "Available", scriptPos)
255
283
  posList.setActive(posList.count)
284
+
285
+ return scriptPos
256
286
 
257
287
  def get_rot_list_controller(self, inObj):
258
288
  """
@@ -392,6 +422,8 @@ class Constraint:
392
422
  """
393
423
  for item in inTargetArray:
394
424
  self.assign_rot_const(inObj, item, keepInit=keepInit)
425
+
426
+ return self.get_rot_const(inObj)
395
427
 
396
428
  def add_target_to_rot_const(self, inObj, inTarget, inWeight):
397
429
  """
@@ -431,8 +463,11 @@ class Constraint:
431
463
  rotList = self.assign_rot_list(inObj)
432
464
 
433
465
  # Euler_XYZ 컨트롤러 할당
434
- rt.setPropertyController(rotList, "Available", rt.Euler_XYZ())
466
+ eulerXYZ = rt.Euler_XYZ()
467
+ rt.setPropertyController(rotList, "Available", eulerXYZ)
435
468
  rotList.setActive(rotList.count)
469
+
470
+ return eulerXYZ
436
471
 
437
472
  def get_lookat(self, inObj):
438
473
  """
@@ -528,6 +563,8 @@ class Constraint:
528
563
  """
529
564
  for item in inTargetArray:
530
565
  self.assign_lookat(inObj, item, keepInit=keepInit)
566
+
567
+ return self.get_lookat(inObj)
531
568
 
532
569
  def assign_lookat_flipless(self, inObj, inTarget):
533
570
  """
pyjallib/max/header.py CHANGED
@@ -27,6 +27,9 @@ from .skin import Skin
27
27
  from .twistBone import TwistBone
28
28
  from .groinBone import GroinBone
29
29
  from .autoClavicle import AutoClavicle
30
+ from .volumePreserveBone import VolumePreserveBone
31
+
32
+ from .morph import Morph
30
33
 
31
34
  class Header:
32
35
  """
@@ -68,6 +71,11 @@ class Header:
68
71
  self.twistBone = TwistBone(nameService=self.name, animService=self.anim, constService=self.constraint, bipService=self.bip, boneService=self.bone)
69
72
  self.groinBone = GroinBone(nameService=self.name, animService=self.anim, helperService=self.helper, constService=self.constraint, bipService=self.bip, boneService=self.bone, twistBoneService=self.twistBone)
70
73
  self.autoClavicle = AutoClavicle(nameService=self.name, animService=self.anim, helperService=self.helper, boneService=self.bone, constraintService=self.constraint, bipService=self.bip)
74
+ self.volumePreserveBone = VolumePreserveBone(nameService=self.name, animService=self.anim, constService=self.constraint, boneService=self.bone, helperService=self.helper)
75
+
76
+ self.morph = Morph()
77
+
78
+ self.tools = []
71
79
 
72
80
  # 모듈 레벨에서 전역 인스턴스 생성
73
81
  jal = Header.get_instance()
pyjallib/max/helper.py CHANGED
@@ -264,6 +264,7 @@ class Helper:
264
264
  부모 헬퍼 생성
265
265
  """
266
266
  # 선택된 객체가 있는 경우에만 처리
267
+ returnHelpers = []
267
268
  if rt.selection.count > 0:
268
269
  selArray = rt.getCurrentSelection()
269
270
 
@@ -297,6 +298,11 @@ class Helper:
297
298
  # 부모 헬퍼로 이름 변경
298
299
  finalName = self.name.replace_name_part("Type", genPoint.name, self.get_name_by_type("Parent"))
299
300
  rt.setProperty(genPoint, "name", finalName)
301
+
302
+ returnHelpers.append(genPoint)
303
+
304
+ return returnHelpers
305
+
300
306
 
301
307
  def create_exp_tm(self):
302
308
  """
pyjallib/max/morph.py ADDED
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ 모프(Morph) 모듈 - 3ds Max용 모프 타겟 관련 기능 제공
6
+ 원본 MAXScript의 morph.ms를 Python으로 변환하였으며, pymxs 모듈 기반으로 구현됨
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+ from pymxs import runtime as rt
11
+
12
+
13
+ @dataclass
14
+ class MorphChannel:
15
+ """
16
+ 모프 채널 정보를 저장하는 데이터 클래스
17
+ """
18
+ index: int = 0
19
+ name: str = ""
20
+ hasData: bool = False
21
+
22
+
23
+ class Morph:
24
+ """
25
+ 모프(Morph) 관련 기능을 제공하는 클래스.
26
+ MAXScript의 _Morph 구조체 개념을 Python으로 재구현한 클래스이며,
27
+ 3ds Max의 기능들을 pymxs API를 통해 제어합니다.
28
+ """
29
+
30
+ def __init__(self):
31
+ """클래스 초기화"""
32
+ self.channelMaxViewNum = 100
33
+
34
+ def get_modifier_index(self, inObj):
35
+ """
36
+ 객체에서 Morpher 모디파이어의 인덱스를 찾음
37
+
38
+ Args:
39
+ inObj: 검색할 객체
40
+
41
+ Returns:
42
+ Morpher 모디파이어의 인덱스 (없으면 0)
43
+ """
44
+ returnVal = 0
45
+ if len(inObj.modifiers) > 0:
46
+ for i in range(len(inObj.modifiers)):
47
+ if rt.classOf(inObj.modifiers[i]) == rt.Morpher:
48
+ returnVal = i + 1 # MaxScript는 1부터 시작하므로 +1 추가
49
+
50
+ return returnVal
51
+
52
+ def get_modifier(self, inObj):
53
+ """
54
+ 객체에서 Morpher 모디파이어를 찾음
55
+
56
+ Args:
57
+ inObj: 검색할 객체
58
+
59
+ Returns:
60
+ Morpher 모디파이어 (없으면 None)
61
+ """
62
+ returnVal = None
63
+ modIndex = self.get_modifier_index(inObj)
64
+ if modIndex > 0:
65
+ returnVal = inObj.modifiers[modIndex - 1] # Python 인덱스는 0부터 시작하므로 -1 조정
66
+
67
+ return returnVal
68
+
69
+ def get_channel_num(self, inObj):
70
+ """
71
+ 객체의 Morpher에 있는 채널 수를 반환
72
+
73
+ Args:
74
+ inObj: 검색할 객체
75
+
76
+ Returns:
77
+ 모프 채널 수
78
+ """
79
+ returnVal = 0
80
+ morphMod = self.get_modifier(inObj)
81
+ if morphMod is not None:
82
+ morphChannelExistance = True
83
+ morphChannelCounter = 0
84
+
85
+ while morphChannelExistance:
86
+ for i in range(morphChannelCounter + 1, morphChannelCounter + self.channelMaxViewNum + 1):
87
+ if not rt.WM3_MC_HasData(morphMod, i):
88
+ returnVal = i - 1
89
+ morphChannelExistance = False
90
+ break
91
+
92
+ morphChannelCounter += self.channelMaxViewNum
93
+
94
+ return returnVal
95
+
96
+ def get_all_channel_info(self, inObj):
97
+ """
98
+ 객체의 모든 모프 채널 정보를 가져옴
99
+
100
+ Args:
101
+ inObj: 검색할 객체
102
+
103
+ Returns:
104
+ MorphChannel 객체의 리스트
105
+ """
106
+ returnVal = []
107
+ morphMod = self.get_modifier(inObj)
108
+
109
+ if morphMod is not None:
110
+ channelNum = self.get_channel_num(inObj)
111
+ if channelNum > 0:
112
+ for i in range(1, channelNum + 1):
113
+ tempChannel = MorphChannel()
114
+ tempChannel.index = i
115
+ tempChannel.hasData = rt.WM3_MC_HasData(morphMod, i)
116
+ tempChannel.name = rt.WM3_MC_GetName(morphMod, i)
117
+ returnVal.append(tempChannel)
118
+
119
+ return returnVal
120
+
121
+ def add_target(self, inObj, inTarget, inIndex):
122
+ """
123
+ 특정 인덱스에 모프 타겟 추가
124
+
125
+ Args:
126
+ inObj: 모프를 적용할 객체
127
+ inTarget: 타겟 객체
128
+ inIndex: 채널 인덱스
129
+
130
+ Returns:
131
+ 성공 여부 (True/False)
132
+ """
133
+ returnVal = False
134
+ morphMod = self.get_modifier(inObj)
135
+
136
+ if morphMod is not None:
137
+ rt.WM3_MC_BuildFromNode(morphMod, inIndex, inTarget)
138
+ returnVal = rt.WM3_MC_HasData(morphMod, inIndex)
139
+
140
+ return returnVal
141
+
142
+ def add_targets(self, inObj, inTargetArray):
143
+ """
144
+ 여러 타겟 객체를 순서대로 모프 채널에 추가
145
+
146
+ Args:
147
+ inObj: 모프를 적용할 객체
148
+ inTargetArray: 타겟 객체 배열
149
+ """
150
+ morphMod = self.get_modifier(inObj)
151
+
152
+ if morphMod is not None:
153
+ for i in range(len(inTargetArray)):
154
+ rt.WM3_MC_BuildFromNode(morphMod, i + 1, inTargetArray[i])
155
+
156
+ def get_all_channel_name(self, inObj):
157
+ """
158
+ 객체의 모든 모프 채널 이름을 가져옴
159
+
160
+ Args:
161
+ inObj: 검색할 객체
162
+
163
+ Returns:
164
+ 채널 이름 리스트
165
+ """
166
+ returnVal = []
167
+ channelArray = self.get_all_channel_info(inObj)
168
+
169
+ if len(channelArray) > 0:
170
+ returnVal = [item.name for item in channelArray]
171
+
172
+ return returnVal
173
+
174
+ def get_channel_name(self, inObj, inIndex):
175
+ """
176
+ 특정 인덱스의 모프 채널 이름을 가져옴
177
+
178
+ Args:
179
+ inObj: 검색할 객체
180
+ inIndex: 채널 인덱스
181
+
182
+ Returns:
183
+ 채널 이름 (없으면 빈 문자열)
184
+ """
185
+ returnVal = ""
186
+ channelArray = self.get_all_channel_info(inObj)
187
+
188
+ try:
189
+ if len(channelArray) > 0:
190
+ returnVal = channelArray[inIndex - 1].name
191
+ except:
192
+ returnVal = ""
193
+
194
+ return returnVal
195
+
196
+ def get_channelIndex(self, inObj, inName):
197
+ """
198
+ 채널 이름으로 모프 채널 인덱스를 가져옴
199
+
200
+ Args:
201
+ inObj: 검색할 객체
202
+ inName: 채널 이름
203
+
204
+ Returns:
205
+ 채널 인덱스 (없으면 0)
206
+ """
207
+ returnVal = 0
208
+ channelArray = self.get_all_channel_info(inObj)
209
+
210
+ if len(channelArray) > 0:
211
+ for item in channelArray:
212
+ if item.name == inName:
213
+ returnVal = item.index
214
+ break
215
+
216
+ return returnVal
217
+
218
+ def get_channel_value_by_name(self, inObj, inName):
219
+ """
220
+ 채널 이름으로 모프 채널 값을 가져옴
221
+
222
+ Args:
223
+ inObj: 검색할 객체
224
+ inName: 채널 이름
225
+
226
+ Returns:
227
+ 채널 값 (0.0 ~ 100.0)
228
+ """
229
+ returnVal = 0.0
230
+ channelIndex = self.get_channelIndex(inObj, inName)
231
+ morphMod = self.get_modifier(inObj)
232
+
233
+ if channelIndex > 0:
234
+ try:
235
+ returnVal = rt.WM3_MC_GetValue(morphMod, channelIndex)
236
+ except:
237
+ returnVal = 0.0
238
+
239
+ return returnVal
240
+
241
+ def get_channel_value_by_index(self, inObj, inIndex):
242
+ """
243
+ 인덱스로 모프 채널 값을 가져옴
244
+
245
+ Args:
246
+ inObj: 검색할 객체
247
+ inIndex: 채널 인덱스
248
+
249
+ Returns:
250
+ 채널 값 (0.0 ~ 100.0)
251
+ """
252
+ returnVal = 0
253
+ morphMod = self.get_modifier(inObj)
254
+
255
+ if morphMod is not None:
256
+ try:
257
+ returnVal = rt.WM3_MC_GetValue(morphMod, inIndex)
258
+ except:
259
+ returnVal = 0
260
+
261
+ return returnVal
262
+
263
+ def set_channel_value_by_name(self, inObj, inName, inVal):
264
+ """
265
+ 채널 이름으로 모프 채널 값을 설정
266
+
267
+ Args:
268
+ inObj: 모프를 적용할 객체
269
+ inName: 채널 이름
270
+ inVal: 설정할 값 (0.0 ~ 100.0)
271
+
272
+ Returns:
273
+ 성공 여부 (True/False)
274
+ """
275
+ returnVal = False
276
+ morphMod = self.get_modifier(inObj)
277
+ channelIndex = self.get_channelIndex(inObj, inName)
278
+
279
+ if channelIndex > 0:
280
+ try:
281
+ rt.WM3_MC_SetValue(morphMod, channelIndex, inVal)
282
+ returnVal = True
283
+ except:
284
+ returnVal = False
285
+
286
+ return returnVal
287
+
288
+ def set_channel_value_by_index(self, inObj, inIndex, inVal):
289
+ """
290
+ 인덱스로 모프 채널 값을 설정
291
+
292
+ Args:
293
+ inObj: 모프를 적용할 객체
294
+ inIndex: 채널 인덱스
295
+ inVal: 설정할 값 (0.0 ~ 100.0)
296
+
297
+ Returns:
298
+ 성공 여부 (True/False)
299
+ """
300
+ returnVal = False
301
+ morphMod = self.get_modifier(inObj)
302
+
303
+ if morphMod is not None:
304
+ try:
305
+ rt.WM3_MC_SetValue(morphMod, inIndex, inVal)
306
+ returnVal = True
307
+ except:
308
+ returnVal = False
309
+
310
+ return returnVal
311
+
312
+ def set_channel_name_by_name(self, inObj, inTargetName, inNewName):
313
+ """
314
+ 채널 이름을 이름으로 검색하여 변경
315
+
316
+ Args:
317
+ inObj: 모프를 적용할 객체
318
+ inTargetName: 대상 채널의 현재 이름
319
+ inNewName: 설정할 새 이름
320
+
321
+ Returns:
322
+ 성공 여부 (True/False)
323
+ """
324
+ returnVal = False
325
+ channelIndex = self.get_channelIndex(inObj, inTargetName)
326
+ morphMod = self.get_modifier(inObj)
327
+
328
+ if channelIndex > 0:
329
+ rt.WM3_MC_SetName(morphMod, channelIndex, inNewName)
330
+ returnVal = True
331
+
332
+ return returnVal
333
+
334
+ def set_channel_name_by_index(self, inObj, inIndex, inName):
335
+ """
336
+ 채널 이름을 인덱스로 검색하여 변경
337
+
338
+ Args:
339
+ inObj: 모프를 적용할 객체
340
+ inIndex: 대상 채널 인덱스
341
+ inName: 설정할 이름
342
+
343
+ Returns:
344
+ 성공 여부 (True/False)
345
+ """
346
+ returnVal = False
347
+ morphMod = self.get_modifier(inObj)
348
+
349
+ if morphMod is not None:
350
+ try:
351
+ rt.WM3_MC_SetName(morphMod, inIndex, inName)
352
+ returnVal = True
353
+ except:
354
+ returnVal = False
355
+
356
+ return returnVal
357
+
358
+ def reset_all_channel_value(self, inObj):
359
+ """
360
+ 모든 모프 채널 값을 0으로 리셋
361
+
362
+ Args:
363
+ inObj: 리셋할 객체
364
+ """
365
+ totalChannelNum = self.get_channel_num(inObj)
366
+
367
+ if totalChannelNum > 0:
368
+ for i in range(1, totalChannelNum + 1):
369
+ self.set_channel_value_by_index(inObj, i, 0.0)
370
+
371
+ def extract_morph_channel_geometry(self, obj, _feedback_=False):
372
+ """
373
+ 모프 채널의 기하학적 형태를 추출하여 개별 객체로 생성
374
+
375
+ Args:
376
+ obj: 추출 대상 객체
377
+ _feedback_: 피드백 메시지 출력 여부
378
+
379
+ Returns:
380
+ 추출된 객체 배열
381
+ """
382
+ extractedObjs = []
383
+ morphMod = self.get_modifier(obj)
384
+
385
+ if rt.IsValidMorpherMod(morphMod):
386
+ # 데이터가 있는 모든 채널 인덱스 수집
387
+ channels = [i for i in range(1, rt.WM3_NumberOfChannels(morphMod) + 1)
388
+ if rt.WM3_MC_HasData(morphMod, i)]
389
+
390
+ for i in channels:
391
+ channelName = rt.WM3_MC_GetName(morphMod, i)
392
+ rt.WM3_MC_SetValue(morphMod, i, 100.0)
393
+
394
+ objSnapshot = rt.snapshot(obj)
395
+ objSnapshot.name = channelName
396
+ extractedObjs.append(objSnapshot)
397
+
398
+ rt.WM3_MC_SetValue(morphMod, i, 0.0)
399
+
400
+ if _feedback_:
401
+ print(f" - FUNCTION - [ extract_morph_channel_geometry ] - Extracted ---- {objSnapshot.name} ---- successfully!!")
402
+ else:
403
+ if _feedback_:
404
+ print(f" - FUNCTION - [ extract_morph_channel_geometry ] - No valid morpher found on ---- {obj.name} ---- ")
405
+
406
+ return extractedObjs
pyjallib/max/name.py CHANGED
@@ -6,6 +6,8 @@
6
6
  3ds Max에 특화된 네이밍 기능 (pymxs 의존)
7
7
  """
8
8
 
9
+ import os
10
+
9
11
  from pymxs import runtime as rt
10
12
  from pyjallib.naming import Naming
11
13
  from pyjallib.namePart import NamePart, NamePartType
@@ -35,35 +37,9 @@ class Name(Naming):
35
37
  # 사용자가 지정한 설정 파일 사용
36
38
  self.load_from_config_file(configPath=configPath)
37
39
  else:
38
- # 설정 파일이 없는 경우, 기본 설정값으로 초기화
39
- # Base 부분 (PREFIX 타입)
40
- basePart = NamePart("Base", NamePartType.PREFIX,
41
- ["b", "Bip001"],
42
- ["SkinBone", "Biped"])
43
- # Type 부분 (PREFIX 타입)
44
- typePart = NamePart("Type", NamePartType.PREFIX,
45
- ["P", "Dum", "Exp", "IK", "T"],
46
- ["Parent", "Dummy", "ExposeTM", "IK", "Target"])
47
- # Side 부분 (PREFIX 타입)
48
- sidePart = NamePart("Side", NamePartType.PREFIX,
49
- ["L", "R"],
50
- ["Left", "Right"],
51
- True)
52
- # FrontBack 부분 (PREFIX 타입)
53
- frontBackPart = NamePart("FrontBack", NamePartType.PREFIX,
54
- ["F", "B"],
55
- ["Front", "Back"],
56
- True)
57
- # RealName 부분 (REALNAME 타입)
58
- realNamePart = NamePart("RealName", NamePartType.REALNAME, [], [])
59
- # Index 부분 (INDEX 타입)
60
- indexPart = NamePart("Index", NamePartType.INDEX, [], [])
61
- # Nub 부분 (SUFFIX 타입)
62
- nubPart = NamePart("Nub", NamePartType.SUFFIX,
63
- ["Nub"],
64
- ["Nub"])
65
- # 기본 순서대로 설정
66
- self._nameParts = [basePart, typePart, sidePart, frontBackPart, realNamePart, indexPart, nubPart]
40
+ configDir = os.path.join(os.path.dirname(__file__), "ConfigFiles")
41
+ nameConfigDir = os.path.join(configDir, "3DSMaxNamingConfig.json")
42
+ self.load_from_config_file(configPath=nameConfigDir)
67
43
 
68
44
  # NamePart 직접 액세스 메소드들
69
45
  # get_<NamePart 이름>_values 메소드들
@@ -0,0 +1,153 @@
1
+ """
2
+ PySide2 Collapsible Widget/ frameLayout
3
+
4
+ Origianlly created by: aronamao
5
+ on GitHub: https://github.com/aronamao/PySide2-Collapsible-Widget
6
+ """
7
+
8
+
9
+ from PySide2 import QtWidgets, QtGui, QtCore
10
+
11
+
12
+ class Header(QtWidgets.QWidget):
13
+ """Header class for collapsible group"""
14
+
15
+ def __init__(self, name, content_widget):
16
+ """Header Class Constructor to initialize the object.
17
+
18
+ Args:
19
+ name (str): Name for the header
20
+ content_widget (QtWidgets.QWidget): Widget containing child elements
21
+ """
22
+ super(Header, self).__init__()
23
+ self.content = content_widget
24
+
25
+ # Try to load icons from resources, use fallback if not available
26
+ self.expand_ico = QtGui.QPixmap(":teDownArrow.png")
27
+ self.collapse_ico = QtGui.QPixmap(":teRightArrow.png")
28
+
29
+ # Check if icons were loaded properly (not empty)
30
+ if self.expand_ico.isNull() or self.collapse_ico.isNull():
31
+ # Create fallback icons programmatically
32
+ self.expand_ico = self._create_arrow_icon("down")
33
+ self.collapse_ico = self._create_arrow_icon("right")
34
+
35
+ self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
36
+
37
+ stacked = QtWidgets.QStackedLayout(self)
38
+ stacked.setStackingMode(QtWidgets.QStackedLayout.StackAll)
39
+ background = QtWidgets.QLabel()
40
+ background.setStyleSheet("QLabel{ background-color: rgb(81, 81, 81); border-radius:2px}")
41
+
42
+ widget = QtWidgets.QWidget()
43
+ layout = QtWidgets.QHBoxLayout(widget)
44
+
45
+ self.icon = QtWidgets.QLabel()
46
+ self.icon.setPixmap(self.expand_ico)
47
+ layout.addWidget(self.icon)
48
+ layout.setContentsMargins(11, 0, 11, 0)
49
+
50
+ font = QtGui.QFont()
51
+ font.setBold(True)
52
+ label = QtWidgets.QLabel(name)
53
+ label.setFont(font)
54
+
55
+ layout.addWidget(label)
56
+ layout.addItem(QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding))
57
+
58
+ stacked.addWidget(widget)
59
+ stacked.addWidget(background)
60
+ background.setMinimumHeight(layout.sizeHint().height() * 1.5)
61
+
62
+ def mousePressEvent(self, *args):
63
+ """Handle mouse events, call the function to toggle groups"""
64
+ self.expand() if not self.content.isVisible() else self.collapse()
65
+
66
+ def expand(self):
67
+ self.content.setVisible(True)
68
+ self.icon.setPixmap(self.expand_ico)
69
+
70
+ def collapse(self):
71
+ self.content.setVisible(False)
72
+ self.icon.setPixmap(self.collapse_ico)
73
+
74
+ def _create_arrow_icon(self, direction):
75
+ """Create a fallback arrow icon when resource icons are not available.
76
+
77
+ Args:
78
+ direction (str): Direction of the arrow ('down' or 'right')
79
+
80
+ Returns:
81
+ QtGui.QPixmap: Created arrow icon
82
+ """
83
+ # Create a pixmap for the arrow
84
+ pixmap = QtGui.QPixmap(16, 16)
85
+ pixmap.fill(QtGui.QColor(0, 0, 0, 0)) # Transparent background
86
+
87
+ # Create a painter to draw the arrow
88
+ painter = QtGui.QPainter(pixmap)
89
+ painter.setRenderHint(QtGui.QPainter.Antialiasing)
90
+
91
+ # Set the pen and brush
92
+ pen = QtGui.QPen(QtGui.QColor(200, 200, 200))
93
+ painter.setPen(pen)
94
+ painter.setBrush(QtGui.QBrush(QtGui.QColor(200, 200, 200)))
95
+
96
+ # Draw the arrow based on direction
97
+ if direction == "down":
98
+ points = [QtCore.QPoint(4, 6), QtCore.QPoint(12, 6), QtCore.QPoint(8, 10)]
99
+ else: # right arrow
100
+ points = [QtCore.QPoint(6, 4), QtCore.QPoint(6, 12), QtCore.QPoint(10, 8)]
101
+
102
+ painter.drawPolygon(points)
103
+ painter.end()
104
+
105
+ return pixmap
106
+
107
+
108
+ class Container(QtWidgets.QWidget):
109
+ """Class for creating a collapsible group similar to how it is implement in Maya
110
+
111
+ Examples:
112
+ Simple example of how to add a Container to a QVBoxLayout and attach a QGridLayout
113
+
114
+ >>> layout = QtWidgets.QVBoxLayout()
115
+ >>> container = Container("Group")
116
+ >>> layout.addWidget(container)
117
+ >>> content_layout = QtWidgets.QGridLayout(container.contentWidget)
118
+ >>> content_layout.addWidget(QtWidgets.QPushButton("Button"))
119
+ """
120
+ def __init__(self, name, color_background=True):
121
+ """Container Class Constructor to initialize the object
122
+
123
+ Args:
124
+ name (str): Name for the header
125
+ color_background (bool): whether or not to color the background lighter like in maya
126
+ """
127
+ super(Container, self).__init__()
128
+ layout = QtWidgets.QVBoxLayout(self)
129
+ layout.setContentsMargins(0, 2, 0, 0)
130
+ layout.setSpacing(0)
131
+ self._content_widget = QtWidgets.QWidget()
132
+ if color_background:
133
+ self._content_widget.setStyleSheet('''
134
+ .QWidget{
135
+ background-color: rgb(81, 81, 81);
136
+ }
137
+ ''')
138
+ header = Header(name, self._content_widget)
139
+ layout.addWidget(header)
140
+ layout.addWidget(self._content_widget)
141
+
142
+ # assign header methods to instance attributes so they can be called outside of this class
143
+ self.collapse = header.collapse
144
+ self.expand = header.expand
145
+ self.toggle = header.mousePressEvent
146
+
147
+ @property
148
+ def contentWidget(self):
149
+ """Getter for the content widget
150
+
151
+ Returns: Content widget
152
+ """
153
+ return self._content_widget
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ 관절 부피 유지 본(Volume preserve Bone) 모듈 - 3ds Max용 관절의 부피를 유지하기 위해 추가되는 중간본들을 위한 모듈
6
+ """
7
+
8
+ from pymxs import runtime as rt
9
+
10
+ # Import necessary service classes for default initialization
11
+ from .name import Name
12
+ from .anim import Anim
13
+ from .constraint import Constraint
14
+ from .bone import Bone
15
+ from .helper import Helper
16
+
17
+ class VolumePreserveBone:
18
+ """
19
+ 관절 부피 유지 본(Volume preserve Bone) 클래스
20
+ 3ds Max에서 관절의 부피를 유지하기 위해 추가되는 중간본들을 위한 클래스
21
+ """
22
+ def __init__(self, nameService=None, animService=None, constService=None, boneService=None, helperService=None):
23
+ self.name = nameService if nameService else Name()
24
+ self.anim = animService if animService else Anim()
25
+ # Ensure dependent services use the potentially newly created instances
26
+ self.const = constService if constService else Constraint(nameService=self.name)
27
+ self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim)
28
+ self.helper = helperService if helperService else Helper(nameService=self.name)
29
+
30
+ def create_rot_helpers(self, inObj, inRotScale=0.5):
31
+ if rt.isValidNode(inObj) == False or rt.isValidNode(inObj.parent) == False:
32
+ return False
33
+
34
+ volName = self.name.get_RealName(inObj.name)
35
+ volName = volName + "Vol"
36
+
37
+ parentObj = inObj.parent
38
+
39
+ rt.select(inObj)
40
+ rotHelpers = self.helper.create_helper(make_two=True)
41
+ rotHelpers[0].parent = inObj
42
+ rotHelpers[1].parent = parentObj
43
+
44
+ rotHelpers[0].name = self.name.replace_RealName(rotHelpers[0].name, volName)
45
+ rotHelpers[1].name = self.name.replace_RealName(rotHelpers[1].name, volName + self.name.get_name_part_value_by_description("Type", "Target"))
46
+
47
+ rotConst = self.const.assign_rot_const_multi(rotHelpers[0], [inObj, rotHelpers[1]])
48
+ rotConst.setWeight(1, inRotScale * 100.0)
49
+ rotConst.setWeight(2, (1.0 - inRotScale) * 100.0)
50
+
51
+ return rotHelpers
52
+
53
+ def create_init_bone(self, inObj, inVolumeSize, inRotHelpers, inRotAxis="Z", inTransAxis="PosY", inTransScale=1.0):
54
+ if rt.isValidNode(inObj) == False or rt.isValidNode(inObj.parent) == False:
55
+ return False
56
+
57
+ returnVal = {
58
+ "Bones": [],
59
+ "Helpers": []
60
+ }
61
+
62
+ volName = self.name.get_RealName(inObj.name)
63
+ volName = volName + "Vol"
64
+
65
+ # ExposeTM을 사용하여 회전값을 가져오는 방법
66
+ # rt.select(inObj)
67
+ # expHelper = self.helper.create_exp_tm()[0]
68
+ # expHelper.parent = parentObj
69
+ # expHelper.exposeNode = rotHelpers[0]
70
+ # expHelper.useParent = False
71
+ # expHelper.localReferenceNode = rotHelpers[1]
72
+ # expHelper.eulerXOrder = 5
73
+ # expHelper.eulerYOrder = 5
74
+ # expHelper.eulerZOrder = 5
75
+
76
+ # expHelper.name = self.name.replace_RealName(expHelper.name, volName)
77
+
78
+ boneGenHelperA = rt.point()
79
+ boneGenHelperB = rt.point()
80
+ boneGenHelperA.transform = inObj.transform
81
+ boneGenHelperB.transform = inObj.transform
82
+
83
+ if inTransAxis == "PosX":
84
+ self.anim.move_local(boneGenHelperB, inVolumeSize, 0, 0)
85
+ elif inTransAxis == "NegX":
86
+ self.anim.move_local(boneGenHelperB, -inVolumeSize, 0, 0)
87
+ elif inTransAxis == "PosY":
88
+ self.anim.move_local(boneGenHelperB, 0, inVolumeSize, 0)
89
+ elif inTransAxis == "NegY":
90
+ self.anim.move_local(boneGenHelperB, 0, -inVolumeSize, 0)
91
+ elif inTransAxis == "PosZ":
92
+ self.anim.move_local(boneGenHelperB, 0, 0, inVolumeSize)
93
+ elif inTransAxis == "NegZ":
94
+ self.anim.move_local(boneGenHelperB, 0, 0, -inVolumeSize)
95
+
96
+ row = ""
97
+ sourceUpAxist = 0
98
+ upAxis = 0
99
+ if inRotAxis == "X":
100
+ row = "row1"
101
+ sourceUpAxist = 1
102
+ upAxis = 1
103
+ elif inRotAxis == "Y":
104
+ row = "row3"
105
+ sourceUpAxist = 2
106
+ upAxis = 2
107
+ elif inRotAxis == "Z":
108
+ row = "row2"
109
+ sourceUpAxist = 3
110
+ upAxis = 3
111
+
112
+ volumeBoneName = self.name.replace_RealName(self.name.get_string(inObj.name), volName + inRotAxis + inTransAxis)
113
+
114
+ volumeBones = self.bone.create_simple_bone(inVolumeSize, volumeBoneName)
115
+ volumeBones[0].transform = inObj.transform
116
+ lookatConst = self.const.assign_lookat(volumeBones[0], boneGenHelperB)
117
+ lookatConst.pickUpNode = inObj
118
+ lookatConst.upnode_world = False
119
+ lookatConst.StoUP_axis = sourceUpAxist
120
+ lookatConst.upnode_axis = upAxis
121
+ self.const.collapse(volumeBones[0])
122
+
123
+ rt.delete(boneGenHelperA)
124
+ rt.delete(boneGenHelperB)
125
+
126
+ volumeBones[0].parent = inRotHelpers[0]
127
+ rt.select(volumeBones[0])
128
+ parentHelper = self.helper.create_parent_helper()[0]
129
+
130
+ posConst = self.const.assign_pos_script_controller(volumeBones[0])
131
+ # posConst.AddNode("rotExp", expHelper)
132
+ posConst.AddNode("rotObj", inRotHelpers[0])
133
+ posConst.AddNode("rotParent", inRotHelpers[1])
134
+
135
+ posConstCode = f""
136
+ posConstCode += f"local parentXAxis = (normalize rotParent.objectTransform.{row})\n"
137
+ posConstCode += f"local rotObjXAxis = (normalize rotObj.objectTransform.{row})\n"
138
+ posConstCode += f"local rotAmount = (1.0 - (dot parentXAxis rotObjXAxis))/2.0\n"
139
+ posConstCode += f"local posX = rotAmount * {inVolumeSize*inTransScale}\n"
140
+ posConstCode += f"[posX, 0.0, 0.0]\n"
141
+
142
+ posConst.SetExpression(posConstCode)
143
+
144
+ returnVal["Bones"] = volumeBones
145
+ returnVal["Helpers"] = [parentHelper]
146
+
147
+ return returnVal
148
+
149
+ def create_bones(self,inObj, inVolumeSize, inRotAxises, inRotScale, inTransAxiese, inTransScales):
150
+ if rt.isValidNode(inObj) == False or rt.isValidNode(inObj.parent) == False:
151
+ return False
152
+
153
+ if not len(inTransAxiese) == len(inTransScales) == len(inTransAxiese) == len(inRotAxises):
154
+ return False
155
+
156
+ rotHelpers = self.create_rot_helpers(inObj, inRotScale=inRotScale)
157
+
158
+ returnVal = {
159
+ "Bones": [],
160
+ "Helpers": []
161
+ }
162
+
163
+ for i in range(len(inRotAxises)):
164
+ genResult = self.create_init_bone(inObj, inVolumeSize, rotHelpers, inRotAxises[i], inTransAxiese[i], inTransScales[i])
165
+ returnVal["Bones"].extend(genResult["Bones"])
166
+ returnVal["Helpers"].extend(genResult["Helpers"])
167
+
168
+ returnVal["Helpers"].insert(0, rotHelpers[0])
169
+ returnVal["Helpers"].insert(1, rotHelpers[1])
170
+
171
+ return returnVal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyjallib
3
- Version: 0.1.6
3
+ Version: 0.1.8
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,4 +1,4 @@
1
- pyjallib/__init__.py,sha256=Vn47aG6w0P4VzzOm7lnSp7G38YVsI40nTwg5CClgdR4,460
1
+ pyjallib/__init__.py,sha256=FD41BluMZ_gDt-gXhEAzIwNyWn_tokcUoZMKTpn_fN8,460
2
2
  pyjallib/namePart.py,sha256=D1hnFNnZbrNicAiW2ZUaIT0LU5pro3uFYrFYOEjt7_Y,24001
3
3
  pyjallib/nameToPath.py,sha256=61EWrc0Wc1K1Qsc4G6jewIccI0IHbiZWroRcU_lX1Wc,4664
4
4
  pyjallib/naming.py,sha256=jJ6w0n_nnUE2uZy_I4KFsMx95Ij3_KvSjhVvQQplxpw,36621
@@ -7,30 +7,33 @@ pyjallib/perforce.py,sha256=YnKAKlbBss4cl66R4nQbr-bsYjTneLzGYaZ187kOSpA,29872
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=JofFmmSDMs8hWSAnS3yKw9y_o0IniQiKfzLtVUI9v8w,888
10
+ pyjallib/max/__init__.py,sha256=1058Mvp_Ebf3DG_DVkAC4o_frQPq7F6-m-C-Ccme64k,1064
11
11
  pyjallib/max/align.py,sha256=HKjCViQCuicGmtvHB6xxVv4BEGEBGtV2gO3NvR_6R2A,5183
12
12
  pyjallib/max/anim.py,sha256=-shQeE0WeAeCefc8FoI63dNDLHhz0uzOJ4shp5AL_Cs,25135
13
- pyjallib/max/autoClavicle.py,sha256=rJtXVcTEksstaXF4s1i-0FwhVkPWJFRxlVq8Gyw45Rg,5792
13
+ pyjallib/max/autoClavicle.py,sha256=FQGFkucRu670TpkEUs8lQRJyNUykvtxF9rZiPjHoH3s,7517
14
14
  pyjallib/max/bip.py,sha256=m6eA-rg-MghYSxbzj-YXa0KJFPm1wiOsOqyJu76_hlY,16967
15
15
  pyjallib/max/bone.py,sha256=GYs3Uohc0AMkLWZAqZTc1DET-WDgZlvlkhSUaBeszvk,33161
16
- pyjallib/max/constraint.py,sha256=WXB6fJ0er4EIZBZ7voQK_bFeqtgjopCKCpwiBn3QyiI,40055
16
+ pyjallib/max/constraint.py,sha256=gVdGezkRmzWRtUZeAdLnWfmvhgy8HAhD_dXmyI4xvUw,41282
17
17
  pyjallib/max/groinBone.py,sha256=rEZQ7tAXgcZ3NedFacIyvfG07nI8rgF_pVufy-hnKuo,3830
18
- pyjallib/max/header.py,sha256=06rzzaOGDtxQGD4SwAFX8nlxNn8lP_gQ_qY0FGZYBFk,2736
19
- pyjallib/max/helper.py,sha256=s_rEhY1wNUrwyEN6osuUuOsnqFAQo8_X0Dcf3R89NY0,18526
18
+ pyjallib/max/header.py,sha256=s2no3jRenY4b-xjofKwV9TGbO9doE4KH8paeBQA7qko,3071
19
+ pyjallib/max/helper.py,sha256=WPoxSoHAFXmAPx_CRJFWBNXn4S2k04UK-1J9x6nIv84,18674
20
20
  pyjallib/max/layer.py,sha256=e9Mn8h7xf0oBYST3QIpyBpLMl8qpWTExO9Y6yH6rKc0,8795
21
21
  pyjallib/max/link.py,sha256=J3z9nkP8ZxAh9yYhR16tjQFCJTCYZMSB0MGbSHfA7uI,2592
22
22
  pyjallib/max/mirror.py,sha256=j8LnsXowyTINzvtWsvCNaDsQ6v7u2RjlY50R8v5JCuc,14517
23
- pyjallib/max/name.py,sha256=z_fclfutesFxhk2hVliqZGnRQauKcB8LJpRbKQ5cYAc,16703
23
+ pyjallib/max/morph.py,sha256=I8HRYx4NznL6GZL4CbT9iTv05SeaBW_mJJ4PzMxCBkw,12664
24
+ pyjallib/max/name.py,sha256=DcJt2td-N7vfUGyWazdGTD4-0JW-noa7z5nwc6SHm6I,15337
24
25
  pyjallib/max/select.py,sha256=HMJD2WNX3zVBEeYrj0UX2YXM3fHNItfw6UtQSItNsoU,9487
25
26
  pyjallib/max/skin.py,sha256=5mBzG2wSUxoGlkFeb9Ys8uUxOwuZRGeqUMTI9LiWWZU,41937
26
27
  pyjallib/max/twistBone.py,sha256=0Z-Z-OiTPM0fXAnzgol0tiY7Kms6-qrEuVCHsCVVbUc,20338
27
- pyjallib/max/ConfigFiles/3DSMaxNamingConfig.json,sha256=_gIsm80fGazA6jUsoBAVxukGNydj6MDgBgGmzb0WQa4,3717
28
+ pyjallib/max/volumePreserveBone.py,sha256=mTtbAOKRjkbVO-PQQMt6FZzAawtUMbwGgj8XO1n9SV8,7193
29
+ pyjallib/max/ConfigFiles/3DSMaxNamingConfig.json,sha256=PBUYawCELG0aLNRdTh6j-Yka4eNdmpF4P8iRyp0Ngpg,3717
28
30
  pyjallib/max/macro/jal_macro_align.py,sha256=t0gQjybkHYAvPtjT5uWjUQd1vsXIsxOkdYfhFWduxJU,4270
29
31
  pyjallib/max/macro/jal_macro_bone.py,sha256=QS492tskLrARGIFDmbKEwJw7122xJga0ZP5z8WVVRn8,12422
30
32
  pyjallib/max/macro/jal_macro_constraint.py,sha256=0gpfan7hXES9jhnpZVlxWtCbqUCyGxjtSmVMbp7nUh8,4160
31
33
  pyjallib/max/macro/jal_macro_helper.py,sha256=ODwAl4C6WK2UE45dqpfw90TgN5vDjLJvODvYeO8tj4o,12906
32
34
  pyjallib/max/macro/jal_macro_link.py,sha256=xkgcCX0fJw4vLfMYybtfUklT3dgcO0tHfpt2X9BfWLw,1193
33
35
  pyjallib/max/macro/jal_macro_select.py,sha256=-r24l84XmDEM4W6H0r1jOBErp3q0UxNrr0m9oAHSgkE,2289
34
- pyjallib-0.1.6.dist-info/METADATA,sha256=CcIpikrV2_YUnr8eiNH2Aggz2DKCsqWx_d81e5N9ORM,869
35
- pyjallib-0.1.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
- pyjallib-0.1.6.dist-info/RECORD,,
36
+ pyjallib/max/ui/Container.py,sha256=QSk3oCqhfiR4aglSSkherRGAyPFqMRUt83L-0ENBz-s,5571
37
+ pyjallib-0.1.8.dist-info/METADATA,sha256=7dGxma4w0wrSzOlLcLnI1dN0ha3emWufRpVDWxkCaXY,869
38
+ pyjallib-0.1.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
39
+ pyjallib-0.1.8.dist-info/RECORD,,