@littlecarlito/blorktools 0.50.4 → 0.51.0

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.
Files changed (114) hide show
  1. package/bin/cli.js +69 -0
  2. package/package.json +13 -7
  3. package/src/asset_debugger/axis-indicator/axis-indicator.css +6 -0
  4. package/src/asset_debugger/axis-indicator/axis-indicator.html +20 -0
  5. package/src/asset_debugger/axis-indicator/axis-indicator.js +822 -0
  6. package/src/asset_debugger/debugger-scene/debugger-scene.css +142 -0
  7. package/src/asset_debugger/debugger-scene/debugger-scene.html +80 -0
  8. package/src/asset_debugger/debugger-scene/debugger-scene.js +791 -0
  9. package/src/asset_debugger/header/header.css +73 -0
  10. package/src/asset_debugger/header/header.html +24 -0
  11. package/src/asset_debugger/header/header.js +224 -0
  12. package/src/asset_debugger/index.html +76 -0
  13. package/src/asset_debugger/landing-page/landing-page.css +396 -0
  14. package/src/asset_debugger/landing-page/landing-page.html +81 -0
  15. package/src/asset_debugger/landing-page/landing-page.js +611 -0
  16. package/src/asset_debugger/loading-splash/loading-splash.css +195 -0
  17. package/src/asset_debugger/loading-splash/loading-splash.html +22 -0
  18. package/src/asset_debugger/loading-splash/loading-splash.js +59 -0
  19. package/src/asset_debugger/loading-splash/preview-loading-splash.js +66 -0
  20. package/src/asset_debugger/main.css +14 -0
  21. package/src/asset_debugger/modals/examples-modal/examples-modal.css +41 -0
  22. package/src/asset_debugger/modals/examples-modal/examples-modal.html +18 -0
  23. package/src/asset_debugger/modals/examples-modal/examples-modal.js +111 -0
  24. package/src/asset_debugger/modals/examples-modal/examples.js +125 -0
  25. package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.css +452 -0
  26. package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.html +87 -0
  27. package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.js +675 -0
  28. package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.css +219 -0
  29. package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.html +20 -0
  30. package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.js +548 -0
  31. package/src/asset_debugger/modals/settings-modal/settings-modal.css +103 -0
  32. package/src/asset_debugger/modals/settings-modal/settings-modal.html +158 -0
  33. package/src/asset_debugger/modals/settings-modal/settings-modal.js +475 -0
  34. package/src/asset_debugger/panels/asset-panel/asset-panel.css +263 -0
  35. package/src/asset_debugger/panels/asset-panel/asset-panel.html +123 -0
  36. package/src/asset_debugger/panels/asset-panel/asset-panel.js +136 -0
  37. package/src/asset_debugger/panels/asset-panel/atlas-heading/atlas-heading.css +94 -0
  38. package/src/asset_debugger/panels/asset-panel/atlas-heading/atlas-heading.js +312 -0
  39. package/src/asset_debugger/panels/asset-panel/mesh-heading/mesh-heading.css +129 -0
  40. package/src/asset_debugger/panels/asset-panel/mesh-heading/mesh-heading.js +486 -0
  41. package/src/asset_debugger/panels/asset-panel/rig-heading/rig-heading.css +545 -0
  42. package/src/asset_debugger/panels/asset-panel/rig-heading/rig-heading.js +538 -0
  43. package/src/asset_debugger/panels/asset-panel/uv-heading/uv-heading.css +70 -0
  44. package/src/asset_debugger/panels/asset-panel/uv-heading/uv-heading.js +586 -0
  45. package/src/asset_debugger/panels/world-panel/world-panel.css +364 -0
  46. package/src/asset_debugger/panels/world-panel/world-panel.html +173 -0
  47. package/src/asset_debugger/panels/world-panel/world-panel.js +1891 -0
  48. package/src/asset_debugger/router.js +190 -0
  49. package/src/asset_debugger/util/animation/playback/animation-playback-controller.js +150 -0
  50. package/src/asset_debugger/util/animation/playback/animation-preview-controller.js +316 -0
  51. package/src/asset_debugger/util/animation/playback/css3d-bounce-controller.js +400 -0
  52. package/src/asset_debugger/util/animation/playback/css3d-reversal-controller.js +821 -0
  53. package/src/asset_debugger/util/animation/render/css3d-prerender-controller.js +696 -0
  54. package/src/asset_debugger/util/animation/render/debug-texture-factory.js +0 -0
  55. package/src/asset_debugger/util/animation/render/iframe2texture-render-controller.js +199 -0
  56. package/src/asset_debugger/util/animation/render/image2texture-prerender-controller.js +461 -0
  57. package/src/asset_debugger/util/animation/render/pbr-material-factory.js +82 -0
  58. package/src/asset_debugger/util/common.css +280 -0
  59. package/src/asset_debugger/util/data/animation-classifier.js +323 -0
  60. package/src/asset_debugger/util/data/duplicate-handler.js +20 -0
  61. package/src/asset_debugger/util/data/glb-buffer-manager.js +407 -0
  62. package/src/asset_debugger/util/data/glb-classifier.js +290 -0
  63. package/src/asset_debugger/util/data/html-formatter.js +76 -0
  64. package/src/asset_debugger/util/data/html-linter.js +276 -0
  65. package/src/asset_debugger/util/data/localstorage-manager.js +265 -0
  66. package/src/asset_debugger/util/data/mesh-html-manager.js +295 -0
  67. package/src/asset_debugger/util/data/string-serder.js +303 -0
  68. package/src/asset_debugger/util/data/texture-classifier.js +663 -0
  69. package/src/asset_debugger/util/data/upload/background-file-handler.js +292 -0
  70. package/src/asset_debugger/util/data/upload/dropzone-preview-controller.js +396 -0
  71. package/src/asset_debugger/util/data/upload/file-upload-manager.js +495 -0
  72. package/src/asset_debugger/util/data/upload/glb-file-handler.js +36 -0
  73. package/src/asset_debugger/util/data/upload/glb-preview-controller.js +317 -0
  74. package/src/asset_debugger/util/data/upload/lighting-file-handler.js +194 -0
  75. package/src/asset_debugger/util/data/upload/model-file-manager.js +104 -0
  76. package/src/asset_debugger/util/data/upload/texture-file-handler.js +166 -0
  77. package/src/asset_debugger/util/data/upload/zip-handler.js +686 -0
  78. package/src/asset_debugger/util/loaders/html2canvas-loader.js +107 -0
  79. package/src/asset_debugger/util/rig/bone-kinematics.js +403 -0
  80. package/src/asset_debugger/util/rig/rig-constraint-manager.js +618 -0
  81. package/src/asset_debugger/util/rig/rig-controller.js +612 -0
  82. package/src/asset_debugger/util/rig/rig-factory.js +628 -0
  83. package/src/asset_debugger/util/rig/rig-handle-factory.js +46 -0
  84. package/src/asset_debugger/util/rig/rig-label-factory.js +441 -0
  85. package/src/asset_debugger/util/rig/rig-mouse-handler.js +377 -0
  86. package/src/asset_debugger/util/rig/rig-state-manager.js +175 -0
  87. package/src/asset_debugger/util/rig/rig-tooltip-manager.js +267 -0
  88. package/src/asset_debugger/util/rig/rig-ui-factory.js +700 -0
  89. package/src/asset_debugger/util/scene/background-manager.js +284 -0
  90. package/src/asset_debugger/util/scene/camera-controller.js +243 -0
  91. package/src/asset_debugger/util/scene/css3d-debug-controller.js +406 -0
  92. package/src/asset_debugger/util/scene/css3d-frame-factory.js +113 -0
  93. package/src/asset_debugger/util/scene/css3d-scene-manager.js +529 -0
  94. package/src/asset_debugger/util/scene/glb-controller.js +208 -0
  95. package/src/asset_debugger/util/scene/lighting-manager.js +690 -0
  96. package/src/asset_debugger/util/scene/threejs-model-manager.js +437 -0
  97. package/src/asset_debugger/util/scene/threejs-preview-manager.js +207 -0
  98. package/src/asset_debugger/util/scene/threejs-preview-setup.js +478 -0
  99. package/src/asset_debugger/util/scene/threejs-scene-controller.js +286 -0
  100. package/src/asset_debugger/util/scene/ui-manager.js +107 -0
  101. package/src/asset_debugger/util/state/animation-state.js +128 -0
  102. package/src/asset_debugger/util/state/css3d-state.js +83 -0
  103. package/src/asset_debugger/util/state/glb-preview-state.js +31 -0
  104. package/src/asset_debugger/util/state/log-util.js +197 -0
  105. package/src/asset_debugger/util/state/scene-state.js +452 -0
  106. package/src/asset_debugger/util/state/threejs-state.js +54 -0
  107. package/src/asset_debugger/util/workers/lighting-worker.js +61 -0
  108. package/src/asset_debugger/util/workers/model-worker.js +109 -0
  109. package/src/asset_debugger/util/workers/texture-worker.js +54 -0
  110. package/src/asset_debugger/util/workers/worker-manager.js +212 -0
  111. package/src/asset_debugger/widgets/mesh-info-widget.js +280 -0
  112. package/src/index.html +261 -0
  113. package/src/index.js +8 -0
  114. package/vite.config.js +66 -0
