Skip to content

Commit

Permalink
Add library for interpreting 3d controller orientation as 2d vertical…
Browse files Browse the repository at this point in the history
… / horizontal movement
  • Loading branch information
ahbnr committed Aug 17, 2020
1 parent 8e8c070 commit bd12a0c
Show file tree
Hide file tree
Showing 15 changed files with 7,765 additions and 8 deletions.
9 changes: 9 additions & 0 deletions Controller/sensor-input-lib/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
7 changes: 7 additions & 0 deletions Controller/sensor-input-lib/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
build/
dist/
node_modules/
.snapshots/
*.min.js
coverage
jest.config.js
32 changes: 32 additions & 0 deletions Controller/sensor-input-lib/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
],
"extends": [
"standard",
"plugin:prettier/recommended",
"prettier/standard",
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended"
],
"env": {
"node": true
},
"parserOptions": {
"ecmaVersion": 2020,
"ecmaFeatures": {
"legacyDecorators": true,
"jsx": true
}
},
"settings": {
},
"rules": {
"space-before-function-paren": 0,
"import/export": 0,
"no-unused-vars": 0,
"no-unused-expressions": 0,
"no-inner-declarations": 0
}
}
22 changes: 22 additions & 0 deletions Controller/sensor-input-lib/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

# See https://help.github.com/ignore-files/ for more about ignoring files.

# dependencies
node_modules

# builds
build
dist
.rpt2_cache

# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
11 changes: 11 additions & 0 deletions Controller/sensor-input-lib/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"singleQuote": true,
"jsxSingleQuote": true,
"semi": true,
"tabWidth": 2,
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid",
"trailingComma": "es5",
"endOfLine":"auto"
}
4 changes: 4 additions & 0 deletions Controller/sensor-input-lib/.travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
language: node_js
node_js:
- 12
- 10
6 changes: 6 additions & 0 deletions Controller/sensor-input-lib/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// jest.config.js
module.exports = {
testMatch: [ "**/src/__tests__/**/*.[jt]s?(x)" ],
preset: 'ts-jest',
testEnvironment: 'node'
};
55 changes: 55 additions & 0 deletions Controller/sensor-input-lib/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "sensor-input-lib",
"version": "1.0.0",
"description": "Made with create-react-library",
"author": "team-p09",
"license": "MIT",
"repository": "",
"main": "dist/index.js",
"module": "dist/index.modern.js",
"source": "src/index.ts",
"engines": {
"node": ">=10"
},
"scripts": {
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"build": "npm run lint && microbundle-crl --no-compress --format modern,cjs",
"start": "microbundle-crl watch --no-compress --format modern,cjs",
"prepublish": "run-s build",
"test": "jest",
"predeploy": "cd example && npm install && npm run build",
"deploy": "gh-pages -d example/build"
},
"peerDependencies": {
"react": "^16.0.0"
},
"devDependencies": {
"@types/jest": "^25.2.3",
"@types/motion-sensors-polyfill": "^0.3.0",
"@typescript-eslint/eslint-plugin": "^2.34.0",
"@typescript-eslint/parser": "^2.34.0",
"babel-eslint": "^10.0.3",
"cross-env": "^7.0.2",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.7.0",
"eslint-config-standard": "^14.1.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-node": "^11.0.0",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"gh-pages": "^2.2.0",
"jest": "^26.0.1",
"microbundle-crl": "^0.13.10",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.4",
"ts-jest": "^26.0.0"
},
"files": [
"dist"
],
"dependencies": {
"autobind-decorator": "^2.4.0",
"motion-sensors-polyfill": "^0.3.1"
}
}
5 changes: 5 additions & 0 deletions Controller/sensor-input-lib/src/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"env": {
"jest": true
}
}
220 changes: 220 additions & 0 deletions Controller/sensor-input-lib/src/OrientationInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { boundClass } from 'autobind-decorator';
import { RelativeOrientationSensor } from 'motion-sensors-polyfill';

type Quaternion = [number, number, number, number];

interface TaitBryanAngle {
yaw: number;
pitch: number;
roll: number;
}

@boundClass
export class OrientationInput {
private static readonly PitchThreshold = 0.1;
private static readonly RollThreshold = 0.05;
private static readonly PitchMax = 0.2;
private static readonly RollMax = 0.3;

private sensor?: RelativeOrientationSensor;

private vertical: number = 0;
private horizontal: number = 0;
private rawQuaternion: Quaternion = [0, 0, 0, 0];
private rawAngle: TaitBryanAngle = {
yaw: 0,
pitch: 0,
roll: 0,
};

public onWarning: (message: string) => any = _ => {};
public onFatalError: (message: string) => any = _ => {};
public onActivated: () => any = () => {};
public onInputChange: (vertical: number, horizontal: number) => any = _ => {};

public start() {
(async () => {
if (await this.checkPermissions()) {
this.initSensor();
} else {
this.handleFatalError(
undefined,
'Could not gain required permissions.'
);
}
})();
}

public stop() {
this.sensor?.stop();
}

public getVertical(): number {
return this.vertical;
}

public getHorizontal(): number {
return this.horizontal;
}

public getRawAngle(): TaitBryanAngle {
return this.rawAngle;
}

public getRawQuaternion(): Quaternion {
return this.rawQuaternion;
}

private initSensor() {
this.sensor = new RelativeOrientationSensor({
frequency: 60,
referenceFrame: 'screen',
});

this.sensor.onerror = this.handleSensorError;
this.sensor.onreading = this.handleSensorReading;
this.sensor.onactivate = this.handleSensorActivation;

this.sensor.start();
}

handleSensorActivation() {
this.onActivated();
}

handleSensorError(event: Event) {
if (event instanceof ErrorEvent) {
if (event.error.name === 'NotReadableError') {
this.handleFatalError(event.error, 'Required sensors are not present.');
} else {
this.handleWarning(event.error);
}
} else {
this.handleWarning();
}
}

handleSensorReading(_: Event) {
if (this.sensor != null) {
this.rawQuaternion = this.sensor.quaternion;
this.rawAngle = OrientationInput.quaternionToTaitBryan(
this.rawQuaternion
);

const newVertical = OrientationInput.axisValueFromAngle(
this.rawAngle.roll,
OrientationInput.RollThreshold,
OrientationInput.RollMax
);
const newHorizontal = OrientationInput.axisValueFromAngle(
this.rawAngle.pitch,
OrientationInput.PitchThreshold,
OrientationInput.PitchMax
);

if (this.vertical !== newVertical || this.horizontal !== newHorizontal) {
this.vertical = newVertical;
this.horizontal = newHorizontal;

this.onInputChange(this.vertical, this.horizontal);
}
} else {
this.handleWarning(
undefined,
'Tried to process sensor reading, but sensor is not fully initialized. This should never happen and is a programming error.'
);
}
}

private handleFatalError(
error?: { message: string },
additionalMessage?: string
) {
if (additionalMessage != null) {
this.onFatalError(additionalMessage);
} else if (error != null) {
this.onFatalError(error.message);
} else {
this.onFatalError(
'Some fatal error occured and sensor input will not work correctly.'
);
}
}

private handleWarning(
error?: { message: string },
additionalMessage?: string
) {
if (additionalMessage != null) {
this.onWarning(additionalMessage);
} else if (error != null) {
this.onWarning(error.message);
} else {
this.onWarning('Some error occured.');
}
}

private async checkPermissions(): Promise<boolean> {
// Based on https://github.com/intel/generic-sensor-demos/tree/master/orientation-phone
if (navigator.permissions) {
// https://w3c.github.io/orientation-sensor/#model
try {
const results = await Promise.all([
navigator.permissions.query({ name: 'accelerometer' }),
navigator.permissions.query({ name: 'magnetometer' }),
navigator.permissions.query({ name: 'gyroscope' }),
]);

return results.every(result => result.state === 'granted');
} catch (error) {
this.handleWarning(
error,
'Error when attempting to gain necessary permissions. Will still continue though.'
);
}
} else {
this.handleWarning(
undefined,
'No Permissions API. Will still continue though.'
);
}

return true;
}

private static quaternionToTaitBryan(q: Quaternion): TaitBryanAngle {
// https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Quaternion_to_Euler_Angles_Conversion
// Slightly adapted, so that roll is 0 when phone is flat on table with screen upwards
return {
yaw: Math.atan2(
2 * (q[0] * q[1] + q[2] * q[3]),
1 - 2 * (q[1] * q[1] + q[2] * q[2])
),
pitch: Math.asin(2 * (q[0] * q[2] - q[3] * q[1])),
roll: Math.atan2(
2 * (q[0] * q[3] + q[1] * q[2]),
2 * (q[2] * q[2] + q[3] * q[3]) - 1
),
};
}

private static axisValueFromAngle(
angle: number,
threshold: number,
max: number
): number {
const absAngle = Math.abs(angle);

if (absAngle > threshold) {
if (angle > max) {
return -1.0; // negative, since mathematical positive rotation direction is anti-clockwise
} else if (angle < -max) {
return 1.0;
} else {
return (-Math.sign(angle) * (absAngle - threshold)) / (max - threshold);
}
} else {
return 0;
}
}
}
1 change: 1 addition & 0 deletions Controller/sensor-input-lib/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { OrientationInput } from './OrientationInput';
Loading

0 comments on commit bd12a0c

Please sign in to comment.