@xeokit/xeokit-sdk 2.4.2-beta-13 → 2.4.2-beta-15

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@xeokit/xeokit-sdk",
3
- "version": "2.4.2-beta-13",
3
+ "version": "2.4.2-beta-15",
4
4
  "description": "Web Programming Toolkit for 3D/2D BIM and AEC Graphics",
5
5
  "module": "./dist/xeokit-sdk.es.js",
6
6
  "main": "./dist/xeokit-sdk.cjs.js",
@@ -4,23 +4,90 @@ import {Frustum, frustumIntersectsAABB3, setFrustum} from "../../viewer/scene/ma
4
4
 
5
5
  /**
6
6
  * Picks a {@link Viewer}'s {@link Entity}s with a canvas-space 2D marquee box.
7
+ *
8
+ * [<img src="https://xeokit.github.io/xeokit-sdk/assets/images/MarqueeSelect.gif">](https://xeokit.github.io/xeokit-sdk/examples/picking/#marqueePick_select)
9
+ *
10
+ * * [[Example 1: Select Objects with Marquee](https://xeokit.github.io/xeokit-sdk/examples/picking/#marqueePick_select)]
11
+ * * [[Example 2: View-Fit Objects with Marquee](https://xeokit.github.io/xeokit-sdk/examples/picking/#marqueePick_viewFit)]
12
+ *
13
+ * # Usage
14
+ *
15
+ * In the example below, we
16
+ *
17
+ * 1. Create a {@link Viewer}, arrange the {@link Camera}
18
+ * 2. Use an {@link XKTLoaderPlugin} to load a BIM model,
19
+ * 3. Create a {@link ObjectsKdTree3} to automatically index the `Viewer's` {@link Entity}s for fast spatial lookup,
20
+ * 4. Create a `MarqueePicker` to pick {@link Entity}s in the {@link Viewer}, using the {@link ObjectsKdTree3} to accelerate picking
21
+ * 5. Create a {@link MarqueePickerMouseControl} to perform the marquee-picking with the `MarqueePicker`, using mouse input to draw the marquee box on the `Viewer's` canvas.
22
+ *
23
+ * When the {@link MarqueePickerMouseControl} is active:
24
+ *
25
+ * * Long-click, drag and release on the canvas to define a marque box that picks {@link Entity}s.
26
+ * * Drag left-to-right to pick {@link Entity}s that intersect the box.
27
+ * * Drag right-to-left to pick {@link Entity}s that are fully inside the box.
28
+ * * On release, the `MarqueePicker` will fire a "picked" event with IDs of the picked {@link Entity}s, if any.
29
+ * * Handling that event, we mark the {@link Entity}s as selected.
30
+ * * Hold down CTRL to multi-pick.
31
+ *
32
+ * ````javascript
33
+ * import {
34
+ * Viewer,
35
+ * XKTLoaderPlugin,
36
+ * ObjectsKdTree3,
37
+ * MarqueePicker,
38
+ * MarqueePickerMouseControl
39
+ * } from "xeokit-sdk.es.js";
40
+ *
41
+ * // 1
42
+ *
43
+ * const viewer = new Viewer({
44
+ * canvasId: "myCanvas"
45
+ * });
46
+ *
47
+ * viewer.scene.camera.eye = [14.9, 14.3, 5.4];
48
+ * viewer.scene.camera.look = [6.5, 8.3, -4.1];
49
+ * viewer.scene.camera.up = [-0.28, 0.9, -0.3];
50
+ *
51
+ * // 2
52
+ *
53
+ * const xktLoader = new XKTLoaderPlugin(viewer);
54
+ *
55
+ * const sceneModel = xktLoader.load({
56
+ * id: "myModel",
57
+ * src: "../../assets/models/xkt/v8/ifc/HolterTower.ifc.xkt"
58
+ * });
59
+ *
60
+ * // 3
61
+ *
62
+ * const objectsKdTree3 = new ObjectsKdTree3({viewer});
63
+ *
64
+ * // 4
65
+ *
66
+ * const marqueePicker = new MarqueePicker({viewer, objectsKdTree3});
67
+ *
68
+ * // 5
69
+ *
70
+ * const marqueePickerMouseControl = new MarqueePickerMouseControl({marqueePicker});
71
+ *
72
+ * marqueePicker.on("clear", () => {
73
+ * viewer.scene.setObjectsSelected(viewer.scene.selectedObjectIds, false);
74
+ * });
75
+ *
76
+ * marqueePicker.on("picked", (objectIds) => {
77
+ * viewer.scene.setObjectsSelected(objectIds, true);
78
+ * });
79
+ *
80
+ * marqueePickerMouseControl.setActive(true);
81
+ * ````
82
+ *
83
+ * # Design Notes
84
+ *
85
+ * * The {@link ObjectsKdTree3} can be shared with any other components that want to use it to spatially search for {@link Entity}s.
86
+ * * The {@link MarqueePickerMouseControl} can be replaced with other types of controllers (i.e. touch), or used alongside them.
87
+ * * The `MarqueePicker` has no input handlers of its own, and provides an API through which to programmatically control marquee picking. By firing the "picked" events, `MarqueePicker` implements the *Blackboard Pattern*.
7
88
  */
