@combeenation/3d-viewer 19.1.0 → 20.0.0-alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/lib-cjs/buildinfo.json +1 -1
  2. package/dist/lib-cjs/commonjs.tsconfig.tsbuildinfo +1 -1
  3. package/dist/lib-cjs/index.d.ts +1 -1
  4. package/dist/lib-cjs/index.js +1 -1
  5. package/dist/lib-cjs/index.js.map +1 -1
  6. package/dist/lib-cjs/internal/animation-helper.d.ts +2 -0
  7. package/dist/lib-cjs/internal/animation-helper.js +16 -0
  8. package/dist/lib-cjs/internal/animation-helper.js.map +1 -0
  9. package/dist/lib-cjs/internal/cloning-helper.js +4 -2
  10. package/dist/lib-cjs/internal/cloning-helper.js.map +1 -1
  11. package/dist/lib-cjs/internal/export-helper.js +1 -1
  12. package/dist/lib-cjs/internal/export-helper.js.map +1 -1
  13. package/dist/lib-cjs/internal/math-helper.d.ts +9 -0
  14. package/dist/lib-cjs/internal/math-helper.js +43 -0
  15. package/dist/lib-cjs/internal/math-helper.js.map +1 -0
  16. package/dist/lib-cjs/internal/{paintable-helper.d.ts → parameter/paintable-parameter-helper.d.ts} +3 -3
  17. package/dist/lib-cjs/internal/{paintable-helper.js → parameter/paintable-parameter-helper.js} +26 -26
  18. package/dist/lib-cjs/internal/parameter/paintable-parameter-helper.js.map +1 -0
  19. package/dist/lib-cjs/internal/parameter/parameter-helper.d.ts +2 -0
  20. package/dist/lib-cjs/internal/parameter/parameter-helper.js +214 -0
  21. package/dist/lib-cjs/internal/parameter/parameter-helper.js.map +1 -0
  22. package/dist/lib-cjs/internal/{texture-parameter-helper.d.ts → parameter/texture-parameter-helper.d.ts} +1 -2
  23. package/dist/lib-cjs/internal/{texture-parameter-helper.js → parameter/texture-parameter-helper.js} +15 -16
  24. package/dist/lib-cjs/internal/parameter/texture-parameter-helper.js.map +1 -0
  25. package/dist/lib-cjs/manager/camera-manager.js +2 -9
  26. package/dist/lib-cjs/manager/camera-manager.js.map +1 -1
  27. package/dist/lib-cjs/manager/debug-manager.d.ts +5 -1
  28. package/dist/lib-cjs/manager/debug-manager.js +21 -5
  29. package/dist/lib-cjs/manager/debug-manager.js.map +1 -1
  30. package/dist/lib-cjs/manager/model-manager.d.ts +35 -4
  31. package/dist/lib-cjs/manager/model-manager.js +50 -8
  32. package/dist/lib-cjs/manager/model-manager.js.map +1 -1
  33. package/dist/lib-cjs/manager/parameter-manager.d.ts +37 -20
  34. package/dist/lib-cjs/manager/parameter-manager.js +109 -204
  35. package/dist/lib-cjs/manager/parameter-manager.js.map +1 -1
  36. package/dist/lib-cjs/utils/viewer-utils.d.ts +35 -0
  37. package/dist/lib-cjs/utils/viewer-utils.js +112 -0
  38. package/dist/lib-cjs/utils/viewer-utils.js.map +1 -0
  39. package/dist/lib-cjs/viewer.d.ts +13 -1
  40. package/dist/lib-cjs/viewer.js +24 -0
  41. package/dist/lib-cjs/viewer.js.map +1 -1
  42. package/package.json +13 -12
  43. package/src/index.ts +1 -1
  44. package/src/internal/animation-helper.ts +18 -0
  45. package/src/internal/cloning-helper.ts +4 -2
  46. package/src/internal/export-helper.ts +1 -2
  47. package/src/internal/math-helper.ts +46 -0
  48. package/src/internal/{paintable-helper.ts → parameter/paintable-parameter-helper.ts} +35 -35
  49. package/src/internal/parameter/parameter-helper.ts +263 -0
  50. package/src/internal/{texture-parameter-helper.ts → parameter/texture-parameter-helper.ts} +13 -3
  51. package/src/manager/camera-manager.ts +2 -10
  52. package/src/manager/debug-manager.ts +35 -6
  53. package/src/manager/model-manager.ts +81 -13
  54. package/src/manager/parameter-manager.ts +138 -232
  55. package/src/utils/viewer-utils.ts +149 -0
  56. package/src/viewer.ts +29 -1
  57. package/dist/lib-cjs/internal/paintable-helper.js.map +0 -1
  58. package/dist/lib-cjs/internal/texture-parameter-helper.js.map +0 -1
  59. package/dist/lib-cjs/utils/node-utils.d.ts +0 -17
  60. package/dist/lib-cjs/utils/node-utils.js +0 -92
  61. package/dist/lib-cjs/utils/node-utils.js.map +0 -1
  62. package/src/utils/node-utils.ts +0 -112
