bucciafico-lib 1.0.7 → 1.0.8

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bucciafico-lib",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Modular 3D rendering engine for Minecraft skins based on Three.js",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -3,6 +3,8 @@ import { CameraManager } from '../managers/CameraManager.js';
3
3
  import { SceneSetup } from '../objects/SceneSetup.js';
4
4
  import { SkinModel } from '../objects/SkinModel.js';
5
5
  import { detectSlimSkin } from '../utils/SkinUtils.js';
6
+ import {disposeObjectTree} from "../utils/ThreeUtils.js";
7
+ import {EventManager} from "../managers/EventManager.js";
6
8
 
7
9
  /**
8
10
  * Core 3D Viewer class.
@@ -30,6 +32,9 @@ export class SkinViewer {
30
32
  ...config
31
33
  };
32
34
 
35
+ this.skinData = null;
36
+ this.capeData = null;
37
+
33
38
  /** @type {Map<string, Object>} Registered plugins. */
34
39
  this.plugins = new Map();
35
40
 
@@ -39,6 +44,12 @@ export class SkinViewer {
39
44
  this.isVisible = true;
40
45
  this.needsRender = true;
41
46
 
47
+ this.events = new EventManager();
48
+
49
+ this.on = this.events.on.bind(this.events);
50
+ this.off = this.events.off.bind(this.events);
51
+ this.emit = this.events.emit.bind(this.events);
52
+
42
53
  // --- 1. RENDERER SETUP ---
43
54
  this.renderer = new THREE.WebGLRenderer({
44
55
  antialias: true,
@@ -88,6 +99,7 @@ export class SkinViewer {
88
99
 
89
100
  // --- 3. START LOOP ---
90
101
  this.animate = this.animate.bind(this);
102
+ this.emit('viewer:ready', this);
91
103
  this.animate();
92
104
  }
93
105
 
@@ -130,8 +142,13 @@ export class SkinViewer {
130
142
  * @returns {Promise<boolean>} isSlim
131
143
  */
132
144
  loadSkin(imageUrl) {
145
+ this.emit('skin:loading', imageUrl);
146
+
133
147
  return new Promise((resolve, reject) => {
134
- new THREE.TextureLoader().load(imageUrl, (texture) => {
148
+ const loader = new THREE.TextureLoader();
149
+ loader.setCrossOrigin('anonymous');
150
+
151
+ loader.load(imageUrl, (texture) => {
135
152
  texture.magFilter = THREE.NearestFilter;
136
153
  texture.colorSpace = THREE.SRGBColorSpace;
137
154
 
@@ -145,9 +162,20 @@ export class SkinViewer {
145
162
  this.skinModel.setPose(currentPose);
146
163
  this.skinData = { type: 'url', value: imageUrl };
147
164
 
165
+ const fxPlugin = this.getPlugin('EffectsPlugin');
166
+ if (fxPlugin) {
167
+ fxPlugin.forceUpdate();
168
+ }
169
+
148
170
  this.requestRender();
171
+
172
+ this.emit('skin:loaded', { isSlim, texture });
173
+
149
174
  resolve(isSlim);
150
- }, undefined, reject);
175
+ }, undefined, (err) => {
176
+ this.emit('skin:error', err);
177
+ reject(err);
178
+ });
151
179
  });
152
180
  }
153
181
 
@@ -161,6 +189,96 @@ export class SkinViewer {
161
189
  });
162
190
  }
163
191
 
192
+ /**
193
+ * Loads a cape from URL.
194
+ * @param {string} imageUrl
195
+ */
196
+ loadCape(imageUrl) {
197
+ return new Promise((resolve, reject) => {
198
+ const loader = new THREE.TextureLoader();
199
+ loader.setCrossOrigin('anonymous');
200
+
201
+ loader.load(
202
+ imageUrl,
203
+ (texture) => {
204
+ texture.magFilter = THREE.NearestFilter;
205
+ texture.colorSpace = THREE.SRGBColorSpace;
206
+
207
+ texture.needsUpdate = true;
208
+
209
+ this.skinModel.setCape(texture);
210
+
211
+ const fxPlugin = this.getPlugin('EffectsPlugin');
212
+ if (fxPlugin) {
213
+ fxPlugin.forceUpdate();
214
+ }
215
+
216
+ this.capeData = { type: 'url', value: imageUrl };
217
+
218
+ this.requestRender();
219
+ this.emit('cape:loaded', imageUrl);
220
+ resolve();
221
+ },
222
+ undefined,
223
+ (err) => {
224
+ console.error("Error loading cape texture:", err);
225
+ reject(err);
226
+ }
227
+ );
228
+ });
229
+ }
230
+
231
+
232
+ /**
233
+ * Loads a cape by username using capes.dev API.
234
+ * Supports Official, Optifine, LabyMod, etc.
235
+ * @param {string} username
236
+ */
237
+ async loadCapeByUsername(username) {
238
+ this.emit('cape:loading', username);
239
+
240
+ try {
241
+ const response = await fetch(`https://api.capes.dev/load/${username}`);
242
+
243
+ if (!response.ok) {
244
+ throw new Error(`User not found in capes.dev (Status: ${response.status})`);
245
+ }
246
+
247
+ const data = await response.json();
248
+ let capeUrl = null;
249
+
250
+ if (data.minecraft?.exists && data.minecraft?.imageUrl) {
251
+ capeUrl = data.minecraft.imageUrl;
252
+ } else if (data.optifine?.exists && data.optifine?.imageUrl) {
253
+ capeUrl = data.optifine.imageUrl;
254
+ } else if (data.labymod?.exists && data.labymod?.imageUrl) {
255
+ capeUrl = data.labymod.imageUrl;
256
+ } else if (data.tlauncher?.exists && data.tlauncher?.imageUrl) {
257
+ capeUrl = data.tlauncher.imageUrl;
258
+ }
259
+
260
+ if (capeUrl) {
261
+ await this.loadCape(capeUrl);
262
+ this.capeData = { type: 'username', value: username };
263
+ return true;
264
+ } else {
265
+ this.resetCape();
266
+ return false;
267
+ }
268
+
269
+ } catch (e) {
270
+ this.emit('cape:error', e);
271
+ return false;
272
+ }
273
+ }
274
+
275
+ resetCape() {
276
+ this.skinModel.setCape(null);
277
+ this.capeData = null;
278
+ this.requestRender();
279
+ this.emit('cape:removed');
280
+ }
281
+
164
282
  setPose(poseData) {
165
283
  // Record history if Editor is present
166
284
  const editor = this.getPlugin('EditorPlugin');
@@ -170,6 +288,15 @@ export class SkinViewer {
170
288
  this.requestRender();
171
289
  }
172
290
 
291
+ /**
292
+ * Updates lighting intensity.
293
+ * @param {Object} config - { global, main, fill }
294
+ */
295
+ setEnvironment(config) {
296
+ this.sceneSetup.setLightConfig(config);
297
+ this.requestRender();
298
+ }
299
+
173
300
  /**
174
301
  * Handles window resize. Should be called by the implementation layer.
175
302
  */
@@ -215,19 +342,37 @@ export class SkinViewer {
215
342
  }
216
343
 
217
344
  dispose() {
345
+ this.emit('viewer:dispose');
218
346
  this.isDisposed = true;
219
347
  this.observer.disconnect();
220
348
 
221
- if (this.container && this.renderer.domElement) {
222
- this.container.removeChild(this.renderer.domElement);
223
- }
224
-
225
- this.renderer.dispose();
226
-
227
- // Dispose all plugins
228
349
  this.plugins.forEach(p => {
229
350
  if (p.dispose) p.dispose();
230
351
  });
231
352
  this.plugins.clear();
353
+
354
+ if (this.skinModel) {
355
+ this.skinModel.dispose();
356
+ }
357
+
358
+ disposeObjectTree(this.scene);
359
+ disposeObjectTree(this.overlayScene);
360
+
361
+ if (this.cameraManager) {
362
+ this.cameraManager.controls.dispose();
363
+ }
364
+
365
+ if (this.renderer) {
366
+ this.renderer.dispose();
367
+ this.renderer.forceContextLoss();
368
+
369
+ if (this.container && this.renderer.domElement) {
370
+ this.container.removeChild(this.renderer.domElement);
371
+ }
372
+ this.renderer.domElement = null;
373
+ this.renderer = null;
374
+ }
375
+
376
+ this.events.dispose();
232
377
  }
233
378
  }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Custom Event Emitter implementation.
3
+ * Handles Pub/Sub architecture to decouple Core from Plugins and UI.
4
+ */
5
+ export class EventManager {
6
+ constructor() {
7
+ /** @type {Map<string, Set<Function>>} */
8
+ this.listeners = new Map();
9
+ }
10
+
11
+ /**
12
+ * Subscribe to an event.
13
+ * @param {string} event - Event name (e.g. 'skin:loaded').
14
+ * @param {Function} callback - Function to execute.
15
+ * @returns {Function} Unsubscribe function for convenience.
16
+ */
17
+ on(event, callback) {
18
+ if (!this.listeners.has(event)) {
19
+ this.listeners.set(event, new Set());
20
+ }
21
+ this.listeners.get(event).add(callback);
22
+
23
+ // Return unsubscribe function pattern
24
+ return () => this.off(event, callback);
25
+ }
26
+
27
+ /**
28
+ * Unsubscribe from an event.
29
+ * @param {string} event
30
+ * @param {Function} callback
31
+ */
32
+ off(event, callback) {
33
+ if (this.listeners.has(event)) {
34
+ this.listeners.get(event).delete(callback);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Subscribe to an event only once.
40
+ * @param {string} event
41
+ * @param {Function} callback
42
+ */
43
+ once(event, callback) {
44
+ const wrapper = (...args) => {
45
+ callback(...args);
46
+ this.off(event, wrapper);
47
+ };
48
+ this.on(event, wrapper);
49
+ }
50
+
51
+ /**
52
+ * Emit an event with optional data.
53
+ * @param {string} event
54
+ * @param {*} [data]
55
+ */
56
+ emit(event, data) {
57
+ if (this.listeners.has(event)) {
58
+ this.listeners.get(event).forEach(cb => {
59
+ try {
60
+ cb(data);
61
+ } catch (e) {
62
+ console.error(`Error in listener for event "${event}":`, e);
63
+ }
64
+ });
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Clears all listeners. Used for disposal.
70
+ */
71
+ dispose() {
72
+ this.listeners.clear();
73
+ }
74
+ }
@@ -131,4 +131,28 @@ export class PostProcessingManager {
131
131
  this.bloomPass.radius = Number(rad);
132
132
  this.bloomPass.threshold = Number(thr);
133
133
  }
134
+
135
+ dispose() {
136
+ if (this.bloomComposer) {
137
+ this.bloomComposer.renderTarget1.dispose();
138
+ this.bloomComposer.renderTarget2.dispose();
139
+ }
140
+
141
+ if (this.finalComposer) {
142
+ this.finalComposer.renderTarget1.dispose();
143
+ this.finalComposer.renderTarget2.dispose();
144
+ }
145
+
146
+ if (this.bloomPass) {
147
+ this.bloomPass.dispose();
148
+ }
149
+
150
+ if (this.outlinePass) {
151
+ this.outlinePass.dispose();
152
+ }
153
+
154
+ if (this.mixPass && this.mixPass.material) {
155
+ this.mixPass.material.dispose();
156
+ }
157
+ }
134
158
  }
@@ -7,19 +7,32 @@ export class SceneSetup {
7
7
  constructor(scene) {
8
8
  this.scene = scene;
9
9
  this.gridHelper = null;
10
+
11
+ this.ambientLight = null;
12
+ this.hemiLight = null;
13
+ this.dirLightMain = null;
14
+ this.dirLightFill = null;
15
+
10
16
  this.initLights();
11
17
  this.initHelpers();
12
18
  }
13
19
 
14
20
  initLights() {
15
- this.scene.add(new THREE.AmbientLight(0xffffff, 0.8));
16
- this.scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 0.5));
17
- const dl = new THREE.DirectionalLight(0xffffff, 0.8);
18
- dl.position.set(10, 20, 10);
19
- this.scene.add(dl);
20
- const fl = new THREE.DirectionalLight(0xffffff, 0.4);
21
- fl.position.set(0, 0, 20);
22
- this.scene.add(fl);
21
+ // 1. Global Illumination
22
+ this.ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
23
+ this.scene.add(this.ambientLight);
24
+
25
+ this.hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.5);
26
+ this.scene.add(this.hemiLight);
27
+
28
+ // 2. Directional Lights (Shadows & Definition)
29
+ this.dirLightMain = new THREE.DirectionalLight(0xffffff, 0.8);
30
+ this.dirLightMain.position.set(10, 20, 10);
31
+ this.scene.add(this.dirLightMain);
32
+
33
+ this.dirLightFill = new THREE.DirectionalLight(0xffffff, 0.4);
34
+ this.dirLightFill.position.set(0, 0, 20);
35
+ this.scene.add(this.dirLightFill);
23
36
  }
24
37
 
25
38
  initHelpers() {
@@ -29,4 +42,32 @@ export class SceneSetup {
29
42
  }
30
43
 
31
44
  setGridVisible(vis) { if(this.gridHelper) this.gridHelper.visible = vis; }
45
+
46
+ /**
47
+ * Updates the intensity of scene lights.
48
+ * @param {Object} config
49
+ * @param {number} [config.global] - Intensity of Ambient/Hemi lights (0.0 - 2.0)
50
+ * @param {number} [config.main] - Intensity of Main Directional Light (0.0 - 2.0)
51
+ * @param {number} [config.fill] - Intensity of Fill Light (0.0 - 2.0)
52
+ */
53
+ setLightConfig(config) {
54
+ if (config.global !== undefined) {
55
+ this.ambientLight.intensity = config.global;
56
+ this.hemiLight.intensity = config.global * 0.6;
57
+ }
58
+ if (config.main !== undefined) {
59
+ this.dirLightMain.intensity = config.main;
60
+ }
61
+ if (config.fill !== undefined) {
62
+ this.dirLightFill.intensity = config.fill;
63
+ }
64
+ }
65
+
66
+ getLightConfig() {
67
+ return {
68
+ global: this.ambientLight.intensity,
69
+ main: this.dirLightMain.intensity,
70
+ fill: this.dirLightFill.intensity
71
+ };
72
+ }
32
73
  }
@@ -3,6 +3,7 @@ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUti
3
3
  import { applySkinUVs } from '../utils/SkinUtils.js';
4
4
  import { createVoxelLayer } from '../utils/Voxelizer.js';
5
5
  import { createGlowMaterial } from '../materials/GlowMaterial.js';
6
+ import {disposeObjectTree} from "../utils/ThreeUtils.js";
6
7
 
7
8
  /**
8
9
  * Represents the Minecraft Character Model (Steve/Alex).
@@ -16,6 +17,8 @@ export class SkinModel {
16
17
  this.bodyMeshes = [];
17
18
  this.defaultPositions = {};
18
19
  this.blackMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
20
+
21
+ this.LAYERS_COUNT = 20;
19
22
  }
20
23
 
21
24
  /**
@@ -59,15 +62,30 @@ export class SkinModel {
59
62
  this.bodyMeshes.push(voxelMesh);
60
63
  }
61
64
 
62
- // 3. Glow Mesh (Copy of geometry for shader)
65
+ // 3. Glow Meshes (Multi-Layer Shells)
63
66
  const glowParts = [innerGeo.clone()];
64
67
  if (voxelGeo) glowParts.push(voxelGeo.clone());
65
- const glowGeo = BufferGeometryUtils.mergeGeometries(glowParts, false);
66
- const glowMat = createGlowMaterial(size.h);
67
- const glowMesh = new THREE.Mesh(glowGeo, glowMat);
68
- glowMesh.userData.glowMat = glowMat;
69
- this.glowMeshes.push(glowMesh);
70
- meshGroup.add(glowMesh);
68
+ const baseGlowGeo = BufferGeometryUtils.mergeGeometries(glowParts, false);
69
+
70
+ const partLayers = [];
71
+
72
+ for (let i = 0; i < this.LAYERS_COUNT; i++) {
73
+ const glowMat = createGlowMaterial(size.h);
74
+
75
+ glowMat.uniforms.thickness.value = 0;
76
+ glowMat.uniforms.opacity.value = 0;
77
+
78
+ const layerMesh = new THREE.Mesh(baseGlowGeo, glowMat);
79
+
80
+ layerMesh.userData.layerIndex = i;
81
+ layerMesh.userData.isGlow = true;
82
+ layerMesh.userData.glowMat = glowMat;
83
+
84
+ meshGroup.add(layerMesh);
85
+ partLayers.push(layerMesh);
86
+ }
87
+
88
+ this.glowMeshes.push(partLayers);
71
89
 
72
90
  pivotGroup.add(meshGroup);
73
91
  return pivotGroup;
@@ -79,7 +97,24 @@ export class SkinModel {
79
97
  * @param {boolean} isSlim - True for Alex model (3px arms), False for Steve (4px arms).
80
98
  */
81
99
  build(texture, isSlim = false) {
82
- this.playerGroup.clear();
100
+ let capeBackup = null;
101
+ if (this.parts.cape) {
102
+ const mesh = this.parts.cape.children.find(c => c.isMesh);
103
+ if (mesh && mesh.material.map) {
104
+ capeBackup = {
105
+ texture: mesh.material.map,
106
+ position: this.parts.cape.position.clone(),
107
+ rotation: this.parts.cape.rotation.clone(),
108
+ scale: this.parts.cape.scale.clone()
109
+ };
110
+ }
111
+ }
112
+
113
+ if (this.playerGroup.children.length > 0) {
114
+ disposeObjectTree(this.playerGroup);
115
+ this.playerGroup.clear();
116
+ }
117
+
83
118
  this.parts = {};
84
119
  this.glowMeshes = [];
85
120
  this.bodyMeshes = [];
@@ -102,13 +137,132 @@ export class SkinModel {
102
137
  this.parts[name] = part;
103
138
  this.playerGroup.add(part);
104
139
  }
140
+
141
+ if (capeBackup) {
142
+ this.setCape(capeBackup.texture);
143
+
144
+ if (this.parts.cape) {
145
+ this.parts.cape.position.copy(capeBackup.position);
146
+ this.parts.cape.rotation.copy(capeBackup.rotation);
147
+ this.parts.cape.scale.copy(capeBackup.scale);
148
+ }
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Adds or updates the Cape mesh.
154
+ * @param {THREE.Texture} texture
155
+ */
156
+ setCape(texture) {
157
+ if (this.parts.cape) {
158
+ if (this.parts.cape.userData.glowLayers) {
159
+ const layersToRemove = this.parts.cape.userData.glowLayers;
160
+ this.glowMeshes = this.glowMeshes.filter(layers => layers !== layersToRemove);
161
+ }
162
+
163
+ this.playerGroup.remove(this.parts.cape);
164
+ disposeObjectTree(this.parts.cape);
165
+ delete this.parts.cape;
166
+ }
167
+
168
+ if (!texture) return;
169
+
170
+ const size = { w: 10, h: 16, d: 1 };
171
+ const pivotPos = new THREE.Vector3(0, 0, -3);
172
+ const meshOffset = new THREE.Vector3(0, -8, 0.6);
173
+
174
+ const pivotGroup = new THREE.Group();
175
+ pivotGroup.position.copy(pivotPos);
176
+ pivotGroup.name = 'cape';
177
+ pivotGroup.rotation.x = 0.2;
178
+
179
+ this.defaultPositions['cape'] = pivotPos.clone();
180
+
181
+ const geo = new THREE.BoxGeometry(size.w, size.h, size.d);
182
+ applySkinUVs(geo, 0, 0, 10, 16, 1, 64, 32);
183
+
184
+ const mat = new THREE.MeshStandardMaterial({
185
+ map: texture,
186
+ side: THREE.DoubleSide,
187
+ transparent: true,
188
+ alphaTest: 0.5
189
+ });
190
+
191
+ const mainMesh = new THREE.Mesh(geo, mat);
192
+ mainMesh.position.copy(meshOffset);
193
+ mainMesh.rotation.y = Math.PI;
194
+ mainMesh.userData.originalMat = mat;
195
+
196
+ pivotGroup.add(mainMesh);
197
+ this.bodyMeshes.push(mainMesh);
198
+
199
+ const capeLayers = [];
200
+ const shellGeo = geo.clone();
201
+
202
+ for (let i = 0; i < this.LAYERS_COUNT; i++) {
203
+ const glowMat = createGlowMaterial(size.h);
204
+
205
+ glowMat.uniforms.thickness.value = 0;
206
+ glowMat.uniforms.opacity.value = 0;
207
+ glowMat.polygonOffset = true;
208
+ glowMat.polygonOffsetFactor = i * 0.1;
209
+
210
+ const layerMesh = new THREE.Mesh(shellGeo, glowMat);
211
+
212
+ layerMesh.position.copy(meshOffset);
213
+ layerMesh.rotation.y = Math.PI;
214
+
215
+ layerMesh.userData.layerIndex = i;
216
+ layerMesh.userData.isGlow = true;
217
+ layerMesh.userData.glowMat = glowMat;
218
+
219
+ pivotGroup.add(layerMesh);
220
+ capeLayers.push(layerMesh);
221
+ }
222
+
223
+ this.glowMeshes.push(capeLayers);
224
+ pivotGroup.userData.glowLayers = capeLayers;
225
+
226
+ this.playerGroup.add(pivotGroup);
227
+ this.parts['cape'] = pivotGroup;
105
228
  }
106
229
 
107
230
  getGroup() { return this.playerGroup; }
108
231
 
109
- updateBorderThickness(v) { const t = v * 0.05; this.glowMeshes.forEach(m => m.userData.glowMat.uniforms.thickness.value = t); }
110
- updateGlowHeight(p) { this.glowMeshes.forEach(m => m.userData.glowMat.uniforms.gradientLimit.value = p); }
111
- setGlowEffect(en) { this.glowMeshes.forEach(m => m.userData.glowMat.uniforms.opacity.value = en ? 1.0 : 0.0); }
232
+ /**
233
+ * Updates thickness creating a solid volume effect.
234
+ * @param {number} v - Base thickness value.
235
+ */
236
+ updateBorderThickness(v) {
237
+ const maxThickness = v * 0.05;
238
+
239
+ this.glowMeshes.forEach(layers => {
240
+ layers.forEach((mesh, i) => {
241
+ const progress = (i + 1) / this.LAYERS_COUNT;
242
+
243
+ mesh.userData.glowMat.uniforms.thickness.value = maxThickness * progress;
244
+ });
245
+ });
246
+ }
247
+
248
+ updateGlowHeight(p) {
249
+ this.glowMeshes.forEach(layers => {
250
+ layers.forEach(m => m.userData.glowMat.uniforms.gradientLimit.value = p);
251
+ });
252
+ }
253
+
254
+ setGlowEffect(en) {
255
+ this.glowMeshes.forEach(layers => {
256
+ layers.forEach((mesh, i) => {
257
+ if (!en) {
258
+ mesh.userData.glowMat.uniforms.opacity.value = 0.0;
259
+ } else {
260
+ mesh.userData.glowMat.uniforms.opacity.value = 1.0 / (this.LAYERS_COUNT * 0.6);
261
+ }
262
+ });
263
+ });
264
+ }
265
+
112
266
  darkenBody() { this.bodyMeshes.forEach(m => m.material = this.blackMaterial); }
113
267
  restoreBody() { this.bodyMeshes.forEach(m => m.material = m.userData.originalMat); }
114
268
 
@@ -117,8 +271,22 @@ export class SkinModel {
117
271
  part.rotation.set(0,0,0);
118
272
  if (this.defaultPositions[name]) part.position.copy(this.defaultPositions[name]);
119
273
  }
274
+
275
+ this.playerGroup.position.set(0, 0, 0);
276
+ this.playerGroup.rotation.set(0, 0, 0);
277
+ this.playerGroup.scale.set(1, 1, 1);
278
+
120
279
  if (!pose) return;
280
+
281
+ if (pose.root) {
282
+ if (pose.root.pos) this.playerGroup.position.fromArray(pose.root.pos);
283
+ if (pose.root.rot) this.playerGroup.rotation.fromArray(pose.root.rot);
284
+ if (pose.root.scl) this.playerGroup.scale.fromArray(pose.root.scl);
285
+ }
286
+
121
287
  for (const [name, data] of Object.entries(pose)) {
288
+ if (name === 'root') continue;
289
+
122
290
  if (this.parts[name]) {
123
291
  if(data.rot) this.parts[name].rotation.set(...data.rot);
124
292
  if(data.pos) this.parts[name].position.set(...data.pos);
@@ -129,9 +297,34 @@ export class SkinModel {
129
297
  getPose() {
130
298
  const pose = {};
131
299
  const f = (n) => parseFloat(n.toFixed(3));
300
+
132
301
  for (const [name, part] of Object.entries(this.parts)) {
133
- pose[name] = { rot: [f(part.rotation.x), f(part.rotation.y), f(part.rotation.z)], pos: [f(part.position.x), f(part.position.y), f(part.position.z)] };
302
+ pose[name] = {
303
+ rot: [f(part.rotation.x), f(part.rotation.y), f(part.rotation.z)],
304
+ pos: [f(part.position.x), f(part.position.y), f(part.position.z)]
305
+ };
134
306
  }
307
+
308
+ pose.root = {
309
+ pos: [f(this.playerGroup.position.x), f(this.playerGroup.position.y), f(this.playerGroup.position.z)],
310
+ rot: [f(this.playerGroup.rotation.x), f(this.playerGroup.rotation.y), f(this.playerGroup.rotation.z)],
311
+ scl: [f(this.playerGroup.scale.x), f(this.playerGroup.scale.y), f(this.playerGroup.scale.z)]
312
+ };
313
+
135
314
  return pose;
136
315
  }
316
+
317
+ dispose() {
318
+ if (this.playerGroup) {
319
+ if (this.playerGroup.parent) {
320
+ this.playerGroup.parent.remove(this.playerGroup);
321
+ }
322
+ disposeObjectTree(this.playerGroup);
323
+ }
324
+
325
+ this.parts = {};
326
+ this.glowMeshes = [];
327
+ this.bodyMeshes = [];
328
+ this.playerGroup = null;
329
+ }
137
330
  }