extens model-viewer so it exposes the models anchor to allow moving the model in AR Context
parent
5ce6d73279
commit
c6f789a7c9
|
|
@ -18154,6 +18154,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;
|
||||
|
|
@ -18172,6 +18173,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;
|
||||
|
|
@ -18384,6 +18386,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.
|
||||
|
|
@ -18407,6 +18438,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());
|
||||
});
|
||||
|
|
@ -18680,6 +18721,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();
|
||||
}
|
||||
|
|
@ -18714,6 +18757,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();
|
||||
|
|
@ -18866,6 +18943,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 &&
|
||||
|
|
@ -28491,6 +28571,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;
|
||||
|
|
@ -28554,6 +28635,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);
|
||||
}
|
||||
|
|
@ -28563,6 +28647,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
|
||||
|
|
@ -28649,6 +28741,7 @@ configuration or device capabilities');
|
|||
else {
|
||||
arRenderer.placementMode = 'floor';
|
||||
}
|
||||
arRenderer.anchorOffset = this.arAnchor;
|
||||
await arRenderer.present(this[$scene], this.xrEnvironment);
|
||||
}
|
||||
catch (error) {
|
||||
|
|
@ -28822,6 +28915,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);
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -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);
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -70,11 +70,13 @@ export declare interface ARInterface {
|
|||
arModes: string;
|
||||
arScale: string;
|
||||
arPlacement: 'floor'|'wall'|'ceiling';
|
||||
arAnchor: string|null;
|
||||
iosSrc: string|null;
|
||||
xrEnvironment: boolean;
|
||||
arUsdzMaxTextureSize: string;
|
||||
readonly canActivateAR: boolean;
|
||||
activateAR(): Promise<void>;
|
||||
getAnchor(): string;
|
||||
}
|
||||
|
||||
export const ARMixin = <T extends Constructor<ModelViewerElementBase>>(
|
||||
|
|
@ -90,6 +92,9 @@ export const ARMixin = <T extends Constructor<ModelViewerElementBase>>(
|
|||
@property({type: String, attribute: 'ar-placement'})
|
||||
arPlacement: 'floor'|'wall'|'ceiling' = 'floor';
|
||||
|
||||
@property({type: String, attribute: 'ar-anchor'})
|
||||
arAnchor: string|null = null;
|
||||
|
||||
@property({type: String, attribute: 'ar-modes'})
|
||||
arModes: string = DEFAULT_AR_MODES;
|
||||
|
||||
|
|
@ -181,6 +186,10 @@ export const ARMixin = <T extends Constructor<ModelViewerElementBase>>(
|
|||
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);
|
||||
}
|
||||
|
|
@ -192,6 +201,15 @@ export const ARMixin = <T extends Constructor<ModelViewerElementBase>>(
|
|||
}
|
||||
}
|
||||
|
||||
public getAnchor(): string {
|
||||
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
|
||||
|
|
@ -286,6 +304,7 @@ configuration or device capabilities');
|
|||
} else {
|
||||
arRenderer.placementMode = 'floor';
|
||||
}
|
||||
arRenderer.anchorOffset = this.arAnchor;
|
||||
await arRenderer.present(this[$scene], this.xrEnvironment);
|
||||
} catch (error) {
|
||||
console.warn('Error while trying to present in AR with WebXR');
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ export class ARRenderer extends EventDispatcher<
|
|||
public threeRenderer: WebGLRenderer;
|
||||
public currentSession: XRSession|null = null;
|
||||
public placementMode:'floor'|'wall'|'ceiling' = 'floor';
|
||||
public anchorOffset: string|null = null;
|
||||
|
||||
private placementBox: PlacementBox|null = null;
|
||||
private menuPanel: XRMenuPanel|null = null;
|
||||
|
|
@ -149,6 +150,7 @@ export class ARRenderer extends EventDispatcher<
|
|||
private xrController1: XRController|null = null;
|
||||
private xrController2: XRController|null = null;
|
||||
private selectedXRController: XRController|null = null;
|
||||
private parsedAnchorOffset: Vector3|null = null;
|
||||
|
||||
private tracking = true;
|
||||
private frames = 0;
|
||||
|
|
@ -224,6 +226,37 @@ export class ARRenderer extends EventDispatcher<
|
|||
return this._presentedScene;
|
||||
}
|
||||
|
||||
public get currentGoalPosition(): Vector3 {
|
||||
return this.goalPosition;
|
||||
}
|
||||
|
||||
public get isObjectPlaced(): boolean {
|
||||
return this.placementComplete;
|
||||
}
|
||||
|
||||
public updateAnchor(newAnchor: string|null) {
|
||||
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;
|
||||
}
|
||||
this.presentedScene?.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.
|
||||
|
|
@ -249,6 +282,16 @@ export class ARRenderer extends EventDispatcher<
|
|||
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<void>((resolve, _reject) => {
|
||||
requestAnimationFrame(() => resolve());
|
||||
});
|
||||
|
|
@ -322,7 +365,7 @@ export class ARRenderer extends EventDispatcher<
|
|||
const r = CEILING_HIT_ANGLE_DEG*Math.PI/180;
|
||||
ray = new XRRay(new DOMPoint(0,0,0), {x:0, y: Math.sin(r), z:-Math.cos(r)});
|
||||
}
|
||||
|
||||
|
||||
currentSession
|
||||
.requestHitTestSource!
|
||||
({space: viewerRefSpace, offsetRay: ray})!.then(hitTestSource => {
|
||||
|
|
@ -711,6 +754,8 @@ export 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!();
|
||||
|
|
@ -753,6 +798,45 @@ export 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);
|
||||
|
|
@ -830,7 +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
|
||||
if (!this.placeOnCeiling || !this.presentedScene || this.presentedScene.visible) return;
|
||||
|
|
@ -931,6 +1015,9 @@ 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 &&
|
||||
|
|
@ -1362,7 +1449,7 @@ export class ARRenderer extends EventDispatcher<
|
|||
|
||||
this.presentedScene!.renderShadow(this.threeRenderer);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Only public to make it testable.
|
||||
*/
|
||||
|
|
@ -1370,7 +1457,7 @@ export class ARRenderer extends EventDispatcher<
|
|||
if (this.xrMode !== XRMode.SCREEN_SPACE) {
|
||||
this.updateXRControllerHover();
|
||||
}
|
||||
|
||||
|
||||
this.frame = frame;
|
||||
// increamenets a counter tracking how many frames have been processed sinces the session started
|
||||
++this.frames;
|
||||
|
|
|
|||
Loading…
Reference in New Issue