@@ -0,0 +1,263 @@
1
+ import {
2
+ AbstractMesh,
3
+ Animation,
4
+ AnimationGroup,
5
+ BuiltInParameter,
6
+ Color3,
7
+ MaterialManager,
8
+ PBRMaterial,
9
+ ParameterManager,
10
+ StandardMaterial,
11
+ Vector3,
12
+ ViewerError,
13
+ ViewerErrorIds,
14
+ } from '../..';
15
+ import { playAnimationGroupOnce } from '../animation-helper';
16
+ import { getInternalMetadataValue, setInternalMetadataValue } from '../metadata-helper';
17
+
18
+ type AnimatableProperties = 'albedoColor' | 'position' | 'rotation' | 'scaling' | 'influence';
19
+
20
+ export function createBuiltInParameter(parameterManager: ParameterManager, materialManager: MaterialManager): void {
21
+ parameterManager.setParameterObserver(BuiltInParameter.Visible, async ({ nodes, newValue }) => {
22
+ const visible = ParameterManager.parseBoolean(newValue);
23
+
24
+ const observerProms = nodes.map(async node => {
25
+ const visibleNested = parameterManager.getNestedVisibilityParameterValueOfNode(node);
26
+ if (visibleNested) {
27
+ // if a mesh gets visible by this operation we have to activate the assigned material which is stored in the
28
+ // internal metadata
29
+ // => consider child meshes as well (CB-10143)
30
+ const activatedNodes = [node, ...node.getChildMeshes(false)];
31
+ const setMaterialProms = activatedNodes.map(async an => {
32
+ const anVisibleNested = parameterManager.getNestedVisibilityParameterValueOfNode(an);
33
+ const anDeferredMaterial = getInternalMetadataValue(an, 'deferredMaterial');
34
+ const anMaterialParamValue = parameterManager.getParameterValueOfNode(an, BuiltInParameter.Material) as
35
+ | string
36
+ | undefined;
37
+ // get latest requested material, either from (new) parameter value or from deferred material
38
+ const anRequestedMaterial = anMaterialParamValue ?? anDeferredMaterial;
39
+ // material observer might have been executed before visibility observer
40
+ // => skip the material application in this case, as if it would be done twice
41
+ const anMaterialToBeSet = getInternalMetadataValue(an, 'materialToBeSet');
42
+ if (anVisibleNested && anRequestedMaterial && !anMaterialToBeSet) {
43
+ await materialManager.setMaterialOnMesh(
44
+ anRequestedMaterial,
45
+ // this cast is fine, as deferred material can only be set on meshes
46
+ an as AbstractMesh
47
+ );
48
+ }
49
+ });
50
+ await Promise.all(setMaterialProms);
51
+ }
52
+
53
+ // set enabled state anyway
54
+ node.setEnabled(visible);
55
+ });
56
+ await Promise.all(observerProms);
57
+ });
58
+ parameterManager.setParameterObserver(BuiltInParameter.Material, async ({ nodes, newValue }) => {
59
+ const material = ParameterManager.parseString(newValue);
60
+
61
+ const observerProms = nodes.map(async node => {
62
+ if (!(node instanceof AbstractMesh)) {
63
+ throw new ViewerError({
64
+ id: ViewerErrorIds.InvalidParameterSubject,
65
+ message: `Material can't be set, as the target node "${node.name}" is not a mesh`,
66
+ });
67
+ }
68
+
69
+ // only set material if the mesh is visible, or gets visible by the parameter update
70
+ const visible = parameterManager.getNestedVisibilityParameterValueOfNode(node);
71
+ if (visible) {
72
+ // visibility observer might have been executed before material observer
73
+ // => skip the material application in this case, as if it would be done twice
74
+ const anMaterialToBeSet = getInternalMetadataValue(node, 'materialToBeSet');
75
+ if (!anMaterialToBeSet) {
76
+ await materialManager.setMaterialOnMesh(material, node);
77
+ }
78
+ } else {
79
+ setInternalMetadataValue(node, 'deferredMaterial', material);
80
+ }
81
+ });
82
+ await Promise.all(observerProms);
83
+ });
84
+ parameterManager.setParameterObserver(BuiltInParameter.Position, async ({ nodes, newValue, options }) => {
85
+ const position = ParameterManager.parseVector(newValue);
86
+
87
+ for (const node of nodes) {
88
+ if (options?.animationTimeMs) {
89
+ _animateParameter(
90
+ node,
91
+ 'position',
92
+ position,
93
+ `${BuiltInParameter.Position}.${node.name}`,
94
+ options.animationTimeMs
95
+ );
96
+ } else {
97
+ node.position = position;
98
+ }
99
+ }
100
+ });
101
+ parameterManager.setParameterObserver(BuiltInParameter.Rotation, async ({ nodes, newValue, options }) => {
102
+ const rotation = ParameterManager.parseRotation(newValue);
103
+
104
+ for (const node of nodes) {
105
+ if (options?.animationTimeMs) {
106
+ if (node.rotationQuaternion) {
107
+ // set initial rotation value, if rotation is defined via quaternion
108
+ // otherwise the animation would just jump to the end value, as there is no start value to interpolate
109
+ node.rotation = node.rotationQuaternion.toEulerAngles();
110
+ }
111
+ _animateParameter(
112
+ node,
113
+ 'rotation',
114
+ rotation,
115
+ `${BuiltInParameter.Rotation}.${node.name}`,
116
+ options.animationTimeMs
117
+ );
118
+ } else {
119
+ node.rotation = rotation;
120
+ }
121
+ }
122
+ });
123
+ parameterManager.setParameterObserver(BuiltInParameter.Scaling, async ({ nodes, newValue, options }) => {
124
+ const scaling = ParameterManager.parseVector(newValue);
125
+
126
+ for (const node of nodes) {
127
+ if (options?.animationTimeMs) {
128
+ _animateParameter(
129
+ node,
130
+ 'scaling',
131
+ scaling,
132
+ `${BuiltInParameter.Scaling}.${node.name}`,
133
+ options.animationTimeMs
134
+ );
135
+ } else {
136
+ node.scaling = scaling;
137
+ }
138
+ }
139
+ });
140
+ parameterManager.setParameterObserver(BuiltInParameter.Color, async ({ materials, newValue, options }) => {
141
+ const color = ParameterManager.parseColor(newValue);
142
+
143
+ for (const material of materials) {
144
+ const materialCls = material.getClassName();
145
+ switch (materialCls) {
146
+ case 'PBRMaterial':
147
+ if (options?.animationTimeMs) {
148
+ _animateParameter(
149
+ material as PBRMaterial,
150
+ 'albedoColor',
151
+ color.toLinearSpace(),
152
+ `${BuiltInParameter.Color}.${material.id}`,
153
+ options.animationTimeMs
154
+ );
155
+ } else {
156
+ (material as PBRMaterial).albedoColor = color.toLinearSpace();
157
+ }
158
+ break;
159
+ case 'StandardMaterial':
160
+ (material as StandardMaterial).diffuseColor = color;
161
+ break;
162
+ default:
163
+ throw new ViewerError({
164
+ id: ViewerErrorIds.InvalidParameterSubject,
165
+ message: `Setting color for material of instance "${materialCls}" not implemented`,
166
+ });
167
+ }
168
+ }
169
+ });
170
+ parameterManager.setParameterObserver(BuiltInParameter.Roughness, async ({ materials, newValue }) => {
171
+ const roughness = ParameterManager.parseNumber(newValue);
172
+
173
+ for (const material of materials) {
174
+ const materialCls = material.getClassName();
175
+ switch (materialCls) {
176
+ case 'PBRMaterial':
177
+ (material as PBRMaterial).roughness = roughness;
178
+ break;
179
+ case 'StandardMaterial':
180
+ (material as StandardMaterial).roughness = roughness;
181
+ break;
182
+ default:
183
+ throw new ViewerError({
184
+ id: ViewerErrorIds.InvalidParameterSubject,
185
+ message: `Setting rougness for material of instance "${materialCls}" not implemented`,
186
+ });
187
+ }
188
+ }
189
+ });
190
+ parameterManager.setParameterObserver(BuiltInParameter.Metallic, async ({ materials, newValue }) => {
191
+ const metallic = ParameterManager.parseNumber(newValue);
192
+
193
+ for (const material of materials) {
194
+ const materialCls = material.getClassName();
195
+ switch (materialCls) {
196
+ case 'PBRMaterial':
197
+ (material as PBRMaterial).metallic = metallic;
198
+ break;
199
+ default:
200
+ throw new ViewerError({
201
+ id: ViewerErrorIds.InvalidParameterSubject,
202
+ message: `Setting metallic for material of instance "${materialCls}" not implemented`,
203
+ });
204
+ }
205
+ }
206
+ });
207
+ parameterManager.setParameterObserver(BuiltInParameter.Influence, async ({ nodes, newValue, options }) => {
208
+ const morphTargetValues = ParameterManager.parseMorphTargets(newValue);
209
+
210
+ for (const morphTargetValue of morphTargetValues) {
211
+ for (const node of nodes) {
212
+ const target = node instanceof AbstractMesh && node.morphTargetManager?.getTargetByName(morphTargetValue.name);
213
+
214
+ if (target) {
215
+ if (options?.animationTimeMs) {
216
+ _animateParameter(
217
+ target,
218
+ 'influence',
219
+ morphTargetValue.value,
220
+ `${BuiltInParameter.Influence}.${node.name}.${target.name}`,
221
+ options.animationTimeMs
222
+ );
223
+ } else {
224
+ target.influence = morphTargetValue.value;
225
+ }
226
+ } else {
227
+ throw new ViewerError({
228
+ id: ViewerErrorIds.InvalidParameterSubject,
229
+ message: `Morph target couldn't be found, node: ${node.name} / morph target: ${morphTargetValue.name}`,
230
+ });
231
+ }
232
+ }
233
+ }
234
+ });
235
+ }
236
+
237
+ async function _animateParameter<T, K extends Extract<keyof T, AnimatableProperties>>(
238
+ target: T,
239
+ property: K,
240
+ value: T[K],
241
+ animationNameSuffix: string,
242
+ animationTimeMs: number
243
+ ): Promise<void> {
244
+ const framesPerSec = 100;
245
+ // extend if further `AnimatableProperties` with different types are added
246
+ const dataType =
247
+ value instanceof Vector3
248
+ ? Animation.ANIMATIONTYPE_VECTOR3
249
+ : value instanceof Color3
250
+ ? Animation.ANIMATIONTYPE_COLOR3
251
+ : Animation.ANIMATIONTYPE_FLOAT;
252
+
253
+ const animationGroup = new AnimationGroup(`${ParameterManager.PARAMETER_ANIMATION_NAME}.${animationNameSuffix}`);
254
+
255
+ const animation = new Animation(property, property, framesPerSec, dataType);
256
+ animation.setKeys([
257
+ { frame: 0, value: target[property] },
258
+ { frame: framesPerSec, value: value },
259
+ ]);
260
+ animationGroup.addTargetedAnimation(animation, target);
261
+
262
+ await playAnimationGroupOnce(animationGroup, framesPerSec, Math.max(animationTimeMs, 0));
263
+ }
@@ -1,6 +1,16 @@
1
- import { BaseTexture, Material, PBRMaterial, Scene, Texture, TextureManager, ViewerError, ViewerErrorIds } from '..';
2
- import { BuiltInParameter, ParameterManager } from '../manager/parameter-manager';
3
- import { embedAssets } from './svg-helper';
1
+ import {
2
+ BaseTexture,
3
+ BuiltInParameter,
4
+ Material,
5
+ PBRMaterial,
6
+ ParameterManager,
7
+ Scene,
8
+ Texture,
9
+ TextureManager,
10
+ ViewerError,
11
+ ViewerErrorIds,
12
+ } from '../..';
13
+ import { embedAssets } from '../svg-helper';
4
14
  import isSvg from 'is-svg';
5
15
 
6
16
  /**
@@ -11,6 +11,7 @@ import {
11
11
  ViewerErrorIds,
12
12
  ViewerEvent,
13
13
  } from '../index';
14
+ import { playAnimationGroupOnce } from '../internal/animation-helper';
14
15
  import { downloadFile } from '../internal/export-helper';
15
16
  import { nodeMatchesAnyCriteria } from '../internal/node-helper';
16
17
  import { createScreenshotCanvas, trimCanvas } from '../internal/screenshot-helper';
@@ -282,16 +283,7 @@ export class CameraManager {
282
283
  );
283
284
  }
284
285
 
285
- // animation is created for 1 second (100 frames), adjust speed ratio according to desired movement time
286
- const speedRatio = 1000 / durationMs;
287
-
288
- return new Promise<void>(resolve => {
289
- animationGroup.onAnimationGroupEndObservable.addOnce(() => {
290
- animationGroup.dispose();
291
- resolve();
292
- });
293
- animationGroup.start(false, speedRatio, 0, 100);
294
- });
286
+ await playAnimationGroupOnce(animationGroup, framesPerSec, durationMs);
295
287
  }
296
288
 
297
289
  /**
@@ -15,6 +15,7 @@ import {
15
15
  Viewer,
16
16
  ViewerEvent,
17
17
  } from '../index';
18
+ import { nodeHasLeftHandedScaling } from '../internal/math-helper';
18
19
 
19
20
  /**
20
21
  * Manager for debugging functionalities
@@ -39,7 +40,7 @@ export class DebugManager {
39
40
  }
40
41
 
41
42
  /**
42
- * Enables the Babylon.js [Inspector](https://doc.babylonjs.com/toolsAndResources/tools/inspector).\
43
+ * Enables the Babylon.js [Inspector](https://doc.babylonjs.com/legacy/inspector/).\
43
44
  * Due to the additional size of the inspector, this function is only available in "development" builds!
44
45
  *
45
46
  * @returns Signalizes if inspector could be loaded
@@ -109,7 +110,12 @@ The inspector can only be used in development builds.`);
109
110
  const factor = DebugManager._getWorldCoordinatesAxesUnifyFactor();
110
111
  this._axesViewer = new AxesViewer(utilityLayerScene, size * factor);
111
112
 
113
+ let isLeftHanded = true;
112
114
  if (node) {
115
+ // required for correct rotation when calling right after creating the node (see CB-11240)
116
+ // force=true might not be needed, but it's safer
117
+ node.computeWorldMatrix(true);
118
+
113
119
  // get the world rotation of the mesh and extract the x, y, z axes from that
114
120
  // NOTE: `node.getDirection` can't be used because it considers the scaling as well, resulting in distorted axes
115
121
  const worldRotMatrix = new Matrix();
@@ -129,13 +135,15 @@ The inspector can only be used in development builds.`);
129
135
  if (node.absoluteScaling.z < 0) {
130
136
  zAxis.scaleInPlace(-1);
131
137
  }
132
-
133
138
  this._axesViewer.update(node.absolutePosition, xAxis, yAxis, zAxis);
139
+
140
+ isLeftHanded = nodeHasLeftHandedScaling(node);
134
141
  }
135
142
 
136
143
  DebugManager._prepareAxisVisual('X', this._axesViewer.xAxis);
137
144
  DebugManager._prepareAxisVisual('Y', this._axesViewer.yAxis);
138
145
  DebugManager._prepareAxisVisual('Z', this._axesViewer.zAxis);
146
+ DebugManager._drawLabel(isLeftHanded ? 'LH' : 'RH', -0.05, '#DD0060', this._axesViewer.xAxis, 2);
139
147
  }
140
148
 
141
149
  public hideCoordinateSystem(): void {
@@ -181,7 +189,6 @@ The inspector can only be used in development builds.`);
181
189
  protected static _prepareAxisVisual(labelText: string, axisNode: TransformNode): void {
182
190
  // fixed values used for label size and distance
183
191
  const colorScale = 2; // used to multiply the original color scale of 0.5 - result is 1
184
- const labelSize = 0.05;
185
192
  const labelDistance = 0.4;
186
193
 
187
194
  // inherit the color from the arrow and adjust brightness scaling to be 1
@@ -191,8 +198,30 @@ The inspector can only be used in development builds.`);
191
198
 
192
199
  axisMaterial.emissiveColor = color;
193
200
 
201
+ DebugManager._drawLabel(labelText, labelDistance, color.toHexString(), axisNode);
202
+ }
203
+
204
+ /**
205
+ * More generic function for drawing a letter on a axis arrow, e.g. for drawing coordinate system letters
206
+ */
207
+ protected static _drawLabel(
208
+ labelText: string,
209
+ labelDistance: number,
210
+ color: string,
211
+ axisNode: TransformNode,
212
+ xScale = 1
213
+ ): void {
214
+ const labelHeight = 0.05;
215
+ const labelWidth = labelHeight * xScale;
216
+ const textureHeight = 1024;
217
+ const textureWidth = textureHeight * xScale;
218
+
194
219
  const utilityLayerScene = UtilityLayerRenderer.DefaultUtilityLayer.utilityLayerScene;
195
- const labelPlane = MeshBuilder.CreatePlane(`${labelText}_label`, { size: labelSize }, utilityLayerScene);
220
+ const labelPlane = MeshBuilder.CreatePlane(
221
+ `${labelText}_label`,
222
+ { width: labelWidth, height: labelHeight },
223
+ utilityLayerScene
224
+ );
196
225
  labelPlane.parent = axisNode;
197
226
  labelPlane.position = new Vector3(0, 0, labelDistance);
198
227
  // keep orientation of mesh, so that it is always visible, independent of the camera position
@@ -201,12 +230,12 @@ The inspector can only be used in development builds.`);
201
230
  // create GUI element
202
231
  const textElement = new TextBlock();
203
232
  textElement.text = labelText;
204
- textElement.color = color.toHexString();
233
+ textElement.color = color;
205
234
  textElement.fontSize = '100%';
206
235
  textElement.fontFamily = 'Arial';
207
236
  textElement.fontWeight = 'bold';
208
237
 
209
- const labelTexture = AdvancedDynamicTexture.CreateForMesh(labelPlane);
238
+ const labelTexture = AdvancedDynamicTexture.CreateForMesh(labelPlane, textureWidth, textureHeight);
210
239
  labelTexture.addControl(textElement);
211
240
  }
212
241
 
@@ -10,6 +10,7 @@ import {
10
10
  } from '../index';
11
11
  import { BaseAsset, loadAsset, prepareAssetForScene } from '../internal/asset-helper';
12
12
  import { cloneModelAssetContainer } from '../internal/cloning-helper';
13
+ import { flipHandednessOfNode, nodeHasLeftHandedScaling } from '../internal/math-helper';
13
14
  import { isArray } from 'lodash-es';
14
15
 
15
16
  export type ParsedDecalConfiguration = DecalConfiguration & { materialId?: string; tags?: string };
@@ -24,6 +25,11 @@ export type ModelVisibilityEntry = {
24
25
  visible: boolean;
25
26
  };
26
27
 
28
+ export type ModelData = {
29
+ assetContainer: AssetContainer;
30
+ rootNode?: TransformNode;
31
+ };
32
+
27
33
  /**
28
34
  * Callback for renaming nodes when cloning nodes in a model
29
35
  */
@@ -34,6 +40,12 @@ export type NodeNamingStrategy = (node: TransformNode, newModelName: string) =>
34
40
  export type TagNamingStrategy = (tag: string, newModelName: string) => string;
35
41
 
36
42
  export type ModelCloneOptions = {
43
+ /**
44
+ * Indicates if cloned model should be shown immediately
45
+ *
46
+ * Default: true
47
+ */
48
+ show?: boolean;
37
49
  /**
38
50
  * `true:` Creates "[InstancedMeshes](https://doc.babylonjs.com/typedoc/classes/BABYLON.InstancedMesh)" from meshes of
39
51
  * the base model instead of "independent" meshes. Most of the data, especially the geometry and material assignment
@@ -42,6 +54,21 @@ export type ModelCloneOptions = {
42
54
  * clones.
43
55
  */
44
56
  createInstance?: boolean;
57
+ /**
58
+ * Optionally set a parent node for the cloned model directly.\
59
+ * Coordinate system handedness of the cloned model root node will be adapted to fit the handedness of this parent
60
+ * node.
61
+ * - parentNode left handed (Babylon.js default): leave imported model root node as is
62
+ * - parentNode right handed (e.g. when adding to existing structure): flip handedness, resetting rotation to 0/0/0
63
+ * and scaling to 1/1/1
64
+ */
65
+ parentNode?: TransformNode;
66
+ /**
67
+ * Optinally overwrite name of root node.\
68
+ * This is especially useful for improving the readability of the node structure via the inspector.\
69
+ * This setting has priority over the `nodeNamingStrategy`.
70
+ */
71
+ rootNodeName?: string;
45
72
  nodeNamingStrategy?: NodeNamingStrategy;
46
73
  tagNamingStrategy?: TagNamingStrategy;
47
74
  };
@@ -252,12 +279,9 @@ export class ModelManager {
252
279
  * @param options additional options for the cloning procedure, like renaming algorithms
253
280
  * @returns asset container for further processing of the clone
254
281
  */
255
- public async cloneModel(
256
- name: string,
257
- newModelName: string,
258
- show: boolean = true,
259
- options?: ModelCloneOptions
260
- ): Promise<AssetContainer> {
282
+ public async cloneModel(name: string, newModelName: string, options?: ModelCloneOptions): Promise<ModelData> {
283
+ const { show, createInstance, parentNode, rootNodeName } = options ?? {};
284
+
261
285
  const sourceModel = this._getModel(name);
262
286
  if (!sourceModel) {
263
287
  throw new ViewerError({
@@ -286,15 +310,38 @@ export class ModelManager {
286
310
  state: 'loaded',
287
311
  assetContainer: cloneModelAssetContainer(sourceModel.assetContainer, newModelName, this.viewer.scene, options),
288
312
  isClone: true,
289
- sourceModelName: options?.createInstance ? sourceModel.name : undefined,
313
+ sourceModelName: createInstance ? sourceModel.name : undefined,
290
314
  };
291
315
  this._modelAssets[newModelName] = clonedModel;
292
316
 
293
- if (show) {
317
+ const rootNode = this._getRootNodeOfModel(clonedModel.assetContainer);
318
+ if (rootNode && rootNodeName) {
319
+ rootNode.name = rootNodeName;
320
+ }
321
+
322
+ if (show !== false) {
294
323
  await this._showModel(clonedModel);
324
+
325
+ if (parentNode && rootNode) {
326
+ const parentNodeIsLeftHanded = nodeHasLeftHandedScaling(parentNode);
327
+
328
+ // Freshly loaded (and cloned) models are left handed by default.
329
+ // If the parent node is right handed, we know that a conversion has already been done and the root node
330
+ // conversion node of the new model has to be reset.
331
+ //
332
+ // We expect a rotation of 0/0/0 and scaling 1/1/1 when following our default workflow, which is uploading GLBs
333
+ // on the platform.
334
+ // Still it's a generic solution and so we actually flip the current world matrix instead of applying these
335
+ // transformation values in a hardcoded way.
336
+ if (!parentNodeIsLeftHanded) {
337
+ flipHandednessOfNode(rootNode);
338
+ }
339
+
340
+ rootNode.parent = parentNode;
341
+ }
295
342
  }
296
343
 
297
- return clonedModel.assetContainer;
344
+ return { assetContainer: clonedModel.assetContainer, rootNode };
298
345
  }
299
346
 
300
347
  /**
@@ -335,11 +382,11 @@ export class ModelManager {
335
382
  }
336
383
 
337
384
  /**
338
- * Loads and returns the asset container of a certain model.\
385
+ * Loads the model and returns the asset container and root node of that model.\
339
386
  * This can be used to access the models content without having to add it to the scene.\
340
- * A typical use case is to clone or instantiate a node from a "library" model.
387
+ * A typical use case is to clone or instantiate a model or single node from a "library" model.
341
388
  */
342
- public async getAssetContainerOfModel(name: string): Promise<AssetContainer> {
389
+ public async getModelData(name: string): Promise<ModelData> {
343
390
  const model = this._getModel(name);
344
391
  if (!model) {
345
392
  throw new ViewerError({
@@ -358,7 +405,9 @@ export class ModelManager {
358
405
  await this._prepareModelForScene(model);
359
406
  }
360
407
 
361
- return model.assetContainer;
408
+ const rootNode = this._getRootNodeOfModel(model.assetContainer);
409
+
410
+ return { assetContainer: model.assetContainer, rootNode };
362
411
  }
363
412
 
364
413
  /**
@@ -465,4 +514,23 @@ export class ModelManager {
465
514
  model.assetContainer.removeFromScene();
466
515
  model.state = 'loaded';
467
516
  }
517
+
518
+ /**
519
+ * Returns the root node of a models asset container.\
520
+ * This can be useful to reposition an entire (cloned) model.\
521
+ * Function expects only one root `TransformNode` and will return `undefined` otherwise.
522
+ */
523
+ protected _getRootNodeOfModel(assetContainer: AssetContainer): TransformNode | undefined {
524
+ // `assetContainer.rootNodes` can also contain other `Node` types like lights or cameras, which we don't want here
525
+ const rootNodes = assetContainer.rootNodes.filter(node => node instanceof TransformNode) as TransformNode[];
526
+ if (rootNodes.length === 0) {
527
+ console.warn(`No root node found for associated model`);
528
+ return undefined;
529
+ } else if (rootNodes.length > 1) {
530
+ console.warn(`Mutltiple root nodes found for associated model`);
531
+ return undefined;
532
+ } else {
533
+ return rootNodes[0];
534
+ }
535
+ }
468
536
  }