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.
@@ -39,6 +39,13 @@ export class EditorPlugin {
39
39
  }
40
40
  });
41
41
 
42
+ this.transformControl.addEventListener('change', () => {
43
+ if (this.transformControl.object) {
44
+ this.viewer.emit('transform:change', this.transformControl.object);
45
+ }
46
+ });
47
+
48
+
42
49
  this.viewer.overlayScene.add(this.transformControl);
43
50
  }
44
51
 
@@ -126,7 +133,10 @@ export class EditorPlugin {
126
133
 
127
134
  this.raycaster.setFromCamera(this.mouse, this.viewer.cameraManager.camera);
128
135
 
129
- let objectsToCheck = [...this.viewer.skinModel.getGroup().children];
136
+ let objectsToCheck = [];
137
+
138
+ const playerGroup = this.viewer.skinModel.getGroup();
139
+ if (playerGroup) objectsToCheck.push(playerGroup);
130
140
 
131
141
  const itemsPlugin = this.viewer.getPlugin('ItemsPlugin');
132
142
  if (itemsPlugin) {
@@ -136,20 +146,27 @@ export class EditorPlugin {
136
146
  const intersects = this.raycaster.intersectObjects(objectsToCheck, true);
137
147
 
138
148
  if (intersects.length > 0) {
139
- let target = intersects[0].object;
149
+ let hitObject = intersects[0].object;
150
+ let logicalTarget = hitObject;
140
151
 
141
- const skinGroup = this.viewer.skinModel.getGroup();
142
- let temp = target;
143
- while(temp) {
144
- if (temp.parent === skinGroup) {
145
- target = temp;
152
+ while (logicalTarget.parent) {
153
+ if (logicalTarget.parent === playerGroup) {
154
+ break;
155
+ }
156
+ if (logicalTarget.parent.type === 'Scene') {
157
+ break;
158
+ }
159
+ if (logicalTarget === playerGroup) {
146
160
  break;
147
161
  }
148
- temp = temp.parent;
162
+
163
+ logicalTarget = logicalTarget.parent;
149
164
  }
150
165
 
151
- if (this.transformControl.object !== target) {
152
- this.selectObject(target);
166
+ if (!logicalTarget) logicalTarget = hitObject;
167
+
168
+ if (this.transformControl.object !== logicalTarget) {
169
+ this.selectObject(logicalTarget);
153
170
  }
154
171
  } else {
155
172
  this.deselect();
@@ -164,7 +181,7 @@ export class EditorPlugin {
164
181
  if (fx) fx.setSelected(obj);
165
182
 
166
183
  // Callback support (can be injected)
167
- if (this.viewer.onSelectionChanged) this.viewer.onSelectionChanged(obj);
184
+ this.viewer.emit('selection:change', obj);
168
185
  }
169
186
 
170
187
  deselect() {
@@ -173,7 +190,7 @@ export class EditorPlugin {
173
190
  const fx = this.viewer.getPlugin('EffectsPlugin');
174
191
  if (fx) fx.setSelected(null);
175
192
 
176
- if (this.viewer.onSelectionChanged) this.viewer.onSelectionChanged(null);
193
+ this.viewer.emit('selection:cleared');
177
194
  }
178
195
 
179
196
  /**
@@ -207,7 +224,21 @@ export class EditorPlugin {
207
224
  }
208
225
 
209
226
  dispose() {
210
- this.viewer.renderer.domElement.removeEventListener('pointerdown', this.onPointerDown);
211
- this.transformControl.dispose();
227
+ if (this.viewer.renderer.domElement) {
228
+ this.viewer.renderer.domElement.removeEventListener('pointerdown', this.onPointerDown);
229
+ this.viewer.renderer.domElement.removeEventListener('pointermove', this.onPointerMove);
230
+ }
231
+
232
+ if (this.transformControl) {
233
+ this.transformControl.detach();
234
+ this.transformControl.object = undefined;
235
+
236
+ if (this.transformControl.parent) {
237
+ this.transformControl.parent.remove(this.transformControl);
238
+ }
239
+
240
+ this.transformControl.dispose();
241
+ this.transformControl = null;
242
+ }
212
243
  }
213
244
  }
@@ -8,7 +8,13 @@ import { PostProcessingManager } from '../managers/PostProcessingManager.js';
8
8
  export class EffectsPlugin {
9
9
  constructor() {
10
10
  this.name = 'EffectsPlugin';
11
- this.isEnabled = false;
11
+ this.state = {
12
+ enabled: false,
13
+ strength: 0,
14
+ radius: 0,
15
+ height: 0.5,
16
+ thickness: 4
17
+ };
12
18
  }
13
19
 
14
20
  init(viewer) {
@@ -26,15 +32,36 @@ export class EffectsPlugin {
26
32
  * @param {Object} config - { enabled, strength, radius, height, thickness }
27
33
  */
28
34
  updateConfig(config) {
29
- this.isEnabled = config.enabled;
35
+ this.state = { ...this.state, ...config };
36
+
37
+ this._applyToScene();
38
+ }
39
+
40
+ /**
41
+ * Re-applies the current internal state to the 3D model.
42
+ * Useful when the model has been rebuilt (skin change).
43
+ */
44
+ forceUpdate() {
45
+ this._applyToScene();
46
+ }
47
+
48
+ /**
49
+ * Internal method to push state to shaders and composer.
50
+ */
51
+ _applyToScene() {
52
+ const config = this.state;
30
53
  const skin = this.viewer.skinModel;
31
54
 
32
- // Update Shader Materials
33
55
  skin.setGlowEffect(config.enabled);
34
- if (config.thickness !== undefined) skin.updateBorderThickness(config.thickness);
35
- if (config.height !== undefined) skin.updateGlowHeight(config.height);
36
56
 
37
- // Update Composer Pass
57
+ skin.updateBorderThickness(config.thickness);
58
+ skin.updateGlowHeight(config.height);
59
+
60
+ const itemsPlugin = this.viewer.getPlugin('ItemsPlugin');
61
+ if (itemsPlugin) {
62
+ itemsPlugin.updateAllGlow(config);
63
+ }
64
+
38
65
  this.composer.setBloom(config.enabled, config.strength, config.radius, 0.85);
39
66
  }
40
67
 
@@ -58,13 +85,11 @@ export class EffectsPlugin {
58
85
  const items = itemsPlugin ? itemsPlugin.items : [];
59
86
 
60
87
  this.composer.renderSelective(
61
- // 1. Prepare Bloom pass (Hide non-glowing elements)
62
88
  () => {
63
89
  skin.darkenBody();
64
90
  this.viewer.sceneSetup.setGridVisible(false);
65
91
  items.forEach(i => i.material = skin.blackMaterial);
66
92
  },
67
- // 2. Restore Scene for main pass
68
93
  () => {
69
94
  skin.restoreBody();
70
95
  this.viewer.sceneSetup.setGridVisible(this.viewer.config.showGrid);
@@ -75,6 +100,13 @@ export class EffectsPlugin {
75
100
  );
76
101
  }
77
102
 
103
+ /**
104
+ * Returns the current configuration state.
105
+ */
106
+ getConfig() {
107
+ return { ...this.state };
108
+ }
109
+
78
110
  /**
79
111
  * Generates a transparent PNG screenshot.
80
112
  * Temporarily resizes renderer if width/height are provided.
@@ -132,18 +164,11 @@ export class EffectsPlugin {
132
164
  return dataUrl;
133
165
  }
134
166
 
135
- getConfig() {
136
- const skin = this.viewer.skinModel;
137
- const glowMesh = skin.glowMeshes[0];
138
-
139
- return {
140
- enabled: this.isEnabled,
141
- strength: this.composer.bloomPass.strength,
142
- radius: this.composer.bloomPass.radius,
143
-
144
- height: glowMesh ? glowMesh.userData.glowMat.uniforms.gradientLimit.value : 0.5,
145
- thickness: glowMesh ? glowMesh.userData.glowMat.uniforms.thickness.value / 0.05 : 4
146
- };
167
+ dispose() {
168
+ if (this.composer) {
169
+ this.composer.dispose();
170
+ this.composer = null;
171
+ }
147
172
  }
148
173
 
149
174
  }
@@ -20,11 +20,11 @@ export class IOPlugin {
20
20
  * @param {boolean} options.pose - Include character pose.
21
21
  * @param {boolean} options.items - Include items.
22
22
  */
23
- exportState(options = { skin: true, camera: true, effects: true, pose: true, items: true }) {
23
+ exportState(options = { skin: true, camera: true, effects: true, pose: true, items: true, env: true }) {
24
24
  const state = {
25
25
  meta: {
26
26
  generator: "Bucciafico Studio",
27
- version: "1.0.7",
27
+ version: "1.0.8",
28
28
  timestamp: Date.now()
29
29
  },
30
30
  core: {}
@@ -32,6 +32,7 @@ export class IOPlugin {
32
32
 
33
33
  if (options.skin) {
34
34
  state.core.skin = this.viewer.skinData || null;
35
+ state.core.cape = this.viewer.capeData || null;
35
36
  }
36
37
 
37
38
  // 2. Camera & Config
@@ -44,12 +45,17 @@ export class IOPlugin {
44
45
  };
45
46
  }
46
47
 
47
- // 3. Pose
48
+ // 3. Environment
49
+ if (options.env) {
50
+ state.environment = this.viewer.sceneSetup.getLightConfig();
51
+ }
52
+
53
+ // 4. Pose
48
54
  if (options.pose) {
49
55
  state.pose = this.viewer.skinModel.getPose();
50
56
  }
51
57
 
52
- // 4. Effects
58
+ // 5. Effects
53
59
  if (options.effects) {
54
60
  const effectsPlugin = this.viewer.getPlugin('EffectsPlugin');
55
61
  if (effectsPlugin) {
@@ -59,7 +65,7 @@ export class IOPlugin {
59
65
  }
60
66
  }
61
67
 
62
- // 5. Items
68
+ // 6. Items
63
69
  if (options.items) {
64
70
  const itemsPlugin = this.viewer.getPlugin('ItemsPlugin');
65
71
  if (itemsPlugin && itemsPlugin.items.length > 0) {
@@ -67,6 +73,7 @@ export class IOPlugin {
67
73
  name: item.name,
68
74
  uuid: item.uuid,
69
75
  sourceUrl: item.userData.sourceUrl || null,
76
+ parentId: item.userData.parentId || null,
70
77
  transform: {
71
78
  pos: item.position.toArray(),
72
79
  rot: item.rotation.toArray(),
@@ -103,27 +110,42 @@ export class IOPlugin {
103
110
  this.viewer.cameraManager.loadSettingsJSON(data.core.camera);
104
111
  }
105
112
 
106
- // 3. Skin (Async)
113
+ // 3. Environment
114
+ if (data.environment) {
115
+ this.viewer.setEnvironment(data.environment);
116
+ }
117
+
118
+ const loadPromises = [];
119
+
120
+ // 4. Skin/Cape (Async)
107
121
  if (data.core?.skin) {
108
122
  const skinInfo = data.core.skin;
109
- try {
110
- if (skinInfo.type === 'username') {
111
- await this.viewer.loadSkinByUsername(skinInfo.value);
112
- } else if (skinInfo.value) {
113
- await this.viewer.loadSkin(skinInfo.value);
114
- }
115
- } catch (e) {
116
- console.warn("Failed to load skin from import:", e);
123
+ if (skinInfo.type === 'username') {
124
+ loadPromises.push(this.viewer.loadSkinByUsername(skinInfo.value));
125
+ } else if (skinInfo.value) {
126
+ loadPromises.push(this.viewer.loadSkin(skinInfo.value));
127
+ }
128
+ }
129
+
130
+ if (data.core?.cape) {
131
+ const capeInfo = data.core.cape;
132
+ if (capeInfo.type === 'username') {
133
+ loadPromises.push(this.viewer.loadCapeByUsername(capeInfo.value));
134
+ } else if (capeInfo.value) {
135
+ loadPromises.push(this.viewer.loadCape(capeInfo.value));
117
136
  }
137
+ } else {
138
+ // Jeśli w JSON nie ma peleryny, a w viewerze jest, to ją czyścimy
139
+ this.viewer.resetCape();
118
140
  }
119
141
 
120
- // 4. Effects
142
+ // 5. Effects
121
143
  if (data.effects?.backlight) {
122
144
  const fx = this.viewer.getPlugin('EffectsPlugin');
123
145
  if (fx) fx.updateConfig(data.effects.backlight);
124
146
  }
125
147
 
126
- // 5. Items (Async & Complex)
148
+ // 6. Items (Async & Complex)
127
149
  const itemsPlugin = this.viewer.getPlugin('ItemsPlugin');
128
150
  if (itemsPlugin) {
129
151
  [...itemsPlugin.items].forEach(item => itemsPlugin.removeItem(item));
@@ -134,6 +156,11 @@ export class IOPlugin {
134
156
 
135
157
  try {
136
158
  const mesh = await itemsPlugin.addItem(itemData.sourceUrl, itemData.name);
159
+
160
+ if (itemData.parentId) {
161
+ itemsPlugin.attachItem(mesh, itemData.parentId);
162
+ }
163
+
137
164
  // Apply Transform
138
165
  if (itemData.transform) {
139
166
  mesh.position.fromArray(itemData.transform.pos);
@@ -148,9 +175,11 @@ export class IOPlugin {
148
175
  }
149
176
  }
150
177
 
151
- // 6. Pose
178
+ // 7. Pose
152
179
  if (data.pose) {
153
180
  this.viewer.setPose(data.pose);
154
181
  }
182
+
183
+ await Promise.all(loadPromises);
155
184
  }
156
185
  }
@@ -1,4 +1,7 @@
1
+ import * as THREE from 'three';
1
2
  import { ItemFactory } from '../objects/ItemFactory.js';
3
+ import {disposeObjectTree} from "../utils/ThreeUtils.js";
4
+ import {createGlowMaterial} from "../materials/GlowMaterial.js";
2
5
 
3
6
  /**
4
7
  * Plugin responsible for managing 3D Items (Swords, Blocks).
@@ -8,12 +11,68 @@ export class ItemsPlugin {
8
11
  this.name = 'ItemsPlugin';
9
12
  /** @type {Array<THREE.Mesh>} List of current items on scene */
10
13
  this.items = [];
14
+
15
+ this.LAYERS_COUNT = 20;
11
16
  }
12
17
 
13
18
  init(viewer) {
14
19
  this.viewer = viewer;
15
20
  }
16
21
 
22
+ attachItem(itemMesh, partName) {
23
+ const editor = this.viewer.getPlugin('EditorPlugin');
24
+ if (editor) editor.saveHistory();
25
+
26
+ const skinModel = this.viewer.skinModel;
27
+
28
+ if (!partName) {
29
+ this.viewer.scene.attach(itemMesh);
30
+ itemMesh.userData.parentId = null;
31
+ }
32
+
33
+ else if (skinModel.parts[partName]) {
34
+ const targetGroup = skinModel.parts[partName];
35
+ targetGroup.attach(itemMesh);
36
+ itemMesh.userData.parentId = partName;
37
+ }
38
+
39
+ if (this.viewer.emit) this.viewer.emit('transform:change', itemMesh);
40
+ }
41
+
42
+ _addGlowShells(mesh) {
43
+ mesh.geometry.computeBoundingBox();
44
+ const size = new THREE.Vector3();
45
+ mesh.geometry.boundingBox.getSize(size);
46
+ const itemHeight = size.y || 1;
47
+
48
+ const glowLayers = [];
49
+
50
+ const glowGroup = new THREE.Group();
51
+ glowGroup.name = "GlowShells";
52
+
53
+ const shellGeo = mesh.geometry.clone();
54
+
55
+ for (let i = 0; i < this.LAYERS_COUNT; i++) {
56
+ const glowMat = createGlowMaterial(itemHeight);
57
+
58
+ glowMat.uniforms.thickness.value = 0;
59
+ glowMat.uniforms.opacity.value = 0;
60
+
61
+ glowMat.polygonOffset = true;
62
+ glowMat.polygonOffsetFactor = i * 0.1;
63
+
64
+ const layerMesh = new THREE.Mesh(shellGeo, glowMat);
65
+ layerMesh.userData.isGlowLayer = true;
66
+ layerMesh.userData.glowMat = glowMat;
67
+
68
+ glowLayers.push(layerMesh);
69
+ glowGroup.add(layerMesh);
70
+ }
71
+
72
+ mesh.add(glowGroup);
73
+ mesh.userData.glowLayers = glowLayers;
74
+ }
75
+
17
76
  /**
18
77
  * Creates and adds an item to the scene.
19
78
  * @param {string} url - Texture URL.
@@ -26,11 +85,22 @@ export class ItemsPlugin {
26
85
  return ItemFactory.createFromURL(url, name).then(mesh => {
27
86
  mesh.position.set(8, 8, 8);
28
87
  mesh.userData.sourceUrl = url;
88
+
89
+ this._addGlowShells(mesh);
90
+
91
+ const fx = this.viewer.getPlugin('EffectsPlugin');
92
+ if (fx) {
93
+ const config = fx.getConfig();
94
+ this.updateItemGlow(mesh, config);
95
+ }
96
+
29
97
  this.viewer.scene.add(mesh);
30
98
  this.items.push(mesh);
31
99
 
32
- // Auto-select if editor is present
33
100
  if (editor) editor.selectObject(mesh);
101
+
102
+ if (this.viewer.emit) this.viewer.emit('items:added', mesh);
103
+
34
104
  return mesh;
35
105
  });
36
106
  }
@@ -42,7 +112,44 @@ export class ItemsPlugin {
42
112
  this.viewer.scene.remove(mesh);
43
113
  this.items = this.items.filter(i => i !== mesh);
44
114
 
115
+ disposeObjectTree(mesh);
116
+
45
117
  if (editor) editor.deselect();
118
+
119
+ if (this.viewer.emit) this.viewer.emit('items:removed', mesh);
120
+ }
121
+
122
+ // --- EFFECTS ---
123
+
124
+ updateAllGlow(config) {
125
+ this.items.forEach(item => {
126
+ this.updateItemGlow(item, config);
127
+ });
128
+ }
129
+
130
+ updateItemGlow(item, config) {
131
+ if (!item.userData.glowLayers) return;
132
+
133
+ const maxThickness = (config.thickness || 4) * 0.05;
134
+ const heightLimit = config.height !== undefined ? config.height : 0.5;
135
+ const enabled = config.enabled;
136
+
137
+ item.userData.glowLayers.forEach((layer, i) => {
138
+ const mat = layer.userData.glowMat;
139
+ if (!mat) return;
140
+
141
+ const progress = (i + 1) / this.LAYERS_COUNT;
142
+ mat.uniforms.thickness.value = maxThickness * progress;
143
+
144
+ mat.uniforms.gradientLimit.value = heightLimit;
145
+
146
+ if (!enabled) {
147
+ mat.uniforms.opacity.value = 0.0;
148
+ } else {
149
+ const baseOpacity = 1.2 / this.LAYERS_COUNT;
150
+ mat.uniforms.opacity.value = baseOpacity;
151
+ }
152
+ });
46
153
  }
47
154
 
48
155
  // --- SNAPSHOT HELPERS ---
@@ -51,6 +158,7 @@ export class ItemsPlugin {
51
158
  return this.items.map(item => ({
52
159
  name: item.name,
53
160
  uuid: item.uuid,
161
+ parentId: item.userData.parentId || null,
54
162
  pos: item.position.toArray(),
55
163
  rot: item.rotation.toArray(),
56
164
  scale: item.scale.toArray()
@@ -59,13 +167,24 @@ export class ItemsPlugin {
59
167
 
60
168
  restoreSnapshot(itemsState) {
61
169
  itemsState.forEach(state => {
62
- // Try to find the item by UUID first, then Name
63
170
  const item = this.items.find(i => i.uuid === state.uuid || i.name === state.name);
64
171
  if (item) {
172
+ if (state.parentId !== item.userData.parentId) {
173
+ this.attachItem(item, state.parentId);
174
+ }
175
+
65
176
  item.position.fromArray(state.pos);
66
177
  item.rotation.fromArray(state.rot);
67
178
  item.scale.fromArray(state.scale);
68
179
  }
69
180
  });
70
181
  }
182
+
183
+ dispose() {
184
+ this.items.forEach(mesh => {
185
+ this.viewer.scene.remove(mesh);
186
+ disposeObjectTree(mesh);
187
+ });
188
+ this.items = [];
189
+ }
71
190
  }
@@ -1,33 +1,33 @@
1
1
  /**
2
2
  * Calculates UV coordinates for standard Minecraft skin layout.
3
3
  */
4
- export function getUV(x, y, w, h) {
5
- const imgW = 64;
6
- const imgH = 64;
7
- return { u0: x/imgW, u1: (x+w)/imgW, v0: (imgH-y-h)/imgH, v1: (imgH-y)/imgH };
4
+ export function getUV(x, y, w, h, imgW = 64, imgH = 64) {
5
+ return {
6
+ u0: x / imgW,
7
+ u1: (x + w) / imgW,
8
+ v0: (imgH - y - h) / imgH,
9
+ v1: (imgH - y) / imgH
10
+ };
8
11
  }
9
12
 
10
13
  /**
11
14
  * Maps UV coordinates to a box geometry (Cube mapping).
15
+ * @param {THREE.BufferGeometry} geometry
16
+ * @param {number} x - Texture X
17
+ * @param {number} y - Texture Y
18
+ * @param {number} w - Width
19
+ * @param {number} h - Height
20
+ * @param {number} d - Depth
21
+ * @param {number} [imgW=64] - Texture Width
22
+ * @param {number} [imgH=64] - Texture Height
12
23
  */
13
- export function applySkinUVs(geometry, x, y, w, h, d) {
24
+ export function applySkinUVs(geometry, x, y, w, h, d, imgW = 64, imgH = 64) {
14
25
  const uvAttr = geometry.attributes.uv;
15
26
 
16
- /**
17
- * Helper to map a single face.
18
- * @param {number} idx - Face index (0-5).
19
- * @param {number} uX - Texture X.
20
- * @param {number} uY - Texture Y.
21
- * @param {number} uW - Texture Width.
22
- * @param {number} uH - Texture Height.
23
- * @param {boolean} flipX - Mirror horizontally.
24
- * @param {boolean} flipY - Mirror vertically.
25
- */
26
27
  const map = (idx, uX, uY, uW, uH, flipX = false, flipY = false) => {
27
- const uv = getUV(uX, uY, uW, uH);
28
+ const uv = getUV(uX, uY, uW, uH, imgW, imgH);
28
29
  const i = idx * 4;
29
30
 
30
- // Handle flipping
31
31
  const u0 = flipX ? uv.u1 : uv.u0;
32
32
  const u1 = flipX ? uv.u0 : uv.u1;
33
33
  const v0 = flipY ? uv.v1 : uv.v0;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Recursively disposes of a Three.js object and its children.
3
+ * Frees memory for Geometries, Materials, and Textures.
4
+ * @param {THREE.Object3D} object - The object to clean up.
5
+ */
6
+ export function disposeObjectTree(object) {
7
+ if (!object) return;
8
+
9
+ object.traverse((child) => {
10
+ if (child.geometry) {
11
+ child.geometry.dispose();
12
+ }
13
+
14
+ if (child.material) {
15
+ const materials = Array.isArray(child.material) ? child.material : [child.material];
16
+
17
+ materials.forEach((mat) => {
18
+ for (const key in mat) {
19
+ if (mat[key] && mat[key].isTexture) {
20
+ mat[key].dispose();
21
+ }
22
+ }
23
+ mat.dispose();
24
+ });
25
+ }
26
+ });
27
+ }