pyjallib 0.1.17__py3-none-any.whl → 0.1.20__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/toolManager.py +22 -6
- pyjallib/max/twistBone.py +22 -19
- pyjallib/max/ui/__init__.py +10 -0
- 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.20.dist-info}/METADATA +1 -1
- {pyjallib-0.1.17.dist-info → pyjallib-0.1.20.dist-info}/RECORD +36 -29
- {pyjallib-0.1.17.dist-info → pyjallib-0.1.20.dist-info}/WHEEL +0 -0
@@ -47,8 +47,8 @@ class BoneNameDialog(QtWidgets.QDialog):
|
|
47
47
|
side_group = QtWidgets.QGroupBox("Side:")
|
48
48
|
side_layout = QtWidgets.QVBoxLayout()
|
49
49
|
self.side_none_radio = QtWidgets.QRadioButton("(None)")
|
50
|
-
self.side_l_radio = QtWidgets.QRadioButton("
|
51
|
-
self.side_r_radio = QtWidgets.QRadioButton("
|
50
|
+
self.side_l_radio = QtWidgets.QRadioButton(jal.name.get_name_part("Side").get_value_by_description("Left"))
|
51
|
+
self.side_r_radio = QtWidgets.QRadioButton(jal.name.get_name_part("Side").get_value_by_description("Right"))
|
52
52
|
self.side_none_radio.setChecked(True)
|
53
53
|
side_layout.addWidget(self.side_none_radio)
|
54
54
|
side_layout.addWidget(self.side_l_radio)
|
@@ -59,8 +59,8 @@ class BoneNameDialog(QtWidgets.QDialog):
|
|
59
59
|
front_group = QtWidgets.QGroupBox("Front:")
|
60
60
|
front_layout = QtWidgets.QVBoxLayout()
|
61
61
|
self.front_none_radio = QtWidgets.QRadioButton("(None)")
|
62
|
-
self.front_f_radio = QtWidgets.QRadioButton("
|
63
|
-
self.front_b_radio = QtWidgets.QRadioButton("
|
62
|
+
self.front_f_radio = QtWidgets.QRadioButton(jal.name.get_name_part("FrontBack").get_value_by_description("Forward"))
|
63
|
+
self.front_b_radio = QtWidgets.QRadioButton(jal.name.get_name_part("FrontBack").get_value_by_description("Backward"))
|
64
64
|
self.front_none_radio.setChecked(True)
|
65
65
|
front_layout.addWidget(self.front_none_radio)
|
66
66
|
front_layout.addWidget(self.front_f_radio)
|
@@ -136,16 +136,16 @@ class BoneNameDialog(QtWidgets.QDialog):
|
|
136
136
|
if self.side_none_radio.isChecked():
|
137
137
|
self.sideName = ""
|
138
138
|
elif self.side_l_radio.isChecked():
|
139
|
-
self.sideName = "
|
139
|
+
self.sideName = jal.name.get_name_part("Side").get_value_by_description("Left")
|
140
140
|
elif self.side_r_radio.isChecked():
|
141
|
-
self.sideName = "
|
141
|
+
self.sideName = jal.name.get_name_part("Side").get_value_by_description("Right")
|
142
142
|
|
143
143
|
if self.front_none_radio.isChecked():
|
144
144
|
self.frontBackName = ""
|
145
145
|
elif self.front_f_radio.isChecked():
|
146
|
-
self.frontBackName = "
|
146
|
+
self.frontBackName = jal.name.get_name_part("FrontBack").get_value_by_description("Forward")
|
147
147
|
elif self.front_b_radio.isChecked():
|
148
|
-
self.frontBackName = "
|
148
|
+
self.frontBackName = jal.name.get_name_part("FrontBack").get_value_by_description("Backward")
|
149
149
|
|
150
150
|
RealName = self.name_edit.text() if self.name_edit.text() != "" else "TempBone"
|
151
151
|
|
@@ -189,6 +189,190 @@ class BoneNameDialog(QtWidgets.QDialog):
|
|
189
189
|
self.boneNameSetted = False
|
190
190
|
self.reject()
|
191
191
|
|
192
|
+
class BoneSizeDialog(QtWidgets.QDialog):
|
193
|
+
def __init__(self, parent=QtWidgets.QWidget.find(rt.windows.getMAXHWND())):
|
194
|
+
super().__init__(parent)
|
195
|
+
self.setWindowTitle("Bone Size")
|
196
|
+
self.setMinimumWidth(300)
|
197
|
+
|
198
|
+
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
|
199
|
+
|
200
|
+
# UI 변수들 초기화
|
201
|
+
self.fin_on = True
|
202
|
+
self.fin_size = 2.0
|
203
|
+
self.bone_size = 4.0
|
204
|
+
self.bone_taper = 90.0
|
205
|
+
|
206
|
+
# 원래 본 모양 저장용 변수들
|
207
|
+
self.original_bone_shapes = {} # {본객체: 원래모양배열}
|
208
|
+
self.original_selection = [] # 원래 선택된 본들
|
209
|
+
|
210
|
+
# Layouts
|
211
|
+
main_layout = QtWidgets.QVBoxLayout(self)
|
212
|
+
fin_layout = QtWidgets.QHBoxLayout()
|
213
|
+
bone_layout = QtWidgets.QHBoxLayout()
|
214
|
+
button_layout = QtWidgets.QHBoxLayout()
|
215
|
+
|
216
|
+
# Fin On 체크박스와 Fin Size 스피너 (가로로 배치)
|
217
|
+
self.fin_on_checkbox = QtWidgets.QCheckBox("Fin On")
|
218
|
+
self.fin_on_checkbox.setChecked(self.fin_on)
|
219
|
+
fin_layout.addWidget(self.fin_on_checkbox)
|
220
|
+
|
221
|
+
fin_layout.addWidget(QtWidgets.QLabel("Fin Size:"))
|
222
|
+
|
223
|
+
self.fin_size_spinner = QtWidgets.QDoubleSpinBox()
|
224
|
+
self.fin_size_spinner.setRange(0.0, 100.0)
|
225
|
+
self.fin_size_spinner.setValue(self.fin_size)
|
226
|
+
self.fin_size_spinner.setDecimals(1)
|
227
|
+
self.fin_size_spinner.setSingleStep(0.1)
|
228
|
+
fin_layout.addWidget(self.fin_size_spinner)
|
229
|
+
|
230
|
+
# Bone Size와 Bone Taper 스피너 (가로로 배치)
|
231
|
+
bone_layout.addWidget(QtWidgets.QLabel("Bone Size:"))
|
232
|
+
|
233
|
+
self.bone_size_spinner = QtWidgets.QDoubleSpinBox()
|
234
|
+
self.bone_size_spinner.setRange(0.0, 100.0)
|
235
|
+
self.bone_size_spinner.setValue(self.bone_size)
|
236
|
+
self.bone_size_spinner.setDecimals(1)
|
237
|
+
self.bone_size_spinner.setSingleStep(0.1)
|
238
|
+
bone_layout.addWidget(self.bone_size_spinner)
|
239
|
+
|
240
|
+
bone_layout.addWidget(QtWidgets.QLabel("Bone Taper:"))
|
241
|
+
|
242
|
+
self.bone_taper_spinner = QtWidgets.QDoubleSpinBox()
|
243
|
+
self.bone_taper_spinner.setRange(0.0, 100.0)
|
244
|
+
self.bone_taper_spinner.setValue(self.bone_taper)
|
245
|
+
self.bone_taper_spinner.setDecimals(1)
|
246
|
+
self.bone_taper_spinner.setSingleStep(0.1)
|
247
|
+
bone_layout.addWidget(self.bone_taper_spinner)
|
248
|
+
|
249
|
+
# OK/Cancel Buttons
|
250
|
+
self.ok_button = QtWidgets.QPushButton("OK")
|
251
|
+
self.cancel_button = QtWidgets.QPushButton("Cancel")
|
252
|
+
|
253
|
+
# Arrange Buttons
|
254
|
+
button_layout.addStretch()
|
255
|
+
button_layout.addWidget(self.ok_button)
|
256
|
+
button_layout.addWidget(self.cancel_button)
|
257
|
+
|
258
|
+
# Add layouts to main layout
|
259
|
+
main_layout.addLayout(fin_layout)
|
260
|
+
main_layout.addLayout(bone_layout)
|
261
|
+
main_layout.addStretch()
|
262
|
+
main_layout.addLayout(button_layout)
|
263
|
+
|
264
|
+
# Connect signals
|
265
|
+
self.ok_button.clicked.connect(self.ok_clicked)
|
266
|
+
self.cancel_button.clicked.connect(self.cancel_clicked)
|
267
|
+
|
268
|
+
# Connect all relevant UI changes to the update method
|
269
|
+
self.fin_on_checkbox.toggled.connect(self.update_ui)
|
270
|
+
self.fin_size_spinner.valueChanged.connect(self.update_ui)
|
271
|
+
self.bone_size_spinner.valueChanged.connect(self.update_ui)
|
272
|
+
self.bone_taper_spinner.valueChanged.connect(self.update_ui)
|
273
|
+
|
274
|
+
# 초기 선택된 본들의 원래 모양 저장
|
275
|
+
self.save_original_bone_shapes()
|
276
|
+
|
277
|
+
self.update_ui() # Initial update
|
278
|
+
|
279
|
+
def save_original_bone_shapes(self):
|
280
|
+
"""선택된 본들의 원래 모양을 저장합니다."""
|
281
|
+
selObjs = rt.getCurrentSelection()
|
282
|
+
for item in selObjs:
|
283
|
+
if rt.classOf(item) == rt.BoneGeometry:
|
284
|
+
self.original_selection.append(item)
|
285
|
+
self.original_bone_shapes[item] = jal.bone.get_bone_shape(item)
|
286
|
+
|
287
|
+
def restore_original_bone_shapes(self):
|
288
|
+
"""저장된 원래 모양으로 본들을 되돌립니다."""
|
289
|
+
for item, shape_array in self.original_bone_shapes.items():
|
290
|
+
if rt.isValidObj(item):
|
291
|
+
jal.bone.pasete_bone_shape(item, shape_array)
|
292
|
+
rt.redrawViews()
|
293
|
+
|
294
|
+
def update_ui(self):
|
295
|
+
"""UI 값들을 업데이트합니다."""
|
296
|
+
self.fin_on = self.fin_on_checkbox.isChecked()
|
297
|
+
self.fin_size = self.fin_size_spinner.value()
|
298
|
+
self.bone_size = self.bone_size_spinner.value()
|
299
|
+
self.bone_taper = self.bone_taper_spinner.value()
|
300
|
+
|
301
|
+
# Fin On 상태에 따라 Fin Size 스피너 활성화/비활성화
|
302
|
+
self.fin_size_spinner.setEnabled(self.fin_on)
|
303
|
+
|
304
|
+
selObjs = rt.getCurrentSelection()
|
305
|
+
for item in selObjs:
|
306
|
+
if self.fin_on:
|
307
|
+
jal.bone.set_fin_on(item, inSize=self.fin_size)
|
308
|
+
else:
|
309
|
+
jal.bone.set_fin_off(item)
|
310
|
+
jal.bone.set_bone_size(item, self.bone_size)
|
311
|
+
jal.bone.set_bone_taper(item, self.bone_taper)
|
312
|
+
|
313
|
+
rt.redrawViews()
|
314
|
+
|
315
|
+
def ok_clicked(self):
|
316
|
+
"""OK 버튼 클릭 처리"""
|
317
|
+
self.update_ui()
|
318
|
+
self.accept()
|
319
|
+
|
320
|
+
def cancel_clicked(self):
|
321
|
+
"""Cancel 버튼 클릭 처리"""
|
322
|
+
# 원래 모양으로 되돌리기
|
323
|
+
self.restore_original_bone_shapes()
|
324
|
+
self.reject()
|
325
|
+
|
326
|
+
class TurnBoneDialog(QtWidgets.QDialog):
|
327
|
+
def __init__(self, parent=QtWidgets.QWidget.find(rt.windows.getMAXHWND())):
|
328
|
+
super().__init__(parent)
|
329
|
+
self.setWindowTitle("Turn Bone")
|
330
|
+
self.setMinimumWidth(300)
|
331
|
+
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
|
332
|
+
|
333
|
+
# 다이얼로그 크기 고정 (예: 220x80)
|
334
|
+
self.setFixedSize(220, 80)
|
335
|
+
|
336
|
+
# 메인 레이아웃 생성
|
337
|
+
main_layout = QtWidgets.QVBoxLayout(self)
|
338
|
+
|
339
|
+
# 버튼 레이아웃 (가로)
|
340
|
+
button_layout = QtWidgets.QHBoxLayout()
|
341
|
+
|
342
|
+
# 90, 45, 5 버튼 생성 및 크기 설정
|
343
|
+
self.btn_90 = QtWidgets.QPushButton("90")
|
344
|
+
self.btn_45 = QtWidgets.QPushButton("45")
|
345
|
+
self.btn_5 = QtWidgets.QPushButton("5")
|
346
|
+
for btn in [self.btn_90, self.btn_45, self.btn_5]:
|
347
|
+
btn.setMinimumWidth(50)
|
348
|
+
btn.setMaximumWidth(50)
|
349
|
+
btn.setMinimumHeight(20)
|
350
|
+
btn.setMaximumHeight(20)
|
351
|
+
button_layout.addWidget(btn)
|
352
|
+
|
353
|
+
# 버튼 레이아웃을 메인 레이아웃에 추가
|
354
|
+
main_layout.addLayout(button_layout)
|
355
|
+
|
356
|
+
# 버튼 클릭 이벤트 연결
|
357
|
+
self.btn_90.clicked.connect(lambda: self.turn_bone_by_angle(90))
|
358
|
+
self.btn_45.clicked.connect(lambda: self.turn_bone_by_angle(45))
|
359
|
+
self.btn_5.clicked.connect(lambda: self.turn_bone_by_angle(5))
|
360
|
+
|
361
|
+
def turn_bone_by_angle(self, inAngle):
|
362
|
+
# Ctrl 키가 눌려 있으면 각도를 음수로 변환
|
363
|
+
modifiers = QtWidgets.QApplication.keyboardModifiers()
|
364
|
+
if modifiers & QtCore.Qt.ControlModifier:
|
365
|
+
angle = -inAngle
|
366
|
+
else:
|
367
|
+
angle = inAngle
|
368
|
+
selObjs = rt.getCurrentSelection()
|
369
|
+
for item in selObjs:
|
370
|
+
if rt.classOf(item) == rt.BoneGeometry:
|
371
|
+
jal.bone.turn_bone(item, angle)
|
372
|
+
|
373
|
+
rt.redrawViews()
|
374
|
+
|
375
|
+
|
192
376
|
def jal_bone_on():
|
193
377
|
jal.bone.set_bone_on_selection()
|
194
378
|
|
@@ -275,6 +459,17 @@ def jal_bone_nub_create():
|
|
275
459
|
else:
|
276
460
|
jal.bone.create_nub_bone("Temp", 2)
|
277
461
|
|
462
|
+
def jal_bone_size_modify():
|
463
|
+
dialog = BoneSizeDialog()
|
464
|
+
dialog.show()
|
465
|
+
|
466
|
+
# 다이얼로그가 닫힐 때까지 대기하지 않고 즉시 반환
|
467
|
+
# 사용자가 다이얼로그를 닫으면 자동으로 정리됨
|
468
|
+
|
469
|
+
def jal_bone_turn():
|
470
|
+
dialog = TurnBoneDialog()
|
471
|
+
dialog.show()
|
472
|
+
|
278
473
|
# Register macroscripts
|
279
474
|
macroScript_Category = "jalTools"
|
280
475
|
|
@@ -358,3 +553,21 @@ rt.macros.new(
|
|
358
553
|
"Bone Nub Create",
|
359
554
|
"jal_bone_nub_create()"
|
360
555
|
)
|
556
|
+
|
557
|
+
rt.jal_bone_size_modify = jal_bone_size_modify
|
558
|
+
rt.macros.new(
|
559
|
+
macroScript_Category,
|
560
|
+
"jal_boneSizeModify",
|
561
|
+
"Bone Size Modify",
|
562
|
+
"Bone Size Modify",
|
563
|
+
"jal_bone_size_modify()"
|
564
|
+
)
|
565
|
+
|
566
|
+
rt.jal_bone_turn = jal_bone_turn
|
567
|
+
rt.macros.new(
|
568
|
+
macroScript_Category,
|
569
|
+
"jal_boneTurn",
|
570
|
+
"Bone Turn",
|
571
|
+
"Bone Turn",
|
572
|
+
"jal_bone_turn()"
|
573
|
+
)
|
@@ -11,6 +11,12 @@ def jal_collapse_const():
|
|
11
11
|
for selObj in selArray:
|
12
12
|
jal.constraint.collapse(selObj)
|
13
13
|
|
14
|
+
def jal_collapse_const_with_tcb_rot():
|
15
|
+
if rt.selection.count > 0:
|
16
|
+
selArray = rt.getCurrentSelection()
|
17
|
+
for selObj in selArray:
|
18
|
+
jal.constraint.collapse(selObj, inUseTCBRot=True)
|
19
|
+
|
14
20
|
def jal_pos_const():
|
15
21
|
if rt.selection.count > 1:
|
16
22
|
selArray = rt.getCurrentSelection()
|
@@ -35,6 +41,12 @@ def jal_ori_const():
|
|
35
41
|
|
36
42
|
jal.constraint.assign_rot_const_multi(oriObj, targetObjArray)
|
37
43
|
|
44
|
+
def jal_tcb_rot():
|
45
|
+
if rt.selection.count > 1:
|
46
|
+
selArray = rt.getCurrentSelection()
|
47
|
+
for item in selArray:
|
48
|
+
jal.constraint.assign_tcb_rot(item)
|
49
|
+
|
38
50
|
def jal_rot_script_const():
|
39
51
|
if rt.selection.count == 2:
|
40
52
|
selArray = rt.getCurrentSelection()
|
@@ -87,6 +99,15 @@ rt.macros.new(
|
|
87
99
|
"jal_collapse_const()"
|
88
100
|
)
|
89
101
|
|
102
|
+
rt.jal_collapse_const_with_tcb_rot = jal_collapse_const_with_tcb_rot
|
103
|
+
rt.macros.new(
|
104
|
+
macroScript_Category,
|
105
|
+
"jal_collapse_const_with_tcb_rot",
|
106
|
+
"Collapse Constraints with TCB Rot",
|
107
|
+
"Collapse Constraints with TCB Rot",
|
108
|
+
"jal_collapse_const_with_tcb_rot()"
|
109
|
+
)
|
110
|
+
|
90
111
|
rt.jal_pos_const = jal_pos_const
|
91
112
|
rt.macros.new(
|
92
113
|
macroScript_Category,
|
@@ -105,6 +126,15 @@ rt.macros.new(
|
|
105
126
|
"jal_ori_const()"
|
106
127
|
)
|
107
128
|
|
129
|
+
rt.jal_tcb_rot = jal_tcb_rot
|
130
|
+
rt.macros.new(
|
131
|
+
macroScript_Category,
|
132
|
+
"jal_tcb_rot",
|
133
|
+
"Constraint TCB Rot",
|
134
|
+
"Constraint TCB Rot",
|
135
|
+
"jal_tcb_rot()"
|
136
|
+
)
|
137
|
+
|
108
138
|
rt.jal_rot_script_const = jal_rot_script_const
|
109
139
|
rt.macros.new(
|
110
140
|
macroScript_Category,
|
pyjallib/max/mirror.py
CHANGED
@@ -251,18 +251,17 @@ class Mirror:
|
|
251
251
|
elif mAxis == 3:
|
252
252
|
axisFactor = [1, 1, -1] # z축 미러링
|
253
253
|
|
254
|
-
# 새 뼈대와
|
255
|
-
parents = []
|
254
|
+
# 새 뼈대와 원본-미러 매핑 정보 저장
|
256
255
|
created = []
|
256
|
+
bone_mapping = {} # 원본 뼈대 -> 미러 뼈대 매핑
|
257
257
|
|
258
258
|
# 시작점 위치 (미러링 중심) 설정
|
259
259
|
root = bones[0].transform.translation
|
260
260
|
|
261
|
-
# 정렬된 뼈대 순서대로 처리
|
261
|
+
# 정렬된 뼈대 순서대로 처리 (실제 뼈대만)
|
262
262
|
for i in range(len(bones)):
|
263
263
|
original = bones[i]
|
264
|
-
if rt.classOf(original) != rt.BoneGeometry: # 실제 뼈대가 아닌 경우
|
265
|
-
parents.append(None) # 부모 없음 표시
|
264
|
+
if rt.classOf(original) != rt.BoneGeometry: # 실제 뼈대가 아닌 경우 건너뛰기
|
266
265
|
continue
|
267
266
|
|
268
267
|
# 원본 뼈대의 시작점, 끝점, Z축 방향 가져오기
|
@@ -310,16 +309,24 @@ class Mirror:
|
|
310
309
|
reflection.wirecolor = original.wirecolor
|
311
310
|
|
312
311
|
created.append(reflection)
|
313
|
-
|
312
|
+
bone_mapping[original] = reflection
|
314
313
|
|
315
314
|
# 계층 구조 연결 (자식부터 상위로)
|
316
|
-
for i in range(len(created)-1,
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
315
|
+
for i in range(len(created)-1, -1, -1): # 인덱스 len(created)-1부터 0까지 역순으로 처리
|
316
|
+
original_bone = None
|
317
|
+
# 원본 뼈대 찾기
|
318
|
+
for orig, mirror in bone_mapping.items():
|
319
|
+
if mirror == created[i]:
|
320
|
+
original_bone = orig
|
321
|
+
break
|
322
|
+
|
323
|
+
if original_bone and original_bone.parent:
|
324
|
+
# 부모가 매핑에 있는지 확인
|
325
|
+
if original_bone.parent in bone_mapping:
|
326
|
+
created[i].parent = bone_mapping[original_bone.parent]
|
327
|
+
else:
|
328
|
+
# 부모가 미러링된 뼈대 중에 없으면 원본 부모 사용
|
329
|
+
created[i].parent = original_bone.parent
|
323
330
|
|
324
331
|
# 부모가 없는 뼈대는 위치 조정
|
325
332
|
for i in range(len(created)):
|
pyjallib/max/shoulder.py
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
"""
|
5
|
+
어깨 모듈
|
6
|
+
자동으로 어깨 본을 생성.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from pymxs import runtime as rt
|
10
|
+
|
11
|
+
# Import necessary service classes for default initialization
|
12
|
+
from .name import Name
|
13
|
+
from .anim import Anim
|
14
|
+
from .helper import Helper
|
15
|
+
from .bone import Bone
|
16
|
+
from .constraint import Constraint
|
17
|
+
from .bip import Bip
|
18
|
+
|
19
|
+
from .boneChain import BoneChain
|
20
|
+
|
21
|
+
class Shoulder:
|
22
|
+
def __init__(self, nameService=None, animService=None, helperService=None, boneService=None, constraintService=None, bipService=None):
|
23
|
+
self.name = nameService if nameService else Name()
|
24
|
+
self.anim = animService if animService else Anim()
|
25
|
+
self.helper = helperService if helperService else Helper(nameService=self.name)
|
26
|
+
self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim)
|
27
|
+
self.const = constraintService if constraintService else Constraint(nameService=self.name)
|
28
|
+
self.bip = bipService if bipService else Bip(nameService=self.name, animService=self.anim)
|
29
|
+
|
30
|
+
self.boneSize = 2.0
|
31
|
+
|
32
|
+
self.genBones = []
|
33
|
+
self.genHelpers = []
|
34
|
+
self.clavicle = None
|
35
|
+
self.upperArm = None
|
36
|
+
self.autoClavicle = None
|
37
|
+
self.upperArmTwist = None
|
38
|
+
self.twistWeight = 0.7
|
39
|
+
|
40
|
+
self.shoulderLookAtTargetPosConstExpression = ""
|
41
|
+
self.shoulderLookAtTargetPosConstExpression += "local clavicleDistance = distance clavicle upperArm\n"
|
42
|
+
self.shoulderLookAtTargetPosConstExpression += "local xPos = clavicleDistance * distanceDir\n"
|
43
|
+
self.shoulderLookAtTargetPosConstExpression += "[xPos, 0.0, 0.0]\n"
|
44
|
+
|
45
|
+
def reset(self):
|
46
|
+
self.genBones = []
|
47
|
+
self.genHelpers = []
|
48
|
+
self.clavicle = None
|
49
|
+
self.upperArm = None
|
50
|
+
self.autoClavicle = None
|
51
|
+
self.upperArmTwist = None
|
52
|
+
self.twistWeight = 0.7
|
53
|
+
|
54
|
+
return self
|
55
|
+
|
56
|
+
def create_bones(self, inClavicle, inUpperArm, inAutoClavicle, inUpperArmTwist, inTwistWeight=0.7):
|
57
|
+
if not rt.isValidNode(inClavicle) or not rt.isValidNode(inUpperArm) or not rt.isValidNode(inAutoClavicle) or not rt.isValidNode(inUpperArmTwist):
|
58
|
+
return False
|
59
|
+
|
60
|
+
clavicleLength = rt.distance(inClavicle, inUpperArm)
|
61
|
+
facingDirVec = inUpperArm.transform.position - inClavicle.transform.position
|
62
|
+
inObjXAxisVec = inClavicle.objectTransform.row1
|
63
|
+
distanceDir = 1.0 if rt.dot(inObjXAxisVec, facingDirVec) > 0 else -1.0
|
64
|
+
clavicleLength *= distanceDir
|
65
|
+
|
66
|
+
genBones = []
|
67
|
+
genHelpers = []
|
68
|
+
|
69
|
+
# 자동 쇄골 이름 생성 및 뼈대 생성
|
70
|
+
shoulderName = self.name.replace_name_part("RealName", inClavicle.name, "Shoulder")
|
71
|
+
if inClavicle.name[0].islower():
|
72
|
+
shoulderName = shoulderName.lower()
|
73
|
+
|
74
|
+
shoulderBone = self.bone.create_nub_bone(shoulderName, self.boneSize)
|
75
|
+
shoulderBone.name = self.name.remove_name_part("Nub", shoulderBone.name)
|
76
|
+
shoulderBone.transform = inUpperArm.transform
|
77
|
+
shoulderBone.parent = inAutoClavicle
|
78
|
+
genBones.append(shoulderBone)
|
79
|
+
|
80
|
+
# 어깨 타겟 포인트 생성
|
81
|
+
shoulderLookAtTarget = self.helper.create_point(self.name.replace_name_part("Type", shoulderName, self.name.get_name_part_value_by_description("Type", "Target")))
|
82
|
+
shoulderLookAtTarget.transform = inUpperArm.transform
|
83
|
+
shoulderLookAtTarget.parent = inUpperArm
|
84
|
+
shoulderLookAtTargetPosConst = self.const.assign_pos_script_controller(shoulderLookAtTarget)
|
85
|
+
shoulderLookAtTargetPosConst.addConstant("distanceDir", distanceDir)
|
86
|
+
shoulderLookAtTargetPosConst.addNode("clavicle", inClavicle)
|
87
|
+
shoulderLookAtTargetPosConst.addNode("upperArm", inUpperArm)
|
88
|
+
shoulderLookAtTargetPosConst.setExpression(self.shoulderLookAtTargetPosConstExpression)
|
89
|
+
genHelpers.append(shoulderLookAtTarget)
|
90
|
+
|
91
|
+
# 어깨 회전 헬퍼 포인트 생성
|
92
|
+
shoulderLookAtHelper = self.helper.create_point(self.name.replace_name_part("Type", shoulderName, self.name.get_name_part_value_by_description("Type", "LookAt")))
|
93
|
+
shoulderLookAtHelper.transform = inClavicle.transform
|
94
|
+
shoulderLookAtHelper.parent = inClavicle
|
95
|
+
shoulderLookAtHelperPosConst = self.const.assign_pos_const(shoulderLookAtHelper, inClavicle)
|
96
|
+
|
97
|
+
lookAtConst = self.const.assign_lookat(shoulderLookAtHelper, shoulderLookAtTarget)
|
98
|
+
lookAtConst.upnode_world = False
|
99
|
+
lookAtConst.pickUpNode = inClavicle
|
100
|
+
lookAtConst.lookat_vector_length = 0.0
|
101
|
+
if distanceDir < 0:
|
102
|
+
lookAtConst.target_axisFlip = True
|
103
|
+
genHelpers.append(shoulderLookAtHelper)
|
104
|
+
|
105
|
+
# 어깨 최종 회전 헬퍼 포인트 생성
|
106
|
+
shoulderRotHelper = self.helper.create_point(self.name.replace_name_part("Type", shoulderName, self.name.get_name_part_value_by_description("Type", "Rotation")))
|
107
|
+
shoulderRotHelper.transform = inUpperArm.transform
|
108
|
+
shoulderRotHelper.parent = inUpperArm
|
109
|
+
shoulderRotHelperPosConst = self.const.assign_pos_const(shoulderRotHelper, inUpperArm)
|
110
|
+
|
111
|
+
shoulderRotHelperRotConst = self.const.assign_rot_const_multi(shoulderRotHelper, [shoulderLookAtHelper, inUpperArmTwist])
|
112
|
+
shoulderRotHelperRotConst.setWeight(1, 100.0-inTwistWeight)
|
113
|
+
shoulderRotHelperRotConst.setWeight(2, inTwistWeight)
|
114
|
+
genHelpers.append(shoulderRotHelper)
|
115
|
+
|
116
|
+
# 어깨 최종 회전 오프셋 헬퍼 포인트 생성
|
117
|
+
shoulderRotOffsetHelepr = self.helper.create_point(self.name.replace_name_part("Type", shoulderName, self.name.get_name_part_value_by_description("Type", "Dummy")))
|
118
|
+
shoulderRotOffsetHelepr.transform = inUpperArm.transform
|
119
|
+
shoulderRotOffsetHelepr.parent = shoulderRotHelper
|
120
|
+
genHelpers.append(shoulderRotOffsetHelepr)
|
121
|
+
|
122
|
+
# 어깨 본 회전 설정
|
123
|
+
shoulderBoneRotConst = self.const.assign_rot_const(shoulderBone, shoulderRotOffsetHelepr)
|
124
|
+
|
125
|
+
self.genBones = genBones
|
126
|
+
self.genHelpers = genHelpers
|
127
|
+
self.clavicle = inClavicle
|
128
|
+
self.upperArm = inUpperArm
|
129
|
+
self.autoClavicle = inAutoClavicle
|
130
|
+
self.upperArmTwist = inUpperArmTwist
|
131
|
+
self.twistWeight = inTwistWeight
|
132
|
+
|
133
|
+
result = {
|
134
|
+
"Bones": genBones,
|
135
|
+
"Helpers": genHelpers,
|
136
|
+
"SourceBones": [inClavicle, inUpperArm, inAutoClavicle, inUpperArmTwist],
|
137
|
+
"Parameters": [inTwistWeight]
|
138
|
+
}
|
139
|
+
|
140
|
+
self.reset()
|
141
|
+
|
142
|
+
rt.redrawViews()
|
143
|
+
|
144
|
+
return BoneChain.from_result(result)
|
145
|
+
|
146
|
+
def create_bones_from_chain(self, inBoneChain: BoneChain):
|
147
|
+
if not inBoneChain or inBoneChain.is_empty():
|
148
|
+
return None
|
149
|
+
|
150
|
+
inBoneChain.delete()
|
151
|
+
|
152
|
+
# BoneChain에서 필요한 정보 추출
|
153
|
+
sourceBones = inBoneChain.sourceBones
|
154
|
+
parameters = inBoneChain.parameters
|
155
|
+
|
156
|
+
# 필수 소스 본 확인
|
157
|
+
if len(sourceBones) < 4 or not rt.isValidNode(sourceBones[0]) or not rt.isValidNode(sourceBones[1]) or not rt.isValidNode(sourceBones[2]) or not rt.isValidNode(sourceBones[3]):
|
158
|
+
return None
|
159
|
+
|
160
|
+
# 파라미터 가져오기 (또는 기본값 사용)
|
161
|
+
twistWeight = parameters[0] if len(parameters) > 0 else 0.7
|
162
|
+
|
163
|
+
# 쇄골 생성
|
164
|
+
inClavicle = sourceBones[0]
|
165
|
+
inUpperArm = sourceBones[1]
|
166
|
+
inAutoClavicle = sourceBones[2]
|
167
|
+
inUpperArmTwist = sourceBones[3]
|
168
|
+
|
169
|
+
# 새로운 쇄골 생성
|
170
|
+
return self.create_bones(inClavicle, inUpperArm, inAutoClavicle, inUpperArmTwist, twistWeight)
|
171
|
+
|
172
|
+
|
173
|
+
|
pyjallib/max/toolManager.py
CHANGED
@@ -10,11 +10,23 @@ from PySide2 import QtWidgets, QtCore
|
|
10
10
|
import gc
|
11
11
|
|
12
12
|
class ToolManager:
|
13
|
+
"""
|
14
|
+
3DS Max에서 실행되는 도구(툴) 인스턴스를 통합 관리하는 클래스입니다.
|
15
|
+
도구의 등록, 닫기, 표시 등 라이프사이클을 관리합니다.
|
16
|
+
"""
|
13
17
|
def __init__(self):
|
18
|
+
"""
|
19
|
+
ToolManager 인스턴스를 초기화합니다.
|
20
|
+
"""
|
14
21
|
self.tools = {} # {tool_class_name: [instances]} 형태로 관리
|
15
22
|
|
16
23
|
def register_tool(self, tool_instance):
|
17
|
-
"""
|
24
|
+
"""
|
25
|
+
도구 인스턴스를 등록합니다.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
tool_instance: 등록할 도구 인스턴스
|
29
|
+
"""
|
18
30
|
class_name = tool_instance.__class__.__name__
|
19
31
|
|
20
32
|
if class_name not in self.tools:
|
@@ -23,7 +35,12 @@ class ToolManager:
|
|
23
35
|
self.tools[class_name].append(tool_instance)
|
24
36
|
|
25
37
|
def close_tool_by_type(self, tool_class):
|
26
|
-
"""
|
38
|
+
"""
|
39
|
+
특정 클래스의 모든 도구 인스턴스를 닫고 정리합니다.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
tool_class: 닫을 도구의 클래스
|
43
|
+
"""
|
27
44
|
class_name = tool_class.__name__
|
28
45
|
|
29
46
|
if class_name not in self.tools:
|
@@ -66,13 +83,12 @@ class ToolManager:
|
|
66
83
|
def show_tool(self, tool_class, **kwargs):
|
67
84
|
"""
|
68
85
|
도구를 표시합니다. 중복 실행을 방지하고 항상 새 인스턴스를 생성합니다.
|
69
|
-
|
86
|
+
|
70
87
|
Args:
|
71
|
-
tool_class: 도구 클래스
|
88
|
+
tool_class: 표시할 도구 클래스
|
72
89
|
**kwargs: 도구 클래스 생성자에 전달할 인자들
|
73
|
-
|
74
90
|
Returns:
|
75
|
-
새로 생성된 도구 인스턴스
|
91
|
+
object: 새로 생성된 도구 인스턴스
|
76
92
|
"""
|
77
93
|
# 기존 인스턴스 모두 정리
|
78
94
|
self.close_tool_by_type(tool_class)
|