@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.
- package/dist/lib-cjs/buildinfo.json +1 -1
- package/dist/lib-cjs/commonjs.tsconfig.tsbuildinfo +1 -1
- package/dist/lib-cjs/helper/decals-helper.d.ts +68 -0
- package/dist/lib-cjs/helper/decals-helper.js +61 -0
- package/dist/lib-cjs/helper/decals-helper.js.map +1 -0
- package/dist/lib-cjs/index.d.ts +1 -0
- package/dist/lib-cjs/index.js +1 -0
- package/dist/lib-cjs/index.js.map +1 -1
- package/dist/lib-cjs/internal/cbn-custom-babylon-loader-plugin.d.ts +6 -4
- package/dist/lib-cjs/internal/cbn-custom-babylon-loader-plugin.js +78 -13
- package/dist/lib-cjs/internal/cbn-custom-babylon-loader-plugin.js.map +1 -1
- package/dist/lib-cjs/internal/metadata-helper.d.ts +1 -1
- package/dist/lib-cjs/manager/model-manager.d.ts +27 -1
- package/dist/lib-cjs/manager/model-manager.js +46 -2
- package/dist/lib-cjs/manager/model-manager.js.map +1 -1
- package/dist/lib-cjs/viewer-error.d.ts +1 -0
- package/dist/lib-cjs/viewer-error.js +1 -0
- package/dist/lib-cjs/viewer-error.js.map +1 -1
- package/package.json +1 -1
- package/src/helper/decals-helper.ts +125 -0
- package/src/index.ts +1 -0
- package/src/internal/cbn-custom-babylon-loader-plugin.ts +101 -25
- package/src/internal/metadata-helper.ts +1 -1
- package/src/manager/model-manager.ts +57 -1
- package/src/viewer-error.ts +1 -0
|
@@ -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
|
@@ -1,58 +1,88 @@
|
|
|
1
|
-
import {
|
|
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
|
|
11
|
-
* dedicated mesh gets
|
|
12
|
-
* This is the case if the babylon file is a Combeenation "3d asset" which comes without materials, as the
|
|
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
|
-
|
|
55
|
+
customLoader = {
|
|
22
56
|
name: 'cbnCustomBabylonLoader',
|
|
23
57
|
extensions: '.babylon',
|
|
24
58
|
importMesh: previousLoaderPlugin.importMesh,
|
|
25
59
|
load: previousLoaderPlugin.load,
|
|
26
|
-
loadAssetContainer:
|
|
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
|
|
72
|
+
return extendedContainer;
|
|
34
73
|
},
|
|
35
74
|
};
|
|
36
75
|
|
|
37
76
|
SceneLoader.RegisterPlugin(customLoader);
|
|
38
77
|
}
|
|
39
78
|
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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,
|
package/src/viewer-error.ts
CHANGED
|
@@ -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 */
|