redhotmagma-visualization 1.22.4 → 1.22.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redhotmagma-visualization",
3
- "version": "1.22.4",
3
+ "version": "1.22.8",
4
4
  "license": "UNLICENSED",
5
5
  "private": false,
6
6
  "main": "./lib/index.js",
package/src/DevApp.js CHANGED
@@ -187,6 +187,119 @@ const modelData = {
187
187
  },
188
188
  },
189
189
  },
190
+ FlexibleOptionsLocalParams: {
191
+ designAreaList: [],
192
+ configuration: {
193
+ visualizationData: {
194
+ sceneData: [
195
+ { src: "models/3d/GroundVehicle/SceneBase.gltf" },
196
+ {
197
+ src: "models/3d/GroundVehicle/WheelsFront.gltf",
198
+ params: {
199
+ material: { params: { map: "FunnyWheel.jpg" } },
200
+ },
201
+ },
202
+ {
203
+ src: "models/3d/GroundVehicle/WheelsFront.gltf",
204
+ params: {
205
+ material: {
206
+ params: {
207
+ map: "FunnyWheel.jpg",
208
+ color: "#FF00FF",
209
+ },
210
+ },
211
+ position: {
212
+ z: -3,
213
+ },
214
+ },
215
+ },
216
+ {
217
+ src: "models/3d/GroundVehicle/WheelsFront.gltf",
218
+ params: {
219
+ material: {
220
+ params: {
221
+ map: "FunnyWheel.jpg",
222
+ color: "#00FF00",
223
+ },
224
+ },
225
+ position: {
226
+ z: 3,
227
+ },
228
+ },
229
+ },
230
+ ],
231
+ },
232
+ },
233
+ },
234
+ FlexibleOptionsGlobalParams: {
235
+ designAreaList: [],
236
+ configuration: {
237
+ visualizationData: {
238
+ sceneData: [
239
+ { src: "models/3d/GroundVehicle/SceneBase.gltf" },
240
+ { src: "models/3d/GroundVehicle/Body.gltf" },
241
+ {
242
+ model: "models/3d/GroundVehicle/WheelsFront.gltf",
243
+ params: {
244
+ position: {
245
+ z: 0,
246
+ },
247
+ },
248
+ },
249
+ {
250
+ model: "models/3d/GroundVehicle/WheelsFront.gltf",
251
+ params: {
252
+ position: {
253
+ z: -3.75,
254
+ },
255
+ },
256
+ },
257
+ {
258
+ model: "models/3d/GroundVehicle/WheelsFront.gltf",
259
+ params: {
260
+ position: {
261
+ z: -5.05,
262
+ },
263
+ },
264
+ },
265
+ {
266
+ params: {
267
+ objectName: "Wheels",
268
+ material: {
269
+ params: {
270
+ map: "FunnyWheel.jpg",
271
+ color: "#FF00FF",
272
+ },
273
+ },
274
+ },
275
+ files: {
276
+ "FunnyWheel.jpg": "models/3d/GroundVehicle/FunnyWheel.jpg",
277
+ },
278
+ },
279
+ {
280
+ params: {
281
+ objectName: "GroundVehicleMesh_0",
282
+ material: {
283
+ params: {
284
+ color: "#00FFFF",
285
+ },
286
+ },
287
+ },
288
+ },
289
+ {
290
+ params: {
291
+ material: {
292
+ params: {
293
+ materialName: "CustomizableMaterial",
294
+ color: "#FFFF00",
295
+ },
296
+ },
297
+ },
298
+ },
299
+ ],
300
+ },
301
+ },
302
+ },
190
303
  },
191
304
  };
192
305
 
