fix model-viewer placement and

master
MrPlatnum 2025-09-15 15:23:11 +02:00
parent 1bb248de1b
commit 83c4ece1e8
8 changed files with 328 additions and 312 deletions

View File

@ -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>

View File

@ -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);
} }
} }

View File

@ -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>

View File

@ -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();
} }
} }

View File

@ -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">&times;</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">&times;</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">&times;</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>

View File

@ -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();
} }
} }

View File

@ -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>

View File

@ -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.';