8
89
  export class MarqueePicker extends Component {
9
90
 
10
- /**
11
- * Pick mode that picks {@link Entity}s that intersect the marquee box.
12
- *
13
- * @type {number}
14
- */
15
- static PICK_MODE_INTERSECTS = 0;
16
-
17
- /**
18
- * Pick mode that picks {@link Entity}s that are completely inside the marquee box.
19
- *
20
- * @type {number}
21
- */
22
- static PICK_MODE_INSIDE = 1;
23
-
24
91
  /**
25
92
  * Creates a MarqueePicker.
26
93
  *
@@ -269,3 +336,17 @@ export class MarqueePicker extends Component {
269
336
  }
270
337
  }
271
338
  }
339
+
340
+ /**
341
+ * Pick mode that picks {@link Entity}s that intersect the marquee box.
342
+ *
343
+ * @type {number}
344
+ */
345
+ MarqueePicker.PICK_MODE_INTERSECTS = 0;
346
+
347
+ /**
348
+ * Pick mode that picks {@link Entity}s that are completely inside the marquee box.
349
+ *
350
+ * @type {number}
351
+ */
352
+ MarqueePicker.PICK_MODE_INSIDE = 1;
@@ -4,6 +4,8 @@ import {MarqueePicker} from "./MarqueePicker";
4
4
  /**
5
5
  * Controls a {@link MarqueePicker} with mouse input.
6
6
  *
7
+ * See {@link MarqueePicker} for usage example.
8
+ *
7
9
  * When the MarqueePickerMouseControl is active:
8
10
  *
9
11
  * * Long-click, drag and release on the canvas to define a marque box that picks {@link Entity}s.
@@ -7,6 +7,8 @@ const kdTreeDimLength = new Float32Array(3);
7
7
  * Automatically indexes a {@link Viewer}'s {@link Entity}s in a 3D k-d tree
8
8
  * to support fast collision detection with 3D World-space axis-aligned boundaries (AABBs) and frustums.
9
9
  *
10
+ * See {@link MarqueePicker} for usage example.
11
+ *
10
12
  * An ObjectsKdTree3 is configured with a Viewer, and will then automatically
11
13
  * keep itself populated with k-d nodes that contain the Viewer's Entitys.
12
14
  *
@@ -69,14 +69,13 @@ class PickController {
69
69
 
70
70
  this._lastPickedEntityId = null;
71
71
 
72
- this._needFireEvents = false;
72
+ this._needFireEvents = 0;
73
73
  }
74
74
 
75
75
  /**
76
76
  * Immediately attempts a pick, if scheduled.
77
77
  */
78
78
  update() {
79
-
80
79
  if (!this._configs.pointerEnabled) {
81
80
  return;
82
81
  }
@@ -90,8 +89,6 @@ class PickController {
90
89
  this.snappedOrPicked = false;
91
90
  this.hoveredSnappedOrSurfaceOff = false;
92
91
 
93
- this._needFireEvents = false;
94
-
95
92
  const hasHoverSurfaceSubs = this._cameraControl.hasSubs("hoverSurface");
96
93
 
97
94
  if (this.scheduleSnapOrPick) {
@@ -104,7 +101,7 @@ class PickController {
104
101
  if (snapPickResult && snapPickResult.snappedWorldPos) {
105
102
  this.snapPickResult = snapPickResult;
106
103
  this.snappedOrPicked = true;
107
- this._needFireEvents = true;
104
+ this._needFireEvents++;
108
105
  } else {
109
106
  this.schedulePickSurface = true; // Fallback
110
107
  this.snapPickResult = null;
@@ -117,7 +114,7 @@ class PickController {
117
114
  if (pickResultCanvasPos[0] === this.pickCursorPos[0] && pickResultCanvasPos[1] === this.pickCursorPos[1]) {
118
115
  this.picked = true;
119
116
  this.pickedSurface = true;
120
- this._needFireEvents = hasHoverSurfaceSubs;
117
+ this._needFireEvents += hasHoverSurfaceSubs ? 1 : 0;
121
118
  this.schedulePickEntity = false;
122
119
  this.schedulePickSurface = false;
123
120
  if (this.scheduleSnapOrPick) {
@@ -137,7 +134,6 @@ class PickController {
137
134
  if (pickResultCanvasPos[0] === this.pickCursorPos[0] && pickResultCanvasPos[1] === this.pickCursorPos[1]) {
138
135
  this.picked = true;
139
136
  this.pickedSurface = false;
140
- this._needFireEvents = false;
141
137
  this.schedulePickEntity = false;
142
138
  this.schedulePickSurface = false;
143
139
  return;
@@ -158,10 +154,10 @@ class PickController {
158
154
  } else {
159
155
  this.pickedSurface = true;
160
156
  }
161
- this._needFireEvents = true;
157
+ this._needFireEvents++;
162
158
  } else if (this.scheduleSnapOrPick) {
163
159
  this.hoveredSnappedOrSurfaceOff = true;
164
- this._needFireEvents = true;
160
+ this._needFireEvents++;
165
161
  }
166
162
 
167
163
  } else { // schedulePickEntity == true
@@ -173,7 +169,7 @@ class PickController {
173
169
  if (this.pickResult) {
174
170
  this.picked = true;
175
171
  this.pickedSurface = false;
176
- this._needFireEvents = true;
172
+ this._needFireEvents++;
177
173
  }
178
174
  }
179
175
 
@@ -184,7 +180,7 @@ class PickController {
184
180
 
185
181
  fireEvents() {
186
182
 
187
- if (!this._needFireEvents) {
183
+ if (this._needFireEvents == 0) {
188
184
  return;
189
185
  }
190
186
 
@@ -254,10 +250,7 @@ class PickController {
254
250
 
255
251
  this.pickResult = null;
256
252
 
257
- this._needFireEvents = false;
258
- }
259
-
260
- destroy() {
253
+ this._needFireEvents = 0;
261
254
  }
262
255
  }
263
256
 
@@ -45,75 +45,78 @@ class MousePickHandler {
45
45
  }
46
46
  };
47
47
 
48
- canvas.addEventListener("mousemove", this._canvasMouseMoveHandler = (e) => {
48
+ const tickifiedMouseMoveFn = scene.tickify (
49
+ this._canvasMouseMoveHandler = (e) => {
50
+ if (!(configs.active && configs.pointerEnabled)) {
51
+ return;
52
+ }
49
53
 
50
- if (!(configs.active && configs.pointerEnabled)) {
51
- return;
52
- }
54
+ if (leftDown || rightDown) {
55
+ return;
56
+ }
53
57
 
54
- if (leftDown || rightDown) {
55
- return;
56
- }
58
+ const hoverSubs = cameraControl.hasSubs("hover");
59
+ const hoverEnterSubs = cameraControl.hasSubs("hoverEnter");
60
+ const hoverOutSubs = cameraControl.hasSubs("hoverOut");
61
+ const hoverOffSubs = cameraControl.hasSubs("hoverOff");
62
+ const hoverSurfaceSubs = cameraControl.hasSubs("hoverSurface");
63
+ const hoverSnapOrSurfaceSubs = cameraControl.hasSubs("hoverSnapOrSurface");
57
64
 
58
- const hoverSubs = cameraControl.hasSubs("hover");
59
- const hoverEnterSubs = cameraControl.hasSubs("hoverEnter");
60
- const hoverOutSubs = cameraControl.hasSubs("hoverOut");
61
- const hoverOffSubs = cameraControl.hasSubs("hoverOff");
62
- const hoverSurfaceSubs = cameraControl.hasSubs("hoverSurface");
63
- const hoverSnapOrSurfaceSubs = cameraControl.hasSubs("hoverSnapOrSurface");
65
+ if (hoverSubs || hoverEnterSubs || hoverOutSubs || hoverOffSubs || hoverSurfaceSubs || hoverSnapOrSurfaceSubs) {
64
66
 
65
- if (hoverSubs || hoverEnterSubs || hoverOutSubs || hoverOffSubs || hoverSurfaceSubs || hoverSnapOrSurfaceSubs) {
67
+ pickController.pickCursorPos = states.pointerCanvasPos;
68
+ pickController.schedulePickEntity = true;
69
+ pickController.schedulePickSurface = hoverSurfaceSubs;
70
+ pickController.scheduleSnapOrPick = hoverSnapOrSurfaceSubs
66
71
 
67
- pickController.pickCursorPos = states.pointerCanvasPos;
68
- pickController.schedulePickEntity = true;
69
- pickController.schedulePickSurface = hoverSurfaceSubs;
70
- pickController.scheduleSnapOrPick = hoverSnapOrSurfaceSubs
72
+ pickController.update();
71
73
 
72
- pickController.update();
74
+ if (pickController.pickResult) {
73
75
 
74
- if (pickController.pickResult) {
76
+ if (pickController.pickResult.entity) {
77
+ const pickedEntityId = pickController.pickResult.entity.id;
75
78
 
76
- if (pickController.pickResult.entity) {
77
- const pickedEntityId = pickController.pickResult.entity.id;
79
+ if (this._lastPickedEntityId !== pickedEntityId) {
78
80
 
79
- if (this._lastPickedEntityId !== pickedEntityId) {
81
+ if (this._lastPickedEntityId !== undefined) {
80
82
 
81
- if (this._lastPickedEntityId !== undefined) {
83
+ cameraControl.fire("hoverOut", { // Hovered off an entity
84
+ entity: scene.objects[this._lastPickedEntityId]
85
+ }, true);
86
+ }
87
+
88
+ cameraControl.fire("hoverEnter", pickController.pickResult, true); // Hovering over a new entity
82
89
 
83
- cameraControl.fire("hoverOut", { // Hovered off an entity
84
- entity: scene.objects[this._lastPickedEntityId]
85
- }, true);
90
+ this._lastPickedEntityId = pickedEntityId;
86
91
  }
92
+ }
87
93
 
88
- cameraControl.fire("hoverEnter", pickController.pickResult, true); // Hovering over a new entity
94
+ cameraControl.fire("hover", pickController.pickResult, true);
89
95
 
90
- this._lastPickedEntityId = pickedEntityId;
96
+ if (pickController.pickResult.worldPos || pickController.pickResult.snappedWorldPos) { // Hovering the surface of an entity
97
+ cameraControl.fire("hoverSurface", pickController.pickResult, true);
91
98
  }
92
- }
93
99
 
94
- cameraControl.fire("hover", pickController.pickResult, true);
100
+ } else {
95
101
 
96
- if (pickController.pickResult.worldPos || pickController.pickResult.snappedWorldPos) { // Hovering the surface of an entity
97
- cameraControl.fire("hoverSurface", pickController.pickResult, true);
98
- }
102
+ if (this._lastPickedEntityId !== undefined) {
99
103
 
100
- } else {
104
+ cameraControl.fire("hoverOut", { // Hovered off an entity
105
+ entity: scene.objects[this._lastPickedEntityId]
106
+ }, true);
101
107
 
102
- if (this._lastPickedEntityId !== undefined) {
108
+ this._lastPickedEntityId = undefined;
109
+ }
103
110
 
104
- cameraControl.fire("hoverOut", { // Hovered off an entity
105
- entity: scene.objects[this._lastPickedEntityId]
111
+ cameraControl.fire("hoverOff", { // Not hovering on any entity
112
+ canvasPos: pickController.pickCursorPos
106
113
  }, true);
107
-
108
- this._lastPickedEntityId = undefined;
109
114
  }
110
-
111
- cameraControl.fire("hoverOff", { // Not hovering on any entity
112
- canvasPos: pickController.pickCursorPos
113
- }, true);
114
115
  }
115
116
  }
116
- });
117
+ );
118
+
119
+ canvas.addEventListener("mousemove", tickifiedMouseMoveFn);
117
120
 
118
121
  canvas.addEventListener('mousedown', this._canvasMouseDownHandler = (e) => {
119
122
 
@@ -1175,23 +1175,31 @@ class Input extends Component {
1175
1175
  }
1176
1176
  });
1177
1177
 
1178
+ const tickifedMouseMoveFn = this.scene.tickify(
1179
+ () => this.fire("mousemove", this.mouseCanvasPos, true)
1180
+ );
1181
+
1178
1182
  this.element.addEventListener("mousemove", this._mouseMoveListener = (e) => {
1179
1183
  if (!this.enabled) {
1180
1184
  return;
1181
1185
  }
1182
1186
  this._getMouseCanvasPos(e);
1183
- this.fire("mousemove", this.mouseCanvasPos, true);
1187
+ tickifedMouseMoveFn();
1184
1188
  if (this.mouseover) {
1185
1189
  e.preventDefault();
1186
1190
  }
1187
1191
  });
1188
1192
 
1193
+ const tickifiedMouseWheelFn = this.scene.tickify(
1194
+ (delta) => { this.fire("mousewheel", delta, true); }
1195
+ );
1196
+
1189
1197
  this.element.addEventListener("wheel", this._mouseWheelListener = (e, d) => {
1190
1198
  if (!this.enabled) {
1191
1199
  return;
1192
1200
  }
1193
1201
  const delta = Math.max(-1, Math.min(1, -e.deltaY * 40));
1194
- this.fire("mousewheel", delta, true);
1202
+ tickifiedMouseWheelFn(delta);
1195
1203
  }, {passive: true});
1196
1204
 
1197
1205
  // mouseclicked
@@ -352,6 +352,11 @@ class Scene extends Component {
352
352
  throw "Mandatory config expected: valid canvasId or canvasElement";
353
353
  }
354
354
 
355
+ /**
356
+ * @type {{[key: string]: {wrapperFunc: Function, tickSubId: string}}}
357
+ */
358
+ this._tickifiedFunctions = {};
359
+
355
360
  const transparent = (!!cfg.transparent);
356
361
  const alphaDepthMask = (!!cfg.alphaDepthMask);
357
362
 
@@ -2562,6 +2567,63 @@ class Scene extends Component {
2562
2567
  return changed;
2563
2568
  }
2564
2569
 
2570
+ /**
2571
+ * This method will "tickify" the provided `cb` function.
2572
+ *
2573
+ * This means, the function will be wrapped so:
2574
+ *
2575
+ * - it runs time-aligned to scene ticks
2576
+ * - it runs maximum once per scene-tick
2577
+ *
2578
+ * @param {Function} cb The function to tickify
2579
+ * @returns {Function)}
2580
+ */
2581
+ tickify(cb) {
2582
+ const cbString = cb.toString();
2583
+
2584
+ /**
2585
+ * Check if the function is already tickified, and if so return the cached one.
2586
+ */
2587
+ if (cbString in this._tickifiedFunctions) {
2588
+ return this._tickifiedFunctions[cbString].wrapperFunc;
2589
+ }
2590
+
2591
+ let alreadyRun = 0;
2592
+ let needToRun = 0;
2593
+
2594
+ let lastArgs;
2595
+
2596
+ /**
2597
+ * The provided `cb` function is replaced with a "set-dirty" function
2598
+ *
2599
+ * @type {Function}
2600
+ */
2601
+ const wrapperFunc = function (...args) {
2602
+ lastArgs = args;
2603
+ needToRun++;
2604
+ };
2605
+
2606
+ /**
2607
+ * An each scene tick, if the "dirty-flag" is set, run the `cb` function.
2608
+ *
2609
+ * This will make it run time-aligned to the scene tick.
2610
+ */
2611
+ const tickSubId = this.on("tick", () => {
2612
+ const tmp = needToRun;
2613
+ if (tmp > alreadyRun) {
2614
+ alreadyRun = tmp;
2615
+ cb(...lastArgs);
2616
+ }
2617
+ });
2618
+
2619
+ /**
2620
+ * And, store the list of subscribers.
2621
+ */
2622
+ this._tickifiedFunctions[cbString] = { tickSubId, wrapperFunc };
2623
+
2624
+ return wrapperFunc;
2625
+ }
2626
+
2565
2627
  /**
2566
2628
  * Destroys this Scene.
2567
2629
  */