redhotmagma-visualization 1.44.10 → 1.45.0

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 (37) hide show
  1. package/Constants.js +1 -0
  2. package/DevApp.js +14 -0
  3. package/Three/Three2D.js +25 -4
  4. package/Three/Three3D.js +121 -83
  5. package/Three/Three3DScene.js +132 -58
  6. package/Utils/createBakedShadow.js +1 -2
  7. package/Utils/initTextureCanvas.js +3 -1
  8. package/Utils/optimizeTextureImage.js +1 -1
  9. package/Utils/rememberCameraViewOffset.js +12 -7
  10. package/Utils/restrictCameraMovement.js +2 -2
  11. package/package.json +3 -3
  12. package/public/libs/draco/README.md +2 -2
  13. package/public/libs/draco/draco_decoder.js +17 -35
  14. package/public/libs/draco/draco_decoder.wasm +0 -0
  15. package/public/libs/draco/draco_wasm_wrapper.js +117 -104
  16. package/public/libs/draco/gltf/draco_decoder.js +16 -31
  17. package/public/libs/draco/gltf/draco_decoder.wasm +0 -0
  18. package/public/libs/draco/gltf/draco_wasm_wrapper.js +116 -104
  19. package/src/Constants.js +1 -0
  20. package/src/DevApp.js +12 -0
  21. package/src/Three/Three2D.js +4 -3
  22. package/src/Three/Three3D.js +15 -8
  23. package/src/Three/Three3DScene.js +85 -29
  24. package/src/Three/__tests__/Three3D.test.js +0 -33
  25. package/src/Utils/createBakedShadow.js +2 -4
  26. package/src/Utils/initTextureCanvas.js +2 -1
  27. package/src/Utils/optimizeTextureImage.js +1 -1
  28. package/src/Utils/rememberCameraViewOffset.js +5 -3
  29. package/src/Utils/restrictCameraMovement.js +1 -1
  30. package/Three/Controls/OrbitControls.js +0 -724
  31. package/Three/Loaders/ColladaLoader.js +0 -2568
  32. package/Three/Loaders/MTLLoader.js +0 -418
  33. package/Three/Loaders/OBJLoader.js +0 -516
  34. package/src/Three/Controls/OrbitControls.js +0 -934
  35. package/src/Three/Loaders/ColladaLoader.js +0 -3245
  36. package/src/Three/Loaders/MTLLoader.js +0 -512
  37. package/src/Three/Loaders/OBJLoader.js +0 -640
package/src/Constants.js CHANGED
@@ -8,6 +8,7 @@ export const EventTypes = {
8
8
  LoadStart: "Visualization/LoadStart",
9
9
  LoadFinished: "Visualization/LoadFinished",
10
10
  LoadError: "Visualization/LoadError",
11
+ EmptySceneError: "Visualization/EmptySceneError",
11
12
  Zoomed: "Visualization/Zoomed",
12
13
  HoverIn: "Visualization/HoverIn",
13
14
  HoverOut: "Visualization/HoverOut",
package/src/DevApp.js CHANGED
@@ -332,6 +332,18 @@ const modelData = {
332
332
  },
333
333
  },
334
334
  },
335
+ BrokenModel: {
336
+ designAreaList: [],
337
+ configuration: {
338
+ visualizationData: {
339
+ sceneData: [
340
+ { model: "models/3d/BrokenModel/MissingBuffer.gltf" },
341
+ { model: "models/3d/BrokenModel/MissingImages.gltf" },
342
+ { model: "models/3d/BrokenModel/WorkingPart.gltf" },
343
+ ],
344
+ },
345
+ },
346
+ },
335
347
  },
336
348
  };
337
349
 
@@ -16,8 +16,8 @@ import {
16
16
  WebGLRenderer,
17
17
  BufferGeometry,
18
18
  Float32BufferAttribute,
19
- DoubleSide,
20
- } from "three";
19
+ DoubleSide, LinearSRGBColorSpace,
20
+ } from 'three';
21
21
  import TWEEN from "@tweenjs/tween.js";
22
22
  import get from "lodash/get";
23
23
  import first from "lodash/first";
@@ -118,6 +118,7 @@ export class Three2D extends ThreeBase {
118
118
  depth: true, // needed only for the admin area, consider to make it configurable
119
119
  });
