bucciafico-lib 1.0.7 → 1.0.9

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,39 +1,39 @@
1
- {
2
- "name": "bucciafico-lib",
3
- "version": "1.0.7",
4
- "description": "Modular 3D rendering engine for Minecraft skins based on Three.js",
5
- "type": "module",
6
- "main": "./index.js",
7
- "exports": {
8
- ".": "./index.js"
9
- },
10
- "files": [
11
- "src",
12
- "index.js",
13
- "README.md"
14
- ],
15
- "scripts": {
16
- "test": "echo \"Error: no test specified\" && exit 1"
17
- },
18
- "keywords": [
19
- "minecraft",
20
- "3d",
21
- "threejs",
22
- "skin",
23
- "viewer",
24
- "renderer"
25
- ],
26
- "author": "Dawid Maj",
27
- "license": "MIT",
28
- "peerDependencies": {
29
- "three": "^0.160.0"
30
- },
31
- "repository": {
32
- "type": "git",
33
- "url": "git+https://github.com/HappyGFX/bucciafico-lib.git"
34
- },
35
- "bugs": {
36
- "url": "https://github.com/HappyGFX/bucciafico-lib/issues"
37
- },
38
- "homepage": "https://github.com/HappyGFX/bucciafico-lib#readme"
39
- }
1
+ {
2
+ "name": "bucciafico-lib",
3
+ "version": "1.0.9",
4
+ "description": "Modular 3D rendering engine for Minecraft skins based on Three.js",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "exports": {
8
+ ".": "./index.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "index.js",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "test": "echo \"Error: no test specified\" && exit 1"
17
+ },
18
+ "keywords": [
19
+ "minecraft",
20
+ "3d",
21
+ "threejs",
22
+ "skin",
23
+ "viewer",
24
+ "renderer"
25
+ ],
26
+ "author": "Dawid Maj",
27
+ "license": "MIT",
28
+ "peerDependencies": {
29
+ "three": "^0.160.0"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/HappyGFX/bucciafico-lib.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/HappyGFX/bucciafico-lib/issues"
37
+ },
38
+ "homepage": "https://github.com/HappyGFX/bucciafico-lib#readme"
39
+ }
@@ -3,6 +3,9 @@ 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";
8
+ import {createPlaceholderTexture} from "../utils/TextureUtils.js";
6
9
 
7
10
  /**
8
11
  * Core 3D Viewer class.
@@ -30,6 +33,9 @@ export class SkinViewer {
30
33
  ...config
31
34
  };
32
35
 
36
+ this.skinData = null;
37
+ this.capeData = null;
38
+
33
39
  /** @type {Map<string, Object>} Registered plugins. */
34
40
  this.plugins = new Map();
35
41
 
@@ -39,6 +45,12 @@ export class SkinViewer {
39
45
  this.isVisible = true;
40
46
  this.needsRender = true;
41
47
 
48
+ this.events = new EventManager();
49
+
50
+ this.on = this.events.on.bind(this.events);
51
+ this.off = this.events.off.bind(this.events);
52
+ this.emit = this.events.emit.bind(this.events);
53
+
42
54
  // --- 1. RENDERER SETUP ---
43
55
  this.renderer = new THREE.WebGLRenderer({
44
56
  antialias: true,
@@ -62,6 +74,9 @@ export class SkinViewer {
62
74
  this.scene.background = new THREE.Color(this.config.bgColor);
63
75
  }
64
76
 
77
+ this.skinModel = new SkinModel();
78
+ this.scene.add(this.skinModel.getGroup());
79
+
65
80
  this.overlayScene = new THREE.Scene();
66
81
 
67
82
  this.sceneSetup = new SceneSetup(this.scene);
@@ -72,8 +87,7 @@ export class SkinViewer {
72
87
  });
73
88
  this.cameraManager.setEnabled(this.config.cameraEnabled);
74
89
 
75
- this.skinModel = new SkinModel();
76
- this.scene.add(this.skinModel.getGroup());
90
+ this.loadPlaceholderSkin();
77
91
 
78
92
  this.observer = new IntersectionObserver((entries) => {
79
93
  if (entries[0].isIntersecting) {
@@ -88,6 +102,7 @@ export class SkinViewer {
88
102
 
89
103
  // --- 3. START LOOP ---
90
104
  this.animate = this.animate.bind(this);
105
+ this.emit('viewer:ready', this);
91
106
  this.animate();
92
107
  }
93
108
 
@@ -124,14 +139,32 @@ export class SkinViewer {
124
139
  return this.plugins.get(name);
125
140
  }
126
141
 
142
+ loadPlaceholderSkin() {
143
+ const placeholderTex = createPlaceholderTexture();
144
+ this.skinData = null;
145
+ this.resetCape();
146
+ this.skinModel.build(placeholderTex, false, false);
147
+ this.requestRender();
148
+ }
149
+
127
150
  /**
128
151
  * Loads a skin from URL.
129
152
  * @param {string} imageUrl
130
153
  * @returns {Promise<boolean>} isSlim
131
154
  */
132
155
  loadSkin(imageUrl) {
156
+ this.emit('skin:loading', imageUrl);
157
+
133
158
  return new Promise((resolve, reject) => {
134
- new THREE.TextureLoader().load(imageUrl, (texture) => {
159
+ const loader = new THREE.TextureLoader();
160
+ loader.setCrossOrigin('anonymous');
161
+
162
+ loader.load(imageUrl, (texture) => {
163
+ if (this.isDisposed) {
164
+ texture.dispose();
165
+ return;
166
+ }
167
+
135
168
  texture.magFilter = THREE.NearestFilter;
136
169
  texture.colorSpace = THREE.SRGBColorSpace;
137
170
 
@@ -141,13 +174,24 @@ export class SkinViewer {
141
174
  const editor = this.getPlugin('EditorPlugin');
142
175
  if (editor) editor.deselect();
143
176
 
144
- this.skinModel.build(texture, isSlim);
177
+ this.skinModel.build(texture, isSlim, true);
145
178
  this.skinModel.setPose(currentPose);
146
179
  this.skinData = { type: 'url', value: imageUrl };
147
180
 
181
+ const fxPlugin = this.getPlugin('EffectsPlugin');
182
+ if (fxPlugin) {
183
+ fxPlugin.forceUpdate();
184
+ }
185
+
148
186
  this.requestRender();
187
+
188
+ this.emit('skin:loaded', { isSlim, texture });
189
+
149
190
  resolve(isSlim);
150
- }, undefined, reject);
191
+ }, undefined, (err) => {
192
+ this.emit('skin:error', err);
193
+ reject(err);
194
+ });
151
195
  });
152
196
  }
153
197
 
@@ -161,6 +205,101 @@ export class SkinViewer {
161
205
  });
162
206
  }
163
207
 
208
+ /**
209
+ * Loads a cape from URL.
210
+ * @param {string} imageUrl
211
+ */
212
+ loadCape(imageUrl) {
213
+ return new Promise((resolve, reject) => {
214
+ const loader = new THREE.TextureLoader();
215
+ loader.setCrossOrigin('anonymous');
216
+
217
+ loader.load(
218
+ imageUrl,
219
+ (texture) => {
220
+ if (this.isDisposed) {
221
+ texture.dispose();
222
+ return;
223
+ }
224
+
225
+ texture.magFilter = THREE.NearestFilter;
226
+ texture.colorSpace = THREE.SRGBColorSpace;
227
+
228
+ texture.needsUpdate = true;
229
+
230
+ this.skinModel.setCape(texture);
231
+
232
+ const fxPlugin = this.getPlugin('EffectsPlugin');
233
+ if (fxPlugin) {
234
+ fxPlugin.forceUpdate();
235
+ }
236
+
237
+ this.capeData = { type: 'url', value: imageUrl };
238
+
239
+ this.requestRender();
240
+ this.emit('cape:loaded', imageUrl);
241
+ resolve();
242
+ },
243
+ undefined,
244
+ (err) => {
245
+ console.error("Error loading cape texture:", err);
246
+ reject(err);
247
+ }
248
+ );
249
+ });
250
+ }
251
+
252
+
253
+ /**
254
+ * Loads a cape by username using capes.dev API.
255
+ * Supports Official, Optifine, LabyMod, etc.
256
+ * @param {string} username
257
+ */
258
+ async loadCapeByUsername(username) {
259
+ this.emit('cape:loading', username);
260
+
261
+ try {
262
+ const response = await fetch(`https://api.capes.dev/load/${username}`);
263
+
264
+ if (!response.ok) {
265
+ throw new Error(`User not found in capes.dev (Status: ${response.status})`);
266
+ }
267
+
268
+ const data = await response.json();
269
+ let capeUrl = null;
270
+
271
+ if (data.minecraft?.exists && data.minecraft?.imageUrl) {
272
+ capeUrl = data.minecraft.imageUrl;
273
+ } else if (data.optifine?.exists && data.optifine?.imageUrl) {
274
+ capeUrl = data.optifine.imageUrl;
275
+ } else if (data.labymod?.exists && data.labymod?.imageUrl) {
276
+ capeUrl = data.labymod.imageUrl;
277
+ } else if (data.tlauncher?.exists && data.tlauncher?.imageUrl) {
278
+ capeUrl = data.tlauncher.imageUrl;
279
+ }
280
+
281
+ if (capeUrl) {
282
+ await this.loadCape(capeUrl);
283
+ this.capeData = { type: 'username', value: username };
284
+ return true;
285
+ } else {
286
+ this.resetCape();
287
+ return false;
288
+ }
289
+
290
+ } catch (e) {
291
+ this.emit('cape:error', e);
292
+ return false;
293
+ }
294
+ }
295
+
296
+ resetCape() {
297
+ this.skinModel.setCape(null);
298
+ this.capeData = null;
299
+ this.requestRender();
300
+ this.emit('cape:removed');
301
+ }
302
+
164
303
  setPose(poseData) {
165
304
  // Record history if Editor is present
166
305
  const editor = this.getPlugin('EditorPlugin');
@@ -170,6 +309,40 @@ export class SkinViewer {
170
309
  this.requestRender();
171
310
  }
172
311
 
312
+ /**
313
+ * Updates lighting intensity.
314
+ * @param {Object} config - { global, main, fill }
315
+ */
316
+ setEnvironment(config) {
317
+ this.sceneSetup.setLightConfig(config);
318
+ this.requestRender();
319
+ }
320
+
321
+ /**
322
+ * Updates scene configuration at runtime.
323
+ * @param {Object} config
324
+ * @param {boolean} [config.showGrid]
325
+ * @param {boolean} [config.transparent]
326
+ * @param {string|number} [config.bgColor] - Hex color
327
+ */
328
+ updateConfig(config) {
329
+ this.config = { ...this.config, ...config };
330
+
331
+ if (config.showGrid !== undefined) {
332
+ this.sceneSetup.setGridVisible(config.showGrid);
333
+ }
334
+
335
+ if (this.config.transparent) {
336
+ this.scene.background = null;
337
+ this.renderer.setClearAlpha(0);
338
+ } else {
339
+ this.renderer.setClearAlpha(1);
340
+ this.scene.background = new THREE.Color(this.config.bgColor);
341
+ }
342
+
343
+ this.requestRender();
344
+ }
345
+
173
346
  /**
174
347
  * Handles window resize. Should be called by the implementation layer.
175
348
  */
@@ -215,19 +388,37 @@ export class SkinViewer {
215
388
  }
216
389
 
217
390
  dispose() {
391
+ this.emit('viewer:dispose');
218
392
  this.isDisposed = true;
219
393
  this.observer.disconnect();
220
394
 
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
395
  this.plugins.forEach(p => {
229
396
  if (p.dispose) p.dispose();
230
397
  });
231
398
  this.plugins.clear();
399
+
400
+ if (this.skinModel) {
401
+ this.skinModel.dispose();
402
+ }
403
+
404
+ disposeObjectTree(this.scene);
405
+ disposeObjectTree(this.overlayScene);
406
+
407
+ if (this.cameraManager) {
408
+ this.cameraManager.controls.dispose();
409
+ }
410
+
411
+ if (this.renderer) {
412
+ this.renderer.dispose();
413
+ this.renderer.forceContextLoss();
414
+
415
+ if (this.container && this.renderer.domElement) {
416
+ this.container.removeChild(this.renderer.domElement);
417
+ }
418
+ this.renderer.domElement = null;
419
+ this.renderer = null;
420
+ }
421
+
422
+ this.events.dispose();
232
423
  }
233
424
  }
@@ -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
  }