@@ -8,6 +8,7 @@ import {
8
8
  Group,
9
9
  HemisphereLight,
10
10
  ImageLoader,
11
+ LoadingManager,
11
12
  Mesh,
12
13
  MeshBasicMaterial,
13
14
  MeshPhongMaterial,
@@ -30,29 +31,31 @@ import { isMobileDevice } from "../Utils/isMobileDevice";
30
31
  import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
31
32
  import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
32
33
  import get from "lodash/get";
33
- import find from "lodash/find";
34
34
  import size from "lodash/size";
35
35
  import isPlainObject from "lodash/isPlainObject";
36
36
  import { exportGLTF } from "./Exporters/glTFExporter";
37
+ import findIndex from "lodash/findIndex";
37
38
 
38
39
  const UNSET = Symbol("unset");
39
40
 
41
+ const allowedObjectProps = ["position", "rotation", "scale"];
42
+
40
43
  function getSrc(item) {
41
- return (item && item.src) || item;
44
+ return item && (item.model || item.src);
42
45
  }
43
46
 
44
47
  function getPatch(oldObj, newObj) {
45
48
  const patch = {};
46
49
  newObj &&
47
50
  Object.keys(newObj).forEach(prop => {
48
- if (isPlainObject(newObj[prop])) {
49
- const objPatch = getPatch(oldObj && oldObj[prop], newObj[prop]);
50
- if (Object.keys(objPatch).length) {
51
- patch[prop] = objPatch;
52
- }
53
- } else {
54
- patch[prop] = newObj[prop];
51
+ if (isPlainObject(newObj[prop])) {
52
+ const objPatch = getPatch(oldObj && oldObj[prop], newObj[prop]);
53
+ if (Object.keys(objPatch).length) {
54
+ patch[prop] = objPatch;
55
55
  }
56
+ } else {
57
+ patch[prop] = newObj[prop];
58
+ }
56
59
  });
57
60
  oldObj &&
58
61
  Object.keys(oldObj).forEach(prop => {
@@ -88,7 +91,6 @@ function disposeNonUsedObjects(root, disposeTarget) {
88
91
  for (const object of disposableObjects) {
89
92
  if (referencedObjects.indexOf(object) === -1) {
90
93
  object.dispose();
91
- console.log("disposed object:", object.constructor.name, object.uuid);
92
94
  }
93
95
  }
94
96
  }
@@ -103,6 +105,16 @@ function getMaterials(tree) {
103
105
  return materials;
104
106
  }
105
107
 
108
+ function getAllMaterials(tree) {
109
+ const materials = [];
110
+ tree.traverse(({ material }) => {
111
+ if (material && materials.indexOf(material) === -1) {
112
+ materials.push(material);
113
+ }
114
+ });
115
+ return materials;
116
+ }
117
+
106
118
  function findImageBySrc(tree, src) {
107
119
  let result = null;
108
120
  tree.traverse(({ material }) => {
@@ -133,35 +145,80 @@ function setLightShadow(light, d = 50) {
133
145
  }
134
146
  }
135
147
 
148
+ function createIndexCounter() {
149
+ let counters = {};
150
+ const add = index => {
151
+ if (undefined === counters[index]) {
152
+ counters[index] = 0;
153
+ } else {
154
+ counters[index]++;
155
+ }
156
+ return counters[index];
157
+ };
158
+ const reset = () => {
159
+ counters = {};
160
+ };
161
+ return {
162
+ add,
163
+ reset,
164
+ };
165
+ }
166
+
136
167
  function diffResourceLists(oldList, newList) {
137
168
  const toBeRemoved = [];
138
169
  const toBeAdded = [];
139
170
  const toBeUpdated = [];
171
+ const toBeApplied = [];
172
+ const indexCounter = createIndexCounter();
173
+
140
174
  for (const item of oldList) {
141
175
  const src = getSrc(item);
142
- const newItem = newList.find(newItem => getSrc(newItem) === src);
176
+ if (!src) {
177
+ continue;
178
+ }
179
+ const index = indexCounter.add(src);
180
+
181
+ const newItem = newList.filter(newItem => getSrc(newItem) === src)[index];
143
182
  if (!newItem) {
144
- toBeRemoved.push(item);
183
+ toBeRemoved.push({
184
+ ...item,
185
+ index,
186
+ });
145
187
  } else {
146
188
  const patch = getPatch(item, newItem);
147
189
  if (Object.keys(patch).length) {
148
190
  toBeUpdated.push({
149
- src,
150
191
  ...patch,
192
+ model: src,
193
+ index,
151
194
  });
152
195
  }
153
196
  }
154
197
  }
198
+
199
+ indexCounter.reset();
200
+
155
201
  for (const item of newList) {
156
202
  const src = getSrc(item);
157
- if (!oldList.find(oldItem => getSrc(oldItem) === src)) {
158
- toBeAdded.push(item);
203
+ if (!src) {
204
+ item.params && toBeApplied.push(item);
205
+ continue;
206
+ }
207
+
208
+ const index = indexCounter.add(src);
209
+
210
+ if (!oldList.filter(oldItem => getSrc(oldItem) === src)[index]) {
211
+ toBeAdded.push({
212
+ ...item,
213
+ index,
214
+ });
159
215
  }
160
216
  }
161
217
  return {
162
218
  toBeRemoved,
163
219
  toBeAdded,
164
220
  toBeUpdated,
221
+ toBeApplied,
165
222
  };
166
223
  }
167
224
 
@@ -175,23 +232,30 @@ function findObjectWith(root, testFn) {
175
232
  return result;
176
233
  }
177
234
 
235
+ function findAllObjectsWith(root, testFn) {
236
+ const result = [];
237
+ root.traverse(object => testFn(object) && result.push(object));
238
+ return result;
239
+ }
240
+
241
+ function mapObjectValues(object, valueMap) {
242
+ return Object.keys(object).reduce(
243
+ (res, key) => ({ ...res, [key]: (valueMap && valueMap[object[key]]) || object[key] }),
244
+ {}
245
+ );
246
+ }
247
+
178
248
  function setCameraAspectFromRendererSize(camera, renderer) {
179
249
  const { width, height } = renderer.domElement;
180
250
  camera.aspect = width / height;
181
251
  camera.updateProjectionMatrix();
182
252
  }
183
253
 
184
- // todo: fix draco loader
185
-
186
254
  export class Three3DScene extends Three3D {
187
255
  DefaultCustomizableMaterialKey = "CustomizableMaterial";
188
256
 
189
257
  clock = new Clock();
190
258
 
191
- loader = this.createLoader();
192
-
193
- imageLoader = this.createImageLoader();
194
-
195
259
  animations = [];
196
260
 
197
261
  currentResourceList = [];
@@ -214,24 +278,37 @@ export class Three3DScene extends Three3D {
214
278
  cfg.modelBasePath ? (this._modelBasePath = cfg.modelBasePath) : (this._modelBasePath = "");
215
279
  }
216
280
 
217
- createLoader() {
218
- const dracoLoader = new DRACOLoader();
219
- dracoLoader.setDecoderPath("libs/draco/gltf/");
220
- const loader = new GLTFLoader(this._loadingManager);
221
- this._loadingManager.setURLModifier(url => {
281
+ createLoadingManager(options = {}) {
282
+ const { basePath = "", resourcePath = "/" } = options;
283
+
284
+ const { onLoad, onProgress, onError } = this._loadingManager;
285
+ const loadingManager = new LoadingManager(onLoad, onProgress, onError);
286
+ loadingManager.setURLModifier(url => {
222
287
  if (/^(data|blob):/.test(url)) {
223
288
  return url;
224
289
  }
290
+ const fixedBasePath = basePath.replace(/\/$/, "");
225
291
  return url.indexOf("/") === 0
226
- ? `${this._modelBasePath}${url}`
227
- : `${this._modelBasePath}${this._resourcePath}${url}`;
292
+ ? `${fixedBasePath}${url}`
293
+ : `${fixedBasePath}${resourcePath}${url.replace("./", "")}`;
228
294
  });
295
+ return loadingManager;
296
+ }
297
+
298
+ createLoader(options = {}) {
299
+ const loadingManager = this.createLoadingManager(options);
300
+
301
+ const dracoLoader = new DRACOLoader();
302
+ dracoLoader.setDecoderPath("libs/draco/gltf/");
303
+
304
+ const loader = new GLTFLoader(loadingManager);
229
305
  loader.setDRACOLoader(dracoLoader);
230
306
  return loader;
231
307
  }
232
308
 
233
- createImageLoader() {
234
- const imageLoader = new ImageLoader(this._loadingManager);
309
+ createImageLoader(options = {}) {
310
+ const loadingManager = this.createLoadingManager(options);
311
+ const imageLoader = new ImageLoader(loadingManager);
235
312
  return imageLoader;
236
313
  }
237
314
 
@@ -260,6 +337,7 @@ export class Three3DScene extends Three3D {
260
337
  }
261
338
 
262
339
  setupDefaultLightning() {}
340
+
263
341
  loadModel() {}
264
342
 
265
343
  createScene() {
@@ -323,10 +401,31 @@ export class Three3DScene extends Three3D {
323
401
  });
324
402
  }
325
403
 
326
- async applyCustomizations(model, item) {
327
- const { params, material } = item;
404
+ async applyCustomizations(model, item, imageLoader, options = {}) {
405
+ const { saveAndRestore = true } = options;
406
+
407
+ const { params } = item;
328
408
  if (params) {
329
409
  Object.keys(params).forEach(p => {
410
+ // skip not allowed props
411
+ if (allowedObjectProps.indexOf(p) === -1) {
412
+ return;
413
+ }
414
+ // restore original value first
415
+ if (params[p] === UNSET) {
416
+ if (model.originalValues && model.originalValues[p]) {
417
+ model[p].copy(model.originalValues[p]);
418
+ }
419
+ return;
420
+ }
421
+ // store original value for later restoration
422
+ if (!model.originalValues) {
423
+ model.originalValues = {};
424
+ }
425
+ if (!model.originalValues[p]) {
426
+ model.originalValues[p] = model[p] ? model[p].clone() : model[p];
427
+ }
428
+ // apply the param values
330
429
  switch (p) {
331
430
  case "scale": {
332
431
  const { x = 1, y = 1, z = 1 } = params[p];
@@ -343,39 +442,46 @@ export class Three3DScene extends Three3D {
343
442
  });
344
443
  }
345
444
 
346
- // store & restore originals
347
- Object.values(getMaterials(model)).map(mat => {
348
- if (!mat.original) {
349
- mat.original = mat.clone();
350
- Object.values(mat)
351
- .filter(value => value && value.isTexture)
352
- .forEach(texture => {
353
- if (!texture.original) {
354
- texture.original = texture.clone();
445
+ // store & restore original material and textures before applying new param values
446
+ saveAndRestore &&
447
+ Object.values(getMaterials(model)).map(mat => {
448
+ if (!mat.original) {
449
+ mat.original = mat.clone();
450
+ Object.values(mat)
451
+ .filter(value => value && value.isTexture)
452
+ .forEach(texture => {
453
+ if (!texture.original) {
454
+ texture.original = texture.clone();
455
+ }
456
+ });
457
+ } else {
458
+ mat.copy(mat.original);
459
+ Object.keys(mat).forEach(key => {
460
+ const value = mat[key];
461
+ if (value && value.original) {
462
+ value.copy(value.original);
355
463
  }
356
464
  });
357
- } else {
358
- // todo: dispose materials that are no longer present in the params
359
- mat.copy(mat.original);
360
- Object.keys(mat).forEach(key => {
361
- const value = mat[key];
362
- if (value && value.original) {
363
- value.copy(value.original);
364
- }
365
- });
366
- mat.needsUpdate = true;
367
- }
368
- });
465
+ mat.needsUpdate = true;
466
+ }
467
+ });
369
468
 
370
- // set new params if provided
469
+ // set new material params if provided
470
+ // the material object should be located under item.params.material
471
+ // item.material is only for backward compatibility
472
+ const material = (params && params.material) || item.material;
371
473
  if (material) {
372
474
  const { params } = material;
373
475
  params &&
374
476
  (await Promise.all(
375
- Object.values(getMaterials(model)).map(async material => {
376
- const { materialNames } = params;
477
+ getAllMaterials(model).map(async material => {
478
+ const { materialName, materialNames } = params;
377
479
  // check materialNames list if provided
378
- if ((materialNames instanceof Array) && materialNames.indexOf(material.name) === -1) {
480
+ if (materialNames instanceof Array && materialNames.indexOf(material.name) === -1) {
481
+ return;
482
+ }
483
+ // check materialName match if provided
484
+ if (materialName && material.name !== materialName) {
379
485
  return;
380
486
  }
381
487
  await Promise.all(
@@ -397,7 +503,7 @@ export class Three3DScene extends Three3D {
397
503
  texture.image =
398
504
  findImageBySrc(this.sceneContainer, `${this._modelBasePath}${params[p]}`) ||
399
505
  (await new Promise((resolve, reject) =>
400
- this.imageLoader.load(params[p], resolve, undefined, reject)
506
+ imageLoader.load(params[p], resolve, undefined, reject)
401
507
  ));
402
508
  texture.needsUpdate = true;
403
509
  material[p] = texture;
@@ -438,20 +544,30 @@ export class Three3DScene extends Three3D {
438
544
  if ("string" === itemType) {
439
545
  path = item;
440
546
  } else if ("object" === itemType) {
441
- path = item.src;
547
+ path = getSrc(item);
442
548
  }
443
- const matches = path.match(/(.+\/)([^\/]+)/);
444
- this._resourcePath = matches[1];
549
+ const { resourcePath, modelFileName } = this.parseModelPath(path);
550
+
551
+ const loaderOptions = {
552
+ basePath: this._modelBasePath,
553
+ resourcePath,
554
+ };
555
+
556
+ const modelLoader = this.createLoader(loaderOptions);
557
+ const imageLoader = this.createImageLoader(loaderOptions);
445
558
 
446
559
  return new Promise((resolve, reject) => {
447
- this.loader.load(
448
- matches[2],
560
+ modelLoader.load(
561
+ modelFileName,
449
562
  async gltf => {
450
563
  const model = gltf.scene;
451
564
  model.userData.path = path;
565
+ if (undefined !== item.index) {
566
+ model.userData.index = item.index;
567
+ }
452
568
  this.fixImportedScene(model);
453
569
  if ("object" === itemType) {
454
- await this.applyCustomizations(model, item);
570
+ await this.applyCustomizations(model, item, imageLoader);
455
571
  }
456
572
  resolve(gltf);
457
573
  },
@@ -461,7 +577,25 @@ export class Three3DScene extends Three3D {
461
577
  });
462
578
  }
463
579
 
580
+ findCustomizableMaterials(materialsObj) {
581
+ for (let key of this._customizableMaterialKeys) {
582
+ const mat = materialsObj[key];
583
+ if (mat) {
584
+ this.prepareCustomCanvasForMaterial(mat);
585
+ const index = findIndex(this._customizableMaterials, { name: key });
586
+ if (index === -1) {
587
+ this._customizableMaterials.push(mat);
588
+ } else if (this._customizableMaterials[index] !== mat) {
589
+ this._customizableMaterials[index] = mat;
590
+ }
591
+ }
592
+ }
593
+ }
594
+
464
595
  async initCustomizableMaterialsIfNeeded() {
596
+ // reset customizable materials to always find the right one in the loaded scene
597
+ this._customizableMaterialKeys.length = 0;
598
+ this._customizableMaterials.length = 0;
465
599
  const materials = getMaterials(this.sceneContainer);
466
600
  if (size(materials)) {
467
601
  this.guessCustomizableMaterialNameIfNotSet(materials);
@@ -470,12 +604,25 @@ export class Three3DScene extends Three3D {
470
604
  await this.dispatchMaterialUpdateEvent();
471
605
  }
472
606
 
607
+ parseModelPath(path) {
608
+ const matches = path.match(/([^/].+\/)([^/]+)/);
609
+ if (!matches.length) {
610
+ throw new Error("Model path cannot be parsed: " + path);
611
+ }
612
+ const resourcePath = "/" + matches[1];
613
+ const modelFileName = matches[2];
614
+ return {
615
+ resourcePath,
616
+ modelFileName,
617
+ };
618
+ }
619
+
473
620
  async loadScene(resourceList) {
474
621
  const { sceneContainer } = this;
475
622
 
476
623
  this.stopAnimation();
477
624
 
478
- const { toBeRemoved, toBeAdded, toBeUpdated } = diffResourceLists(
625
+ const { toBeRemoved, toBeAdded, toBeUpdated, toBeApplied } = diffResourceLists(
479
626
  this.currentResourceList || [],
480
627
  resourceList || []
481
628
  );
@@ -488,7 +635,10 @@ export class Three3DScene extends Three3D {
488
635
  const toBeDisposed = [];
489
636
  toBeRemoved.forEach(item => {
490
637
  const itemSrc = getSrc(item);
491
- const object = findObjectWith(sceneContainer, ({ userData: { path } }) => path === itemSrc);
638
+ const object = findObjectWith(
639
+ sceneContainer,
640
+ ({ userData }) => userData.path === itemSrc && userData.index === item.index
641
+ );
492
642
  if (object) {
493
643
  object.parent.remove(object);
494
644
  toBeDisposed.push(object);
@@ -496,7 +646,7 @@ export class Three3DScene extends Three3D {
496
646
  });
497
647
  toBeDisposed.forEach(object => disposeNonUsedObjects(sceneContainer, object));
498
648
 
499
- const triggerLoadEvent = !!(toBeAdded.length || toBeUpdated.length);
649
+ const triggerLoadEvent = !!(toBeAdded.length || toBeUpdated.length || toBeApplied.length);
500
650
 
501
651
  triggerLoadEvent && this.onLoadStart();
502
652
 
@@ -504,6 +654,8 @@ export class Three3DScene extends Three3D {
504
654
 
505
655
  sceneParts.forEach(({ scene, animations }) => {
506
656
  sceneContainer.add(scene);
657
+
658
+ // collect animations
507
659
  if (animations.length) {
508
660
  this.animations.push(
509
661
  ...animations.filter(
@@ -518,8 +670,49 @@ export class Three3DScene extends Three3D {
518
670
  await Promise.all(
519
671
  toBeUpdated.map(item => {
520
672
  const itemSrc = getSrc(item);
521
- const object = findObjectWith(sceneContainer, ({ userData: { path } }) => path === itemSrc);
522
- return object && this.applyCustomizations(object, item);
673
+ const object = findObjectWith(
674
+ sceneContainer,
675
+ ({ userData }) => userData.path === itemSrc && userData.index === item.index
676
+ );
677
+
678
+ const path = getSrc(item);
679
+ const { resourcePath } = this.parseModelPath(path);
680
+ const loaderOptions = {
681
+ basePath: this._modelBasePath,
682
+ resourcePath,
683
+ };
684
+ const imageLoader = this.createImageLoader(loaderOptions);
685
+
686
+ return object && this.applyCustomizations(object, item, imageLoader);
687
+ })
688
+ );
689
+
690
+ // apply global or targeted param values
691
+ const imageLoader = this.createImageLoader({
692
+ basePath: this._modelBasePath,
693
+ });
694
+ await Promise.all(
695
+ toBeApplied.map(({ params: optionParams, files }) => {
696
+ // find model based on objectName
697
+ const { objectName, material = {} } = optionParams;
698
+ const objects = objectName
699
+ ? findAllObjectsWith(sceneContainer, ({ name }) => name === objectName)
700
+ : sceneContainer.children;
701
+ const { params } = material;
702
+ const item = {
703
+ params: {
704
+ ...optionParams,
705
+ material: {
706
+ ...material,
707
+ params: params && mapObjectValues(params, files),
708
+ },
709
+ },
710
+ };
711
+ return Promise.all(
712
+ objects.map(object =>
713
+ this.applyCustomizations(object, item, imageLoader, { saveAndRestore: false })
714
+ )
715
+ );
523
716
  })
524
717
  );
525
718
 
@@ -562,6 +755,8 @@ export class Three3DScene extends Three3D {
562
755
  const center = new Vector3();
563
756
  box.getCenter(center);
564
757
 
758
+ this.cameraViews.default = null;
759
+
565
760
  this.cameraViews = cameras.reduce((views, camera) => {
566
761
  const position = new Vector3();
567
762
  camera.getWorldPosition(position);