@combeenation/3d-viewer 14.0.1-rc1 → 15.0.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 (81) hide show
  1. package/README.md +9 -9
  2. package/dist/lib-cjs/buildinfo.json +3 -3
  3. package/dist/lib-cjs/commonjs.tsconfig.tsbuildinfo +1 -1
  4. package/dist/lib-cjs/index.d.ts +51 -62
  5. package/dist/lib-cjs/index.js +84 -94
  6. package/dist/lib-cjs/index.js.map +1 -1
  7. package/dist/lib-cjs/internal/cbn-custom-babylon-loader-plugin.d.ts +10 -10
  8. package/dist/lib-cjs/internal/cbn-custom-babylon-loader-plugin.js +131 -131
  9. package/dist/lib-cjs/internal/cbn-custom-babylon-loader-plugin.js.map +1 -1
  10. package/dist/lib-cjs/internal/cloning-helper.d.ts +19 -19
  11. package/dist/lib-cjs/internal/cloning-helper.js +163 -163
  12. package/dist/lib-cjs/internal/device-helper.d.ts +9 -9
  13. package/dist/lib-cjs/internal/device-helper.js +24 -24
  14. package/dist/lib-cjs/internal/geometry-helper.d.ts +21 -21
  15. package/dist/lib-cjs/internal/geometry-helper.js +145 -145
  16. package/dist/lib-cjs/internal/metadata-helper.d.ts +26 -26
  17. package/dist/lib-cjs/internal/metadata-helper.js +50 -50
  18. package/dist/lib-cjs/internal/paintable-helper.d.ts +40 -40
  19. package/dist/lib-cjs/internal/paintable-helper.js +234 -286
  20. package/dist/lib-cjs/internal/paintable-helper.js.map +1 -1
  21. package/dist/lib-cjs/internal/svg-helper.d.ts +4 -0
  22. package/dist/lib-cjs/internal/svg-helper.js +67 -0
  23. package/dist/lib-cjs/internal/svg-helper.js.map +1 -0
  24. package/dist/lib-cjs/internal/tags-helper.d.ts +12 -12
  25. package/dist/lib-cjs/internal/tags-helper.js +39 -37
  26. package/dist/lib-cjs/internal/tags-helper.js.map +1 -1
  27. package/dist/lib-cjs/internal/texture-parameter-helper.d.ts +37 -0
  28. package/dist/lib-cjs/internal/texture-parameter-helper.js +287 -0
  29. package/dist/lib-cjs/internal/texture-parameter-helper.js.map +1 -0
  30. package/dist/lib-cjs/manager/camera-manager.d.ts +110 -110
  31. package/dist/lib-cjs/manager/camera-manager.js +209 -206
  32. package/dist/lib-cjs/manager/camera-manager.js.map +1 -1
  33. package/dist/lib-cjs/manager/debug-manager.d.ts +60 -60
  34. package/dist/lib-cjs/manager/debug-manager.js +217 -217
  35. package/dist/lib-cjs/manager/event-manager.d.ts +52 -52
  36. package/dist/lib-cjs/manager/event-manager.js +71 -71
  37. package/dist/lib-cjs/manager/gltf-export-manager.d.ts +75 -84
  38. package/dist/lib-cjs/manager/gltf-export-manager.js +286 -290
  39. package/dist/lib-cjs/manager/gltf-export-manager.js.map +1 -1
  40. package/dist/lib-cjs/manager/material-manager.d.ts +35 -35
  41. package/dist/lib-cjs/manager/material-manager.js +125 -125
  42. package/dist/lib-cjs/manager/model-manager.d.ts +145 -145
  43. package/dist/lib-cjs/manager/model-manager.js +382 -382
  44. package/dist/lib-cjs/manager/parameter-manager.d.ts +228 -210
  45. package/dist/lib-cjs/manager/parameter-manager.js +573 -514
  46. package/dist/lib-cjs/manager/parameter-manager.js.map +1 -1
  47. package/dist/lib-cjs/manager/scene-manager.d.ts +45 -45
  48. package/dist/lib-cjs/manager/scene-manager.js +64 -64
  49. package/dist/lib-cjs/manager/texture-manager.d.ts +12 -12
  50. package/dist/lib-cjs/manager/texture-manager.js +43 -43
  51. package/dist/lib-cjs/viewer-error.d.ts +49 -48
  52. package/dist/lib-cjs/viewer-error.js +61 -60
  53. package/dist/lib-cjs/viewer-error.js.map +1 -1
  54. package/dist/lib-cjs/viewer.d.ts +115 -115
  55. package/dist/lib-cjs/viewer.js +217 -217
  56. package/dist/lib-cjs/viewer.js.map +1 -1
  57. package/package.json +94 -91
  58. package/src/buildinfo.json +3 -3
  59. package/src/dev.ts +47 -47
  60. package/src/global-types.d.ts +39 -39
  61. package/src/index.ts +71 -81
  62. package/src/internal/cbn-custom-babylon-loader-plugin.ts +159 -159
  63. package/src/internal/cloning-helper.ts +225 -225
  64. package/src/internal/device-helper.ts +25 -25
  65. package/src/internal/geometry-helper.ts +181 -181
  66. package/src/internal/metadata-helper.ts +63 -63
  67. package/src/internal/paintable-helper.ts +258 -310
  68. package/src/internal/svg-helper.ts +52 -0
  69. package/src/internal/tags-helper.ts +43 -41
  70. package/src/internal/texture-parameter-helper.ts +353 -0
  71. package/src/manager/camera-manager.ts +368 -365
  72. package/src/manager/debug-manager.ts +245 -245
  73. package/src/manager/event-manager.ts +72 -72
  74. package/src/manager/gltf-export-manager.ts +356 -357
  75. package/src/manager/material-manager.ts +135 -135
  76. package/src/manager/model-manager.ts +458 -458
  77. package/src/manager/parameter-manager.ts +730 -652
  78. package/src/manager/scene-manager.ts +101 -101
  79. package/src/manager/texture-manager.ts +32 -32
  80. package/src/viewer-error.ts +69 -68
  81. package/src/viewer.ts +290 -290
