fix model-viewer placement and
parent
1bb248de1b
commit
83c4ece1e8
|
|
@ -108,22 +108,6 @@
|
|||
<option value="prefer-not-to-say">Keine Angabe</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="education-select" class="block text-sm font-medium text-gray-700 mb-2">Höchster Bildungsabschluss</label>
|
||||
<select id="education-select" formControlName="education" (change)="logInteraction($event)" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option [ngValue]="null" disabled>Bitte auswählen</option>
|
||||
<option value="high-school">Schulabschluss</option>
|
||||
<option value="some-college">Berufsausbildung</option>
|
||||
<option value="bachelors">Bachelor</option>
|
||||
<option value="masters">Master</option>
|
||||
<option value="doctorate">Promotion</option>
|
||||
<option value="other">Andere</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="occupation-input" class="block text-sm font-medium text-gray-700 mb-2">Berufsfeld/Studienfach</label>
|
||||
<input id="occupation-input" type="text" formControlName="occupation" (change)="logInteraction($event)" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="z.B. Student, Ingenieur, ...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import {
|
|||
OnDestroy,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
ChangeDetectorRef,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
EventEmitter,
|
||||
Output,
|
||||
inject
|
||||
inject,
|
||||
NgZone
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
|
@ -30,15 +30,17 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDest
|
|||
@Output() redoTest = new EventEmitter<number>();
|
||||
|
||||
private metricsService = inject(MetricsTrackerService);
|
||||
|
||||
private zone = inject(NgZone);
|
||||
public scale = 1;
|
||||
public verticalOffset = 0;
|
||||
protected isModelPlaced = false;
|
||||
private initialArAnchor: { x: number; y: number; z: number } | null = null;
|
||||
private hasStartedTracking = false;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {
|
||||
constructor() {
|
||||
this.logInteraction = this.logInteraction.bind(this);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.metricsService.stopTracking();
|
||||
}
|
||||
|
|
@ -46,10 +48,19 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDest
|
|||
ngAfterViewInit() {
|
||||
const mv = this.modelViewerRef.nativeElement;
|
||||
mv.addEventListener('ar-status', (e: any) => {
|
||||
if (e.detail.status === 'session-started') {
|
||||
setTimeout(() => this.captureAnchor(), 500);
|
||||
this.metricsService.startTracking(mv, "position");
|
||||
}
|
||||
this.zone.run(() => {
|
||||
const status = e.detail.status;
|
||||
console.log(status)
|
||||
if (status === 'session-started' && !this.hasStartedTracking) {
|
||||
this.hasStartedTracking = true;
|
||||
this.metricsService.startTracking(mv, "position");
|
||||
}
|
||||
|
||||
if (status === 'object-placed' && !this.isModelPlaced) {
|
||||
console.log("placed")
|
||||
this.captureAnchor();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -57,24 +68,26 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDest
|
|||
this.metricsService.logInteraction(event);
|
||||
}
|
||||
|
||||
private async captureAnchor() {
|
||||
private captureAnchor() {
|
||||
const mv = this.modelViewerRef.nativeElement;
|
||||
const anchor = mv.getAnchor();
|
||||
if (!anchor.includes('not placed')) {
|
||||
if (anchor && !anchor.includes('not placed')) {
|
||||
const [x, y, z] = anchor.split(' ').map(parseFloat);
|
||||
this.initialArAnchor = { x, y, z };
|
||||
this.isModelPlaced = true;
|
||||
this.cdr.detectChanges();
|
||||
console.log('Anchor captured successfully:', this.initialArAnchor);
|
||||
} else {
|
||||
setTimeout(() => this.captureAnchor(), 500);
|
||||
console.warn('Object placed, but anchor not ready. Retrying once.');
|
||||
setTimeout(() => this.captureAnchor(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
onSliderInput() {
|
||||
if (!this.initialArAnchor) return;
|
||||
const { x, y, z } = this.initialArAnchor!;
|
||||
const { x, y, z } = this.initialArAnchor;
|
||||
const newY = y + this.verticalOffset;
|
||||
this.modelViewerRef.nativeElement.setAttribute('ar-anchor', `${x} ${newY} ${z}`);
|
||||
this.modelViewerRef.nativeElement.scale = `${this.scale} ${this.scale} ${this.scale}`;
|
||||
}
|
||||
|
||||
resetPosition() {
|
||||
|
|
@ -99,13 +112,10 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDest
|
|||
}));
|
||||
|
||||
console.log('Spatial position results captured locally.');
|
||||
|
||||
this.testComplete.emit();
|
||||
}
|
||||
|
||||
|
||||
retryCurrent() {
|
||||
this.redoTest.emit(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,60 +17,65 @@
|
|||
WebXR Umgebung laden
|
||||
</button>
|
||||
|
||||
<!-- Adjustment Phase Controls -->
|
||||
<div *ngIf="currentPhase === 1 && isModelPlaced"
|
||||
class="ar-controls absolute bottom-24 left-1/2 -translate-x-1/2 flex flex-col items-center space-y-6 bg-black bg-opacity-70 p-6 rounded-xl">
|
||||
|
||||
<div class="text-white text-center mb-4">
|
||||
<h4 id="adjust-phase-title" class="text-lg font-semibold">Finden sie ihre Optionale Betrachtungsposition</h4>
|
||||
</div>
|
||||
<div *ngIf="isArActive"
|
||||
class="absolute bottom-24 left-1/2 -translate-x-1/2 transition-opacity duration-300"
|
||||
[class.opacity-50]="!isModelPlaced"
|
||||
[class.pointer-events-none]="!isModelPlaced">
|
||||
|
||||
<div class="flex flex-col items-center text-white w-64">
|
||||
<label for="stability-offset-slider" class="text-sm font-medium mb-2">Höhe verschieben</label>
|
||||
<input
|
||||
id="stability-offset-slider"
|
||||
type="range"
|
||||
min="-1.5"
|
||||
max="1.5"
|
||||
step="0.01"
|
||||
[(ngModel)]="verticalOffset"
|
||||
(input)="onSliderInput(); logInteraction($event)"
|
||||
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" />
|
||||
<span class="text-xs mt-1">{{ verticalOffset >= 0 ? '+' : '' }}{{ verticalOffset.toFixed(2) }}m</span>
|
||||
</div>
|
||||
<!-- Adjustment Phase Controls -->
|
||||
<div *ngIf="currentPhase === 1"
|
||||
class="ar-controls flex flex-col items-center space-y-6 bg-black bg-opacity-70 p-6 rounded-xl">
|
||||
|
||||
<div class="text-white text-center mb-4">
|
||||
<h4 id="adjust-phase-title" class="text-lg font-semibold">Finden Sie Ihren Optionale Betrachtungsposition</h4>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="lock-position-btn"
|
||||
(click)="lockPosition(); logInteraction($event)"
|
||||
class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg text-lg">
|
||||
🔒 Diese Position fixieren.
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-center text-white w-64">
|
||||
<label for="stability-offset-slider" class="text-sm font-medium mb-2">Höhe verschieben</label>
|
||||
<input
|
||||
id="stability-offset-slider"
|
||||
type="range"
|
||||
min="-1.5"
|
||||
max="1.5"
|
||||
step="0.01"
|
||||
[(ngModel)]="verticalOffset"
|
||||
(input)="onSliderInput(); logInteraction($event)"
|
||||
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" />
|
||||
<span class="text-xs mt-1">{{ verticalOffset >= 0 ? '+' : '' }}{{ verticalOffset.toFixed(2) }}m</span>
|
||||
</div>
|
||||
|
||||
<!-- Lock Confirmation Phase -->
|
||||
<div *ngIf="currentPhase === 2"
|
||||
class="ar-controls absolute bottom-24 left-1/2 -translate-x-1/2 flex flex-col items-center space-y-4 bg-yellow-600 bg-opacity-90 p-6 rounded-xl">
|
||||
|
||||
<div class="text-white text-center">
|
||||
<h4 id="lock-confirm-title" class="text-xl font-bold">Position fixiert!</h4>
|
||||
<p class="text-xs mt-2 opacity-80">Halten Sie das Modell für die gesamte Dauer in möglichst genau dieser Position.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-4">
|
||||
<button
|
||||
id="adjust-more-btn"
|
||||
(click)="adjustMore(); logInteraction($event)"
|
||||
class="bg-gray-600 hover:bg-gray-700 text-white py-2 px-4 rounded-lg">
|
||||
Anpassen
|
||||
</button>
|
||||
<button
|
||||
id="start-countdown-btn"
|
||||
(click)="startCountdown(); logInteraction($event)"
|
||||
class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-6 rounded-lg">
|
||||
20s Timer starten
|
||||
id="lock-position-btn"
|
||||
(click)="lockPosition(); logInteraction($event)"
|
||||
class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg text-lg">
|
||||
🔒 Diese Position fixieren.
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lock Confirmation Phase -->
|
||||
<div *ngIf="currentPhase === 2"
|
||||
class="ar-controls flex flex-col items-center space-y-4 bg-yellow-600 bg-opacity-90 p-6 rounded-xl">
|
||||
|
||||
<div class="text-white text-center">
|
||||
<h4 id="lock-confirm-title" class="text-xl font-bold">Position fixiert!</h4>
|
||||
<p class="text-xs mt-2 opacity-80">Halten Sie das Modell für die gesamte Dauer in möglichst genau dieser Position.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-4">
|
||||
<button
|
||||
id="adjust-more-btn"
|
||||
(click)="adjustMore(); logInteraction($event)"
|
||||
class="bg-gray-600 hover:bg-gray-700 text-white py-2 px-4 rounded-lg">
|
||||
Anpassen
|
||||
</button>
|
||||
<button
|
||||
id="start-countdown-btn"
|
||||
(click)="startCountdown(); logInteraction($event)"
|
||||
class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-6 rounded-lg">
|
||||
20s Timer starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Countdown Phase -->
|
||||
<div *ngIf="currentPhase === 3"
|
||||
|
|
@ -117,5 +122,6 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</model-viewer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
ViewChild,
|
||||
ElementRef,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
ChangeDetectorRef,
|
||||
NgZone,
|
||||
EventEmitter,
|
||||
Output,
|
||||
inject
|
||||
|
|
@ -13,7 +13,6 @@ import {
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MetricsTrackerService } from '../../../../services/metrics-tracker.service';
|
||||
|
||||
import '../../../../../assets/scripts/model-viewer';
|
||||
|
||||
@Component({
|
||||
|
|
@ -24,68 +23,99 @@ import '../../../../../assets/scripts/model-viewer';
|
|||
styleUrls: ['./spatial-stability-assessment.component.css'],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDestroy { // <-- Implement OnDestroy
|
||||
export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDestroy {
|
||||
@ViewChild('modelViewer') modelViewerRef!: ElementRef<any>;
|
||||
@Output() testComplete = new EventEmitter<void>();
|
||||
@Output() redoTest = new EventEmitter<number>();
|
||||
|
||||
private metricsService = inject(MetricsTrackerService);
|
||||
private zone = inject(NgZone);
|
||||
|
||||
public isArActive = false;
|
||||
public isModelPlaced = false;
|
||||
|
||||
public verticalOffset = 0;
|
||||
public currentPhase = 0; // 0: waiting, 1: adjust, 2: lock, 3: countdown, 4: complete
|
||||
public currentPhase = 0;
|
||||
public remainingTime = 20;
|
||||
public progressPercentage = 0;
|
||||
|
||||
private initialArAnchor: { x: number, y: number, z: number } | null = null;
|
||||
private lockedPosition: { verticalOffset: number, anchor: string } | null = null;
|
||||
private countdownInterval: any = null;
|
||||
protected isModelPlaced = false;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {
|
||||
private initialArAnchor: { x: number, y: number, z: number } | null = null;
|
||||
private lockedPosition: { verticalOffset: number } | null = null;
|
||||
private countdownInterval: any = null;
|
||||
|
||||
constructor() {
|
||||
this.logInteraction = this.logInteraction.bind(this);
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
const mv = this.modelViewerRef.nativeElement;
|
||||
|
||||
mv.addEventListener('ar-status', (event: any) => {
|
||||
if (event.detail.status === 'session-started' && !this.isModelPlaced) {
|
||||
setTimeout(() => this.getInitialAnchor(), 500);
|
||||
this.metricsService.startTracking(mv, "stability");
|
||||
}
|
||||
this.zone.run(() => {
|
||||
const status = event.detail.status;
|
||||
|
||||
switch (status) {
|
||||
case 'session-started':
|
||||
this.isArActive = true;
|
||||
this.isModelPlaced = false;
|
||||
this.currentPhase = 1;
|
||||
this.metricsService.startTracking(mv, "stability");
|
||||
break;
|
||||
|
||||
case 'object-placed':
|
||||
if (this.isArActive && !this.isModelPlaced) {
|
||||
this.isModelPlaced = true;
|
||||
this.getInitialAnchor();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'not-presenting':
|
||||
this.isArActive = false;
|
||||
this.isModelPlaced = false;
|
||||
this.currentPhase = 0;
|
||||
this.resetComponentState();
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public logInteraction(event: Event) {
|
||||
this.metricsService.logInteraction(event);
|
||||
|
||||
private resetComponentState() {
|
||||
this.clearCountdown();
|
||||
this.initialArAnchor = null;
|
||||
this.lockedPosition = null;
|
||||
this.verticalOffset = 0;
|
||||
}
|
||||
|
||||
private async getInitialAnchor() {
|
||||
const modelViewer = this.modelViewerRef.nativeElement;
|
||||
const anchorString = modelViewer.getAnchor();
|
||||
if (!anchorString.includes('not placed')) {
|
||||
private getInitialAnchor() {
|
||||
const mv = this.modelViewerRef.nativeElement;
|
||||
const anchorString = mv.getAnchor();
|
||||
|
||||
if (anchorString && !anchorString.includes('not placed')) {
|
||||
const coords = anchorString.split(' ').map(parseFloat);
|
||||
this.initialArAnchor = { x: coords[0], y: coords[1], z: coords[2] };
|
||||
this.isModelPlaced = true;
|
||||
this.currentPhase = 1;
|
||||
this.cdr.detectChanges();
|
||||
if (coords.length === 3 && !coords.some(isNaN)) {
|
||||
this.initialArAnchor = { x: coords[0], y: coords[1], z: coords[2] };
|
||||
}
|
||||
} else {
|
||||
setTimeout(() => this.getInitialAnchor(), 500);
|
||||
setTimeout(() => this.getInitialAnchor(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
onSliderInput() {
|
||||
if (this.initialArAnchor && this.currentPhase === 1) {
|
||||
const { x, z } = this.initialArAnchor;
|
||||
const newY = this.initialArAnchor.y + this.verticalOffset;
|
||||
const anchor = `${this.initialArAnchor.x} ${newY} ${this.initialArAnchor.z}`;
|
||||
this.modelViewerRef.nativeElement.setAttribute('ar-anchor', anchor);
|
||||
this.modelViewerRef.nativeElement.setAttribute('ar-anchor', `${x} ${newY} ${z}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public logInteraction(event: Event) {
|
||||
this.metricsService.logInteraction(event);
|
||||
}
|
||||
|
||||
lockPosition() {
|
||||
this.lockedPosition = {
|
||||
verticalOffset: this.verticalOffset,
|
||||
anchor: this.modelViewerRef.nativeElement.getAttribute('ar-anchor')
|
||||
};
|
||||
this.lockedPosition = { verticalOffset: this.verticalOffset };
|
||||
this.currentPhase = 2;
|
||||
}
|
||||
|
||||
|
|
@ -97,64 +127,53 @@ export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDes
|
|||
this.currentPhase = 3;
|
||||
this.remainingTime = 20;
|
||||
this.progressPercentage = 0;
|
||||
|
||||
|
||||
this.countdownInterval = setInterval(() => {
|
||||
this.remainingTime--;
|
||||
this.progressPercentage = ((20 - this.remainingTime) / 20) * 100;
|
||||
|
||||
if (this.remainingTime <= 0) {
|
||||
this.completeTest(true);
|
||||
}
|
||||
this.cdr.detectChanges();
|
||||
this.zone.run(() => {
|
||||
this.remainingTime--;
|
||||
this.progressPercentage = ((20 - this.remainingTime) / 20) * 100;
|
||||
if (this.remainingTime <= 0) {
|
||||
this.completeTest(true);
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private clearCountdown() {
|
||||
if (this.countdownInterval) {
|
||||
clearInterval(this.countdownInterval);
|
||||
this.countdownInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
cancelTest() {
|
||||
if (this.countdownInterval) {
|
||||
clearInterval(this.countdownInterval);
|
||||
this.countdownInterval = null;
|
||||
}
|
||||
this.clearCountdown();
|
||||
this.currentPhase = 2;
|
||||
this.metricsService.logInteraction(new CustomEvent('test-cancelled', { detail: { phase: 'countdown' }}));
|
||||
this.metricsService.logInteraction(new CustomEvent('test-cancelled', { detail: { phase: 'countdown' } }));
|
||||
}
|
||||
|
||||
private completeTest(wasCompleted: boolean) {
|
||||
if (this.countdownInterval) {
|
||||
clearInterval(this.countdownInterval);
|
||||
this.countdownInterval = null;
|
||||
}
|
||||
|
||||
this.clearCountdown();
|
||||
const finalResults = {
|
||||
testCompletedSuccessfully: wasCompleted,
|
||||
initialAnchor: this.initialArAnchor,
|
||||
lockedPosition: this.lockedPosition,
|
||||
};
|
||||
|
||||
this.metricsService.logInteraction(new CustomEvent('test-results', {
|
||||
detail: {
|
||||
testName: 'SpatialStability',
|
||||
results: finalResults
|
||||
}
|
||||
detail: { testName: 'SpatialStability', results: finalResults }
|
||||
}));
|
||||
|
||||
if (wasCompleted) {
|
||||
this.currentPhase = 4;
|
||||
}
|
||||
|
||||
if(wasCompleted) {
|
||||
this.testComplete.emit();
|
||||
this.currentPhase = 4;
|
||||
this.testComplete.emit();
|
||||
}
|
||||
}
|
||||
|
||||
restartTest() {
|
||||
this.clearCountdown();
|
||||
this.currentPhase = 1;
|
||||
this.remainingTime = 20;
|
||||
this.progressPercentage = 0;
|
||||
this.lockedPosition = null;
|
||||
if (this.countdownInterval) {
|
||||
clearInterval(this.countdownInterval);
|
||||
this.countdownInterval = null;
|
||||
}
|
||||
this.verticalOffset = 0;
|
||||
this.onSliderInput();
|
||||
}
|
||||
|
||||
finishAssessment() {
|
||||
|
|
@ -162,9 +181,7 @@ export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDes
|
|||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.countdownInterval) {
|
||||
clearInterval(this.countdownInterval);
|
||||
}
|
||||
this.clearCountdown();
|
||||
this.metricsService.stopTracking();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,11 @@
|
|||
src="https://modelviewer.dev/shared-assets/models/Astronaut.glb" ar ar-modes="webxr" ar-placement="ceiling"
|
||||
reveal="manual" camera-orbit="0deg 75deg 2m">
|
||||
|
||||
|
||||
<button slot="ar-button" id="ar-button-legibility" (click)="logInteraction($event)"
|
||||
class="text-xl absolute top-4 left-1/2 -translate-x-1/2 bg-blue-500 text-white py-2 px-4 rounded-lg z-10">
|
||||
WebXR Umgebung laden
|
||||
</button>
|
||||
|
||||
|
||||
<button slot="hotspot-text" class="hotspot ar-hotspot" data-position="-0.1 0.93 0.1" data-normal="0 1 0">
|
||||
<div class="annotation" [ngStyle]="{'font-size.px': currentSize}"
|
||||
style="background:rgba(255,255,255,0.9);padding:8px;border-radius:4px;color:black;">
|
||||
|
|
@ -17,85 +15,89 @@
|
|||
labore et
|
||||
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
|
||||
Stet
|
||||
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. </div>
|
||||
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
||||
<div
|
||||
class="ar-controls absolute bottom-24 left-1/2 -translate-x-1/2 bg-black bg-opacity-70 p-4 rounded-xl space-y-4 text-white z-10">
|
||||
|
||||
<div *ngIf="isArActive"
|
||||
class="absolute bottom-24 left-1/2 -translate-x-1/2 flex flex-col items-center space-y-4 bg-black bg-opacity-70 p-4 rounded-xl text-white z-10 transition-opacity duration-300"
|
||||
[class.opacity-50]="!isModelPlaced" [class.pointer-events-none]="!isModelPlaced">
|
||||
|
||||
<!-- Phase: Min -->
|
||||
<ng-container *ngIf="phase==='min'">
|
||||
<div>Minimum setzen</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="min-decrease-btn" (click)="decrease(); logInteraction($event)"
|
||||
class="w-10 h-10 bg-gray-600 rounded-full">–</button>
|
||||
<span>{{ currentSize }}px</span>
|
||||
<button id="min-increase-btn" (click)="increase(); logInteraction($event)"
|
||||
class="w-10 h-10 bg-gray-600 rounded-full">+</button>
|
||||
<div *ngIf="isDescriptionVisible" class="relative bg-black/30 p-3 pr-8 rounded-lg text-center w-[280px]">
|
||||
<p>Minimum setzen. Der Text sollte gerade so noch lesbar sein. Den Arm hierfür bitte ganz austrecken.</p>
|
||||
<button (click)="toggleDescription()" class="absolute top-1 right-2 text-2xl leading-none">×</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="min-decrease-btn" (click)="decrease(); logInteraction($event)" class="w-10 h-10 bg-gray-600 rounded-full">–</button>
|
||||
<span>{{ currentSize }}px</span>
|
||||
<button id="min-increase-btn" (click)="increase(); logInteraction($event)" class="w-10 h-10 bg-gray-600 rounded-full">+</button>
|
||||
</div>
|
||||
<button id="min-confirm-btn" (click)="nextPhase(); logInteraction($event)" class="bg-green-600 py-2 px-6 rounded-lg">Minimum bestätigen</button>
|
||||
</div>
|
||||
<button id="min-confirm-btn" (click)="nextPhase(); logInteraction($event)"
|
||||
class="bg-green-600 py-2 px-6 rounded-lg">Minimum bestätigen</button>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<!-- Phase: Confirmed Min -->
|
||||
<ng-container *ngIf="phase==='confirmedMin'">
|
||||
<div>Minimum bestätigt: {{ minSizeResult }}px</div>
|
||||
<button id="confirmed-min-reset-btn" (click)="resetToMid(); logInteraction($event)"
|
||||
class="bg-gray-600 py-2 px-4 rounded-lg">Reset</button>
|
||||
<button id="confirmed-min-continue-btn" (click)="nextPhase(); logInteraction($event)"
|
||||
class="bg-blue-600 py-2 px-6 rounded-lg">Weiter zu Maximum</button>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<div>Minimum bestätigt: {{ minSizeResult }}px</div>
|
||||
<div class="flex space-x-2">
|
||||
<button id="confirmed-min-reset-btn" (click)="resetToMid(); logInteraction($event)" class="bg-gray-600 py-2 px-4 rounded-lg">Reset</button>
|
||||
<button id="confirmed-min-continue-btn" (click)="nextPhase(); logInteraction($event)" class="bg-blue-600 py-2 px-6 rounded-lg">Weiter zu Maximum</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<!-- Phase: Max -->
|
||||
<ng-container *ngIf="phase==='max'">
|
||||
<div>Maximum setzen</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="max-decrease-btn" (click)="decrease(); logInteraction($event)"
|
||||
class="w-10 h-10 bg-gray-600 rounded-full">–</button>
|
||||
<span>{{ currentSize }}px</span>
|
||||
<button id="max-increase-btn" (click)="increase(); logInteraction($event)"
|
||||
class="w-10 h-10 bg-gray-600 rounded-full">+</button>
|
||||
<div *ngIf="isDescriptionVisible" class="relative bg-black/30 p-3 pr-8 rounded-lg text-center w-[280px]">
|
||||
<p>Maximum setzen. Der Text sollte komplett lesbar sein, ohne das Gerät zu bewegen.</p>
|
||||
<button (click)="toggleDescription()" class="absolute top-1 right-2 text-2xl leading-none">×</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="max-decrease-btn" (click)="decrease(); logInteraction($event)" class="w-10 h-10 bg-gray-600 rounded-full">–</button>
|
||||
<span>{{ currentSize }}px</span>
|
||||
<button id="max-increase-btn" (click)="increase(); logInteraction($event)" class="w-10 h-10 bg-gray-600 rounded-full">+</button>
|
||||
</div>
|
||||
<button id="max-confirm-btn" (click)="nextPhase(); logInteraction($event)" class="bg-green-600 py-2 px-6 rounded-lg">Maximum bestätigen</button>
|
||||
</div>
|
||||
<button id="max-confirm-btn" (click)="nextPhase(); logInteraction($event)"
|
||||
class="bg-green-600 py-2 px-6 rounded-lg">Maximum bestätigen</button>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<!-- Phase: Confirmed Max -->
|
||||
<ng-container *ngIf="phase==='confirmedMax'">
|
||||
<div>Maximum bestätigt: {{ maxSizeResult }}px</div>
|
||||
<button id="confirmed-max-continue-btn" (click)="nextPhase(); logInteraction($event)"
|
||||
class="bg-blue-600 py-2 px-6 rounded-lg">Weiter zu optiomal</button>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<div>Maximum bestätigt: {{ maxSizeResult }}px</div>
|
||||
<button id="confirmed-max-continue-btn" (click)="nextPhase(); logInteraction($event)" class="bg-blue-600 py-2 px-6 rounded-lg">Weiter zu Optimal</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<!-- Phase: Comfortable -->
|
||||
<ng-container *ngIf="phase==='comfortable'">
|
||||
<div>Optimale Größe setzen</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="comfort-decrease-btn" (click)="decrease(); logInteraction($event)"
|
||||
class="w-10 h-10 bg-gray-600 rounded-full">–</button>
|
||||
<span>{{ currentSize }}px</span>
|
||||
<button id="comfort-increase-btn" (click)="increase(); logInteraction($event)"
|
||||
class="w-10 h-10 bg-gray-600 rounded-full">+</button>
|
||||
<div *ngIf="isDescriptionVisible" class="relative bg-black/30 p-3 pr-8 rounded-lg text-center w-[280px]">
|
||||
<p>Optimale Größe setzen.</p>
|
||||
<button (click)="toggleDescription()" class="absolute top-1 right-2 text-2xl leading-none">×</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="comfort-decrease-btn" (click)="decrease(); logInteraction($event)" class="w-10 h-10 bg-gray-600 rounded-full">–</button>
|
||||
<span>{{ currentSize }}px</span>
|
||||
<button id="comfort-increase-btn" (click)="increase(); logInteraction($event)" class="w-10 h-10 bg-gray-600 rounded-full">+</button>
|
||||
</div>
|
||||
<button id="comfort-confirm-btn" (click)="nextPhase(); logInteraction($event)" class="bg-green-600 py-2 px-6 rounded-lg">Optimale Größe bestätigen</button>
|
||||
</div>
|
||||
<button id="comfort-confirm-btn" (click)="nextPhase(); logInteraction($event)"
|
||||
class="bg-green-600 py-2 px-6 rounded-lg">Optimale Größe setzen</button>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<!-- Phase: Confirmed Comfortable -->
|
||||
<ng-container *ngIf="phase==='confirmedComfort'">
|
||||
<div>Optimale Größe bestätigt: {{ comfortableSizeResult }}px</div>
|
||||
<button id="finish-assessment-btn" (click)="finishAssessment(); logInteraction($event)"
|
||||
class="bg-blue-600 py-2 px-6 rounded-lg">Test beenden</button>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<div>Optimale Größe bestätigt: {{ comfortableSizeResult }}px</div>
|
||||
<button id="finish-assessment-btn" (click)="finishAssessment(); logInteraction($event)" class="bg-blue-600 py-2 px-6 rounded-lg">Test beenden</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
</div>
|
||||
</model-viewer>
|
||||
</div>
|
||||
|
|
@ -4,18 +4,17 @@ import {
|
|||
OnDestroy,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
ChangeDetectorRef,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
EventEmitter,
|
||||
Output,
|
||||
inject
|
||||
inject,
|
||||
NgZone
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MetricsTrackerService } from '../../../../services/metrics-tracker.service';
|
||||
import '../../../../../assets/scripts/model-viewer';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-text-legibility-assessment',
|
||||
standalone: true,
|
||||
|
|
@ -29,58 +28,79 @@ export class TextLegibilityAssessmentComponent implements AfterViewInit, OnDestr
|
|||
@Output() testComplete = new EventEmitter<void>();
|
||||
@Output() redoTest = new EventEmitter<number>();
|
||||
|
||||
|
||||
private metricsService = inject(MetricsTrackerService);
|
||||
private zone = inject(NgZone);
|
||||
|
||||
public isArActive = false;
|
||||
public isModelPlaced = false;
|
||||
public isDescriptionVisible = true;
|
||||
|
||||
minSize = 2;
|
||||
maxSize = 64;
|
||||
currentSize = 16;
|
||||
|
||||
|
||||
phase: 'min' | 'confirmedMin' | 'max' | 'confirmedMax' | 'comfortable' | 'confirmedComfort' = 'min';
|
||||
minSizeResult: number | null = null;
|
||||
maxSizeResult: number | null = null;
|
||||
comfortableSizeResult: number | null = null;
|
||||
|
||||
|
||||
phase: 'min' | 'confirmedMin' | 'max' | 'confirmedMax' | 'comfortable' | 'confirmedComfort' = 'min';
|
||||
|
||||
|
||||
private offsetApplied = false;
|
||||
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {
|
||||
constructor() {
|
||||
this.logInteraction = this.logInteraction.bind(this);
|
||||
}
|
||||
|
||||
|
||||
ngAfterViewInit() {
|
||||
const mv = this.modelViewerRef.nativeElement;
|
||||
mv.addEventListener('ar-status', (event: any) => {
|
||||
if (event.detail.status === 'session-started') {
|
||||
console.log('AR session started. Delaying startTracking call to ensure session is ready.');
|
||||
this.metricsService.startTracking(mv, "text-legibility");
|
||||
}
|
||||
this.zone.run(() => {
|
||||
const status = event.detail.status;
|
||||
switch (status) {
|
||||
case 'session-started':
|
||||
this.isArActive = true;
|
||||
this.isModelPlaced = false;
|
||||
this.isDescriptionVisible = true;
|
||||
this.metricsService.startTracking(mv, "text-legibility");
|
||||
break;
|
||||
case 'object-placed':
|
||||
if (this.isArActive && !this.isModelPlaced) {
|
||||
this.isModelPlaced = true;
|
||||
}
|
||||
break;
|
||||
case 'not-presenting':
|
||||
this.isArActive = false;
|
||||
this.isModelPlaced = false;
|
||||
this.resetComponentState();
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private resetComponentState() {
|
||||
this.phase = 'min';
|
||||
this.currentSize = 16;
|
||||
this.minSizeResult = null;
|
||||
this.maxSizeResult = null;
|
||||
this.comfortableSizeResult = null;
|
||||
this.isDescriptionVisible = true;
|
||||
}
|
||||
|
||||
public logInteraction(event: Event) {
|
||||
this.metricsService.logInteraction(event);
|
||||
}
|
||||
|
||||
|
||||
toggleDescription() {
|
||||
this.isDescriptionVisible = !this.isDescriptionVisible;
|
||||
}
|
||||
|
||||
decrease() {
|
||||
if (this.currentSize > this.minSize) this.currentSize--;
|
||||
}
|
||||
|
||||
|
||||
increase() {
|
||||
if (this.currentSize < this.maxSize) this.currentSize++;
|
||||
}
|
||||
|
||||
|
||||
nextPhase() {
|
||||
this.isDescriptionVisible = true;
|
||||
switch (this.phase) {
|
||||
case 'min':
|
||||
this.minSizeResult = this.currentSize;
|
||||
|
|
@ -107,49 +127,24 @@ export class TextLegibilityAssessmentComponent implements AfterViewInit, OnDestr
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
resetToMid() {
|
||||
this.currentSize = Math.floor((this.minSize + this.maxSize) / 2);
|
||||
this.phase = 'min';
|
||||
this.minSizeResult = null;
|
||||
this.maxSizeResult = null;
|
||||
this.comfortableSizeResult = null;
|
||||
this.resetComponentState();
|
||||
}
|
||||
|
||||
|
||||
retry() {
|
||||
this.redoTest.emit(1);
|
||||
}
|
||||
|
||||
|
||||
finishAssessment() {
|
||||
const finalResults = {
|
||||
minReadableSize: this.minSizeResult,
|
||||
maxReadableSize: this.maxSizeResult,
|
||||
comfortableReadableSize: this.comfortableSizeResult
|
||||
};
|
||||
|
||||
|
||||
this.metricsService.logInteraction(new CustomEvent('test-results', {
|
||||
detail: {
|
||||
testName: 'TextLegibility',
|
||||
results: finalResults
|
||||
}
|
||||
detail: { testName: 'TextLegibility', results: finalResults }
|
||||
}));
|
||||
|
||||
|
||||
console.log(finalResults);
|
||||
|
||||
|
||||
this.testComplete.emit();
|
||||
}
|
||||
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
|
||||
this.metricsService.stopTracking();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,9 +63,9 @@
|
|||
<div class="text-black-600">
|
||||
<h2 class="text-base mb-2">In diesem Testabschnitt untersuchen wir die ergonomisch optimale Positionierung eines virtuellen Objekts an der Decke.</h2>
|
||||
<ul class="list-disc pl-4 space-y-1">
|
||||
<li>Positionieren Sie das virtuelle Objekt mithilfe der Steuerung an der Decke.</li>
|
||||
<li>Das Ziel ist, eine Position zu finden, die es Ihnen erlaubt, das Objekt für 20 Sekunden mit möglichst geringer körperlicher Anstrengung zu betrachten.</li>
|
||||
<li>Bestätigen Sie Ihre Auswahl, um die 20-sekündige Haltephase zu starten.</li>
|
||||
<li>Positionieren Sie das virtuelle Objekt mithilfe der Steuerung an der Decke.</li>
|
||||
<li>Bestätigen Sie Ihre Auswahl, um den 20 Sekunden Timer zu starten.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -70014,6 +70014,12 @@ class ARRenderer extends EventDispatcher {
|
|||
get currentGoalPosition() {
|
||||
return this.goalPosition;
|
||||
}
|
||||
get currentPosition() {
|
||||
if (this.presentedScene) {
|
||||
return this.presentedScene.pivot.position;
|
||||
}
|
||||
return this.goalPosition;
|
||||
}
|
||||
get isObjectPlaced() {
|
||||
return this.placementComplete;
|
||||
}
|
||||
|
|
@ -70378,28 +70384,26 @@ class ARRenderer extends EventDispatcher {
|
|||
}
|
||||
}
|
||||
placeInitially() {
|
||||
var _a;
|
||||
const scene = this.presentedScene;
|
||||
const { pivot, element } = scene;
|
||||
const { position } = pivot;
|
||||
const xrCamera = scene.getCamera();
|
||||
if (this.parsedAnchorOffset != null) {
|
||||
// Set position directly from the provided anchor offset.
|
||||
// This position is relative to the initial XR reference space origin.
|
||||
position.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.visible = true;
|
||||
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) {
|
||||
this.placementBox.show = false;
|
||||
}
|
||||
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) {
|
||||
const { session } = this.frame;
|
||||
session.addEventListener('selectstart', this.onSelectStart);
|
||||
|
|
@ -70409,12 +70413,8 @@ class ARRenderer extends EventDispatcher {
|
|||
}
|
||||
else { // WORLD_SPACE
|
||||
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();
|
||||
scene.setSize(width, height);
|
||||
|
|
@ -70425,10 +70425,11 @@ class ARRenderer extends EventDispatcher {
|
|||
const cameraDirection = xrCamera.getWorldDirection(vector3$1);
|
||||
scene.yaw = Math.atan2(-cameraDirection.x, -cameraDirection.z) - theta;
|
||||
this.goalYaw = scene.yaw;
|
||||
// Defer placement if ceiling is the target but view is not pointing up.
|
||||
if (this.placeOnCeiling && !this.isViewPointingUp()) {
|
||||
scene.visible = false; // Hide until properly oriented
|
||||
scene.setHotspotsVisibility(true); // Still show UI
|
||||
// Set up touch interaction for screen-space mode
|
||||
scene.visible = false;
|
||||
scene.setHotspotsVisibility(true);
|
||||
// Set up touch interaction for screen-space mode even if deferred
|
||||
if (this.xrMode === XRMode.SCREEN_SPACE) {
|
||||
const { session } = this.frame;
|
||||
session.addEventListener('selectstart', this.onSelectStart);
|
||||
|
|
@ -70436,28 +70437,21 @@ class ARRenderer extends EventDispatcher {
|
|||
session.requestHitTestSourceForTransientInput({ profile: 'generic-touchscreen' })
|
||||
.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
|
||||
if (this.xrMode === XRMode.WORLD_SPACE && !this.worldSpaceInitialPlacementDone) {
|
||||
// Use automatic optimal placement for world-space AR only on first session
|
||||
// --- Main Placement Logic ---
|
||||
if (this.xrMode === XRMode.WORLD_SPACE) {
|
||||
const { position: optimalPosition, scale: optimalScale } = this.calculateWorldSpaceOptimalPlacement(scene, xrCamera);
|
||||
this.goalPosition.copy(optimalPosition);
|
||||
this.goalScale = optimalScale;
|
||||
// Store the initial scale for toggle functionality
|
||||
this.initialModelScale = optimalScale;
|
||||
// Set initial position and scale immediately for world-space
|
||||
position.copy(optimalPosition);
|
||||
pivot.scale.set(optimalScale, optimalScale, optimalScale);
|
||||
// Mark that initial placement is done
|
||||
this.worldSpaceInitialPlacementDone = true;
|
||||
// Calculate scale limits for world-space mode (SVXR logic)
|
||||
this.calculateWorldSpaceScaleLimits(scene);
|
||||
// Enable user interaction after initial placement
|
||||
this.enableWorldSpaceUserInteraction();
|
||||
}
|
||||
else if (this.xrMode === XRMode.SCREEN_SPACE) {
|
||||
// Use original placement logic for screen-space AR
|
||||
else { // SCREEN_SPACE
|
||||
const radius = Math.max(1, 2 * scene.boundingSphere.radius);
|
||||
position.copy(xrCamera.position)
|
||||
.add(cameraDirection.multiplyScalar(radius));
|
||||
|
|
@ -70466,8 +70460,17 @@ class ARRenderer extends EventDispatcher {
|
|||
position.add(target).sub(this.oldTarget);
|
||||
this.goalPosition.copy(position);
|
||||
}
|
||||
// --- Finalize Placement ---
|
||||
this.placementComplete = true;
|
||||
scene.visible = 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) {
|
||||
const { session } = this.frame;
|
||||
session.addEventListener('selectstart', this.onSelectStart);
|
||||
|
|
@ -70518,8 +70521,10 @@ class ARRenderer extends EventDispatcher {
|
|||
session.requestHitTestSourceForTransientInput({ profile: 'generic-touchscreen' })
|
||||
.then(hitTestSource => { this.transientHitTestSource = hitTestSource; });
|
||||
}
|
||||
this.placementComplete = true;
|
||||
scene.visible = true;
|
||||
scene.setHotspotsVisibility(true);
|
||||
scene.setShadowIntensity(AR_SHADOW_INTENSITY);
|
||||
this.dispatchEvent({ type: 'status', status: ARStatus.OBJECT_PLACED });
|
||||
}
|
||||
getTouchLocation() {
|
||||
|
|
@ -70568,40 +70573,27 @@ class ARRenderer extends EventDispatcher {
|
|||
* until a ceiling hit arrives (no premature floor placement).
|
||||
*/
|
||||
moveToAnchor(frame) {
|
||||
if (this.parsedAnchorOffset != null) {
|
||||
if (this.parsedAnchorOffset != null || this.placementComplete) {
|
||||
return;
|
||||
}
|
||||
// Handle deferred initial placement for ceiling mode
|
||||
if (this.placeOnCeiling &&
|
||||
this.xrMode === XRMode.WORLD_SPACE &&
|
||||
!this.worldSpaceInitialPlacementDone &&
|
||||
!this.presentedScene.visible) {
|
||||
// Check if orientation is now sufficient
|
||||
if (!this.isViewPointingUp()) {
|
||||
console.log('[ARR/moveToAnchor] Still waiting for proper ceiling orientation');
|
||||
return;
|
||||
const hitSource = this.initialHitSource;
|
||||
if (hitSource == null) {
|
||||
return;
|
||||
}
|
||||
const hitResults = frame.getHitTestResults(hitSource);
|
||||
if (hitResults.length === 0) {
|
||||
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
|
||||
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.presentedScene.setShadowIntensity(AR_SHADOW_INTENSITY);
|
||||
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) {
|
||||
|
|
@ -80252,6 +80244,7 @@ const ARMixin = (ModelViewerElement) => {
|
|||
this[$arAnchor].removeEventListener('message', this[$onARTap]);
|
||||
}
|
||||
update(changedProperties) {
|
||||
var _l;
|
||||
super.update(changedProperties);
|
||||
if (changedProperties.has('arScale')) {
|
||||
this[$scene].canScale = this.arScale !== 'fixed';
|
||||
|
|
@ -80266,6 +80259,15 @@ const ARMixin = (ModelViewerElement) => {
|
|||
if (changedProperties.has('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') ||
|
||||
changedProperties.has('src') || changedProperties.has('iosSrc') ||
|
||||
changedProperties.has('arUsdzMaxTextureSize')) {
|
||||
|
|
@ -80275,7 +80277,7 @@ const ARMixin = (ModelViewerElement) => {
|
|||
getAnchor() {
|
||||
const arRenderer = this[$renderer].arRenderer;
|
||||
if (arRenderer.isPresenting && arRenderer.isObjectPlaced) {
|
||||
const position = arRenderer.currentGoalPosition;
|
||||
const position = arRenderer.currentPosition;
|
||||
return `${position.x} ${position.y} ${position.z}`;
|
||||
}
|
||||
return 'Model not placed in AR yet.';
|
||||
|
|
|
|||
Loading…
Reference in New Issue