fix model-viewer placement and
parent
1bb248de1b
commit
83c4ece1e8
|
|
@ -108,22 +108,6 @@
|
||||||
<option value="prefer-not-to-say">Keine Angabe</option>
|
<option value="prefer-not-to-say">Keine Angabe</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import {
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
ChangeDetectorRef,
|
|
||||||
CUSTOM_ELEMENTS_SCHEMA,
|
CUSTOM_ELEMENTS_SCHEMA,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Output,
|
Output,
|
||||||
inject
|
inject,
|
||||||
|
NgZone
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
@ -30,15 +30,17 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDest
|
||||||
@Output() redoTest = new EventEmitter<number>();
|
@Output() redoTest = new EventEmitter<number>();
|
||||||
|
|
||||||
private metricsService = inject(MetricsTrackerService);
|
private metricsService = inject(MetricsTrackerService);
|
||||||
|
private zone = inject(NgZone);
|
||||||
public scale = 1;
|
public scale = 1;
|
||||||
public verticalOffset = 0;
|
public verticalOffset = 0;
|
||||||
protected isModelPlaced = false;
|
protected isModelPlaced = false;
|
||||||
private initialArAnchor: { x: number; y: number; z: number } | null = null;
|
private initialArAnchor: { x: number; y: number; z: number } | null = null;
|
||||||
|
private hasStartedTracking = false;
|
||||||
|
|
||||||
constructor(private cdr: ChangeDetectorRef) {
|
constructor() {
|
||||||
this.logInteraction = this.logInteraction.bind(this);
|
this.logInteraction = this.logInteraction.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.metricsService.stopTracking();
|
this.metricsService.stopTracking();
|
||||||
}
|
}
|
||||||
|
|
@ -46,10 +48,19 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDest
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
const mv = this.modelViewerRef.nativeElement;
|
const mv = this.modelViewerRef.nativeElement;
|
||||||
mv.addEventListener('ar-status', (e: any) => {
|
mv.addEventListener('ar-status', (e: any) => {
|
||||||
if (e.detail.status === 'session-started') {
|
this.zone.run(() => {
|
||||||
setTimeout(() => this.captureAnchor(), 500);
|
const status = e.detail.status;
|
||||||
this.metricsService.startTracking(mv, "position");
|
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);
|
this.metricsService.logInteraction(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async captureAnchor() {
|
private captureAnchor() {
|
||||||
const mv = this.modelViewerRef.nativeElement;
|
const mv = this.modelViewerRef.nativeElement;
|
||||||
const anchor = mv.getAnchor();
|
const anchor = mv.getAnchor();
|
||||||
if (!anchor.includes('not placed')) {
|
if (anchor && !anchor.includes('not placed')) {
|
||||||
const [x, y, z] = anchor.split(' ').map(parseFloat);
|
const [x, y, z] = anchor.split(' ').map(parseFloat);
|
||||||
this.initialArAnchor = { x, y, z };
|
this.initialArAnchor = { x, y, z };
|
||||||
this.isModelPlaced = true;
|
this.isModelPlaced = true;
|
||||||
this.cdr.detectChanges();
|
console.log('Anchor captured successfully:', this.initialArAnchor);
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => this.captureAnchor(), 500);
|
console.warn('Object placed, but anchor not ready. Retrying once.');
|
||||||
|
setTimeout(() => this.captureAnchor(), 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSliderInput() {
|
onSliderInput() {
|
||||||
if (!this.initialArAnchor) return;
|
if (!this.initialArAnchor) return;
|
||||||
const { x, y, z } = this.initialArAnchor!;
|
const { x, y, z } = this.initialArAnchor;
|
||||||
const newY = y + this.verticalOffset;
|
const newY = y + this.verticalOffset;
|
||||||
this.modelViewerRef.nativeElement.setAttribute('ar-anchor', `${x} ${newY} ${z}`);
|
this.modelViewerRef.nativeElement.setAttribute('ar-anchor', `${x} ${newY} ${z}`);
|
||||||
|
this.modelViewerRef.nativeElement.scale = `${this.scale} ${this.scale} ${this.scale}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
resetPosition() {
|
resetPosition() {
|
||||||
|
|
@ -99,13 +112,10 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDest
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log('Spatial position results captured locally.');
|
console.log('Spatial position results captured locally.');
|
||||||
|
|
||||||
this.testComplete.emit();
|
this.testComplete.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
retryCurrent() {
|
retryCurrent() {
|
||||||
this.redoTest.emit(1);
|
this.redoTest.emit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,60 +17,65 @@
|
||||||
WebXR Umgebung laden
|
WebXR Umgebung laden
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Adjustment Phase Controls -->
|
<div *ngIf="isArActive"
|
||||||
<div *ngIf="currentPhase === 1 && isModelPlaced"
|
class="absolute bottom-24 left-1/2 -translate-x-1/2 transition-opacity duration-300"
|
||||||
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">
|
[class.opacity-50]="!isModelPlaced"
|
||||||
|
[class.pointer-events-none]="!isModelPlaced">
|
||||||
<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 class="flex flex-col items-center text-white w-64">
|
<!-- Adjustment Phase Controls -->
|
||||||
<label for="stability-offset-slider" class="text-sm font-medium mb-2">Höhe verschieben</label>
|
<div *ngIf="currentPhase === 1"
|
||||||
<input
|
class="ar-controls flex flex-col items-center space-y-6 bg-black bg-opacity-70 p-6 rounded-xl">
|
||||||
id="stability-offset-slider"
|
|
||||||
type="range"
|
<div class="text-white text-center mb-4">
|
||||||
min="-1.5"
|
<h4 id="adjust-phase-title" class="text-lg font-semibold">Finden Sie Ihren Optionale Betrachtungsposition</h4>
|
||||||
max="1.5"
|
</div>
|
||||||
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>
|
|
||||||
|
|
||||||
<button
|
<div class="flex flex-col items-center text-white w-64">
|
||||||
id="lock-position-btn"
|
<label for="stability-offset-slider" class="text-sm font-medium mb-2">Höhe verschieben</label>
|
||||||
(click)="lockPosition(); logInteraction($event)"
|
<input
|
||||||
class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg text-lg">
|
id="stability-offset-slider"
|
||||||
🔒 Diese Position fixieren.
|
type="range"
|
||||||
</button>
|
min="-1.5"
|
||||||
</div>
|
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
|
<button
|
||||||
id="adjust-more-btn"
|
id="lock-position-btn"
|
||||||
(click)="adjustMore(); logInteraction($event)"
|
(click)="lockPosition(); logInteraction($event)"
|
||||||
class="bg-gray-600 hover:bg-gray-700 text-white py-2 px-4 rounded-lg">
|
class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg text-lg">
|
||||||
Anpassen
|
🔒 Diese Position fixieren.
|
||||||
</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>
|
</button>
|
||||||
</div>
|
</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 -->
|
<!-- Countdown Phase -->
|
||||||
<div *ngIf="currentPhase === 3"
|
<div *ngIf="currentPhase === 3"
|
||||||
|
|
@ -117,5 +122,6 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</model-viewer>
|
</model-viewer>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
ViewChild,
|
ViewChild,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
CUSTOM_ELEMENTS_SCHEMA,
|
CUSTOM_ELEMENTS_SCHEMA,
|
||||||
ChangeDetectorRef,
|
NgZone,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Output,
|
Output,
|
||||||
inject
|
inject
|
||||||
|
|
@ -13,7 +13,6 @@ import {
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { MetricsTrackerService } from '../../../../services/metrics-tracker.service';
|
import { MetricsTrackerService } from '../../../../services/metrics-tracker.service';
|
||||||
|
|
||||||
import '../../../../../assets/scripts/model-viewer';
|
import '../../../../../assets/scripts/model-viewer';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|
@ -24,68 +23,99 @@ import '../../../../../assets/scripts/model-viewer';
|
||||||
styleUrls: ['./spatial-stability-assessment.component.css'],
|
styleUrls: ['./spatial-stability-assessment.component.css'],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDestroy { // <-- Implement OnDestroy
|
export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDestroy {
|
||||||
@ViewChild('modelViewer') modelViewerRef!: ElementRef<any>;
|
@ViewChild('modelViewer') modelViewerRef!: ElementRef<any>;
|
||||||
@Output() testComplete = new EventEmitter<void>();
|
@Output() testComplete = new EventEmitter<void>();
|
||||||
@Output() redoTest = new EventEmitter<number>();
|
@Output() redoTest = new EventEmitter<number>();
|
||||||
|
|
||||||
private metricsService = inject(MetricsTrackerService);
|
private metricsService = inject(MetricsTrackerService);
|
||||||
|
private zone = inject(NgZone);
|
||||||
|
|
||||||
|
public isArActive = false;
|
||||||
|
public isModelPlaced = false;
|
||||||
|
|
||||||
public verticalOffset = 0;
|
public verticalOffset = 0;
|
||||||
public currentPhase = 0; // 0: waiting, 1: adjust, 2: lock, 3: countdown, 4: complete
|
public currentPhase = 0;
|
||||||
public remainingTime = 20;
|
public remainingTime = 20;
|
||||||
public progressPercentage = 0;
|
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);
|
this.logInteraction = this.logInteraction.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
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) {
|
this.zone.run(() => {
|
||||||
setTimeout(() => this.getInitialAnchor(), 500);
|
const status = event.detail.status;
|
||||||
this.metricsService.startTracking(mv, "stability");
|
|
||||||
}
|
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) {
|
private resetComponentState() {
|
||||||
this.metricsService.logInteraction(event);
|
this.clearCountdown();
|
||||||
|
this.initialArAnchor = null;
|
||||||
|
this.lockedPosition = null;
|
||||||
|
this.verticalOffset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getInitialAnchor() {
|
private getInitialAnchor() {
|
||||||
const modelViewer = this.modelViewerRef.nativeElement;
|
const mv = this.modelViewerRef.nativeElement;
|
||||||
const anchorString = modelViewer.getAnchor();
|
const anchorString = mv.getAnchor();
|
||||||
if (!anchorString.includes('not placed')) {
|
|
||||||
|
if (anchorString && !anchorString.includes('not placed')) {
|
||||||
const coords = anchorString.split(' ').map(parseFloat);
|
const coords = anchorString.split(' ').map(parseFloat);
|
||||||
this.initialArAnchor = { x: coords[0], y: coords[1], z: coords[2] };
|
if (coords.length === 3 && !coords.some(isNaN)) {
|
||||||
this.isModelPlaced = true;
|
this.initialArAnchor = { x: coords[0], y: coords[1], z: coords[2] };
|
||||||
this.currentPhase = 1;
|
}
|
||||||
this.cdr.detectChanges();
|
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => this.getInitialAnchor(), 500);
|
setTimeout(() => this.getInitialAnchor(), 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSliderInput() {
|
onSliderInput() {
|
||||||
if (this.initialArAnchor && this.currentPhase === 1) {
|
if (this.initialArAnchor && this.currentPhase === 1) {
|
||||||
|
const { x, z } = this.initialArAnchor;
|
||||||
const newY = this.initialArAnchor.y + this.verticalOffset;
|
const newY = this.initialArAnchor.y + this.verticalOffset;
|
||||||
const anchor = `${this.initialArAnchor.x} ${newY} ${this.initialArAnchor.z}`;
|
this.modelViewerRef.nativeElement.setAttribute('ar-anchor', `${x} ${newY} ${z}`);
|
||||||
this.modelViewerRef.nativeElement.setAttribute('ar-anchor', anchor);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public logInteraction(event: Event) {
|
||||||
|
this.metricsService.logInteraction(event);
|
||||||
|
}
|
||||||
|
|
||||||
lockPosition() {
|
lockPosition() {
|
||||||
this.lockedPosition = {
|
this.lockedPosition = { verticalOffset: this.verticalOffset };
|
||||||
verticalOffset: this.verticalOffset,
|
|
||||||
anchor: this.modelViewerRef.nativeElement.getAttribute('ar-anchor')
|
|
||||||
};
|
|
||||||
this.currentPhase = 2;
|
this.currentPhase = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,64 +127,53 @@ export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDes
|
||||||
this.currentPhase = 3;
|
this.currentPhase = 3;
|
||||||
this.remainingTime = 20;
|
this.remainingTime = 20;
|
||||||
this.progressPercentage = 0;
|
this.progressPercentage = 0;
|
||||||
|
|
||||||
this.countdownInterval = setInterval(() => {
|
this.countdownInterval = setInterval(() => {
|
||||||
this.remainingTime--;
|
this.zone.run(() => {
|
||||||
this.progressPercentage = ((20 - this.remainingTime) / 20) * 100;
|
this.remainingTime--;
|
||||||
|
this.progressPercentage = ((20 - this.remainingTime) / 20) * 100;
|
||||||
if (this.remainingTime <= 0) {
|
if (this.remainingTime <= 0) {
|
||||||
this.completeTest(true);
|
this.completeTest(true);
|
||||||
}
|
}
|
||||||
this.cdr.detectChanges();
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private clearCountdown() {
|
||||||
|
if (this.countdownInterval) {
|
||||||
|
clearInterval(this.countdownInterval);
|
||||||
|
this.countdownInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cancelTest() {
|
cancelTest() {
|
||||||
if (this.countdownInterval) {
|
this.clearCountdown();
|
||||||
clearInterval(this.countdownInterval);
|
|
||||||
this.countdownInterval = null;
|
|
||||||
}
|
|
||||||
this.currentPhase = 2;
|
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) {
|
private completeTest(wasCompleted: boolean) {
|
||||||
if (this.countdownInterval) {
|
this.clearCountdown();
|
||||||
clearInterval(this.countdownInterval);
|
|
||||||
this.countdownInterval = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalResults = {
|
const finalResults = {
|
||||||
testCompletedSuccessfully: wasCompleted,
|
testCompletedSuccessfully: wasCompleted,
|
||||||
initialAnchor: this.initialArAnchor,
|
initialAnchor: this.initialArAnchor,
|
||||||
lockedPosition: this.lockedPosition,
|
lockedPosition: this.lockedPosition,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.metricsService.logInteraction(new CustomEvent('test-results', {
|
this.metricsService.logInteraction(new CustomEvent('test-results', {
|
||||||
detail: {
|
detail: { testName: 'SpatialStability', results: finalResults }
|
||||||
testName: 'SpatialStability',
|
|
||||||
results: finalResults
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (wasCompleted) {
|
if (wasCompleted) {
|
||||||
this.currentPhase = 4;
|
this.currentPhase = 4;
|
||||||
}
|
this.testComplete.emit();
|
||||||
|
|
||||||
if(wasCompleted) {
|
|
||||||
this.testComplete.emit();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
restartTest() {
|
restartTest() {
|
||||||
|
this.clearCountdown();
|
||||||
this.currentPhase = 1;
|
this.currentPhase = 1;
|
||||||
this.remainingTime = 20;
|
|
||||||
this.progressPercentage = 0;
|
|
||||||
this.lockedPosition = null;
|
this.lockedPosition = null;
|
||||||
if (this.countdownInterval) {
|
this.verticalOffset = 0;
|
||||||
clearInterval(this.countdownInterval);
|
this.onSliderInput();
|
||||||
this.countdownInterval = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
finishAssessment() {
|
finishAssessment() {
|
||||||
|
|
@ -162,9 +181,7 @@ export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDes
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
if (this.countdownInterval) {
|
this.clearCountdown();
|
||||||
clearInterval(this.countdownInterval);
|
|
||||||
}
|
|
||||||
this.metricsService.stopTracking();
|
this.metricsService.stopTracking();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,11 @@
|
||||||
src="https://modelviewer.dev/shared-assets/models/Astronaut.glb" ar ar-modes="webxr" ar-placement="ceiling"
|
src="https://modelviewer.dev/shared-assets/models/Astronaut.glb" ar ar-modes="webxr" ar-placement="ceiling"
|
||||||
reveal="manual" camera-orbit="0deg 75deg 2m">
|
reveal="manual" camera-orbit="0deg 75deg 2m">
|
||||||
|
|
||||||
|
|
||||||
<button slot="ar-button" id="ar-button-legibility" (click)="logInteraction($event)"
|
<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">
|
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
|
WebXR Umgebung laden
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
<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;">
|
||||||
|
|
@ -17,85 +15,89 @@
|
||||||
labore et
|
labore et
|
||||||
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
|
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
|
||||||
Stet
|
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>
|
</button>
|
||||||
|
|
||||||
|
<div *ngIf="isArActive"
|
||||||
<div
|
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="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">
|
[class.opacity-50]="!isModelPlaced" [class.pointer-events-none]="!isModelPlaced">
|
||||||
|
|
||||||
|
|
||||||
<!-- Phase: Min -->
|
<!-- Phase: Min -->
|
||||||
<ng-container *ngIf="phase==='min'">
|
<ng-container *ngIf="phase==='min'">
|
||||||
<div>Minimum setzen</div>
|
<div *ngIf="isDescriptionVisible" class="relative bg-black/30 p-3 pr-8 rounded-lg text-center w-[280px]">
|
||||||
<div class="flex items-center space-x-4">
|
<p>Minimum setzen. Der Text sollte gerade so noch lesbar sein. Den Arm hierfür bitte ganz austrecken.</p>
|
||||||
<button id="min-decrease-btn" (click)="decrease(); logInteraction($event)"
|
<button (click)="toggleDescription()" class="absolute top-1 right-2 text-2xl leading-none">×</button>
|
||||||
class="w-10 h-10 bg-gray-600 rounded-full">–</button>
|
</div>
|
||||||
<span>{{ currentSize }}px</span>
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<button id="min-increase-btn" (click)="increase(); logInteraction($event)"
|
<div class="flex items-center space-x-4">
|
||||||
class="w-10 h-10 bg-gray-600 rounded-full">+</button>
|
<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>
|
</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>
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
<!-- Phase: Confirmed Min -->
|
<!-- Phase: Confirmed Min -->
|
||||||
<ng-container *ngIf="phase==='confirmedMin'">
|
<ng-container *ngIf="phase==='confirmedMin'">
|
||||||
<div>Minimum bestätigt: {{ minSizeResult }}px</div>
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<button id="confirmed-min-reset-btn" (click)="resetToMid(); logInteraction($event)"
|
<div>Minimum bestätigt: {{ minSizeResult }}px</div>
|
||||||
class="bg-gray-600 py-2 px-4 rounded-lg">Reset</button>
|
<div class="flex space-x-2">
|
||||||
<button id="confirmed-min-continue-btn" (click)="nextPhase(); logInteraction($event)"
|
<button id="confirmed-min-reset-btn" (click)="resetToMid(); logInteraction($event)" class="bg-gray-600 py-2 px-4 rounded-lg">Reset</button>
|
||||||
class="bg-blue-600 py-2 px-6 rounded-lg">Weiter zu Maximum</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>
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
<!-- Phase: Max -->
|
<!-- Phase: Max -->
|
||||||
<ng-container *ngIf="phase==='max'">
|
<ng-container *ngIf="phase==='max'">
|
||||||
<div>Maximum setzen</div>
|
<div *ngIf="isDescriptionVisible" class="relative bg-black/30 p-3 pr-8 rounded-lg text-center w-[280px]">
|
||||||
<div class="flex items-center space-x-4">
|
<p>Maximum setzen. Der Text sollte komplett lesbar sein, ohne das Gerät zu bewegen.</p>
|
||||||
<button id="max-decrease-btn" (click)="decrease(); logInteraction($event)"
|
<button (click)="toggleDescription()" class="absolute top-1 right-2 text-2xl leading-none">×</button>
|
||||||
class="w-10 h-10 bg-gray-600 rounded-full">–</button>
|
</div>
|
||||||
<span>{{ currentSize }}px</span>
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<button id="max-increase-btn" (click)="increase(); logInteraction($event)"
|
<div class="flex items-center space-x-4">
|
||||||
class="w-10 h-10 bg-gray-600 rounded-full">+</button>
|
<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>
|
</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>
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
<!-- Phase: Confirmed Max -->
|
<!-- Phase: Confirmed Max -->
|
||||||
<ng-container *ngIf="phase==='confirmedMax'">
|
<ng-container *ngIf="phase==='confirmedMax'">
|
||||||
<div>Maximum bestätigt: {{ maxSizeResult }}px</div>
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<button id="confirmed-max-continue-btn" (click)="nextPhase(); logInteraction($event)"
|
<div>Maximum bestätigt: {{ maxSizeResult }}px</div>
|
||||||
class="bg-blue-600 py-2 px-6 rounded-lg">Weiter zu optiomal</button>
|
<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>
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
<!-- Phase: Comfortable -->
|
<!-- Phase: Comfortable -->
|
||||||
<ng-container *ngIf="phase==='comfortable'">
|
<ng-container *ngIf="phase==='comfortable'">
|
||||||
<div>Optimale Größe setzen</div>
|
<div *ngIf="isDescriptionVisible" class="relative bg-black/30 p-3 pr-8 rounded-lg text-center w-[280px]">
|
||||||
<div class="flex items-center space-x-4">
|
<p>Optimale Größe setzen.</p>
|
||||||
<button id="comfort-decrease-btn" (click)="decrease(); logInteraction($event)"
|
<button (click)="toggleDescription()" class="absolute top-1 right-2 text-2xl leading-none">×</button>
|
||||||
class="w-10 h-10 bg-gray-600 rounded-full">–</button>
|
</div>
|
||||||
<span>{{ currentSize }}px</span>
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<button id="comfort-increase-btn" (click)="increase(); logInteraction($event)"
|
<div class="flex items-center space-x-4">
|
||||||
class="w-10 h-10 bg-gray-600 rounded-full">+</button>
|
<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>
|
</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>
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
<!-- Phase: Confirmed Comfortable -->
|
<!-- Phase: Confirmed Comfortable -->
|
||||||
<ng-container *ngIf="phase==='confirmedComfort'">
|
<ng-container *ngIf="phase==='confirmedComfort'">
|
||||||
<div>Optimale Größe bestätigt: {{ comfortableSizeResult }}px</div>
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<button id="finish-assessment-btn" (click)="finishAssessment(); logInteraction($event)"
|
<div>Optimale Größe bestätigt: {{ comfortableSizeResult }}px</div>
|
||||||
class="bg-blue-600 py-2 px-6 rounded-lg">Test beenden</button>
|
<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>
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</model-viewer>
|
</model-viewer>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -4,18 +4,17 @@ import {
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
ChangeDetectorRef,
|
|
||||||
CUSTOM_ELEMENTS_SCHEMA,
|
CUSTOM_ELEMENTS_SCHEMA,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Output,
|
Output,
|
||||||
inject
|
inject,
|
||||||
|
NgZone
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { MetricsTrackerService } from '../../../../services/metrics-tracker.service';
|
import { MetricsTrackerService } from '../../../../services/metrics-tracker.service';
|
||||||
import '../../../../../assets/scripts/model-viewer';
|
import '../../../../../assets/scripts/model-viewer';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-text-legibility-assessment',
|
selector: 'app-text-legibility-assessment',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
|
@ -29,58 +28,79 @@ export class TextLegibilityAssessmentComponent implements AfterViewInit, OnDestr
|
||||||
@Output() testComplete = new EventEmitter<void>();
|
@Output() testComplete = new EventEmitter<void>();
|
||||||
@Output() redoTest = new EventEmitter<number>();
|
@Output() redoTest = new EventEmitter<number>();
|
||||||
|
|
||||||
|
|
||||||
private metricsService = inject(MetricsTrackerService);
|
private metricsService = inject(MetricsTrackerService);
|
||||||
|
private zone = inject(NgZone);
|
||||||
|
|
||||||
|
public isArActive = false;
|
||||||
|
public isModelPlaced = false;
|
||||||
|
public isDescriptionVisible = true;
|
||||||
|
|
||||||
minSize = 2;
|
minSize = 2;
|
||||||
maxSize = 64;
|
maxSize = 64;
|
||||||
currentSize = 16;
|
currentSize = 16;
|
||||||
|
phase: 'min' | 'confirmedMin' | 'max' | 'confirmedMax' | 'comfortable' | 'confirmedComfort' = 'min';
|
||||||
|
|
||||||
minSizeResult: number | null = null;
|
minSizeResult: number | null = null;
|
||||||
maxSizeResult: number | null = null;
|
maxSizeResult: number | null = null;
|
||||||
comfortableSizeResult: number | null = null;
|
comfortableSizeResult: number | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
phase: 'min' | 'confirmedMin' | 'max' | 'confirmedMax' | 'comfortable' | 'confirmedComfort' = 'min';
|
|
||||||
|
|
||||||
|
|
||||||
private offsetApplied = false;
|
|
||||||
|
|
||||||
|
|
||||||
constructor(private cdr: ChangeDetectorRef) {
|
|
||||||
this.logInteraction = this.logInteraction.bind(this);
|
this.logInteraction = this.logInteraction.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
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.zone.run(() => {
|
||||||
console.log('AR session started. Delaying startTracking call to ensure session is ready.');
|
const status = event.detail.status;
|
||||||
this.metricsService.startTracking(mv, "text-legibility");
|
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) {
|
public logInteraction(event: Event) {
|
||||||
this.metricsService.logInteraction(event);
|
this.metricsService.logInteraction(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleDescription() {
|
||||||
|
this.isDescriptionVisible = !this.isDescriptionVisible;
|
||||||
|
}
|
||||||
|
|
||||||
decrease() {
|
decrease() {
|
||||||
if (this.currentSize > this.minSize) this.currentSize--;
|
if (this.currentSize > this.minSize) this.currentSize--;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
increase() {
|
increase() {
|
||||||
if (this.currentSize < this.maxSize) this.currentSize++;
|
if (this.currentSize < this.maxSize) this.currentSize++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
nextPhase() {
|
nextPhase() {
|
||||||
|
this.isDescriptionVisible = true;
|
||||||
switch (this.phase) {
|
switch (this.phase) {
|
||||||
case 'min':
|
case 'min':
|
||||||
this.minSizeResult = this.currentSize;
|
this.minSizeResult = this.currentSize;
|
||||||
|
|
@ -107,49 +127,24 @@ export class TextLegibilityAssessmentComponent implements AfterViewInit, OnDestr
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetToMid() {
|
resetToMid() {
|
||||||
this.currentSize = Math.floor((this.minSize + this.maxSize) / 2);
|
this.resetComponentState();
|
||||||
this.phase = 'min';
|
|
||||||
this.minSizeResult = null;
|
|
||||||
this.maxSizeResult = null;
|
|
||||||
this.comfortableSizeResult = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
retry() {
|
|
||||||
this.redoTest.emit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
finishAssessment() {
|
finishAssessment() {
|
||||||
const finalResults = {
|
const finalResults = {
|
||||||
minReadableSize: this.minSizeResult,
|
minReadableSize: this.minSizeResult,
|
||||||
maxReadableSize: this.maxSizeResult,
|
maxReadableSize: this.maxSizeResult,
|
||||||
comfortableReadableSize: this.comfortableSizeResult
|
comfortableReadableSize: this.comfortableSizeResult
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
this.metricsService.logInteraction(new CustomEvent('test-results', {
|
this.metricsService.logInteraction(new CustomEvent('test-results', {
|
||||||
detail: {
|
detail: { testName: 'TextLegibility', results: finalResults }
|
||||||
testName: 'TextLegibility',
|
|
||||||
results: finalResults
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
console.log(finalResults);
|
|
||||||
|
|
||||||
|
|
||||||
this.testComplete.emit();
|
this.testComplete.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
|
||||||
|
|
||||||
this.metricsService.stopTracking();
|
this.metricsService.stopTracking();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,9 @@
|
||||||
<div class="text-black-600">
|
<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>
|
<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">
|
<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>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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -70014,6 +70014,12 @@ class ARRenderer extends EventDispatcher {
|
||||||
get currentGoalPosition() {
|
get currentGoalPosition() {
|
||||||
return this.goalPosition;
|
return this.goalPosition;
|
||||||
}
|
}
|
||||||
|
get currentPosition() {
|
||||||
|
if (this.presentedScene) {
|
||||||
|
return this.presentedScene.pivot.position;
|
||||||
|
}
|
||||||
|
return this.goalPosition;
|
||||||
|
}
|
||||||
get isObjectPlaced() {
|
get isObjectPlaced() {
|
||||||
return this.placementComplete;
|
return this.placementComplete;
|
||||||
}
|
}
|
||||||
|
|
@ -70378,28 +70384,26 @@ class ARRenderer extends EventDispatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
placeInitially() {
|
placeInitially() {
|
||||||
|
var _a;
|
||||||
const scene = this.presentedScene;
|
const scene = this.presentedScene;
|
||||||
const { pivot, element } = scene;
|
const { pivot, element } = scene;
|
||||||
const { position } = pivot;
|
const { position } = pivot;
|
||||||
const xrCamera = scene.getCamera();
|
const xrCamera = scene.getCamera();
|
||||||
if (this.parsedAnchorOffset != null) {
|
if (this.parsedAnchorOffset != null) {
|
||||||
// Set position directly from the provided anchor offset.
|
// Set position directly from the provided anchor offset.
|
||||||
// This position is relative to the initial XR reference space origin.
|
|
||||||
position.copy(this.parsedAnchorOffset);
|
position.copy(this.parsedAnchorOffset);
|
||||||
this.goalPosition.copy(this.parsedAnchorOffset);
|
this.goalPosition.copy(this.parsedAnchorOffset);
|
||||||
// Set up the scene for presentation
|
// Mark placement as complete and set up the scene
|
||||||
|
this.placementComplete = true;
|
||||||
|
this.worldSpaceInitialPlacementDone = true;
|
||||||
scene.setHotspotsVisibility(true);
|
scene.setHotspotsVisibility(true);
|
||||||
scene.visible = true;
|
scene.visible = true;
|
||||||
scene.setShadowIntensity(AR_SHADOW_INTENSITY);
|
scene.setShadowIntensity(AR_SHADOW_INTENSITY);
|
||||||
// Mark placement as complete to bypass further automatic placement.
|
|
||||||
this.placementComplete = true;
|
|
||||||
this.worldSpaceInitialPlacementDone = true;
|
|
||||||
// Hide the placement box as it's not needed.
|
|
||||||
if (this.placementBox) {
|
if (this.placementBox) {
|
||||||
this.placementBox.show = false;
|
this.placementBox.show = false;
|
||||||
}
|
}
|
||||||
this.dispatchEvent({ type: 'status', status: ARStatus.OBJECT_PLACED });
|
this.dispatchEvent({ type: 'status', status: ARStatus.OBJECT_PLACED });
|
||||||
// Enable user interaction controls for the appropriate mode.
|
// Enable user interaction controls
|
||||||
if (this.xrMode === XRMode.SCREEN_SPACE) {
|
if (this.xrMode === XRMode.SCREEN_SPACE) {
|
||||||
const { session } = this.frame;
|
const { session } = this.frame;
|
||||||
session.addEventListener('selectstart', this.onSelectStart);
|
session.addEventListener('selectstart', this.onSelectStart);
|
||||||
|
|
@ -70409,12 +70413,8 @@ class ARRenderer extends EventDispatcher {
|
||||||
}
|
}
|
||||||
else { // WORLD_SPACE
|
else { // WORLD_SPACE
|
||||||
this.enableWorldSpaceUserInteraction();
|
this.enableWorldSpaceUserInteraction();
|
||||||
// Hide the placement box again as it's not needed for manual anchoring.
|
|
||||||
if (this.placementBox) {
|
|
||||||
this.placementBox.show = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return; // Skip the rest of the automatic placement logic.
|
return;
|
||||||
}
|
}
|
||||||
const { width, height } = this.overlay.getBoundingClientRect();
|
const { width, height } = this.overlay.getBoundingClientRect();
|
||||||
scene.setSize(width, height);
|
scene.setSize(width, height);
|
||||||
|
|
@ -70425,10 +70425,11 @@ class ARRenderer extends EventDispatcher {
|
||||||
const cameraDirection = xrCamera.getWorldDirection(vector3$1);
|
const cameraDirection = xrCamera.getWorldDirection(vector3$1);
|
||||||
scene.yaw = Math.atan2(-cameraDirection.x, -cameraDirection.z) - theta;
|
scene.yaw = Math.atan2(-cameraDirection.x, -cameraDirection.z) - theta;
|
||||||
this.goalYaw = scene.yaw;
|
this.goalYaw = scene.yaw;
|
||||||
|
// Defer placement if ceiling is the target but view is not pointing up.
|
||||||
if (this.placeOnCeiling && !this.isViewPointingUp()) {
|
if (this.placeOnCeiling && !this.isViewPointingUp()) {
|
||||||
scene.visible = false; // Hide until properly oriented
|
scene.visible = false;
|
||||||
scene.setHotspotsVisibility(true); // Still show UI
|
scene.setHotspotsVisibility(true);
|
||||||
// Set up touch interaction for screen-space mode
|
// Set up touch interaction for screen-space mode even if deferred
|
||||||
if (this.xrMode === XRMode.SCREEN_SPACE) {
|
if (this.xrMode === XRMode.SCREEN_SPACE) {
|
||||||
const { session } = this.frame;
|
const { session } = this.frame;
|
||||||
session.addEventListener('selectstart', this.onSelectStart);
|
session.addEventListener('selectstart', this.onSelectStart);
|
||||||
|
|
@ -70436,28 +70437,21 @@ class ARRenderer extends EventDispatcher {
|
||||||
session.requestHitTestSourceForTransientInput({ profile: 'generic-touchscreen' })
|
session.requestHitTestSourceForTransientInput({ profile: 'generic-touchscreen' })
|
||||||
.then(hitTestSource => { this.transientHitTestSource = hitTestSource; });
|
.then(hitTestSource => { this.transientHitTestSource = hitTestSource; });
|
||||||
}
|
}
|
||||||
return; // Exit early - don't place yet
|
return; // Exit early, placement will be handled by checkForDeferredCeilingPlacement
|
||||||
}
|
}
|
||||||
// Use different placement logic for world-space vs screen-space
|
// --- Main Placement Logic ---
|
||||||
if (this.xrMode === XRMode.WORLD_SPACE && !this.worldSpaceInitialPlacementDone) {
|
if (this.xrMode === XRMode.WORLD_SPACE) {
|
||||||
// Use automatic optimal placement for world-space AR only on first session
|
|
||||||
const { position: optimalPosition, scale: optimalScale } = this.calculateWorldSpaceOptimalPlacement(scene, xrCamera);
|
const { position: optimalPosition, scale: optimalScale } = this.calculateWorldSpaceOptimalPlacement(scene, xrCamera);
|
||||||
this.goalPosition.copy(optimalPosition);
|
this.goalPosition.copy(optimalPosition);
|
||||||
this.goalScale = optimalScale;
|
this.goalScale = optimalScale;
|
||||||
// Store the initial scale for toggle functionality
|
|
||||||
this.initialModelScale = optimalScale;
|
this.initialModelScale = optimalScale;
|
||||||
// Set initial position and scale immediately for world-space
|
|
||||||
position.copy(optimalPosition);
|
position.copy(optimalPosition);
|
||||||
pivot.scale.set(optimalScale, optimalScale, optimalScale);
|
pivot.scale.set(optimalScale, optimalScale, optimalScale);
|
||||||
// Mark that initial placement is done
|
|
||||||
this.worldSpaceInitialPlacementDone = true;
|
this.worldSpaceInitialPlacementDone = true;
|
||||||
// Calculate scale limits for world-space mode (SVXR logic)
|
|
||||||
this.calculateWorldSpaceScaleLimits(scene);
|
this.calculateWorldSpaceScaleLimits(scene);
|
||||||
// Enable user interaction after initial placement
|
|
||||||
this.enableWorldSpaceUserInteraction();
|
this.enableWorldSpaceUserInteraction();
|
||||||
}
|
}
|
||||||
else if (this.xrMode === XRMode.SCREEN_SPACE) {
|
else { // SCREEN_SPACE
|
||||||
// Use original placement logic for screen-space AR
|
|
||||||
const radius = Math.max(1, 2 * scene.boundingSphere.radius);
|
const radius = Math.max(1, 2 * scene.boundingSphere.radius);
|
||||||
position.copy(xrCamera.position)
|
position.copy(xrCamera.position)
|
||||||
.add(cameraDirection.multiplyScalar(radius));
|
.add(cameraDirection.multiplyScalar(radius));
|
||||||
|
|
@ -70466,8 +70460,17 @@ class ARRenderer extends EventDispatcher {
|
||||||
position.add(target).sub(this.oldTarget);
|
position.add(target).sub(this.oldTarget);
|
||||||
this.goalPosition.copy(position);
|
this.goalPosition.copy(position);
|
||||||
}
|
}
|
||||||
|
// --- Finalize Placement ---
|
||||||
|
this.placementComplete = true;
|
||||||
|
scene.visible = true;
|
||||||
scene.setHotspotsVisibility(true);
|
scene.setHotspotsVisibility(true);
|
||||||
scene.visible = true; // Model is properly oriented, show it
|
if (this.placementBox) {
|
||||||
|
this.placementBox.show = false;
|
||||||
|
}
|
||||||
|
// Set shadow and dispatch the crucial event
|
||||||
|
(_a = this.presentedScene) === null || _a === void 0 ? void 0 : _a.setShadowIntensity(AR_SHADOW_INTENSITY);
|
||||||
|
this.dispatchEvent({ type: 'status', status: ARStatus.OBJECT_PLACED });
|
||||||
|
// Setup user interaction for screen-space after placement
|
||||||
if (this.xrMode === XRMode.SCREEN_SPACE) {
|
if (this.xrMode === XRMode.SCREEN_SPACE) {
|
||||||
const { session } = this.frame;
|
const { session } = this.frame;
|
||||||
session.addEventListener('selectstart', this.onSelectStart);
|
session.addEventListener('selectstart', this.onSelectStart);
|
||||||
|
|
@ -70518,8 +70521,10 @@ class ARRenderer extends EventDispatcher {
|
||||||
session.requestHitTestSourceForTransientInput({ profile: 'generic-touchscreen' })
|
session.requestHitTestSourceForTransientInput({ profile: 'generic-touchscreen' })
|
||||||
.then(hitTestSource => { this.transientHitTestSource = hitTestSource; });
|
.then(hitTestSource => { this.transientHitTestSource = hitTestSource; });
|
||||||
}
|
}
|
||||||
|
this.placementComplete = true;
|
||||||
scene.visible = true;
|
scene.visible = true;
|
||||||
scene.setHotspotsVisibility(true);
|
scene.setHotspotsVisibility(true);
|
||||||
|
scene.setShadowIntensity(AR_SHADOW_INTENSITY);
|
||||||
this.dispatchEvent({ type: 'status', status: ARStatus.OBJECT_PLACED });
|
this.dispatchEvent({ type: 'status', status: ARStatus.OBJECT_PLACED });
|
||||||
}
|
}
|
||||||
getTouchLocation() {
|
getTouchLocation() {
|
||||||
|
|
@ -70568,40 +70573,27 @@ class ARRenderer extends EventDispatcher {
|
||||||
* until a ceiling hit arrives (no premature floor placement).
|
* until a ceiling hit arrives (no premature floor placement).
|
||||||
*/
|
*/
|
||||||
moveToAnchor(frame) {
|
moveToAnchor(frame) {
|
||||||
if (this.parsedAnchorOffset != null) {
|
if (this.parsedAnchorOffset != null || this.placementComplete) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Handle deferred initial placement for ceiling mode
|
const hitSource = this.initialHitSource;
|
||||||
if (this.placeOnCeiling &&
|
if (hitSource == null) {
|
||||||
this.xrMode === XRMode.WORLD_SPACE &&
|
return;
|
||||||
!this.worldSpaceInitialPlacementDone &&
|
}
|
||||||
!this.presentedScene.visible) {
|
const hitResults = frame.getHitTestResults(hitSource);
|
||||||
// Check if orientation is now sufficient
|
if (hitResults.length === 0) {
|
||||||
if (!this.isViewPointingUp()) {
|
return;
|
||||||
console.log('[ARR/moveToAnchor] Still waiting for proper ceiling orientation');
|
}
|
||||||
return;
|
const hitResult = hitResults[0];
|
||||||
|
const hitPosition = this.getHitPoint(hitResult);
|
||||||
|
if (hitPosition != null) {
|
||||||
|
this.goalPosition.copy(hitPosition);
|
||||||
|
this.placementComplete = true;
|
||||||
|
if (this.placementBox) {
|
||||||
|
this.placementBox.show = false;
|
||||||
}
|
}
|
||||||
// Orientation is good - complete the deferred world-space placement
|
this.presentedScene.setShadowIntensity(AR_SHADOW_INTENSITY);
|
||||||
const scene = this.presentedScene;
|
|
||||||
const xrCamera = scene.getCamera();
|
|
||||||
const { position: optimalPosition, scale: optimalScale } = this.calculateWorldSpaceOptimalPlacement(scene, xrCamera);
|
|
||||||
this.goalPosition.copy(optimalPosition);
|
|
||||||
this.goalScale = optimalScale;
|
|
||||||
this.initialModelScale = optimalScale;
|
|
||||||
scene.pivot.position.copy(optimalPosition);
|
|
||||||
scene.pivot.scale.set(optimalScale, optimalScale, optimalScale);
|
|
||||||
this.worldSpaceInitialPlacementDone = true;
|
|
||||||
this.calculateWorldSpaceScaleLimits(scene);
|
|
||||||
this.enableWorldSpaceUserInteraction();
|
|
||||||
scene.visible = true;
|
|
||||||
this.dispatchEvent({ type: 'status', status: ARStatus.OBJECT_PLACED });
|
this.dispatchEvent({ type: 'status', status: ARStatus.OBJECT_PLACED });
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Skip for world-space mode after initial placement (unless ceiling was deferred)
|
|
||||||
if (this.xrMode === XRMode.WORLD_SPACE && this.worldSpaceInitialPlacementDone) {
|
|
||||||
this.placementBox.show = false;
|
|
||||||
this.dispatchEvent({ type: 'status', status: ARStatus.OBJECT_PLACED });
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isViewPointingUp(thresholdDeg = CEILING_ORIENTATION_THRESHOLD) {
|
isViewPointingUp(thresholdDeg = CEILING_ORIENTATION_THRESHOLD) {
|
||||||
|
|
@ -80252,6 +80244,7 @@ const ARMixin = (ModelViewerElement) => {
|
||||||
this[$arAnchor].removeEventListener('message', this[$onARTap]);
|
this[$arAnchor].removeEventListener('message', this[$onARTap]);
|
||||||
}
|
}
|
||||||
update(changedProperties) {
|
update(changedProperties) {
|
||||||
|
var _l;
|
||||||
super.update(changedProperties);
|
super.update(changedProperties);
|
||||||
if (changedProperties.has('arScale')) {
|
if (changedProperties.has('arScale')) {
|
||||||
this[$scene].canScale = this.arScale !== 'fixed';
|
this[$scene].canScale = this.arScale !== 'fixed';
|
||||||
|
|
@ -80266,6 +80259,15 @@ const ARMixin = (ModelViewerElement) => {
|
||||||
if (changedProperties.has('arModes')) {
|
if (changedProperties.has('arModes')) {
|
||||||
this[$arModes] = deserializeARModes(this.arModes);
|
this[$arModes] = deserializeARModes(this.arModes);
|
||||||
}
|
}
|
||||||
|
if (changedProperties.has('arAnchor') && this[$renderer].arRenderer.isPresenting) {
|
||||||
|
const arRenderer = this[$renderer].arRenderer;
|
||||||
|
const isDeferredCeiling = this.arPlacement === 'ceiling' &&
|
||||||
|
!arRenderer.isObjectPlaced &&
|
||||||
|
!((_l = arRenderer.presentedScene) === null || _l === void 0 ? void 0 : _l.visible);
|
||||||
|
if (!isDeferredCeiling) {
|
||||||
|
arRenderer.updateAnchor(this.arAnchor);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (changedProperties.has('ar') || changedProperties.has('arModes') ||
|
if (changedProperties.has('ar') || changedProperties.has('arModes') ||
|
||||||
changedProperties.has('src') || changedProperties.has('iosSrc') ||
|
changedProperties.has('src') || changedProperties.has('iosSrc') ||
|
||||||
changedProperties.has('arUsdzMaxTextureSize')) {
|
changedProperties.has('arUsdzMaxTextureSize')) {
|
||||||
|
|
@ -80275,7 +80277,7 @@ const ARMixin = (ModelViewerElement) => {
|
||||||
getAnchor() {
|
getAnchor() {
|
||||||
const arRenderer = this[$renderer].arRenderer;
|
const arRenderer = this[$renderer].arRenderer;
|
||||||
if (arRenderer.isPresenting && arRenderer.isObjectPlaced) {
|
if (arRenderer.isPresenting && arRenderer.isObjectPlaced) {
|
||||||
const position = arRenderer.currentGoalPosition;
|
const position = arRenderer.currentPosition;
|
||||||
return `${position.x} ${position.y} ${position.z}`;
|
return `${position.x} ${position.y} ${position.z}`;
|
||||||
}
|
}
|
||||||
return 'Model not placed in AR yet.';
|
return 'Model not placed in AR yet.';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue