@combeenation/3d-viewer 15.1.0 → 16.0.0-alpha2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,125 @@
1
+ import { IAssetContainer, Material, Mesh, MeshBuilder, Tuple, Vector3, ViewerError, ViewerErrorIds } from '..';
2
+
3
+ export const DEFAULT_DECAL_CONFIG = {
4
+ normal: [0, 0, 1],
5
+ angle: 0,
6
+ offset: 0.001,
7
+ scaling: [1, 1, 1],
8
+ captureUVS: false,
9
+ cullBackFaces: true,
10
+ localMode: true,
11
+ sideOrientation: Material.ClockWiseSideOrientation,
12
+ };
13
+
14
+ export type DecalConfiguration = {
15
+ // basic settings for decal creation
16
+ name: string;
17
+ sourceMeshName: string;
18
+ position: Tuple<number, 3>;
19
+ size: Tuple<number, 3>;
20
+
21
+ /** Default `[0, 0, 1]` (forward direction) */
22
+ normal?: Tuple<number, 3>;
23
+
24
+ /** Default `0` */
25
+ angle?: number;
26
+
27
+ /**
28
+ * Shifts decals away from source mesh in direction of "normal" vector
29
+ *
30
+ * Default `0.001` (1mm)
31
+ */
32
+ offset?: number;
33
+
34
+ /**
35
+ * Can be used for shifting the decal away from the source mesh as alternative to `offset`.\
36
+ * Use this setting when the offset should be applied in radial direction (e.g. cylinders)
37
+ *
38
+ * Default `[1, 1, 1]` (no offset through scaling)
39
+ */
40
+ scaling?: Tuple<number, 3>;
41
+
42
+ /**
43
+ * `true`: Use UV mapping from source mesh\
44
+ * `false`: Use default box mapping from the decal
45
+ *
46
+ * Default `false`
47
+ */
48
+ captureUVS?: boolean;
49
+
50
+ /**
51
+ * Create decal only on front side if set to `true`
52
+ *
53
+ * Default `true`
54
+ */
55
+ cullBackFaces?: boolean;
56
+
57
+ /** `true`: Sets created decal as child of source mesh
58
+ *
59
+ * Default `true`
60
+ */
61
+ localMode?: boolean;
62
+
63
+ /**
64
+ * The Babylon.js default for mesh side orientation value is `Material.CounterClockWiseSideOrientation`.\
65
+ * However if the mesh (in this case the created decal mesh) is located under a glTF root node, it has to be flipped,
66
+ * using `Material.ClockWiseSideOrientation`.
67
+ *
68
+ * Default `Material.ClockWiseSideOrientation`
69
+ */
70
+ sideOrientation?: number;
71
+ };
72
+
73
+ /**
74
+ * Creates mesh from a decal configuration.\
75
+ * This is basically calling `MeshBuilder.CreateDecal`, with some extra steps:
76
+ * - add created decal mesh to source mesh asset container
77
+ * - overwrite decal mesh side orientation (important for glTF models)
78
+ * - move decal along the "normal" to avoid clipping
79
+ */
80
+ export function createDecalMesh(config: DecalConfiguration, assetContainer: IAssetContainer, addToScene = true): Mesh {
81
+ const sourceMesh = assetContainer.meshes.find(mesh => mesh.name === config.sourceMeshName);
82
+ if (!sourceMesh) {
83
+ throw new ViewerError({
84
+ id: ViewerErrorIds.InvalidDecalConfiguration,
85
+ message: `Source mesh for decal "${config.name}" couldn't be found`,
86
+ });
87
+ }
88
+
89
+ const position = Vector3.FromArray(config.position);
90
+ const normal = Vector3.FromArray(config.normal ?? DEFAULT_DECAL_CONFIG.normal).normalizeToNew();
91
+
92
+ const localMode = config.localMode ?? DEFAULT_DECAL_CONFIG.localMode;
93
+ const worldNormal = localMode ? Vector3.TransformNormal(normal, sourceMesh.getWorldMatrix()) : normal;
94
+
95
+ const decalMesh = MeshBuilder.CreateDecal(config.name, sourceMesh, {
96
+ position: position,
97
+ normal: normal,
98
+ size: Vector3.FromArray(config.size),
99
+ angle: config.angle ?? DEFAULT_DECAL_CONFIG.angle,
100
+ captureUVS: config.captureUVS ?? DEFAULT_DECAL_CONFIG.captureUVS,
101
+ cullBackFaces: config.cullBackFaces ?? DEFAULT_DECAL_CONFIG.cullBackFaces,
102
+ localMode: config.localMode ?? DEFAULT_DECAL_CONFIG.localMode,
103
+ });
104
+
105
+ if (!addToScene) {
106
+ const scene = sourceMesh.getScene();
107
+ scene.removeMesh(decalMesh);
108
+ }
109
+
110
+ // make sure that the decal is part of the source mesh asset container
111
+ decalMesh._parentContainer = assetContainer;
112
+ assetContainer.meshes.push(decalMesh);
113
+
114
+ decalMesh.sideOrientation = config.sideOrientation ?? DEFAULT_DECAL_CONFIG.sideOrientation;
115
+
116
+ // move decal away from mesh to avoid clipping
117
+ // NOTE: zOffset of material can't be used, since it's not supported in glTF and therefore not usable in AR
118
+ const offsetVector = worldNormal.scale(config.offset ?? DEFAULT_DECAL_CONFIG.offset);
119
+ decalMesh.position.addInPlace(offsetVector);
120
+
121
+ // apply scaling, this is an alternative approach for creating an offset, which is preferred for round surfaces
122
+ decalMesh.scaling = Vector3.FromArray(config.scaling ?? DEFAULT_DECAL_CONFIG.scaling);
123
+
124
+ return decalMesh;
125
+ }
package/src/index.ts CHANGED
@@ -69,3 +69,4 @@ export * from './manager/model-manager';
69
69
  export * from './manager/parameter-manager';
