improves placement logic and implements a feature to disable touch interaction in ar mode
parent
c6f789a7c9
commit
8b07a88309
File diff suppressed because one or more lines are too long
|
|
@ -68,6 +68,7 @@ const $triggerLoad = Symbol('triggerLoad');
|
|||
export declare interface ARInterface {
|
||||
ar: boolean;
|
||||
arModes: string;
|
||||
arInteraction: boolean;
|
||||
arScale: string;
|
||||
arPlacement: 'floor'|'wall'|'ceiling';
|
||||
arAnchor: string|null;
|
||||
|
|
@ -84,6 +85,9 @@ export const ARMixin = <T extends Constructor<ModelViewerElementBase>>(
|
|||
class ARModelViewerElement extends ModelViewerElement {
|
||||
@property({type: Boolean, attribute: 'ar'}) ar: boolean = false;
|
||||
|
||||
@property({type: Boolean, attribute: 'ar-interaction'})
|
||||
arInteraction: boolean = true;
|
||||
|
||||
@property({type: String, attribute: 'ar-scale'}) arScale: string = 'auto';
|
||||
|
||||
@property({type: String, attribute: 'ar-usdz-max-texture-size'})
|
||||
|
|
@ -181,6 +185,10 @@ export const ARMixin = <T extends Constructor<ModelViewerElementBase>>(
|
|||
this[$scene].canScale = this.arScale !== 'fixed';
|
||||
}
|
||||
|
||||
if (changedProperties.has('arInteraction')) {
|
||||
this[$renderer].arRenderer.isInteractionEnabled = this.arInteraction;
|
||||
}
|
||||
|
||||
if (changedProperties.has('arPlacement')) {
|
||||
this[$scene].updateShadow();
|
||||
this[$needsRender]();
|
||||
|
|
@ -194,6 +202,17 @@ export const ARMixin = <T extends Constructor<ModelViewerElementBase>>(
|
|||
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 &&
|
||||
!arRenderer.presentedScene?.visible;
|
||||
|
||||
if (!isDeferredCeiling) {
|
||||
arRenderer.updateAnchor(this.arAnchor);
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has('ar') || changedProperties.has('arModes') ||
|
||||
changedProperties.has('src') || changedProperties.has('iosSrc') ||
|
||||
changedProperties.has('arUsdzMaxTextureSize')) {
|
||||
|
|
@ -204,7 +223,7 @@ export const ARMixin = <T extends Constructor<ModelViewerElementBase>>(
|
|||
public getAnchor(): string {
|
||||
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.';
|
||||
|
|
@ -297,6 +316,7 @@ configuration or device capabilities');
|
|||
this[$arButtonContainer].removeEventListener(
|
||||
'click', this[$onARButtonContainerClick]);
|
||||
const {arRenderer} = this[$renderer];
|
||||
arRenderer.isInteractionEnabled = this.arInteraction;
|
||||
if (this.arPlacement === 'wall') {
|
||||
arRenderer.placementMode = 'wall';
|
||||
} else if (this.arPlacement === 'ceiling') {
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ export class ARRenderer extends EventDispatcher<
|
|||
public currentSession: XRSession|null = null;
|
||||
public placementMode:'floor'|'wall'|'ceiling' = 'floor';
|
||||
public anchorOffset: string|null = null;
|
||||
public isInteractionEnabled = true;
|
||||
|
||||
private placementBox: PlacementBox|null = null;
|
||||
private menuPanel: XRMenuPanel|null = null;
|
||||
|
|
@ -230,6 +231,13 @@ export class ARRenderer extends EventDispatcher<
|
|||
return this.goalPosition;
|
||||
}
|
||||
|
||||
public get currentPosition(): Vector3 {
|
||||
if (this.presentedScene) {
|
||||
return this.presentedScene.pivot.position;
|
||||
}
|
||||
return this.goalPosition;
|
||||
}
|
||||
|
||||
public get isObjectPlaced(): boolean {
|
||||
return this.placementComplete;
|
||||
}
|
||||
|
|
@ -417,9 +425,11 @@ export class ARRenderer extends EventDispatcher<
|
|||
|
||||
private setupController(controller: XRController) {
|
||||
this.setupXRControllerLine(controller);
|
||||
if (this.isInteractionEnabled) {
|
||||
controller.addEventListener('selectstart', this.onControllerSelectStart);
|
||||
controller.addEventListener('selectend', this.onControllerSelectEnd);
|
||||
}
|
||||
}
|
||||
private setupXRControllers() {
|
||||
this.xrController1 = this.threeRenderer.xr.getController(0) as XRController;
|
||||
this.xrController2 = this.threeRenderer.xr.getController(1) as XRController;
|
||||
|
|
@ -801,27 +811,22 @@ export class ARRenderer extends EventDispatcher<
|
|||
|
||||
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
|
||||
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.
|
||||
if (this.isInteractionEnabled) {
|
||||
if (this.xrMode === XRMode.SCREEN_SPACE) {
|
||||
const { session } = this.frame!;
|
||||
session.addEventListener('selectstart', this.onSelectStart);
|
||||
|
|
@ -830,12 +835,9 @@ export class ARRenderer extends EventDispatcher<
|
|||
.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.
|
||||
return;
|
||||
}
|
||||
|
||||
const {width, height} = this.overlay!.getBoundingClientRect();
|
||||
|
|
@ -852,10 +854,9 @@ export class ARRenderer extends EventDispatcher<
|
|||
this.goalYaw = scene.yaw;
|
||||
|
||||
if (this.placeOnCeiling && !this.isViewPointingUp()) {
|
||||
scene.visible = false; // Hide until properly oriented
|
||||
scene.setHotspotsVisibility(true); // Still show UI
|
||||
scene.visible = false;
|
||||
scene.setHotspotsVisibility(true);
|
||||
|
||||
// Set up touch interaction for screen-space mode
|
||||
if (this.xrMode === XRMode.SCREEN_SPACE) {
|
||||
const {session} = this.frame!;
|
||||
session.addEventListener('selectstart', this.onSelectStart);
|
||||
|
|
@ -863,35 +864,25 @@ export class ARRenderer extends EventDispatcher<
|
|||
session.requestHitTestSourceForTransientInput!({profile: 'generic-touchscreen'})!
|
||||
.then(hitTestSource => { this.transientHitTestSource = hitTestSource; });
|
||||
}
|
||||
return; // Exit early - don't place yet
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
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));
|
||||
|
|
@ -903,9 +894,18 @@ export class ARRenderer extends EventDispatcher<
|
|||
this.goalPosition.copy(position);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
this.presentedScene?.setShadowIntensity(AR_SHADOW_INTENSITY);
|
||||
this.dispatchEvent({ type: 'status', status: ARStatus.OBJECT_PLACED });
|
||||
|
||||
if (this.isInteractionEnabled) {
|
||||
if (this.xrMode === XRMode.SCREEN_SPACE) {
|
||||
const { session } = this.frame!;
|
||||
session.addEventListener('selectstart', this.onSelectStart);
|
||||
|
|
@ -914,6 +914,7 @@ export class ARRenderer extends EventDispatcher<
|
|||
.then(hitTestSource => { this.transientHitTestSource = hitTestSource; });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkForDeferredCeilingPlacement(): void {
|
||||
// Check on every frame—both XR modes, only when ceiling is the target and the model is hidden
|
||||
|
|
@ -952,17 +953,23 @@ export class ARRenderer extends EventDispatcher<
|
|||
scene.pivot.position.add(target).sub(this.oldTarget);
|
||||
this.goalPosition.copy(scene.pivot.position);
|
||||
// Setup touch interaction if needed
|
||||
if (this.isInteractionEnabled) {
|
||||
const { session } = this.frame!;
|
||||
session.addEventListener('selectstart', this.onSelectStart);
|
||||
session.addEventListener('selectend', this.onSelectEnd);
|
||||
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});
|
||||
}
|
||||
|
||||
|
||||
private getTouchLocation(): Vector3|null {
|
||||
const {axes} = this.inputSource!.gamepad!;
|
||||
let location = this.placementBox!.getExpandedHit(
|
||||
|
|
@ -1015,48 +1022,32 @@ export class ARRenderer extends EventDispatcher<
|
|||
* until a ceiling hit arrives (no premature floor placement).
|
||||
*/
|
||||
public moveToAnchor(frame: XRFrame) {
|
||||
if (this.parsedAnchorOffset != null) {
|
||||
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');
|
||||
if (this.parsedAnchorOffset != null || this.placementComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
const hitSource = this.initialHitSource;
|
||||
if (hitSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.goalPosition.copy(optimalPosition);
|
||||
this.goalScale = optimalScale;
|
||||
this.initialModelScale = optimalScale;
|
||||
const hitResults = frame.getHitTestResults(hitSource);
|
||||
if (hitResults.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
scene.pivot.position.copy(optimalPosition);
|
||||
scene.pivot.scale.set(optimalScale, optimalScale, optimalScale);
|
||||
const hitResult = hitResults[0];
|
||||
const hitPosition = this.getHitPoint(hitResult);
|
||||
|
||||
this.worldSpaceInitialPlacementDone = true;
|
||||
this.calculateWorldSpaceScaleLimits(scene);
|
||||
this.enableWorldSpaceUserInteraction();
|
||||
if (hitPosition != null) {
|
||||
this.goalPosition.copy(hitPosition);
|
||||
|
||||
scene.visible = true;
|
||||
this.placementComplete = true;
|
||||
if (this.placementBox) {
|
||||
this.placementBox.show = false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1085,6 +1076,9 @@ export class ARRenderer extends EventDispatcher<
|
|||
|
||||
|
||||
private onSelectStart = (event: Event) => {
|
||||
if (!this.isInteractionEnabled) {
|
||||
return;
|
||||
}
|
||||
const hitSource = this.transientHitTestSource;
|
||||
if (hitSource == null) {
|
||||
return;
|
||||
|
|
|
|||
Loading…
Reference in New Issue