120
120
  renderer.setPixelRatio(window.devicePixelRatio ? window.devicePixelRatio : 1);
121
+ renderer.outputColorSpace = LinearSRGBColorSpace;
121
122
  return renderer;
122
123
  }
123
124
 
@@ -856,7 +857,7 @@ export class Three2D extends ThreeBase {
856
857
  };
857
858
  }
858
859
 
859
- async getScreenshots(callback, { designViewList, areaList, variantList, size }) {
860
+ async getScreenshots(callback, { designViewList, areaList, variantList, size } = {}) {
860
861
  if (!this.renderer) {
861
862
  return callback({});
862
863
  }
@@ -15,9 +15,6 @@ import first from "lodash/first";
15
15
  import findIndex from "lodash/findIndex";
16
16
 
17
17
  import { ThreeBase, onProgress } from "./ThreeBase";
18
- import MTLLoader from "./Loaders/MTLLoader";
19
- import OBJLoader from "./Loaders/OBJLoader";
20
- import ColladaLoader from "./Loaders/ColladaLoader";
21
18
 
22
19
  import { initTextureCanvas } from "../Utils/initTextureCanvas";
23
20
  import { createWorkingCanvas } from "../Utils/createCanvas";
@@ -28,9 +25,12 @@ import { disposeMaterial } from "../Utils/disposeMaterial";
28
25
  import { rememberCameraViewOffset } from "../Utils/rememberCameraViewOffset";
29
26
  import { restrictOrbitControls } from "../Utils/restrictCameraMovement";
30
27
  import { isMobileDevice } from "../Utils/isMobileDevice";
31
- import OrbitControls from "./Controls/OrbitControls";
32
28
  import { createZoomHandler } from "../CommonUtils/pointerEvent";
33
29
  import { createTextureBuilder } from "../Utils/TextureBuilder/createTextureBuilder";
30
+ import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
31
+ import { ColladaLoader } from "three/examples/jsm/loaders/ColladaLoader";
32
+ import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
33
+ import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader";
34
34
 
35
35
  export const MAX_DISTANCE_MULTIPLIER = 1.1;
36
36
  export const MIN_DISTANCE_MULTIPLIER = 0.1;
@@ -580,10 +580,13 @@ export class Three3D extends ThreeBase {
580
580
  return () => {};
581
581
  }
582
582
 
583
- async getScreenshots(callback, { viewList, variantList, size }) {
583
+ async getScreenshots(callback, { viewList, variantList, size } = {}) {
584
584
  if (!this.renderer) {
585
585
  return callback({});
586
586
  }
587
+ if (!variantList) {
588
+ variantList = ["default"];
589
+ }
587
590
  this._screenshottingInProgress = true;
588
591
  this.endAnimation();
589
592
  await this.animationRunning();
@@ -619,10 +622,12 @@ export class Three3D extends ThreeBase {
619
622
  const height = size?.height || 768;
620
623
 
621
624
  const screenshots = {};
622
- const views = this.screenshotViews || this.cameraViews;
625
+ const views = { ...this.cameraViews, ...this.screenshotViews };
623
626
  // if the viewList option is provided, use it as keys instead of the list of all views
624
627
  const viewKeyList =
625
- viewList instanceof Array && viewList.length > 0 ? viewList : Object.keys(views);
628
+ viewList instanceof Array && viewList.length > 0
629
+ ? viewList.filter(viewKey => views[viewKey])
630
+ : Object.keys(views);
626
631
 
627
632
  for (let variant of variantList) {
628
633
  if (this._abortScreenshotting) {
@@ -701,9 +706,11 @@ export class Three3D extends ThreeBase {
701
706
  ? this.findMaterialByName(materialName)
702
707
  : first(this._customizableMaterials);
703
708
  if (!material || !material.map || !material.map.image) {
704
- throw new Error(
709
+ // eslint-disable-next-line no-console
710
+ console.error(
705
711
  `Cannot get drawing context for material: ${materialName}! Is it prepared already?`
706
712
  );
713
+ return;
707
714
  }
708
715
 
709
716
  const { ctx, canvas, originalImage, texture } = this.getCanvasContextForTexture(material.map);
@@ -6,6 +6,7 @@ import {
6
6
  Clock,
7
7
  Color,
8
8
  DirectionalLight,
9
+ DoubleSide,
9
10
  Group,
10
11
  ImageLoader,
11
12
  LoadingManager,
@@ -14,16 +15,13 @@ import {
14
15
  Object3D,
15
16
  PCFSoftShadowMap,
16
17
  PerspectiveCamera,
17
- PlaneBufferGeometry,
18
+ PlaneGeometry,
18
19
  PMREMGenerator,
19
- Quaternion,
20
20
  Scene,
21
21
  ShadowMaterial,
22
22
  Sphere,
23
23
  SphereGeometry,
24
- sRGBEncoding,
25
24
  Texture,
26
- UnsignedByteType,
27
25
  Vector3,
28
26
  WebGLRenderer,
29
27
  } from "three";
@@ -41,6 +39,9 @@ import findIndex from "lodash/findIndex";
41
39
  import merge from "lodash/merge";
42
40
  import createBakedShadow from "../Utils/createBakedShadow";
43
41
  import setLightShadow from "../Utils/setLightShadow";
42
+ import { EventTypes } from "../Constants";
43
+ import { createImageRepository } from "../Utils/createImageRepository";
44
+ import { createCache } from "../Utils/createCache";
44
45
 
45
46
  const UNSET = Symbol("unset");
46
47
 
@@ -126,8 +127,9 @@ function findImageBySrc(tree, src) {
126
127
  tree.traverse(({ material }) => {
127
128
  if (!result && material) {
128
129
  for (const value of Object.values(material)) {
129
- if (value && value.isTexture && value.image && value.image.src === src) {
130
- result = value.image;
130
+ if (value && value.isTexture && (value.image?.src === src || value.userData?.src === src)) {
131
+ result = value.source;
132
+ return result;
131
133
  }
132
134
  }
133
135
  }
@@ -263,7 +265,7 @@ export class Three3DScene extends Three3D {
263
265
  dirLightPosition = new Vector3(1, 1, 1).normalize();
264
266
 
265
267
  defaultCameraPosition = new Vector3(0, 1, 2).normalize();
266
- defaultScreenshotPosition = new Vector3(0, 0, 1).normalize();
268
+ defaultScreenshotPosition = new Vector3(0, 1, 2).normalize();
267
269
 
268
270
  relativeCameraViews = {};
269
271
  relativeScreenshotViews = {};
@@ -284,6 +286,7 @@ export class Three3DScene extends Three3D {
284
286
  blurRadius: 6,
285
287
  shadowOpacity: 0.8,
286
288
  },
289
+ textureImageCacheSize: 20,
287
290
  };
288
291
 
289
292
  constructor(cfg = {}) {
@@ -315,14 +318,40 @@ export class Three3DScene extends Three3D {
315
318
  createLoader(options = {}) {
316
319
  const loadingManager = this.createLoadingManager(options);
317
320
 
318
- const dracoLoader = new DRACOLoader();
319
- dracoLoader.setDecoderPath("libs/draco/gltf/");
321
+ if (!this.dracoLoader) {
322
+ const dracoLoader = new (class PatchedDRACOLoader extends DRACOLoader {
323
+ decodeGeometry(buffer, taskConfig) {
324
+ return super.decodeGeometry(buffer, taskConfig).catch(e => {
325
+ // eslint-disable-next-line no-console
326
+ console.error(e.message || e.error);
327
+ // make sure an empty geometry is returned to not break the app
328
+ return { attributes: {} };
329
+ });
330
+ }
331
+ })();
332
+ dracoLoader.setDecoderPath("libs/draco/gltf/");
333
+ this.dracoLoader = dracoLoader;
334
+ }
320
335
 
321
336
  const loader = new GLTFLoader(loadingManager);
322
- loader.setDRACOLoader(dracoLoader);
337
+ loader.setDRACOLoader(this.dracoLoader);
323
338
  return loader;
324
339
  }
325
340
 
341
+ getImageCache() {
342
+ if (!this._imageCache) {
343
+ this._imageCache = createCache(this.config.textureImageCacheSize);
344
+ }
345
+ return this._imageCache;
346
+ }
347
+
348
+ useImageRepository(imageLoader) {
349
+ return createImageRepository(
350
+ this.getImageCache(),
351
+ src => new Promise((resolve, reject) => imageLoader.load(src, resolve, undefined, reject))
352
+ );
353
+ }
354
+
326
355
  createImageLoader(options = {}) {
327
356
  const loadingManager = this.createLoadingManager(options);
328
357
  const imageLoader = new ImageLoader(loadingManager);
@@ -334,7 +363,6 @@ export class Three3DScene extends Three3D {
334
363
  alpha: true,
335
364
  antialias: true,
336
365
  });
337
- renderer.outputEncoding = sRGBEncoding;
338
366
  renderer.shadowMap.enabled = true;
339
367
  renderer.shadowMap.type = PCFSoftShadowMap;
340
368
  renderer.toneMapping = ACESFilmicToneMapping;
@@ -362,7 +390,7 @@ export class Three3DScene extends Three3D {
362
390
  pmremGenerator.compileEquirectangularShader();
363
391
 
364
392
  this.envMapPromise = new Promise((resolve, reject) => {
365
- new RGBELoader().setDataType(UnsignedByteType).load(
393
+ new RGBELoader().load(
366
394
  envMapURL,
367
395
  texture => {
368
396
  this.envMap = pmremGenerator.fromEquirectangular(texture).texture;
@@ -415,7 +443,7 @@ export class Three3DScene extends Three3D {
415
443
  shadowMaterial = new ShadowMaterial({ color: 0x202020 });
416
444
  }
417
445
 
418
- const mesh = new Mesh(new PlaneBufferGeometry(1, 1), shadowMaterial);
446
+ const mesh = new Mesh(new PlaneGeometry(1, 1), shadowMaterial);
419
447
  mesh.name = "ground";
420
448
  mesh.rotation.x = -Math.PI / 2;
421
449
  mesh.receiveShadow = true;
@@ -477,6 +505,7 @@ export class Three3DScene extends Three3D {
477
505
  }
478
506
 
479
507
  async applyMaterialParams(model, materialOptions, imageLoader) {
508
+ const imageRepository = this.useImageRepository(imageLoader);
480
509
  if (materialOptions) {
481
510
  const { params } = materialOptions;
482
511
  params &&
@@ -514,13 +543,16 @@ export class Three3DScene extends Three3D {
514
543
  case "specularColorMap":
515
544
  case "thicknessMap":
516
545
  case "transmissionMap": {
517
- const texture =
518
- material[p] || (material.map && material.map.clone()) || new Texture();
519
- texture.image =
520
- findImageBySrc(this.sceneContainer, `${this._modelBasePath}${params[p]}`) ||
521
- (await new Promise((resolve, reject) =>
522
- imageLoader.load(params[p], resolve, undefined, reject)
523
- ));
546
+ const texture = material[p]?.clone() || new Texture();
547
+ texture.flipY = material.map?.flipY || false;
548
+ const src = imageLoader.manager.resolveURL(params[p]);
549
+ const textureSource = findImageBySrc(this.sceneContainer, src);
550
+ if (textureSource) {
551
+ texture.source = textureSource;
552
+ } else {
553
+ texture.image = await imageRepository.loadImage(params[p]);
554
+ }
555
+ texture.userData.src = src;
524
556
  texture.needsUpdate = true;
525
557
  material[p] = texture;
526
558
  break;
@@ -560,7 +592,10 @@ export class Three3DScene extends Three3D {
560
592
  }
561
593
  })
562
594
  );
563
- material.transparent = !!material.alphaMap;
595
+ if (material.alphaMap) {
596
+ material.transparent = true;
597
+ material.side = DoubleSide;
598
+ }
564
599
  material.needsUpdate = true;
565
600
  })
566
601
  ));
@@ -614,7 +649,7 @@ export class Three3DScene extends Three3D {
614
649
  if (!mat.original) {
615
650
  mat.original = mat.clone();
616
651
  Object.values(mat)
617
- .filter(value => value && value.isTexture)
652
+ .filter(value => value && value.clone)
618
653
  .forEach(texture => {
619
654
  if (!texture.original) {
620
655
  texture.original = texture.clone();
@@ -624,7 +659,7 @@ export class Three3DScene extends Three3D {
624
659
  mat.copy(mat.original);
625
660
  Object.keys(mat).forEach(key => {
626
661
  const value = mat[key];
627
- if (value && value.original) {
662
+ if (typeof value === "object" && value?.copy && value.original) {
628
663
  value.copy(value.original);
629
664
  }
630
665
  });
@@ -723,6 +758,18 @@ export class Three3DScene extends Three3D {
723
758
  };
724
759
  }
725
760
 
761
+ onLoadError = url => {
762
+ const errorMessage = `Could not load file: ${url}`;
763
+ // eslint-disable-next-line no-console
764
+ console.error(errorMessage);
765
+ };
766
+
767
+ checkEmptyScene() {
768
+ if (!this.sceneContainer.children.length) {
769
+ this.observable.dispatch(EventTypes.EmptySceneError);
770
+ }
771
+ }
772
+
726
773
  async loadSceneSync(resourceList) {
727
774
  // fix LoadFinished event by trapping the loadScene calls to finish the current one before starting the next one
728
775
  try {
@@ -787,9 +834,19 @@ export class Three3DScene extends Three3D {
787
834
 
788
835
  triggerLoadEvent && this.onLoadStart();
789
836
 
790
- const sceneParts = await Promise.all(toBeAdded.map(item => this.loadSceneContainer(item)));
837
+ const sceneParts = await Promise.allSettled(
838
+ toBeAdded.map(item => this.loadSceneContainer(item))
839
+ );
840
+
841
+ sceneParts.forEach(({ value, reason }, index) => {
842
+ if (reason) {
843
+ // eslint-disable-next-line no-console
844
+ console.error(`Failed to load 3D model: ${toBeAdded[index].model}`);
845
+ return;
846
+ }
847
+
848
+ const { scene, animations } = value;
791
849
 
792
- sceneParts.forEach(({ scene, animations }) => {
793
850
  sceneContainer.add(scene);
794
851
 
795
852
  // collect animations
@@ -802,6 +859,8 @@ export class Three3DScene extends Three3D {
802
859
  }
803
860
  });
804
861
 
862
+ this.checkEmptyScene();
863
+
805
864
  sceneContainer.updateWorldMatrix(true, true);
806
865
 
807
866
  await Promise.all(
@@ -896,10 +955,7 @@ export class Three3DScene extends Three3D {
896
955
  const distance = position.distanceTo(center);
897
956
  const lookat = dir.multiplyScalar(distance).add(position);
898
957
 
899
- const rotation = new Quaternion();
900
- camera.getWorldQuaternion(rotation);
901
- const orientation = Object3D.DefaultUp.clone();
902
-
958
+ const orientation = Object3D.DEFAULT_UP;
903
959
  const fov = camera.fov || 45;
904
960
 
905
961
  const cameraView = {
@@ -31,19 +31,6 @@ describe("Visualization/Three/Three3D", () => {
31
31
  });
32
32
  });
33
33
 
34
- // FIXME this test fails since CI-423
35
- test.skip("the loadModel function calls the loadMaterial and loadObject methods with the correct path", async () => {
36
- three.loadMaterial = jest.fn(() => Promise.resolve(false));
37
- three.loadObject = jest.fn(() => Promise.resolve(false));
38
- await three.loadModel();
39
- expect(three.loadMaterial.mock.calls[0][0]).toEqual(
40
- "/path/to/3D/content/path_for_item_id/materials/path_for_variant_id/material.mtl"
41
- );
42
- expect(three.loadObject.mock.calls[0][0]).toEqual(
43
- "/path/to/3D/content/path_for_item_id/model.obj"
44
- );
45
- });
46
-
47
34
  test("setCustomizableMaterialName and findCustomizableMaterials", () => {
48
35
  const matObj = {
49
36
  myMaterial: {
@@ -61,24 +48,4 @@ describe("Visualization/Three/Three3D", () => {
61
48
  ]);
62
49
  expect(three.prepareCustomCanvasForMaterial).toHaveBeenCalledWith(matObj.myMaterial);
63
50
  });
64
-
65
- test("loads the camera views correctly", async () => {
66
- // read the test obj file (cameraviews.obj) as a data url
67
- const cameraViewsDataUrl =
68
- "data:text/plain;base64," +
69
- btoa(fs.readFileSync(path.resolve(__dirname, "cameraviews.obj"), "utf8"));
70
- const three = new Three3D();
71
-
72
- three.controls = { update: () => true };
73
-
74
- const views = await three.loadCameraViews(cameraViewsDataUrl, false);
75
-
76
- expect(views).toEqual({
77
- default: {
78
- lookat: { x: 0, y: 0, z: 0 },
79
- position: { x: 3, y: 5, z: -12 },
80
- },
81
- sleeve: { lookat: { x: 0, y: 0, z: 0 } },
82
- });
83
- });
84
51
  });
@@ -2,13 +2,12 @@ import {
2
2
  Box3,
3
3
  DoubleSide,
4
4
  Sphere,
5
- PlaneBufferGeometry,
5
+ PlaneGeometry,
6
6
  Mesh,
7
7
  ShadowMaterial,
8
8
  DirectionalLight,
9
9
  WebGLRenderTarget,
10
10
  WebGLRenderer,
11
- sRGBEncoding,
12
11
  PCFSoftShadowMap,
13
12
  MeshStandardMaterial,
14
13
  CanvasTexture,
@@ -52,7 +51,7 @@ export default function createBakedShadow(scene, options) {
52
51
  const sphere = box.getBoundingSphere(new Sphere());
53
52
  const { radius } = sphere;
54
53
 
55
- const shadowGeometry = new PlaneBufferGeometry(1, 1);
54
+ const shadowGeometry = new PlaneGeometry(1, 1);
56
55
  shadowGeometry.rotateX(-Math.PI * 0.5);
57
56
  shadowGeometry.rotateY(Math.PI);
58
57
  const shadowGround = new Mesh(
@@ -97,7 +96,6 @@ export default function createBakedShadow(scene, options) {
97
96
  alpha: true,
98
97
  antialias: true,
99
98
  });
100
- renderer.outputEncoding = sRGBEncoding;
101
99
  renderer.shadowMap.enabled = true;
102
100
  renderer.shadowMap.type = PCFSoftShadowMap;
103
101
  renderer.setRenderTarget(renderTarget);
@@ -14,7 +14,8 @@ export function initTextureCanvas(texture) {
14
14
  if (!texture.originalImage || texture.image.nodeName !== "CANVAS") {
15
15
  const originalImage = optimizeTextureImage(texture.image);
16
16
  // eslint-disable-next-line no-console
17
- console.log(`Creating canvas from texture: ${originalImage.src}`);
17
+ texture.userData.src = texture.image?.src || texture.userData?.src;
18
+ console.log(`Creating canvas from texture: ${texture.userData.src}`);
18
19
  texture.image = createCanvasFromImage(originalImage);
19
20
  texture.originalImage = originalImage;
20
21
  } else {
@@ -9,7 +9,7 @@ import { createWorkingCanvas } from "./createCanvas";
9
9
  * @param {number} minTextureSize
10
10
  * @return {HTMLImageElement|HTMLCanvasElement}
11
11
  */
12
- export const optimizeTextureImage = (image, maxTextureSize = 2048, minTextureSize = 2048) => {
12
+ export const optimizeTextureImage = (image, maxTextureSize = 4096, minTextureSize = 2048) => {
13
13
  let textureSize = Math.max(image.width, image.height);
14
14
  if (textureSize <= maxTextureSize && textureSize >= minTextureSize) {
15
15
  return image;
@@ -1,6 +1,8 @@
1
1
  export const rememberCameraViewOffset = camera => {
2
- const {
3
- view: { fullWidth, fullHeight, offsetX, offsetY, width, height },
4
- } = camera;
2
+ const { view } = camera;
3
+ if (!view) {
4
+ return () => null;
5
+ }
6
+ const { fullWidth, fullHeight, offsetX, offsetY, width, height } = view;
5
7
  return () => camera.setViewOffset(fullWidth, fullHeight, offsetX, offsetY, width, height);
6
8
  };
@@ -1,5 +1,5 @@
1
1
  import pick from "lodash/pick";
2
- import OrbitControls from "../Three/Controls/OrbitControls";
2
+ import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
3
3
 
4
4
  const stateMemo = (obj, props) => {
5
5
  const state = pick(obj, props);