Просмотр исходного кода

Introduced PointerTracker and PointerSource

customisations
alemart 10 месяцев назад
Родитель
Сommit
e1d94a961f

+ 175
- 0
src/sources/pointer-source.ts Просмотреть файл

@@ -0,0 +1,175 @@
1
+/*
2
+ * encantar.js
3
+ * GPU-accelerated Augmented Reality for the web
4
+ * Copyright (C) 2022-2024 Alexandre Martins <alemartf(at)gmail.com>
5
+ *
6
+ * This program is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU Lesser General Public License as published
8
+ * by the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * This program is distributed in the hope that it will be useful,
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
+ * GNU Lesser General Public License for more details.
15
+ *
16
+ * You should have received a copy of the GNU Lesser General Public License
17
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
18
+ *
19
+ * pointer-source.ts
20
+ * Source of data of pointer-based input: mouse, touch, pen...
21
+ */
22
+
23
+import Speedy from 'speedy-vision';
24
+import { SpeedyPromise } from 'speedy-vision/types/core/speedy-promise';
25
+import { Source } from './source';
26
+import { Viewport } from '../core/viewport'
27
+import { Utils, Nullable } from '../utils/utils';
28
+
29
+
30
+/**
31
+ * Source of data of pointer-based input: mouse, touch, pen...
32
+ */
33
+export class PointerSource implements Source
34
+{
35
+    /** a queue of incoming pointer events */
36
+    private _queue: PointerEvent[];
37
+
38
+    /** the viewport linked to this source of data */
39
+    private _viewport: Nullable<Viewport>;
40
+
41
+
42
+
43
+    /**
44
+     * Constructor
45
+     */
46
+    constructor()
47
+    {
48
+        this._queue = [];
49
+        this._viewport = null;
50
+        this._onPointerEvent = this._onPointerEvent.bind(this);
51
+    }
52
+
53
+    /**
54
+     * A type-identifier of the source of data
55
+     * @internal
56
+     */
57
+    get _type(): string
58
+    {
59
+        return 'pointer-source';
60
+    }
61
+
62
+    /**
63
+     * Consume a pointer event
64
+     * @returns the next pointer event to be consumed, or null if there are none
65
+     * @internal
66
+     */
67
+    _consume(): PointerEvent | null
68
+    {
69
+        // producer-consumer mechanism
70
+        return this._queue.shift() || null;
71
+    }
72
+
73
+    /**
74
+     * Stats related to this source of data
75
+     * @internal
76
+     */
77
+    get _stats(): string
78
+    {
79
+        return 'pointer input';
80
+    }
81
+
82
+    /**
83
+     * Initialize this source of data
84
+     * @returns a promise that resolves as soon as this source of data is initialized
85
+     * @internal
86
+     */
87
+    _init(): SpeedyPromise<void>
88
+    {
89
+        Utils.log('Initializing PointerSource...');
90
+
91
+        // nothing to do yet; we need the viewport
92
+        return Speedy.Promise.resolve();
93
+    }
94
+
95
+    /**
96
+     * Release this source of data
97
+     * @returns a promise that resolves as soon as this source of data is released
98
+     * @internal
99
+     */
100
+    _release(): SpeedyPromise<void>
101
+    {
102
+        this._setViewport(null);
103
+        return Speedy.Promise.resolve();
104
+    }
105
+
106
+    /**
107
+     * Link a viewport to this source of data
108
+     * @param viewport possibly null
109
+     * @internal
110
+     */
111
+    _setViewport(viewport: Viewport | null): void
112
+    {
113
+        // unlink previous viewport, if any
114
+        if(this._viewport !== null) {
115
+            this._viewport.hud.container.style.removeProperty('pointer-events');
116
+            this._viewport._subContainer.style.removeProperty('pointer-events');
117
+            this._viewport.container.style.removeProperty('pointer-events');
118
+            this._viewport.canvas.style.removeProperty('pointer-events');
119
+            this._removeEventListeners(this._viewport.canvas);
120
+        }
121
+
122
+        // link new viewport, if any
123
+        if((this._viewport = viewport) !== null) {
124
+            this._addEventListeners(this._viewport.canvas);
125
+            this._viewport.canvas.style.pointerEvents = 'auto';
126
+            this._viewport.container.style.pointerEvents = 'none';
127
+            this._viewport._subContainer.style.pointerEvents = 'none';
128
+            this._viewport.hud.container.style.pointerEvents = 'none';
129
+
130
+            // Make HUD elements accept pointer events
131
+            for(const element of this._viewport.hud.container.children) {
132
+                const el = element as HTMLElement;
133
+                if(el.style.getPropertyValue('pointer-events') == '')
134
+                    el.style.pointerEvents = 'auto';
135
+            }
136
+        }
137
+    }
138
+
139
+    /**
140
+     * Event handler
141
+     * @param event
142
+     */
143
+    private _onPointerEvent(event: PointerEvent): void
144
+    {
145
+        this._queue.push(event);
146
+    }
147
+
148
+    /**
149
+     * Add event listeners
150
+     * @param canvas
151
+     */
152
+    private _addEventListeners(canvas: HTMLCanvasElement): void
153
+    {
154
+        canvas.addEventListener('pointerdown', this._onPointerEvent);
155
+        canvas.addEventListener('pointerup', this._onPointerEvent);
156
+        canvas.addEventListener('pointermove', this._onPointerEvent);
157
+        canvas.addEventListener('pointercancel', this._onPointerEvent);
158
+        canvas.addEventListener('pointerleave', this._onPointerEvent);
159
+        canvas.addEventListener('pointerenter', this._onPointerEvent);
160
+    }
161
+
162
+    /**
163
+     * Remove event listeners
164
+     * @param canvas
165
+     */
166
+    private _removeEventListeners(canvas: HTMLCanvasElement): void
167
+    {
168
+        canvas.removeEventListener('pointerenter', this._onPointerEvent);
169
+        canvas.removeEventListener('pointerleave', this._onPointerEvent);
170
+        canvas.removeEventListener('pointercancel', this._onPointerEvent);
171
+        canvas.removeEventListener('pointermove', this._onPointerEvent);
172
+        canvas.removeEventListener('pointerup', this._onPointerEvent);
173
+        canvas.removeEventListener('pointerdown', this._onPointerEvent);
174
+    }
175
+}

+ 13
- 0
src/sources/source-factory.ts Просмотреть файл

@@ -23,6 +23,7 @@
23 23
 import { VideoSource } from './video-source';
24 24
 import { CanvasSource } from './canvas-source';
25 25
 import { CameraSource, CameraSourceOptions } from './camera-source';
26
+import { PointerSource } from './pointer-source';
26 27
 
27 28
 
28 29
 /**
@@ -33,6 +34,7 @@ export class SourceFactory
33 34
     /**
34 35
      * Create a <video>-based source of data
35 36
      * @param video video element
37
+     * @returns a video source
36 38
      */
37 39
     static Video(video: HTMLVideoElement): VideoSource
38 40
     {
@@ -42,6 +44,7 @@ export class SourceFactory
42 44
     /**
43 45
      * Create a <canvas>-based source of data
44 46
      * @param canvas canvas element
47
+     * @returns a canvas source
45 48
      */
46 49
     static Canvas(canvas: HTMLCanvasElement): CanvasSource
47 50
     {
@@ -51,9 +54,19 @@ export class SourceFactory
51 54
     /**
52 55
      * Create a Webcam-based source of data
53 56
      * @param options optional options object
57
+     * @returns a camera source
54 58
      */
55 59
     static Camera(options: CameraSourceOptions = {}): CameraSource
56 60
     {
57 61
         return new CameraSource(options);
58 62
     }
63
+
64
+    /**
65
+     * Create a source of pointer-based input
66
+     * @returns a pointer source
67
+    */
68
+    static Pointer(): PointerSource
69
+    {
70
+        return new PointerSource();
71
+    }
59 72
 }

+ 359
- 0
src/trackers/pointer-tracker/pointer-tracker.ts Просмотреть файл

@@ -0,0 +1,359 @@
1
+/*
2
+ * encantar.js
3
+ * GPU-accelerated Augmented Reality for the web
4
+ * Copyright (C) 2022-2024 Alexandre Martins <alemartf(at)gmail.com>
5
+ *
6
+ * This program is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU Lesser General Public License as published
8
+ * by the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * This program is distributed in the hope that it will be useful,
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
+ * GNU Lesser General Public License for more details.
15
+ *
16
+ * You should have received a copy of the GNU Lesser General Public License
17
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
18
+ *
19
+ * pointer-tracker.ts
20
+ * Tracker of pointer-based input
21
+ */
22
+
23
+import Speedy from 'speedy-vision';
24
+import { SpeedyPromise } from 'speedy-vision/types/core/speedy-promise';
25
+import { Trackable, TrackerResult, TrackerOutput, Tracker } from '../tracker';
26
+import { PointerSource } from '../../sources/pointer-source';
27
+import { Vector2 } from '../../geometry/vector2';
28
+import { Utils, Nullable } from '../../utils/utils';
29
+import { IllegalOperationError } from '../../utils/errors';
30
+import { Session } from '../../core/session';
31
+import { Viewport } from '../../core/viewport';
32
+
33
+/**
34
+ * The possible phases of a TrackablePointer
35
+ */
36
+export type TrackablePointerPhase = 'began' | 'moved' | 'stationary' | 'ended' | 'canceled';
37
+
38
+/**
39
+ * A trackable representing an instance of pointer-based input such as a mouse
40
+ * click or a touch on the screen
41
+ */
42
+export interface TrackablePointer extends Trackable
43
+{
44
+    /** a unique identifier assigned to this trackable */
45
+    readonly id: number;
46
+
47
+    /** the phase of the trackable */
48
+    readonly phase: TrackablePointerPhase;
49
+
50
+    /** current position in normalized coordinates [-1,1]x[-1,1] */
51
+    readonly position: Vector2;
52
+
53
+    /** the first position, given in normalized coordinates */
54
+    readonly initialPosition: Vector2;
55
+
56
+    /** the type of the originating device; typically "mouse", "touch" or "pen" */
57
+    readonly type: string;
58
+}
59
+
60
+/**
61
+ * A result of a PointerTracker. It's meant to be consumed by the user/application
62
+ */
63
+export interface PointerTrackerResult extends TrackerResult
64
+{
65
+    /** the tracker that generated this result */
66
+    readonly tracker: PointerTracker;
67
+
68
+    /** the trackables */
69
+    readonly trackables: TrackablePointer[];
70
+}
71
+
72
+/**
73
+ * The output of a PointerTracker in a particular Frame of a Session
74
+ */
75
+export interface PointerTrackerOutput extends TrackerOutput
76
+{
77
+    /** tracker result to be consumed by the user */
78
+    readonly exports: PointerTrackerResult;
79
+}
80
+
81
+/** Convert event type to trackable pointer phase */
82
+const EVENTTYPE2PHASE: Record<string, TrackablePointerPhase> = {
83
+    'pointerdown': 'began',
84
+    'pointerup': 'ended',
85
+    'pointermove': 'moved',
86
+    'pointercancel': 'canceled',
87
+    'pointerleave': 'ended',
88
+    'pointerenter': 'began',
89
+};
90
+
91
+
92
+
93
+
94
+/**
95
+ * A tracker of pointer-based input such as mouse, touch or pen
96
+ */
97
+export class PointerTracker implements Tracker
98
+{
99
+    /** the source of data */
100
+    private _source: Nullable<PointerSource>;
101
+
102
+    /** the viewport */
103
+    private _viewport: Nullable<Viewport>;
104
+
105
+    /** active pointers */
106
+    private _activePointers: Map<number, TrackablePointer>;
107
+
108
+    /** new pointers */
109
+    private _newPointers: Map<number, TrackablePointer>;
110
+
111
+    /** last output */
112
+    private _lastOutput: PointerTrackerOutput;
113
+
114
+
115
+
116
+    /**
117
+     * Constructor
118
+     */
119
+    constructor()
120
+    {
121
+        this._source = null;
122
+        this._viewport = null;
123
+        this._activePointers = new Map();
124
+        this._newPointers = new Map();
125
+        this._lastOutput = this._generateOutput();
126
+    }
127
+
128
+    /**
129
+     * The type of the tracker
130
+     */
131
+    get type(): string
132
+    {
133
+        return 'pointer-tracker';
134
+    }
135
+
136
+    /**
137
+     * Initialize the tracker
138
+     * @param session
139
+     * @returns a promise that is resolved as soon as the tracker is initialized
140
+     * @internal
141
+     */
142
+    _init(session: Session): SpeedyPromise<void>
143
+    {
144
+        Utils.log('Initializing PointerTracker...');
145
+
146
+        // set the viewport
147
+        this._viewport = session.viewport;
148
+
149
+        // find the pointer source
150
+        for(const source of session.sources) {
151
+            if(source._type == 'pointer-source') {
152
+                this._source = source as PointerSource;
153
+                break;
154
+            }
155
+        }
156
+
157
+        if(this._source === null)
158
+            return Speedy.Promise.reject(new IllegalOperationError('A PointerTracker expects a PointerSource'));
159
+
160
+        // link the pointer source to the viewport
161
+        this._source._setViewport(this._viewport);
162
+
163
+        // done!
164
+        return Speedy.Promise.resolve();
165
+    }
166
+
167
+    /**
168
+     * Release the tracker
169
+     * @returns a promise that is resolved as soon as the tracker is released
170
+     * @internal
171
+     */
172
+    _release(): SpeedyPromise<void>
173
+    {
174
+        this._source = null;
175
+        this._viewport = null;
176
+        this._activePointers.clear();
177
+        this._newPointers.clear();
178
+
179
+        return Speedy.Promise.resolve();
180
+    }
181
+
182
+    /**
183
+     * Update the tracker (update cycle)
184
+     * @returns a promise that is resolved as soon as the tracker is updated
185
+     * @internal
186
+     */
187
+    _update(): SpeedyPromise<void>
188
+    {
189
+        const canvas = this._viewport!.canvas;
190
+        const rect = canvas.getBoundingClientRect(); // may be different in different frames!
191
+
192
+        // remove inactive trackables from the previous frame (update cycle)
193
+        const inactiveTrackables = this._findUnwantedTrackables();
194
+        for(let i = inactiveTrackables.length - 1; i >= 0; i--)
195
+            this._activePointers.delete(inactiveTrackables[i].id);
196
+
197
+        // make all active trackables stationary
198
+        this._activePointers.forEach((trackable, id) => {
199
+            this._activePointers.set(id, Object.assign({}, trackable, {
200
+                phase: 'stationary'
201
+            }));
202
+        });
203
+
204
+        // consume events
205
+        let event: Nullable<PointerEvent>;
206
+        while((event = this._source!._consume()) !== null) {
207
+
208
+            // sanity check
209
+            if(event.target !== canvas)
210
+                return Speedy.Promise.reject(new IllegalOperationError('Invalid PointerEvent target ' + event.target));
211
+            else if(!EVENTTYPE2PHASE.hasOwnProperty(event.type))
212
+                return Speedy.Promise.reject(new IllegalOperationError('Invalid PointerEvent type ' + event.type));
213
+
214
+            // determine the ID
215
+            // XXX different hardware devices acting simultaneously may produce
216
+            // events with the same pointerId - handling this seems overkill?
217
+            const id = event.pointerId;
218
+
219
+            // determine the previous states, if any, of the trackable
220
+            const previous = this._activePointers.get(id); // state in the previous frame
221
+            const current = this._newPointers.get(id); // previous state in the current frame
222
+
223
+            // determine the phase
224
+            const phase = EVENTTYPE2PHASE[event.type];
225
+
226
+            // new trackables always begin with a pointerdown event,
227
+            // or with a pointerenter event having buttons pressed
228
+            // (example: a mousemove without a previous mousedown isn't tracked)
229
+            if(!(event.type == 'pointerdown' || (event.type == 'pointerenter' && event.buttons > 0))) {
230
+                if(!previous && !current)
231
+                    continue; // discard event
232
+            }
233
+            else if(previous) {
234
+                // discard a 'began' after another 'began'
235
+                continue;
236
+            }
237
+            else if(event.button != 0 && event.pointerType == 'mouse') {
238
+                // require left mouse click
239
+                continue;
240
+            }
241
+
242
+            // discard event if 'began' and 'ended' happened in the same frame
243
+            // (difficult to reproduce, but it can be done ;)
244
+            if(!previous) {
245
+                if(phase == 'ended' || phase == 'canceled') {
246
+                    this._newPointers.delete(id);
247
+                    continue;
248
+                }
249
+            }
250
+
251
+            // what if we receive 'began' after 'ended' in the same frame?
252
+            else if(phase == 'began' && current) {
253
+                if(current.phase == 'ended' || current.phase == 'canceled') {
254
+                    this._newPointers.delete(id);
255
+                    continue;
256
+                }
257
+            }
258
+
259
+            // more special rules
260
+            switch(event.type) {
261
+                case 'pointermove':
262
+                    if(event.buttons == 0 || current?.phase == 'began')
263
+                        continue;
264
+                    break;
265
+
266
+                case 'pointerenter':
267
+                    if(event.buttons == 0 || previous?.phase == 'began' || current?.phase == 'began')
268
+                        continue;
269
+                    break;
270
+            }
271
+
272
+            // determine the current position
273
+            const absX = event.pageX - (rect.left + window.scrollX);
274
+            const absY = event.pageY - (rect.top + window.scrollY);
275
+            const relX = 2 * absX / rect.width - 1; // convert to [-1,1]
276
+            const relY = -(2 * absY / rect.height - 1); // flip Y axis
277
+            const position = new Vector2(relX, relY);
278
+
279
+            // determine the initial position
280
+            const initialPosition = previous ? previous.initialPosition :
281
+                                    Object.freeze(new Vector2()._copyFrom(position));
282
+
283
+            // determine the type of the originating device
284
+            const type = event.pointerType;
285
+
286
+            // we create new trackable instances on each frame;
287
+            // these will be exported and consumed by the user
288
+            this._newPointers.set(id, { id, phase, position, initialPosition, type });
289
+
290
+        }
291
+
292
+        // update trackables
293
+        this._newPointers.forEach((trackable, id) => this._activePointers.set(id, trackable));
294
+        this._newPointers.clear();
295
+
296
+        // generate output
297
+        this._lastOutput = this._generateOutput();
298
+
299
+        // test
300
+        //console.log(JSON.stringify(this._lastOutput.exports.trackables, null, 4));
301
+
302
+        // done!
303
+        return Speedy.Promise.resolve();
304
+    }
305
+
306
+    /**
307
+     * Output of the last frame
308
+     * @internal
309
+     */
310
+    get _output(): PointerTrackerOutput
311
+    {
312
+        return this._lastOutput;
313
+    }
314
+
315
+    /**
316
+     * Stats info
317
+     * @internal
318
+     */
319
+    get _stats(): string
320
+    {
321
+        const n = this._activePointers.size;
322
+        const s = n != 1 ? 's' : '';
323
+
324
+        return n + ' pointer' + s;
325
+    }
326
+
327
+    /**
328
+     * Generate tracker output
329
+     * @returns a new PointerTrackerOutput object
330
+     */
331
+    private _generateOutput(): PointerTrackerOutput
332
+    {
333
+        const trackables: TrackablePointer[] = [];
334
+        this._activePointers.forEach(trackable => trackables.push(trackable));
335
+
336
+        return {
337
+            exports: {
338
+                tracker: this,
339
+                trackables: trackables
340
+            }
341
+        };
342
+    }
343
+
344
+    /**
345
+     * Find trackables to remove
346
+     * @returns a list of trackables to remove
347
+     */
348
+    private _findUnwantedTrackables(): TrackablePointer[]
349
+    {
350
+        const trackables: TrackablePointer[] = [];
351
+
352
+        this._activePointers.forEach(trackable => {
353
+            if(trackable.phase == 'ended' || trackable.phase == 'canceled')
354
+                trackables.push(trackable);
355
+        });
356
+
357
+        return trackables;
358
+    }
359
+}

+ 9
- 0
src/trackers/tracker-factory.ts Просмотреть файл

@@ -21,6 +21,7 @@
21 21
  */
22 22
 
23 23
 import { ImageTracker } from './image-tracker/image-tracker';
24
+import { PointerTracker } from './pointer-tracker/pointer-tracker';
24 25
 
25 26
 /**
26 27
  * Tracker factory
@@ -34,4 +35,12 @@ export class TrackerFactory
34 35
     {
35 36
         return new ImageTracker();
36 37
     }
38
+
39
+    /**
40
+     * Create a Pointer Tracker
41
+     */
42
+    static Pointer(): PointerTracker
43
+    {
44
+        return new PointerTracker();
45
+    }
37 46
 }

Загрузка…
Отмена
Сохранить