@preference-sl/pref-viewer 2.11.0-beta.2 → 2.11.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,52 +1,64 @@
1
1
  {
2
- "name": "@preference-sl/pref-viewer",
3
- "version": "2.11.0-beta.2",
4
- "description": "Web Component to preview GLTF models with Babylon.js",
5
- "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
- "scripts": {
7
- "release": "standard-version --releaseCommitMessageFormat \"chore(release): {{currentTag}} [skip ci]\"",
8
- "release:beta": "standard-version --prerelease beta --releaseCommitMessageFormat \"chore(release): {{currentTag}} [skip ci]\"",
9
- "build": "esbuild src/index.js --bundle --platform=node --outfile=dist/bundle.js --sourcemap",
10
- "start": "npm run build && http-server -c-1 . -p 8080",
11
- "test": "vitest"
12
- },
13
- "repository": {
14
- "type": "git",
15
- "url": "git+https://bitbucket.org/preferencesl/pref-viewer.git"
16
- },
17
- "publishConfig": {
18
- "access": "public"
19
- },
20
- "license": "MIT",
21
- "type": "module",
22
- "main": "src/index.js",
23
- "types": "index.d.ts",
24
- "exports": {
25
- ".": {
26
- "import": "./src/index.js",
27
- "require": "./src/index.js"
2
+ "name": "@preference-sl/pref-viewer",
3
+ "version": "2.11.0-beta.20",
4
+ "description": "Web Component to preview GLTF models with Babylon.js",
5
+ "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
+ "scripts": {
7
+ "build:prefweb": "webpack --config webpack.config.cjs --mode production",
8
+ "build:prefweb:dev": "webpack --config webpack.config.cjs --mode development",
9
+ "release": "standard-version --releaseCommitMessageFormat \"chore(release): {{currentTag}} [skip ci]\"",
10
+ "release:beta": "standard-version --prerelease beta --releaseCommitMessageFormat \"chore(release): {{currentTag}} [skip ci]\"",
11
+ "build": "esbuild src/index.js --bundle --platform=node --format=esm --outfile=dist/bundle.js --sourcemap",
12
+ "start": "npm run build && http-server -c-1 . -p 8080",
13
+ "test": "vitest"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://bitbucket.org/preferencesl/pref-viewer.git"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "license": "MIT",
23
+ "type": "module",
24
+ "main": "src/index.js",
25
+ "types": "index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "import": "./src/index.js",
29
+ "require": "./src/index.js"
30
+ }
31
+ },
32
+ "sideEffects": false,
33
+ "files": [
34
+ "src",
35
+ "index.d.ts"
36
+ ],
37
+ "dependencies": {
38
+ "@babylonjs/core": "^8.39.2",
39
+ "@babylonjs/loaders": "^8.39.2",
40
+ "@babylonjs/serializers": "^8.39.2",
41
+ "@panzoom/panzoom": "^4.6.0",
42
+ "babylonjs-gltf2interface": "^8.39.2",
43
+ "buffer": "^6.0.3",
44
+ "idb": "^8.0.3",
45
+ "is-svg": "^6.1.0",
46
+ "jszip": "^3.10.1",
47
+ "stream": "^0.0.3",
48
+ "string_decoder": "^1.3.0"
49
+ },
50
+ "devDependencies": {
51
+ "@babel/core": "^7.22.0",
52
+ "@babel/preset-env": "^7.22.0",
53
+ "babel-loader": "^9.2.1",
54
+ "clean-webpack-plugin": "^4.0.0",
55
+ "esbuild": "^0.25.10",
56
+ "http-server": "^14.1.1",
57
+ "jsdom": "^26.1.0",
58
+ "standard-version": "^9.5.0",
59
+ "terser-webpack-plugin": "^5.3.6",
60
+ "vitest": "^3.2.3",
61
+ "webpack": "^5.88.2",
62
+ "webpack-cli": "^5.1.4"
28
63
  }
29
- },
30
- "sideEffects": false,
31
- "files": [
32
- "src",
33
- "src/models",
34
- "index.d.ts"
35
- ],
36
- "dependencies": {
37
- "@babylonjs/core": "^8.31.3",
38
- "@babylonjs/loaders": "^8.31.3",
39
- "@babylonjs/serializers": "^8.31.3",
40
- "@panzoom/panzoom": "^4.6.0",
41
- "babylonjs-gltf2interface": "^8.31.3",
42
- "idb": "^8.0.3",
43
- "is-svg": "^6.1.0"
44
- },
45
- "devDependencies": {
46
- "esbuild": "^0.25.10",
47
- "http-server": "^14.1.1",
48
- "jsdom": "^26.1.0",
49
- "standard-version": "^9.5.0",
50
- "vitest": "^3.2.3"
51
- }
52
64
  }