@@ -1,652 +1,730 @@
1
- import {
2
- AbstractMesh,
3
- AbstractScene,
4
- Color3,
5
- Material,
6
- PBRMaterial,
7
- StandardMaterial,
8
- TransformNode,
9
- Vector3,
10
- Viewer,
11
- ViewerError,
12
- ViewerErrorIds,
13
- } from '../index';
14
- import { getInternalMetadataValue, setInternalMetadataValue } from '../internal/metadata-helper';
15
- import { paintableParameterObserver } from '../internal/paintable-helper';
16
- import { getTags, hasTag } from '../internal/tags-helper';
17
- import { capitalize, isString } from 'lodash-es';
18
-
19
- /**
20
- * Parameters with a built in observer implementation
21
- */
22
- export const BuiltInParameter = {
23
- Visible: 'visible',
24
- Material: 'material',
25
- Position: 'position',
26
- Rotation: 'rotation',
27
- Scaling: 'scaling',
28
- Color: 'color',
29
- Roughness: 'roughness',
30
- Metallic: 'metallic',
31
- Paintable: 'paintable',
32
- };
33
-
34
- export type ParameterName = string;
35
- export type ParameterValue = string | number | boolean;
36
-
37
- export type TagParameterSubject = { tagName: string; nodeName?: never; materialName?: never };
38
- export type NodeParameterSubject = { tagName?: never; nodeName: string; materialName?: never };
39
- export type MaterialParameterSubject = { tagName?: never; nodeName?: never; materialName: string };
40
-
41
- /**
42
- * Defines which objects are affected by the parameter.\
43
- * This can be single nodes and materials or tags, which can be used to apply a parameter to a group of nodes or
44
- * materials.
45
- */
46
- export type ParameterSubject = TagParameterSubject | NodeParameterSubject | MaterialParameterSubject;
47
-
48
- const isTagParameterSubject = (subject: ParameterSubject): subject is TagParameterSubject =>
49
- 'tagName' in subject && !!subject.tagName;
50
- const isNodeParameterSubject = (subject: ParameterSubject): subject is NodeParameterSubject =>
51
- 'nodeName' in subject && !!subject.nodeName;
52
- const isMaterialParameterSubject = (subject: ParameterSubject): subject is MaterialParameterSubject =>
53
- 'materialName' in subject && !!subject.materialName;
54
-
55
- /**
56
- * Bulk of targeted parameter values, mainly used as input for {@link ParameterManager.setParameterValues} function.\
57
- * E.g.
58
- * ```
59
- * [
60
- * { nodeName: 'someMesh', parameterName: BuiltInParameter.Visible, value: false },
61
- * { materialName: 'someMaterial', parameterName: BuiltInParameter.Color, value: '#DD0060' }
62
- * ]
63
- * ```
64
- * Each parameter value entry has to set exactly one of the subject keys (`nodeName`, `materialName` or `tagName`)
65
- */
66
- export type ParameterValues = (ParameterSubject & {
67
- parameterName: ParameterName;
68
- value: ParameterValue;
69
- })[];
70
-
71
- /**
72
- * Definition of callback function for parameter change
73
- */
74
- export type ParameterObserver = (payload: ParameterObserverPayload) => Promise<void>;
75
-
76
- /**
77
- * Payload of parameter observer.\
78
- * Contains current data of parameter entry, which can be usefull for implementing the dedicated observer
79
- */
80
- export type ParameterObserverPayload = {
81
- subject: ParameterSubject;
82
- nodes: TransformNode[];
83
- materials: Material[];
84
- newValue: ParameterValue;
85
- oldValue: ParameterValue | undefined;
86
- };
87
-
88
- // internal type which holds the data of a certain parameter entry
89
- type ParameterEntry = {
90
- subject: ParameterSubject;
91
- parameterName: ParameterName;
92
- value: ParameterValue;
93
- oldValue: ParameterValue | undefined;
94
- };
95
-
96
- export class ParameterManager {
97
- /**
98
- * Parses and converts input to a boolean value, valid values are:
99
- * - true / false
100
- * - 1 / 0
101
- */
102
- public static parseBoolean(value: ParameterValue): boolean {
103
- if (value.toString() === 'true' || value.toString() === '1') {
104
- return true;
105
- } else if (value.toString() === 'false' || value.toString() === '0') {
106
- return false;
107
- }
108
- throw new ViewerError({
109
- id: ViewerErrorIds.InvalidParameterValue,
110
- message: `Unable to parse "${value}" to a boolean`,
111
- });
112
- }
113
-
114
- /**
115
- * Parses and converts input to a number value
116
- */
117
- public static parseNumber(value: ParameterValue): number {
118
- return parseFloat(value.toString());
119
- }
120
-
121
- /**
122
- * Parses and converts input to a string value
123
- */
124
- public static parseString(value: ParameterValue): string {
125
- return value.toString();
126
- }
127
-
128
- // TODO WTT: enable setting Vector3 on the input directly
129
- /**
130
- * Parses a string of format "(x,y,z)"" to a "Vector3".
131
- */
132
- public static parseVector(value: ParameterValue): Vector3 {
133
- if (!isString(value)) {
134
- throw new ViewerError({
135
- id: ViewerErrorIds.InvalidParameterValue,
136
- message: `Unable to parse "${value}" to a vector: not a string`,
137
- });
138
- }
139
- let cleanedValue = value.split(' ').join('');
140
- if (cleanedValue.startsWith('(') && cleanedValue.endsWith(')')) {
141
- cleanedValue = cleanedValue.substring(1, cleanedValue.length - 1);
142
- const [x, y, z] = cleanedValue.split(',').map(value => parseFloat(value));
143
- return new Vector3(x, y, z);
144
- } else {
145
- throw new ViewerError({
146
- id: ViewerErrorIds.InvalidParameterValue,
147
- message: `Unable to parse "${value}" to a vector: expected "(x,y,z)"`,
148
- });
149
- }
150
- }
151
-
152
- // TODO WTT: enable setting Vector3 on the input directly (maybe quaternion as well)
153
- /**
154
- * Parses a string of format `'(x,y,z)'` with angular degrees to a `Vector3` with rotation information in radians.
155
- */
156
- public static parseRotation(value: ParameterValue): Vector3 {
157
- const rotation = ParameterManager.parseVector(value);
158
- const deg2rad = (deg: number): number => {
159
- return (deg * Math.PI) / 180;
160
- };
161
- return rotation.set(deg2rad(rotation.x), deg2rad(rotation.y), deg2rad(rotation.z));
162
- }
163
-
164
- // TODO WTT: enable setting Color3 on the input directly
165
- /**
166
- * Parses a string of format `'#rrggbb'` or `'(r,g,b)'` to a `Color3`.
167
- */
168
- public static parseColor(value: ParameterValue): Color3 {
169
- const cleanedValue = value.toString().split(' ').join('');
170
- if (cleanedValue.startsWith('#')) {
171
- return Color3.FromHexString(value.toString());
172
- }
173
- if (cleanedValue.startsWith('(') && cleanedValue.endsWith(')')) {
174
- const rgb = cleanedValue.substring(1, cleanedValue.length - 1);
175
- const [r, g, b] = rgb.split(',').map(value => parseFloat(value));
176
- return Color3.FromInts(r, g, b);
177
- }
178
- const humanReadable = capitalize(cleanedValue);
179
-
180
- if (Object.prototype.hasOwnProperty.call(Color3, humanReadable)) {
181
- return (Color3 as any)[humanReadable]();
182
- }
183
- throw new ViewerError({
184
- id: ViewerErrorIds.InvalidParameterValue,
185
- message: `Unable to parse "${value}" to a color: expected "#rrggbb", "(r,g,b)" or any human readable (e.g. Red) property implemented in Color3`,
186
- });
187
- }
188
-
189
- protected _parameterEntries: ParameterEntry[] = [];
190
- protected _parameterObserver: { [parameterName: ParameterName]: ParameterObserver } = {};
191
-
192
- public constructor(protected viewer: Viewer) {
193
- this._addBuiltInParameterObservers();
194
- }
195
-
196
- /**
197
- * Set parameter value for a certain node and calls the corresponding observer if the value has changed
198
- *
199
- * @returns "true" if parameter value has changed
200
- */
201
- public async setNodeParameterValue(
202
- nodeName: string,
203
- parameterName: ParameterName,
204
- value: ParameterValue
205
- ): Promise<boolean> {
206
- const valueChanged = this._addParameterValue({ nodeName }, parameterName, value);
207
- if (valueChanged) {
208
- await this._applyParameterValue({ nodeName }, parameterName);
209
- }
210
-
211
- return valueChanged;
212
- }
213
-
214
- /**
215
- * Set parameter value for a certain material and calls the corresponding observer if the value has changed
216
- *
217
- * @returns "true" if parameter value has changed
218
- */
219
- public async setMaterialParameterValue(
220
- materialName: string,
221
- parameterName: ParameterName,
222
- value: ParameterValue
223
- ): Promise<boolean> {
224
- const valueChanged = this._addParameterValue({ materialName }, parameterName, value);
225
- if (valueChanged) {
226
- await this._applyParameterValue({ materialName }, parameterName);
227
- }
228
-
229
- return valueChanged;
230
- }
231
-
232
- /**
233
- * Set parameter value for a certain tag and calls the corresponding observer if the value has changed.\
234
- * Setting a parameter value on a tag can affect multiple nodes and tags, depending which of these objects contains
235
- * the desired tag.
236
- *
237
- * @returns "true" if parameter value has changed
238
- */
239
- public async setTagParameterValue(
240
- tagName: string,
241
- parameterName: ParameterName,
242
- value: ParameterValue
243
- ): Promise<boolean> {
244
- const valueChanged = this._addParameterValue({ tagName }, parameterName, value);
245
- if (valueChanged) {
246
- await this._applyParameterValue({ tagName }, parameterName);
247
- }
248
-
249
- return valueChanged;
250
- }
251
-
252
- /**
253
- * Set multiple parameter values simultaniously.\
254
- * Tag parameters are applied before node and material parameters, node and materials are more specific and should
255
- * have priority.
256
- *
257
- * @returns Array of parameters, which have changed values
258
- */
259
- public async setParameterValues(values: ParameterValues): Promise<ParameterValues> {
260
- const parameterEntries = values.map<ParameterEntry>(valueEntry => {
261
- const subject: ParameterSubject = isNodeParameterSubject(valueEntry)
262
- ? { nodeName: valueEntry.nodeName }
263
- : isMaterialParameterSubject(valueEntry)
264
- ? { materialName: valueEntry.materialName }
265
- : { tagName: valueEntry.tagName };
266
-
267
- return { subject, parameterName: valueEntry.parameterName, value: valueEntry.value, oldValue: undefined };
268
- });
269
-
270
- const changedParameterEntries = parameterEntries.filter(paramEntry =>
271
- this._addParameterValue(paramEntry.subject, paramEntry.parameterName, paramEntry.value)
272
- );
273
-
274
- await this._applyParameterValues(changedParameterEntries);
275
-
276
- // convert back to original typing
277
- const changedParameterValues: ParameterValues = changedParameterEntries.map(paramEntry => {
278
- return { ...paramEntry.subject, parameterName: paramEntry.parameterName, value: paramEntry.value };
279
- });
280
-
281
- return changedParameterValues;
282
- }
283
-
284
- /**
285
- * @returns desired parameter value or "undefined" if parameter entry is not available.
286
- */
287
- public getParameterValue(subject: ParameterSubject, parameterName: ParameterName): ParameterValue | undefined {
288
- const entry = this._getEntry(subject, parameterName);
289
-
290
- return entry?.value;
291
- }
292
-
293
- /**
294
- * Define observer callback for certain parameter.\
295
- * There can only be one observer for a certain parameter name.\
296
- * Parameter observers can not be overwritten once they are defined, this also includes system observers for
297
- * {@link BuiltInParameter}.
298
- */
299
- public setParameterObserver(parameterName: ParameterName, observer: ParameterObserver): void {
300
- if (this._parameterObserver[parameterName]) {
301
- console.warn(`Observer for parameter "${parameterName}" already set`);
302
- return;
303
- }
304
-
305
- this._parameterObserver[parameterName] = observer;
306
- }
307
-
308
- /**
309
- * Print all parameter entries in table format into the console
310
- */
311
- public printAllParameters(): void {
312
- const printable = this._parameterEntries.map(entry => ({
313
- ...entry,
314
- subject: JSON.stringify(entry.subject),
315
- }));
316
-
317
- console.table(printable);
318
- }
319
-
320
- /**
321
- * Applies all existing parameter entries to a certain "model", as defined in the {@link ModelManager}.\
322
- * This can be usefull when updating the model before showing it in the scene.
323
- *
324
- * @internal
325
- */
326
- public async applyAllParameterValuesToModel(model: AbstractScene): Promise<void> {
327
- const parameterEntriesToApply = this._parameterEntries;
328
-
329
- await this._applyParameterValues(parameterEntriesToApply, model);
330
- }
331
-
332
- /**
333
- * Applies all parameter values which are targeting a material.\
334
- * This can be usefull when updating a material definition before creating it.
335
- *
336
- * @internal
337
- */
338
- public async applyParameterValuesToMaterial(material: Material): Promise<void> {
339
- const tags = getTags(material);
340
-
341
- const parameterEntriesToApply: ParameterEntry[] = [];
342
- tags.forEach(tagName => {
343
- const tagParamEntries = this._getEntriesOfSubject({ tagName });
344
- parameterEntriesToApply.push(...tagParamEntries);
345
- });
346
-
347
- const materialParamEntries = this._getEntriesOfSubject({ materialName: material.id });
348
- parameterEntriesToApply.push(...materialParamEntries);
349
-
350
- await this._applyParameterValues(parameterEntriesToApply);
351
- }
352
-
353
- /**
354
- * @returns Desired parameter value of a certain node.
355
- * Tags are considered as well but have lower priority than node parameters, as these are more specific.
356
- *
357
- * @internal
358
- */
359
- public getParameterValueOfNode(node: TransformNode, parameterName: string): ParameterValue | undefined {
360
- const nodeParamValue = this.getParameterValue({ nodeName: node.name }, parameterName);
361
- if (nodeParamValue !== undefined) {
362
- return nodeParamValue;
363
- }
364
-
365
- const tags = getTags(node);
366
- const tagParamValue = tags.reduce<ParameterValue | undefined>((accValue, curTag) => {
367
- // NOTE: it is possible that values are available for multiple tags
368
- // in this case the resulting parameter value is quite "random" as the last tag in the tag string of the node has
369
- // priority
370
- const tagParamValue = this.getParameterValue({ tagName: curTag }, parameterName);
371
- return accValue ?? tagParamValue;
372
- }, undefined);
373
-
374
- return tagParamValue;
375
- }
376
-
377
- /**
378
- * @returns Desired parameter value of a certain material.
379
- * Tags are considered as well but have lower priority than material parameters, as these are more specific.
380
- * Unused ATM, but added for consistency as counter part for {@link getParameterValueOfNode}
381
- *
382
- * @internal
383
- */
384
- public getParameterValueOfMaterial(material: Material, parameterName: string): ParameterValue | undefined {
385
- const materialParamValue = this.getParameterValue({ materialName: material.name }, parameterName);
386
- if (materialParamValue !== undefined) {
387
- return materialParamValue;
388
- }
389
-
390
- const tags = getTags(material);
391
- const tagParamValue = tags.reduce<ParameterValue | undefined>((accValue, curTag) => {
392
- const tagParamValue = this.getParameterValue({ tagName: curTag }, parameterName);
393
- return accValue ?? tagParamValue;
394
- }, undefined);
395
-
396
- return tagParamValue;
397
- }
398
-
399
- /**
400
- * Parameter observer implementation of default parameters
401
- */
402
- protected _addBuiltInParameterObservers(): void {
403
- this.setParameterObserver(BuiltInParameter.Visible, async ({ nodes, newValue }) => {
404
- const visible = ParameterManager.parseBoolean(newValue);
405
-
406
- for (const node of nodes) {
407
- if (visible) {
408
- const deferredMaterial = getInternalMetadataValue(node, 'deferredMaterial');
409
- if (deferredMaterial) {
410
- await this.viewer.materialManager.setMaterialOnMesh(deferredMaterial as string, node as AbstractMesh);
411
- }
412
-
413
- node.setEnabled(true);
414
- } else {
415
- node.setEnabled(false);
416
- }
417
- }
418
- });
419
- this.setParameterObserver(BuiltInParameter.Material, async ({ nodes, newValue }) => {
420
- const material = ParameterManager.parseString(newValue);
421
-
422
- for (const node of nodes) {
423
- // NOTE: don't use node.isEnabled() as visibility observer is probably called in same cycle but later
424
- // however the parameter value is already correct at this stage
425
- const rawVisibleValue = this.getParameterValueOfNode(node, BuiltInParameter.Visible);
426
- const visible =
427
- rawVisibleValue !== undefined ? ParameterManager.parseBoolean(rawVisibleValue) : node.isEnabled();
428
- if (visible) {
429
- // TODO WTT: check mesh type and throw error if it doesn't fit
430
- // think of creating a framework with type guards (isMesh, canHaveMaterial, ...) around this
431
- await this.viewer.materialManager.setMaterialOnMesh(material, node as AbstractMesh);
432
- } else {
433
- setInternalMetadataValue(node, 'deferredMaterial', material);
434
- }
435
- }
436
- });
437
- this.setParameterObserver(BuiltInParameter.Position, async ({ nodes, newValue }) => {
438
- const position = ParameterManager.parseVector(newValue);
439
-
440
- for (const node of nodes) {
441
- node.position = position;
442
- }
443
- });
444
- this.setParameterObserver(BuiltInParameter.Rotation, async ({ nodes, newValue }) => {
445
- const rotation = ParameterManager.parseRotation(newValue);
446
-
447
- for (const node of nodes) {
448
- node.rotation = rotation;
449
- }
450
- });
451
- this.setParameterObserver(BuiltInParameter.Scaling, async ({ nodes, newValue }) => {
452
- const scaling = ParameterManager.parseVector(newValue);
453
-
454
- for (const node of nodes) {
455
- node.scaling = scaling;
456
- }
457
- });
458
- this.setParameterObserver(BuiltInParameter.Color, async ({ materials, newValue }) => {
459
- const color = ParameterManager.parseColor(newValue);
460
-
461
- for (const material of materials) {
462
- const materialCls = material.getClassName();
463
- switch (materialCls) {
464
- case 'PBRMaterial':
465
- (material as PBRMaterial).albedoColor = color.toLinearSpace();
466
- break;
467
- case 'StandardMaterial':
468
- (material as StandardMaterial).diffuseColor = color;
469
- break;
470
- default:
471
- throw new Error(`Setting color for material of instance "${materialCls}" not implemented`);
472
- }
473
- }
474
- });
475
- this.setParameterObserver(BuiltInParameter.Roughness, async ({ materials, newValue }) => {
476
- const roughness = ParameterManager.parseNumber(newValue);
477
-
478
- for (const material of materials) {
479
- const materialCls = material.getClassName();
480
- switch (materialCls) {
481
- case 'PBRMaterial':
482
- (material as PBRMaterial).roughness = roughness;
483
- break;
484
- case 'StandardMaterial':
485
- (material as StandardMaterial).roughness = roughness;
486
- break;
487
- default:
488
- throw new Error(`Setting rougness for material of instance "${materialCls}" not implemented`);
489
- }
490
- }
491
- });
492
- this.setParameterObserver(BuiltInParameter.Metallic, async ({ materials, newValue }) => {
493
- const metallic = ParameterManager.parseNumber(newValue);
494
-
495
- for (const material of materials) {
496
- const materialCls = material.getClassName();
497
- switch (materialCls) {
498
- case 'PBRMaterial':
499
- (material as PBRMaterial).metallic = metallic;
500
- break;
501
- default:
502
- throw new Error(`Setting metallic for material of instance "${materialCls}" not implemented`);
503
- }
504
- }
505
- });
506
- this.setParameterObserver(BuiltInParameter.Paintable, async ({ newValue, materials }) => {
507
- paintableParameterObserver(newValue, materials, this.viewer.scene);
508
- });
509
- }
510
-
511
- /**
512
- * Change parameter value in array of existing parameter entries or create a new entry
513
- *
514
- * @returns "true" if parameter has changed or wasn't available before
515
- */
516
- protected _addParameterValue(
517
- subject: ParameterSubject,
518
- parameterName: ParameterName,
519
- value: ParameterValue
520
- ): boolean {
521
- const existingEntry = this._getEntry(subject, parameterName);
522
-
523
- if (existingEntry?.value === value) {
524
- return false;
525
- }
526
-
527
- if (existingEntry) {
528
- existingEntry.oldValue = existingEntry.value;
529
- existingEntry.value = value;
530
- } else {
531
- this._parameterEntries.push({ subject, parameterName, value, oldValue: undefined });
532
- }
533
-
534
- return true;
535
- }
536
-
537
- /**
538
- * Call parameter observer of desired parameter which usually apply the new values to the scene.
539
- *
540
- * @param assetContainer Asset container in which to look for the paramter entries subjects (e.g. the nodes and
541
- * materials to which the parameter values should be applied to).\
542
- * Defaults to `viewer.scene`.
543
- */
544
- protected async _applyParameterValues(
545
- parameterEntries: ParameterEntry[],
546
- assetContainer?: AbstractScene
547
- ): Promise<void> {
548
- const tagParamEntries = parameterEntries.filter(entry => isTagParameterSubject(entry.subject));
549
- const nonTagParamEntries = parameterEntries.filter(entry => !isTagParameterSubject(entry.subject));
550
-
551
- for (const entry of tagParamEntries) {
552
- await this._applyParameterValue(entry.subject, entry.parameterName, assetContainer);
553
- }
554
- for (const entry of nonTagParamEntries) {
555
- await this._applyParameterValue(entry.subject, entry.parameterName, assetContainer);
556
- }
557
- }
558
-
559
- /**
560
- * Call parameter observer of desired parameter
561
- *
562
- * @param assetContainer Optionally add an "asset container", which actually represents a model in the
563
- * {@link ModelManager}. Viewer scene is used if left empty.
564
- */
565
- protected async _applyParameterValue(
566
- subject: ParameterSubject,
567
- parameterName: ParameterName,
568
- assetContainer?: AbstractScene
569
- ): Promise<void> {
570
- const parameterEntry = this._getEntry(subject, parameterName);
571
- const observer = this._parameterObserver[parameterName];
572
- if (!parameterEntry || !observer) {
573
- return;
574
- }
575
-
576
- const nodes = this._getAffectedNodes(subject, assetContainer);
577
- const materials = this._getAffectedMaterials(subject, assetContainer);
578
-
579
- await observer({
580
- subject,
581
- newValue: parameterEntry.value,
582
- oldValue: parameterEntry.oldValue,
583
- nodes,
584
- materials,
585
- });
586
- }
587
-
588
- protected _getAffectedMaterials(subject: ParameterSubject, assetContainer?: AbstractScene): Material[] {
589
- assetContainer = assetContainer ?? this.viewer.scene;
590
- let materials: Material[] = [];
591
-
592
- // materials have priority over tags
593
- if (isMaterialParameterSubject(subject)) {
594
- materials = assetContainer.materials.filter(material => material.name === subject.materialName);
595
- if (materials.length > 1) {
596
- console.warn(`Multiple materials for material name "${subject.materialName}" have been found`);
597
- }
598
- } else if (isTagParameterSubject(subject)) {
599
- materials = assetContainer.materials.filter(material => hasTag(material, subject.tagName));
600
- }
601
-
602
- return materials;
603
- }
604
-
605
- protected _getAffectedNodes(subject: ParameterSubject, assetContainer?: AbstractScene): TransformNode[] {
606
- assetContainer = assetContainer ?? this.viewer.scene;
607
- const allNodes = [...assetContainer.meshes, ...assetContainer.transformNodes];
608
- let nodes: TransformNode[] = [];
609
-
610
- // nodes have priority over tags
611
- if (isNodeParameterSubject(subject)) {
612
- nodes = allNodes.filter(node => node.name === subject.nodeName);
613
- if (nodes.length > 1) {
614
- console.warn(`Multiple nodes for node name "${subject.nodeName}" have been found`);
615
- }
616
- } else if (isTagParameterSubject(subject)) {
617
- nodes = allNodes.filter(node => hasTag(node, subject.tagName));
618
- }
619
-
620
- return nodes;
621
- }
622
-
623
- protected _getEntry(subject: ParameterSubject, parameterName: ParameterName): ParameterEntry | undefined {
624
- const entriesOfSubject = this._getEntriesOfSubject(subject);
625
- const entry = entriesOfSubject.find(entry => entry.parameterName === parameterName);
626
-
627
- return entry;
628
- }
629
-
630
- protected _getEntriesOfSubject(subject: ParameterSubject): ParameterEntry[] {
631
- const entries = this._parameterEntries.filter(entry => {
632
- const nodeNameMatches =
633
- isNodeParameterSubject(entry.subject) &&
634
- isNodeParameterSubject(subject) &&
635
- entry.subject.nodeName === subject.nodeName;
636
-
637
- const materialNameMatches =
638
- isMaterialParameterSubject(entry.subject) &&
639
- isMaterialParameterSubject(subject) &&
640
- entry.subject.materialName === subject.materialName;
641
-
642
- const tagNameMatches =
643
- isTagParameterSubject(entry.subject) &&
644
- isTagParameterSubject(subject) &&
645
- entry.subject.tagName === subject.tagName;
646
-
647
- return nodeNameMatches || materialNameMatches || tagNameMatches;
648
- });
649
-
650
- return entries;
651
- }
652
- }
1
+ import {
2
+ AbstractMesh,
3
+ AbstractScene,
4
+ Color3,
5
+ Material,
6
+ PBRMaterial,
7
+ StandardMaterial,
8
+ TransformNode,
9
+ Vector3,
10
+ Viewer,
11
+ ViewerError,
12
+ ViewerErrorIds,
13
+ } from '../index';
14
+ import { getInternalMetadataValue, setInternalMetadataValue } from '../internal/metadata-helper';
15
+ import { paintableParameterObserver } from '../internal/paintable-helper';
16
+ import { getTags, hasTag } from '../internal/tags-helper';
17
+ import {
18
+ BuiltInTextureParameter,
19
+ BuiltInTextureParameterKeys,
20
+ ParameterTextureChannelsKeys,
21
+ createBuiltInTextureParameter,
22
+ } from '../internal/texture-parameter-helper';
23
+ import { capitalize, isString } from 'lodash-es';
24
+
25
+ /**
26
+ * Parameters with a built in observer implementation
27
+ */
28
+ export const BuiltInParameter = {
29
+ Visible: 'visible',
30
+ Material: 'material',
31
+ Position: 'position',
32
+ Rotation: 'rotation',
33
+ Scaling: 'scaling',
34
+ Color: 'color',
35
+ Roughness: 'roughness',
36
+ Metallic: 'metallic',
37
+ /**
38
+ * Texture parameters are always a combination of the channel (e.g. `albedoTexture`) and a sub parameter
39
+ * (e.g. `uScale`). Use this function to create the parameter accordingly
40
+ * (e.g. createTextureParameter("albedoTexture", "uScale")).
41
+ */
42
+ createTextureParameter: (channel: ParameterTextureChannelsKeys, parameter: BuiltInTextureParameterKeys): string =>
43
+ `${channel}.${parameter}`,
44
+ UseDetailmap: 'useDetailmap',
45
+ };
46
+
47
+ /** @ignore @deprected Use "texture parameters" {@link BuiltInParameter}.`createTextureParameter` instead */
48
+ export const LegacyParameter = {
49
+ Paintable: 'paintable',
50
+ };
51
+
52
+ export type ParameterName = string;
53
+ export type ParameterValue = string | number | boolean;
54
+
55
+ export type TagParameterSubject = { tagName: string; nodeName?: never; materialName?: never };
56
+ export type NodeParameterSubject = { tagName?: never; nodeName: string; materialName?: never };
57
+ export type MaterialParameterSubject = { tagName?: never; nodeName?: never; materialName: string };
58
+
59
+ /**
60
+ * Defines which objects are affected by the parameter.\
61
+ * This can be single nodes and materials or tags, which can be used to apply a parameter to a group of nodes or
62
+ * materials.
63
+ */
64
+ export type ParameterSubject = TagParameterSubject | NodeParameterSubject | MaterialParameterSubject;
65
+
66
+ const isTagParameterSubject = (subject: ParameterSubject): subject is TagParameterSubject =>
67
+ 'tagName' in subject && !!subject.tagName;
68
+ const isNodeParameterSubject = (subject: ParameterSubject): subject is NodeParameterSubject =>
69
+ 'nodeName' in subject && !!subject.nodeName;
70
+ const isMaterialParameterSubject = (subject: ParameterSubject): subject is MaterialParameterSubject =>
71
+ 'materialName' in subject && !!subject.materialName;
72
+
73
+ /**
74
+ * Bulk of targeted parameter values, mainly used as input for {@link ParameterManager.setParameterValues} function.\
75
+ * E.g.
76
+ * ```
77
+ * [
78
+ * { nodeName: 'someMesh', parameterName: BuiltInParameter.Visible, value: false },
79
+ * { materialName: 'someMaterial', parameterName: BuiltInParameter.Color, value: '#DD0060' }
80
+ * ]
81
+ * ```
82
+ * Each parameter value entry has to set exactly one of the subject keys (`nodeName`, `materialName` or `tagName`)
83
+ */
84
+ export type ParameterValues = (ParameterSubject & {
85
+ parameterName: ParameterName;
86
+ value: ParameterValue;
87
+ })[];
88
+
89
+ /**
90
+ * Definition of callback function for parameter change
91
+ */
92
+ export type ParameterObserver = (payload: ParameterObserverPayload) => Promise<void>;
93
+
94
+ /**
95
+ * Payload of parameter observer.\
96
+ * Contains current data of parameter entry, which can be usefull for implementing the dedicated observer
97
+ */
98
+ export type ParameterObserverPayload = {
99
+ subject: ParameterSubject;
100
+ nodes: TransformNode[];
101
+ materials: Material[];
102
+ newValue: ParameterValue;
103
+ oldValue: ParameterValue | undefined;
104
+ };
105
+
106
+ // internal type which holds the data of a certain parameter entry
107
+ type ParameterEntry = {
108
+ subject: ParameterSubject;
109
+ parameterName: ParameterName;
110
+ value: ParameterValue;
111
+ oldValue: ParameterValue | undefined;
112
+ };
113
+
114
+ export class ParameterManager {
115
+ /**
116
+ * Parses and converts input to a boolean value, valid values are:
117
+ * - true / false
118
+ * - 1 / 0
119
+ */
120
+ public static parseBoolean(value: ParameterValue): boolean {
121
+ if (value.toString() === 'true' || value.toString() === '1') {
122
+ return true;
123
+ } else if (value.toString() === 'false' || value.toString() === '0') {
124
+ return false;
125
+ }
126
+ throw new ViewerError({
127
+ id: ViewerErrorIds.InvalidParameterValue,
128
+ message: `Unable to parse "${value}" to a boolean`,
129
+ });
130
+ }
131
+
132
+ /**
133
+ * Parses and converts input to a number value
134
+ */
135
+ public static parseNumber(value: ParameterValue): number {
136
+ return parseFloat(value.toString());
137
+ }
138
+
139
+ /**
140
+ * Parses and converts input to a string value
141
+ */
142
+ public static parseString(value: ParameterValue): string {
143
+ return value.toString();
144
+ }
145
+
146
+ // TODO WTT: enable setting Vector3 on the input directly
147
+ /**
148
+ * Parses a string of format "(x,y,z)"" to a "Vector3".
149
+ */
150
+ public static parseVector(value: ParameterValue): Vector3 {
151
+ if (!isString(value)) {
152
+ throw new ViewerError({
153
+ id: ViewerErrorIds.InvalidParameterValue,
154
+ message: `Unable to parse "${value}" to a vector: not a string`,
155
+ });
156
+ }
157
+ let cleanedValue = value.split(' ').join('');
158
+ if (cleanedValue.startsWith('(') && cleanedValue.endsWith(')')) {
159
+ cleanedValue = cleanedValue.substring(1, cleanedValue.length - 1);
160
+ const [x, y, z] = cleanedValue.split(',').map(value => parseFloat(value));
161
+ return new Vector3(x, y, z);
162
+ } else {
163
+ throw new ViewerError({
164
+ id: ViewerErrorIds.InvalidParameterValue,
165
+ message: `Unable to parse "${value}" to a vector: expected "(x,y,z)"`,
166
+ });
167
+ }
168
+ }
169
+
170
+ // TODO WTT: enable setting Vector3 on the input directly (maybe quaternion as well)
171
+ /**
172
+ * Parses a string of format `'(x,y,z)'` with angular degrees to a `Vector3` with rotation information in radians.
173
+ */
174
+ public static parseRotation(value: ParameterValue): Vector3 {
175
+ const rotation = ParameterManager.parseVector(value);
176
+ const deg2rad = (deg: number): number => {
177
+ return (deg * Math.PI) / 180;
178
+ };
179
+ return rotation.set(deg2rad(rotation.x), deg2rad(rotation.y), deg2rad(rotation.z));
180
+ }
181
+
182
+ // TODO WTT: enable setting Color3 on the input directly
183
+ /**
184
+ * Parses a string of format `'#rrggbb'` or `'(r,g,b)'` to a `Color3`.
185
+ */
186
+ public static parseColor(value: ParameterValue): Color3 {
187
+ const cleanedValue = value.toString().split(' ').join('');
188
+ if (cleanedValue.startsWith('#')) {
189
+ return Color3.FromHexString(value.toString());
190
+ }
191
+ if (cleanedValue.startsWith('(') && cleanedValue.endsWith(')')) {
192
+ const rgb = cleanedValue.substring(1, cleanedValue.length - 1);
193
+ const [r, g, b] = rgb.split(',').map(value => parseFloat(value));
194
+ return Color3.FromInts(r, g, b);
195
+ }
196
+ const humanReadable = capitalize(cleanedValue);
197
+
198
+ if (Object.prototype.hasOwnProperty.call(Color3, humanReadable)) {
199
+ return (Color3 as any)[humanReadable]();
200
+ }
201
+ throw new ViewerError({
202
+ id: ViewerErrorIds.InvalidParameterValue,
203
+ message: `Unable to parse "${value}" to a color: expected "#rrggbb", "(r,g,b)" or any human readable (e.g. Red) property implemented in Color3`,
204
+ });
205
+ }
206
+
207
+ protected _parameterEntries: ParameterEntry[] = [];
208
+ protected _parameterObserver: { [parameterName: ParameterName]: ParameterObserver } = {};
209
+
210
+ public constructor(protected viewer: Viewer) {
211
+ this._addBuiltInParameterObservers();
212
+ }
213
+
214
+ /**
215
+ * Set parameter value for a certain node and calls the corresponding observer if the value has changed
216
+ *
217
+ * @returns "true" if parameter value has changed
218
+ */
219
+ public async setNodeParameterValue(
220
+ nodeName: string,
221
+ parameterName: ParameterName,
222
+ value: ParameterValue
223
+ ): Promise<boolean> {
224
+ const valueChanged = this._addParameterValue({ nodeName }, parameterName, value);
225
+ if (valueChanged) {
226
+ await this._applyParameterValue({ nodeName }, parameterName);
227
+ }
228
+
229
+ return valueChanged;
230
+ }
231
+
232
+ /**
233
+ * Set parameter value for a certain material and calls the corresponding observer if the value has changed
234
+ *
235
+ * @returns "true" if parameter value has changed
236
+ */
237
+ public async setMaterialParameterValue(
238
+ materialName: string,
239
+ parameterName: ParameterName,
240
+ value: ParameterValue
241
+ ): Promise<boolean> {
242
+ const valueChanged = this._addParameterValue({ materialName }, parameterName, value);
243
+ if (valueChanged) {
244
+ await this._applyParameterValue({ materialName }, parameterName);
245
+ }
246
+
247
+ return valueChanged;
248
+ }
249
+
250
+ /**
251
+ * Set parameter value for a certain tag and calls the corresponding observer if the value has changed.\
252
+ * Setting a parameter value on a tag can affect multiple nodes and tags, depending which of these objects contains
253
+ * the desired tag.
254
+ *
255
+ * @returns "true" if parameter value has changed
256
+ */
257
+ public async setTagParameterValue(
258
+ tagName: string,
259
+ parameterName: ParameterName,
260
+ value: ParameterValue
261
+ ): Promise<boolean> {
262
+ const valueChanged = this._addParameterValue({ tagName }, parameterName, value);
263
+ if (valueChanged) {
264
+ await this._applyParameterValue({ tagName }, parameterName);
265
+ }
266
+
267
+ return valueChanged;
268
+ }
269
+
270
+ /**
271
+ * Set multiple parameter values simultaniously.\
272
+ * Tag parameters are applied before node and material parameters, node and materials are more specific and should
273
+ * have priority.
274
+ *
275
+ * @returns Array of parameters, which have changed values
276
+ */
277
+ public async setParameterValues(values: ParameterValues): Promise<ParameterValues> {
278
+ const parameterEntries = values.map<ParameterEntry>(valueEntry => {
279
+ const subject: ParameterSubject = isNodeParameterSubject(valueEntry)
280
+ ? { nodeName: valueEntry.nodeName }
281
+ : isMaterialParameterSubject(valueEntry)
282
+ ? { materialName: valueEntry.materialName }
283
+ : { tagName: valueEntry.tagName };
284
+
285
+ return { subject, parameterName: valueEntry.parameterName, value: valueEntry.value, oldValue: undefined };
286
+ });
287
+
288
+ const changedParameterEntries = parameterEntries.filter(paramEntry =>
289
+ this._addParameterValue(paramEntry.subject, paramEntry.parameterName, paramEntry.value)
290
+ );
291
+
292
+ await this._applyParameterValues(changedParameterEntries);
293
+
294
+ // convert back to original typing
295
+ const changedParameterValues: ParameterValues = changedParameterEntries.map(paramEntry => {
296
+ return { ...paramEntry.subject, parameterName: paramEntry.parameterName, value: paramEntry.value };
297
+ });
298
+
299
+ return changedParameterValues;
300
+ }
301
+
302
+ /**
303
+ * @returns desired parameter value or "undefined" if parameter entry is not available.
304
+ */
305
+ public getParameterValue(subject: ParameterSubject, parameterName: ParameterName): ParameterValue | undefined {
306
+ const entry = this._getEntry(subject, parameterName);
307
+
308
+ return entry?.value;
309
+ }
310
+
311
+ /**
312
+ * Define observer callback for certain parameter.\
313
+ * There can only be one observer for a certain parameter name.\
314
+ * Parameter observers can not be overwritten once they are defined, this also includes system observers for
315
+ * {@link BuiltInParameter}.
316
+ */
317
+ public setParameterObserver(parameterName: ParameterName, observer: ParameterObserver): void {
318
+ if (this._parameterObserver[parameterName]) {
319
+ console.warn(`Observer for parameter "${parameterName}" already set`);
320
+ return;
321
+ }
322
+
323
+ this._parameterObserver[parameterName] = observer;
324
+ }
325
+
326
+ /**
327
+ * Print all parameter entries in table format into the console
328
+ */
329
+ public printAllParameters(): void {
330
+ const printable = this._parameterEntries.map(entry => ({
331
+ ...entry,
332
+ subject: JSON.stringify(entry.subject),
333
+ }));
334
+
335
+ console.table(printable);
336
+ }
337
+
338
+ /**
339
+ * Applies all existing parameter entries to a certain "model", as defined in the {@link ModelManager}.\
340
+ * This can be usefull when updating the model before showing it in the scene.
341
+ *
342
+ * @internal
343
+ */
344
+ public async applyAllParameterValuesToModel(model: AbstractScene): Promise<void> {
345
+ const parameterEntriesToApply = this._parameterEntries;
346
+
347
+ await this._applyParameterValues(parameterEntriesToApply, model);
348
+ }
349
+
350
+ /**
351
+ * Applies all parameter values which are targeting a material.\
352
+ * This can be usefull when updating a material definition before creating it.
353
+ *
354
+ * @internal
355
+ */
356
+ public async applyParameterValuesToMaterial(material: Material): Promise<void> {
357
+ const tags = getTags(material);
358
+
359
+ const parameterEntriesToApply: ParameterEntry[] = [];
360
+ tags.forEach(tagName => {
361
+ const tagParamEntries = this._getEntriesOfSubject({ tagName });
362
+ parameterEntriesToApply.push(...tagParamEntries);
363
+ });
364
+
365
+ const materialParamEntries = this._getEntriesOfSubject({ materialName: material.id });
366
+ parameterEntriesToApply.push(...materialParamEntries);
367
+
368
+ await this._applyParameterValues(parameterEntriesToApply);
369
+ }
370
+
371
+ /**
372
+ * Applies subset of texture parameter values which are targeting a certain texture channel in a material.\
373
+ * This can be useful when updating texture settings (e.g. uScale) before the texture is actually created.
374
+ *
375
+ * @internal
376
+ */
377
+ public async applyTextureSettingsParameter(
378
+ material: PBRMaterial,
379
+ channel: ParameterTextureChannelsKeys
380
+ ): Promise<void> {
381
+ const tags = getTags(material);
382
+
383
+ const parameterEntriesToApply: ParameterEntry[] = [];
384
+ tags.forEach(tagName => {
385
+ const tagParamEntries = this._getEntriesOfSubject({ tagName });
386
+ parameterEntriesToApply.push(...tagParamEntries);
387
+ });
388
+
389
+ const materialParamEntries = this._getEntriesOfSubject({ materialName: material.id });
390
+ parameterEntriesToApply.push(...materialParamEntries);
391
+
392
+ const textureParameterEntriesToApply = parameterEntriesToApply.filter(parameterEntry => {
393
+ const paramPath = parameterEntry.parameterName.split('.');
394
+ // find texture parameters of this channel, e.g. "albedoTexture.uScale"
395
+ // "image" should not be applied here as this is part of the texture initialization anyway
396
+ const isTextureParamOfChannel =
397
+ paramPath.length === 2 && paramPath[0] === channel && paramPath[1] !== BuiltInTextureParameter.image;
398
+
399
+ return isTextureParamOfChannel;
400
+ });
401
+
402
+ await this._applyParameterValues(textureParameterEntriesToApply);
403
+ }
404
+
405
+ /**
406
+ * @returns Desired parameter value of a certain node.
407
+ * Tags are considered as well but have lower priority than node parameters, as these are more specific.
408
+ *
409
+ * @internal
410
+ */
411
+ public getParameterValueOfNode(node: TransformNode, parameterName: string): ParameterValue | undefined {
412
+ const nodeParamValue = this.getParameterValue({ nodeName: node.name }, parameterName);
413
+ if (nodeParamValue !== undefined) {
414
+ return nodeParamValue;
415
+ }
416
+
417
+ const tags = getTags(node);
418
+ const tagParamValue = tags.reduce<ParameterValue | undefined>((accValue, curTag) => {
419
+ // NOTE: it is possible that values are available for multiple tags
420
+ // in this case the resulting parameter value is quite "random" as the last tag in the tag string of the node has
421
+ // priority
422
+ const tagParamValue = this.getParameterValue({ tagName: curTag }, parameterName);
423
+ return accValue ?? tagParamValue;
424
+ }, undefined);
425
+
426
+ return tagParamValue;
427
+ }
428
+
429
+ /**
430
+ * @returns Desired parameter value of a certain material.
431
+ * Tags are considered as well but have lower priority than material parameters, as these are more specific.
432
+ * Unused ATM, but added for consistency as counter part for {@link getParameterValueOfNode}
433
+ *
434
+ * @internal
435
+ */
436
+ public getParameterValueOfMaterial(material: Material, parameterName: string): ParameterValue | undefined {
437
+ const materialParamValue = this.getParameterValue({ materialName: material.name }, parameterName);
438
+ if (materialParamValue !== undefined) {
439
+ return materialParamValue;
440
+ }
441
+
442
+ const tags = getTags(material);
443
+ const tagParamValue = tags.reduce<ParameterValue | undefined>((accValue, curTag) => {
444
+ const tagParamValue = this.getParameterValue({ tagName: curTag }, parameterName);
445
+ return accValue ?? tagParamValue;
446
+ }, undefined);
447
+
448
+ return tagParamValue;
449
+ }
450
+
451
+ /**
452
+ * Parameter observer implementation of default parameters
453
+ */
454
+ protected _addBuiltInParameterObservers(): void {
455
+ this.setParameterObserver(BuiltInParameter.Visible, async ({ nodes, newValue }) => {
456
+ const visible = ParameterManager.parseBoolean(newValue);
457
+
458
+ for (const node of nodes) {
459
+ if (visible) {
460
+ // if a mesh gets visible by this operation we have to activate the assigned material which is stored in the
461
+ // internal metadata
462
+ // => consider child meshes as well (CB-10143)
463
+ const activatedNodes = [node, ...node.getChildMeshes(false)];
464
+ for (const activatedNode of activatedNodes) {
465
+ const deferredMaterial = getInternalMetadataValue(activatedNode, 'deferredMaterial');
466
+ if (deferredMaterial) {
467
+ await this.viewer.materialManager.setMaterialOnMesh(
468
+ deferredMaterial as string,
469
+ // this cast is fine, as deferred material can only be set on meshes
470
+ activatedNode as AbstractMesh
471
+ );
472
+ }
473
+ }
474
+
475
+ node.setEnabled(true);
476
+ } else {
477
+ node.setEnabled(false);
478
+ }
479
+ }
480
+ });
481
+ this.setParameterObserver(BuiltInParameter.Material, async ({ nodes, newValue }) => {
482
+ const material = ParameterManager.parseString(newValue);
483
+
484
+ for (const node of nodes) {
485
+ // NOTE: don't use node.isEnabled() as visibility observer is probably called in same cycle but later
486
+ // however the parameter value is already correct at this stage
487
+ // we have to go through all parents as well, because a parent may have been set to false, which also disables
488
+ // this child node
489
+ let curNode: TransformNode | null = node;
490
+ let visibleByParameter: boolean | undefined = undefined;
491
+ while (curNode && visibleByParameter !== false) {
492
+ const curNodeVisibleByParameter = this.getParameterValueOfNode(curNode, BuiltInParameter.Visible);
493
+ if (curNodeVisibleByParameter !== undefined) {
494
+ visibleByParameter = curNodeVisibleByParameter as boolean;
495
+ }
496
+ curNode = curNode.parent as TransformNode;
497
+ }
498
+
499
+ // parameter visibility has priority, but if the visiblity is not controlled by the parameter use the plain
500
+ // Babylon.js isEnabled() check
501
+ const visible =
502
+ visibleByParameter !== undefined ? ParameterManager.parseBoolean(visibleByParameter) : node.isEnabled();
503
+ if (visible) {
504
+ // TODO WTT: check mesh type and throw error if it doesn't fit
505
+ // think of creating a framework with type guards (isMesh, canHaveMaterial, ...) around this
506
+ await this.viewer.materialManager.setMaterialOnMesh(material, node as AbstractMesh);
507
+ } else {
508
+ setInternalMetadataValue(node, 'deferredMaterial', material);
509
+ }
510
+ }
511
+ });
512
+ this.setParameterObserver(BuiltInParameter.Position, async ({ nodes, newValue }) => {
513
+ const position = ParameterManager.parseVector(newValue);
514
+
515
+ for (const node of nodes) {
516
+ node.position = position;
517
+ }
518
+ });
519
+ this.setParameterObserver(BuiltInParameter.Rotation, async ({ nodes, newValue }) => {
520
+ const rotation = ParameterManager.parseRotation(newValue);
521
+
522
+ for (const node of nodes) {
523
+ node.rotation = rotation;
524
+ }
525
+ });
526
+ this.setParameterObserver(BuiltInParameter.Scaling, async ({ nodes, newValue }) => {
527
+ const scaling = ParameterManager.parseVector(newValue);
528
+
529
+ for (const node of nodes) {
530
+ node.scaling = scaling;
531
+ }
532
+ });
533
+ this.setParameterObserver(BuiltInParameter.Color, async ({ materials, newValue }) => {
534
+ const color = ParameterManager.parseColor(newValue);
535
+
536
+ for (const material of materials) {
537
+ const materialCls = material.getClassName();
538
+ switch (materialCls) {
539
+ case 'PBRMaterial':
540
+ (material as PBRMaterial).albedoColor = color.toLinearSpace();
541
+ break;
542
+ case 'StandardMaterial':
543
+ (material as StandardMaterial).diffuseColor = color;
544
+ break;
545
+ default:
546
+ throw new Error(`Setting color for material of instance "${materialCls}" not implemented`);
547
+ }
548
+ }
549
+ });
550
+ this.setParameterObserver(BuiltInParameter.Roughness, async ({ materials, newValue }) => {
551
+ const roughness = ParameterManager.parseNumber(newValue);
552
+
553
+ for (const material of materials) {
554
+ const materialCls = material.getClassName();
555
+ switch (materialCls) {
556
+ case 'PBRMaterial':
557
+ (material as PBRMaterial).roughness = roughness;
558
+ break;
559
+ case 'StandardMaterial':
560
+ (material as StandardMaterial).roughness = roughness;
561
+ break;
562
+ default:
563
+ throw new Error(`Setting rougness for material of instance "${materialCls}" not implemented`);
564
+ }
565
+ }
566
+ });
567
+ this.setParameterObserver(BuiltInParameter.Metallic, async ({ materials, newValue }) => {
568
+ const metallic = ParameterManager.parseNumber(newValue);
569
+
570
+ for (const material of materials) {
571
+ const materialCls = material.getClassName();
572
+ switch (materialCls) {
573
+ case 'PBRMaterial':
574
+ (material as PBRMaterial).metallic = metallic;
575
+ break;
576
+ default:
577
+ throw new Error(`Setting metallic for material of instance "${materialCls}" not implemented`);
578
+ }
579
+ }
580
+ });
581
+ this.setParameterObserver(LegacyParameter.Paintable, async ({ newValue, materials }) => {
582
+ paintableParameterObserver(newValue, materials, this.viewer.scene);
583
+ });
584
+
585
+ // texture parameter use a more generic approach (`channel`.`parameter`) and are therefore coded in a dedicated file
586
+ createBuiltInTextureParameter(this, this.viewer.scene);
587
+ }
588
+
589
+ /**
590
+ * Change parameter value in array of existing parameter entries or create a new entry
591
+ *
592
+ * @returns "true" if parameter has changed or wasn't available before
593
+ */
594
+ protected _addParameterValue(
595
+ subject: ParameterSubject,
596
+ parameterName: ParameterName,
597
+ value: ParameterValue
598
+ ): boolean {
599
+ const existingEntry = this._getEntry(subject, parameterName);
600
+
601
+ if (existingEntry?.value === value) {
602
+ return false;
603
+ }
604
+
605
+ if (existingEntry) {
606
+ existingEntry.oldValue = existingEntry.value;
607
+ existingEntry.value = value;
608
+ } else {
609
+ this._parameterEntries.push({ subject, parameterName, value, oldValue: undefined });
610
+ }
611
+
612
+ return true;
613
+ }
614
+
615
+ /**
616
+ * Call parameter observer of desired parameter which usually apply the new values to the scene.
617
+ *
618
+ * @param assetContainer Asset container in which to look for the paramter entries subjects (e.g. the nodes and
619
+ * materials to which the parameter values should be applied to).\
620
+ * Defaults to `viewer.scene`.
621
+ */
622
+ protected async _applyParameterValues(
623
+ parameterEntries: ParameterEntry[],
624
+ assetContainer?: AbstractScene
625
+ ): Promise<void> {
626
+ const tagParamEntries = parameterEntries.filter(entry => isTagParameterSubject(entry.subject));
627
+ const nonTagParamEntries = parameterEntries.filter(entry => !isTagParameterSubject(entry.subject));
628
+
629
+ for (const entry of tagParamEntries) {
630
+ await this._applyParameterValue(entry.subject, entry.parameterName, assetContainer);
631
+ }
632
+ for (const entry of nonTagParamEntries) {
633
+ await this._applyParameterValue(entry.subject, entry.parameterName, assetContainer);
634
+ }
635
+ }
636
+
637
+ /**
638
+ * Call parameter observer of desired parameter
639
+ *
640
+ * @param assetContainer Optionally add an "asset container", which actually represents a model in the
641
+ * {@link ModelManager}. Viewer scene is used if left empty.
642
+ */
643
+ protected async _applyParameterValue(
644
+ subject: ParameterSubject,
645
+ parameterName: ParameterName,
646
+ assetContainer?: AbstractScene
647
+ ): Promise<void> {
648
+ const parameterEntry = this._getEntry(subject, parameterName);
649
+ const observer = this._parameterObserver[parameterName];
650
+ if (!parameterEntry || !observer) {
651
+ return;
652
+ }
653
+
654
+ const nodes = this._getAffectedNodes(subject, assetContainer);
655
+ const materials = this._getAffectedMaterials(subject, assetContainer);
656
+
657
+ await observer({
658
+ subject,
659
+ newValue: parameterEntry.value,
660
+ oldValue: parameterEntry.oldValue,
661
+ nodes,
662
+ materials,
663
+ });
664
+ }
665
+
666
+ protected _getAffectedMaterials(subject: ParameterSubject, assetContainer?: AbstractScene): Material[] {
667
+ assetContainer = assetContainer ?? this.viewer.scene;
668
+ let materials: Material[] = [];
669
+
670
+ // materials have priority over tags
671
+ if (isMaterialParameterSubject(subject)) {
672
+ materials = assetContainer.materials.filter(material => material.name === subject.materialName);
673
+ if (materials.length > 1) {
674
+ console.warn(`Multiple materials for material name "${subject.materialName}" have been found`);
675
+ }
676
+ } else if (isTagParameterSubject(subject)) {
677
+ materials = assetContainer.materials.filter(material => hasTag(material, subject.tagName));
678
+ }
679
+
680
+ return materials;
681
+ }
682
+
683
+ protected _getAffectedNodes(subject: ParameterSubject, assetContainer?: AbstractScene): TransformNode[] {
684
+ assetContainer = assetContainer ?? this.viewer.scene;
685
+ const allNodes = [...assetContainer.meshes, ...assetContainer.transformNodes];
686
+ let nodes: TransformNode[] = [];
687
+
688
+ // nodes have priority over tags
689
+ if (isNodeParameterSubject(subject)) {
690
+ nodes = allNodes.filter(node => node.name === subject.nodeName);
691
+ if (nodes.length > 1) {
692
+ console.warn(`Multiple nodes for node name "${subject.nodeName}" have been found`);
693
+ }
694
+ } else if (isTagParameterSubject(subject)) {
695
+ nodes = allNodes.filter(node => hasTag(node, subject.tagName));
696
+ }
697
+
698
+ return nodes;
699
+ }
700
+
701
+ protected _getEntry(subject: ParameterSubject, parameterName: ParameterName): ParameterEntry | undefined {
702
+ const entriesOfSubject = this._getEntriesOfSubject(subject);
703
+ const entry = entriesOfSubject.find(entry => entry.parameterName === parameterName);
704
+
705
+ return entry;
706
+ }
707
+
708
+ protected _getEntriesOfSubject(subject: ParameterSubject): ParameterEntry[] {
709
+ const entries = this._parameterEntries.filter(entry => {
710
+ const nodeNameMatches =
711
+ isNodeParameterSubject(entry.subject) &&
712
+ isNodeParameterSubject(subject) &&
713
+ entry.subject.nodeName === subject.nodeName;
714
+
715
+ const materialNameMatches =
716
+ isMaterialParameterSubject(entry.subject) &&
717
+ isMaterialParameterSubject(subject) &&
718
+ entry.subject.materialName === subject.materialName;
719
+
720
+ const tagNameMatches =
721
+ isTagParameterSubject(entry.subject) &&
722
+ isTagParameterSubject(subject) &&
723
+ entry.subject.tagName === subject.tagName;
724
+
725
+ return nodeNameMatches || materialNameMatches || tagNameMatches;
726
+ });
727
+
728
+ return entries;
729
+ }
730
+ }