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.
@@ -3,6 +3,7 @@ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUti
3
3
  import { applySkinUVs } from '../utils/SkinUtils.js';
4
4
  import { createVoxelLayer } from '../utils/Voxelizer.js';
5
5
  import { createGlowMaterial } from '../materials/GlowMaterial.js';
6
+ import {disposeObjectTree} from "../utils/ThreeUtils.js";
6
7
 
7
8
  /**
8
9
  * Represents the Minecraft Character Model (Steve/Alex).
@@ -16,6 +17,8 @@ export class SkinModel {
16
17
  this.bodyMeshes = [];
17
18
  this.defaultPositions = {};
18
19
  this.blackMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
20
+
21
+ this.LAYERS_COUNT = 20;
19
22
  }
20
23
 
21
24
  /**
@@ -59,15 +62,30 @@ export class SkinModel {
59
62
  this.bodyMeshes.push(voxelMesh);
60
63
  }
61
64
 
62
- // 3. Glow Mesh (Copy of geometry for shader)
65
+ // 3. Glow Meshes (Multi-Layer Shells)
63
66
  const glowParts = [innerGeo.clone()];
64
67
  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);
68
+ const baseGlowGeo = BufferGeometryUtils.mergeGeometries(glowParts, false);
69
+
70
+ const partLayers = [];
71
+
72
+ for (let i = 0; i < this.LAYERS_COUNT; i++) {
73
+ const glowMat = createGlowMaterial(size.h);
74
+
75
+ glowMat.uniforms.thickness.value = 0;
76
+ glowMat.uniforms.opacity.value = 0;
77
+
78
+ const layerMesh = new THREE.Mesh(baseGlowGeo, glowMat);
79
+
80
+ layerMesh.userData.layerIndex = i;
81
+ layerMesh.userData.isGlow = true;
82
+ layerMesh.userData.glowMat = glowMat;
83
+
84
+ meshGroup.add(layerMesh);
85
+ partLayers.push(layerMesh);
86
+ }
87
+
88
+ this.glowMeshes.push(partLayers);
71
89
 
72
90
  pivotGroup.add(meshGroup);
73
91
  return pivotGroup;
@@ -79,7 +97,24 @@ export class SkinModel {
79
97
  * @param {boolean} isSlim - True for Alex model (3px arms), False for Steve (4px arms).
80
98
  */
81
99
  build(texture, isSlim = false) {
82
- this.playerGroup.clear();
100
+ let capeBackup = null;
101
+ if (this.parts.cape) {
102
+ const mesh = this.parts.cape.children.find(c => c.isMesh);
103
+ if (mesh && mesh.material.map) {
104
+ capeBackup = {
105
+ texture: mesh.material.map,
106
+ position: this.parts.cape.position.clone(),
107
+ rotation: this.parts.cape.rotation.clone(),
108
+ scale: this.parts.cape.scale.clone()
109
+ };
110
+ }
111
+ }
112
+
113
+ if (this.playerGroup.children.length > 0) {
114
+ disposeObjectTree(this.playerGroup);
115
+ this.playerGroup.clear();
116
+ }
117
+
83
118
  this.parts = {};
84
119
  this.glowMeshes = [];
85
120
  this.bodyMeshes = [];
@@ -102,13 +137,132 @@ export class SkinModel {
102
137
  this.parts[name] = part;
103
138
  this.playerGroup.add(part);
104
139
  }
140
+
141
+ if (capeBackup) {
142
+ this.setCape(capeBackup.texture);
143
+
144
+ if (this.parts.cape) {
145
+ this.parts.cape.position.copy(capeBackup.position);
146
+ this.parts.cape.rotation.copy(capeBackup.rotation);
147
+ this.parts.cape.scale.copy(capeBackup.scale);
148
+ }
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Adds or updates the Cape mesh.
154
+ * @param {THREE.Texture} texture
155
+ */
156
+ setCape(texture) {
157
+ if (this.parts.cape) {
158
+ if (this.parts.cape.userData.glowLayers) {
159
+ const layersToRemove = this.parts.cape.userData.glowLayers;
160
+ this.glowMeshes = this.glowMeshes.filter(layers => layers !== layersToRemove);
161
+ }
162
+
163
+ this.playerGroup.remove(this.parts.cape);
164
+ disposeObjectTree(this.parts.cape);
165
+ delete this.parts.cape;
166
+ }
167
+
168
+ if (!texture) return;
169
+
170
+ const size = { w: 10, h: 16, d: 1 };
171
+ const pivotPos = new THREE.Vector3(0, 0, -3);
172
+ const meshOffset = new THREE.Vector3(0, -8, 0.6);
173
+
174
+ const pivotGroup = new THREE.Group();
175
+ pivotGroup.position.copy(pivotPos);
176
+ pivotGroup.name = 'cape';
177
+ pivotGroup.rotation.x = 0.2;
178
+
179
+ this.defaultPositions['cape'] = pivotPos.clone();
180
+
181
+ const geo = new THREE.BoxGeometry(size.w, size.h, size.d);
182
+ applySkinUVs(geo, 0, 0, 10, 16, 1, 64, 32);
183
+
184
+ const mat = new THREE.MeshStandardMaterial({
185
+ map: texture,
186
+ side: THREE.DoubleSide,
187
+ transparent: true,
188
+ alphaTest: 0.5
189
+ });
190
+
191
+ const mainMesh = new THREE.Mesh(geo, mat);
192
+ mainMesh.position.copy(meshOffset);
193
+ mainMesh.rotation.y = Math.PI;
194
+ mainMesh.userData.originalMat = mat;
195
+
196
+ pivotGroup.add(mainMesh);
197
+ this.bodyMeshes.push(mainMesh);
198
+
199
+ const capeLayers = [];
200
+ const shellGeo = geo.clone();
201
+
202
+ for (let i = 0; i < this.LAYERS_COUNT; i++) {
203
+ const glowMat = createGlowMaterial(size.h);
204
+
205
+ glowMat.uniforms.thickness.value = 0;
206
+ glowMat.uniforms.opacity.value = 0;
207
+ glowMat.polygonOffset = true;
208
+ glowMat.polygonOffsetFactor = i * 0.1;
209
+
210
+ const layerMesh = new THREE.Mesh(shellGeo, glowMat);
211
+
212
+ layerMesh.position.copy(meshOffset);
213
+ layerMesh.rotation.y = Math.PI;
214
+
215
+ layerMesh.userData.layerIndex = i;
216
+ layerMesh.userData.isGlow = true;
217
+ layerMesh.userData.glowMat = glowMat;
218
+
219
+ pivotGroup.add(layerMesh);
220
+ capeLayers.push(layerMesh);
221
+ }
222
+
223
+ this.glowMeshes.push(capeLayers);
224
+ pivotGroup.userData.glowLayers = capeLayers;
225
+
226
+ this.playerGroup.add(pivotGroup);
227
+ this.parts['cape'] = pivotGroup;
105
228
  }
106
229
 
107
230
  getGroup() { return this.playerGroup; }
108
231
 
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); }
232
+ /**
233
+ * Updates thickness creating a solid volume effect.
234
+ * @param {number} v - Base thickness value.
235
+ */
236
+ updateBorderThickness(v) {
237
+ const maxThickness = v * 0.05;
238
+
239
+ this.glowMeshes.forEach(layers => {
240
+ layers.forEach((mesh, i) => {
241
+ const progress = (i + 1) / this.LAYERS_COUNT;
242
+
243
+ mesh.userData.glowMat.uniforms.thickness.value = maxThickness * progress;
244
+ });
245
+ });
246
+ }
247
+
248
+ updateGlowHeight(p) {
249
+ this.glowMeshes.forEach(layers => {
250
+ layers.forEach(m => m.userData.glowMat.uniforms.gradientLimit.value = p);
251
+ });
252
+ }
253
+
254
+ setGlowEffect(en) {
255
+ this.glowMeshes.forEach(layers => {
256
+ layers.forEach((mesh, i) => {
257
+ if (!en) {
258
+ mesh.userData.glowMat.uniforms.opacity.value = 0.0;
259
+ } else {
260
+ mesh.userData.glowMat.uniforms.opacity.value = 1.0 / (this.LAYERS_COUNT * 0.6);
261
+ }
262
+ });
263
+ });
264
+ }
265
+
112
266
  darkenBody() { this.bodyMeshes.forEach(m => m.material = this.blackMaterial); }
