fix model-viewer placement and
This commit is contained in:
1 parent
1bb248de1b
commit
83c4ece1e8
8 files changed
+328
-312
No files matched your search
-16
@@ -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>
|
||||
|
||||
|
||||
+26
-16
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+54
-48
@@ -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>
|
||||
+87
-70
@@ -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();
|
||||
}
|
||||
}
|
||||
+53
-51
@@ -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">×</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">×</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">×</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>
|
||||
+46
-51
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user