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 +1 -1
- package/src/core/SkinViewer.js +154 -9
- package/src/managers/EventManager.js +74 -0
- package/src/managers/PostProcessingManager.js +40 -2
- package/src/objects/SceneSetup.js +49 -8
- package/src/objects/SkinModel.js +205 -12
- package/src/plugins/EditorPlugin.js +45 -14
- package/src/plugins/EffectsPlugin.js +45 -20
- package/src/plugins/IOPlugin.js +46 -17
- package/src/plugins/ItemsPlugin.js +121 -2
- package/src/utils/SkinUtils.js +17 -17
- package/src/utils/ThreeUtils.js +27 -0
- package/src/utils/Voxelizer.js +139 -33
package/package.json
CHANGED
package/src/core/SkinViewer.js
CHANGED
|
@@ -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()
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
16
|
-
this.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
this.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
}
|