bucciafico-lib 1.0.4-BETA
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/README.md +178 -0
- package/index.js +5 -0
- package/package.json +39 -0
- package/src/core/SkinViewer.js +195 -0
- package/src/managers/CameraManager.js +52 -0
- package/src/managers/HistoryManager.js +65 -0
- package/src/managers/PostProcessingManager.js +107 -0
- package/src/materials/GlowMaterial.js +56 -0
- package/src/objects/ItemFactory.js +83 -0
- package/src/objects/SceneSetup.js +32 -0
- package/src/objects/SkinModel.js +137 -0
- package/src/plugins/EditorPlugin.js +213 -0
- package/src/plugins/EffectsPlugin.js +149 -0
- package/src/plugins/IOPlugin.js +156 -0
- package/src/plugins/ItemsPlugin.js +71 -0
- package/src/utils/SkinUtils.js +66 -0
- package/src/utils/Voxelizer.js +58 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin responsible for Import/Export of the entire scene state.
|
|
3
|
+
* It gathers data from Core and all other Plugins to create a comprehensive JSON snapshot.
|
|
4
|
+
*/
|
|
5
|
+
export class IOPlugin {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.name = 'IOPlugin';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
init(viewer) {
|
|
11
|
+
this.viewer = viewer;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Exports the scene state based on provided options.
|
|
16
|
+
* @param {Object} options - Filters for export.
|
|
17
|
+
* @param {boolean} options.skin - Include skin source (username/url).
|
|
18
|
+
* @param {boolean} options.camera - Include camera position/target.
|
|
19
|
+
* @param {boolean} options.effects - Include effects config.
|
|
20
|
+
* @param {boolean} options.pose - Include character pose.
|
|
21
|
+
* @param {boolean} options.items - Include items.
|
|
22
|
+
*/
|
|
23
|
+
exportState(options = { skin: true, camera: true, effects: true, pose: true, items: true }) {
|
|
24
|
+
const state = {
|
|
25
|
+
meta: {
|
|
26
|
+
generator: "Bucciafico Studio",
|
|
27
|
+
version: "1.0.4-BETA",
|
|
28
|
+
timestamp: Date.now()
|
|
29
|
+
},
|
|
30
|
+
core: {}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (options.skin) {
|
|
34
|
+
state.core.skin = this.viewer.skinData || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Camera & Config
|
|
38
|
+
if (options.camera) {
|
|
39
|
+
state.core.camera = this.viewer.cameraManager.getSettingsJSON();
|
|
40
|
+
state.core.config = {
|
|
41
|
+
bgColor: this.viewer.config.bgColor,
|
|
42
|
+
transparent: this.viewer.config.transparent,
|
|
43
|
+
showGrid: this.viewer.config.showGrid
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Pose
|
|
48
|
+
if (options.pose) {
|
|
49
|
+
state.pose = this.viewer.skinModel.getPose();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 4. Effects
|
|
53
|
+
if (options.effects) {
|
|
54
|
+
const effectsPlugin = this.viewer.getPlugin('EffectsPlugin');
|
|
55
|
+
if (effectsPlugin) {
|
|
56
|
+
state.effects = {
|
|
57
|
+
backlight: effectsPlugin.getConfig()
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 5. Items
|
|
63
|
+
if (options.items) {
|
|
64
|
+
const itemsPlugin = this.viewer.getPlugin('ItemsPlugin');
|
|
65
|
+
if (itemsPlugin && itemsPlugin.items.length > 0) {
|
|
66
|
+
state.items = itemsPlugin.items.map(item => ({
|
|
67
|
+
name: item.name,
|
|
68
|
+
uuid: item.uuid,
|
|
69
|
+
sourceUrl: item.userData.sourceUrl || null,
|
|
70
|
+
transform: {
|
|
71
|
+
pos: item.position.toArray(),
|
|
72
|
+
rot: item.rotation.toArray(),
|
|
73
|
+
scale: item.scale.toArray()
|
|
74
|
+
}
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return state;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Imports a full state from a JSON object.
|
|
84
|
+
* Reconstructs the scene step-by-step.
|
|
85
|
+
* @param {Object|string} jsonData
|
|
86
|
+
* @returns {Promise<void>}
|
|
87
|
+
*/
|
|
88
|
+
async importState(jsonData) {
|
|
89
|
+
const data = typeof jsonData === 'string' ? JSON.parse(jsonData) : jsonData;
|
|
90
|
+
|
|
91
|
+
// 1. Core Config (Background, Grid)
|
|
92
|
+
if (data.core?.config) {
|
|
93
|
+
const cfg = data.core.config;
|
|
94
|
+
this.viewer.config.showGrid = cfg.showGrid;
|
|
95
|
+
this.viewer.sceneSetup.setGridVisible(cfg.showGrid);
|
|
96
|
+
if (!cfg.transparent && cfg.bgColor) {
|
|
97
|
+
this.viewer.scene.background.setHex(cfg.bgColor);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. Camera
|
|
102
|
+
if (data.core?.camera) {
|
|
103
|
+
this.viewer.cameraManager.loadSettingsJSON(data.core.camera);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. Skin (Async)
|
|
107
|
+
if (data.core?.skin) {
|
|
108
|
+
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);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 4. Effects
|
|
121
|
+
if (data.effects?.backlight) {
|
|
122
|
+
const fx = this.viewer.getPlugin('EffectsPlugin');
|
|
123
|
+
if (fx) fx.updateConfig(data.effects.backlight);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 5. Items (Async & Complex)
|
|
127
|
+
const itemsPlugin = this.viewer.getPlugin('ItemsPlugin');
|
|
128
|
+
if (itemsPlugin) {
|
|
129
|
+
[...itemsPlugin.items].forEach(item => itemsPlugin.removeItem(item));
|
|
130
|
+
|
|
131
|
+
if (data.items && Array.isArray(data.items)) {
|
|
132
|
+
const promises = data.items.map(async (itemData) => {
|
|
133
|
+
if (!itemData.sourceUrl) return;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const mesh = await itemsPlugin.addItem(itemData.sourceUrl, itemData.name);
|
|
137
|
+
// Apply Transform
|
|
138
|
+
if (itemData.transform) {
|
|
139
|
+
mesh.position.fromArray(itemData.transform.pos);
|
|
140
|
+
mesh.rotation.fromArray(itemData.transform.rot);
|
|
141
|
+
mesh.scale.fromArray(itemData.transform.scale);
|
|
142
|
+
}
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.warn(`Failed to import item ${itemData.name}:`, e);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
await Promise.all(promises);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 6. Pose
|
|
152
|
+
if (data.pose) {
|
|
153
|
+
this.viewer.setPose(data.pose);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ItemFactory } from '../objects/ItemFactory.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin responsible for managing 3D Items (Swords, Blocks).
|
|
5
|
+
*/
|
|
6
|
+
export class ItemsPlugin {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.name = 'ItemsPlugin';
|
|
9
|
+
/** @type {Array<THREE.Mesh>} List of current items on scene */
|
|
10
|
+
this.items = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
init(viewer) {
|
|
14
|
+
this.viewer = viewer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates and adds an item to the scene.
|
|
19
|
+
* @param {string} url - Texture URL.
|
|
20
|
+
* @param {string} name - Item name.
|
|
21
|
+
*/
|
|
22
|
+
addItem(url, name) {
|
|
23
|
+
const editor = this.viewer.getPlugin('EditorPlugin');
|
|
24
|
+
if (editor) editor.saveHistory();
|
|
25
|
+
|
|
26
|
+
return ItemFactory.createFromURL(url, name).then(mesh => {
|
|
27
|
+
mesh.position.set(8, 8, 8);
|
|
28
|
+
mesh.userData.sourceUrl = url;
|
|
29
|
+
this.viewer.scene.add(mesh);
|
|
30
|
+
this.items.push(mesh);
|
|
31
|
+
|
|
32
|
+
// Auto-select if editor is present
|
|
33
|
+
if (editor) editor.selectObject(mesh);
|
|
34
|
+
return mesh;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
removeItem(mesh) {
|
|
39
|
+
const editor = this.viewer.getPlugin('EditorPlugin');
|
|
40
|
+
if (editor) editor.saveHistory();
|
|
41
|
+
|
|
42
|
+
this.viewer.scene.remove(mesh);
|
|
43
|
+
this.items = this.items.filter(i => i !== mesh);
|
|
44
|
+
|
|
45
|
+
if (editor) editor.deselect();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- SNAPSHOT HELPERS ---
|
|
49
|
+
|
|
50
|
+
getSnapshot() {
|
|
51
|
+
return this.items.map(item => ({
|
|
52
|
+
name: item.name,
|
|
53
|
+
uuid: item.uuid,
|
|
54
|
+
pos: item.position.toArray(),
|
|
55
|
+
rot: item.rotation.toArray(),
|
|
56
|
+
scale: item.scale.toArray()
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
restoreSnapshot(itemsState) {
|
|
61
|
+
itemsState.forEach(state => {
|
|
62
|
+
// Try to find the item by UUID first, then Name
|
|
63
|
+
const item = this.items.find(i => i.uuid === state.uuid || i.name === state.name);
|
|
64
|
+
if (item) {
|
|
65
|
+
item.position.fromArray(state.pos);
|
|
66
|
+
item.rotation.fromArray(state.rot);
|
|
67
|
+
item.scale.fromArray(state.scale);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculates UV coordinates for standard Minecraft skin layout.
|
|
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 };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Maps UV coordinates to a box geometry (Cube mapping).
|
|
12
|
+
*/
|
|
13
|
+
export function applySkinUVs(geometry, x, y, w, h, d) {
|
|
14
|
+
const uvAttr = geometry.attributes.uv;
|
|
15
|
+
|
|
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
|
+
const map = (idx, uX, uY, uW, uH, flipX = false, flipY = false) => {
|
|
27
|
+
const uv = getUV(uX, uY, uW, uH);
|
|
28
|
+
const i = idx * 4;
|
|
29
|
+
|
|
30
|
+
// Handle flipping
|
|
31
|
+
const u0 = flipX ? uv.u1 : uv.u0;
|
|
32
|
+
const u1 = flipX ? uv.u0 : uv.u1;
|
|
33
|
+
const v0 = flipY ? uv.v1 : uv.v0;
|
|
34
|
+
const v1 = flipY ? uv.v0 : uv.v1;
|
|
35
|
+
|
|
36
|
+
uvAttr.setXY(i+0, u0, v1);
|
|
37
|
+
uvAttr.setXY(i+1, u1, v1);
|
|
38
|
+
uvAttr.setXY(i+2, u0, v0);
|
|
39
|
+
uvAttr.setXY(i+3, u1, v0);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
map(0, x + d + w, y + d, d, h); // Right
|
|
43
|
+
map(1, x, y + d, d, h); // Left
|
|
44
|
+
map(2, x + d, y, w, d); // Top
|
|
45
|
+
map(3, x + d + w, y, w, d, false, true); // Bottom
|
|
46
|
+
map(4, x + d, y + d, w, h); // Front
|
|
47
|
+
map(5, x + d + w + d, y + d, w, h); // Back
|
|
48
|
+
|
|
49
|
+
uvAttr.needsUpdate = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Detects if a skin is Slim (Alex model) by checking the pixel at (55, 20).
|
|
54
|
+
* If transparent, it's Slim. If opaque, it's Classic.
|
|
55
|
+
* @param {HTMLImageElement} image
|
|
56
|
+
* @returns {boolean} True if Slim.
|
|
57
|
+
*/
|
|
58
|
+
export function detectSlimSkin(image) {
|
|
59
|
+
const canvas = document.createElement('canvas');
|
|
60
|
+
canvas.width = image.width;
|
|
61
|
+
canvas.height = image.height;
|
|
62
|
+
const ctx = canvas.getContext('2d');
|
|
63
|
+
ctx.drawImage(image, 0, 0);
|
|
64
|
+
// Check specific pixel transparency
|
|
65
|
+
return ctx.getImageData(55, 20, 1, 1).data[3] === 0;
|
|
66
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts the 2nd layer of a skin (Hat, Jacket) into 3D Voxels.
|
|
6
|
+
* This gives the skin actual depth instead of flat planes.
|
|
7
|
+
* @param {THREE.Texture} texture
|
|
8
|
+
* @param {Object} layerDef - Definition of UVs and dimensions.
|
|
9
|
+
* @returns {THREE.BufferGeometry|null} Merged geometry of all voxels.
|
|
10
|
+
*/
|
|
11
|
+
export function createVoxelLayer(texture, layerDef) {
|
|
12
|
+
const canvas = document.createElement('canvas');
|
|
13
|
+
canvas.width = 64; canvas.height = 64;
|
|
14
|
+
const ctx = canvas.getContext('2d');
|
|
15
|
+
ctx.drawImage(texture.image, 0, 0);
|
|
16
|
+
const imgData = ctx.getImageData(0, 0, 64, 64).data;
|
|
17
|
+
|
|
18
|
+
const geometries = [];
|
|
19
|
+
const { outer } = layerDef.uv;
|
|
20
|
+
const { w, h, d } = layerDef.size;
|
|
21
|
+
|
|
22
|
+
// Mapping faces of the body part to UV coordinates
|
|
23
|
+
const faces = [
|
|
24
|
+
{ u: outer.x+d, v: outer.y+d, w: w, h: h, pos: (i,j) => new THREE.Vector3(i-w/2+0.5, j-h/2+0.5, d/2+0.25) },
|
|
25
|
+
{ u: outer.x+d+w+d, v: outer.y+d, w: w, h: h, pos: (i,j) => new THREE.Vector3(-(i-w/2+0.5), j-h/2+0.5, -d/2-0.25) },
|
|
26
|
+
{ u: outer.x, v: outer.y+d, w: d, h: h, pos: (i,j) => new THREE.Vector3(-w/2-0.25, j-h/2+0.5, i-d/2+0.5), sx: 0.5 },
|
|
27
|
+
{ u: outer.x+d+w, v: outer.y+d, w: d, h: h, pos: (i,j) => new THREE.Vector3(w/2+0.25, j-h/2+0.5, -(i-d/2+0.5)), sx: 0.5 },
|
|
28
|
+
{ u: outer.x+d, v: outer.y, w: w, h: d, pos: (i,j) => new THREE.Vector3(i-w/2+0.5, h/2+0.25, -(j-d/2+0.5)), sy: 0.5 },
|
|
29
|
+
{ u: outer.x+d+w, v: outer.y, w: w, h: d, pos: (i,j) => new THREE.Vector3(i-w/2+0.5, -h/2-0.25, (d - 1 - j) - d/2 + 0.5), sy: 0.5 }
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const baseGeo = new THREE.BoxGeometry(1, 1, 1);
|
|
33
|
+
|
|
34
|
+
faces.forEach(f => {
|
|
35
|
+
for(let i=0; i<f.w; i++) {
|
|
36
|
+
for(let j=0; j<f.h; j++) {
|
|
37
|
+
const u = f.u + i;
|
|
38
|
+
const v = f.v + j;
|
|
39
|
+
|
|
40
|
+
if(imgData[(v*64+u)*4+3] > 0) {
|
|
41
|
+
const geo = baseGeo.clone();
|
|
42
|
+
|
|
43
|
+
const uvAttr = geo.attributes.uv;
|
|
44
|
+
for(let k=0; k<uvAttr.count; k++) uvAttr.setXY(k, (u+0.5)/64, 1.0-(v+0.5)/64);
|
|
45
|
+
|
|
46
|
+
const m = new THREE.Matrix4();
|
|
47
|
+
const scale = new THREE.Vector3(f.sx||1, f.sy||1, f.sz||1);
|
|
48
|
+
if(f === faces[0] || f === faces[1]) scale.z = 0.5;
|
|
49
|
+
m.compose(f.pos(i, (f.h-1)-j), new THREE.Quaternion(), scale);
|
|
50
|
+
geo.applyMatrix4(m);
|
|
51
|
+
geometries.push(geo);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return geometries.length > 0 ? BufferGeometryUtils.mergeGeometries(geometries) : null;
|
|
58
|
+
}
|