master
MrPlatnum 2025-09-15 09:51:27 +02:00
parent 4cdca6b4bf
commit 4b4759b29e
2 changed files with 116 additions and 84 deletions

View File

@ -11,8 +11,9 @@
<button slot="hotspot-text" class="hotspot ar-hotspot" data-position="-0.1 0.93 0.1" data-normal="0 1 0"> <button slot="hotspot-text" class="hotspot ar-hotspot" data-position="-0.1 0.93 0.1" data-normal="0 1 0">
<div class="annotation" [ngStyle]="{'font-size.px': currentSize}" <div class="annotation" [ngStyle]="{'font-size.px': currentSize}"
style="background:rgba(255,255,255,0.9);padding:8px;border-radius:4px;color:black;"> style="background:rgba(255,255,255,0.9);padding:8px;border-radius:4px;color:black;">
Lorem ipsum dolor sit amet... Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
</div> dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. </div>
</button> </button>
<div <div
@ -91,4 +92,4 @@
</div> </div>
</model-viewer> </model-viewer>
</div> </div>

View File

@ -1,9 +1,8 @@
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 { interval, Subscription, tap } from 'rxjs'; import { tap } from 'rxjs';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
// --- Data Interfaces (Unchanged) ---
export interface InteractionEvent { export interface InteractionEvent {
timestamp: number; timestamp: number;
type: string; type: string;
@ -23,22 +22,17 @@ export interface DeviceOrientation {
export interface ArTrackingData { export interface ArTrackingData {
timestamp: number; timestamp: number;
anchor: string | null; anchor: string | null;
cameraOrbit: { cameraPosition: { x: number; y: number; z: number; w?: number } | null;
theta: number; cameraOrientation: { x: number; y: number; z: number; w: number } | null;
phi: number; cameraEuler: { pitch: number; yaw: number; roll: number } | null;
radius: number; cameraOrbit: { theta: number; phi: number; radius: number } | null;
} | 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[];
arData: ArTrackingData[]; arTrackingData: ArTrackingData[];
} }
@Injectable({ @Injectable({
@ -47,29 +41,28 @@ export interface MetricsLog {
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 = { private metricsLog: MetricsLog = {
interactions: [], interactions: [],
deviceOrientations: [], deviceOrientations: [],
arData: [] arTrackingData: []
}; };
private trackingSubscription: Subscription | null = null; private xrFrameSubscriptionId: number | null = null;
private lastDeviceOrientation: DeviceOrientationEvent | null = null; private lastDeviceOrientation: DeviceOrientationEvent | null = null;
private modelViewerElement: any = null;
private xrFrameCount = 0;
constructor() { constructor() {
this.handleDeviceOrientation = this.handleDeviceOrientation.bind(this); this.handleDeviceOrientation = this.handleDeviceOrientation.bind(this);
this.onXrFrame = this.onXrFrame.bind(this);
const storedId = localStorage.getItem('device-uuid'); const storedId = localStorage.getItem('device-uuid');
if (storedId) { this.deviceId = storedId || uuidv4();
this.deviceId = storedId; if (!storedId) {
} else {
this.deviceId = uuidv4();
localStorage.setItem('device-uuid', this.deviceId); localStorage.setItem('device-uuid', this.deviceId);
} }
console.log('Device ID for this session:', this.deviceId);
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.addEventListener('deviceorientation', this.handleDeviceOrientation, true); window.addEventListener('deviceorientation', this.handleDeviceOrientation, true);
@ -81,74 +74,96 @@ export class MetricsTrackerService {
} }
public logInteraction(event: Event): void { public logInteraction(event: Event): void {
if (event.target) { const target = event.target as HTMLElement;
const target = event.target as HTMLElement; const interaction: InteractionEvent = {
const interaction: InteractionEvent = { timestamp: Date.now(),
timestamp: Date.now(), type: event.type,
type: event.type, elementId: target.id || undefined,
elementId: target.id || undefined, elementTag: target.tagName,
elementTag: target.tagName, elementClasses: target.className,
elementClasses: target.className, value: (target as any).value ?? undefined
value: (target as any).value ?? undefined };
}; this.metricsLog.interactions.push(interaction);
this.metricsLog.interactions.push(interaction);
} else if (event instanceof CustomEvent && event.detail) {
const interaction: InteractionEvent = {
timestamp: Date.now(),
type: event.type,
value: event.detail
};
this.metricsLog.interactions.push(interaction);
} else {
console.warn("logInteraction called with an unknown event type:", event);
}
} }
public startTracking(modelViewerElement: any): void { public startTracking(modelViewerElement: any): void {
this.stopTracking(); this.stopTracking();
if (!modelViewerElement) { if (!modelViewerElement) {
console.error("startTracking called with no modelViewerElement."); console.error("startTracking called with no modelViewerElement.");
return; return;
} }
this.modelViewerElement = modelViewerElement;
console.log("Starting a new periodic metrics tracking subscription."); if (this.modelViewerElement.xrSession) {
this.trackingSubscription = interval(500).subscribe(() => { this.startXrTracking(this.modelViewerElement.xrSession);
}
}
const timestamp = Date.now(); private startXrTracking(xrSession: any): void {
if (this.xrFrameSubscriptionId === null) {
this.xrFrameCount = 0;
this.xrFrameSubscriptionId = xrSession.requestAnimationFrame(this.onXrFrame);
}
}
// Capture AR Data private onXrFrame(time: DOMHighResTimeStamp, frame: any): void {
const anchor = modelViewerElement.getAnchor ? modelViewerElement.getAnchor() : 'getAnchor not available'; const session = frame.session;
const orbit = modelViewerElement.getCameraOrbit ? modelViewerElement.getCameraOrbit() : null; this.xrFrameSubscriptionId = session.requestAnimationFrame(this.onXrFrame);
const target = modelViewerElement.getCameraTarget ? modelViewerElement.getCameraTarget() : null;
const arData: ArTrackingData = { this.xrFrameCount++;
timestamp: timestamp, if (this.xrFrameCount % 30 !== 0) {
anchor: anchor.includes('not placed') ? null : anchor, return;
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);
// Capture Device Orientation const referenceSpace = this.modelViewerElement?.renderer.xr.getReferenceSpace();
if (this.lastDeviceOrientation) { if (!referenceSpace) return;
const orientation: DeviceOrientation = {
timestamp: timestamp, const viewerPose = frame.getViewerPose(referenceSpace);
alpha: this.lastDeviceOrientation.alpha, const timestamp = Date.now();
beta: this.lastDeviceOrientation.beta, let position = null, orientation = null, euler = null;
gamma: this.lastDeviceOrientation.gamma
}; if (viewerPose) {
this.metricsLog.deviceOrientations.push(orientation); 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 { public stopTracking(): void {
if (this.trackingSubscription) { if (this.xrFrameSubscriptionId !== null) {
console.log("Stopping existing tracking subscription."); const xrSession = this.modelViewerElement?.xrSession;
this.trackingSubscription.unsubscribe(); if (xrSession) {
this.trackingSubscription = null; xrSession.cancelAnimationFrame(this.xrFrameSubscriptionId);
}
this.xrFrameSubscriptionId = null;
} }
this.modelViewerElement = null;
} }
public sendMetricsToServer(testName: string, formData?: any) { public sendMetricsToServer(testName: string, formData?: any) {
@ -158,20 +173,36 @@ export class MetricsTrackerService {
metricsLog: this.metricsLog, metricsLog: this.metricsLog,
...(formData && { formData }) ...(formData && { formData })
}; };
console.log("Sending final payload:", payload);
this.stopTracking(); this.stopTracking();
return this.http.post(this.serverUrl, payload).pipe( return this.http.post(this.serverUrl, payload).pipe(
tap({ tap({
next: (response) => { next: () => this.resetMetrics(),
console.log(`Metrics for '${testName}' sent successfully:`, response);
this.resetMetrics();
},
error: (err) => console.error(`Failed to send metrics for '${testName}':`, err) error: (err) => console.error(`Failed to send metrics for '${testName}':`, err)
}) })
); );
} }
public resetMetrics(): void { public resetMetrics(): void {
this.metricsLog = { interactions: [], deviceOrientations: [], arData: [] }; 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)
};
} }
} }