bucciafico-lib 1.0.6 → 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.6",
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
+ }
@@ -12,10 +12,18 @@ import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js';
12
12
  export class PostProcessingManager {
13
13
  constructor(renderer, scene, camera, width, height) {
14
14
  this.scene = scene;
15
+ this.renderer = renderer;
16
+
17
+ this.INTERNAL_HEIGHT = 1080;
18
+
19
+ const ratio = width / height;
20
+ const virtualW = this.INTERNAL_HEIGHT * ratio;
21
+ const virtualH = this.INTERNAL_HEIGHT;
15
22
 
16
23
  // 1. BLOOM COMPOSER (Renders glow map)
17
24
  this.bloomComposer = new EffectComposer(renderer);
18
25
  this.bloomComposer.renderToScreen = false;
26
+ this.bloomComposer.setSize(virtualW, virtualH);
19
27
  this.bloomComposer.addPass(new RenderPass(scene, camera));
20
28
 
21
29
  this.bloomPass = new UnrealBloomPass(new THREE.Vector2(width, height), 1.5, 0.4, 0.85);
@@ -23,6 +31,7 @@ export class PostProcessingManager {
23
31
 
24
32
  // 2. FINAL COMPOSER
25
33
  this.finalComposer = new EffectComposer(renderer);
34
+ this.finalComposer.setSize(width, height);
26
35
  this.finalComposer.addPass(new RenderPass(scene, camera));
27
36
 
28
37
  // 3. OUTLINE PASS (Selection highlight)
@@ -82,9 +91,14 @@ export class PostProcessingManager {
82
91
  }
83
92
 
84
93
  resize(width, height) {
85
- this.bloomComposer.setSize(width, height);
94
+ const ratio = width / height;
95
+
96
+ const virtualH = this.INTERNAL_HEIGHT;
97
+ const virtualW = virtualH * ratio;
98
+
99
+ this.bloomComposer.setSize(virtualW, virtualH);
86
100
  this.finalComposer.setSize(width, height);
87
- this.bloomPass.resolution.set(width, height);
101
+ this.bloomPass.resolution.set(virtualW, virtualH);
88
102
  this.outlinePass.setSize(width, height);
89
103
  }
90
104
 
@@ -117,4 +131,28 @@ export class PostProcessingManager {
117
131
  this.bloomPass.radius = Number(rad);
118
132
  this.bloomPass.threshold = Number(thr);
119
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
+ }
120
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
  }