70
70
  export * from './manager/scene-manager';
71
71
  export * from './manager/texture-manager';
72
+ export * from './helper/decals-helper';
@@ -1,58 +1,88 @@
1
- import { AssetContainer, ISceneLoaderPlugin, InstancedMesh, SceneLoader } from '../index';
1
+ import {
2
+ AssetContainer,
3
+ ExtendedAssetContainer,
4
+ ISceneLoaderPlugin,
5
+ InstancedMesh,
6
+ ParsedDecalConfiguration,
7
+ SceneLoader,
8
+ ViewerError,
9
+ ViewerErrorIds,
10
+ createDecalMesh,
11
+ } from '../index';
2
12
  import { setInternalMetadataValue } from './metadata-helper';
3
13
  import { deleteAllTags, getTags, setTagsAsString } from './tags-helper';
4
14
  import { isArray, isString } from 'lodash-es';
5
15
 
16
+ type DataWithMeshes = { meshes: unknown[] };
17
+ type DataWithDecalConfigurations = { cbnData: { decals: unknown[] } };
18
+
19
+ type MeshData = {
20
+ name: string;
21
+ materialId?: string;
22
+ instances?: unknown[];
23
+ };
24
+
25
+ type InstanceData = {
26
+ name: string;
27
+ tags?: string;
28
+ };
29
+
30
+ let customLoader: ISceneLoaderPlugin;
31
+
6
32
  /**
7
33
  * Create and return a custom loader plugin to be registered with SceneLoader, that allows
8
34
  * us to run our own code against the input data before using the standard procedure to
9
35
  * import.
10
- * The main use case is to mark missing material in meshes, which will get loaded on demand at the first time the
11
- * dedicated mesh gets visible.
12
- * This is the case if the babylon file is a Combeenation "3d asset" which comes without materials, as the materials
13
- * are defined as "material assets".
36
+ * The main use cases are:
37
+ * - Marking missing material in meshes, which will get loaded on demand at the first time the dedicated mesh gets
38
+ * visible. This is the case if the babylon file is a Combeenation "3d asset" which comes without materials, as the
39
+ * materials are defined as "material assets".
40
+ * - Interpreting custom cbn data that have been injected in the babylon file, e.g. by the "decals editor" in the asset
41
+ * editor area
14
42
  */
