fix metrics gathering
parent
b6266a160e
commit
1bb248de1b
|
|
@ -26,6 +26,7 @@
|
||||||
"@angular/cli": "^18.0.6",
|
"@angular/cli": "^18.0.6",
|
||||||
"@angular/compiler-cli": "^18.0.0",
|
"@angular/compiler-cli": "^18.0.0",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"@types/three": "^0.180.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"jasmine-core": "~5.1.0",
|
"jasmine-core": "~5.1.0",
|
||||||
|
|
@ -36,6 +37,7 @@
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
|
"three": "^0.180.0",
|
||||||
"typescript": "~5.4.2"
|
"typescript": "~5.4.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -2553,6 +2555,13 @@
|
||||||
"node": ">=0.1.90"
|
"node": ">=0.1.90"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dimforge/rapier3d-compat": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@discoveryjs/json-ext": {
|
"node_modules/@discoveryjs/json-ext": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.1.tgz",
|
||||||
|
|
@ -4412,6 +4421,13 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tweenjs/tween.js": {
|
||||||
|
"version": "23.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||||
|
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.6",
|
"version": "1.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||||
|
|
@ -4629,6 +4645,29 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/stats.js": {
|
||||||
|
"version": "0.17.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||||
|
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/three": {
|
||||||
|
"version": "0.180.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz",
|
||||||
|
"integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||||
|
"@tweenjs/tween.js": "~23.1.3",
|
||||||
|
"@types/stats.js": "*",
|
||||||
|
"@types/webxr": "*",
|
||||||
|
"@webgpu/types": "*",
|
||||||
|
"fflate": "~0.8.2",
|
||||||
|
"meshoptimizer": "~0.22.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/uuid": {
|
"node_modules/@types/uuid": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
|
@ -4636,6 +4675,13 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/webxr": {
|
||||||
|
"version": "0.5.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.23.tgz",
|
||||||
|
"integrity": "sha512-GPe4AsfOSpqWd3xA/0gwoKod13ChcfV67trvxaW2krUbgb9gxQjnCx8zGshzMl8LSHZlNH5gQ8LNScsDuc7nGQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/wrap-ansi": {
|
"node_modules/@types/wrap-ansi": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz",
|
||||||
|
|
@ -4827,6 +4873,13 @@
|
||||||
"@xtuc/long": "4.2.2"
|
"@xtuc/long": "4.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@webgpu/types": {
|
||||||
|
"version": "0.1.64",
|
||||||
|
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.64.tgz",
|
||||||
|
"integrity": "sha512-84kRIAGV46LJTlJZWxShiOrNL30A+9KokD7RB3dRCIqODFjodS5tCD5yyiZ8kIReGVZSDfA3XkkwyyOIF6K62A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/@xtuc/ieee754": {
|
"node_modules/@xtuc/ieee754": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||||
|
|
@ -7193,6 +7246,13 @@
|
||||||
"node": ">=0.8.0"
|
"node": ">=0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
|
|
@ -9478,6 +9538,13 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/meshoptimizer": {
|
||||||
|
"version": "0.22.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz",
|
||||||
|
"integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/methods": {
|
"node_modules/methods": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
|
|
@ -13077,6 +13144,13 @@
|
||||||
"tslib": "^2"
|
"tslib": "^2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/three": {
|
||||||
|
"version": "0.180.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz",
|
||||||
|
"integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/thunky": {
|
"node_modules/thunky": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
"@angular/cli": "^18.0.6",
|
"@angular/cli": "^18.0.6",
|
||||||
"@angular/compiler-cli": "^18.0.0",
|
"@angular/compiler-cli": "^18.0.0",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"@types/three": "^0.180.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"jasmine-core": "~5.1.0",
|
"jasmine-core": "~5.1.0",
|
||||||
|
|
@ -38,6 +39,7 @@
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
|
"three": "^0.180.0",
|
||||||
"typescript": "~5.4.2"
|
"typescript": "~5.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { MetricsTrackerService } from '../../../../services/metrics-tracker.serv
|
||||||
templateUrl: './demographics-feedback.component.html',
|
templateUrl: './demographics-feedback.component.html',
|
||||||
styleUrls: ['./demographics-feedback.component.css']
|
styleUrls: ['./demographics-feedback.component.css']
|
||||||
})
|
})
|
||||||
export class DemographicsFeedbackComponent implements OnInit, OnDestroy {
|
export class DemographicsFeedbackComponent implements OnInit {
|
||||||
private metricsService = inject(MetricsTrackerService);
|
private metricsService = inject(MetricsTrackerService);
|
||||||
|
|
||||||
@Output() testComplete = new EventEmitter<void>();
|
@Output() testComplete = new EventEmitter<void>();
|
||||||
|
|
@ -88,7 +88,7 @@ export class DemographicsFeedbackComponent implements OnInit, OnDestroy {
|
||||||
submittedAt: new Date().toISOString()
|
submittedAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
this.metricsService.sendMetricsToServer('FullTestSuite', formData).subscribe({
|
this.metricsService.sendMetricsToServer(formData).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.isSubmitting = false;
|
this.isSubmitting = false;
|
||||||
this.isSubmitted = true;
|
this.isSubmitted = true;
|
||||||
|
|
@ -123,8 +123,4 @@ export class DemographicsFeedbackComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
return score * 2.5;
|
return score * 2.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.metricsService.resetMetrics();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDest
|
||||||
mv.addEventListener('ar-status', (e: any) => {
|
mv.addEventListener('ar-status', (e: any) => {
|
||||||
if (e.detail.status === 'session-started') {
|
if (e.detail.status === 'session-started') {
|
||||||
setTimeout(() => this.captureAnchor(), 500);
|
setTimeout(() => this.captureAnchor(), 500);
|
||||||
this.metricsService.startTracking(mv);
|
this.metricsService.startTracking(mv, "position");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,8 @@ export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDes
|
||||||
const mv = this.modelViewerRef.nativeElement;
|
const mv = this.modelViewerRef.nativeElement;
|
||||||
mv.addEventListener('ar-status', (event: any) => {
|
mv.addEventListener('ar-status', (event: any) => {
|
||||||
if (event.detail.status === 'session-started' && !this.isModelPlaced) {
|
if (event.detail.status === 'session-started' && !this.isModelPlaced) {
|
||||||
setTimeout(() => this.getInitialAnchor(), 1000);
|
setTimeout(() => this.getInitialAnchor(), 500);
|
||||||
this.metricsService.startTracking(mv);
|
this.metricsService.startTracking(mv, "stability");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +155,6 @@ export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDes
|
||||||
clearInterval(this.countdownInterval);
|
clearInterval(this.countdownInterval);
|
||||||
this.countdownInterval = null;
|
this.countdownInterval = null;
|
||||||
}
|
}
|
||||||
this.metricsService.resetMetrics();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
finishAssessment() {
|
finishAssessment() {
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,8 @@ export class TextLegibilityAssessmentComponent implements AfterViewInit, OnDestr
|
||||||
const mv = this.modelViewerRef.nativeElement;
|
const mv = this.modelViewerRef.nativeElement;
|
||||||
mv.addEventListener('ar-status', (event: any) => {
|
mv.addEventListener('ar-status', (event: any) => {
|
||||||
if (event.detail.status === 'session-started') {
|
if (event.detail.status === 'session-started') {
|
||||||
this.metricsService.startTracking(mv);
|
console.log('AR session started. Delaying startTracking call to ensure session is ready.');
|
||||||
|
this.metricsService.startTracking(mv, "text-legibility");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,257 +1,236 @@
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { inject, Injectable } from '@angular/core';
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { tap } from 'rxjs';
|
import { interval, Subscription, tap } from 'rxjs';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
export interface InteractionEvent {
|
export interface InteractionEvent {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
type: string;
|
type: string;
|
||||||
elementId?: string;
|
elementId?: string;
|
||||||
elementTag?: string;
|
elementTag?: string;
|
||||||
elementClasses?: string;
|
elementClasses?: string;
|
||||||
value?: any;
|
value?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface DeviceOrientation {
|
export interface DeviceOrientation {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
alpha: number | null;
|
alpha: number | null;
|
||||||
beta: number | null;
|
beta: number | null;
|
||||||
gamma: number | null;
|
gamma: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ArTrackingData {
|
export interface ArTrackingData {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
anchor: string | null;
|
modelIsVisible: boolean | null;
|
||||||
cameraPosition: { x: number; y: number; z: number; w?: number } | null;
|
modelWorldPosition: { x: number; y: number; z: number } | null;
|
||||||
cameraOrientation: { x: number; y: number; z: number; w: number } | null;
|
modelInitialScale: { x: number; y: number; z: number } | null;
|
||||||
cameraEuler: { pitch: number; yaw: number; roll: number } | null;
|
cameraFov: number | null;
|
||||||
cameraOrbit: { theta: number; phi: number; radius: number } | null;
|
cameraAspect: number | null;
|
||||||
cameraTarget: { x: number; y: number; z: number } | null;
|
viewerPosition?: { x: number; y: number; z: number; w: number } | null;
|
||||||
|
viewerOrientation?: { x: number; y: number; z: number; w: number } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeviceInformation {
|
||||||
|
screenWidth: number;
|
||||||
|
screenHeight: number;
|
||||||
|
devicePixelRatio: number;
|
||||||
|
userAgent: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MetricsLog {
|
export interface MetricsLog {
|
||||||
interactions: InteractionEvent[];
|
testName: string;
|
||||||
deviceOrientations: DeviceOrientation[];
|
interactions: InteractionEvent[];
|
||||||
arData: ArTrackingData[];
|
deviceOrientations: DeviceOrientation[];
|
||||||
|
arTrackingData: ArTrackingData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class MetricsTrackerService {
|
export class MetricsTrackerService {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
private serverUrl = '/api/log';
|
private serverUrl = '/api/log';
|
||||||
private deviceId: string;
|
private deviceId: string;
|
||||||
|
|
||||||
|
private allTestLogs: MetricsLog[] = [];
|
||||||
|
private deviceInfo: DeviceInformation | null = null;
|
||||||
|
private activeLog: MetricsLog | null = null;
|
||||||
|
|
||||||
private metricsLog: MetricsLog = {
|
private modelViewerElement: any = null;
|
||||||
interactions: [],
|
private trackingSubscription: Subscription | null = null;
|
||||||
deviceOrientations: [],
|
private lastDeviceOrientation: DeviceOrientationEvent | null = null;
|
||||||
arData: []
|
|
||||||
};
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.handleDeviceOrientation = this.handleDeviceOrientation.bind(this);
|
||||||
|
|
||||||
private xrFrameSubscriptionId: number | null = null;
|
const storedId = localStorage.getItem('device-uuid');
|
||||||
private lastDeviceOrientation: DeviceOrientationEvent | null = null;
|
this.deviceId = storedId || uuidv4();
|
||||||
private modelViewerElement: any = null;
|
if (!storedId) {
|
||||||
private xrFrameCount = 0;
|
localStorage.setItem('device-uuid', this.deviceId);
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.handleDeviceOrientation = this.handleDeviceOrientation.bind(this);
|
|
||||||
this.onXrFrame = this.onXrFrame.bind(this);
|
|
||||||
|
|
||||||
|
|
||||||
const storedId = localStorage.getItem('device-uuid');
|
|
||||||
this.deviceId = storedId || uuidv4();
|
|
||||||
if (!storedId) {
|
|
||||||
localStorage.setItem('device-uuid', this.deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.addEventListener('deviceorientation', this.handleDeviceOrientation, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
private handleDeviceOrientation(event: DeviceOrientationEvent): void {
|
window.addEventListener('deviceorientation', this.handleDeviceOrientation, true);
|
||||||
this.lastDeviceOrientation = event;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDeviceOrientation(event: DeviceOrientationEvent): void {
|
||||||
|
this.lastDeviceOrientation = event;
|
||||||
|
}
|
||||||
|
|
||||||
public logInteraction(event: Event): void {
|
public logInteraction(event: Event): void {
|
||||||
const timestamp = Date.now();
|
if (!this.activeLog) return; // Don't log if no test is active
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
let interaction: InteractionEvent;
|
|
||||||
|
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
let interaction: InteractionEvent;
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
// Standard DOM event with a target
|
interaction = { timestamp: Date.now(), type: event.type, elementId: target.id, elementTag: target.tagName, elementClasses: target.className, value: (target as any).value ?? undefined };
|
||||||
interaction = {
|
} else if (event instanceof CustomEvent) {
|
||||||
timestamp,
|
interaction = { timestamp: Date.now(), type: event.type, value: event.detail };
|
||||||
type: event.type,
|
} else {
|
||||||
elementId: target.id || undefined,
|
console.warn("logInteraction called with an unknown event type:", event);
|
||||||
elementTag: target.tagName,
|
return;
|
||||||
elementClasses: target.className,
|
|
||||||
value: (target as any).value ?? undefined
|
|
||||||
};
|
|
||||||
} else if (event instanceof CustomEvent) {
|
|
||||||
// CustomEvent, likely without a target
|
|
||||||
interaction = {
|
|
||||||
timestamp,
|
|
||||||
type: event.type,
|
|
||||||
value: event.detail
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Fallback for other events without a target
|
|
||||||
interaction = {
|
|
||||||
timestamp,
|
|
||||||
type: event.type
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.metricsLog.interactions.push(interaction);
|
|
||||||
}
|
}
|
||||||
|
this.activeLog.interactions.push(interaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getInternalThreeObjects(modelViewerElement: any): { scene: any; renderer: any } {
|
||||||
|
let scene = null;
|
||||||
|
let renderer = null;
|
||||||
|
|
||||||
|
for (let p = modelViewerElement; p != null; p = Object.getPrototypeOf(p)) {
|
||||||
public startTracking(modelViewerElement: any): void {
|
const privateAPI = Object.getOwnPropertySymbols(p);
|
||||||
this.stopTracking();
|
const rendererSym = privateAPI.find((s) => s.toString() === 'Symbol(renderer)');
|
||||||
if (!modelViewerElement) {
|
const sceneSym = privateAPI.find((s) => s.toString() === 'Symbol(scene)');
|
||||||
console.error("startTracking called with no modelViewerElement.");
|
|
||||||
return;
|
if (rendererSym && modelViewerElement[rendererSym]) {
|
||||||
}
|
renderer = modelViewerElement[rendererSym].threeRenderer;
|
||||||
this.modelViewerElement = modelViewerElement;
|
}
|
||||||
|
if (sceneSym && modelViewerElement[sceneSym]) {
|
||||||
|
scene = modelViewerElement[sceneSym];
|
||||||
if (this.modelViewerElement.xrSession) {
|
}
|
||||||
this.startXrTracking(this.modelViewerElement.xrSession);
|
if (renderer && scene) {
|
||||||
}
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private startXrTracking(xrSession: any): void {
|
|
||||||
if (this.xrFrameSubscriptionId === null) {
|
|
||||||
this.xrFrameCount = 0;
|
|
||||||
this.xrFrameSubscriptionId = xrSession.requestAnimationFrame(this.onXrFrame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private onXrFrame(time: DOMHighResTimeStamp, frame: any): void {
|
|
||||||
const session = frame.session;
|
|
||||||
this.xrFrameSubscriptionId = session.requestAnimationFrame(this.onXrFrame);
|
|
||||||
|
|
||||||
|
|
||||||
this.xrFrameCount++;
|
|
||||||
if (this.xrFrameCount % 30 !== 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const referenceSpace = this.modelViewerElement?.renderer.xr.getReferenceSpace();
|
|
||||||
if (!referenceSpace) return;
|
|
||||||
|
|
||||||
const viewerPose = frame.getViewerPose(referenceSpace);
|
|
||||||
const timestamp = Date.now();
|
|
||||||
let position = null, orientation = null, euler = null;
|
|
||||||
|
|
||||||
|
|
||||||
if (viewerPose) {
|
|
||||||
const { transform } = viewerPose;
|
|
||||||
position = { x: transform.position.x, y: transform.position.y, z: transform.position.z, w: transform.position.w };
|
|
||||||
orientation = { x: transform.orientation.x, y: transform.orientation.y, z: transform.orientation.z, w: transform.orientation.w };
|
|
||||||
euler = this.quaternionToEuler(orientation.x, orientation.y, orientation.z, orientation.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const anchorRaw = this.modelViewerElement.getAnchor ? this.modelViewerElement.getAnchor() : null;
|
|
||||||
const anchor = (typeof anchorRaw === 'string' && anchorRaw.includes('not placed')) ? null : anchorRaw;
|
|
||||||
const orbit = this.modelViewerElement.getCameraOrbit ? this.modelViewerElement.getCameraOrbit() : null;
|
|
||||||
const target = this.modelViewerElement.getCameraTarget ? this.modelViewerElement.getCameraTarget() : null;
|
|
||||||
|
|
||||||
|
|
||||||
const arData: ArTrackingData = {
|
|
||||||
timestamp,
|
|
||||||
anchor,
|
|
||||||
cameraPosition: position,
|
|
||||||
cameraOrientation: orientation,
|
|
||||||
cameraEuler: euler,
|
|
||||||
cameraOrbit: orbit ? { theta: orbit.theta, phi: orbit.phi, radius: orbit.radius } : null,
|
|
||||||
cameraTarget: target ? { x: target.x, y: target.y, z: target.z } : null,
|
|
||||||
};
|
|
||||||
this.metricsLog.arData.push(arData);
|
|
||||||
|
|
||||||
|
|
||||||
if (this.lastDeviceOrientation) {
|
|
||||||
this.metricsLog.deviceOrientations.push({
|
|
||||||
timestamp,
|
|
||||||
alpha: this.lastDeviceOrientation.alpha,
|
|
||||||
beta: this.lastDeviceOrientation.beta,
|
|
||||||
gamma: this.lastDeviceOrientation.gamma
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public stopTracking(): void {
|
|
||||||
if (this.xrFrameSubscriptionId !== null) {
|
|
||||||
const xrSession = this.modelViewerElement?.xrSession;
|
|
||||||
if (xrSession) {
|
|
||||||
xrSession.cancelAnimationFrame(this.xrFrameSubscriptionId);
|
|
||||||
}
|
}
|
||||||
this.xrFrameSubscriptionId = null;
|
|
||||||
}
|
}
|
||||||
this.modelViewerElement = null;
|
return { scene, renderer };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public startTracking(modelViewerElement: any, testName: string): void {
|
||||||
|
this.stopTracking();
|
||||||
|
|
||||||
public sendMetricsToServer(testName: string, formData?: any) {
|
if (!modelViewerElement) {
|
||||||
const payload = {
|
console.error("startTracking called with no modelViewerElement.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.modelViewerElement = modelViewerElement;
|
||||||
|
|
||||||
|
const newLog: MetricsLog = {
|
||||||
testName,
|
testName,
|
||||||
|
interactions: [],
|
||||||
|
deviceOrientations: [],
|
||||||
|
arTrackingData: []
|
||||||
|
};
|
||||||
|
this.allTestLogs.push(newLog);
|
||||||
|
this.activeLog = newLog;
|
||||||
|
|
||||||
|
if (!this.deviceInfo) {
|
||||||
|
this.deviceInfo = {
|
||||||
|
screenWidth: window.screen.width,
|
||||||
|
screenHeight: window.screen.height,
|
||||||
|
devicePixelRatio: window.devicePixelRatio,
|
||||||
|
userAgent: navigator.userAgent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { scene, renderer } = this.getInternalThreeObjects(this.modelViewerElement);
|
||||||
|
|
||||||
|
if (!scene || !renderer) {
|
||||||
|
console.error("Could not access internal Three.js scene or renderer. Tracking cannot start.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Starting tracking for test: ${testName}`);
|
||||||
|
this.trackingSubscription = interval(500).subscribe(async () => {
|
||||||
|
if (!this.activeLog) return;
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const model = scene?._model;
|
||||||
|
const camera = scene?.camera;
|
||||||
|
const session = renderer?.xr?.getSession();
|
||||||
|
|
||||||
|
let modelWorldPosition = null;
|
||||||
|
if (model) {
|
||||||
|
model.updateWorldMatrix(true, false);
|
||||||
|
modelWorldPosition = new THREE.Vector3();
|
||||||
|
modelWorldPosition.setFromMatrixPosition(model.matrixWorld);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arData: ArTrackingData = {
|
||||||
|
timestamp,
|
||||||
|
modelIsVisible: model ? model.visible : null,
|
||||||
|
modelWorldPosition: modelWorldPosition ? { ...modelWorldPosition } : null,
|
||||||
|
modelInitialScale: model ? { ...model.scale } : null,
|
||||||
|
cameraFov: camera ? camera.fov : null,
|
||||||
|
cameraAspect: camera ? camera.aspect : null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
const frame = await new Promise<any>(resolve => session.requestAnimationFrame((time: any, frame: any) => resolve(frame)));
|
||||||
|
const referenceSpace = renderer.xr.getReferenceSpace();
|
||||||
|
if (frame && referenceSpace) {
|
||||||
|
const viewerPose = frame.getViewerPose(referenceSpace);
|
||||||
|
if (viewerPose) {
|
||||||
|
const { transform } = viewerPose;
|
||||||
|
arData.viewerPosition = { x: transform.position.x, y: transform.position.y, z: transform.position.z, w: transform.position.w };
|
||||||
|
arData.viewerOrientation = { x: transform.orientation.x, y: transform.orientation.y, z: transform.orientation.z, w: transform.orientation.w };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeLog.arTrackingData.push(arData);
|
||||||
|
|
||||||
|
if (this.lastDeviceOrientation) {
|
||||||
|
const orientationData: DeviceOrientation = { timestamp, alpha: this.lastDeviceOrientation.alpha, beta: this.lastDeviceOrientation.beta, gamma: this.lastDeviceOrientation.gamma };
|
||||||
|
this.activeLog.deviceOrientations.push(orientationData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopTracking(): void {
|
||||||
|
if (this.trackingSubscription) {
|
||||||
|
this.trackingSubscription.unsubscribe();
|
||||||
|
this.trackingSubscription = null;
|
||||||
|
}
|
||||||
|
this.modelViewerElement = null;
|
||||||
|
this.activeLog = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendMetricsToServer(formData?: any) {
|
||||||
|
const payload = {
|
||||||
deviceId: this.deviceId,
|
deviceId: this.deviceId,
|
||||||
metricsLog: this.metricsLog,
|
deviceInfo: this.deviceInfo,
|
||||||
|
testLogs: this.allTestLogs, // Send the entire collection of logs
|
||||||
...(formData && { formData })
|
...(formData && { formData })
|
||||||
};
|
};
|
||||||
|
|
||||||
this.stopTracking();
|
|
||||||
return this.http.post(this.serverUrl, payload).pipe(
|
|
||||||
tap({
|
|
||||||
next: () => this.resetMetrics(),
|
|
||||||
error: (err) => console.error(`Failed to send metrics for '${testName}':`, err)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public resetMetrics(): void {
|
|
||||||
this.metricsLog = { interactions: [], deviceOrientations: [], arData: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
private quaternionToEuler(x: number, y: number, z: number, w: number): { pitch: number, yaw: number, roll: number } {
|
this.stopTracking();
|
||||||
const sinr_cosp = 2 * (w * x + y * z);
|
return this.http.post(this.serverUrl, payload).pipe(
|
||||||
const cosr_cosp = 1 - 2 * (x * x + y * y);
|
tap({
|
||||||
const roll = Math.atan2(sinr_cosp, cosr_cosp);
|
next: () => {
|
||||||
|
console.log(`Metrics for session sent successfully.`);
|
||||||
|
this.resetAllMetrics();
|
||||||
|
},
|
||||||
|
error: (err) => console.error(`Failed to send metrics:`, err)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetAllMetrics(): void {
|
||||||
const sinp = 2 * (w * y - z * x);
|
this.allTestLogs = [];
|
||||||
const pitch = Math.abs(sinp) >= 1 ? (Math.PI / 2) * Math.sign(sinp) : Math.asin(sinp);
|
this.deviceInfo = null;
|
||||||
|
this.activeLog = null;
|
||||||
|
}
|
||||||
const siny_cosp = 2 * (w * z + x * y);
|
|
||||||
const cosy_cosp = 1 - 2 * (y * y + z * z);
|
|
||||||
const yaw = Math.atan2(siny_cosp, cosy_cosp);
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
pitch: pitch * (180 / Math.PI),
|
|
||||||
yaw: yaw * (180 / Math.PI),
|
|
||||||
roll: roll * (180 / Math.PI)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue