@archvisioninc/canvas 3.3.8 → 3.3.9

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.
@@ -0,0 +1,1365 @@
1
+ /* eslint-disable */
2
+ import { getUserMeshes, resetSelectedMeshes, addNewMesh, scene, mirrorGround, ground, newVector, newColor, newScreenshot, serializeScene, newMetaDataEntry, selectedMeshes, singleMeshTNode, toRadians, toDegrees, multiMeshTNode, handleMousePointerTap, prepareCamera, buildMaterialsArray, buildMeshPositionsArray, createBoundingMesh, attachToSelectMeshesNode, newTexture, getUserMaterials, buildSelectedMaterialArray, buildMeshIdArray, refreshHighlight, guiTexture, getBoundingMeshData, getScaleFactor, gizmoManager, iblShadowPipeline, buildLightsArray } from '../helpers';
3
+ import { toggleSafeFrame, toggleOrthographicViews, toggleBoundingBoxWidget, resetManagerGizmos, modelToOrigin, focusCamera } from '../actions';
4
+ import { GLTF2 } from 'babylonjs-loaders';
5
+ import { GIZMOS, TRANSPARENCY_MODES, GUI } from '../constants';
6
+ import { reactProps as props } from '../Canvas';
7
+ import { ratios, scaleUnits } from '../enums';
8
+ import * as BABYLON from 'babylonjs';
9
+ import _ from 'lodash';
10
+ export let hiddenMeshes = [];
11
+ const unitless = 'unitless';
12
+ const performTransform = args => {
13
+ const {
14
+ type,
15
+ vector,
16
+ globalTransform,
17
+ newTrackingData,
18
+ tx,
19
+ ty,
20
+ tz,
21
+ rx,
22
+ ry,
23
+ rz,
24
+ sx,
25
+ sy,
26
+ sz
27
+ } = args;
28
+ const globalTransforms = scene.metadata.globalTransforms;
29
+ let newTransforms;
30
+ const updateNode = () => {
31
+ const fallback = type === 'scale' ? 1 : 0;
32
+ const node0 = scene.getNodeByName('node0');
33
+ const node = globalTransform ? node0 : singleMeshTNode;
34
+ if (node) {
35
+ switch (type) {
36
+ case 'move':
37
+ node.position = globalTransform ? vector.multiplyByFloats(-1, 1, 1) : vector;
38
+ newTransforms = {
39
+ tx: tx || fallback,
40
+ ty: ty || fallback,
41
+ tz: tz || fallback
42
+ };
43
+ break;
44
+ case 'rotate':
45
+ if (globalTransform) {
46
+ vector.applyRotationQuaternionInPlace(node.absoluteRotationQuaternion);
47
+ }
48
+ node.rotation = globalTransform ? vector.multiplyByFloats(-1, 1, -1) : vector;
49
+ newTransforms = {
50
+ rx: (rx || fallback) % 360,
51
+ ry: (ry || fallback) % 360,
52
+ rz: (rz || fallback) % 360
53
+ };
54
+ break;
55
+ default:
56
+ node.scaling = vector;
57
+ newTransforms = {
58
+ sx: sx || fallback,
59
+ sy: sy || fallback,
60
+ sz: sz || fallback
61
+ };
62
+ break;
63
+ }
64
+ }
65
+ };
66
+ const getPositionsChangeAfterGlobal = () => {
67
+ const userMeshes = getUserMeshes();
68
+ const updatedPositions = scene.metadata.meshChangeTracking?.map(mesh => {
69
+ const userMesh = userMeshes.find(userMesh => userMesh.id === mesh.meshId);
70
+ const parent = userMesh?.parent;
71
+ userMesh?.setParent?.(null);
72
+ const boundingBox = getBoundingMeshData([userMesh]);
73
+ const {
74
+ center,
75
+ rotation
76
+ } = boundingBox;
77
+ const newTracking = {
78
+ ...mesh,
79
+ tx: center.x,
80
+ ty: center.y,
81
+ tz: center.z,
82
+ rx: -toDegrees(rotation.x) % 360,
83
+ ry: toDegrees(rotation.y) % 360,
84
+ rz: -toDegrees(rotation.z) % 360
85
+ };
86
+ userMesh?.setParent?.(parent);
87
+ return newTracking;
88
+ });
89
+ return updatedPositions;
90
+ };
91
+ if (globalTransform) {
92
+ /*
93
+ TODO: Changing any transform input box value when multiple meshes are selected does nothing.
94
+ TODO: Look into this rotation bug:
95
+ Rotation:
96
+ - Global rotate 90 on x.
97
+ - Select balloon.
98
+ - Local reset rotation Y to 0.
99
+ - Local reset rotation X to 0.
100
+ - Global reset rotation X to 0.
101
+ - Select balloon.
102
+ - First issue: Local rotation X value should be 90, reads -90.
103
+ - Local reset rotation X to 0.
104
+ - Result: Mesh is upside down.
105
+ */
106
+
107
+ updateNode();
108
+ newMetaDataEntry('globalTransforms', {
109
+ ...globalTransforms,
110
+ ...newTransforms
111
+ });
112
+ newMetaDataEntry('meshChangeTracking', getPositionsChangeAfterGlobal());
113
+ return;
114
+ }
115
+ updateNode();
116
+ newMetaDataEntry('meshChangeTracking', newTrackingData(newTransforms));
117
+ };
118
+ const applyUVSettings = args => {
119
+ const {
120
+ texture,
121
+ material
122
+ } = args || {};
123
+ if (!_.isEmpty(texture) && !_.isEmpty(material)) {
124
+ const workingMaterial = scene.metadata.materials.find(mat => mat.materialId === material.id);
125
+ const {
126
+ uvXScale,
127
+ uvYScale,
128
+ uvXRotation,
129
+ uvYRotation,
130
+ uvZRotation,
131
+ uvXOffset,
132
+ uvYOffset
133
+ } = workingMaterial?.uvSettings || {};
134
+ texture.uAng = uvXRotation;
135
+ texture.wAng = uvYRotation;
136
+ texture.vAng = uvZRotation;
137
+ texture.uOffset = uvXOffset;
138
+ texture.vOffset = uvYOffset;
139
+ texture.uScale = uvXScale;
140
+ texture.vScale = uvYScale;
141
+ }
142
+ };
143
+ const updateTextureChannel = (imageToMaintain, newImage, channelToUpdate) => {
144
+ const maintainHeight = imageToMaintain?.naturalHeight ?? 0;
145
+ const maintainWidth = imageToMaintain?.naturalWidth ?? 0;
146
+ const newHeight = newImage?.naturalHeight ?? 0;
147
+ const newWidth = newImage?.naturalWidth ?? 0;
148
+ let maintainData;
149
+ let newData;
150
+ const maxHeight = Math.max(...[maintainHeight, newHeight, 1024]);
151
+ const maxWidth = Math.max(...[maintainWidth, newWidth, 1024]);
152
+ const canvas = document.createElement('canvas');
153
+ const ctx = canvas.getContext('2d', {
154
+ willReadFrequently: true
155
+ });
156
+ canvas.width = maxWidth;
157
+ canvas.height = maxHeight;
158
+ if (newImage) {
159
+ ctx.drawImage(newImage, 0, 0, maxWidth, maxHeight);
160
+ newData = ctx.getImageData(0, 0, maxWidth, maxHeight).data;
161
+ }
162
+ if (imageToMaintain) {
163
+ ctx.drawImage(imageToMaintain, 0, 0, maxWidth, maxHeight);
164
+ maintainData = ctx.getImageData(0, 0, maxWidth, maxHeight).data;
165
+ }
166
+ const combinedImageData = ctx.createImageData(maxWidth, maxHeight);
167
+ const combinedData = combinedImageData.data;
168
+ for (let i = 0; i < combinedData.length; i += 4) {
169
+ const maintainRed = maintainData ? maintainData[i] : 1;
170
+ const maintainGreen = maintainData ? maintainData[i + 1] : 0;
171
+ const maintainBlue = maintainData ? maintainData[i + 2] : 0;
172
+ const maintainAlpha = maintainData ? maintainData[i + 3] : 255;
173
+ const newRed = newData ? newData[i] : 1;
174
+ const newGreen = newData ? newData[i + 1] : 0;
175
+ const newBlue = newData ? newData[i + 2] : 0;
176
+ const newAlpha = newData ? newData[i + 3] : 255;
177
+ combinedData[i] = channelToUpdate === 'R' ? newRed : maintainRed;
178
+ combinedData[i + 1] = channelToUpdate === 'G' ? newGreen : maintainGreen;
179
+ combinedData[i + 2] = channelToUpdate === 'B' ? newBlue : maintainBlue;
180
+ combinedData[i + 3] = channelToUpdate === 'A' ? newAlpha : maintainAlpha;
181
+ }
182
+ ctx.putImageData(combinedImageData, 0, 0);
183
+ return canvas.toDataURL('image/png');
184
+ };
185
+
186
+ // eslint-disable-next-line
187
+ const loadImage = async file => {
188
+ return new Promise(resolve => {
189
+ const reader = new FileReader();
190
+ reader.onload = event => {
191
+ const img = new Image();
192
+ img.onload = () => resolve(img);
193
+ img.onerror = () => {
194
+ resolve(null);
195
+ };
196
+ img.src = event.target.result;
197
+ };
198
+ reader.onerror = () => {
199
+ resolve(null);
200
+ };
201
+ reader.readAsDataURL(file);
202
+ });
203
+ };
204
+ const metallicTextureToImage = texture => {
205
+ return new Promise(resolve => {
206
+ if (!texture.readPixels()) {
207
+ resolve(null);
208
+ return;
209
+ }
210
+ texture.readPixels().then(pixels => {
211
+ const canvas = document.createElement('canvas');
212
+ const ctx = canvas.getContext('2d');
213
+ const {
214
+ width,
215
+ height
216
+ } = texture.getSize();
217
+ canvas.width = width;
218
+ canvas.height = height;
219
+ const img = new Image();
220
+ const imageData = ctx.createImageData(width, height);
221
+ imageData.data.set(new Uint8ClampedArray(pixels));
222
+ ctx.putImageData(imageData, 0, 0);
223
+ img.onload = () => {
224
+ resolve(img);
225
+ };
226
+ img.onerror = () => {
227
+ resolve(null);
228
+ };
229
+ img.src = canvas.toDataURL();
230
+ }).catch(() => {
231
+ resolve(null);
232
+ });
233
+ });
234
+ };
235
+ const dataUrlToBlob = dataURI => {
236
+ // convert base64 to raw binary data held in a string
237
+ // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
238
+ const byteString = atob(dataURI.split(',')[1]);
239
+
240
+ // separate out the mime component
241
+ const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
242
+
243
+ // write the bytes of the string to an ArrayBuffer
244
+ const ab = new ArrayBuffer(byteString.length);
245
+
246
+ // create a view into the buffer
247
+ const ia = new Uint8Array(ab);
248
+
249
+ // set the bytes of the buffer to the correct values
250
+ for (let i = 0; i < byteString.length; i++) {
251
+ ia[i] = byteString.charCodeAt(i);
252
+ }
253
+
254
+ // write the ArrayBuffer to a blob, and you're done
255
+ const blob = new Blob([ab], {
256
+ type: mimeString
257
+ });
258
+ return blob;
259
+ };
260
+ const getActiveAnimationGroup = animationGroups => {
261
+ const selectedAnimationIndex = scene.metadata?.selectedAnimation;
262
+ if (selectedAnimationIndex === null || selectedAnimationIndex === undefined) {
263
+ return null;
264
+ }
265
+ return animationGroups.find((_group, index) => {
266
+ return `${index}` === `${selectedAnimationIndex}`;
267
+ }) || null;
268
+ };
269
+ let animationSyncObserver = null;
270
+ let lastAnimationSerializeAt = 0;
271
+ const ANIMATION_SYNC_MS = 50;
272
+ const pushAnimationStateToReact = () => {
273
+ const activeAnimationGroup = getActiveAnimationGroup(scene.animationGroups || []);
274
+ syncSelectedAnimationMeta(activeAnimationGroup);
275
+ const now = performance.now();
276
+ if (props.setSerializedData && now - lastAnimationSerializeAt >= ANIMATION_SYNC_MS) {
277
+ lastAnimationSerializeAt = now;
278
+ props.setSerializedData(serializeScene());
279
+ }
280
+ };
281
+ const ensureAnimationStateSync = () => {
282
+ if (animationSyncObserver || !scene?.onBeforeRenderObservable) {
283
+ return;
284
+ }
285
+ animationSyncObserver = scene.onBeforeRenderObservable.add(() => {
286
+ const activeAnimationGroup = getActiveAnimationGroup(scene.animationGroups || []);
287
+ if (!activeAnimationGroup) {
288
+ return;
289
+ }
290
+ pushAnimationStateToReact();
291
+ });
292
+ };
293
+ const syncSelectedAnimationMeta = animationGroup => {
294
+ if (!scene.metadata) return;
295
+ if (!animationGroup) {
296
+ scene.metadata.selectedAnimationTime = 0;
297
+ scene.metadata.selectedAnimationProgress = 0;
298
+ scene.metadata.selectedAnimationDuration = 0;
299
+ scene.metadata.selectedAnimationIsPlaying = false;
300
+ return;
301
+ }
302
+ const runtimeAnimation = animationGroup.animatables?.[0]?.getAnimations?.()?.[0];
303
+ const currentFrame = runtimeAnimation?.currentFrame ?? animationGroup.from ?? 0;
304
+ const from = animationGroup.from ?? 0;
305
+ const to = animationGroup.to ?? 0;
306
+ const totalFrames = Math.max(to - from, 1);
307
+ const progress = BABYLON.Scalar.Clamp((currentFrame - from) / totalFrames, 0, 1);
308
+ const duration = animationGroup.getLength?.() ?? 0;
309
+ scene.metadata.selectedAnimationProgress = progress;
310
+ scene.metadata.selectedAnimationTime = progress * duration;
311
+ scene.metadata.selectedAnimationDuration = duration;
312
+ scene.metadata.selectedAnimationIsPlaying = Boolean(animationGroup.isPlaying);
313
+ };
314
+ export const updateMaterial = inboundData => {
315
+ const {
316
+ payload
317
+ } = inboundData;
318
+ const {
319
+ id,
320
+ meshSimplification,
321
+ textureDownscaling,
322
+ newId,
323
+ selectMaterial,
324
+ backfaceCullingEnabled,
325
+ wireFrameEnabled,
326
+ pointsCloudEnabled,
327
+ albedoColor,
328
+ albedoTexture,
329
+ environmentIntensity,
330
+ metallic,
331
+ metallicTexture,
332
+ roughness,
333
+ roughnessTexture,
334
+ ambientColor,
335
+ ambientTextureStrength,
336
+ ambientTexture,
337
+ microSurfaceTexture,
338
+ emissiveColor,
339
+ emissiveIntensity,
340
+ emissiveTexture,
341
+ bumpIntensity,
342
+ bumpTexture,
343
+ clearCoatEnabled,
344
+ clearCoatIntensity,
345
+ clearCoatRoughness,
346
+ clearCoatIOR,
347
+ alpha,
348
+ transparencyType,
349
+ sssIOR,
350
+ sssRefractionIntensity,
351
+ transparencyEnabled,
352
+ opacityTexture,
353
+ newSelectedMaterial,
354
+ albedoHasAlpha,
355
+ clearTextureChannels,
356
+ uvXScale,
357
+ uvYScale,
358
+ uvScaleLock,
359
+ uvXRotation,
360
+ uvYRotation,
361
+ uvZRotation,
362
+ uvXOffset,
363
+ uvYOffset
364
+ } = payload;
365
+ const material = scene?.getMaterialByName?.(id, true);
366
+ if (material) {
367
+ // Renaming material.
368
+ if (newId) {
369
+ material.id = newId;
370
+ material.name = newId;
371
+ if (Array.isArray(scene.metadata.selectedMaterials) && scene.metadata.selectedMaterials.length > 0) {
372
+ // We currently only support one selectedMaterial at a time so we should set the changed material to selected
373
+ newMetaDataEntry('selectedMaterials', [material]);
374
+ }
375
+ }
376
+
377
+ // Select all meshes with current material.
378
+ if (selectMaterial) {
379
+ resetSelectedMeshes();
380
+ const userMeshes = getUserMeshes();
381
+ userMeshes.forEach(mesh => mesh.material?.name === id && addNewMesh(mesh));
382
+ newMetaDataEntry('selectedMeshes', buildMeshIdArray());
383
+ refreshHighlight();
384
+ return;
385
+ }
386
+
387
+ // Selecting single material.
388
+ if (newSelectedMaterial) {
389
+ resetSelectedMeshes();
390
+ const newMaterial = scene.metadata.materials.find(mat => mat.materialId === id);
391
+ newMetaDataEntry('selectedMaterials', [newMaterial]);
392
+ refreshHighlight();
393
+ return;
394
+ }
395
+
396
+ // Reset supported texture channels, useful when manually importing a new material.
397
+ if (clearTextureChannels) {
398
+ const workingMaterial = scene.metadata.materials.find(mat => mat.materialId === id) || {};
399
+ Object.keys(workingMaterial)?.forEach(key => {
400
+ if (key.endsWith('Texture')) {
401
+ material[key] = null;
402
+ }
403
+ });
404
+ }
405
+
406
+ // Back-face culling
407
+ if (backfaceCullingEnabled !== undefined) material.backFaceCulling = backfaceCullingEnabled;
408
+
409
+ // Wireframe
410
+ if (wireFrameEnabled !== undefined) material.wireframe = wireFrameEnabled;
411
+
412
+ // Point cloud
413
+ if (pointsCloudEnabled !== undefined) material.pointsCloud = pointsCloudEnabled;
414
+
415
+ // Mesh simplification and texture downscaling
416
+ if (!_.isEmpty(meshSimplification)) {
417
+ material.meshSimplification = {
418
+ ...material.meshSimplification,
419
+ ...meshSimplification
420
+ };
421
+ }
422
+ if (!_.isEmpty(textureDownscaling)) {
423
+ material.textureDownscaling = {
424
+ ...material.textureDownscaling,
425
+ ...textureDownscaling
426
+ };
427
+ }
428
+
429
+ // Diffuse
430
+ if (albedoColor) material.albedoColor = newColor(albedoColor);
431
+ if (environmentIntensity !== undefined) material.environmentIntensity = environmentIntensity;
432
+ if (!_.isEmpty(albedoTexture?.src || albedoTexture?.url)) {
433
+ material.albedoTexture = newTexture(albedoTexture?.src || albedoTexture?.url);
434
+ material.albedoTexture.name = albedoTexture.name;
435
+ applyUVSettings({
436
+ texture: material.albedoTexture,
437
+ material
438
+ });
439
+ }
440
+
441
+ // Metallic
442
+ if (metallic !== undefined) material.metallic = metallic;
443
+ if (!_.isEmpty(metallicTexture?.src || metallicTexture?.url)) {
444
+ // Metallic Texture uses the B channel of the metallicTexture for a PBRMaterial
445
+ const texture = newTexture(metallicTexture?.src || metallicTexture?.url);
446
+ let currentTexture = material.metallicTexture;
447
+ if (!currentTexture) {
448
+ material.metallicTexture = newTexture();
449
+ material.metallicTexture.name = `${material.name} (Metallic-Roughness)`;
450
+ currentTexture = material.metallicTexture;
451
+ }
452
+ const metallicBlob = dataUrlToBlob(texture.url);
453
+ Promise.all([loadImage(metallicBlob), metallicTextureToImage(currentTexture)]).then(data => {
454
+ const [metallicImage, currentImage] = data;
455
+ const combinedTexture = updateTextureChannel(currentImage, metallicImage, 'B');
456
+ currentTexture.updateURL(combinedTexture);
457
+ material.useRoughnessFromMetallicTextureAlpha = false;
458
+ material.useMetalnessFromMetallicTextureBlue = true;
459
+ texture.dispose();
460
+ applyUVSettings({
461
+ texture: material.metallicTexture,
462
+ material
463
+ });
464
+ newMetaDataEntry('materials', buildMaterialsArray());
465
+ newMetaDataEntry('selectedMaterials', buildSelectedMaterialArray());
466
+ props.clearNotifications?.();
467
+ props.setSerializedData?.(serializeScene());
468
+ }).catch(err => {
469
+ console.log(`Error updating the roughness texture of material: ${material.name}`);
470
+ console.log({
471
+ err
472
+ });
473
+ });
474
+ }
475
+
476
+ // Roughness
477
+ if (roughness !== undefined) material.roughness = roughness;
478
+ if (!_.isEmpty(microSurfaceTexture?.src || microSurfaceTexture?.url)) {
479
+ material.microSurfaceTexture = newTexture(microSurfaceTexture?.src || microSurfaceTexture?.url);
480
+ material.microSurfaceTexture.name = microSurfaceTexture.name;
481
+ applyUVSettings({
482
+ texture: material.microSurfaceTexture,
483
+ material
484
+ });
485
+ }
486
+ if (!_.isEmpty(roughnessTexture?.src || roughnessTexture?.url)) {
487
+ // Roughness Texture uses the G channel of the metallicTexture for a PBRMaterial
488
+ const texture = newTexture(roughnessTexture?.src || roughnessTexture?.url);
489
+ let currentTexture = material.metallicTexture;
490
+ if (!currentTexture) {
491
+ material.metallicTexture = newTexture();
492
+ material.metallicTexture.name = `${material.name} (Metallic-Roughness)`;
493
+ currentTexture = material.metallicTexture;
494
+ }
495
+ const roughnessBlob = dataUrlToBlob(texture.url);
496
+ Promise.all([loadImage(roughnessBlob), metallicTextureToImage(currentTexture)]).then(data => {
497
+ const [roughnessImage, metallicImage] = data;
498
+ const combinedTexture = updateTextureChannel(metallicImage, roughnessImage, 'G');
499
+ currentTexture.updateURL(combinedTexture);
500
+ material.useRoughnessFromMetallicTextureAlpha = false;
501
+ material.useRoughnessFromMetallicTextureGreen = true;
502
+ texture.dispose();
503
+ applyUVSettings({
504
+ texture: material.metallicTexture,
505
+ material
506
+ });
507
+ newMetaDataEntry('materials', buildMaterialsArray());
508
+ newMetaDataEntry('selectedMaterials', buildSelectedMaterialArray());
509
+ props.clearNotifications?.();
510
+ props.setSerializedData?.(serializeScene());
511
+ }).catch(err => {
512
+ console.log(`Error updating the roughness texture of material: ${material.name}`);
513
+ console.log({
514
+ err
515
+ });
516
+ });
517
+ }
518
+
519
+ // Ambient
520
+ if (ambientColor) material.ambientColor = newColor(ambientColor);
521
+ if (ambientTextureStrength !== undefined) material.ambientTextureStrength = ambientTextureStrength;
522
+ if (!_.isEmpty(ambientTexture?.src || ambientTexture?.url)) {
523
+ material.ambientTexture = newTexture(ambientTexture?.src || ambientTexture?.url);
524
+ material.ambientTexture.name = ambientTexture.name;
525
+ applyUVSettings({
526
+ texture: material.ambientTexture,
527
+ material
528
+ });
529
+ }
530
+
531
+ // Emissive
532
+ if (emissiveColor) material.emissiveColor = newColor(emissiveColor);
533
+ if (emissiveIntensity !== undefined) material.emissiveIntensity = emissiveIntensity;
534
+ if (!_.isEmpty(emissiveTexture?.src || emissiveTexture?.url)) {
535
+ material.emissiveTexture = newTexture(emissiveTexture?.src || emissiveTexture?.url);
536
+ material.emissiveTexture.name = emissiveTexture.name;
537
+ applyUVSettings({
538
+ texture: material.emissiveTexture,
539
+ material
540
+ });
541
+ }
542
+
543
+ // Normal/Bump
544
+ if (bumpIntensity !== undefined && !_.isEmpty(material.bumpTexture?.url)) {
545
+ material.bumpTexture.level = bumpIntensity;
546
+ }
547
+ if (!_.isEmpty(bumpTexture?.src || bumpTexture?.url)) {
548
+ material.bumpTexture = newTexture(bumpTexture?.src || bumpTexture?.url);
549
+ material.bumpTexture.name = bumpTexture.name;
550
+ applyUVSettings({
551
+ texture: material.bumpTexture,
552
+ material
553
+ });
554
+ }
555
+
556
+ // Clear coat
557
+ if (clearCoatEnabled !== undefined) material.clearCoat.isEnabled = clearCoatEnabled;
558
+ if (material.clearCoat?.isEnabled) {
559
+ if (clearCoatIntensity !== undefined) material.clearCoat.intensity = clearCoatIntensity ?? 1;
560
+ if (clearCoatRoughness !== undefined) material.clearCoat.roughness = clearCoatRoughness ?? 0.1;
561
+ if (clearCoatIOR !== undefined) material.clearCoat.indexOfRefraction = clearCoatIOR ?? 1.52;
562
+ }
563
+
564
+ // Transparency
565
+ if (!_.isEmpty(id) && material && transparencyEnabled !== undefined) {
566
+ if (!albedoHasAlpha) {
567
+ material.transparencyMode = BABYLON.PBRMaterial.PBRMATERIAL_ALPHABLEND;
568
+ }
569
+ material.transparencyEnabled = transparencyEnabled;
570
+ }
571
+
572
+ // Albedo alpha
573
+ if (!_.isEmpty(id) && material && albedoHasAlpha !== undefined) {
574
+ const hasAlbedoTexture = material?.albedoTexture;
575
+ const alphaMode = albedoHasAlpha ? 'PBRMATERIAL_ALPHATEST' : 'PBRMATERIAL_ALPHABLEND';
576
+ if (hasAlbedoTexture) {
577
+ material.albedoTexture.hasAlpha = albedoHasAlpha;
578
+ material.albedoTexture.useAlphaFromAlbedoTexture = albedoHasAlpha;
579
+ material.transparencyMode = BABYLON.PBRMaterial[alphaMode];
580
+ }
581
+ }
582
+ if (alpha !== undefined) material.alpha = alpha ?? 1;
583
+ if (!_.isEmpty(opacityTexture?.src || opacityTexture?.url)) {
584
+ material.opacityTexture = newTexture(opacityTexture?.src || opacityTexture?.url);
585
+ material.opacityTexture.name = opacityTexture.name;
586
+ applyUVSettings({
587
+ texture: material.opacityTexture,
588
+ material
589
+ });
590
+ }
591
+ if (transparencyType) {
592
+ material.transparencyType = _.toLower(transparencyType);
593
+ }
594
+ const isComplex = _.toLower(material.transparencyType) === TRANSPARENCY_MODES.complex;
595
+ if (material.subSurface) {
596
+ material.subSurface.isRefractionEnabled = isComplex;
597
+ }
598
+ if (isComplex) {
599
+ if (sssRefractionIntensity !== undefined) material.subSurface.refractionIntensity = sssRefractionIntensity ?? 1;
600
+ if (sssIOR !== undefined) material.subSurface.indexOfRefraction = sssIOR ?? 1.52;
601
+ }
602
+
603
+ // UV scale, rotation and offset
604
+ const xScaleReq = uvXScale !== undefined;
605
+ const yScaleReq = uvYScale !== undefined;
606
+ const xRotationReq = uvXRotation !== undefined;
607
+ const yRotationReq = uvYRotation !== undefined;
608
+ const zRotationReq = uvZRotation !== undefined;
609
+ const xOffsetReq = uvXOffset !== undefined;
610
+ const yOffsetReq = uvYOffset !== undefined;
611
+ const uvChangeRequest = xScaleReq || yScaleReq || xRotationReq || yRotationReq || zRotationReq || xOffsetReq || yOffsetReq;
612
+ if (uvChangeRequest) {
613
+ const workingMaterial = scene.metadata.materials.find(mat => mat.materialId === id) || {};
614
+ Object.keys(workingMaterial)?.forEach(key => {
615
+ const texture = material[key];
616
+ const validMaterialWithTexture = _.isObject(texture) && !_.isArray(texture) && key.endsWith('Texture');
617
+ const hasAllKeys = texture && ['uAng', 'wAng', 'vAng', 'uOffset', 'vOffset', 'uScale', 'vScale'].every(key => texture[key] !== undefined);
618
+ if (validMaterialWithTexture && hasAllKeys) {
619
+ if (xRotationReq) texture.uAng = uvXRotation;
620
+ if (yRotationReq) texture.wAng = uvYRotation;
621
+ if (zRotationReq) texture.vAng = uvZRotation;
622
+ if (xOffsetReq) texture.uOffset = uvXOffset;
623
+ if (yOffsetReq) texture.vOffset = uvYOffset;
624
+ if ((xScaleReq || yScaleReq) && uvScaleLock) {
625
+ texture.uScale = uvXScale ?? uvYScale;
626
+ texture.vScale = uvXScale ?? uvYScale;
627
+ return;
628
+ }
629
+ if (xScaleReq) texture.uScale = uvXScale;
630
+ if (yScaleReq) texture.vScale = uvYScale;
631
+ if (uvScaleLock !== undefined) newMetaDataEntry('uvScaleLock', uvScaleLock);
632
+ }
633
+ });
634
+ }
635
+ newMetaDataEntry('materials', buildMaterialsArray());
636
+ newMetaDataEntry('selectedMaterials', buildSelectedMaterialArray());
637
+ }
638
+ };
639
+ export const updateMesh = inboundData => {
640
+ const {
641
+ payload
642
+ } = inboundData;
643
+ const {
644
+ id,
645
+ useSavedPosition,
646
+ axisCompensation,
647
+ boundingBoxEnabled,
648
+ transforms,
649
+ flipHorizontally,
650
+ globalTransform,
651
+ variantName,
652
+ resetVariant,
653
+ animationId,
654
+ resetAnimation,
655
+ loopAnimation = true,
656
+ playAnimation,
657
+ pauseAnimation,
658
+ seekAnimation,
659
+ animationProgress,
660
+ sourceUnit,
661
+ displayUnit
662
+ } = payload;
663
+ const khrExtension = GLTF2.KHR_materials_variants;
664
+ const selectedMesh = scene.metadata.selectedMeshes?.[0];
665
+ const meshChangeTracking = scene.metadata?.meshChangeTracking || [];
666
+ const selectedSourceUnit = scene.metadata.selectedSourceUnit;
667
+ const selectedDisplayUnit = scene.metadata?.selectedDisplayUnit;
668
+ const isBoundingBoxEnabled = gizmoManager.boundingBoxGizmoEnabled;
669
+ const userMeshes = getUserMeshes();
670
+ const rootMesh = scene.getMeshByID('__root__');
671
+ const newTrackingData = args => meshChangeTracking?.map(mesh => {
672
+ return mesh.meshId === selectedMesh?.id ? {
673
+ ...mesh,
674
+ ...args
675
+ } : mesh;
676
+ });
677
+
678
+ // Change display units
679
+ if (!_.isEmpty(displayUnit) && selectedSourceUnit && selectedSourceUnit !== unitless) {
680
+ const newDisplayUnit = displayUnit === unitless ? selectedSourceUnit : displayUnit;
681
+ newMetaDataEntry('selectedDisplayUnit', newDisplayUnit);
682
+ if (isBoundingBoxEnabled) {
683
+ toggleBoundingBoxWidget();
684
+ toggleBoundingBoxWidget();
685
+ }
686
+ }
687
+
688
+ // Change mesh scale based on source units
689
+ if (!_.isEmpty(sourceUnit) && !_.isEmpty(userMeshes)) {
690
+ const globalTransforms = scene.metadata.globalTransforms;
691
+ const {
692
+ tx,
693
+ ty,
694
+ tz,
695
+ sx,
696
+ sy,
697
+ sz
698
+ } = globalTransforms;
699
+ const scaleFactor = getScaleFactor(sourceUnit);
700
+ const resetScaleFactor = getScaleFactor(selectedSourceUnit) / scaleFactor;
701
+ const isDifferent = !_.isEqual(sourceUnit, selectedSourceUnit);
702
+ const isMeter = _.isEqual(sourceUnit, 'm') || _.isEqual(sourceUnit, 'meter');
703
+ const newScale = scaleValue => {
704
+ if (!selectedSourceUnit && !isMeter) return scaleValue * scaleFactor;
705
+ if (selectedSourceUnit && isMeter) return scaleValue / resetScaleFactor;
706
+ if (isMeter) return scaleValue / scaleFactor;
707
+ if (selectedSourceUnit === 'm' || selectedSourceUnit === 'meter') {
708
+ return scaleValue / resetScaleFactor;
709
+ }
710
+ if (selectedSourceUnit && !isMeter || selectedSourceUnit && sourceUnit === unitless) {
711
+ // NOTE: If switching to unit other than meter, or user sends source unit reset,
712
+ // reset back to original mystery unit size/scale first.
713
+ const lastScaleFactor = getScaleFactor(selectedSourceUnit);
714
+ const originalScale = scaleValue / lastScaleFactor;
715
+ return originalScale * scaleFactor;
716
+ }
717
+ return scaleValue / resetScaleFactor * scaleFactor;
718
+ };
719
+ const convertedScales = {
720
+ tx: newScale(tx),
721
+ ty: newScale(ty),
722
+ tz: newScale(tz),
723
+ sx: newScale(sx),
724
+ sy: newScale(sy),
725
+ sz: newScale(sz)
726
+ };
727
+ if (isDifferent) {
728
+ const unit = scaleUnits.find(unit => unit.abbreviation === sourceUnit || unit.name === sourceUnit);
729
+ const {
730
+ tx,
731
+ ty,
732
+ tz,
733
+ sx,
734
+ sy,
735
+ sz
736
+ } = convertedScales;
737
+ const scalingVector = newVector(sx, sy, sz);
738
+ const positionVector = newVector(tx, ty, tz);
739
+ const sourceUnitScaleOperation = true;
740
+ performTransform({
741
+ type: 'scale',
742
+ vector: scalingVector,
743
+ newTrackingData,
744
+ globalTransform: true,
745
+ sx,
746
+ sy,
747
+ sz
748
+ });
749
+ performTransform({
750
+ type: 'move',
751
+ vector: positionVector,
752
+ newTrackingData,
753
+ globalTransform: true,
754
+ tx,
755
+ ty,
756
+ tz
757
+ });
758
+ if (sourceUnit !== unitless) {
759
+ newMetaDataEntry('selectedSourceUnit', sourceUnit);
760
+ !selectedDisplayUnit && newMetaDataEntry('selectedDisplayUnit', unit?.abbreviation);
761
+ }
762
+ if (sourceUnit === unitless) {
763
+ newMetaDataEntry('selectedSourceUnit', null);
764
+ newMetaDataEntry('selectedDisplayUnit', null);
765
+ }
766
+ newMetaDataEntry('globalTransforms', {
767
+ ...globalTransforms,
768
+ ...convertedScales
769
+ });
770
+ modelToOrigin(sourceUnitScaleOperation);
771
+ isBoundingBoxEnabled && toggleBoundingBoxWidget();
772
+ focusCamera();
773
+ isBoundingBoxEnabled && toggleBoundingBoxWidget();
774
+ return;
775
+ }
776
+ }
777
+
778
+ // Change mesh variant data
779
+ if (!_.isEmpty(variantName) && rootMesh) {
780
+ khrExtension.SelectVariant(rootMesh, variantName);
781
+ newMetaDataEntry('selectedMaterialVariant', variantName);
782
+ }
783
+
784
+ // Reset mesh variant data
785
+ if (_.isEmpty(variantName) && resetVariant && rootMesh) {
786
+ khrExtension.Reset(rootMesh);
787
+ newMetaDataEntry('selectedMaterialVariant', null);
788
+ }
789
+ const animationGroups = scene.animationGroups || [];
790
+ const hasAnimationId = animationId !== undefined && animationId !== null && `${animationId}` !== '';
791
+ const stopAllAnimationGroups = () => {
792
+ animationGroups.forEach(group => {
793
+ group.stop();
794
+ group.reset();
795
+ });
796
+ };
797
+
798
+ // Reset current animation
799
+ if (resetAnimation) {
800
+ stopAllAnimationGroups();
801
+ newMetaDataEntry('selectedAnimation', null);
802
+ syncSelectedAnimationMeta(null);
803
+ props.setSerializedData?.(serializeScene());
804
+ }
805
+
806
+ // Change active animation
807
+ if (hasAnimationId) {
808
+ const animationGroup = animationGroups.find((group, index) => `${index}` === `${animationId}`);
809
+ stopAllAnimationGroups();
810
+ if (animationGroup) {
811
+ animationGroup.start(Boolean(loopAnimation));
812
+ newMetaDataEntry('selectedAnimation', `${animationId}`);
813
+ syncSelectedAnimationMeta(animationGroup);
814
+ ensureAnimationStateSync();
815
+ props.setSerializedData?.(serializeScene());
816
+ }
817
+ }
818
+ const activeAnimationGroup = getActiveAnimationGroup(animationGroups);
819
+
820
+ // Play current animation
821
+ if (playAnimation && activeAnimationGroup) {
822
+ activeAnimationGroup.play(Boolean(loopAnimation));
823
+ syncSelectedAnimationMeta(activeAnimationGroup);
824
+ ensureAnimationStateSync();
825
+ props.setSerializedData?.(serializeScene());
826
+ }
827
+
828
+ // Pause current animation
829
+ if (pauseAnimation && activeAnimationGroup) {
830
+ activeAnimationGroup.pause();
831
+ syncSelectedAnimationMeta(activeAnimationGroup);
832
+ props.setSerializedData?.(serializeScene());
833
+ }
834
+
835
+ // Seek current animation to a progress position (0–1)
836
+ if (seekAnimation && activeAnimationGroup && animationProgress !== undefined) {
837
+ const {
838
+ from,
839
+ to
840
+ } = activeAnimationGroup;
841
+ const frame = from + animationProgress * (to - from);
842
+ activeAnimationGroup.goToFrame(frame);
843
+ syncSelectedAnimationMeta(activeAnimationGroup);
844
+ props.setSerializedData?.(serializeScene());
845
+ }
846
+
847
+ // Bounding Box Toggle
848
+ if (boundingBoxEnabled !== undefined) {
849
+ toggleBoundingBoxWidget();
850
+ }
851
+ if (flipHorizontally) {
852
+ const multiMeshOperation = userMeshes.length > 1;
853
+ userMeshes.forEach(mesh => addNewMesh(mesh));
854
+ multiMeshOperation ? multiMeshTNode.scaling = newVector(-1, 1, 1) : singleMeshTNode.scaling = newVector(-1, 1, 1);
855
+ handleMousePointerTap();
856
+ return;
857
+ }
858
+ const meshPosition = useSavedPosition ? scene.metadata?.userMeshPositions?.find(mesh => mesh.positionId === id && selectedMesh.id === mesh.meshId) || {} : transforms;
859
+ if (_.isEmpty(meshPosition)) return;
860
+ const {
861
+ tx,
862
+ ty,
863
+ tz,
864
+ rx,
865
+ ry,
866
+ rz,
867
+ sx,
868
+ sy,
869
+ sz
870
+ } = meshPosition;
871
+ if (useSavedPosition) {
872
+ const filteredPositions = scene.metadata.selectedUserMeshPositions.filter(pos => pos.meshId !== meshPosition.meshId);
873
+ filteredPositions.push(meshPosition);
874
+ newMetaDataEntry('selectedUserMeshPositions', filteredPositions);
875
+ }
876
+
877
+ // Axis Compensation
878
+ if (_.isString(axisCompensation) && transforms && scene.metadata.selectedAxisCompensation !== axisCompensation) {
879
+ const userMaterials = getUserMaterials();
880
+ const firstMaterial = userMaterials[0] || {};
881
+ const multiMeshOperation = userMeshes.length > 1;
882
+ resetSelectedMeshes();
883
+ userMeshes.forEach(mesh => addNewMesh(mesh));
884
+ multiMeshOperation ? multiMeshTNode.rotation = newVector(toRadians(rx), toRadians(ry), toRadians(rz)) : singleMeshTNode.rotation = newVector(toRadians(rx), toRadians(ry), toRadians(rz));
885
+ handleMousePointerTap();
886
+ newMetaDataEntry('selectedAxisCompensation', axisCompensation);
887
+ newMetaDataEntry('meshChangeTracking', buildMeshPositionsArray());
888
+ newMetaDataEntry('selectedMaterials', []);
889
+
890
+ // Reset scaling to previous
891
+ meshChangeTracking.forEach(mesh => {
892
+ const foundMesh = meshChangeTracking.find(prevMesh => prevMesh.id === mesh.id);
893
+ if (foundMesh) {
894
+ mesh.sx = foundMesh.sx;
895
+ mesh.sy = foundMesh.sy;
896
+ mesh.sz = foundMesh.sz;
897
+ }
898
+ });
899
+ if (!_.isEmpty(firstMaterial)) {
900
+ const existingMaterial = scene.metadata.materials.find(mat => mat.materialId === firstMaterial.id);
901
+ newMetaDataEntry('selectedMaterials', [existingMaterial]);
902
+ }
903
+ newMetaDataEntry('selectedMeshes', []);
904
+ modelToOrigin();
905
+ focusCamera();
906
+ multiMeshOperation ? multiMeshTNode.rotation = newVector(toRadians(0), toRadians(0), toRadians(0)) : singleMeshTNode.rotation = newVector(toRadians(0), toRadians(0), toRadians(0));
907
+ }
908
+
909
+ // Transform inputs and Custom user position restoring.
910
+ if (meshPosition) {
911
+ const doPosition = [tx, ty, tz].every(val => val !== undefined);
912
+ const doRotation = [rx, ry, rz].every(val => val !== undefined);
913
+ const doScale = [sx, sy, sz].every(val => val !== undefined);
914
+ if (doPosition) {
915
+ const positionVector = newVector(tx, ty, tz);
916
+ performTransform({
917
+ type: 'move',
918
+ vector: positionVector,
919
+ newTrackingData,
920
+ globalTransform,
921
+ tx,
922
+ ty,
923
+ tz
924
+ });
925
+ }
926
+ if (doRotation) {
927
+ const rotationVector = newVector(toRadians(rx), toRadians(ry), toRadians(rz));
928
+ performTransform({
929
+ type: 'rotate',
930
+ vector: rotationVector,
931
+ newTrackingData,
932
+ globalTransform,
933
+ rx,
934
+ ry,
935
+ rz
936
+ });
937
+ }
938
+ if (doScale) {
939
+ // TODO: Handle zeroes
940
+ const currentMeshTracking = globalTransform ? scene.metadata.globalTransforms : scene.metadata.meshChangeTracking.find(mesh => mesh.meshId === selectedMesh.id);
941
+ const {
942
+ sx: currentSx,
943
+ sy: currentSy,
944
+ sz: currentSz
945
+ } = currentMeshTracking;
946
+ const newScaleX = globalTransform ? sx : sx / currentSx || 1;
947
+ const newScaleY = globalTransform ? sy : sy / currentSy || 1;
948
+ const newScaleZ = globalTransform ? sz : sz / currentSz || 1;
949
+ const isValid = [newScaleX, newScaleY, newScaleZ].every(val => val !== 0);
950
+ if (isValid) {
951
+ const scalingVector = newVector(newScaleX, newScaleY, newScaleZ);
952
+ performTransform({
953
+ type: 'scale',
954
+ vector: scalingVector,
955
+ newTrackingData,
956
+ globalTransform,
957
+ sx,
958
+ sy,
959
+ sz
960
+ });
961
+ }
962
+ }
963
+ }
964
+ attachToSelectMeshesNode();
965
+ createBoundingMesh();
966
+ resetManagerGizmos(GIZMOS.BoundingBoxGizmo);
967
+ };
968
+ export const updateCamera = inboundData => {
969
+ const {
970
+ payload
971
+ } = inboundData;
972
+ const {
973
+ id,
974
+ transforms,
975
+ enableSafeFrame,
976
+ aspectRatio,
977
+ orthoCamera,
978
+ resetCamera,
979
+ takeScreenshot,
980
+ isBillboard,
981
+ imageDataOnly
982
+ } = payload;
983
+ const camera = scene.activeCamera;
984
+ if (camera) {
985
+ // Orthographic view selections.
986
+ if (orthoCamera !== undefined) toggleOrthographicViews(null, orthoCamera);
987
+
988
+ // Update camera position and rotation.
989
+ if (_.isString(id) && !_.isEmpty(transforms)) {
990
+ const {
991
+ tx,
992
+ ty,
993
+ tz,
994
+ rx,
995
+ ry,
996
+ rz,
997
+ tarX,
998
+ tarY,
999
+ tarZ
1000
+ } = transforms;
1001
+ if (tx && ty && tz) camera.position = newVector(tx, ty, tz);
1002
+ if (rx && ry && rz) camera.rotation = newVector(rx, ry, rz);
1003
+ if (tarX && tarY && tarZ) camera.target = newVector(tarX, tarY, tarZ);
1004
+ if (camera.mode === BABYLON.Camera.ORTHOGRAPHIC_CAMERA) {
1005
+ camera.mode = BABYLON.Camera.PERSPECTIVE_CAMERA;
1006
+ }
1007
+ newMetaDataEntry('selectedUserCameraView', {
1008
+ id,
1009
+ tx,
1010
+ ty,
1011
+ tz,
1012
+ rx,
1013
+ ry,
1014
+ rz,
1015
+ tarX,
1016
+ tarY,
1017
+ tarZ
1018
+ });
1019
+ }
1020
+
1021
+ // Reset camera to default position and rotation.
1022
+ if (resetCamera !== undefined) {
1023
+ prepareCamera();
1024
+ const {
1025
+ position,
1026
+ rotation,
1027
+ target
1028
+ } = scene.activeCamera;
1029
+ const [tx, ty, tz] = [position.x, position.y, position.z];
1030
+ const [rx, ry, rz] = [rotation.x, rotation.y, rotation.z];
1031
+ const [tarX, tarY, tarZ] = [target.x, target.y, target.z];
1032
+ newMetaDataEntry('selectedOrthoView', '');
1033
+ newMetaDataEntry('selectedUserCameraView', {
1034
+ id: 'Default',
1035
+ tx,
1036
+ ty,
1037
+ tz,
1038
+ rx,
1039
+ ry,
1040
+ rz,
1041
+ tarX,
1042
+ tarY,
1043
+ tarZ
1044
+ });
1045
+ }
1046
+
1047
+ // Toggle safe frame.
1048
+ if (enableSafeFrame !== undefined && _.isNumber(aspectRatio)) {
1049
+ const newMetaValue = enableSafeFrame ? Object.values(ratios).find(ratio => ratio.value === aspectRatio) : {};
1050
+ toggleSafeFrame(null, aspectRatio, enableSafeFrame);
1051
+ newMetaDataEntry('selectedRatio', newMetaValue);
1052
+ }
1053
+
1054
+ // Take screenshot.
1055
+ if (takeScreenshot !== undefined && _.isNumber(aspectRatio)) {
1056
+ const callback = screenshotData => {
1057
+ newMetaDataEntry('screenshotData', screenshotData);
1058
+ const outerFrame = guiTexture.getControlByName(GUI.outerSafeFrame);
1059
+ const {
1060
+ widthInPixels,
1061
+ heightInPixels,
1062
+ centerX,
1063
+ centerY
1064
+ } = outerFrame;
1065
+ const baseResolution = 2048;
1066
+ const ratio = widthInPixels > heightInPixels ? widthInPixels / baseResolution : heightInPixels / baseResolution;
1067
+ const highResHeight = heightInPixels / ratio;
1068
+ const highResWidth = widthInPixels / ratio;
1069
+
1070
+ // NOTE: Serializes scene for billboard screenshots exactly onSuccess.
1071
+ props.setSerializedData?.(serializeScene());
1072
+ const canvas = document.createElement('canvas');
1073
+ const context = canvas.getContext('2d');
1074
+ canvas.width = highResWidth;
1075
+ canvas.height = highResHeight;
1076
+ const image = new Image();
1077
+ image.src = screenshotData;
1078
+ image.onload = () => {
1079
+ context.drawImage(image, outerFrame.isVisible ? centerX - widthInPixels / 2 : centerX - canvas.width / 2,
1080
+ // Moves image right
1081
+ outerFrame.isVisible ? centerY - heightInPixels / 2 : centerY - canvas.height / 2,
1082
+ // Moves image down
1083
+ outerFrame.isVisible ? widthInPixels : canvas.width, outerFrame.isVisible ? heightInPixels : canvas.height, 0,
1084
+ // Moves image right
1085
+ 0,
1086
+ // Moves image down
1087
+ highResWidth, highResHeight);
1088
+ const imageData = canvas.toDataURL('image/png', 1.0);
1089
+
1090
+ // Download
1091
+ if (!imageDataOnly) {
1092
+ const link = document.createElement('a');
1093
+ link.download = 'screenshot.png';
1094
+ link.href = imageData;
1095
+ document.body.appendChild(link);
1096
+ link.click();
1097
+ document.body.removeChild(link);
1098
+ }
1099
+ };
1100
+ };
1101
+ newScreenshot(isBillboard, callback);
1102
+ newMetaDataEntry('selectedRatio', Object.values(ratios).find(ratio => ratio.value === aspectRatio));
1103
+ }
1104
+ }
1105
+ };
1106
+ export const updateEnvironment = inboundData => {
1107
+ const {
1108
+ payload
1109
+ } = inboundData;
1110
+ const {
1111
+ url,
1112
+ envIntensity,
1113
+ envBlur,
1114
+ transforms
1115
+ } = payload;
1116
+ const skyBox = scene.getMaterialById('skyBox', true);
1117
+ if (skyBox) {
1118
+ // Intensity
1119
+ if (envIntensity !== undefined) {
1120
+ scene.environmentIntensity = envIntensity;
1121
+ newMetaDataEntry('environmentIntensity', envIntensity);
1122
+ }
1123
+
1124
+ // Blur
1125
+ if (envBlur !== undefined) {
1126
+ skyBox.microSurface = 1 - envBlur;
1127
+ newMetaDataEntry('environmentBlur', envBlur);
1128
+ }
1129
+
1130
+ // Update environment texture.
1131
+ if (_.isString(url)) {
1132
+ const selectedUserEnvironment = scene.metadata?.userEnvironments?.find(env => env.url === url) || props.defaultEnvironments.find(item => item.envURL === url) || {
1133
+ id: 'Default',
1134
+ url: scene.metadata.defaultEnvironment
1135
+ };
1136
+ const {
1137
+ id,
1138
+ name,
1139
+ envURL,
1140
+ url: userURL
1141
+ } = selectedUserEnvironment;
1142
+ const metaName = id || name;
1143
+ const metaURL = userURL || envURL;
1144
+ scene.environmentTexture.url = metaURL;
1145
+ scene.environmentTexture.name = metaURL;
1146
+ skyBox.reflectionTexture.url = metaURL;
1147
+ skyBox.reflectionTexture.name = metaURL;
1148
+ const newEnv = scene.environmentTexture.clone();
1149
+ const newSkyBox = skyBox.reflectionTexture.clone();
1150
+ scene.environmentTexture.dispose();
1151
+ skyBox.reflectionTexture.dispose();
1152
+ scene.environmentTexture = newEnv;
1153
+ skyBox.reflectionTexture = newSkyBox;
1154
+ newMetaDataEntry('selectedUserEnvironment', {
1155
+ id: metaName,
1156
+ url: metaURL
1157
+ });
1158
+ }
1159
+
1160
+ // Rotation
1161
+ if (transforms?.rotation?.ry !== undefined) {
1162
+ const envRotation = toRadians(transforms.rotation.ry);
1163
+ skyBox.reflectionTexture.rotationY = envRotation;
1164
+ scene.environmentTexture.rotationY = envRotation;
1165
+ newMetaDataEntry('environmentRotation', toDegrees(envRotation));
1166
+ }
1167
+ }
1168
+ };
1169
+ export const updateViewport = inboundData => {
1170
+ const {
1171
+ payload
1172
+ } = inboundData;
1173
+ const {
1174
+ envVisible,
1175
+ shadows,
1176
+ iblPipeline,
1177
+ axes,
1178
+ grid,
1179
+ hideSelected,
1180
+ unhideAll,
1181
+ unhideLast,
1182
+ deselectAll
1183
+ } = payload;
1184
+ const hdrSkyBox = scene.getMeshByName('hdrSkyBox');
1185
+ const xAxisMesh = scene.getMeshByName('xAxisMesh');
1186
+ const yAxisMesh = scene.getMeshByName('yAxisMesh');
1187
+ const zAxisMesh = scene.getMeshByName('zAxisMesh');
1188
+
1189
+ // Hide selected meshes.
1190
+ if (hideSelected) {
1191
+ selectedMeshes.forEach(mesh => {
1192
+ mesh.isVisible = false;
1193
+ hiddenMeshes.push(mesh);
1194
+ });
1195
+ resetSelectedMeshes();
1196
+ }
1197
+
1198
+ // Unhide all meshes.
1199
+ if (unhideAll) {
1200
+ const userMeshes = getUserMeshes();
1201
+ userMeshes.forEach(mesh => mesh.isVisible = true);
1202
+ hiddenMeshes = [];
1203
+ }
1204
+
1205
+ // Unhide last hidden mesh.
1206
+ if (unhideLast) {
1207
+ if (!_.isEmpty(hiddenMeshes)) {
1208
+ const mesh = hiddenMeshes.pop();
1209
+ mesh.isVisible = true;
1210
+ }
1211
+ }
1212
+
1213
+ // Deselect all meshes.
1214
+ if (deselectAll) resetSelectedMeshes();
1215
+
1216
+ // Environment visibility.
1217
+ if (envVisible !== undefined) {
1218
+ hdrSkyBox.setEnabled(envVisible);
1219
+ newMetaDataEntry('viewportEnvironment', envVisible);
1220
+ }
1221
+
1222
+ // Shadow visbility
1223
+ if (shadows !== undefined) {
1224
+ mirrorGround.receiveShadows = shadows;
1225
+ newMetaDataEntry('viewportShadows', shadows);
1226
+ }
1227
+
1228
+ // Grid visibility
1229
+ if (grid !== undefined) {
1230
+ ground.setEnabled(grid);
1231
+ newMetaDataEntry('viewportGround', grid);
1232
+ }
1233
+
1234
+ // Axis visibility
1235
+ if (axes !== undefined) {
1236
+ xAxisMesh.setEnabled(axes);
1237
+ yAxisMesh.setEnabled(axes);
1238
+ zAxisMesh.setEnabled(axes);
1239
+ newMetaDataEntry('viewportAxes', axes);
1240
+ }
1241
+
1242
+ // IBL Shadow Rendering Pipeline
1243
+ if (iblPipeline !== undefined) {
1244
+ iblShadowPipeline.toggleShadow(iblPipeline);
1245
+ newMetaDataEntry('iblShadowPipelineEnabled', iblPipeline);
1246
+ }
1247
+ };
1248
+ export const updatePublish = inboundData => {
1249
+ const {
1250
+ payload
1251
+ } = inboundData;
1252
+ const existingPublish = scene.metadata?.publish || {};
1253
+ const totalTriangles = +(scene.metadata.statistics?.triangles?.replace?.(/,/g, '') || 0);
1254
+ const incomingTargetTriangles = payload?.meshSimplification?.userTargetTriangles || existingPublish?.meshSimplification?.userTargetTriangles || totalTriangles;
1255
+ const estimatedTriangles = payload?.meshSimplification?.estimatedTriangles || existingPublish?.meshSimplification?.estimatedTriangles || 99; // Percent
1256
+
1257
+ const category = payload?.category || existingPublish?.category;
1258
+ const tags = payload?.tags || existingPublish?.tags;
1259
+ const proxy = payload?.proxy || existingPublish?.proxy;
1260
+ const title = payload?.title || existingPublish?.title;
1261
+ const withOptimizationValues = {
1262
+ ...existingPublish,
1263
+ category,
1264
+ tags,
1265
+ proxy,
1266
+ title,
1267
+ textureDownscaling: {
1268
+ ...(existingPublish?.textureDownscaling || {}),
1269
+ ...(payload?.textureDownscaling || {})
1270
+ },
1271
+ meshSimplification: {
1272
+ ...(existingPublish?.meshSimplification || {}),
1273
+ ...(payload?.meshSimplification || {}),
1274
+ totalTriangles: totalTriangles,
1275
+ estimatedTriangles: estimatedTriangles,
1276
+ userTargetTriangles: incomingTargetTriangles,
1277
+ targetTriangles: Math.round(totalTriangles * (estimatedTriangles / 100))
1278
+ }
1279
+ };
1280
+ newMetaDataEntry('publish', withOptimizationValues);
1281
+ };
1282
+ export const updateLighting = inboundData => {
1283
+ const {
1284
+ payload
1285
+ } = inboundData;
1286
+ const {
1287
+ id,
1288
+ transforms,
1289
+ target,
1290
+ enable,
1291
+ diffuse,
1292
+ specular,
1293
+ radius,
1294
+ intensity,
1295
+ distance,
1296
+ rotation
1297
+ } = payload;
1298
+ const light = scene.getLightByName(id);
1299
+ if (!_.isEmpty(transforms)) {
1300
+ const {
1301
+ tx,
1302
+ ty,
1303
+ tz,
1304
+ dirX,
1305
+ dirY,
1306
+ dirZ
1307
+ } = transforms;
1308
+ const isPosition = tx !== undefined && ty !== undefined && tz !== undefined;
1309
+ const isDirection = dirX !== undefined && dirY !== undefined && dirZ !== undefined;
1310
+ if (isPosition) light.position = newVector(tx, ty, tz);
1311
+ if (isDirection) light.direction = newVector(dirX, dirY, dirZ);
1312
+ }
1313
+ if (target) {
1314
+ light.setDirectionToTarget(target);
1315
+ }
1316
+ if (enable !== undefined) {
1317
+ light.setEnabled(enable);
1318
+ }
1319
+ if (radius !== undefined) {
1320
+ light.radius = radius;
1321
+ }
1322
+ if (diffuse) {
1323
+ const {
1324
+ r,
1325
+ g,
1326
+ b
1327
+ } = diffuse;
1328
+ light.diffuse = newColor(r, g, b);
1329
+ }
1330
+ if (specular) {
1331
+ const {
1332
+ r,
1333
+ g,
1334
+ b
1335
+ } = specular;
1336
+ light.specular = newColor(r, g, b);
1337
+ }
1338
+ if (intensity !== undefined) {
1339
+ light.intensity = intensity;
1340
+ }
1341
+ if (distance !== undefined) {
1342
+ const rotation = light?.rotation || 0;
1343
+ const angleRad = rotation * Math.PI / 180;
1344
+ light.position = newVector(distance * Math.cos(angleRad), distance * Math.tanh(angleRad), distance * Math.sin(angleRad));
1345
+ light.setDirectionToTarget(newVector(0, 0, 0));
1346
+ light.distance = distance;
1347
+ }
1348
+ if (rotation !== undefined) {
1349
+ const {
1350
+ position
1351
+ } = light;
1352
+ const {
1353
+ x,
1354
+ y,
1355
+ z
1356
+ } = position;
1357
+ const angleRad = rotation * Math.PI / 180;
1358
+ let distance = light?.distance || Math.sqrt(Math.pow(Math.abs(x), 2) + Math.pow(Math.abs(y), 2) + Math.pow(Math.abs(z), 2));
1359
+ distance = distance > 200 ? 200 : distance;
1360
+ light.position = newVector(distance * Math.cos(angleRad), distance * Math.tanh(angleRad), distance * Math.sin(angleRad));
1361
+ light.setDirectionToTarget(newVector(0, 0, 0));
1362
+ light.rotation = rotation;
1363
+ }
1364
+ newMetaDataEntry('lights', buildLightsArray());
1365
+ };