@archvisioninc/canvas 3.3.8 → 3.3.10

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