diff --git a/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.html b/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.html index ce96ca8..34fa3ab 100644 --- a/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.html +++ b/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.html @@ -108,22 +108,6 @@ -
- - -
-
- - -
diff --git a/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.ts b/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.ts index b0c1db4..1d27699 100644 --- a/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.ts +++ b/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.ts @@ -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(); 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); } - } diff --git a/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.html b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.html index b65280a..eb0d07e 100644 --- a/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.html +++ b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.html @@ -17,60 +17,65 @@ WebXR Umgebung laden - -
- -
-

Finden sie ihre Optionale Betrachtungsposition

-
+
-
- - - {{ verticalOffset >= 0 ? '+' : '' }}{{ verticalOffset.toFixed(2) }}m -
+ +
+ +
+

Finden Sie Ihren Optionale Betrachtungsposition

+
- -
+
+ + + {{ verticalOffset >= 0 ? '+' : '' }}{{ verticalOffset.toFixed(2) }}m +
- -
- -
-

Position fixiert!

-

Halten Sie das Modell für die gesamte Dauer in möglichst genau dieser Position.

-
- -
-
-
+ + +
+ +
+

Position fixiert!

+

Halten Sie das Modell für die gesamte Dauer in möglichst genau dieser Position.

+
+ +
+ + +
+
+
diff --git a/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.ts b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.ts index 638d0d5..47ec775 100644 --- a/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.ts +++ b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.ts @@ -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; @Output() testComplete = new EventEmitter(); @Output() redoTest = new EventEmitter(); 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(); } } diff --git a/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.html b/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.html index dca05aa..ce79dee 100644 --- a/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.html +++ b/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.html @@ -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"> - - - -
- +
-
Minimum setzen
-
- - {{ currentSize }}px - +
+

Minimum setzen. Der Text sollte gerade so noch lesbar sein. Den Arm hierfür bitte ganz austrecken.

+ +
+
+
+ + {{ currentSize }}px + +
+
- - -
Minimum bestätigt: {{ minSizeResult }}px
- - +
+
Minimum bestätigt: {{ minSizeResult }}px
+
+ + +
+
- -
Maximum setzen
-
- - {{ currentSize }}px - +
+

Maximum setzen. Der Text sollte komplett lesbar sein, ohne das Gerät zu bewegen.

+ +
+
+
+ + {{ currentSize }}px + +
+
- - -
Maximum bestätigt: {{ maxSizeResult }}px
- +
+
Maximum bestätigt: {{ maxSizeResult }}px
+ +
- -
Optimale Größe setzen
-
- - {{ currentSize }}px - +
+

Optimale Größe setzen.

+ +
+
+
+ + {{ currentSize }}px + +
+
- - -
Optimale Größe bestätigt: {{ comfortableSizeResult }}px
- +
+
Optimale Größe bestätigt: {{ comfortableSizeResult }}px
+ +
-
\ No newline at end of file diff --git a/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.ts b/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.ts index 48a748c..5a98395 100644 --- a/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.ts +++ b/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.ts @@ -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(); @Output() redoTest = new EventEmitter(); - 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(); } - - - } diff --git a/src/app/components/test-suite/test-suite.component.html b/src/app/components/test-suite/test-suite.component.html index 66508ab..93bab0a 100644 --- a/src/app/components/test-suite/test-suite.component.html +++ b/src/app/components/test-suite/test-suite.component.html @@ -63,9 +63,9 @@

In diesem Testabschnitt untersuchen wir die ergonomisch optimale Positionierung eines virtuellen Objekts an der Decke.

    -
  • Positionieren Sie das virtuelle Objekt mithilfe der Steuerung an der Decke.
  • 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.
  • -
  • Bestätigen Sie Ihre Auswahl, um die 20-sekündige Haltephase zu starten.
  • +
  • Positionieren Sie das virtuelle Objekt mithilfe der Steuerung an der Decke.
  • +
  • Bestätigen Sie Ihre Auswahl, um den 20 Sekunden Timer zu starten.
diff --git a/src/assets/scripts/model-viewer.js b/src/assets/scripts/model-viewer.js index 63834bc..a2b809c 100644 --- a/src/assets/scripts/model-viewer.js +++ b/src/assets/scripts/model-viewer.js @@ -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.';