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 +1 -1
- package/src/core/SkinViewer.js +154 -9
- package/src/managers/EventManager.js +74 -0
- package/src/managers/PostProcessingManager.js +24 -0
- 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
|
@@ -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 = [
|
|
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
|
|
149
|
+
let hitObject = intersects[0].object;
|
|
150
|
+
let logicalTarget = hitObject;
|
|
140
151
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
162
|
+
|
|
163
|
+
logicalTarget = logicalTarget.parent;
|
|
149
164
|
}
|
|
150
165
|
|
|
151
|
-
if (
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
211
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
}
|
package/src/plugins/IOPlugin.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
}
|
package/src/utils/SkinUtils.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
}
|