diff --git a/src/app/app.component.html b/src/app/app.component.html index 9722ad0..2788e36 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,8 +1,4 @@ -
-

- Ceiling-AR Assessment Suite -

- - - +
+ +
diff --git a/src/app/components/shared/ar-prompt/ar-prompt.component.css b/src/app/components/shared/ar-prompt/ar-prompt.component.css new file mode 100644 index 0000000..1ac27bd --- /dev/null +++ b/src/app/components/shared/ar-prompt/ar-prompt.component.css @@ -0,0 +1,36 @@ +.ar-prompt-container { + position: absolute; + bottom: 50%; + left: 50%; + transform: translate(-50%, 50%); + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.7); + color: white; + padding: 1rem 1.5rem; + border-radius: 12px; + text-align: center; + font-size: 1.25rem; + font-weight: 500; + pointer-events: none; + } + + + .prompt-content { + display: flex; + align-items: center; + gap: 1rem; + } + + .arrow { + border: solid white; + border-width: 0 4px 4px 0; + display: inline-block; + padding: 8px; + } + + .right { transform: rotate(-45deg); } + .left { transform: rotate(135deg); } + .up { transform: rotate(-135deg); } + \ No newline at end of file diff --git a/src/app/components/shared/ar-prompt/ar-prompt.component.html b/src/app/components/shared/ar-prompt/ar-prompt.component.html new file mode 100644 index 0000000..d4e3b8f --- /dev/null +++ b/src/app/components/shared/ar-prompt/ar-prompt.component.html @@ -0,0 +1,15 @@ +
+ +
+
+

Look Up

+
+ +
+
+

Look Around

