Warning, /firebird/packages/firebird-ng/display/src/lib/services/game-controller.service.ts is written in an unsupported language. File is not indexed.
0001 import { Injectable } from '@angular/core';
0002 import * as THREE from "three";
0003 import {BehaviorSubject, Observable, Subject} from "rxjs";
0004 import {ThreeService} from "./three.service";
0005 import { ConfigService } from '@dexvis/app-features';
0006 import { USE_CONTROLLER_CONFIG } from '@dexvis/firebird-ng/api';
0007
0008
0009 export enum GamepadButtonIndexes {
0010 ButtonA = 0,
0011 ButtonB = 1,
0012 ButtonX = 2,
0013 ButtonY = 3,
0014 ButtonLB = 4,
0015 ButtonRB = 5,
0016 ButtonLT = 6,
0017 ButtonRT = 7,
0018 Select = 8,
0019 Start = 9,
0020 }
0021
0022 export class GamepadObservableButton {
0023 private onPressSubject = new Subject<boolean>();
0024 public onPress = this.onPressSubject.asObservable();
0025 public state: GamepadButton = {pressed: false, touched: false, value: 0};
0026
0027 constructor(public index: GamepadButtonIndexes) {
0028 }
0029
0030 updateState(newState: GamepadButton) {
0031 if(this.onPressSubject.observed) {
0032 if(newState.pressed != this.state.pressed) {
0033 this.onPressSubject.next(newState.pressed);
0034 }
0035 }
0036 this.state = newState;
0037 }
0038
0039 updateFromGamepadState(gamepad: Gamepad) {
0040 this.updateState(gamepad.buttons[this.index]);
0041 }
0042 }
0043
0044 @Injectable({
0045 providedIn: 'root'
0046 })
0047 export class GameControllerService {
0048
0049 private xAxisSubject = new BehaviorSubject<number>(0);
0050 public xAxisChanged = this.xAxisSubject.asObservable();
0051 public xAxis = 0;
0052
0053 private yAxisSubject = new BehaviorSubject<number>(0);
0054 private yAxisChanged = this.yAxisSubject.asObservable();
0055 public yAxis: number = 0;
0056 public buttons: GamepadButton[] = [];
0057 public prevButtons: GamepadButton[] = [];
0058
0059
0060 public buttonA: GamepadObservableButton = new GamepadObservableButton(GamepadButtonIndexes.ButtonA);
0061 public buttonB: GamepadObservableButton = new GamepadObservableButton(GamepadButtonIndexes.ButtonB);
0062 public buttonX: GamepadObservableButton = new GamepadObservableButton(GamepadButtonIndexes.ButtonX);
0063 public buttonY: GamepadObservableButton = new GamepadObservableButton(GamepadButtonIndexes.ButtonY);
0064 public buttonLB: GamepadObservableButton = new GamepadObservableButton(GamepadButtonIndexes.ButtonLB);
0065 public buttonRB: GamepadObservableButton = new GamepadObservableButton(GamepadButtonIndexes.ButtonRB);
0066 public buttonLT: GamepadObservableButton = new GamepadObservableButton(GamepadButtonIndexes.ButtonLT);
0067 public buttonRT: GamepadObservableButton = new GamepadObservableButton(GamepadButtonIndexes.ButtonRT);
0068 public buttonStart: GamepadObservableButton = new GamepadObservableButton(GamepadButtonIndexes.Start);
0069 public buttonSelect: GamepadObservableButton = new GamepadObservableButton(GamepadButtonIndexes.Select);
0070
0071 public activeGamepad: Gamepad|null = null;
0072 private frameCallbackRef: (() => void) | null = null;
0073 private isControllerEnabled: boolean = false;
0074
0075 animationLoopHandler () {
0076 // Only process if controller is enabled
0077 if (!this.isControllerEnabled) {
0078 return;
0079 }
0080
0081 const epsilon = 0.01;
0082 const gamepads = navigator.getGamepads();
0083 for (const gamepad of gamepads) {
0084 if (gamepad) {
0085
0086 this.activeGamepad = gamepad;
0087 // Example: Using left joystick to control OrbitControls
0088 // Axis 0: Left joystick horizontal (left/right)
0089 // Axis 1: Left joystick vertical (up/down)
0090 this.xAxis = gamepad.axes[0];
0091 this.yAxis = gamepad.axes[1];
0092
0093 if(Math.abs(this.xAxis - this.xAxisSubject.value) > epsilon) {
0094 this.xAxisSubject.next(this.xAxis);
0095 }
0096
0097 if(Math.abs(this.yAxis - this.yAxisSubject.value) > epsilon) {
0098 this.yAxisSubject.next(this.yAxis);
0099 }
0100
0101 const xAxis = gamepad.axes[0];
0102 const yAxis = gamepad.axes[1];
0103
0104 if (Math.abs(xAxis) > 0.1 || Math.abs(yAxis) > 0.1) {
0105 this.rotateCamera(xAxis, yAxis);
0106 }
0107
0108 // Zooming using buttons (X and A)
0109 const zoomInButton = gamepad.buttons[2]; // X button
0110 const zoomOutButton = gamepad.buttons[0]; // A button
0111
0112 if (zoomInButton.pressed) {
0113 this.zoom(0.99);
0114 }
0115
0116 if (zoomOutButton.pressed) {
0117 this.zoom(1.01);
0118 }
0119
0120 // Strafe functionality
0121 // B button for strafe left
0122 if (this.buttonB.state.pressed) {
0123 this.strafe(-0.5);
0124 }
0125
0126 // RT button for strafe right
0127 if (this.buttonRT.state.pressed) {
0128 this.strafe(0.5);
0129 }
0130
0131 // Default view on LT button press
0132 if (this.buttonLT.state.pressed) {
0133 this.resetToDefaultView();
0134 }
0135
0136 this.buttonSelect.updateFromGamepadState(gamepad);
0137 this.buttonStart.updateFromGamepadState(gamepad);
0138
0139 this.buttonA.updateFromGamepadState(gamepad);
0140 this.buttonB.updateFromGamepadState(gamepad);
0141 this.buttonY.updateFromGamepadState(gamepad);
0142 this.buttonX.updateFromGamepadState(gamepad);
0143
0144 this.buttonLB.updateFromGamepadState(gamepad);
0145 this.buttonRB.updateFromGamepadState(gamepad);
0146 this.buttonLT.updateFromGamepadState(gamepad);
0147 this.buttonRT.updateFromGamepadState(gamepad);
0148
0149 break; // Only use the first connected gamepad
0150 }
0151 }
0152 };
0153
0154
0155 rotateCamera(xAxisChange: number, yAxisChange: number) {
0156 let orbitControls = this.three.controls;
0157 let camera = this.three.camera;
0158
0159 const offset = new THREE.Vector3(); // Offset of the camera from the target
0160 const quat = new THREE.Quaternion().setFromUnitVectors(camera.up, new THREE.Vector3(0, 1, 0));
0161 const quatInverse = quat.clone().invert();
0162
0163 const currentPosition = camera.position.clone().sub(orbitControls.target);
0164 currentPosition.applyQuaternion(quat); // Apply the quaternion
0165
0166 // Spherical coordinates
0167 const spherical = new THREE.Spherical().setFromVector3(currentPosition);
0168
0169 // Adjusting spherical coordinates
0170 spherical.theta -= xAxisChange * 0.023; // Azimuth angle change
0171 spherical.phi += yAxisChange * 0.023; // Polar angle change, for rotating up/down
0172
0173 // Ensure phi is within bounds to avoid flipping
0174 spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
0175
0176 // Convert back to Cartesian coordinates
0177 const newPostion = new THREE.Vector3().setFromSpherical(spherical);
0178 newPostion.applyQuaternion(quatInverse);
0179
0180 camera.position.copy(newPostion.add(orbitControls.target));
0181 camera.lookAt(orbitControls.target);
0182 orbitControls.update();
0183 }
0184
0185 zoom(factor: number) {
0186 let orbitControls = this.three.controls;
0187 let camera = this.three.camera;
0188 orbitControls.object.position.subVectors(camera.position, orbitControls.target).multiplyScalar(factor).add(orbitControls.target);
0189 orbitControls.update();
0190 }
0191
0192 strafe(amount: number) {
0193 const camera = this.three.camera;
0194 const controls = this.three.controls;
0195
0196 // Move along Z axis (right = positive Z, left = negative Z)
0197 const offset = new THREE.Vector3(0, 0, amount * 10); // Scale for reasonable movement
0198
0199 // Move camera and target together
0200 camera.position.add(offset);
0201 controls.target.add(offset);
0202 controls.update();
0203 }
0204
0205 resetToDefaultView() {
0206 const camera = this.three.camera;
0207 const controls = this.three.controls;
0208
0209 // Reset camera to default position
0210 camera.position.set(-7000, 0, 0);
0211 controls.target.set(0, 0, 0);
0212
0213 // Update controls
0214 controls.update();
0215
0216 console.log('[GameController] Camera reset to default view');
0217 }
0218
0219 constructor(
0220 private three: ThreeService,
0221 private localStorageService: ConfigService
0222 ) {
0223 // The config page's "Use controller" toggle writes this key.
0224 const useController = this.localStorageService.declare(USE_CONTROLLER_CONFIG);
0225 this.isControllerEnabled = useController.value;
0226
0227 // Create the callback reference
0228 this.frameCallbackRef = () => { this.animationLoopHandler(); };
0229
0230 // Only attach handler if controller is enabled
0231 if (this.isControllerEnabled) {
0232 this.three.addFrameCallback(this.frameCallbackRef);
0233 console.log('[GameController] Controller enabled on initialization');
0234 }
0235
0236 // Subscribe to changes in the use controller setting (root singleton: lives with the app)
0237 useController.changes$.subscribe((enabled) => {
0238 this.setControllerEnabled(enabled);
0239 });
0240
0241 this.xAxisChanged.subscribe((data)=>{
0242 if (this.isControllerEnabled) {
0243 console.log(`[joystick] x: ${data}`);
0244 }
0245 });
0246 this.yAxisChanged.subscribe((data)=>{
0247 if (this.isControllerEnabled) {
0248 console.log(`[joystick] y: ${data}`);
0249 }
0250 });
0251 }
0252
0253 private setControllerEnabled(enabled: boolean) {
0254 if (this.isControllerEnabled === enabled) {
0255 return;
0256 }
0257
0258 this.isControllerEnabled = enabled;
0259
0260 if (enabled) {
0261 // Attach the frame callback
0262 if (this.frameCallbackRef) {
0263 this.three.addFrameCallback(this.frameCallbackRef);
0264 console.log('[GameController] Controller enabled');
0265 }
0266 } else {
0267 // Remove the frame callback
0268 if (this.frameCallbackRef) {
0269 this.three.removeFrameCallback(this.frameCallbackRef);
0270 console.log('[GameController] Controller disabled');
0271 }
0272 }
0273 }
0274 }