@@ -0,0 +1,217 @@
1
+ import { Color3, HighlightLayer, Mesh, PickingInfo, Scene } from "@babylonjs/core";
2
+ import { PrefViewerColors } from "./styles.js";
3
+ import OpeningAnimation from "./babylonjs-animation-opening.js";
4
+
5
+ /**
6
+ * BabylonJSAnimationController - Manages animation playback, highlighting, and interactive controls for animated nodes in Babylon.js scenes.
7
+ *
8
+ * Summary:
9
+ * This class detects, groups, and manages opening/closing animations for scene nodes, provides interactive highlighting of animated nodes and their meshes, and displays a menu for animation control. It is designed for integration with product configurators and interactive 3D applications using Babylon.js.
10
+ *
11
+ * Key features:
12
+ * - Detects and groups opening/closing animations in the scene.
13
+ * - Tracks animated transformation nodes and their relationships to meshes.
14
+ * - Highlights animated nodes and their child meshes on pointer hover.
15
+ * - Displays and disposes the animation control menu for animated nodes.
16
+ * - Provides public API for highlighting, showing the animation menu, and disposing resources.
17
+ * - Cleans up all resources and observers to prevent memory leaks.
18
+ *
19
+ * Public Methods:
20
+ * - dispose(): Disposes all resources managed by the animation controller.
21
+ * - hightlightMeshes(pickingInfo): Highlights meshes that are children of an animated node when hovered.
22
+ * - hideMenu(): Hides and disposes the animation control menu if it exists.
23
+ * - showMenu(pickingInfo): Displays the animation control menu for the animated node under the pointer.
24
+ *
25
+ * @class
26
+ */
27
+ export default class BabylonJSAnimationController {
28
+ #scene = null;
29
+ #canvas = null;
30
+ #animatedNodes = [];
31
+ #highlightLayer = null;
32
+ #highlightColor = Color3.FromHexString(PrefViewerColors.primary);
33
+ #openingAnimations = [];
34
+
35
+ /**
36
+ * Creates a new BabylonJSAnimationController for a Babylon.js scene.
37
+ * @param {Scene} scene - The Babylon.js scene instance.
38
+ */
39
+ constructor(scene) {
40
+ this.#scene = scene;
41
+ this.#canvas = this.#scene._engine._renderingCanvas;
42
+ this.#initializeAnimations();
43
+ }
44
+
45
+ /**
46
+ * Detects and stores animatable objects and animated nodes in the scene.
47
+ * @private
48
+ */
49
+ #initializeAnimations() {
50
+ this.hideMenu(); // Clean up any existing menus
51
+ if (!this.#scene.animationGroups.length) {
52
+ return;
53
+ }
54
+ this.#getAnimatedNodes();
55
+ this.#getOpeningAnimations();
56
+ }
57
+
58
+ /**
59
+ * Collects the IDs of all nodes targeted by any animation group in the scene.
60
+ * @private
61
+ */
62
+ #getAnimatedNodes() {
63
+ this.#scene.animationGroups.forEach((animationGroup) => {
64
+ animationGroup.stop();
65
+ if (!animationGroup._targetedAnimations.length) {
66
+ return;
67
+ }
68
+ animationGroup._targetedAnimations.forEach((targetedAnimation) => {
69
+ if (!this.#animatedNodes.includes(targetedAnimation.target.id)) {
70
+ this.#animatedNodes.push(targetedAnimation.target.id);
71
+ }
72
+ });
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Groups opening and closing animations by node name and creates OpeningAnimation instances.
78
+ * @private
79
+ * @description
80
+ * Uses animation group names with the pattern "animation_open_<name>" and "animation_close_<name>".
81
+ */
82
+ #getOpeningAnimations() {
83
+ const openings = {};
84
+ this.#scene.animationGroups.forEach((animationGroup) => {
85
+ const match = animationGroup.name.match(/^animation_(open|close)_(.+)$/);
86
+ if (!match) {
87
+ return;
88
+ }
89
+ const [, type, openingName] = match;
90
+ if (!openings[openingName]) {
91
+ openings[openingName] = { name: openingName, animationOpen: null, animationClose: null };
92
+ }
93
+ if (type === "open") {
94
+ openings[openingName].animationOpen = animationGroup;
95
+ } else if (type === "close") {
96
+ openings[openingName].animationClose = animationGroup;
97
+ }
98
+ });
99
+
100
+ Object.values(openings).forEach((opening) => {
101
+ this.#openingAnimations.push(new OpeningAnimation(opening.name, opening.animationOpen, opening.animationClose));
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Finds the OpeningAnimation instance associated with a given node ID.
107
+ * @private
108
+ * @param {string} nodeId - The node identifier.
109
+ * @returns {OpeningAnimation|null} The matching OpeningAnimation instance or null.
110
+ */
111
+ #getOpeningAnimationByNode(nodeId) {
112
+ return this.#openingAnimations.find((openingAnimation) => openingAnimation.isAnimationForNode(nodeId));
113
+ }
114
+
115
+ /**
116
+ * Determines if a mesh belongs to a node targeted by an animation.
117
+ * @private
118
+ * @param {Mesh} mesh - The mesh to check.
119
+ * @returns {string|false} The animated node ID if found, otherwise false.
120
+ */
121
+ #getNodeAnimatedByMesh = function (mesh) {
122
+ let nodeId = false;
123
+ let node = mesh;
124
+ while (node.parent !== null && !nodeId) {
125
+ node = node.parent;
126
+ if (this.#animatedNodes.includes(node.id)) {
127
+ nodeId = node.id;
128
+ }
129
+ }
130
+ return nodeId;
131
+ };
132
+
133
+ /**
134
+ * ---------------------------
135
+ * Public methods
136
+ * ---------------------------
137
+ */
138
+
139
+ /**
140
+ * Disposes all resources managed by the animation controller.
141
+ * Cleans up the highlight layer, animation menu, and internal animation/node lists.
142
+ * Should be called when the controller is no longer needed to prevent memory leaks.
143
+ * @public
144
+ */
145
+ dispose() {
146
+ if (this.#highlightLayer) {
147
+ this.#highlightLayer.removeAllMeshes();
148
+ this.#highlightLayer.dispose();
149
+ this.#highlightLayer = null;
150
+ }
151
+ this.hideMenu();
152
+ this.#animatedNodes = [];
153
+ this.#openingAnimations.forEach((openingAnimation) => openingAnimation.dispose());
154
+ this.#openingAnimations = [];
155
+ }
156
+
157
+ /**
158
+ * Highlights meshes that are children of an animated node when hovered.
159
+ * @public
160
+ * @param {PickingInfo} pickingInfo - Raycast info from pointer position.
161
+ */
162
+ hightlightMeshes(pickingInfo) {
163
+ if (!this.#highlightLayer) {
164
+ this.#highlightLayer = new HighlightLayer("hl_animations", this.#scene);
165
+ }
166
+
167
+ this.#highlightLayer.removeAllMeshes();
168
+ if (!pickingInfo?.hit && !pickingInfo?.pickedMesh) {
169
+ return;
170
+ }
171
+
172
+ const nodeId = this.#getNodeAnimatedByMesh(pickingInfo.pickedMesh);
173
+ if (!nodeId) {
174
+ return;
175
+ }
176
+
177
+ const transformNode = this.#scene.getTransformNodeByID(nodeId);
178
+ const nodeMeshes = transformNode.getChildMeshes();
179
+ if (nodeMeshes.length) {
180
+ nodeMeshes.forEach((mesh) => this.#highlightLayer.addMesh(mesh, this.#highlightColor));
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Hides and disposes the animation control menu if it exists.
186
+ * @public
187
+ * @returns {void}
188
+ */
189
+ hideMenu() {
190
+ this.#openingAnimations.forEach((openingAnimation) => openingAnimation.hideControls());
191
+ this.#canvas.parentElement.querySelectorAll("div.pref-viewer-3d.animation-menu").forEach((menu) => menu.remove());
192
+ }
193
+
194
+ /**
195
+ * Displays the animation control menu for the animated node under the pointer.
196
+ * @public
197
+ * @param {PickingInfo} pickingInfo - Raycast info from pointer position.
198
+ */
199
+ showMenu(pickingInfo) {
200
+ if (!pickingInfo?.hit && !pickingInfo?.pickedMesh) {
201
+ return;
202
+ }
203
+
204
+ this.hideMenu();
205
+
206
+ const nodeId = this.#getNodeAnimatedByMesh(pickingInfo.pickedMesh);
207
+ if (!nodeId) {
208
+ return;
209
+ }
210
+ const openingAnimation = this.#getOpeningAnimationByNode(nodeId);
211
+ if (!openingAnimation) {
212
+ return;
213
+ }
214
+
215
+ openingAnimation.showControls(this.#canvas);
216
+ }
217
+ }