@combeenation/3d-viewer 9.0.0 → 9.0.2-alpha1

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": "@combeenation/3d-viewer",
3
- "version": "9.0.0",
3
+ "version": "9.0.2-alpha1",
4
4
  "description": "Combeenation 3D Viewer",
5
5
  "homepage": "https://github.com/Combeenation/3d-viewer#readme",
6
6
  "bugs": {
@@ -160,11 +160,18 @@ export class Parameter {
160
160
  public static readonly METALLIC = 'metallic';
161
161
 
162
162
  /**
163
- * Mutates the texture of a material, based on an svg string, image path or image data.
163
+ * Paintables can be used to let the client/browser draw graphics on a 3d model by mutating the texture of a
164
+ * material.\
165
+ * For detailed information have a look at the [paintables](./../pages/documentation/Paintables.html) page.
166
+ *
167
+ * Also see {@link parsePaintable} for detailed information on which values you can pass to the `paintable`
168
+ * parameter.
164
169
  *
165
170
  * Scopes:
166
171
  * * {@link Material} via {@link TagManager}
167
172
  *
173
+ * @parser {@link parsePaintable}
174
+ *
168
175
  */
169
176
  public static readonly PAINTABLE = 'paintable';
170
177
 
@@ -276,7 +283,7 @@ export class Parameter {
276
283
  *
277
284
  * Usually the URL to an `*.env` file.
278
285
  *
279
- * See [environment page](./../pages/Documentation/Environment.html) for details on how to use this.
286
+ * See [environment page](./../pages/documentation/Environment.html) for details on how to use this.
280
287
  *
281
288
  * Scopes:
282
289
  * * {@link SceneManager}
@@ -288,7 +295,7 @@ export class Parameter {
288
295
  /**
289
296
  * Sets [BABYLON.Scene.clearColor](https://doc.babylonjs.com/typedoc/classes/BABYLON.Scene#clearColor)
290
297
  *
291
- * See [environment page](./../pages/Documentation/Environment.html) for details on how to use this.
298
+ * See [environment page](./../pages/documentation/Environment.html) for details on how to use this.
292
299
  *
293
300
  * Scopes:
294
301
  * * {@link SceneManager}
@@ -301,7 +308,7 @@ export class Parameter {
301
308
  * Rotates connected textures, i.e. IBL, reflection, background, ...
302
309
  * Rotation angle in degree.
303
310
  *
304
- * See [environment page](./../pages/Documentation/Environment.html) for details on how to use this.
311
+ * See [environment page](./../pages/documentation/Environment.html) for details on how to use this.
305
312
  *
306
313
  * Scopes:
307
314
  * * {@link SceneManager}
@@ -313,7 +320,7 @@ export class Parameter {
313
320
  /**
314
321
  * Intensity of IBL. Defaults to 1, usually between 0 & 2.
315
322
  *
316
- * See [environment page](./../pages/Documentation/Environment.html) for details on how to use this.
323
+ * See [environment page](./../pages/documentation/Environment.html) for details on how to use this.
317
324
  *
318
325
  * Scopes:
319
326
  * * {@link SceneManager}
@@ -329,7 +336,7 @@ export class Parameter {
329
336
  * Recommended format is `*.jpg`. HDR files are not supported as background but can be converted to `*.jpg` using
330
337
  * tools like Photoshop, Gimp or various free online converters.
331
338
  *
332
- * See [environment page](./../pages/Documentation/Environment.html) for details on how to use this.
339
+ * See [environment page](./../pages/documentation/Environment.html) for details on how to use this.
333
340
  *
334
341
  * Scopes:
335
342
  * * {@link SceneManager}
@@ -347,7 +354,7 @@ export class Parameter {
347
354
  *
348
355
  * The value of this parameter is ignored if {@link ENVIRONMENT_BACKGROUND} is also set.
349
356
  *
350
- * See [environment page](./../pages/Documentation/Environment.html) for details on how to use this.
357
+ * See [environment page](./../pages/documentation/Environment.html) for details on how to use this.
351
358
  *
352
359
  * Scopes:
353
360
  * * {@link SceneManager}
@@ -372,6 +379,10 @@ export class Parameter {
372
379
  [Parameter.MATERIAL_ROUGHNESS]: { type: 'number', parser: Parameter.parseNumber },
373
380
  [Parameter.HIGHLIGHT_ENABLED]: { type: 'boolean', parser: Parameter.parseBoolean },
374
381
  [Parameter.HIGHLIGHT_COLOR]: { type: 'color', parser: Parameter.parseColor },
382
+ [Parameter.COLOR]: { type: 'color', parser: Parameter.parseColor },
383
+ [Parameter.METALLIC]: { type: 'number', parser: Parameter.parseNumber },
384
+ [Parameter.ROUGHNESS]: { type: 'number', parser: Parameter.parseNumber },
385
+ [Parameter.PAINTABLE]: { type: 'paintable', parser: Parameter.parsePaintable },
375
386
  [Parameter.HIGHLIGHTED]: { type: 'boolean', parser: Parameter.parseBoolean },
376
387
  [Parameter.CAST_SHADOW]: { type: 'boolean', parser: Parameter.parseBoolean },
377
388
  [Parameter.CAST_SHADOW_FROM_LIGHTS]: { type: 'csl', parser: Parameter.parseCommaSeparatedList },
@@ -402,13 +413,6 @@ export class Parameter {
402
413
  };
403
414
  }
404
415
 
405
- /**
406
- * Gets the {@link ParameterDeclaration} for a given {@link Parameter} declared in this class.
407
- */
408
- public static getDeclaration(parameter: string): ParameterDeclaration {
409
- return Parameter.declarations[parameter];
410
- }
411
-
412
416
  /**
413
417
  * Gets the default {@link ParameterValue} for a given {@link Parameter} declared in this class.
414
418
  */
@@ -440,14 +444,14 @@ export class Parameter {
440
444
  return;
441
445
  }
442
446
  const declaration = parameterDeclaration[parameter];
443
- const genericError = `"${value}" is not a valid value for parameter "${parameter}" of type "${declaration.type}".`;
447
+ const errorPrefix = `Invalid value for parameter "${parameter}" of type "${declaration.type}" given:`;
444
448
  if (declaration.parser) {
445
449
  try {
446
450
  declaration.parser(value);
447
451
  } catch (e: any) {
448
452
  throw new ViewerError({
449
453
  id: ViewerErrorIds.InvalidParameterValue,
450
- message: `${genericError} ${e.message}`,
454
+ message: `${errorPrefix}\n${e.message}.\nGiven value: ${value}`,
451
455
  });
452
456
  }
453
457
  }
@@ -460,7 +464,9 @@ export class Parameter {
460
464
  });
461
465
  }
462
466
  if (declaration.options.indexOf(value) === -1) {
463
- throw Error(genericError + ` Valid values are: "${declaration.options.join('", "')}".`);
467
+ throw Error(
468
+ `${errorPrefix}\nValid values are: "${declaration.options.join('", "')}".\nGiven value: ${value}`
469
+ );
464
470
  }
465
471
  break;
466
472
  }
@@ -589,18 +595,28 @@ export class Parameter {
589
595
  }
590
596
 
591
597
  /**
592
- * Parses string defintion of a paintable into a paintable object.
598
+ * Parses string defintion of a paintable into a paintable object.\
593
599
  * Here are some examples:
594
- * - '{ "src": "pathToCertain.jpg", "uScale": 0.5 }' => default definition as JSON object
595
- * - 'pathToCertain.jpg' => short hand definition, only contains source string
596
- * - '{ "src": "pathToCertain.jpg", "uScale": -1, "vScale": -1, "uOffset": 0, "vOffset": 0 }' => full content,
597
- * paintable texture is flipped in both directions
598
- * - '<svg>...</svg>' => SVG content can be used directly
599
- * - { "src": "<svg>...</svg>", "uScale": 0.5 } => SVG in src property works as well
600
- */
601
- public static parsePaintableValue(value: ParameterValue): PaintableValue {
600
+ * ```ts
601
+ * // Default definition as JSON object:
602
+ * '{ "src": "https://path.to/image.jpg", "uScale": 0.5 }'
603
+ *
604
+ * // Short hand definition, only contains source string:
605
+ * 'https://path.to/image.jpg'
606
+ *
607
+ * // Full content, paintable texture is flipped in both directions
608
+ * '{ "src": "https://path.to/image.jpg", "uScale": -1, "vScale": -1, "uOffset": 0, "vOffset": 0 }'
609
+ *
610
+ * // SVG content can be used directly:
611
+ * '<svg>...</svg>'
612
+ *
613
+ * // SVG in src property works as well:
614
+ * '{ "src": "<svg>...</svg>", "uScale": 0.5 }'
615
+ * ```
616
+ */
617
+ public static parsePaintable(value: ParameterValue): PaintableValue {
602
618
  if (!isString(value)) {
603
- throw new Error(`Unable to parse "${value}" to a PaintableValue: not a string.`);
619
+ throw new Error(`Not a string.`);
604
620
  }
605
621
 
606
622
  const paintableValue: PaintableValue = { src: '' };
@@ -614,18 +630,18 @@ export class Parameter {
614
630
 
615
631
  if (value.startsWith('{')) {
616
632
  // seems like the user tried to use a JSON string, as the input starts with {
617
- throw new Error(`Unable to parse "${value}" to a PaintableValue: not a valid JSON string.`);
633
+ throw new Error(`Not a valid JSON string.`);
618
634
  }
619
635
  }
620
636
 
621
637
  if (valObj) {
622
638
  // input string is JSON, src attribute is required
623
639
  if (!valObj.src) {
624
- throw new Error(`Unable to parse "${value}" to a PaintableValue: property "src" is missing.`);
640
+ throw new Error(`Property "src" is missing.`);
625
641
  }
626
642
 
627
643
  if (!isString(valObj.src)) {
628
- throw new Error(`Unable to parse "${value}" to a PaintableValue: property "src" is not a string.`);
644
+ throw new Error(`Property "src" is not a string.`);
629
645
  }
630
646
 
631
647
  // split src and options
@@ -649,7 +665,7 @@ export class Parameter {
649
665
  );
650
666
 
651
667
  if (invalidKeys.length) {
652
- console.warn('parsePaintableValue: invalid paintable options provided: ' + invalidKeys.toString());
668
+ console.warn('parsePaintable: invalid paintable options provided: ' + invalidKeys.toString());
653
669
  }
654
670
 
655
671
  paintableValue.options = validOptions;
@@ -2,6 +2,7 @@ import { Event, emitter } from '../classes/event';
2
2
  import { FuzzyMap } from '../classes/fuzzyMap';
3
3
  import { Parameter } from '../classes/parameter';
4
4
  import { Viewer } from '../classes/viewer';
5
+ import { ViewerError, ViewerErrorIds } from '../classes/viewerError';
5
6
  import {
6
7
  assertMeshCapability,
7
8
  drawPaintableOnMaterial,
@@ -427,7 +428,7 @@ export class TagManager {
427
428
  return true;
428
429
  });
429
430
  this.parameterObservers.set(Parameter.PAINTABLE, async payload => {
430
- const paintableValue: PaintableValue = Parameter.parsePaintableValue(payload.newValue);
431
+ const paintableValue: PaintableValue = Parameter.parsePaintable(payload.newValue);
431
432
 
432
433
  // check if value is svg or image source, do the conversion accordingly
433
434
  const srcIsSvg = isSvg(paintableValue.src);
@@ -437,9 +438,20 @@ export class TagManager {
437
438
  throw new Error(`Setting paintable value failed: "${paintableValue.src}" is no valid SVG string.`);
438
439
  }
439
440
 
440
- const imageSource: CanvasImageSource = srcIsSvg
441
- ? await createImageFromSvg(paintableValue.src)
442
- : await createImageFromImgSrc(paintableValue.src);
441
+ let imageSource: CanvasImageSource;
442
+ try {
443
+ imageSource = srcIsSvg
444
+ ? await createImageFromSvg(paintableValue.src)
445
+ : await createImageFromImgSrc(paintableValue.src);
446
+ } catch {
447
+ // SVG might be invalid, even if it passes `isSvg` check
448
+ // in this case the image can't be created and will throw an error, which should be handled by the viewer and
449
+ // Combeenation viewer control
450
+ throw new ViewerError({
451
+ id: ViewerErrorIds.InvalidParameterValue,
452
+ message: `Invalid value for parameter "paintable" given:\nImage can't be created from source string.\nGiven value: ${paintableValue.src}`,
453
+ });
454
+ }
443
455
 
444
456
  // apply image source on desired material(s)
445
457
  for (const material of payload.materials) {
@@ -37,6 +37,10 @@ const backgroundDomeName = 'BackgroundDome_ViewerGenerated';
37
37
  const envHelperMetadataName = 'viewerEnvHelper';
38
38
  const materialWaitingToBeSetMetadataName = 'materialWaitingToBeSet';
39
39
 
40
+ // Holds pending `createMaterialFromCbnAssets` promises for dedicated material ids
41
+ // In this case we can make sure that a single material is not requested multiple times if used in various nodes
42
+ const getMaterialPromises: Map<string, Promise<Material | null>> = new Map();
43
+
40
44
  /**
41
45
  * @param node
42
46
  * @return Node
@@ -435,15 +439,54 @@ const setMaterial = function (node: TransformNode, materialId: string, deep: boo
435
439
  }
436
440
  };
437
441
 
442
+ /**
443
+ * Removes material from promise map once it has been added to the scene, because we can be sure that the material will
444
+ * be taken from the scene directly, the next time it is requested
445
+ */
446
+ const onNewMaterialAddedCb = (material: Material) => {
447
+ if (getMaterialPromises.has(material.id)) {
448
+ getMaterialPromises.delete(material.id);
449
+ }
450
+ };
451
+
452
+ /**
453
+ * Responsible for adding the `onNewMaterialAddedCb` only once
454
+ */
455
+ const registerOnNewMaterialAddedCbOnce = (scene: Scene) => {
456
+ const observerAlreadyAssigned = scene.onNewMaterialAddedObservable.observers.find(
457
+ observer => observer.callback === onNewMaterialAddedCb
458
+ );
459
+
460
+ if (!observerAlreadyAssigned) {
461
+ scene.onNewMaterialAddedObservable.add(onNewMaterialAddedCb);
462
+ }
463
+ };
464
+
438
465
  /**
439
466
  * Gets the Material either from the given {@link Variant}, the given scene or tries to create it from an Combeenation
440
467
  * material asset when inside a Combeenation configurator.
441
468
  */
442
469
  const getOrCreateMaterial = async function (scene: Scene, materialId: string, variant?: Variant): Promise<Material> {
470
+ registerOnNewMaterialAddedCbOnce(scene);
471
+
443
472
  let chosenMaterial: Material | undefined | null;
444
473
  chosenMaterial = variant?.inheritedMaterials.find(mat => mat.id === materialId);
445
474
  chosenMaterial = chosenMaterial || scene.materials.find(mat => mat.id === materialId);
446
- chosenMaterial = chosenMaterial || (await createMaterialFromCbnAssets(materialId, scene));
475
+
476
+ if (!chosenMaterial) {
477
+ // material not available yet, request it from Combeenation assets
478
+ if (getMaterialPromises.has(materialId)) {
479
+ // request is already pending, just wait for the result
480
+ chosenMaterial = await getMaterialPromises.get(materialId);
481
+ } else {
482
+ // request not pending, call the dedicated function
483
+ const getMaterialProm = createMaterialFromCbnAssets(materialId, scene);
484
+ // store the promise in a global map, so that subsequent requests can reference it
485
+ getMaterialPromises.set(materialId, getMaterialProm);
486
+ chosenMaterial = await getMaterialProm;
487
+ }
488
+ }
489
+
447
490
  if (chosenMaterial) {
448
491
  return chosenMaterial as Material;
449
492
  }
@@ -394,7 +394,7 @@ type ParameterDeclarations = {
394
394
  };
395
395
 
396
396
  type ParameterDeclaration = {
397
- type: 'string' | 'boolean' | 'number' | 'color' | 'select' | 'vector' | 'csl';
397
+ type: 'string' | 'boolean' | 'number' | 'color' | 'select' | 'vector' | 'csl' | 'paintable';
398
398
  parser?: any;
399
399
  options?: ParameterValue[];
400
400
  };
@@ -131,10 +131,13 @@ const createImageFromSvg = async function (svgSrc: string): Promise<HTMLImageEle
131
131
  const createImageFromImgSrc = async function (imgSrc: string): Promise<HTMLImageElement> {
132
132
  const img = new Image();
133
133
 
134
- await new Promise(resolve => {
134
+ await new Promise((resolve, reject) => {
135
135
  img.onload = () => {
136
136
  setTimeout(resolve, 0);
137
137
  };
138
+ img.onerror = () => {
139
+ reject();
140
+ };
138
141
  img.crossOrigin = 'anonymous';
139
142
  img.src = imgSrc;
140
143
  });
package/src/dev.ts CHANGED
@@ -25,7 +25,7 @@ window.Cbn = {
25
25
  Assets: {
26
26
  async getMaterial(materialId: string) {
27
27
  //! this creates a new function on the object that uses the imported function of the same name..
28
- const material = getMaterial(materialId);
28
+ const material = await getMaterial(materialId);
29
29
  if (material) return material;
30
30
 
31
31
  // Fallback to random mock material