+
+
+ +
+ \ No newline at end of file diff --git a/src/app/components/shared/ar-prompt/ar-prompt.component.spec.ts b/src/app/components/shared/ar-prompt/ar-prompt.component.spec.ts new file mode 100644 index 0000000..9377ac9 --- /dev/null +++ b/src/app/components/shared/ar-prompt/ar-prompt.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ArPromptComponent } from './ar-prompt.component'; + +describe('ArPromptComponent', () => { + let component: ArPromptComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ArPromptComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ArPromptComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/shared/ar-prompt/ar-prompt.component.ts b/src/app/components/shared/ar-prompt/ar-prompt.component.ts new file mode 100644 index 0000000..6a17f9f --- /dev/null +++ b/src/app/components/shared/ar-prompt/ar-prompt.component.ts @@ -0,0 +1,44 @@ +import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +type PromptState = 'look-up' | 'look-around'; + +@Component({ + selector: 'app-ar-prompt', + standalone: true, + imports: [CommonModule], + templateUrl: './ar-prompt.component.html', + styleUrls: ['./ar-prompt.component.css'] +}) +export class ArPromptComponent implements OnInit, OnDestroy { + + private readonly ANGLE_THRESHOLD = 45; + public currentState: PromptState = 'look-up'; + + private deviceOrientationHandler: (event: DeviceOrientationEvent) => void; + + constructor(private cdr: ChangeDetectorRef) { + this.deviceOrientationHandler = this.handleDeviceOrientation.bind(this); + } + + ngOnInit(): void { + window.addEventListener('deviceorientation', this.deviceOrientationHandler, true); + } + + ngOnDestroy(): void { + window.removeEventListener('deviceorientation', this.deviceOrientationHandler, true); + } + + private handleDeviceOrientation(event: DeviceOrientationEvent): void { + const beta = event.beta; + + if (beta === null) return; + + const newState = beta > this.ANGLE_THRESHOLD ? 'look-around' : 'look-up'; + + if (newState !== this.currentState) { + this.currentState = newState; + this.cdr.detectChanges(); + } + } +} diff --git a/src/app/components/test-suite/assessments/demographics-assessment/demographics-assessment.component.html b/src/app/components/test-suite/assessments/demographics-assessment/demographics-assessment.component.html deleted file mode 100644 index aa8b70c..0000000 --- a/src/app/components/test-suite/assessments/demographics-assessment/demographics-assessment.component.html +++ /dev/null @@ -1 +0,0 @@ -

demographics-assessment works!

diff --git a/src/app/components/test-suite/assessments/demographics-assessment/demographics-assessment.component.spec.ts b/src/app/components/test-suite/assessments/demographics-assessment/demographics-assessment.component.spec.ts deleted file mode 100644 index b7c3ca4..0000000 --- a/src/app/components/test-suite/assessments/demographics-assessment/demographics-assessment.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DemographicsAssessmentComponent } from './demographics-assessment.component'; - -describe('DemographicsAssessmentComponent', () => { - let component: DemographicsAssessmentComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DemographicsAssessmentComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(DemographicsAssessmentComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/components/test-suite/assessments/demographics-assessment/demographics-assessment.component.ts b/src/app/components/test-suite/assessments/demographics-assessment/demographics-assessment.component.ts deleted file mode 100644 index c76e123..0000000 --- a/src/app/components/test-suite/assessments/demographics-assessment/demographics-assessment.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-demographics-assessment', - standalone: true, - imports: [], - templateUrl: './demographics-assessment.component.html', - styleUrl: './demographics-assessment.component.css' -}) -export class DemographicsAssessmentComponent { - -} diff --git a/src/app/components/test-suite/assessments/demographics-assessment/demographics-assessment.component.css b/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.css similarity index 100% rename from src/app/components/test-suite/assessments/demographics-assessment/demographics-assessment.component.css rename to src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.css 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 new file mode 100644 index 0000000..bc055c3 --- /dev/null +++ b/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.html @@ -0,0 +1,199 @@ +
+
+
+

Abschließender Fragebogen

+

Ihre Angaben helfen uns bei der wissenschaftlichen Auswertung der Studie.

+
+ +
+ + +
+

+ 1 + Demografische Angaben +

+
+
+ + +
Die Angabe des Alters ist erforderlich.
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

+ 2 + Vorerfahrung mit AR/VR +

+
+
+
+ +
110
+
{{ demographicsForm.get('arExperience')?.value }}/10
+
+
+ +
110
+
{{ demographicsForm.get('vrExperience')?.value }}/10
+
+
+
+ +
+ +
+
+
+ + +
+
+
+ + +
+

+ 3 + Visuelle & Physische Angaben +

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

+ 4 + Technologiekompetenz +

+
+
+ +
+ 1 + + 10 + {{ demographicsForm.get(tech.formControl)?.value }} +
+
+
+
+ + +
+

+ 5 + Feedback zur Studie +

+
+
+ +
+ + {{ getEaseText(demographicsForm.get('easeOfUse')?.value) }} +
+
+
+ + +
+
+ +
+ + + +
+
+
+
+ + +
+ +
+
+

Bitte füllen Sie alle mit * markierten Pflichtfelder aus.

+
+
+ + +
+

Vielen Dank!

+

Ihre Angaben wurden erfolgreich übermittelt. Die Studie ist hiermit abgeschlossen.

+
+
+
diff --git a/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.spec.ts b/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.spec.ts new file mode 100644 index 0000000..89b517b --- /dev/null +++ b/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DemographicsFeedbackComponent } from './demographics-feedback.component'; + +describe('DemographicsFeedbackComponent', () => { + let component: DemographicsFeedbackComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DemographicsFeedbackComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DemographicsFeedbackComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.ts b/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.ts new file mode 100644 index 0000000..4ad29fd --- /dev/null +++ b/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.ts @@ -0,0 +1,132 @@ +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; + +@Component({ + selector: 'app-demographics-feedback', + standalone: true, + imports: [CommonModule, ReactiveFormsModule], + templateUrl: './demographics-feedback.component.html', + styleUrls: ['./demographics-feedback.component.css'] +}) +export class DemographicsFeedbackComponent implements OnInit { + @Output() testComplete = new EventEmitter(); + @Output() redoTest = new EventEmitter(); + + demographicsForm!: FormGroup; + isSubmitting = false; + isSubmitted = false; + + arVrDevices = [ + { value: 'smartphone-ar', label: 'Smartphone-AR' }, + { value: 'meta-quest', label: 'Meta Quest/Oculus' }, + { value: 'hololens', label: 'HoloLens' }, + { value: 'magic-leap', label: 'Magic Leap' }, + { value: 'playstation-vr', label: 'PlayStation VR' }, + { value: 'htc-vive', label: 'HTC Vive' }, + { value: 'google-cardboard', label: 'Google Cardboard' }, + { value: 'other', label: 'Andere' }, + { value: 'none', label: 'Keine' } + ]; + + technologyComfort = [ + { label: 'Kompetenz im Umgang mit Smartphones', formControl: 'smartphoneComfort' }, + { label: 'Allgemeine Technologieaffinität', formControl: 'techComfort' }, + { label: 'Erfahrung mit Videospielen', formControl: 'gamingExperience' } + ]; + + selectedDevices: string[] = []; + + constructor(private formBuilder: FormBuilder) {} + + ngOnInit() { + this.createForm(); + } + + createForm() { + this.demographicsForm = this.formBuilder.group({ + // Demografische Daten + age: ['', [Validators.required, Validators.min(13), Validators.max(120)]], + gender: ['', Validators.required], + education: [''], + occupation: [''], + + // AR/VR Vorerfahrung + arExperience: [1, Validators.required], + vrExperience: [1, Validators.required], + arVrFrequency: [''], + + // Physische und visuelle Einschränkungen + visionCorrection: [''], + colorVision: [''], + dominantHand: [''], + mobilityLimitations: [''], + + // Technologiekompetenz + smartphoneComfort: [5], + techComfort: [5], + gamingExperience: [5], + + // Feedback zur Studie + overallRating: [0, [Validators.required, Validators.min(1)]], + easeOfUse: [0, [Validators.required, Validators.min(1)]], + comments: [''], + wouldRecommend: ['', Validators.required] + }); + } + + onDeviceChange(event: any) { + const value = event.target.value; + if (event.target.checked) { + this.selectedDevices.push(value); + } else { + this.selectedDevices = this.selectedDevices.filter(device => device !== value); + } + } + + setRating(controlName: string, rating: number) { + this.demographicsForm.get(controlName)?.setValue(rating); + } + + getRatingText(rating: number): string { + const ratings = ['', 'Mangelhaft', 'Ausreichend', 'Gut', 'Sehr Gut', 'Exzellent']; + return ratings[rating] || ''; + } + + getEaseText(rating: number): string { + const ratings = ['', 'Sehr schwierig', 'Schwierig', 'Neutral', 'Einfach', 'Sehr einfach']; + return ratings[rating] || ''; + } + + onSubmit() { + if (this.demographicsForm.valid) { + this.isSubmitting = true; + + const formData = { + ...this.demographicsForm.value, + devicesUsed: this.selectedDevices, + submittedAt: new Date().toISOString() + }; + + console.log('Demographics and Feedback Data:', formData); + + setTimeout(() => { + this.isSubmitting = false; + this.isSubmitted = true; + this.testComplete.emit(); + }, 2000); + } else { + Object.keys(this.demographicsForm.controls).forEach(key => { + this.demographicsForm.get(key)?.markAsTouched(); + }); + } + } + + finishAssessment() { + this.testComplete.emit(); + } + + retryCurrent() { + this.redoTest.emit(); + } +} diff --git a/src/app/components/test-suite/assessments/rotation-speed-assessment/rotation-speed-assessment.component.html b/src/app/components/test-suite/assessments/rotation-speed-assessment/rotation-speed-assessment.component.html deleted file mode 100644 index 213d723..0000000 --- a/src/app/components/test-suite/assessments/rotation-speed-assessment/rotation-speed-assessment.component.html +++ /dev/null @@ -1 +0,0 @@ -

rotation-speed-assessment works!

diff --git a/src/app/components/test-suite/assessments/rotation-speed-assessment/rotation-speed-assessment.component.ts b/src/app/components/test-suite/assessments/rotation-speed-assessment/rotation-speed-assessment.component.ts deleted file mode 100644 index 3c8d9f8..0000000 --- a/src/app/components/test-suite/assessments/rotation-speed-assessment/rotation-speed-assessment.component.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Component, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { ArLoggerService } from '../../../../services/ar-logger.service'; - -@Component({ - selector: 'app-rotation-speed-assessment', - standalone: true, - imports: [CommonModule, FormsModule], - template: ``}) -export class RotationSpeedAssessmentComponent implements OnInit, OnDestroy { - @Output() testComplete = new EventEmitter(); - @Output() redoTest = new EventEmitter(); - - rotationSpeed = 50; - showConfirmButton = false; - showRedoButton = false; - xrSession: any = null; - - constructor(private logger: ArLoggerService) {} - - ngOnInit(): void { - this.logger.updateStatus('Assessment 2 initialized - Ready to test rotation speed'); - this.initializeWebXRTracking(); - } - - ngOnDestroy(): void { - this.exitWebXRSession(); - } - - private initializeWebXRTracking(): void { - const modelViewer = document.getElementById('speed-model') as any; - - if (modelViewer) { - modelViewer.addEventListener('ar-status', (event: any) => { - if (event.detail.status === 'session-started') { - this.xrSession = modelViewer.model?.webXRCamera?.session; - this.logger.updateStatus('WebXR active - Adjust rotation speed with slider'); - } - }); - } - } - - onSpeedChange(event: Event): void { - const target = event.target as HTMLInputElement; - this.rotationSpeed = parseInt(target.value); - - this.logger.logParameterAdjustment('speed', 'rotationSpeed', this.rotationSpeed); - - this.updateSpeedModel(this.rotationSpeed); - } - - private updateSpeedModel(speed: number): void { - const modelViewer = document.getElementById('speed-model') as any; - if (modelViewer) { - modelViewer.style.setProperty('--auto-rotate-delay', `${101 - speed}0ms`); - } - } - - confirmSpeedSetting(): void { - this.logger.markTestComplete('speedTest'); - this.showConfirmButton = true; - this.showRedoButton = true; - this.logger.updateStatus(`Speed confirmed at ${this.rotationSpeed}%`); - } - - protected completeTest(): void { - this.exitWebXRSession(); - this.testComplete.emit(); - } - - onRedoTest(): void { - this.rotationSpeed = 50; - this.showConfirmButton = false; - this.showRedoButton = false; - this.exitWebXRSession(); - this.redoTest.emit(); - } - - private exitWebXRSession(): void { - if (this.xrSession) { - this.xrSession.end().then(() => { - console.log('WebXR session ended'); - this.xrSession = null; - }).catch((err: any) => { - console.error('Error ending WebXR session:', err); - }); - } - } -} \ No newline at end of file diff --git a/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.css b/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.css index e69de29..3f785d5 100644 --- a/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.css +++ b/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.css @@ -0,0 +1,2 @@ +.ar-controls { display: none; } +model-viewer[ar-tracking="tracking"] .ar-controls { display: flex; } diff --git a/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.html b/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.html index 276661b..5363dc5 100644 --- a/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.html +++ b/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.html @@ -1,54 +1,52 @@ -
- - - - -
- -
- - - {{ scale.toFixed(2) }} -
- - -
- - - {{ verticalOffset.toFixed(2) }}m -
+
+ + + + +
+ +
+ + + {{ scale.toFixed(2) }}×
- -
- \ No newline at end of file + + +
+ + + {{ verticalOffset >= 0 ? '+' : '' }}{{ verticalOffset.toFixed(2) }}m +
+ + +
+ + +
+
+ +
\ No newline at end of file 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 bc07222..3dc2a9e 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 @@ -1,52 +1,79 @@ -import { Component, AfterViewInit, ElementRef, ViewChild, ChangeDetectorRef, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { + Component, + AfterViewInit, + ViewChild, + ElementRef, + ChangeDetectorRef, + CUSTOM_ELEMENTS_SCHEMA, + EventEmitter, + Output +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; +import '../../../../../assets/scripts/model-viewer'; + @Component({ selector: 'app-spatial-position-assessment', standalone: true, - imports: [CommonModule, FormsModule], + imports: [CommonModule, FormsModule], templateUrl: './spatial-position-assessment.component.html', - styleUrl: './spatial-position-assessment.component.css', + styleUrls: ['./spatial-position-assessment.component.css'], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class SpatialPositionAssessmentComponent implements AfterViewInit { @ViewChild('modelViewer') modelViewerRef!: ElementRef; + @Output() testComplete = new EventEmitter(); + @Output() redoTest = new EventEmitter(); - isInWebXR = false; - scale = 1; - verticalOffset = 0; + public scale = 1; + public verticalOffset = 0; + + protected isModelPlaced = false; + private initialArAnchor: { x: number; y: number; z: number } | null = null; constructor(private cdr: ChangeDetectorRef) {} ngAfterViewInit() { - const modelViewerElement = this.modelViewerRef.nativeElement; - - modelViewerElement.addEventListener('enter-vr', () => { - this.isInWebXR = true; - modelViewerElement.cameraControls = false; - this.cdr.detectChanges(); - }); - - modelViewerElement.addEventListener('exit-vr', () => { - this.isInWebXR = false; - modelViewerElement.cameraControls = true; - this.resetModelTransform(); - this.cdr.detectChanges(); + const mv = this.modelViewerRef.nativeElement; + mv.addEventListener('ar-status', (e: any) => { + if (e.detail.status === 'session-started') { + setTimeout(() => this.captureAnchor(), 500); + } }); } - updateModelTransform() { - const model = this.modelViewerRef.nativeElement.model; - if (model) { - model.scale.set(this.scale, this.scale, this.scale); - model.position.y = this.verticalOffset; + private async captureAnchor() { + const mv = this.modelViewerRef.nativeElement; + const anchor = mv.getAnchor(); + if (!anchor.includes('not placed')) { + const [x, y, z] = anchor.split(' ').map(parseFloat); + this.initialArAnchor = { x, y, z }; + this.isModelPlaced = true; + this.cdr.detectChanges(); + } else { + setTimeout(() => this.captureAnchor(), 500); } } - resetModelTransform() { + onSliderInput() { + if (!this.initialArAnchor) return; + const { x, y, z } = this.initialArAnchor!; + const newY = y + this.verticalOffset; + this.modelViewerRef.nativeElement.setAttribute('ar-anchor', `${x} ${newY} ${z}`); + } + + resetPosition() { this.scale = 1; this.verticalOffset = 0; - this.updateModelTransform(); + this.onSliderInput(); + } + + confirmPlacement() { + this.testComplete.emit(); + } + + retryCurrent() { + this.redoTest.emit(1); } } diff --git a/src/app/components/test-suite/assessments/rotation-speed-assessment/rotation-speed-assessment.component.css b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.css similarity index 100% rename from src/app/components/test-suite/assessments/rotation-speed-assessment/rotation-speed-assessment.component.css rename to src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.css 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 new file mode 100644 index 0000000..b21ecbf --- /dev/null +++ b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.html @@ -0,0 +1,122 @@ +
+ + + + + +
+ +
+

Finden sie ihre 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.

+
+ +
+ + +
+
+ + +
+ +
+

{{ remainingTime }}

+

Sekunden verbleibend

+

Halten Sie das Modell in Position.

+
+ + +
+
+
+
+ + +
+ + +
+ +
+

Test abgeschlossen

+
+ +
+ + +
+
+
+
+ \ No newline at end of file diff --git a/src/app/components/test-suite/assessments/rotation-speed-assessment/rotation-speed-assessment.component.spec.ts b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.spec.ts similarity index 87% rename from src/app/components/test-suite/assessments/rotation-speed-assessment/rotation-speed-assessment.component.spec.ts rename to src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.spec.ts index 31aa5e0..8274f3a 100644 --- a/src/app/components/test-suite/assessments/rotation-speed-assessment/rotation-speed-assessment.component.spec.ts +++ b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RotationSpeedAssessmentComponent } from './rotation-speed-assessment.component'; +import { RotationSpeedAssessmentComponent } from './spatial-stability-assessment.component'; describe('RotationSpeedAssessmentComponent', () => { let component: RotationSpeedAssessmentComponent; 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 new file mode 100644 index 0000000..c6fb3fe --- /dev/null +++ b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.ts @@ -0,0 +1,140 @@ +import { Component, AfterViewInit, ViewChild, ElementRef, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectorRef, EventEmitter, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import '../../../../../assets/scripts/model-viewer'; + +@Component({ + selector: 'app-spatial-stability-assessment', + standalone: true, + imports: [CommonModule, FormsModule], + templateUrl: './spatial-stability-assessment.component.html', + styleUrls: ['./spatial-stability-assessment.component.css'], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class SpatialStabilityAssessmentComponent implements AfterViewInit { + @ViewChild('modelViewer') modelViewerRef!: ElementRef; + @Output() testComplete = new EventEmitter(); + @Output() redoTest = new EventEmitter(); + + public scale = 1; + public verticalOffset = 0; + public currentPhase = 0; // 0: waiting, 1: adjust, 2: lock, 3: countdown, 4: complete + public remainingTime = 20; + public progressPercentage = 0; + + private initialArAnchor: { x: number, y: number, z: number } | null = null; + private lockedPosition: { scale: number, verticalOffset: number } | null = null; + private countdownInterval: any = null; + protected isModelPlaced = false; + + constructor(private cdr: ChangeDetectorRef) {} + + ngAfterViewInit() { + const modelViewer = this.modelViewerRef.nativeElement; + + modelViewer.addEventListener('ar-status', (event: any) => { + if (event.detail.status === 'session-started') { + setTimeout(() => this.getInitialAnchor(), 1000); + } + }); + } + + private async getInitialAnchor() { + const modelViewer = this.modelViewerRef.nativeElement; + const anchorString = modelViewer.getAnchor(); + + if (!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; + console.log('Starting adjustment phase'); + } else { + setTimeout(() => this.getInitialAnchor(), 500); + } + } + + onSliderInput() { + if (this.initialArAnchor && this.currentPhase === 1) { + const newY = this.initialArAnchor.y + this.verticalOffset; + const anchor = `${this.initialArAnchor.x} ${newY} ${this.initialArAnchor.z}`; + this.modelViewerRef.nativeElement.setAttribute('ar-anchor', anchor); + } + } + + lockPosition() { + this.lockedPosition = { + scale: this.scale, + verticalOffset: this.verticalOffset + }; + this.currentPhase = 2; + console.log('Position locked:', this.lockedPosition); + } + + adjustMore() { + this.currentPhase = 1; + } + + startCountdown() { + 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(); + } + + this.cdr.detectChanges(); + }, 1000); + + console.log('Countdown started'); + } + + cancelTest() { + if (this.countdownInterval) { + clearInterval(this.countdownInterval); + this.countdownInterval = null; + } + this.currentPhase = 2; + } + + private completeTest() { + if (this.countdownInterval) { + clearInterval(this.countdownInterval); + this.countdownInterval = null; + } + this.currentPhase = 4; // Complete phase + console.log('Test completed successfully!'); + } + + restartTest() { + this.currentPhase = 1; + this.remainingTime = 60; + this.progressPercentage = 0; + this.lockedPosition = null; + if (this.countdownInterval) { + clearInterval(this.countdownInterval); + this.countdownInterval = null; + } + } + + + ngOnDestroy() { + if (this.countdownInterval) { + clearInterval(this.countdownInterval); + } + } + + finishAssessment() { + this.testComplete.emit(); + } + + retryCurrent() { + this.redoTest.emit(1); + } +} diff --git a/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.css b/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.css index a37b52e..0014507 100644 --- a/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.css +++ b/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.css @@ -1,5 +1,26 @@ +.ar-controls { + display: none; +} -.annotation { - line-height: 1.4; - } - \ No newline at end of file +.ar-hotspot { + display: none; +} + +.ar-prompt { + display: none; +} + + */ +model-viewer[ar-status="session-started"]:not([ar-tracking="tracking"]) .ar-prompt { + display: block; +} + + +model-viewer[ar-tracking="tracking"] .ar-hotspot, +model-viewer[ar-tracking="tracking"] .ar-controls { + display: block; +} + +model-viewer[ar-tracking="tracking"] .ar-controls { + display: flex; +} \ No newline at end of file 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 8f189dd..60f587a 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 @@ -1,56 +1,73 @@ -
-
-

Assessment: Text Legibility Optimization

-

Objective: Determine optimal text scaling for ceiling-AR applications.

-

Procedure: Adjust text size using the WebXR slider until it is clearly legible.

-
- - - - - - - - -
-
- - -
+
+ + + +
- \ No newline at end of file + +
+ + + +
Minimum setzen
+
+ + {{ currentSize }}px + +
+ +
+ + + +
Minimum bestätigt: {{ currentSize }}px
+ + +
+ + + +
Maximum setzen
+
+ + {{ currentSize }}px + +
+ +
+ + + +
Maximum bestätigt: {{ currentSize }}px
+ +
+ + + +
Optimale Größe setzen
+
+ + {{ currentSize }}px + +
+ +
+ +
+ +
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 c891178..dd64643 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 @@ -1,42 +1,124 @@ -import { Component, AfterViewInit, ViewChild, ElementRef, ChangeDetectorRef, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { + Component, + AfterViewInit, + ViewChild, + ElementRef, + ChangeDetectorRef, + CUSTOM_ELEMENTS_SCHEMA, + EventEmitter, + Output +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; +import '../../../../../assets/scripts/model-viewer'; + @Component({ selector: 'app-text-legibility-assessment', standalone: true, imports: [CommonModule, FormsModule], templateUrl: './text-legibility-assessment.component.html', - styleUrl: './text-legibility-assessment.component.css', + styleUrls: ['./text-legibility-assessment.component.css'], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class TextLegibilityAssessmentComponent implements AfterViewInit { @ViewChild('modelViewer') modelViewerRef!: ElementRef; + @Output() testComplete = new EventEmitter(); + @Output() redoTest = new EventEmitter(); - isInWebXR = false; - textSize = 16; + minSize = 2; + maxSize = 64; + currentSize = 16; + comfortableSize = 16; + + phase: + | 'min' + | 'confirmedMin' + | 'max' + | 'confirmedMax' + | 'comfortable' + | 'confirmedComfort' + | 'finished' = 'min'; + + private offsetApplied = false; constructor(private cdr: ChangeDetectorRef) {} ngAfterViewInit() { - const modelViewerElement = this.modelViewerRef.nativeElement; + const mv = this.modelViewerRef.nativeElement; + mv.setAttribute('scale', '0.25 0.25 0.25'); - modelViewerElement.addEventListener('enter-vr', () => { - this.isInWebXR = true; - this.cdr.detectChanges(); - }); - - modelViewerElement.addEventListener('exit-vr', () => { - this.isInWebXR = false; - this.resetTextSize(); - this.cdr.detectChanges(); + mv.addEventListener('ar-status', async (e: any) => { + if (e.detail.status === 'session-started' && !this.offsetApplied) { + await mv.updateComplete; + const anchor = mv.getAnchor(); + if (!anchor.includes('not placed')) { + const [x, y, z] = anchor.split(' ').map(parseFloat); + mv.setAttribute('ar-anchor', `${x} ${y + 3.0} ${z}`); + this.offsetApplied = true; + } + } }); } - updateTextSize() { + decrease() { + if (this.currentSize > this.minSize) { + this.currentSize--; + this.syncComfortable(); + } } - resetTextSize() { - this.textSize = 16; + increase() { + if (this.currentSize < this.maxSize) { + this.currentSize++; + this.syncComfortable(); + } + } + + private syncComfortable() { + if (this.phase === 'comfortable') { + this.comfortableSize = this.currentSize; + } + } + + nextPhase() { + switch (this.phase) { + case 'min': + this.phase = 'confirmedMin'; + break; + case 'confirmedMin': + this.phase = 'max'; + this.currentSize = this.maxSize; + break; + case 'max': + this.phase = 'confirmedMax'; + break; + case 'confirmedMax': + this.phase = 'comfortable'; + this.currentSize = Math.floor((this.minSize + this.maxSize) / 2); + this.comfortableSize = this.currentSize; + break; + case 'comfortable': + this.phase = 'confirmedComfort'; + this.comfortableSize = this.currentSize; + break; + case 'confirmedComfort': + this.phase = 'finished'; + this.testComplete.emit(); + break; + } + } + + resetToMid() { + this.currentSize = Math.floor((this.minSize + this.maxSize) / 2); + this.phase = 'min'; + } + + retry() { + this.redoTest.emit(1); + } + + finishAssessment() { + this.testComplete.emit(); } } diff --git a/src/app/components/test-suite/test-suite.component.html b/src/app/components/test-suite/test-suite.component.html index 3c2480c..66508ab 100644 --- a/src/app/components/test-suite/test-suite.component.html +++ b/src/app/components/test-suite/test-suite.component.html @@ -1,91 +1,92 @@ -
-

Ceiling-AR Design Parameter Assessment Suite

+
+ + +
+

Augmented Reality Umfrage

- - Assessment {{ currentTest }} of {{ totalTests }} - - - {{ progress | number:'1.1-1' }}% - + Test {{ currentTest }} von {{ totalTests }} + {{ progress | number:'1.1-1' }}%
- -
-
+
+
+
+ + +
+
Bitte achten Sie darauf die Kamera auf eine Deckenfläche zu richten.
+
+
+ + +
+ + +
+ +
+
+

In diesem Test geht es darum, die Lesbarkeit von Texten in Augmented-Reality-Umgebungen (AR) im Kontext von Decken zu untersuchen.

+
    +
  • Stellen Sie bitte zunächst die Textgröße auf die minimale Stufe ein, bei der Sie den Text gerade noch entziffern können.
  • +
  • Passen Sie als Nächstes die Textgröße auf die maximale Stufe an, bei der der gesamte Inhalt noch vollständig auf dem Bildschirm sichtbar bleibt.
  • +
  • Wählen Sie zum Abschluss die Schriftgröße, die Sie persönlich als am angenehmsten empfinden.
  • +
-
- - -
-
- Status: {{ status }} -
-
- - -
- -
- - + +
+
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
-
-
\ No newline at end of file + + +
+
+
+

In diesem Test soll untersucht werden, welche Position eines virtuellen Objekts an der Decke als ideal empfunden wird.

+

Stellen Sie die beiden Schieberegler bitte so ein, dass das Modell für Sie optisch ansprechend und realistisch an der Decke platziert ist.

+
+
+
+ +
+
+ + +
+
+
+

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.
  • +
+
+
+
+ +
+
+ + +
+ +
+ +
+ + +
+ +
+ +
\ No newline at end of file diff --git a/src/app/components/test-suite/test-suite.component.ts b/src/app/components/test-suite/test-suite.component.ts index d571eba..d87f62f 100644 --- a/src/app/components/test-suite/test-suite.component.ts +++ b/src/app/components/test-suite/test-suite.component.ts @@ -4,12 +4,12 @@ import { Subject, takeUntil } from 'rxjs'; import { TestProgressService } from '../../services/test-progress.service'; import { ArLoggerService } from '../../services/ar-logger.service'; -import { RotationSpeedAssessmentComponent } from './assessments/rotation-speed-assessment/rotation-speed-assessment.component'; +import { SpatialStabilityAssessmentComponent } from './assessments/spatial-stability-assessment/spatial-stability-assessment.component'; import { ErgonomicAssessmentComponent } from './assessments/ergonomic-assessment/ergonomic-assessment.component'; import { TextLegibilityAssessmentComponent } from './assessments/text-legibility-assessment/text-legibility-assessment.component'; import { ScaleReferenceAssessmentComponent } from './assessments/scale-reference-assessment/scale-reference-assessment.component'; import { SpatialPositionAssessmentComponent } from './assessments/spatial-position-assessment/spatial-position-assessment.component'; -import { DemographicsAssessmentComponent } from './assessments/demographics-assessment/demographics-assessment.component'; +import { DemographicsFeedbackComponent } from './assessments/demographics-feedback/demographics-feedback.component'; import { FirstInputAssessmentComponent } from './assessments/first-input-assesment/first-input-assesment.component'; @Component({ @@ -18,21 +18,21 @@ import { FirstInputAssessmentComponent } from './assessments/first-input-assesme imports: [ CommonModule, FirstInputAssessmentComponent, - RotationSpeedAssessmentComponent, + SpatialStabilityAssessmentComponent, ErgonomicAssessmentComponent, TextLegibilityAssessmentComponent, ScaleReferenceAssessmentComponent, SpatialPositionAssessmentComponent, - DemographicsAssessmentComponent - ], + DemographicsFeedbackComponent, + DemographicsFeedbackComponent +], templateUrl: './test-suite.component.html', styleUrls: ['./test-suite.component.css'] }) export class TestSuiteComponent implements OnInit, OnDestroy { currentTest = 1; - totalTests = 7; - progress = 14.3; - status = 'Ready to begin assessments'; + totalTests = 4; + progress = 25; private destroy$ = new Subject(); @@ -58,7 +58,6 @@ export class TestSuiteComponent implements OnInit, OnDestroy { this.logger.status$ .pipe(takeUntil(this.destroy$)) .subscribe(status => { - this.status = status; }); this.totalTests = this.progressService.getTotalTests(); @@ -77,4 +76,9 @@ export class TestSuiteComponent implements OnInit, OnDestroy { redoTest(testNumber: number): void { this.progressService.goToTest(testNumber); } + + forceNextTest(): void { + this.logger.updateStatus(`Forcefully skipped to next test from test ${this.currentTest}.`); + this.progressService.nextTest(); + } } \ No newline at end of file diff --git a/src/app/services/test-progress.service.ts b/src/app/services/test-progress.service.ts index 900410a..18b0ab6 100644 --- a/src/app/services/test-progress.service.ts +++ b/src/app/services/test-progress.service.ts @@ -6,9 +6,9 @@ import { Router } from '@angular/router'; providedIn: 'root' }) export class TestProgressService { - private totalTests = 7; + private totalTests = 4; private currentTestSubject = new BehaviorSubject(1); - private progressSubject = new BehaviorSubject(14.3); // (1/7) * 100 + private progressSubject = new BehaviorSubject(25); // (1/totalTests) * 100 currentTest$ = this.currentTestSubject.asObservable(); progress$ = this.progressSubject.asObservable(); @@ -30,7 +30,6 @@ export class TestProgressService { this.currentTestSubject.next(nextTest); this.updateProgress(nextTest); } else { - // All tests completed, navigate to completion screen this.router.navigate(['/completion']); } } diff --git a/src/assets/scripts/model-viewer.js b/src/assets/scripts/model-viewer.js index 10c2c70..63834bc 100644 --- a/src/assets/scripts/model-viewer.js +++ b/src/assets/scripts/model-viewer.js @@ -69779,6 +69779,7 @@ class ARRenderer extends EventDispatcher { this.renderer = renderer; this.currentSession = null; this.placementMode = 'floor'; + this.anchorOffset = null; this.placementBox = null; this.menuPanel = null; this.lastTick = null; @@ -69797,6 +69798,7 @@ class ARRenderer extends EventDispatcher { this.xrController1 = null; this.xrController2 = null; this.selectedXRController = null; + this.parsedAnchorOffset = null; this.tracking = true; this.frames = 0; this.initialized = false; @@ -70009,6 +70011,35 @@ class ARRenderer extends EventDispatcher { get presentedScene() { return this._presentedScene; } + get currentGoalPosition() { + return this.goalPosition; + } + get isObjectPlaced() { + return this.placementComplete; + } + updateAnchor(newAnchor) { + var _a; + if (newAnchor) { + const parts = newAnchor.split(' ').map(Number); + if (parts.length === 3 && parts.every(p => !isNaN(p))) { + const newPosition = new Vector3(parts[0], parts[1], parts[2]); + this.goalPosition.copy(newPosition); + this.parsedAnchorOffset = newPosition; + if (!this.placementComplete) { + this.placementComplete = true; + this.worldSpaceInitialPlacementDone = true; + if (this.placementBox) { + this.placementBox.show = false; + } + (_a = this.presentedScene) === null || _a === void 0 ? void 0 : _a.setShadowIntensity(AR_SHADOW_INTENSITY); + this.dispatchEvent({ type: 'status', status: ARStatus.OBJECT_PLACED }); + } + } + else { + console.warn(`Invalid dynamic ar-anchor value: "${newAnchor}"`); + } + } + } /** * Resolves to true if the renderer has detected all the necessary qualities * to support presentation in AR. @@ -70032,6 +70063,16 @@ class ARRenderer extends EventDispatcher { if (this.isPresenting) { console.warn('Cannot present while a model is already presenting'); } + this.parsedAnchorOffset = null; + if (this.anchorOffset) { + const parts = this.anchorOffset.split(' ').map(Number); + if (parts.length === 3 && parts.every(p => !isNaN(p))) { + this.parsedAnchorOffset = new Vector3(parts[0], parts[1], parts[2]); + } + else { + console.warn(`Invalid ar-anchor value: "${this.anchorOffset}"`); + } + } let waitForAnimationFrame = new Promise((resolve, _reject) => { requestAnimationFrame(() => resolve()); }); @@ -70305,6 +70346,8 @@ class ARRenderer extends EventDispatcher { this.inputSource = null; this.overlay = null; this.worldSpaceInitialPlacementDone = false; + this.anchorOffset = null; + this.parsedAnchorOffset = null; if (this.resolveCleanup != null) { this.resolveCleanup(); } @@ -70339,6 +70382,40 @@ class ARRenderer extends EventDispatcher { 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 + 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. + if (this.xrMode === XRMode.SCREEN_SPACE) { + const { session } = this.frame; + session.addEventListener('selectstart', this.onSelectStart); + session.addEventListener('selectend', this.onSelectEnd); + session.requestHitTestSourceForTransientInput({ profile: 'generic-touchscreen' }) + .then(hitTestSource => { this.transientHitTestSource = hitTestSource; }); + } + 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. + } const { width, height } = this.overlay.getBoundingClientRect(); scene.setSize(width, height); xrCamera.projectionMatrixInverse.copy(xrCamera.projectionMatrix).invert(); @@ -70491,6 +70568,9 @@ class ARRenderer extends EventDispatcher { * until a ceiling hit arrives (no premature floor placement). */ moveToAnchor(frame) { + if (this.parsedAnchorOffset != null) { + return; + } // Handle deferred initial placement for ceiling mode if (this.placeOnCeiling && this.xrMode === XRMode.WORLD_SPACE && @@ -80116,6 +80196,7 @@ const ARMixin = (ModelViewerElement) => { this.arScale = 'auto'; this.arUsdzMaxTextureSize = 'auto'; this.arPlacement = 'floor'; + this.arAnchor = null; this.arModes = DEFAULT_AR_MODES; this.iosSrc = null; this.xrEnvironment = false; @@ -80179,6 +80260,9 @@ const ARMixin = (ModelViewerElement) => { this[$scene].updateShadow(); this[$needsRender](); } + if (changedProperties.has('arAnchor') && this[$renderer].arRenderer.isPresenting) { + this[$renderer].arRenderer.updateAnchor(this.arAnchor); + } if (changedProperties.has('arModes')) { this[$arModes] = deserializeARModes(this.arModes); } @@ -80188,6 +80272,14 @@ const ARMixin = (ModelViewerElement) => { this[$selectARMode](); } } + getAnchor() { + const arRenderer = this[$renderer].arRenderer; + if (arRenderer.isPresenting && arRenderer.isObjectPlaced) { + const position = arRenderer.currentGoalPosition; + return `${position.x} ${position.y} ${position.z}`; + } + return 'Model not placed in AR yet.'; + } /** * Activates AR. Note that for any mode that is not WebXR-based, this * method most likely has to be called synchronous from a user @@ -80274,6 +80366,7 @@ configuration or device capabilities'); else { arRenderer.placementMode = 'floor'; } + arRenderer.anchorOffset = this.arAnchor; await arRenderer.present(this[$scene], this.xrEnvironment); } catch (error) { @@ -80447,6 +80540,9 @@ configuration or device capabilities'); __decorate$2([ property({ type: String, attribute: 'ar-placement' }) ], ARModelViewerElement.prototype, "arPlacement", void 0); + __decorate$2([ + property({ type: String, attribute: 'ar-anchor' }) + ], ARModelViewerElement.prototype, "arAnchor", void 0); __decorate$2([ property({ type: String, attribute: 'ar-modes' }) ], ARModelViewerElement.prototype, "arModes", void 0); diff --git a/tsconfig.json b/tsconfig.json index d3dbc4a..639558a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "esModuleInterop": true, "sourceMap": true, "declaration": false, + "allowJs": true, "experimentalDecorators": true, "moduleResolution": "bundler", "importHelpers": true,