@@ -0,0 +1,700 @@
1
+ import { jointPreviousValues } from "../../panels/asset-panel/rig-heading/rig-heading";
2
+ import { findBoneByName, lockedBones, toggleBoneLock } from "./bone-kinematics";
3
+ import { jointSettingsDebug, updateConstraintSettingsState } from "./rig-state-manager";
4
+
5
+ /**
6
+ * Add bone constraint controls to a bone item element
7
+ * @param {HTMLElement} itemElem - The bone item element
8
+ * @param {Object} item - The bone item data
9
+ * @param {Object} details - Rig details object
10
+ */
11
+ export function addBoneConstraintControls(itemElem, item, details) {
12
+ const boneName = item.name;
13
+ const bone = findBoneByName(boneName);
14
+
15
+ if (!bone) return;
16
+
17
+ const constraintContainer = document.createElement('div');
18
+ constraintContainer.className = 'rig-constraint-container';
19
+
20
+ const constraintLabel = document.createElement('label');
21
+ constraintLabel.className = 'rig-constraint-label';
22
+ constraintLabel.textContent = 'Constraint:';
23
+
24
+ const constraintSelect = document.createElement('select');
25
+ constraintSelect.className = 'rig-constraint-select';
26
+ constraintSelect.setAttribute('data-bone-constraint', 'true');
27
+ constraintSelect.setAttribute('data-bone-name', boneName);
28
+
29
+ const constraintOptions = [
30
+ { value: 'NONE', label: 'None' },
31
+ { value: 'FIXED_POSITION', label: 'Fixed Position' },
32
+ { value: 'SINGLE_AXIS_ROTATION', label: 'Single Axis Rotation' },
33
+ { value: 'LIMIT_ROTATION_XYZ', label: 'Limit Rotation (XYZ)' },
34
+ { value: 'DYNAMIC_SPRING', label: 'Dynamic Spring' }
35
+ ];
36
+
37
+ constraintOptions.forEach(option => {
38
+ const optionElem = document.createElement('option');
39
+ optionElem.value = option.value;
40
+ optionElem.textContent = option.label;
41
+ constraintSelect.appendChild(optionElem);
42
+ });
43
+
44
+ let initialConstraintType = 'NONE';
45
+
46
+ if (bone.userData && bone.userData.constraints) {
47
+ const constraintTypeMap = {
48
+ 'none': 'NONE',
49
+ 'fixed': 'FIXED_POSITION',
50
+ 'hinge': 'SINGLE_AXIS_ROTATION',
51
+ 'limitRotation': 'LIMIT_ROTATION_XYZ',
52
+ 'spring': 'DYNAMIC_SPRING'
53
+ };
54
+ initialConstraintType = constraintTypeMap[bone.userData.constraints.type] || 'NONE';
55
+ } else if (details.constraints) {
56
+ const existingConstraint = details.constraints.find(c =>
57
+ c.boneName === boneName || c.nodeName === boneName);
58
+ if (existingConstraint) {
59
+ const constraintTypeMap = {
60
+ 'none': 'NONE',
61
+ 'fixed': 'FIXED_POSITION',
62
+ 'hinge': 'SINGLE_AXIS_ROTATION',
63
+ 'limitRotation': 'LIMIT_ROTATION_XYZ',
64
+ 'spring': 'DYNAMIC_SPRING'
65
+ };
66
+ initialConstraintType = constraintTypeMap[existingConstraint.type] || 'NONE';
67
+ }
68
+ }
69
+
70
+ item.constraintType = initialConstraintType;
71
+
72
+ if (bone.userData && bone.userData.constraints) {
73
+ if (bone.userData.constraints.type === 'hinge') {
74
+ item.hingeAxis = bone.userData.hinge?.axis || 'y';
75
+ item.hingeMin = bone.userData.hinge?.min || -Math.PI/2;
76
+ item.hingeMax = bone.userData.hinge?.max || Math.PI/2;
77
+ } else if (bone.userData.constraints.type === 'limitRotation') {
78
+ item.rotationLimits = bone.userData.rotationLimits || {
79
+ x: { min: -Math.PI/4, max: Math.PI/4 },
80
+ y: { min: -Math.PI/4, max: Math.PI/4 },
81
+ z: { min: -Math.PI/4, max: Math.PI/4 }
82
+ };
83
+ } else if (bone.userData.constraints.type === 'spring') {
84
+ item.spring = {
85
+ stiffness: bone.userData.spring?.stiffness || 50,
86
+ damping: bone.userData.spring?.damping || 5
87
+ };
88
+ }
89
+ }
90
+
91
+ constraintSelect.value = initialConstraintType;
92
+ jointPreviousValues.set(boneName, initialConstraintType);
93
+
94
+ constraintSelect.addEventListener('change', () => {
95
+ item.constraintType = constraintSelect.value;
96
+
97
+ if (jointSettingsDebug) {
98
+ console.log(`Bone constraint ${boneName} changed to "${constraintSelect.value}"`);
99
+ }
100
+
101
+ const controlSelectors = [
102
+ '.rig-constraint-controls',
103
+ '.rig-axis-container',
104
+ '.rig-limits-container',
105
+ '.rig-rotation-limits-container',
106
+ '.rig-spring-container'
107
+ ];
108
+
109
+ controlSelectors.forEach(selector => {
110
+ const existingControls = itemElem.querySelectorAll(selector);
111
+ existingControls.forEach(control => {
112
+ itemElem.removeChild(control);
113
+ });
114
+ });
115
+
116
+ if (constraintSelect.value === 'SINGLE_AXIS_ROTATION') {
117
+ addHingeAxisSelector(itemElem, item);
118
+ } else if (constraintSelect.value === 'LIMIT_ROTATION_XYZ') {
119
+ addRotationLimitControls(itemElem, item);
120
+ } else if (constraintSelect.value === 'DYNAMIC_SPRING') {
121
+ addSpringControls(itemElem, item);
122
+ }
123
+
124
+ updateConstraintSettingsState();
125
+ });
126
+
127
+ constraintContainer.appendChild(constraintLabel);
128
+ constraintContainer.appendChild(constraintSelect);
129
+ itemElem.appendChild(constraintContainer);
130
+
131
+ if (initialConstraintType === 'SINGLE_AXIS_ROTATION') {
132
+ addHingeAxisSelector(itemElem, item);
133
+ } else if (initialConstraintType === 'LIMIT_ROTATION_XYZ') {
134
+ addRotationLimitControls(itemElem, item);
135
+ } else if (initialConstraintType === 'DYNAMIC_SPRING') {
136
+ addSpringControls(itemElem, item);
137
+ }
138
+
139
+ const lockContainer = document.createElement('div');
140
+ lockContainer.className = 'rig-lock-container';
141
+
142
+ const lockLabel = document.createElement('label');
143
+ lockLabel.className = 'rig-lock-label';
144
+ lockLabel.textContent = 'Lock Rotation:';
145
+
146
+ const lockCheckbox = document.createElement('input');
147
+ lockCheckbox.type = 'checkbox';
148
+ lockCheckbox.className = 'rig-lock-checkbox';
149
+ lockCheckbox.checked = lockedBones.has(bone.uuid);
150
+
151
+ lockCheckbox.addEventListener('change', (e) => {
152
+ toggleBoneLock(bone, e.target.checked);
153
+ });
154
+
155
+ lockContainer.appendChild(lockLabel);
156
+ lockContainer.appendChild(lockCheckbox);
157
+ itemElem.appendChild(lockContainer);
158
+ }
159
+
160
+ /**
161
+ * Add hinge axis selector to a joint item
162
+ * @param {HTMLElement} itemElem - The joint item element
163
+ * @param {Object} item - The joint data
164
+ */
165
+ function addHingeAxisSelector(itemElem, item) {
166
+ const axisContainer = document.createElement('div');
167
+ axisContainer.className = 'rig-axis-container rig-constraint-controls';
168
+
169
+ const axisLabel = document.createElement('label');
170
+ axisLabel.className = 'rig-axis-label';
171
+ axisLabel.textContent = 'Locked Axis:';
172
+
173
+ const axisSelect = document.createElement('select');
174
+ axisSelect.className = 'rig-axis-select';
175
+
176
+ const axes = [
177
+ { value: 'x', label: 'X Axis' },
178
+ { value: 'y', label: 'Y Axis' },
179
+ { value: 'z', label: 'Z Axis' }
180
+ ];
181
+
182
+ axes.forEach(axis => {
183
+ const option = document.createElement('option');
184
+ option.value = axis.value;
185
+ option.textContent = axis.label;
186
+ axisSelect.appendChild(option);
187
+ });
188
+
189
+ // Set initial value or default to Y
190
+ if (!item.hingeAxis) {
191
+ item.hingeAxis = 'y';
192
+ }
193
+ axisSelect.value = item.hingeAxis;
194
+
195
+ axisSelect.addEventListener('change', () => {
196
+ item.hingeAxis = axisSelect.value;
197
+ updateConstraintSettingsState();
198
+ });
199
+
200
+ axisContainer.appendChild(axisLabel);
201
+ axisContainer.appendChild(axisSelect);
202
+ itemElem.appendChild(axisContainer);
203
+
204
+ // Add min/max angle inputs
205
+ const limitsContainer = document.createElement('div');
206
+ limitsContainer.className = 'rig-limits-container';
207
+
208
+ // Min angle
209
+ const minContainer = document.createElement('div');
210
+ minContainer.className = 'rig-min-container';
211
+
212
+ const minLabel = document.createElement('label');
213
+ minLabel.textContent = 'Min Angle:';
214
+ minLabel.className = 'rig-min-label';
215
+
216
+ const minControlWrapper = document.createElement('div');
217
+ minControlWrapper.className = 'rig-angle-control-wrapper';
218
+
219
+ // Add decrement button
220
+ const minDecBtn = document.createElement('button');
221
+ minDecBtn.className = 'rig-angle-btn';
222
+ minDecBtn.textContent = '−';
223
+ minDecBtn.type = 'button';
224
+
225
+ const minInput = document.createElement('input');
226
+ minInput.type = 'number';
227
+ minInput.className = 'rig-min-input';
228
+ minInput.min = -180;
229
+ minInput.max = 180;
230
+ minInput.step = 5;
231
+ minInput.value = item.hingeMin ? Math.round(item.hingeMin * 180 / Math.PI) : -90;
232
+
233
+ // Add increment button
234
+ const minIncBtn = document.createElement('button');
235
+ minIncBtn.className = 'rig-angle-btn';
236
+ minIncBtn.textContent = '+';
237
+ minIncBtn.type = 'button';
238
+
239
+ // Update item data when input changes
240
+ minInput.addEventListener('change', () => {
241
+ item.hingeMin = minInput.value * Math.PI / 180;
242
+ updateConstraintSettingsState();
243
+ });
244
+
245
+ // Button event listeners
246
+ minDecBtn.addEventListener('click', () => {
247
+ minInput.value = parseInt(minInput.value) - parseInt(minInput.step);
248
+ // Trigger the change event manually
249
+ minInput.dispatchEvent(new Event('change'));
250
+ // Explicitly update constraint settings state for button click
251
+ updateConstraintSettingsState();
252
+ });
253
+
254
+ minIncBtn.addEventListener('click', () => {
255
+ minInput.value = parseInt(minInput.value) + parseInt(minInput.step);
256
+ // Trigger the change event manually
257
+ minInput.dispatchEvent(new Event('change'));
258
+ // Explicitly update constraint settings state for button click
259
+ updateConstraintSettingsState();
260
+ });
261
+
262
+ minControlWrapper.appendChild(minDecBtn);
263
+ minControlWrapper.appendChild(minInput);
264
+ minControlWrapper.appendChild(minIncBtn);
265
+
266
+ minContainer.appendChild(minLabel);
267
+ minContainer.appendChild(minControlWrapper);
268
+
269
+ // Max angle
270
+ const maxContainer = document.createElement('div');
271
+ maxContainer.className = 'rig-max-container';
272
+
273
+ const maxLabel = document.createElement('label');
274
+ maxLabel.textContent = 'Max Angle:';
275
+ maxLabel.className = 'rig-max-label';
276
+
277
+ const maxControlWrapper = document.createElement('div');
278
+ maxControlWrapper.className = 'rig-angle-control-wrapper';
279
+
280
+ // Add decrement button
281
+ const maxDecBtn = document.createElement('button');
282
+ maxDecBtn.className = 'rig-angle-btn';
283
+ maxDecBtn.textContent = '−';
284
+ maxDecBtn.type = 'button';
285
+
286
+ const maxInput = document.createElement('input');
287
+ maxInput.type = 'number';
288
+ maxInput.className = 'rig-max-input';
289
+ maxInput.min = -180;
290
+ maxInput.max = 180;
291
+ maxInput.step = 5;
292
+ maxInput.value = item.hingeMax ? Math.round(item.hingeMax * 180 / Math.PI) : 90;
293
+
294
+ // Add increment button
295
+ const maxIncBtn = document.createElement('button');
296
+ maxIncBtn.className = 'rig-angle-btn';
297
+ maxIncBtn.textContent = '+';
298
+ maxIncBtn.type = 'button';
299
+
300
+ maxInput.addEventListener('change', () => {
301
+ item.hingeMax = maxInput.value * Math.PI / 180;
302
+ updateConstraintSettingsState();
303
+ });
304
+
305
+ // Button event listeners
306
+ maxDecBtn.addEventListener('click', () => {
307
+ maxInput.value = parseInt(maxInput.value) - parseInt(maxInput.step);
308
+ // Trigger the change event manually
309
+ maxInput.dispatchEvent(new Event('change'));
310
+ // Explicitly update constraint settings state for button click
311
+ updateConstraintSettingsState();
312
+ });
313
+
314
+ maxIncBtn.addEventListener('click', () => {
315
+ maxInput.value = parseInt(maxInput.value) + parseInt(maxInput.step);
316
+ // Trigger the change event manually
317
+ maxInput.dispatchEvent(new Event('change'));
318
+ // Explicitly update constraint settings state for button click
319
+ updateConstraintSettingsState();
320
+ });
321
+
322
+ maxControlWrapper.appendChild(maxDecBtn);
323
+ maxControlWrapper.appendChild(maxInput);
324
+ maxControlWrapper.appendChild(maxIncBtn);
325
+
326
+ maxContainer.appendChild(maxLabel);
327
+ maxContainer.appendChild(maxControlWrapper);
328
+
329
+ limitsContainer.appendChild(minContainer);
330
+ limitsContainer.appendChild(maxContainer);
331
+ itemElem.appendChild(limitsContainer);
332
+
333
+ // Store original values immediately when control is created
334
+ const boneName = itemElem.closest('.rig-item')?.querySelector('select[data-bone-constraint]')?.getAttribute('data-bone-name');
335
+ if (boneName) {
336
+ jointPreviousValues.set(`${boneName}:hinge-config`, {
337
+ axis: axisSelect.value,
338
+ min: parseInt(minInput.value),
339
+ max: parseInt(maxInput.value)
340
+ });
341
+
342
+ if (jointSettingsDebug) {
343
+ console.log(`Stored initial hinge config for ${boneName}:`, jointPreviousValues.get(`${boneName}:hinge-config`));
344
+ }
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Add rotation limit controls to a joint item
350
+ * @param {HTMLElement} itemElem - The joint item element
351
+ * @param {Object} item - The joint data
352
+ */
353
+ function addRotationLimitControls(itemElem, item) {
354
+ // Initialize limits object if not exists
355
+ if (!item.rotationLimits) {
356
+ item.rotationLimits = {
357
+ x: { min: -Math.PI/4, max: Math.PI/4 },
358
+ y: { min: -Math.PI/4, max: Math.PI/4 },
359
+ z: { min: -Math.PI/4, max: Math.PI/4 }
360
+ };
361
+ }
362
+
363
+ const limitsContainer = document.createElement('div');
364
+ limitsContainer.className = 'rig-rotation-limits-container rig-constraint-controls';
365
+
366
+ const axisLabels = ['X', 'Y', 'Z'];
367
+
368
+ // Create a config object to store initial values
369
+ const initialConfig = { x: {}, y: {}, z: {} };
370
+
371
+ // Create controls for each axis
372
+ axisLabels.forEach(axis => {
373
+ const axisLower = axis.toLowerCase();
374
+
375
+ const axisContainer = document.createElement('div');
376
+ axisContainer.className = 'rig-axis-limits';
377
+
378
+ const axisLabel = document.createElement('div');
379
+ axisLabel.className = 'rig-axis-limit-label';
380
+ axisLabel.textContent = `${axis} Axis:`;
381
+ axisContainer.appendChild(axisLabel);
382
+
383
+ // Min limit
384
+ const minContainer = document.createElement('div');
385
+ minContainer.className = 'rig-min-limit';
386
+
387
+ const minLabel = document.createElement('label');
388
+ minLabel.textContent = 'Min:';
389
+
390
+ const minControlWrapper = document.createElement('div');
391
+ minControlWrapper.className = 'rig-angle-control-wrapper';
392
+
393
+ // Add decrement button
394
+ const minDecBtn = document.createElement('button');
395
+ minDecBtn.className = 'rig-angle-btn';
396
+ minDecBtn.textContent = '−';
397
+ minDecBtn.type = 'button';
398
+
399
+ const minInput = document.createElement('input');
400
+ minInput.type = 'number';
401
+ minInput.min = -180;
402
+ minInput.max = 180;
403
+ minInput.step = 5;
404
+ minInput.value = Math.round((item.rotationLimits[axisLower]?.min || -45) * 180 / Math.PI);
405
+
406
+ // Store initial value in config
407
+ initialConfig[axisLower].min = parseInt(minInput.value);
408
+
409
+ // Add increment button
410
+ const minIncBtn = document.createElement('button');
411
+ minIncBtn.className = 'rig-angle-btn';
412
+ minIncBtn.textContent = '+';
413
+ minIncBtn.type = 'button';
414
+
415
+ minInput.addEventListener('change', () => {
416
+ if (!item.rotationLimits[axisLower]) {
417
+ item.rotationLimits[axisLower] = {};
418
+ }
419
+ item.rotationLimits[axisLower].min = minInput.value * Math.PI / 180;
420
+ updateConstraintSettingsState();
421
+ });
422
+
423
+ // Button event listeners
424
+ minDecBtn.addEventListener('click', () => {
425
+ minInput.value = parseInt(minInput.value) - parseInt(minInput.step || 5);
426
+ // Trigger the change event manually
427
+ minInput.dispatchEvent(new Event('change'));
428
+ // Explicitly update constraint settings state for button click
429
+ updateConstraintSettingsState();
430
+ });
431
+
432
+ minIncBtn.addEventListener('click', () => {
433
+ minInput.value = parseInt(minInput.value) + parseInt(minInput.step || 5);
434
+ // Trigger the change event manually
435
+ minInput.dispatchEvent(new Event('change'));
436
+ // Explicitly update constraint settings state for button click
437
+ updateConstraintSettingsState();
438
+ });
439
+
440
+ minControlWrapper.appendChild(minDecBtn);
441
+ minControlWrapper.appendChild(minInput);
442
+ minControlWrapper.appendChild(minIncBtn);
443
+
444
+ minContainer.appendChild(minLabel);
445
+ minContainer.appendChild(minControlWrapper);
446
+
447
+ // Max limit
448
+ const maxContainer = document.createElement('div');
449
+ maxContainer.className = 'rig-max-limit';
450
+
451
+ const maxLabel = document.createElement('label');
452
+ maxLabel.textContent = 'Max:';
453
+
454
+ const maxControlWrapper = document.createElement('div');
455
+ maxControlWrapper.className = 'rig-angle-control-wrapper';
456
+
457
+ // Add decrement button
458
+ const maxDecBtn = document.createElement('button');
459
+ maxDecBtn.className = 'rig-angle-btn';
460
+ maxDecBtn.textContent = '−';
461
+ maxDecBtn.type = 'button';
462
+
463
+ const maxInput = document.createElement('input');
464
+ maxInput.type = 'number';
465
+ maxInput.min = -180;
466
+ maxInput.max = 180;
467
+ maxInput.step = 5;
468
+ maxInput.value = Math.round((item.rotationLimits[axisLower]?.max || 45) * 180 / Math.PI);
469
+
470
+ // Store initial value in config
471
+ initialConfig[axisLower].max = parseInt(maxInput.value);
472
+
473
+ // Add increment button
474
+ const maxIncBtn = document.createElement('button');
475
+ maxIncBtn.className = 'rig-angle-btn';
476
+ maxIncBtn.textContent = '+';
477
+ maxIncBtn.type = 'button';
478
+
479
+ maxInput.addEventListener('change', () => {
480
+ if (!item.rotationLimits[axisLower]) {
481
+ item.rotationLimits[axisLower] = {};
482
+ }
483
+ item.rotationLimits[axisLower].max = maxInput.value * Math.PI / 180;
484
+ updateConstraintSettingsState();
485
+ });
486
+
487
+ // Button event listeners
488
+ maxDecBtn.addEventListener('click', () => {
489
+ maxInput.value = parseInt(maxInput.value) - parseInt(maxInput.step || 5);
490
+ // Trigger the change event manually
491
+ maxInput.dispatchEvent(new Event('change'));
492
+ // Explicitly update constraint settings state for button click
493
+ updateConstraintSettingsState();
494
+ });
495
+
496
+ maxIncBtn.addEventListener('click', () => {
497
+ maxInput.value = parseInt(maxInput.value) + parseInt(maxInput.step || 5);
498
+ // Trigger the change event manually
499
+ maxInput.dispatchEvent(new Event('change'));
500
+ // Explicitly update constraint settings state for button click
501
+ updateConstraintSettingsState();
502
+ });
503
+
504
+ maxControlWrapper.appendChild(maxDecBtn);
505
+ maxControlWrapper.appendChild(maxInput);
506
+ maxControlWrapper.appendChild(maxIncBtn);
507
+
508
+ maxContainer.appendChild(maxLabel);
509
+ maxContainer.appendChild(maxControlWrapper);
510
+
511
+ axisContainer.appendChild(minContainer);
512
+ axisContainer.appendChild(maxContainer);
513
+ limitsContainer.appendChild(axisContainer);
514
+ });
515
+
516
+ itemElem.appendChild(limitsContainer);
517
+
518
+ // Store initial rotation limits immediately when control is created
519
+ const boneName = itemElem.closest('.rig-item')?.querySelector('select[data-bone-constraint]')?.getAttribute('data-bone-name');
520
+ if (boneName) {
521
+ jointPreviousValues.set(`${boneName}:rotation-limits`, JSON.parse(JSON.stringify(initialConfig)));
522
+
523
+ if (jointSettingsDebug) {
524
+ console.log(`Stored initial rotation limits for ${boneName}:`, jointPreviousValues.get(`${boneName}:rotation-limits`));
525
+ }
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Add spring controls to a joint item
531
+ * @param {HTMLElement} itemElem - The joint item element
532
+ * @param {Object} item - The joint data
533
+ */
534
+ function addSpringControls(itemElem, item) {
535
+ // Initialize spring properties if not exists
536
+ if (!item.spring) {
537
+ item.spring = {
538
+ stiffness: 50,
539
+ damping: 5,
540
+ gravity: 1.0
541
+ };
542
+ }
543
+
544
+ const springContainer = document.createElement('div');
545
+ springContainer.className = 'rig-spring-container rig-constraint-controls';
546
+
547
+ // Stiffness control
548
+ const stiffnessContainer = document.createElement('div');
549
+ stiffnessContainer.className = 'rig-stiffness-container';
550
+
551
+ const stiffnessLabel = document.createElement('label');
552
+ stiffnessLabel.textContent = 'Stiffness:';
553
+ stiffnessLabel.className = 'rig-stiffness-label';
554
+
555
+ // Create slider container
556
+ const stiffnessSliderContainer = document.createElement('div');
557
+ stiffnessSliderContainer.className = 'rig-slider-container';
558
+
559
+ const stiffnessInput = document.createElement('input');
560
+ stiffnessInput.type = 'range';
561
+ stiffnessInput.min = 1;
562
+ stiffnessInput.max = 100;
563
+ stiffnessInput.value = item.spring.stiffness || 50;
564
+ stiffnessInput.className = 'rig-stiffness-input';
565
+
566
+ const stiffnessValue = document.createElement('span');
567
+ stiffnessValue.textContent = stiffnessInput.value;
568
+ stiffnessValue.className = 'rig-stiffness-value';
569
+
570
+ stiffnessInput.addEventListener('input', () => {
571
+ item.spring.stiffness = parseInt(stiffnessInput.value);
572
+ stiffnessValue.textContent = stiffnessInput.value;
573
+ updateConstraintSettingsState();
574
+ });
575
+
576
+ // Assemble stiffness controls
577
+ stiffnessSliderContainer.appendChild(stiffnessInput);
578
+ stiffnessSliderContainer.appendChild(stiffnessValue);
579
+ stiffnessContainer.appendChild(stiffnessLabel);
580
+ stiffnessContainer.appendChild(stiffnessSliderContainer);
581
+
582
+ // Damping control
583
+ const dampingContainer = document.createElement('div');
584
+ dampingContainer.className = 'rig-damping-container';
585
+
586
+ const dampingLabel = document.createElement('label');
587
+ dampingLabel.textContent = 'Damping:';
588
+ dampingLabel.className = 'rig-damping-label';
589
+
590
+ // Create slider container
591
+ const dampingSliderContainer = document.createElement('div');
592
+ dampingSliderContainer.className = 'rig-slider-container';
593
+
594
+ const dampingInput = document.createElement('input');
595
+ dampingInput.type = 'range';
596
+ dampingInput.min = 0;
597
+ dampingInput.max = 20;
598
+ dampingInput.value = item.spring.damping || 5;
599
+ dampingInput.className = 'rig-damping-input';
600
+
601
+ const dampingValue = document.createElement('span');
602
+ dampingValue.textContent = dampingInput.value;
603
+ dampingValue.className = 'rig-damping-value';
604
+
605
+ dampingInput.addEventListener('input', () => {
606
+ item.spring.damping = parseInt(dampingInput.value);
607
+ dampingValue.textContent = dampingInput.value;
608
+ updateConstraintSettingsState();
609
+ });
610
+
611
+ // Assemble damping controls
612
+ dampingSliderContainer.appendChild(dampingInput);
613
+ dampingSliderContainer.appendChild(dampingValue);
614
+ dampingContainer.appendChild(dampingLabel);
615
+ dampingContainer.appendChild(dampingSliderContainer);
616
+
617
+ // Gravity influence control
618
+ const gravityContainer = document.createElement('div');
619
+ gravityContainer.className = 'rig-gravity-container';
620
+
621
+ const gravityLabel = document.createElement('label');
622
+ gravityLabel.textContent = 'Gravity:';
623
+ gravityLabel.className = 'rig-gravity-label';
624
+
625
+ // Create slider container
626
+ const gravitySliderContainer = document.createElement('div');
627
+ gravitySliderContainer.className = 'rig-slider-container';
628
+
629
+ const gravityInput = document.createElement('input');
630
+ gravityInput.type = 'range';
631
+ gravityInput.min = 0;
632
+ gravityInput.max = 20;
633
+ gravityInput.step = 0.1;
634
+ gravityInput.value = item.spring.gravity || 1.0;
635
+ gravityInput.className = 'rig-gravity-input';
636
+
637
+ const gravityValue = document.createElement('span');
638
+ gravityValue.textContent = gravityInput.value;
639
+ gravityValue.className = 'rig-gravity-value';
640
+
641
+ gravityInput.addEventListener('input', () => {
642
+ item.spring.gravity = parseFloat(gravityInput.value);
643
+ gravityValue.textContent = gravityInput.value;
644
+ updateConstraintSettingsState();
645
+ });
646
+
647
+ // Assemble gravity controls
648
+ gravitySliderContainer.appendChild(gravityInput);
649
+ gravitySliderContainer.appendChild(gravityValue);
650
+ gravityContainer.appendChild(gravityLabel);
651
+ gravityContainer.appendChild(gravitySliderContainer);
652
+
653
+ springContainer.appendChild(stiffnessContainer);
654
+ springContainer.appendChild(dampingContainer);
655
+ springContainer.appendChild(gravityContainer);
656
+ itemElem.appendChild(springContainer);
657
+
658
+ // Store original values immediately when control is created
659
+ const boneName = itemElem.closest('.rig-item')?.querySelector('select[data-bone-constraint]')?.getAttribute('data-bone-name');
660
+ if (boneName) {
661
+ jointPreviousValues.set(`${boneName}:spring-config`, {
662
+ stiffness: parseInt(stiffnessInput.value),
663
+ damping: parseInt(dampingInput.value),
664
+ gravity: parseFloat(gravityInput.value)
665
+ });
666
+
667
+ if (jointSettingsDebug) {
668
+ console.log(`Stored initial spring config for ${boneName}:`, jointPreviousValues.get(`${boneName}:spring-config`));
669
+ }
670
+ }
671
+ }
672
+
673
+ /**
674
+ * Disable the Apply Changes button
675
+ * @param {HTMLElement} button - The button to disable
676
+ */
677
+ export function disableApplyButton(button) {
678
+ button.disabled = true;
679
+ button.style.backgroundColor = 'rgba(0,0,0,0.2)';
680
+ button.style.color = '#ccc';
681
+ button.style.cursor = 'not-allowed';
682
+ button.style.opacity = '0.5';
683
+ }
684
+
685
+ /**
686
+ * Enable the Apply Changes button
687
+ * @param {HTMLElement} button - The button to enable
688
+ */
689
+ export function enableApplyButton(button) {
690
+ if (button) {
691
+ button.removeAttribute('disabled');
692
+ button.classList.remove('disabled');
693
+
694
+ // Restore visual appearance to match enabled state
695
+ button.style.backgroundColor = '#3f51b5'; // Standard blue button color
696
+ button.style.color = '#ffffff'; // White text
697
+ button.style.cursor = 'pointer';
698
+ button.style.opacity = '1.0';
699
+ }
700
+ }