fix model-viewer placement and

This commit is contained in:
MrPlatnum committed 2025-09-15 15:23:11 +02:00
1 parent 1bb248de1b
commit 83c4ece1e8
8 files changed
+328 -312

No files matched your search

@@ -108,22 +108,6 @@
<option value="prefer-not-to-say">Keine Angabe</option>
</select>
</div>
<div>
<label for="education-select" class="block text-sm font-medium text-gray-700 mb-2">Höchster Bildungsabschluss</label>
<select id="education-select" formControlName="education" (change)="logInteraction($event)" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option [ngValue]="null" disabled>Bitte auswählen</option>
<option value="high-school">Schulabschluss</option>
<option value="some-college">Berufsausbildung</option>
<option value="bachelors">Bachelor</option>
<option value="masters">Master</option>
<option value="doctorate">Promotion</option>
<option value="other">Andere</option>
</select>
</div>
<div>
<label for="occupation-input" class="block text-sm font-medium text-gray-700 mb-2">Berufsfeld/Studienfach</label>
<input id="occupation-input" type="text" formControlName="occupation" (change)="logInteraction($event)" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="z.B. Student, Ingenieur, ...">
</div>
</div>
</div>
@@ -4,11 +4,11 @@ import {
OnDestroy,
ViewChild,
ElementRef,
ChangeDetectorRef,
CUSTOM_ELEMENTS_SCHEMA,
EventEmitter,
Output,
inject
inject,
NgZone
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@@ -30,15 +30,17 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDest
@Output() redoTest = new EventEmitter<number>();
private metricsService = inject(MetricsTrackerService);
private zone = inject(NgZone);
public scale = 1;
public verticalOffset = 0;
protected isModelPlaced = false;
private initialArAnchor: { x: number; y: number; z: number } | null = null;
private hasStartedTracking = false;
constructor(private cdr: ChangeDetectorRef) {
constructor() {
this.logInteraction = this.logInteraction.bind(this);
}
ngOnDestroy() {
this.metricsService.stopTracking();
}
@@ -46,10 +48,19 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDest
ngAfterViewInit() {
const mv = this.modelViewerRef.nativeElement;
mv.addEventListener('ar-status', (e: any) => {
if (e.detail.status === 'session-started') {
setTimeout(() => this.captureAnchor(), 500);
this.metricsService.startTracking(mv, "position");
}
this.zone.run(() => {
const status = e.detail.status;
console.log(status)
if (status === 'session-started' && !this.hasStartedTracking) {
this.hasStartedTracking = true;
this.metricsService.startTracking(mv, "position");
}
if (status === 'object-placed' && !this.isModelPlaced) {
console.log("placed")
this.captureAnchor();
}
});
});
}
@@ -57,24 +68,26 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDest
this.metricsService.logInteraction(event);
}
private async captureAnchor() {
private captureAnchor() {
const mv = this.modelViewerRef.nativeElement;
const anchor = mv.getAnchor();
if (!anchor.includes('not placed')) {
if (anchor && !anchor.includes('not placed')) {
const [x, y, z] = anchor.split(' ').map(parseFloat);
this.initialArAnchor = { x, y, z };
this.isModelPlaced = true;
this.cdr.detectChanges();
console.log('Anchor captured successfully:', this.initialArAnchor);
} else {
setTimeout(() => this.captureAnchor(), 500);
console.warn('Object placed, but anchor not ready. Retrying once.');
setTimeout(() => this.captureAnchor(), 100);
}
}
onSliderInput() {
if (!this.initialArAnchor) return;
const { x, y, z } = this.initialArAnchor!;
const { x, y, z } = this.initialArAnchor;
const newY = y + this.verticalOffset;
this.modelViewerRef.nativeElement.setAttribute('ar-anchor', `${x} ${newY} ${z}`);
this.modelViewerRef.nativeElement.scale = `${this.scale} ${this.scale} ${this.scale}`;
}
resetPosition() {
@@ -99,13 +112,10 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDest
}));
console.log('Spatial position results captured locally.');
this.testComplete.emit();
}
retryCurrent() {
this.redoTest.emit(1);
}
}
@@ -17,60 +17,65 @@
WebXR Umgebung laden
</button>
<!-- Adjustment Phase Controls -->
<div *ngIf="currentPhase === 1 && isModelPlaced"
class="ar-controls absolute bottom-24 left-1/2 -translate-x-1/2 flex flex-col items-center space-y-6 bg-black bg-opacity-70 p-6 rounded-xl">
<div class="text-white text-center mb-4">
<h4 id="adjust-phase-title" class="text-lg font-semibold">Finden sie ihre Optionale Betrachtungsposition</h4>
</div>
<div *ngIf="isArActive"
class="absolute bottom-24 left-1/2 -translate-x-1/2 transition-opacity duration-300"
[class.opacity-50]="!isModelPlaced"
[class.pointer-events-none]="!isModelPlaced">
<div class="flex flex-col items-center text-white w-64">
<label for="stability-offset-slider" class="text-sm font-medium mb-2">Höhe verschieben</label>
<input
id="stability-offset-slider"
type="range"
min="-1.5"
max="1.5"
step="0.01"
[(ngModel)]="verticalOffset"
(input)="onSliderInput(); logInteraction($event)"
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" />
<span class="text-xs mt-1">{{ verticalOffset >= 0 ? '+' : '' }}{{ verticalOffset.toFixed(2) }}m</span>
</div>
<!-- Adjustment Phase Controls -->
<div *ngIf="currentPhase === 1"
class="ar-controls flex flex-col items-center space-y-6 bg-black bg-opacity-70 p-6 rounded-xl">
<div class="text-white text-center mb-4">
<h4 id="adjust-phase-title" class="text-lg font-semibold">Finden Sie Ihren Optionale Betrachtungsposition</h4>
</div>
<button
id="lock-position-btn"
(click)="lockPosition(); logInteraction($event)"
class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg text-lg">
🔒 Diese Position fixieren.
</button>
</div>
<div class="flex flex-col items-center text-white w-64">
<label for="stability-offset-slider" class="text-sm font-medium mb-2">Höhe verschieben</label>
<input
id="stability-offset-slider"
type="range"
min="-1.5"
max="1.5"
step="0.01"
[(ngModel)]="verticalOffset"
(input)="onSliderInput(); logInteraction($event)"
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" />
<span class="text-xs mt-1">{{ verticalOffset >= 0 ? '+' : '' }}{{ verticalOffset.toFixed(2) }}m</span>
</div>
<!-- Lock Confirmation Phase -->
<div *ngIf="currentPhase === 2"
class="ar-controls absolute bottom-24 left-1/2 -translate-x-1/2 flex flex-col items-center space-y-4 bg-yellow-600 bg-opacity-90 p-6 rounded-xl">
<div class="text-white text-center">
<h4 id="lock-confirm-title" class="text-xl font-bold">Position fixiert!</h4>
<p class="text-xs mt-2 opacity-80">Halten Sie das Modell für die gesamte Dauer in möglichst genau dieser Position.</p>
</div>
<div class="flex space-x-4">
<button
id="adjust-more-btn"
(click)="adjustMore(); logInteraction($event)"
class="bg-gray-600 hover:bg-gray-700 text-white py-2 px-4 rounded-lg">
Anpassen
</button>
<button
id="start-countdown-btn"
(click)="startCountdown(); logInteraction($event)"
class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-6 rounded-lg">
20s Timer starten
id="lock-position-btn"
(click)="lockPosition(); logInteraction($event)"
class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg text-lg">
🔒 Diese Position fixieren.
</button>
</div>
</div>
<!-- Lock Confirmation Phase -->
<div *ngIf="currentPhase === 2"
class="ar-controls flex flex-col items-center space-y-4 bg-yellow-600 bg-opacity-90 p-6 rounded-xl">
<div class="text-white text-center">
<h4 id="lock-confirm-title" class="text-xl font-bold">Position fixiert!</h4>
<p class="text-xs mt-2 opacity-80">Halten Sie das Modell für die gesamte Dauer in möglichst genau dieser Position.</p>
</div>
<div class="flex space-x-4">
<button
id="adjust-more-btn"
(click)="adjustMore(); logInteraction($event)"
class="bg-gray-600 hover:bg-gray-700 text-white py-2 px-4 rounded-lg">
Anpassen
</button>
<button
id="start-countdown-btn"
(click)="startCountdown(); logInteraction($event)"
class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-6 rounded-lg">
20s Timer starten
</button>
</div>
</div>
<!-- Countdown Phase -->
<div *ngIf="currentPhase === 3"
@@ -117,5 +122,6 @@
</button>
</div>
</div>
</div>
</model-viewer>
</div>
@@ -5,7 +5,7 @@ import {
ViewChild,
ElementRef,
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectorRef,
NgZone,
EventEmitter,
Output,
inject
@@ -13,7 +13,6 @@ import {
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MetricsTrackerService } from '../../../../services/metrics-tracker.service';
import '../../../../../assets/scripts/model-viewer';
@Component({
@@ -24,68 +23,99 @@ import '../../../../../assets/scripts/model-viewer';
styleUrls: ['./spatial-stability-assessment.component.css'],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDestroy { // <-- Implement OnDestroy
export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDestroy {
@ViewChild('modelViewer') modelViewerRef!: ElementRef<any>;
@Output() testComplete = new EventEmitter<void>();
@Output() redoTest = new EventEmitter<number>();
private metricsService = inject(MetricsTrackerService);
private zone = inject(NgZone);
public isArActive = false;
public isModelPlaced = false;
public verticalOffset = 0;
public currentPhase = 0; // 0: waiting, 1: adjust, 2: lock, 3: countdown, 4: complete
public currentPhase = 0;
public remainingTime = 20;
public progressPercentage = 0;
private initialArAnchor: { x: number, y: number, z: number } | null = null;
private lockedPosition: { verticalOffset: number, anchor: string } | null = null;
private countdownInterval: any = null;
protected isModelPlaced = false;
constructor(private cdr: ChangeDetectorRef) {
private initialArAnchor: { x: number, y: number, z: number } | null = null;
private lockedPosition: { verticalOffset: number } | null = null;
private countdownInterval: any = null;
constructor() {
this.logInteraction = this.logInteraction.bind(this);
}
ngAfterViewInit() {
const mv = this.modelViewerRef.nativeElement;
mv.addEventListener('ar-status', (event: any) => {
if (event.detail.status === 'session-started' && !this.isModelPlaced) {
setTimeout(() => this.getInitialAnchor(), 500);
this.metricsService.startTracking(mv, "stability");
}
this.zone.run(() => {
const status = event.detail.status;
switch (status) {
case 'session-started':
this.isArActive = true;
this.isModelPlaced = false;
this.currentPhase = 1;
this.metricsService.startTracking(mv, "stability");
break;
case 'object-placed':
if (this.isArActive && !this.isModelPlaced) {
this.isModelPlaced = true;
this.getInitialAnchor();
}
break;
case 'not-presenting':
this.isArActive = false;
this.isModelPlaced = false;
this.currentPhase = 0;
this.resetComponentState();
break;
}
});
});
}
public logInteraction(event: Event) {
this.metricsService.logInteraction(event);
private resetComponentState() {
this.clearCountdown();
this.initialArAnchor = null;
this.lockedPosition = null;
this.verticalOffset = 0;
}
private async getInitialAnchor() {
const modelViewer = this.modelViewerRef.nativeElement;
const anchorString = modelViewer.getAnchor();
if (!anchorString.includes('not placed')) {
private getInitialAnchor() {
const mv = this.modelViewerRef.nativeElement;
const anchorString = mv.getAnchor();
if (anchorString && !anchorString.includes('not placed')) {
const coords = anchorString.split(' ').map(parseFloat);
this.initialArAnchor = { x: coords[0], y: coords[1], z: coords[2] };
this.isModelPlaced = true;
this.currentPhase = 1;
this.cdr.detectChanges();
if (coords.length === 3 && !coords.some(isNaN)) {
this.initialArAnchor = { x: coords[0], y: coords[1], z: coords[2] };
}
} else {
setTimeout(() => this.getInitialAnchor(), 500);
setTimeout(() => this.getInitialAnchor(), 100);
}
}
onSliderInput() {
if (this.initialArAnchor && this.currentPhase === 1) {
const { x, z } = this.initialArAnchor;
const newY = this.initialArAnchor.y + this.verticalOffset;
const anchor = `${this.initialArAnchor.x} ${newY} ${this.initialArAnchor.z}`;
this.modelViewerRef.nativeElement.setAttribute('ar-anchor', anchor);
this.modelViewerRef.nativeElement.setAttribute('ar-anchor', `${x} ${newY} ${z}`);
}
}
public logInteraction(event: Event) {
this.metricsService.logInteraction(event);
}
lockPosition() {
this.lockedPosition = {
verticalOffset: this.verticalOffset,
anchor: this.modelViewerRef.nativeElement.getAttribute('ar-anchor')
};
this.lockedPosition = { verticalOffset: this.verticalOffset };
this.currentPhase = 2;
}
@@ -97,64 +127,53 @@ export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDes
this.currentPhase = 3;
this.remainingTime = 20;
this.progressPercentage = 0;
this.countdownInterval = setInterval(() => {
this.remainingTime--;
this.progressPercentage = ((20 - this.remainingTime) / 20) * 100;
if (this.remainingTime <= 0) {
this.completeTest(true);
}
this.cdr.detectChanges();
this.zone.run(() => {
this.remainingTime--;
this.progressPercentage = ((20 - this.remainingTime) / 20) * 100;
if (this.remainingTime <= 0) {
this.completeTest(true);
}
});
}, 1000);
}
private clearCountdown() {
if (this.countdownInterval) {
clearInterval(this.countdownInterval);
this.countdownInterval = null;
}
}
cancelTest() {
if (this.countdownInterval) {
clearInterval(this.countdownInterval);
this.countdownInterval = null;
}
this.clearCountdown();
this.currentPhase = 2;
this.metricsService.logInteraction(new CustomEvent('test-cancelled', { detail: { phase: 'countdown' }}));
this.metricsService.logInteraction(new CustomEvent('test-cancelled', { detail: { phase: 'countdown' } }));
}
private completeTest(wasCompleted: boolean) {
if (this.countdownInterval) {
clearInterval(this.countdownInterval);
this.countdownInterval = null;
}
this.clearCountdown();
const finalResults = {
testCompletedSuccessfully: wasCompleted,
initialAnchor: this.initialArAnchor,
lockedPosition: this.lockedPosition,
};
this.metricsService.logInteraction(new CustomEvent('test-results', {
detail: {
testName: 'SpatialStability',
results: finalResults
}
detail: { testName: 'SpatialStability', results: finalResults }
}));
if (wasCompleted) {
this.currentPhase = 4;
}
if(wasCompleted) {
this.testComplete.emit();
this.currentPhase = 4;
this.testComplete.emit();
}
}
restartTest() {
this.clearCountdown();
this.currentPhase = 1;
this.remainingTime = 20;
this.progressPercentage = 0;
this.lockedPosition = null;
if (this.countdownInterval) {
clearInterval(this.countdownInterval);
this.countdownInterval = null;
}
this.verticalOffset = 0;
this.onSliderInput();
}
finishAssessment() {
@@ -162,9 +181,7 @@ export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDes
}
ngOnDestroy() {
if (this.countdownInterval) {
clearInterval(this.countdownInterval);
}
this.clearCountdown();
this.metricsService.stopTracking();
}
}
@@ -3,13 +3,11 @@
src="https://modelviewer.dev/shared-assets/models/Astronaut.glb" ar ar-modes="webxr" ar-placement="ceiling"
reveal="manual" camera-orbit="0deg 75deg 2m">
<button slot="ar-button" id="ar-button-legibility" (click)="logInteraction($event)"
class="text-xl absolute top-4 left-1/2 -translate-x-1/2 bg-blue-500 text-white py-2 px-4 rounded-lg z-10">
WebXR Umgebung laden
</button>
<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}"
style="background:rgba(255,255,255,0.9);padding:8px;border-radius:4px;color:black;">
@@ -17,85 +15,89 @@
labore et
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>
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
</div>
</button>
<div
class="ar-controls absolute bottom-24 left-1/2 -translate-x-1/2 bg-black bg-opacity-70 p-4 rounded-xl space-y-4 text-white z-10">
<div *ngIf="isArActive"
class="absolute bottom-24 left-1/2 -translate-x-1/2 flex flex-col items-center space-y-4 bg-black bg-opacity-70 p-4 rounded-xl text-white z-10 transition-opacity duration-300"
[class.opacity-50]="!isModelPlaced" [class.pointer-events-none]="!isModelPlaced">
<!-- Phase: Min -->
<ng-container *ngIf="phase==='min'">
<div>Minimum setzen</div>
<div class="flex items-center space-x-4">
<button id="min-decrease-btn" (click)="decrease(); logInteraction($event)"
class="w-10 h-10 bg-gray-600 rounded-full">–</button>
<span>{{ currentSize }}px</span>
<button id="min-increase-btn" (click)="increase(); logInteraction($event)"
class="w-10 h-10 bg-gray-600 rounded-full">+</button>
<div *ngIf="isDescriptionVisible" class="relative bg-black/30 p-3 pr-8 rounded-lg text-center w-[280px]">
<p>Minimum setzen. Der Text sollte gerade so noch lesbar sein. Den Arm hierfür bitte ganz austrecken.</p>
<button (click)="toggleDescription()" class="absolute top-1 right-2 text-2xl leading-none">&times;</button>
</div>
<div class="flex flex-col items-center space-y-4">
<div class="flex items-center space-x-4">
<button id="min-decrease-btn" (click)="decrease(); logInteraction($event)" class="w-10 h-10 bg-gray-600 rounded-full">–</button>
<span>{{ currentSize }}px</span>
<button id="min-increase-btn" (click)="increase(); logInteraction($event)" class="w-10 h-10 bg-gray-600 rounded-full">+</button>
</div>
<button id="min-confirm-btn" (click)="nextPhase(); logInteraction($event)" class="bg-green-600 py-2 px-6 rounded-lg">Minimum bestätigen</button>
</div>
<button id="min-confirm-btn" (click)="nextPhase(); logInteraction($event)"
class="bg-green-600 py-2 px-6 rounded-lg">Minimum bestätigen</button>
</ng-container>
<!-- Phase: Confirmed Min -->
<ng-container *ngIf="phase==='confirmedMin'">
<div>Minimum bestätigt: {{ minSizeResult }}px</div>
<button id="confirmed-min-reset-btn" (click)="resetToMid(); logInteraction($event)"
class="bg-gray-600 py-2 px-4 rounded-lg">Reset</button>
<button id="confirmed-min-continue-btn" (click)="nextPhase(); logInteraction($event)"
class="bg-blue-600 py-2 px-6 rounded-lg">Weiter zu Maximum</button>
<div class="flex flex-col items-center space-y-4">
<div>Minimum bestätigt: {{ minSizeResult }}px</div>
<div class="flex space-x-2">
<button id="confirmed-min-reset-btn" (click)="resetToMid(); logInteraction($event)" class="bg-gray-600 py-2 px-4 rounded-lg">Reset</button>
<button id="confirmed-min-continue-btn" (click)="nextPhase(); logInteraction($event)" class="bg-blue-600 py-2 px-6 rounded-lg">Weiter zu Maximum</button>
</div>
</div>
</ng-container>
<!-- Phase: Max -->
<ng-container *ngIf="phase==='max'">
<div>Maximum setzen</div>
<div class="flex items-center space-x-4">
<button id="max-decrease-btn" (click)="decrease(); logInteraction($event)"
class="w-10 h-10 bg-gray-600 rounded-full">–</button>
<span>{{ currentSize }}px</span>
<button id="max-increase-btn" (click)="increase(); logInteraction($event)"
class="w-10 h-10 bg-gray-600 rounded-full">+</button>
<div *ngIf="isDescriptionVisible" class="relative bg-black/30 p-3 pr-8 rounded-lg text-center w-[280px]">
<p>Maximum setzen. Der Text sollte komplett lesbar sein, ohne das Gerät zu bewegen.</p>
<button (click)="toggleDescription()" class="absolute top-1 right-2 text-2xl leading-none">&times;</button>
</div>
<div class="flex flex-col items-center space-y-4">
<div class="flex items-center space-x-4">
<button id="max-decrease-btn" (click)="decrease(); logInteraction($event)" class="w-10 h-10 bg-gray-600 rounded-full">–</button>
<span>{{ currentSize }}px</span>
<button id="max-increase-btn" (click)="increase(); logInteraction($event)" class="w-10 h-10 bg-gray-600 rounded-full">+</button>
</div>
<button id="max-confirm-btn" (click)="nextPhase(); logInteraction($event)" class="bg-green-600 py-2 px-6 rounded-lg">Maximum bestätigen</button>
</div>
<button id="max-confirm-btn" (click)="nextPhase(); logInteraction($event)"
class="bg-green-600 py-2 px-6 rounded-lg">Maximum bestätigen</button>
</ng-container>
<!-- Phase: Confirmed Max -->
<ng-container *ngIf="phase==='confirmedMax'">
<div>Maximum bestätigt: {{ maxSizeResult }}px</div>
<button id="confirmed-max-continue-btn" (click)="nextPhase(); logInteraction($event)"
class="bg-blue-600 py-2 px-6 rounded-lg">Weiter zu optiomal</button>
<div class="flex flex-col items-center space-y-4">
<div>Maximum bestätigt: {{ maxSizeResult }}px</div>
<button id="confirmed-max-continue-btn" (click)="nextPhase(); logInteraction($event)" class="bg-blue-600 py-2 px-6 rounded-lg">Weiter zu Optimal</button>
</div>
</ng-container>
<!-- Phase: Comfortable -->
<ng-container *ngIf="phase==='comfortable'">
<div>Optimale Größe setzen</div>
<div class="flex items-center space-x-4">
<button id="comfort-decrease-btn" (click)="decrease(); logInteraction($event)"
class="w-10 h-10 bg-gray-600 rounded-full">–</button>
<span>{{ currentSize }}px</span>
<button id="comfort-increase-btn" (click)="increase(); logInteraction($event)"
class="w-10 h-10 bg-gray-600 rounded-full">+</button>
<div *ngIf="isDescriptionVisible" class="relative bg-black/30 p-3 pr-8 rounded-lg text-center w-[280px]">
<p>Optimale Größe setzen.</p>
<button (click)="toggleDescription()" class="absolute top-1 right-2 text-2xl leading-none">&times;</button>
</div>
<div class="flex flex-col items-center space-y-4">
<div class="flex items-center space-x-4">
<button id="comfort-decrease-btn" (click)="decrease(); logInteraction($event)" class="w-10 h-10 bg-gray-600 rounded-full">–</button>
<span>{{ currentSize }}px</span>
<button id="comfort-increase-btn" (click)="increase(); logInteraction($event)" class="w-10 h-10 bg-gray-600 rounded-full">+</button>
</div>
<button id="comfort-confirm-btn" (click)="nextPhase(); logInteraction($event)" class="bg-green-600 py-2 px-6 rounded-lg">Optimale Größe bestätigen</button>
</div>
<button id="comfort-confirm-btn" (click)="nextPhase(); logInteraction($event)"
class="bg-green-600 py-2 px-6 rounded-lg">Optimale Größe setzen</button>
</ng-container>
<!-- Phase: Confirmed Comfortable -->
<ng-container *ngIf="phase==='confirmedComfort'">
<div>Optimale Größe bestätigt: {{ comfortableSizeResult }}px</div>
<button id="finish-assessment-btn" (click)="finishAssessment(); logInteraction($event)"
class="bg-blue-600 py-2 px-6 rounded-lg">Test beenden</button>
<div class="flex flex-col items-center space-y-4">
<div>Optimale Größe bestätigt: {{ comfortableSizeResult }}px</div>
<button id="finish-assessment-btn" (click)="finishAssessment(); logInteraction($event)" class="bg-blue-600 py-2 px-6 rounded-lg">Test beenden</button>
</div>
</ng-container>
</div>
</model-viewer>
</div>
@@ -4,18 +4,17 @@ import {
OnDestroy,
ViewChild,
ElementRef,
ChangeDetectorRef,
CUSTOM_ELEMENTS_SCHEMA,
EventEmitter,
Output,
inject
inject,
NgZone
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MetricsTrackerService } from '../../../../services/metrics-tracker.service';
import '../../../../../assets/scripts/model-viewer';
@Component({
selector: 'app-text-legibility-assessment',
standalone: true,
@@ -29,58 +28,79 @@ export class TextLegibilityAssessmentComponent implements AfterViewInit, OnDestr
@Output() testComplete = new EventEmitter<void>();
@Output() redoTest = new EventEmitter<number>();
private metricsService = inject(MetricsTrackerService);
private zone = inject(NgZone);
public isArActive = false;
public isModelPlaced = false;
public isDescriptionVisible = true;
minSize = 2;
maxSize = 64;
currentSize = 16;
phase: 'min' | 'confirmedMin' | 'max' | 'confirmedMax' | 'comfortable' | 'confirmedComfort' = 'min';
minSizeResult: number | null = null;
maxSizeResult: number | null = null;
comfortableSizeResult: number | null = null;
phase: 'min' | 'confirmedMin' | 'max' | 'confirmedMax' | 'comfortable' | 'confirmedComfort' = 'min';
private offsetApplied = false;
constructor(private cdr: ChangeDetectorRef) {
constructor() {
this.logInteraction = this.logInteraction.bind(this);
}
ngAfterViewInit() {
const mv = this.modelViewerRef.nativeElement;
mv.addEventListener('ar-status', (event: any) => {
if (event.detail.status === 'session-started') {
console.log('AR session started. Delaying startTracking call to ensure session is ready.');
this.metricsService.startTracking(mv, "text-legibility");
}
this.zone.run(() => {
const status = event.detail.status;
switch (status) {
case 'session-started':
this.isArActive = true;
this.isModelPlaced = false;
this.isDescriptionVisible = true;
this.metricsService.startTracking(mv, "text-legibility");
break;
case 'object-placed':
if (this.isArActive && !this.isModelPlaced) {
this.isModelPlaced = true;
}
break;
case 'not-presenting':
this.isArActive = false;
this.isModelPlaced = false;
this.resetComponentState();
break;
}
});
});
}
private resetComponentState() {
this.phase = 'min';
this.currentSize = 16;
this.minSizeResult = null;
this.maxSizeResult = null;
this.comfortableSizeResult = null;
this.isDescriptionVisible = true;
}
public logInteraction(event: Event) {
this.metricsService.logInteraction(event);
}
toggleDescription() {
this.isDescriptionVisible = !this.isDescriptionVisible;
}
decrease() {
if (this.currentSize > this.minSize) this.currentSize--;
}
increase() {
if (this.currentSize < this.maxSize) this.currentSize++;
}
nextPhase() {
this.isDescriptionVisible = true;
switch (this.phase) {
case 'min':
this.minSizeResult = this.currentSize;
@@ -107,49 +127,24 @@ export class TextLegibilityAssessmentComponent implements AfterViewInit, OnDestr
break;
}
}
resetToMid() {
this.currentSize = Math.floor((this.minSize + this.maxSize) / 2);
this.phase = 'min';
this.minSizeResult = null;
this.maxSizeResult = null;
this.comfortableSizeResult = null;
this.resetComponentState();
}
retry() {
this.redoTest.emit(1);
}
finishAssessment() {
const finalResults = {
minReadableSize: this.minSizeResult,
maxReadableSize: this.maxSizeResult,
comfortableReadableSize: this.comfortableSizeResult
};
this.metricsService.logInteraction(new CustomEvent('test-results', {
detail: {
testName: 'TextLegibility',
results: finalResults
}
detail: { testName: 'TextLegibility', results: finalResults }
}));
console.log(finalResults);
this.testComplete.emit();
}
ngOnDestroy() {
this.metricsService.stopTracking();
}
}
@@ -63,9 +63,9 @@
<div class="text-black-600">
<h2 class="text-base mb-2">In diesem Testabschnitt untersuchen wir die ergonomisch optimale Positionierung eines virtuellen Objekts an der Decke.</h2>
<ul class="list-disc pl-4 space-y-1">
<li>Positionieren Sie das virtuelle Objekt mithilfe der Steuerung an der Decke.</li>
<li>Das Ziel ist, eine Position zu finden, die es Ihnen erlaubt, das Objekt für 20 Sekunden mit möglichst geringer körperlicher Anstrengung zu betrachten.</li>
<li>Bestätigen Sie Ihre Auswahl, um die 20-sekündige Haltephase zu starten.</li>
<li>Positionieren Sie das virtuelle Objekt mithilfe der Steuerung an der Decke.</li>
<li>Bestätigen Sie Ihre Auswahl, um den 20 Sekunden Timer zu starten.</li>
</ul>
</div>
</div>