fix
parent
d97640ee63
commit
2654d1178b
|
|
@ -3,206 +3,255 @@ import { inject, Injectable } from '@angular/core';
|
||||||
import { tap } from 'rxjs';
|
import { tap } from 'rxjs';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
|
||||||
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;
|
anchor: string | null;
|
||||||
cameraPosition: { x: number; y: number; z: number; w?: number } | null;
|
cameraPosition: { x: number; y: number; z: number; w?: number } | null;
|
||||||
cameraOrientation: { x: number; y: number; z: number; w: number } | null;
|
cameraOrientation: { x: number; y: number; z: number; w: number } | null;
|
||||||
cameraEuler: { pitch: number; yaw: number; roll: number } | null;
|
cameraEuler: { pitch: number; yaw: number; roll: number } | null;
|
||||||
cameraOrbit: { theta: number; phi: number; radius: number } | null;
|
cameraOrbit: { theta: number; phi: number; radius: number } | null;
|
||||||
cameraTarget: { x: number; y: number; z: number } | null;
|
cameraTarget: { x: number; y: number; z: number } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface MetricsLog {
|
export interface MetricsLog {
|
||||||
interactions: InteractionEvent[];
|
interactions: InteractionEvent[];
|
||||||
deviceOrientations: DeviceOrientation[];
|
deviceOrientations: DeviceOrientation[];
|
||||||
arTrackingData: ArTrackingData[];
|
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 metricsLog: MetricsLog = {
|
|
||||||
interactions: [],
|
|
||||||
deviceOrientations: [],
|
|
||||||
arTrackingData: []
|
|
||||||
};
|
|
||||||
|
|
||||||
private xrFrameSubscriptionId: number | null = null;
|
private metricsLog: MetricsLog = {
|
||||||
private lastDeviceOrientation: DeviceOrientationEvent | null = null;
|
interactions: [],
|
||||||
private modelViewerElement: any = null;
|
deviceOrientations: [],
|
||||||
private xrFrameCount = 0;
|
arTrackingData: []
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.handleDeviceOrientation = this.handleDeviceOrientation.bind(this);
|
|
||||||
this.onXrFrame = this.onXrFrame.bind(this);
|
|
||||||
|
|
||||||
const storedId = localStorage.getItem('device-uuid');
|
private xrFrameSubscriptionId: number | null = null;
|
||||||
this.deviceId = storedId || uuidv4();
|
private lastDeviceOrientation: DeviceOrientationEvent | null = null;
|
||||||
if (!storedId) {
|
private modelViewerElement: any = null;
|
||||||
localStorage.setItem('device-uuid', this.deviceId);
|
private xrFrameCount = 0;
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.addEventListener('deviceorientation', this.handleDeviceOrientation, true);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleDeviceOrientation(event: DeviceOrientationEvent): void {
|
|
||||||
this.lastDeviceOrientation = event;
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('deviceorientation', this.handleDeviceOrientation, true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public logInteraction(event: Event): void {
|
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
const interaction: InteractionEvent = {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
type: event.type,
|
|
||||||
elementId: target.id || undefined,
|
|
||||||
elementTag: target.tagName,
|
|
||||||
elementClasses: target.className,
|
|
||||||
value: (target as any).value ?? undefined
|
|
||||||
};
|
|
||||||
this.metricsLog.interactions.push(interaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
public startTracking(modelViewerElement: any): void {
|
private handleDeviceOrientation(event: DeviceOrientationEvent): void {
|
||||||
this.stopTracking();
|
this.lastDeviceOrientation = event;
|
||||||
if (!modelViewerElement) {
|
}
|
||||||
console.error("startTracking called with no modelViewerElement.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.modelViewerElement = modelViewerElement;
|
|
||||||
|
|
||||||
if (this.modelViewerElement.xrSession) {
|
|
||||||
this.startXrTracking(this.modelViewerElement.xrSession);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private startXrTracking(xrSession: any): void {
|
public logInteraction(event: Event): void {
|
||||||
if (this.xrFrameSubscriptionId === null) {
|
const timestamp = Date.now();
|
||||||
this.xrFrameCount = 0;
|
const target = event.target as HTMLElement;
|
||||||
this.xrFrameSubscriptionId = xrSession.requestAnimationFrame(this.onXrFrame);
|
let interaction: InteractionEvent;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onXrFrame(time: DOMHighResTimeStamp, frame: any): void {
|
|
||||||
const session = frame.session;
|
|
||||||
this.xrFrameSubscriptionId = session.requestAnimationFrame(this.onXrFrame);
|
|
||||||
|
|
||||||
this.xrFrameCount++;
|
if (target) {
|
||||||
if (this.xrFrameCount % 30 !== 0) {
|
// Standard DOM event with a target
|
||||||
return;
|
interaction = {
|
||||||
}
|
timestamp,
|
||||||
|
type: event.type,
|
||||||
const referenceSpace = this.modelViewerElement?.renderer.xr.getReferenceSpace();
|
elementId: target.id || undefined,
|
||||||
if (!referenceSpace) return;
|
elementTag: target.tagName,
|
||||||
|
elementClasses: target.className,
|
||||||
const viewerPose = frame.getViewerPose(referenceSpace);
|
value: (target as any).value ?? undefined
|
||||||
const timestamp = Date.now();
|
};
|
||||||
let position = null, orientation = null, euler = null;
|
} else if (event instanceof CustomEvent) {
|
||||||
|
// CustomEvent, likely without a target
|
||||||
if (viewerPose) {
|
interaction = {
|
||||||
const { transform } = viewerPose;
|
timestamp,
|
||||||
position = { x: transform.position.x, y: transform.position.y, z: transform.position.z, w: transform.position.w };
|
type: event.type,
|
||||||
orientation = { x: transform.orientation.x, y: transform.orientation.y, z: transform.orientation.z, w: transform.orientation.w };
|
value: event.detail
|
||||||
euler = this.quaternionToEuler(orientation.x, orientation.y, orientation.z, orientation.w);
|
};
|
||||||
}
|
} else {
|
||||||
|
// Fallback for other events without a target
|
||||||
const anchorRaw = this.modelViewerElement.getAnchor ? this.modelViewerElement.getAnchor() : null;
|
interaction = {
|
||||||
const anchor = (typeof anchorRaw === 'string' && anchorRaw.includes('not placed')) ? null : anchorRaw;
|
timestamp,
|
||||||
const orbit = this.modelViewerElement.getCameraOrbit ? this.modelViewerElement.getCameraOrbit() : null;
|
type: event.type
|
||||||
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.arTrackingData.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendMetricsToServer(testName: string, formData?: any) {
|
|
||||||
const payload = {
|
|
||||||
testName,
|
|
||||||
deviceId: this.deviceId,
|
|
||||||
metricsLog: this.metricsLog,
|
|
||||||
...(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: [], arTrackingData: [] };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private quaternionToEuler(x: number, y: number, z: number, w: number): { pitch: number, yaw: number, roll: number } {
|
this.metricsLog.interactions.push(interaction);
|
||||||
const sinr_cosp = 2 * (w * x + y * z);
|
}
|
||||||
const cosr_cosp = 1 - 2 * (x * x + y * y);
|
|
||||||
const roll = Math.atan2(sinr_cosp, cosr_cosp);
|
|
||||||
|
|
||||||
const sinp = 2 * (w * y - z * x);
|
|
||||||
const pitch = Math.abs(sinp) >= 1 ? (Math.PI / 2) * Math.sign(sinp) : Math.asin(sinp);
|
|
||||||
|
|
||||||
const siny_cosp = 2 * (w * z + x * y);
|
public startTracking(modelViewerElement: any): void {
|
||||||
const cosy_cosp = 1 - 2 * (y * y + z * z);
|
this.stopTracking();
|
||||||
const yaw = Math.atan2(siny_cosp, cosy_cosp);
|
if (!modelViewerElement) {
|
||||||
|
console.error("startTracking called with no modelViewerElement.");
|
||||||
return {
|
return;
|
||||||
pitch: pitch * (180 / Math.PI),
|
|
||||||
yaw: yaw * (180 / Math.PI),
|
|
||||||
roll: roll * (180 / Math.PI)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
this.modelViewerElement = modelViewerElement;
|
||||||
|
|
||||||
|
|
||||||
|
if (this.modelViewerElement.xrSession) {
|
||||||
|
this.startXrTracking(this.modelViewerElement.xrSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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.arTrackingData.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public sendMetricsToServer(testName: string, formData?: any) {
|
||||||
|
const payload = {
|
||||||
|
testName,
|
||||||
|
deviceId: this.deviceId,
|
||||||
|
metricsLog: this.metricsLog,
|
||||||
|
...(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: [], arTrackingData: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
private quaternionToEuler(x: number, y: number, z: number, w: number): { pitch: number, yaw: number, roll: number } {
|
||||||
|
const sinr_cosp = 2 * (w * x + y * z);
|
||||||
|
const cosr_cosp = 1 - 2 * (x * x + y * y);
|
||||||
|
const roll = Math.atan2(sinr_cosp, cosr_cosp);
|
||||||
|
|
||||||
|
|
||||||
|
const sinp = 2 * (w * y - z * x);
|
||||||
|
const pitch = Math.abs(sinp) >= 1 ? (Math.PI / 2) * Math.sign(sinp) : Math.asin(sinp);
|
||||||
|
|
||||||
|
|
||||||
|
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