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

graphty-org / graphty-element / 20390753610

20 Dec 2025 06:53AM UTC coverage: 82.423% (-1.2%) from 83.666%
20390753610

push

github

apowers313
Merge branch 'master' of https://github.com/graphty-org/graphty-element

5162 of 6088 branches covered (84.79%)

Branch coverage included in aggregate %.

24775 of 30233 relevant lines covered (81.95%)

6480.4 hits per line

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

82.55
/src/cameras/TwoDInputController.ts
1
// TwoDInputController.ts
2
import {type Observer, PointerEventTypes, type PointerInfo, type PointerInfoPre} from "@babylonjs/core";
2✔
3
import Hammer from "hammerjs";
2!
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
export class InputController {
2✔
24
    private keyState: Record<string, boolean> = {};
2✔
25
    private gestureSession: GestureSession | null = null;
2✔
26
    private hammer: HammerManager | null = null;
2✔
27
    private enabled = false;
2✔
28

29
    // Store observable subscriptions for cleanup
30
    private pointerObserverHandle: Observer<PointerInfo> | null = null;
2✔
31
    private prePointerObserverHandle: Observer<PointerInfoPre> | null = null;
2✔
32

33
    private pointerDownHandler = (): void => {
2✔
34
        this.cam.canvas.focus();
2✔
35
    };
2✔
36

37
    private keyDownHandler = (e: KeyboardEvent): void => {
2✔
38
        this.keyState[e.key] = true;
13✔
39
    };
13✔
40

41
    private keyUpHandler = (e: KeyboardEvent): void => {
2✔
42
        this.keyState[e.key] = false;
9✔
43
    };
9✔
44

45
    constructor(
2✔
46
        private cam: TwoDCameraController,
810✔
47
        private canvas: HTMLCanvasElement,
810✔
48
        private config: TwoDCameraControlsConfigType,
810✔
49
    ) {
810✔
50
        this.canvas.setAttribute("tabindex", "0"); // Make focusable
810✔
51
        // Note: setupMouse() and setupTouch() are called in enable() to allow re-enabling
52
        // after disable() removes the observers
53
    }
810✔
54

55
    private setupMouse(): void {
2✔
56
        let isPanning = false;
200✔
57
        let lastX = 0;
200✔
58
        let lastY = 0;
200✔
59

60
        this.pointerObserverHandle = this.cam.scene.onPointerObservable.add((pi) => {
200✔
61
            const e = pi.event as PointerEvent;
4✔
62

63
            switch (pi.type) {
4✔
64
                case PointerEventTypes.POINTERDOWN:
4✔
65
                    isPanning = true;
2✔
66
                    lastX = e.clientX;
2✔
67
                    lastY = e.clientY;
2✔
68
                    this.pointerDownHandler();
2✔
69
                    break;
2✔
70

71
                case PointerEventTypes.POINTERUP:
4✔
72
                    isPanning = false;
1✔
73
                    break;
1✔
74

75
                case PointerEventTypes.POINTERMOVE:
4✔
76
                    if (isPanning && e.buttons === 1) {
1✔
77
                        const orthoRight = this.cam.camera.orthoRight ?? 1;
1✔
78
                        const orthoLeft = this.cam.camera.orthoLeft ?? 1;
1✔
79
                        const orthoTop = this.cam.camera.orthoTop ?? 1;
1✔
80
                        const orthoBottom = this.cam.camera.orthoBottom ?? 1;
1✔
81
                        const scaleX = (orthoRight - orthoLeft) / this.cam.engine.getRenderWidth();
1✔
82
                        const scaleY = (orthoTop - orthoBottom) / this.cam.engine.getRenderHeight();
1✔
83

84
                        const dx = e.clientX - lastX;
1✔
85
                        const dy = e.clientY - lastY;
1✔
86

87
                        this.cam.pan(-dx * scaleX * this.config.mousePanScale, dy * scaleY * this.config.mousePanScale);
1✔
88

89
                        lastX = e.clientX;
1✔
90
                        lastY = e.clientY;
1✔
91
                    }
1✔
92

93
                    break;
1✔
94

95
                default:
4✔
96
                    break;
×
97
            }
4✔
98
        });
200✔
99

100
        this.prePointerObserverHandle = this.cam.scene.onPrePointerObservable.add((pi) => {
200✔
101
            const e = pi.event as WheelEvent;
1✔
102
            if (pi.type === PointerEventTypes.POINTERWHEEL) {
1✔
103
                const delta = e.deltaY > 0 ? this.config.mouseWheelZoomSpeed : 1 / this.config.mouseWheelZoomSpeed;
1✔
104
                this.cam.zoom(delta);
1✔
105
                e.preventDefault();
1✔
106
            }
1✔
107
        }, PointerEventTypes.POINTERWHEEL);
200✔
108
    }
200✔
109

110
    private setupTouch(): void {
2✔
111
        this.hammer = new Hammer.Manager(this.canvas);
200✔
112
        const pan = new Hammer.Pan({threshold: 0, pointers: 0});
200✔
113
        const pinch = new Hammer.Pinch();
200✔
114
        const rotate = new Hammer.Rotate();
200✔
115

116
        this.hammer.add([pan, pinch, rotate]);
200✔
117
        this.hammer.get("pinch").recognizeWith(this.hammer.get("rotate"));
200✔
118
        this.hammer.get("pan").requireFailure(this.hammer.get("pinch"));
200✔
119

120
        this.hammer.on("panstart pinchstart rotatestart", (ev) => {
200✔
121
            this.gestureSession = {
×
122
                panX: ev.center.x,
×
123
                panY: ev.center.y,
×
124
                panStartX: this.cam.camera.position.x,
×
125
                panStartY: this.cam.camera.position.y,
×
126
                ortho: {
×
127
                    left: this.cam.camera.orthoLeft ?? 1,
×
128
                    right: this.cam.camera.orthoRight ?? 1,
×
129
                    top: this.cam.camera.orthoTop ?? 1,
×
130
                    bottom: this.cam.camera.orthoBottom ?? 1,
×
131
                },
×
132
                scale: ev.scale || 1,
×
133
                rotation: this.cam.parent.rotation.z,
×
134
                startRotDeg: ev.rotation || 0,
×
135
            };
×
136
        });
200✔
137

138
        this.hammer.on("panmove pinchmove rotatemove", (ev) => {
200✔
139
            if (!this.gestureSession) {
×
140
                return;
×
141
            }
×
142

143
            const orthoRight = this.cam.camera.orthoRight ?? 1;
×
144
            const orthoLeft = this.cam.camera.orthoLeft ?? 1;
×
145
            const orthoTop = this.cam.camera.orthoTop ?? 1;
×
146
            const orthoBottom = this.cam.camera.orthoBottom ?? 1;
×
147
            const scaleX = (orthoRight - orthoLeft) / this.cam.engine.getRenderWidth();
×
148
            const scaleY = (orthoTop - orthoBottom) / this.cam.engine.getRenderHeight();
×
149

150
            const dx = ev.center.x - this.gestureSession.panX;
×
151
            const dy = ev.center.y - this.gestureSession.panY;
×
152

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

156
            const pinch = (ev.scale || 1) / this.gestureSession.scale;
×
157
            this.cam.camera.orthoLeft = this.gestureSession.ortho.left / pinch;
×
158
            this.cam.camera.orthoRight = this.gestureSession.ortho.right / pinch;
×
159
            this.cam.camera.orthoTop = this.gestureSession.ortho.top / pinch;
×
160
            this.cam.camera.orthoBottom = this.gestureSession.ortho.bottom / pinch;
×
161

162
            const rotRad = (-(ev.rotation - this.gestureSession.startRotDeg) * Math.PI) / 180;
×
163
            this.cam.parent.rotation.z = this.gestureSession.rotation + rotRad;
×
164
        });
200✔
165

166
        this.hammer.on("panend pinchend rotateend", () => {
200✔
167
            this.gestureSession = null;
×
168
        });
200✔
169
    }
200✔
170

171
    public enable(): void {
2✔
172
        if (this.enabled) {
200!
173
            return;
×
174
        }
×
175

176
        this.enabled = true;
200✔
177

178
        this.canvas.addEventListener("keydown", this.keyDownHandler);
200✔
179
        this.canvas.addEventListener("keyup", this.keyUpHandler);
200✔
180

181
        // Setup mouse and touch handlers (these are removed in disable())
182
        this.setupMouse();
200✔
183
        this.setupTouch();
200✔
184
    }
200✔
185

186
    public disable(): void {
2✔
187
        if (!this.enabled) {
172✔
188
            return;
1✔
189
        }
1✔
190

191
        this.enabled = false;
171✔
192

193
        this.canvas.removeEventListener("keydown", this.keyDownHandler);
171✔
194
        this.canvas.removeEventListener("keyup", this.keyUpHandler);
171✔
195

196
        // Remove scene observable subscriptions
197
        if (this.pointerObserverHandle !== null) {
171✔
198
            this.cam.scene.onPointerObservable.remove(this.pointerObserverHandle);
171✔
199
            this.pointerObserverHandle = null;
171✔
200
        }
171✔
201

202
        if (this.prePointerObserverHandle !== null) {
171✔
203
            this.cam.scene.onPrePointerObservable.remove(this.prePointerObserverHandle);
171✔
204
            this.prePointerObserverHandle = null;
171✔
205
        }
171✔
206

207
        // Destroy Hammer.js instance
208
        if (this.hammer) {
171✔
209
            this.hammer.destroy();
171✔
210
            this.hammer = null;
171✔
211
        }
171✔
212

213
        // Clear gesture session
214
        this.gestureSession = null;
171✔
215
    }
172✔
216

217
    public applyKeyboardInertia(): void {
2✔
218
        if (!this.enabled) {
2,558✔
219
            return;
1✔
220
        }
1✔
221

222
        const v = this.cam.velocity;
2,557✔
223
        const c = this.config;
2,557✔
224

225
        if (this.keyState.w || this.keyState.ArrowUp) {
2,558✔
226
            v.y += c.panAcceleration;
2✔
227
        }
2✔
228

229
        if (this.keyState.s || this.keyState.ArrowDown) {
2,558✔
230
            v.y -= c.panAcceleration;
2✔
231
        }
2✔
232

233
        if (this.keyState.a || this.keyState.ArrowLeft) {
2,558✔
234
            v.x -= c.panAcceleration;
2✔
235
        }
2✔
236

237
        if (this.keyState.d || this.keyState.ArrowRight) {
2,558✔
238
            v.x += c.panAcceleration;
2✔
239
        }
2✔
240

241
        if (this.keyState["+"] || this.keyState["="]) {
2,558✔
242
            v.zoom -= c.zoomFactorPerFrame;
2✔
243
        }
2✔
244

245
        if (this.keyState["-"] || this.keyState._) {
2,558✔
246
            v.zoom += c.zoomFactorPerFrame;
1✔
247
        }
1✔
248

249
        if (this.keyState.q) {
2,558✔
250
            v.rotate += c.rotateSpeedPerFrame;
1✔
251
        }
1✔
252

253
        if (this.keyState.e) {
2,558✔
254
            v.rotate -= c.rotateSpeedPerFrame;
1✔
255
        }
1✔
256
    }
2,558✔
257

258
    public update(): void {
2✔
259
        this.applyKeyboardInertia();
2,544✔
260
        this.cam.applyInertia();
2,544✔
261
    }
2,544✔
262
}
2✔
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