• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

graphty-org / graphty-element / 20514590651

26 Dec 2025 02:37AM UTC coverage: 70.559% (-0.3%) from 70.836%
20514590651

push

github

apowers313
ci: fix npm ci

9591 of 13363 branches covered (71.77%)

Branch coverage included in aggregate %.

25136 of 35854 relevant lines covered (70.11%)

6233.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.78
/src/cameras/TwoDInputController.ts
1
// TwoDInputController.ts
1✔
2
import {type Observer, PointerEventTypes, type PointerInfo, type PointerInfoPre} from "@babylonjs/core";
14✔
3
import Hammer from "hammerjs";
14!
4

5
import {TwoDCameraController, TwoDCameraControlsConfigType} from "./TwoDCameraController";
6

7
interface GestureSession {
8
    panX: number;
9
    panY: number;
10
    panStartX: number;
11
    panStartY: number;
12
    ortho: {
13
        left: number;
14
        right: number;
15
        top: number;
16
        bottom: number;
17
    };
18
    scale: number;
19
    rotation: number;
20
    startRotDeg: number;
21
}
22

23
/**
24
 * Handles user input for the 2D camera controller.
25
 * Processes mouse, touch, and keyboard input for 2D camera manipulation.
26
 */
27
export class InputController {
14✔
28
    private keyState: Record<string, boolean> = {};
14✔
29
    private gestureSession: GestureSession | null = null;
14✔
30
    private hammer: HammerManager | null = null;
14✔
31
    private enabled = false;
14✔
32

33
    // Store observable subscriptions for cleanup
34
    private pointerObserverHandle: Observer<PointerInfo> | null = null;
14✔
35
    private prePointerObserverHandle: Observer<PointerInfoPre> | null = null;
14✔
36

37
    private pointerDownHandler = (): void => {
14✔
38
        this.cam.canvas.focus();
5✔
39
    };
5✔
40

41
    private keyDownHandler = (e: KeyboardEvent): void => {
14✔
42
        this.keyState[e.key] = true;
21✔
43
    };
21✔
44

45
    private keyUpHandler = (e: KeyboardEvent): void => {
14✔
46
        this.keyState[e.key] = false;
9✔
47
    };
9✔
48

49
    /**
50
     * Creates a new InputController instance.
51
     * @param cam - The 2D camera controller to manipulate
52
     * @param canvas - The HTML canvas element to attach input listeners to
53
     * @param config - Configuration options for 2D camera controls
54
     */
55
    constructor(
14✔
56
        private cam: TwoDCameraController,
991✔
57
        private canvas: HTMLCanvasElement,
991✔
58
        private config: TwoDCameraControlsConfigType,
991✔
59
    ) {
991✔
60
        this.canvas.setAttribute("tabindex", "0"); // Make focusable
991✔
61
        // Note: setupMouse() and setupTouch() are called in enable() to allow re-enabling
62
        // after disable() removes the observers
63
    }
991✔
64

65
    private setupMouse(): void {
14✔
66
        let isPanning = false;
283✔
67
        let lastX = 0;
283✔
68
        let lastY = 0;
283✔
69

70
        this.pointerObserverHandle = this.cam.scene.onPointerObservable.add((pi) => {
283✔
71
            const e = pi.event as PointerEvent;
12✔
72

73
            switch (pi.type) {
12✔
74
                case PointerEventTypes.POINTERDOWN:
12✔
75
                    isPanning = true;
5✔
76
                    lastX = e.clientX;
5✔
77
                    lastY = e.clientY;
5✔
78
                    this.pointerDownHandler();
5✔
79
                    break;
5✔
80

81
                case PointerEventTypes.POINTERUP:
12✔
82
                    isPanning = false;
4✔
83
                    break;
4✔
84

85
                case PointerEventTypes.POINTERMOVE:
12✔
86
                    if (isPanning && e.buttons === 1) {
3✔
87
                        const orthoRight = this.cam.camera.orthoRight ?? 1;
3✔
88
                        const orthoLeft = this.cam.camera.orthoLeft ?? 1;
3✔
89
                        const orthoTop = this.cam.camera.orthoTop ?? 1;
3✔
90
                        const orthoBottom = this.cam.camera.orthoBottom ?? 1;
3✔
91
                        const scaleX = (orthoRight - orthoLeft) / this.cam.engine.getRenderWidth();
3✔
92
                        const scaleY = (orthoTop - orthoBottom) / this.cam.engine.getRenderHeight();
3✔
93

94
                        const dx = e.clientX - lastX;
3✔
95
                        const dy = e.clientY - lastY;
3✔
96

97
                        this.cam.pan(-dx * scaleX * this.config.mousePanScale, dy * scaleY * this.config.mousePanScale);
3✔
98

99
                        lastX = e.clientX;
3✔
100
                        lastY = e.clientY;
3✔
101
                    }
3✔
102

103
                    break;
3✔
104

105
                default:
12✔
106
                    break;
×
107
            }
12✔
108
        });
283✔
109

110
        this.prePointerObserverHandle = this.cam.scene.onPrePointerObservable.add((pi) => {
283✔
111
            const e = pi.event as WheelEvent;
24✔
112
            if (pi.type === PointerEventTypes.POINTERWHEEL) {
24✔
113
                const delta = e.deltaY > 0 ? this.config.mouseWheelZoomSpeed : 1 / this.config.mouseWheelZoomSpeed;
5✔
114
                this.cam.zoom(delta);
5✔
115
                e.preventDefault();
5✔
116
            }
5✔
117
        }, PointerEventTypes.POINTERWHEEL);
283✔
118
    }
283✔
119

120
    private setupTouch(): void {
14✔
121
        this.hammer = new Hammer.Manager(this.canvas);
283✔
122
        const pan = new Hammer.Pan({threshold: 0, pointers: 0});
283✔
123
        const pinch = new Hammer.Pinch();
283✔
124
        const rotate = new Hammer.Rotate();
283✔
125

126
        this.hammer.add([pan, pinch, rotate]);
283✔
127
        this.hammer.get("pinch").recognizeWith(this.hammer.get("rotate"));
283✔
128
        this.hammer.get("pan").requireFailure(this.hammer.get("pinch"));
283✔
129

130
        this.hammer.on("panstart pinchstart rotatestart", (ev) => {
283✔
131
            this.gestureSession = {
×
132
                panX: ev.center.x,
×
133
                panY: ev.center.y,
×
134
                panStartX: this.cam.camera.position.x,
×
135
                panStartY: this.cam.camera.position.y,
×
136
                ortho: {
×
137
                    left: this.cam.camera.orthoLeft ?? 1,
×
138
                    right: this.cam.camera.orthoRight ?? 1,
×
139
                    top: this.cam.camera.orthoTop ?? 1,
×
140
                    bottom: this.cam.camera.orthoBottom ?? 1,
×
141
                },
×
142
                scale: ev.scale || 1,
×
143
                rotation: this.cam.parent.rotation.z,
×
144
                startRotDeg: ev.rotation || 0,
×
145
            };
×
146
        });
283✔
147

148
        this.hammer.on("panmove pinchmove rotatemove", (ev) => {
283✔
149
            if (!this.gestureSession) {
×
150
                return;
×
151
            }
×
152

153
            const orthoRight = this.cam.camera.orthoRight ?? 1;
×
154
            const orthoLeft = this.cam.camera.orthoLeft ?? 1;
×
155
            const orthoTop = this.cam.camera.orthoTop ?? 1;
×
156
            const orthoBottom = this.cam.camera.orthoBottom ?? 1;
×
157
            const scaleX = (orthoRight - orthoLeft) / this.cam.engine.getRenderWidth();
×
158
            const scaleY = (orthoTop - orthoBottom) / this.cam.engine.getRenderHeight();
×
159

160
            const dx = ev.center.x - this.gestureSession.panX;
×
161
            const dy = ev.center.y - this.gestureSession.panY;
×
162

163
            this.cam.camera.position.x = this.gestureSession.panStartX - (dx * scaleX * this.config.touchPanScale);
×
164
            this.cam.camera.position.y = this.gestureSession.panStartY + (dy * scaleY * this.config.touchPanScale);
×
165

166
            const pinch = (ev.scale || 1) / this.gestureSession.scale;
×
167
            this.cam.camera.orthoLeft = this.gestureSession.ortho.left / pinch;
×
168
            this.cam.camera.orthoRight = this.gestureSession.ortho.right / pinch;
×
169
            this.cam.camera.orthoTop = this.gestureSession.ortho.top / pinch;
×
170
            this.cam.camera.orthoBottom = this.gestureSession.ortho.bottom / pinch;
×
171

172
            const rotRad = (-(ev.rotation - this.gestureSession.startRotDeg) * Math.PI) / 180;
×
173
            this.cam.parent.rotation.z = this.gestureSession.rotation + rotRad;
×
174
        });
283✔
175

176
        this.hammer.on("panend pinchend rotateend", () => {
283✔
177
            this.gestureSession = null;
×
178
        });
283✔
179
    }
283✔
180

181
    /**
182
     * Enables input handling by attaching event listeners.
183
     * Idempotent - can be called multiple times safely.
184
     */
185
    public enable(): void {
14✔
186
        if (this.enabled) {
283!
187
            return;
×
188
        }
×
189

190
        this.enabled = true;
283✔
191

192
        this.canvas.addEventListener("keydown", this.keyDownHandler);
283✔
193
        this.canvas.addEventListener("keyup", this.keyUpHandler);
283✔
194

195
        // Setup mouse and touch handlers (these are removed in disable())
196
        this.setupMouse();
283✔
197
        this.setupTouch();
283✔
198
    }
283✔
199

200
    /**
201
     * Disables input handling by removing event listeners.
202
     * Idempotent - can be called multiple times safely.
203
     */
204
    public disable(): void {
14✔
205
        if (!this.enabled) {
255!
206
            return;
1✔
207
        }
1✔
208

209
        this.enabled = false;
254✔
210

211
        this.canvas.removeEventListener("keydown", this.keyDownHandler);
254✔
212
        this.canvas.removeEventListener("keyup", this.keyUpHandler);
254✔
213

214
        // Remove scene observable subscriptions
215
        if (this.pointerObserverHandle !== null) {
254✔
216
            this.cam.scene.onPointerObservable.remove(this.pointerObserverHandle);
254✔
217
            this.pointerObserverHandle = null;
254✔
218
        }
254✔
219

220
        if (this.prePointerObserverHandle !== null) {
254✔
221
            this.cam.scene.onPrePointerObservable.remove(this.prePointerObserverHandle);
254✔
222
            this.prePointerObserverHandle = null;
254✔
223
        }
254✔
224

225
        // Destroy Hammer.js instance
226
        if (this.hammer) {
254✔
227
            this.hammer.destroy();
254✔
228
            this.hammer = null;
254✔
229
        }
254✔
230

231
        // Clear gesture session
232
        this.gestureSession = null;
254✔
233
    }
255✔
234

235
    /**
236
     * Applies keyboard input to camera velocity for inertia-based movement.
237
     * Processes WASD and arrow key presses.
238
     */
239
    public applyKeyboardInertia(): void {
14✔
240
        if (!this.enabled) {
4,020!
241
            return;
9✔
242
        }
9✔
243

244
        const v = this.cam.velocity;
4,011✔
245
        const c = this.config;
4,011✔
246

247
        if (this.keyState.w || this.keyState.ArrowUp) {
4,020!
248
            v.y += c.panAcceleration;
34✔
249
        }
34✔
250

251
        if (this.keyState.s || this.keyState.ArrowDown) {
4,020!
252
            v.y -= c.panAcceleration;
24✔
253
        }
24✔
254

255
        if (this.keyState.a || this.keyState.ArrowLeft) {
4,020!
256
            v.x -= c.panAcceleration;
24✔
257
        }
24✔
258

259
        if (this.keyState.d || this.keyState.ArrowRight) {
4,020!
260
            v.x += c.panAcceleration;
24✔
261
        }
24✔
262

263
        if (this.keyState["+"] || this.keyState["="]) {
4,020!
264
            v.zoom -= c.zoomFactorPerFrame;
22✔
265
        }
22✔
266

267
        if (this.keyState["-"] || this.keyState._) {
4,020!
268
            v.zoom += c.zoomFactorPerFrame;
11✔
269
        }
11✔
270

271
        if (this.keyState.q) {
4,020!
272
            v.rotate += c.rotateSpeedPerFrame;
11✔
273
        }
11✔
274

275
        if (this.keyState.e) {
4,020!
276
            v.rotate -= c.rotateSpeedPerFrame;
11✔
277
        }
11✔
278
    }
4,020✔
279

280
    /**
281
     * Updates camera based on keyboard input and applies inertia.
282
     * Should be called each frame in the render loop.
283
     */
284
    public update(): void {
14✔
285
        this.applyKeyboardInertia();
3,997✔
286
        this.cam.applyInertia();
3,997✔
287
    }
3,997✔
288
}
14✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc