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,83 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Static factory to generate 3D extruded meshes from 2D item textures.
|
|
6
|
+
*/
|
|
7
|
+
export class ItemFactory {
|
|
8
|
+
/**
|
|
9
|
+
* Loads a texture and creates an extruded mesh.
|
|
10
|
+
* @param {string} url - Image URL.
|
|
11
|
+
* @param {string} name - Item name.
|
|
12
|
+
* @returns {Promise<THREE.Mesh>}
|
|
13
|
+
*/
|
|
14
|
+
static createFromURL(url, name) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
new THREE.TextureLoader().load(url, (texture) => {
|
|
17
|
+
texture.magFilter = THREE.NearestFilter;
|
|
18
|
+
texture.minFilter = THREE.NearestFilter;
|
|
19
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
20
|
+
const mesh = this.generateMesh(texture);
|
|
21
|
+
if(mesh) {
|
|
22
|
+
mesh.name = name;
|
|
23
|
+
resolve(mesh);
|
|
24
|
+
} else {
|
|
25
|
+
reject("Error generating geometry");
|
|
26
|
+
}
|
|
27
|
+
}, undefined, reject);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generates geometry by iterating over pixels and creating voxels for non-transparent ones.
|
|
33
|
+
*/
|
|
34
|
+
static generateMesh(texture) {
|
|
35
|
+
const img = texture.image;
|
|
36
|
+
const canvas = document.createElement('canvas');
|
|
37
|
+
canvas.width = img.width;
|
|
38
|
+
canvas.height = img.height;
|
|
39
|
+
const ctx = canvas.getContext('2d');
|
|
40
|
+
ctx.drawImage(img, 0, 0);
|
|
41
|
+
|
|
42
|
+
const data = ctx.getImageData(0, 0, img.width, img.height).data;
|
|
43
|
+
const geometries = [];
|
|
44
|
+
const baseGeo = new THREE.BoxGeometry(1, 1, 1);
|
|
45
|
+
|
|
46
|
+
for (let y = 0; y < img.height; y++) {
|
|
47
|
+
for (let x = 0; x < img.width; x++) {
|
|
48
|
+
// Check Alpha > 10
|
|
49
|
+
if (data[(y * img.width + x) * 4 + 3] > 10) {
|
|
50
|
+
const geo = baseGeo.clone();
|
|
51
|
+
const matrix = new THREE.Matrix4().makeTranslation((x - img.width/2), ((img.height-1-y) - img.height/2), 0);
|
|
52
|
+
geo.applyMatrix4(matrix);
|
|
53
|
+
|
|
54
|
+
// Map UVs for this single voxel
|
|
55
|
+
const uvAttr = geo.attributes.uv;
|
|
56
|
+
for(let i=0; i<uvAttr.count; i++) {
|
|
57
|
+
uvAttr.setXY(i, (x+0.5)/img.width, 1.0-(y+0.5)/img.height);
|
|
58
|
+
}
|
|
59
|
+
geometries.push(geo);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (geometries.length === 0) return null;
|
|
65
|
+
|
|
66
|
+
const merged = BufferGeometryUtils.mergeGeometries(geometries);
|
|
67
|
+
merged.center();
|
|
68
|
+
|
|
69
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
70
|
+
map: texture,
|
|
71
|
+
side: THREE.DoubleSide
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const mesh = new THREE.Mesh(merged, mat);
|
|
75
|
+
|
|
76
|
+
// Scale to match Minecraft pixel density roughly
|
|
77
|
+
const scale = 16 / Math.max(img.width, img.height);
|
|
78
|
+
mesh.scale.set(scale, scale, scale);
|
|
79
|
+
|
|
80
|
+
mesh.userData.originalMat = mat;
|
|
81
|
+
return mesh;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Initializes basic scene elements: Lights and Grid.
|
|
5
|
+
*/
|
|
6
|
+
export class SceneSetup {
|
|
7
|
+
constructor(scene) {
|
|
8
|
+
this.scene = scene;
|
|
9
|
+
this.gridHelper = null;
|
|
10
|
+
this.initLights();
|
|
11
|
+
this.initHelpers();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
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);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
initHelpers() {
|
|
26
|
+
this.gridHelper = new THREE.GridHelper(2000, 125, 0x8b5cf6, 0x222222);
|
|
27
|
+
this.gridHelper.position.y = -24;
|
|
28
|
+
this.scene.add(this.gridHelper);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
setGridVisible(vis) { if(this.gridHelper) this.gridHelper.visible = vis; }
|
|
32
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
3
|
+
import { applySkinUVs } from '../utils/SkinUtils.js';
|
|
4
|
+
import { createVoxelLayer } from '../utils/Voxelizer.js';
|
|
5
|
+
import { createGlowMaterial } from '../materials/GlowMaterial.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Represents the Minecraft Character Model (Steve/Alex).
|
|
9
|
+
* Handles geometry generation, UV mapping, and hierarchy.
|
|
10
|
+
*/
|
|
11
|
+
export class SkinModel {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.playerGroup = new THREE.Group();
|
|
14
|
+
this.parts = {};
|
|
15
|
+
this.glowMeshes = [];
|
|
16
|
+
this.bodyMeshes = [];
|
|
17
|
+
this.defaultPositions = {};
|
|
18
|
+
this.blackMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a single body part (e.g., Head, Arm).
|
|
23
|
+
* Adds Inner layer (Box), Outer layer (Voxels), and Glow mesh.
|
|
24
|
+
*/
|
|
25
|
+
createBodyPart(texture, coords, size, pivotPos, meshOffset, name) {
|
|
26
|
+
const pivotGroup = new THREE.Group();
|
|
27
|
+
pivotGroup.position.copy(pivotPos);
|
|
28
|
+
pivotGroup.name = name;
|
|
29
|
+
this.defaultPositions[name] = pivotPos.clone();
|
|
30
|
+
|
|
31
|
+
const meshGroup = new THREE.Group();
|
|
32
|
+
meshGroup.position.copy(meshOffset);
|
|
33
|
+
|
|
34
|
+
// 1. Inner Layer (Standard Box)
|
|
35
|
+
const innerGeo = new THREE.BoxGeometry(size.w, size.h, size.d);
|
|
36
|
+
applySkinUVs(innerGeo, coords.inner.x, coords.inner.y, size.w, size.h, size.d);
|
|
37
|
+
const innerMat = new THREE.MeshStandardMaterial({
|
|
38
|
+
map: texture,
|
|
39
|
+
transparent: false, // Opaque for correct depth sorting
|
|
40
|
+
alphaTest: 0.5
|
|
41
|
+
});
|
|
42
|
+
const innerMesh = new THREE.Mesh(innerGeo, innerMat);
|
|
43
|
+
innerMesh.userData.originalMat = innerMat;
|
|
44
|
+
meshGroup.add(innerMesh);
|
|
45
|
+
this.bodyMeshes.push(innerMesh);
|
|
46
|
+
|
|
47
|
+
// 2. Outer Layer (Voxelized 2nd Layer)
|
|
48
|
+
const voxelGeo = createVoxelLayer(texture, { uv: coords, size: size });
|
|
49
|
+
if (voxelGeo) {
|
|
50
|
+
const outerMat = new THREE.MeshStandardMaterial({
|
|
51
|
+
map: texture,
|
|
52
|
+
transparent: false,
|
|
53
|
+
alphaTest: 0.5,
|
|
54
|
+
side: THREE.FrontSide
|
|
55
|
+
});
|
|
56
|
+
const voxelMesh = new THREE.Mesh(voxelGeo, outerMat);
|
|
57
|
+
voxelMesh.userData.originalMat = outerMat;
|
|
58
|
+
meshGroup.add(voxelMesh);
|
|
59
|
+
this.bodyMeshes.push(voxelMesh);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. Glow Mesh (Copy of geometry for shader)
|
|
63
|
+
const glowParts = [innerGeo.clone()];
|
|
64
|
+
if (voxelGeo) glowParts.push(voxelGeo.clone());
|
|
65
|
+
const glowGeo = BufferGeometryUtils.mergeGeometries(glowParts, false);
|
|
66
|
+
const glowMat = createGlowMaterial(size.h);
|
|
67
|
+
const glowMesh = new THREE.Mesh(glowGeo, glowMat);
|
|
68
|
+
glowMesh.userData.glowMat = glowMat;
|
|
69
|
+
this.glowMeshes.push(glowMesh);
|
|
70
|
+
meshGroup.add(glowMesh);
|
|
71
|
+
|
|
72
|
+
pivotGroup.add(meshGroup);
|
|
73
|
+
return pivotGroup;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Builds the entire character model from a texture.
|
|
78
|
+
* @param {THREE.Texture} texture
|
|
79
|
+
* @param {boolean} isSlim - True for Alex model (3px arms), False for Steve (4px arms).
|
|
80
|
+
*/
|
|
81
|
+
build(texture, isSlim = false) {
|
|
82
|
+
this.playerGroup.clear();
|
|
83
|
+
this.parts = {};
|
|
84
|
+
this.glowMeshes = [];
|
|
85
|
+
this.bodyMeshes = [];
|
|
86
|
+
this.defaultPositions = {};
|
|
87
|
+
|
|
88
|
+
const armW = isSlim ? 3 : 4;
|
|
89
|
+
const armOff = isSlim ? 5.0 : 6.0;
|
|
90
|
+
|
|
91
|
+
const defs = {
|
|
92
|
+
head: { uv: { inner: {x:0, y:0}, outer: {x:32, y:0} }, size: { w:8, h:8, d:8 }, pivotPos: new THREE.Vector3(0, 0, 0), meshOffset: new THREE.Vector3(0, 4, 0) },
|
|
93
|
+
body: { uv: { inner: {x:16, y:16}, outer: {x:16, y:32} }, size: { w:8, h:12, d:4 }, pivotPos: new THREE.Vector3(0, 0, 0), meshOffset: new THREE.Vector3(0, -6, 0) },
|
|
94
|
+
rightArm: { uv: { inner: {x:40, y:16}, outer: {x:40, y:32} }, size: { w:armW, h:12, d:4 }, pivotPos: new THREE.Vector3(-armOff, -2, 0), meshOffset: new THREE.Vector3(0, -4, 0) },
|
|
95
|
+
leftArm: { uv: { inner: {x:32, y:48}, outer: {x:48, y:48} }, size: { w:armW, h:12, d:4 }, pivotPos: new THREE.Vector3(armOff, -2, 0), meshOffset: new THREE.Vector3(0, -4, 0) },
|
|
96
|
+
rightLeg: { uv: { inner: {x:0, y:16}, outer: {x:0, y:32} }, size: { w:4, h:12, d:4 }, pivotPos: new THREE.Vector3(-1.9, -12, 0), meshOffset: new THREE.Vector3(0, -6, 0) },
|
|
97
|
+
leftLeg: { uv: { inner: {x:16, y:48}, outer: {x:0, y:48} }, size: { w:4, h:12, d:4 }, pivotPos: new THREE.Vector3(1.9, -12, 0), meshOffset: new THREE.Vector3(0, -6, 0) }
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
for (const [name, def] of Object.entries(defs)) {
|
|
101
|
+
const part = this.createBodyPart(texture, def.uv, def.size, def.pivotPos, def.meshOffset, name);
|
|
102
|
+
this.parts[name] = part;
|
|
103
|
+
this.playerGroup.add(part);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getGroup() { return this.playerGroup; }
|
|
108
|
+
|
|
109
|
+
updateBorderThickness(v) { const t = v * 0.05; this.glowMeshes.forEach(m => m.userData.glowMat.uniforms.thickness.value = t); }
|
|
110
|
+
updateGlowHeight(p) { this.glowMeshes.forEach(m => m.userData.glowMat.uniforms.gradientLimit.value = p); }
|
|
111
|
+
setGlowEffect(en) { this.glowMeshes.forEach(m => m.userData.glowMat.uniforms.opacity.value = en ? 1.0 : 0.0); }
|
|
112
|
+
darkenBody() { this.bodyMeshes.forEach(m => m.material = this.blackMaterial); }
|
|
113
|
+
restoreBody() { this.bodyMeshes.forEach(m => m.material = m.userData.originalMat); }
|
|
114
|
+
|
|
115
|
+
setPose(pose) {
|
|
116
|
+
for (const [name, part] of Object.entries(this.parts)) {
|
|
117
|
+
part.rotation.set(0,0,0);
|
|
118
|
+
if (this.defaultPositions[name]) part.position.copy(this.defaultPositions[name]);
|
|
119
|
+
}
|
|
120
|
+
if (!pose) return;
|
|
121
|
+
for (const [name, data] of Object.entries(pose)) {
|
|
122
|
+
if (this.parts[name]) {
|
|
123
|
+
if(data.rot) this.parts[name].rotation.set(...data.rot);
|
|
124
|
+
if(data.pos) this.parts[name].position.set(...data.pos);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getPose() {
|
|
130
|
+
const pose = {};
|
|
131
|
+
const f = (n) => parseFloat(n.toFixed(3));
|
|
132
|
+
for (const [name, part] of Object.entries(this.parts)) {
|
|
133
|
+
pose[name] = { rot: [f(part.rotation.x), f(part.rotation.y), f(part.rotation.z)], pos: [f(part.position.x), f(part.position.y), f(part.position.z)] };
|
|
134
|
+
}
|
|
135
|
+
return pose;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
|
|
3
|
+
import { HistoryManager } from '../managers/HistoryManager.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Plugin responsible for User Interaction.
|
|
7
|
+
* Handles: Transform Gizmos, Raycasting (Selecting objects), History (Undo/Redo).
|
|
8
|
+
*/
|
|
9
|
+
export class EditorPlugin {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.name = 'EditorPlugin';
|
|
12
|
+
this.hoveredObject = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Called by SkinViewer when plugin is added.
|
|
17
|
+
* @param {SkinViewer} viewer
|
|
18
|
+
*/
|
|
19
|
+
init(viewer) {
|
|
20
|
+
this.viewer = viewer;
|
|
21
|
+
this.history = new HistoryManager((state) => this.restoreState(state));
|
|
22
|
+
|
|
23
|
+
this.raycaster = new THREE.Raycaster();
|
|
24
|
+
this.mouse = new THREE.Vector2();
|
|
25
|
+
|
|
26
|
+
this.setupGizmo();
|
|
27
|
+
this.bindEvents();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setupGizmo() {
|
|
31
|
+
this.transformControl = new TransformControls(this.viewer.cameraManager.camera, this.viewer.renderer.domElement);
|
|
32
|
+
this.transformControl.setMode('rotate');
|
|
33
|
+
|
|
34
|
+
// Handle History recording on drag start
|
|
35
|
+
this.transformControl.addEventListener('dragging-changed', (event) => {
|
|
36
|
+
this.viewer.cameraManager.setEnabled(!event.value);
|
|
37
|
+
if (event.value === true) {
|
|
38
|
+
this.saveHistory();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
this.viewer.overlayScene.add(this.transformControl);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
bindEvents() {
|
|
46
|
+
this.onPointerDown = (e) => this.handleClick(e);
|
|
47
|
+
this.onPointerMove = (e) => this.handleHover(e);
|
|
48
|
+
|
|
49
|
+
const canvas = this.viewer.renderer.domElement;
|
|
50
|
+
canvas.addEventListener('pointerdown', this.onPointerDown);
|
|
51
|
+
canvas.addEventListener('pointermove', this.onPointerMove);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getIntersects(event) {
|
|
55
|
+
const rect = this.viewer.renderer.domElement.getBoundingClientRect();
|
|
56
|
+
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
57
|
+
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|
58
|
+
|
|
59
|
+
this.raycaster.setFromCamera(this.mouse, this.viewer.cameraManager.camera);
|
|
60
|
+
|
|
61
|
+
let objectsToCheck = [];
|
|
62
|
+
|
|
63
|
+
this.viewer.skinModel.getGroup().traverse((child) => {
|
|
64
|
+
if (child.isMesh && child.material.visible) {
|
|
65
|
+
if (child.material.side !== THREE.BackSide) {
|
|
66
|
+
objectsToCheck.push(child);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const itemsPlugin = this.viewer.getPlugin('ItemsPlugin');
|
|
72
|
+
if (itemsPlugin) {
|
|
73
|
+
objectsToCheck = [...objectsToCheck, ...itemsPlugin.items];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return this.raycaster.intersectObjects(objectsToCheck, false);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
handleHover(event) {
|
|
80
|
+
if (this.transformControl.dragging) return;
|
|
81
|
+
|
|
82
|
+
const intersects = this.getIntersects(event);
|
|
83
|
+
|
|
84
|
+
if (intersects.length > 0) {
|
|
85
|
+
const target = intersects[0].object;
|
|
86
|
+
|
|
87
|
+
if (this.hoveredObject !== target) {
|
|
88
|
+
this.unhighlightObject();
|
|
89
|
+
this.highlightObject(target);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.viewer.renderer.domElement.style.cursor = 'pointer';
|
|
93
|
+
} else {
|
|
94
|
+
this.unhighlightObject();
|
|
95
|
+
this.viewer.renderer.domElement.style.cursor = 'default';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
highlightObject(obj) {
|
|
100
|
+
this.hoveredObject = obj;
|
|
101
|
+
|
|
102
|
+
if (obj.material && obj.material.emissive) {
|
|
103
|
+
if (!obj.userData.originalHex) {
|
|
104
|
+
obj.userData.originalHex = obj.material.emissive.getHex();
|
|
105
|
+
}
|
|
106
|
+
obj.material.emissive.setHex(0x444444);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
unhighlightObject() {
|
|
111
|
+
if (this.hoveredObject) {
|
|
112
|
+
const obj = this.hoveredObject;
|
|
113
|
+
if (obj.material && obj.material.emissive && obj.userData.originalHex !== undefined) {
|
|
114
|
+
obj.material.emissive.setHex(obj.userData.originalHex);
|
|
115
|
+
}
|
|
116
|
+
this.hoveredObject = null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
handleClick(event) {
|
|
121
|
+
if (this.transformControl.dragging) return;
|
|
122
|
+
|
|
123
|
+
const rect = this.viewer.renderer.domElement.getBoundingClientRect();
|
|
124
|
+
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
125
|
+
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|
126
|
+
|
|
127
|
+
this.raycaster.setFromCamera(this.mouse, this.viewer.cameraManager.camera);
|
|
128
|
+
|
|
129
|
+
let objectsToCheck = [...this.viewer.skinModel.getGroup().children];
|
|
130
|
+
|
|
131
|
+
const itemsPlugin = this.viewer.getPlugin('ItemsPlugin');
|
|
132
|
+
if (itemsPlugin) {
|
|
133
|
+
objectsToCheck = [...objectsToCheck, ...itemsPlugin.items];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const intersects = this.raycaster.intersectObjects(objectsToCheck, true);
|
|
137
|
+
|
|
138
|
+
if (intersects.length > 0) {
|
|
139
|
+
let target = intersects[0].object;
|
|
140
|
+
|
|
141
|
+
const skinGroup = this.viewer.skinModel.getGroup();
|
|
142
|
+
let temp = target;
|
|
143
|
+
while(temp) {
|
|
144
|
+
if (temp.parent === skinGroup) {
|
|
145
|
+
target = temp;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
temp = temp.parent;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (this.transformControl.object !== target) {
|
|
152
|
+
this.selectObject(target);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
this.deselect();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
selectObject(obj) {
|
|
160
|
+
this.transformControl.attach(obj);
|
|
161
|
+
|
|
162
|
+
// Notify EffectsPlugin to draw outline
|
|
163
|
+
const fx = this.viewer.getPlugin('EffectsPlugin');
|
|
164
|
+
if (fx) fx.setSelected(obj);
|
|
165
|
+
|
|
166
|
+
// Callback support (can be injected)
|
|
167
|
+
if (this.viewer.onSelectionChanged) this.viewer.onSelectionChanged(obj);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
deselect() {
|
|
171
|
+
this.transformControl.detach();
|
|
172
|
+
|
|
173
|
+
const fx = this.viewer.getPlugin('EffectsPlugin');
|
|
174
|
+
if (fx) fx.setSelected(null);
|
|
175
|
+
|
|
176
|
+
if (this.viewer.onSelectionChanged) this.viewer.onSelectionChanged(null);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Sets gizmo mode.
|
|
181
|
+
* @param {'translate'|'rotate'|'scale'} mode
|
|
182
|
+
*/
|
|
183
|
+
setTransformMode(mode) {
|
|
184
|
+
this.transformControl.setMode(mode);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// --- HISTORY API ---
|
|
188
|
+
|
|
189
|
+
getSnapshot() {
|
|
190
|
+
const pose = this.viewer.skinModel.getPose();
|
|
191
|
+
const itemsPlugin = this.viewer.getPlugin('ItemsPlugin');
|
|
192
|
+
const itemsState = itemsPlugin ? itemsPlugin.getSnapshot() : [];
|
|
193
|
+
return { pose, items: itemsState };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
saveHistory() { this.history.pushState(this.getSnapshot()); }
|
|
197
|
+
undo() { this.history.undo(this.getSnapshot()); }
|
|
198
|
+
redo() { this.history.redo(this.getSnapshot()); }
|
|
199
|
+
|
|
200
|
+
restoreState(state) {
|
|
201
|
+
if (state.pose) this.viewer.skinModel.setPose(state.pose);
|
|
202
|
+
|
|
203
|
+
const itemsPlugin = this.viewer.getPlugin('ItemsPlugin');
|
|
204
|
+
if (itemsPlugin && state.items) {
|
|
205
|
+
itemsPlugin.restoreSnapshot(state.items);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
dispose() {
|
|
210
|
+
this.viewer.renderer.domElement.removeEventListener('pointerdown', this.onPointerDown);
|
|
211
|
+
this.transformControl.dispose();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { PostProcessingManager } from '../managers/PostProcessingManager.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Plugin responsible for visual effects and post-processing.
|
|
6
|
+
* Handles Bloom (Glow), Outlines, and high-res Screenshots.
|
|
7
|
+
*/
|
|
8
|
+
export class EffectsPlugin {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.name = 'EffectsPlugin';
|
|
11
|
+
this.isEnabled = false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
init(viewer) {
|
|
15
|
+
this.viewer = viewer;
|
|
16
|
+
const w = viewer.container.clientWidth;
|
|
17
|
+
const h = viewer.container.clientHeight;
|
|
18
|
+
|
|
19
|
+
this.composer = new PostProcessingManager(viewer.renderer, viewer.scene, viewer.cameraManager.camera, w, h);
|
|
20
|
+
|
|
21
|
+
this.composer.setBloom(false, 0, 0, 0.1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Updates effect parameters.
|
|
26
|
+
* @param {Object} config - { enabled, strength, radius, height, thickness }
|
|
27
|
+
*/
|
|
28
|
+
updateConfig(config) {
|
|
29
|
+
this.isEnabled = config.enabled;
|
|
30
|
+
const skin = this.viewer.skinModel;
|
|
31
|
+
|
|
32
|
+
// Update Shader Materials
|
|
33
|
+
skin.setGlowEffect(config.enabled);
|
|
34
|
+
if (config.thickness !== undefined) skin.updateBorderThickness(config.thickness);
|
|
35
|
+
if (config.height !== undefined) skin.updateGlowHeight(config.height);
|
|
36
|
+
|
|
37
|
+
// Update Composer Pass
|
|
38
|
+
this.composer.setBloom(config.enabled, config.strength, config.radius, 0.1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Highlights an object (used by EditorPlugin).
|
|
43
|
+
*/
|
|
44
|
+
setSelected(obj) {
|
|
45
|
+
this.composer.setSelected(obj);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
onResize(w, h) {
|
|
49
|
+
this.composer.resize(w, h);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Custom render loop called by the Core animate().
|
|
54
|
+
*/
|
|
55
|
+
render() {
|
|
56
|
+
const skin = this.viewer.skinModel;
|
|
57
|
+
const itemsPlugin = this.viewer.getPlugin('ItemsPlugin');
|
|
58
|
+
const items = itemsPlugin ? itemsPlugin.items : [];
|
|
59
|
+
|
|
60
|
+
this.composer.renderSelective(
|
|
61
|
+
// 1. Prepare Bloom pass (Hide non-glowing elements)
|
|
62
|
+
() => {
|
|
63
|
+
skin.darkenBody();
|
|
64
|
+
this.viewer.sceneSetup.setGridVisible(false);
|
|
65
|
+
items.forEach(i => i.material = skin.blackMaterial);
|
|
66
|
+
},
|
|
67
|
+
// 2. Restore Scene for main pass
|
|
68
|
+
() => {
|
|
69
|
+
skin.restoreBody();
|
|
70
|
+
this.viewer.sceneSetup.setGridVisible(this.viewer.config.showGrid);
|
|
71
|
+
items.forEach(i => {
|
|
72
|
+
if(i.userData.originalMat) i.material = i.userData.originalMat;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Generates a transparent PNG screenshot.
|
|
80
|
+
* Temporarily resizes renderer if width/height are provided.
|
|
81
|
+
*/
|
|
82
|
+
captureScreenshot(width, height) {
|
|
83
|
+
const renderer = this.viewer.renderer;
|
|
84
|
+
const camera = this.viewer.cameraManager.camera;
|
|
85
|
+
|
|
86
|
+
const originalSize = new THREE.Vector2();
|
|
87
|
+
renderer.getSize(originalSize);
|
|
88
|
+
const originalAspect = camera.aspect;
|
|
89
|
+
|
|
90
|
+
if (width && height) {
|
|
91
|
+
renderer.setSize(width, height);
|
|
92
|
+
camera.aspect = width / height;
|
|
93
|
+
camera.updateProjectionMatrix();
|
|
94
|
+
this.composer.resize(width, height);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const wasGridEnabled = this.viewer.config.showGrid;
|
|
98
|
+
const prevBg = this.viewer.scene.background;
|
|
99
|
+
const prevSel = this.composer.outlinePass.selectedObjects;
|
|
100
|
+
const prevClearColor = new THREE.Color();
|
|
101
|
+
renderer.getClearColor(prevClearColor);
|
|
102
|
+
const prevClearAlpha = renderer.getClearAlpha();
|
|
103
|
+
|
|
104
|
+
this.composer.setSelected(null);
|
|
105
|
+
|
|
106
|
+
this.viewer.config.showGrid = false;
|
|
107
|
+
this.viewer.sceneSetup.setGridVisible(false);
|
|
108
|
+
|
|
109
|
+
this.viewer.scene.background = null;
|
|
110
|
+
renderer.setClearColor(0x000000, 0);
|
|
111
|
+
|
|
112
|
+
this.render();
|
|
113
|
+
|
|
114
|
+
const dataUrl = renderer.domElement.toDataURL("image/png");
|
|
115
|
+
|
|
116
|
+
this.viewer.scene.background = prevBg;
|
|
117
|
+
renderer.setClearColor(prevClearColor, prevClearAlpha);
|
|
118
|
+
|
|
119
|
+
this.viewer.config.showGrid = wasGridEnabled;
|
|
120
|
+
this.viewer.sceneSetup.setGridVisible(wasGridEnabled);
|
|
121
|
+
|
|
122
|
+
this.composer.outlinePass.selectedObjects = prevSel;
|
|
123
|
+
|
|
124
|
+
if (width && height) {
|
|
125
|
+
renderer.setSize(originalSize.x, originalSize.y);
|
|
126
|
+
camera.aspect = originalAspect;
|
|
127
|
+
camera.updateProjectionMatrix();
|
|
128
|
+
this.composer.resize(originalSize.x, originalSize.y);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.viewer.cameraManager.update();
|
|
132
|
+
return dataUrl;
|
|
133
|
+
}
|
|
134
|
+
|
|
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
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
}
|