113
267
  restoreBody() { this.bodyMeshes.forEach(m => m.material = m.userData.originalMat); }
114
268
 
@@ -117,8 +271,22 @@ export class SkinModel {
117
271
  part.rotation.set(0,0,0);
118
272
  if (this.defaultPositions[name]) part.position.copy(this.defaultPositions[name]);
119
273
  }
274
+
275
+ this.playerGroup.position.set(0, 0, 0);
276
+ this.playerGroup.rotation.set(0, 0, 0);
277
+ this.playerGroup.scale.set(1, 1, 1);
278
+
120
279
  if (!pose) return;
280
+
281
+ if (pose.root) {
282
+ if (pose.root.pos) this.playerGroup.position.fromArray(pose.root.pos);
283
+ if (pose.root.rot) this.playerGroup.rotation.fromArray(pose.root.rot);
284
+ if (pose.root.scl) this.playerGroup.scale.fromArray(pose.root.scl);
285
+ }
286
+
121
287
  for (const [name, data] of Object.entries(pose)) {
288
+ if (name === 'root') continue;
289
+
122
290
  if (this.parts[name]) {
123
291
  if(data.rot) this.parts[name].rotation.set(...data.rot);
124
292
  if(data.pos) this.parts[name].position.set(...data.pos);
@@ -129,9 +297,34 @@ export class SkinModel {
129
297
  getPose() {
130
298
  const pose = {};
131
299
  const f = (n) => parseFloat(n.toFixed(3));
300
+
132
301
  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)] };
302
+ pose[name] = {
303
+ rot: [f(part.rotation.x), f(part.rotation.y), f(part.rotation.z)],
304
+ pos: [f(part.position.x), f(part.position.y), f(part.position.z)]
305
+ };
134
306
  }
307
+
308
+ pose.root = {
309
+ pos: [f(this.playerGroup.position.x), f(this.playerGroup.position.y), f(this.playerGroup.position.z)],
310
+ rot: [f(this.playerGroup.rotation.x), f(this.playerGroup.rotation.y), f(this.playerGroup.rotation.z)],
311
+ scl: [f(this.playerGroup.scale.x), f(this.playerGroup.scale.y), f(this.playerGroup.scale.z)]
312
+ };
313
+
135
314
  return pose;
136
315
  }
316
+
317
+ dispose() {
318
+ if (this.playerGroup) {
319
+ if (this.playerGroup.parent) {
320
+ this.playerGroup.parent.remove(this.playerGroup);
321
+ }
322
+ disposeObjectTree(this.playerGroup);
323
+ }
324
+
325
+ this.parts = {};
326
+ this.glowMeshes = [];
327
+ this.bodyMeshes = [];
328
+ this.playerGroup = null;
329
+ }
137
330
  }
@@ -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-BETA",
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
  }