15
43
  export function registerCustomCbnBabylonLoaderPlugin(): void {
44
+ if (customLoader) {
45
+ // create the custom loader only once, otherwise receiving the current .babylon plugin would return the custom
46
+ // loader, resulting in multiple calls of the plugin (e.g. decals will be created multiple times)
47
+ return;
48
+ }
49
+
16
50
  // get original plugin for babylon files
17
51
  // we only want to manipulate Combeenation 3d assets, which are represented as babylon files
18
52
  // the plugin is not used for GLB files, "local" babylon are also not really affected by this plugin
19
53
  const previousLoaderPlugin = SceneLoader.GetPluginForExtension('.babylon') as ISceneLoaderPlugin;
20
54
 
21
- const customLoader: ISceneLoaderPlugin = {
55
+ customLoader = {
22
56
  name: 'cbnCustomBabylonLoader',
23
57
  extensions: '.babylon',
24
58
  importMesh: previousLoaderPlugin.importMesh,
25
59
  load: previousLoaderPlugin.load,
26
- loadAssetContainer: function (scene, data, rootUrl, onError) {
60
+ loadAssetContainer: (scene, data, rootUrl, onError): ExtendedAssetContainer => {
27
61
  const dataParsed = JSON.parse(data as string);
28
62
  const importedContainer = previousLoaderPlugin.loadAssetContainer(scene, data, rootUrl);
29
63
 
30
64
  _addMissingMaterialMetadata(dataParsed, importedContainer);
31
65
  _reconstructTagsForInstancedMeshes(dataParsed, importedContainer);
66
+ _createDecals(dataParsed, importedContainer);
67
+
68
+ // add `cbnData` to output asset container, so that this information can be store as metadata for the model
69
+ const extendedContainer = importedContainer as ExtendedAssetContainer;
70
+ extendedContainer.cbnData = dataParsed.cbnData;
32
71
 
33
- return importedContainer;
72
+ return extendedContainer;
34
73
  },
35
74
  };
36
75
 
37
76
  SceneLoader.RegisterPlugin(customLoader);
38
77
  }
39
78
 
40
- type InstanceData = {
41
- name: string;
42
- tags?: string;
43
- };
44
-
45
- function _isMeshInstanceData(data: any): data is InstanceData {
46
- const hasName = isString(data.name);
47
- const hasValidTags = !data.tags || isString(data.tags);
48
- return hasName && hasValidTags;
79
+ function _isDataWithMeshes(data: any): data is DataWithMeshes {
80
+ return isArray(data?.meshes);
49
81
  }
50
82
 
51
- type MeshData = {
52
- name: string;
53
- materialId?: string;
54
- instances?: unknown[];
55
- };
83
+ function _isDataWithDecalConfigurations(data: any): data is DataWithDecalConfigurations {
84
+ return isArray(data?.cbnData?.decals);
85
+ }
56
86
 
57
87
  function _isMeshData(data: any): data is MeshData {
58
88
  const hasName = isString(data.name);
@@ -61,10 +91,25 @@ function _isMeshData(data: any): data is MeshData {
61
91
  return hasName && hasValidMaterialId;
62
92
  }
63
93
 
64
- type DataWithMeshes = { meshes: unknown[] };
94
+ function _isMeshInstanceData(data: any): data is InstanceData {
95
+ const hasName = isString(data.name);
96
+ const hasValidTags = !data.tags || isString(data.tags);
65
97
 
66
- function _isDataWithMeshes(data: any): data is DataWithMeshes {
67
- return data && isArray(data.meshes);
98
+ return hasName && hasValidTags;
99
+ }
100
+
101
+ function _isDecalConfiguration(data: any): data is ParsedDecalConfiguration {
102
+ const hasValidProps =
103
+ isString(data.name) && isString(data.sourceMeshName) && isArray(data.position) && isArray(data.size);
104
+
105
+ if (!hasValidProps) {
106
+ throw new ViewerError({
107
+ id: ViewerErrorIds.InvalidDecalConfiguration,
108
+ message: `Configuration for decal "${data.name}" invalid`,
109
+ });
110
+ }
111
+
112
+ return hasValidProps;
68
113
  }
69
114
 
70
115
  /**
@@ -128,3 +173,34 @@ function _reconstructTagsForInstancedMeshes(dataParsed: unknown, container: Asse
128
173
  }
129
174
  });
130
175
  }
176
+
177
+ /**
178
+ * This function interprets the decal configuration, that is stored top level in the babylon file (`dataParsed`) and
179
+ * creates decal meshes in the given `container`.
180
+ *
181
+ * Having the decal information stored in that way is required for the "decals editor" in the asset manager, as meshes
182
+ * created from decals have to be updateable.
183
+ *
184
+ * @param container This is being manipulated, by adding decal meshes to it based on the decal configuration read from
185
+ * `dataParsed`.
186
+ */
187
+ function _createDecals(dataParsed: unknown, container: AssetContainer): void {
188
+ if (!_isDataWithDecalConfigurations(dataParsed)) return;
189
+
190
+ const validatedDecals = dataParsed.cbnData.decals.filter(_isDecalConfiguration);
191
+
192
+ validatedDecals.forEach(decalConfig => {
193
+ const { materialId, tags, ...decalMeshConfig } = decalConfig;
194
+ const decalMesh = createDecalMesh(decalMeshConfig, container, false);
195
+
196
+ // optionally set material and tags directly
197
+ if (materialId) {
198
+ window.Cbn?.Assets.assertMaterialExists(materialId);
199
+ setInternalMetadataValue(decalMesh, 'deferredMaterial', materialId);
200
+ }
201
+
202
+ if (tags) {
203
+ setTagsAsString(decalMesh, tags);
204
+ }
205
+ });
206
+ }
@@ -1,7 +1,7 @@
1
1
  import { BaseTexture, Material, Node } from '../index';
2
2
  import { cloneDeep } from 'lodash-es';
3
3
 
4
- type MetadataValue = string | number | boolean | undefined;
4
+ type MetadataValue = string | number | boolean | object | undefined;
5
5
  type MetadataTarget = Node | Material | BaseTexture;
6
6
  type MetadataKeys =
7
7
  // CBN babylon loader
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AssetContainer,
3
3
  BuiltInParameter,
4
+ DecalConfiguration,
4
5
  MaterialManager,
5
6
  MeshBuilder,
6
7
  ParameterManager,
@@ -14,6 +15,21 @@ import { cloneModelAssetContainer } from '../internal/cloning-helper';
14
15
  import { getInternalMetadataValue } from '../internal/metadata-helper';
15
16
  import { isArray } from 'lodash-es';
16
17
 
18
+ /**
19
+ * Contains cbn custom data, like decals.
20
+ * This is just a temporary type, as the `loadAssetContainer` function only returns an asset container, which can be
21
+ * altered by our file loader plugin.
22
+ * After loading the model, `cbnData` is cropped and a pure asset container is available for further processing.
23
+ *
24
+ * @internal
25
+ */
26
+ export class ExtendedAssetContainer extends AssetContainer {
27
+ cbnData?: CbnBabylonFileData;
28
+ }
29
+
30
+ type CbnBabylonFileData = { decals?: ParsedDecalConfiguration[] };
31
+ export type ParsedDecalConfiguration = DecalConfiguration & { materialId?: string; tags?: string };
32
+
17
33
  export type ModelAssetDefinition = {
18
34
  name: string;
19
35
  url: string;
@@ -43,6 +59,7 @@ type Model = {
43
59
  url: string;
44
60
  state: ModelAssetState;
45
61
  assetContainer: AssetContainer;
62
+ cbnBabylonFileData?: CbnBabylonFileData;
46
63
  isClone: boolean;
47
64
  visibilityCallId?: number;
48
65
  };
@@ -359,6 +376,34 @@ export class ModelManager {
359
376
  return model.assetContainer;
360
377
  }
361
378
 
379
+ /**
380
+ * Returns the decals configuration of a certain model.\
381
+ * The model will be loaded before being able to access this configuration.\
382
+ * Decals are already converted to "normal" meshes when loading a model, still the original decals configuration can
383
+ * be useful e.g. for alterning decals.
384
+ */
385
+ public async getDecalsConfigurationOfModel(name: string): Promise<ParsedDecalConfiguration[]> {
386
+ const model = this._getModel(name);
387
+ if (!model) {
388
+ throw new ViewerError({
389
+ id: ViewerErrorIds.ModelNotRegistered,
390
+ message: `Can't get decals configuration of model "${name}" as model is not registered`,
391
+ });
392
+ }
393
+
394
+ if (model.state === 'notLoaded') {
395
+ await this._loadModel(model);
396
+ } else if (model.state === 'loading') {
397
+ await this._loadModelPromises[model.name];
398
+ }
399
+
400
+ if (model.state !== 'inScene') {
401
+ await this._prepareModelForScene(model);
402
+ }
403
+
404
+ return model.cbnBabylonFileData?.decals ?? [];
405
+ }
406
+
362
407
  /**
363
408
  * Get model by name
364
409
  */
@@ -384,7 +429,18 @@ export class ModelManager {
384
429
 
385
430
  let assetContainer;
386
431
  try {
387
- assetContainer = await SceneLoader.LoadAssetContainerAsync('', model.url, this.viewer.scene);
432
+ const fullContainer = (await SceneLoader.LoadAssetContainerAsync(
433
+ '',
434
+ model.url,
435
+ this.viewer.scene
436
+ )) as ExtendedAssetContainer;
437
+
438
+ // crop and store custom cbn data from .babylon file
439
+ model.cbnBabylonFileData = fullContainer.cbnData;
440
+ delete fullContainer.cbnData;
441
+
442
+ // from here it's a basic asset container again
443
+ assetContainer = fullContainer as AssetContainer;
388
444
  } catch (e) {
389
445
  throw new ViewerError({
390
446
  id: ViewerErrorIds.AssetLoadingFailed,
@@ -29,6 +29,7 @@ export const ViewerErrorIds = {
29
29
  TextureCouldNotBeParsed: 'TextureCouldNotBeParsed',
30
30
  MaterialAlreadyExists: 'MaterialAlreadyExists',
31
31
  NotAClonedMaterial: 'NotAClonedMaterial',
32
+ InvalidDecalConfiguration: 'InvalidDecalConfiguration',
32
33
  };
33
34
 
34
35
  /** @internal */