diff --git a/src/components/media-views.js b/src/components/media-views.js index ae72461f1d69bb73af6c29b3a777f2a1d6487681..de356c7491dc644cd46b9a5f14a5ca76d78abf86 100644 --- a/src/components/media-views.js +++ b/src/components/media-views.js @@ -231,6 +231,7 @@ AFRAME.registerComponent("media-video", { init() { this.onPauseStateChange = this.onPauseStateChange.bind(this); + this.tryUpdateVideoPlaybackState = this.tryUpdateVideoPlaybackState.bind(this); this._grabStart = this._grabStart.bind(this); this._grabEnd = this._grabEnd.bind(this); @@ -290,7 +291,7 @@ AFRAME.registerComponent("media-video", { togglePlayingIfOwner() { if (this.networkedEl && NAF.utils.isMine(this.networkedEl) && this.video) { - this.data.videoPaused ? this.video.play() : this.video.pause(); + this.tryUpdateVideoPlaybackState(!this.data.videoPaused); } }, @@ -306,6 +307,10 @@ AFRAME.registerComponent("media-video", { onPauseStateChange() { this.el.setAttribute("media-video", "videoPaused", this.video.paused); + + if (this.networkedEl && NAF.utils.isMine(this.networkedEl)) { + this.el.emit("owned-video-state-changed"); + } }, async updateTexture(src) { @@ -353,9 +358,39 @@ AFRAME.registerComponent("media-video", { updatePlaybackState(force) { if (force || (this.networkedEl && !NAF.utils.isMine(this.networkedEl) && this.video)) { if (Math.abs(this.data.time - this.video.currentTime) > this.data.syncTolerance) { - this.video.currentTime = this.data.time; + this.tryUpdateVideoPlaybackState(this.data.videoPaused, this.data.time); + } else { + this.tryUpdateVideoPlaybackState(this.data.videoPaused); + } + } + }, + + tryUpdateVideoPlaybackState(pause, currentTime) { + if (this._playbackStateChangeTimeout) { + clearTimeout(this._playbackStateChangeTimeout); + } + + if (pause) { + this.video.pause(); + + if (currentTime) { + this.video.currentTime = currentTime; } - this.data.videoPaused ? this.video.pause() : this.video.play(); + } else { + // Need to deal with the fact play() may fail if user has not interacted with browser yet. + this.video + .play() + .then(() => { + if (currentTime) { + this.video.currentTime = currentTime; + } + }) + .catch(() => { + this._playbackStateChangeTimeout = setTimeout( + () => this.tryUpdateVideoPlaybackState(pause, currentTime), + 1000 + ); + }); } }, diff --git a/src/components/pinnable.js b/src/components/pinnable.js index 1ab9573bcd06aca7316b974b3c789eacbc063780..e719c033cde63a9f5d8c5ad025dafeeee2e845ae 100644 --- a/src/components/pinnable.js +++ b/src/components/pinnable.js @@ -19,6 +19,9 @@ AFRAME.registerComponent("pinnable", { // Fire pinned events when page changes so we can persist the page. this.el.addEventListener("pager-page-changed", this._fireEvents); + // Fire pinned events when video state changes so we can persist the page. + this.el.addEventListener("owned-video-state-changed", this._fireEvents); + // Hack: need to wait for the initial grabbable and stretchable components // to show up from the template before applying. this.el.addEventListener("componentinitialized", this._allowApplyOnceComponentsReady); diff --git a/src/components/position-at-box-shape-border.js b/src/components/position-at-box-shape-border.js index 561577b5eb1d302f0dfa44d508352e76cad11829..962433b6637c5fed3cf0373a31d1c893f62f87be 100644 --- a/src/components/position-at-box-shape-border.js +++ b/src/components/position-at-box-shape-border.js @@ -54,14 +54,27 @@ AFRAME.registerComponent("position-at-box-shape-border", { const camWorldPos = new THREE.Vector3(); const targetPosition = new THREE.Vector3(); const pointOnBoxFace = new THREE.Vector3(); + const tempParentWorldScale = new THREE.Vector3(); + return function() { if (!this.target) { - this.target = this.el.querySelector(this.data.target).object3D; + this.targetEl = this.el.querySelector(this.data.target); + this.target = this.targetEl.object3D; + if (!this.target) return; + + if (this.targetEl.getAttribute("visible") === false) { + this.target.scale.setScalar(0.01); // To avoid "pop" of gigantic button first time + return; + } } + + if (this.targetEl.getAttribute("visible") === false) return; + if (!this.el.getObject3D("mesh")) { return; } + if (!this.halfExtents || this.mesh !== this.el.getObject3D("mesh") || this.shape !== this.el.components.shape) { this.mesh = this.el.getObject3D("mesh"); if (this.el.components.shape) { @@ -102,7 +115,14 @@ AFRAME.registerComponent("position-at-box-shape-border", { this.target.position.copy(targetPosition.copy(targetDir).multiplyScalar(targetHalfExtent)); this.target.rotation.set(0, targetRotation, 0); - this.target.scale.setScalar(this.halfExtents[inverseHalfExtents[targetHalfExtentStr]] * 4); + + tempParentWorldScale.setFromMatrixScale(this.target.parent.matrixWorld); + + const distance = Math.sqrt(minSquareDistance); + const scale = this.halfExtents[inverseHalfExtents[targetHalfExtentStr]] * distance; + const targetScale = Math.min(2.0, Math.max(0.5, scale * tempParentWorldScale.x)); + + this.target.scale.setScalar(targetScale / tempParentWorldScale.x); }; })() }); diff --git a/src/components/super-networked-interactable.js b/src/components/super-networked-interactable.js index 3943725e4f85801edf94402dcf13064bc2925d66..8f2d9bee8588c541dc5845a5f047b6e6a67f7cac 100644 --- a/src/components/super-networked-interactable.js +++ b/src/components/super-networked-interactable.js @@ -35,7 +35,7 @@ AFRAME.registerComponent("super-networked-interactable", { NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { this.networkedEl = networkedEl; - this._syncCounterRegistration(networkedEl); + this._syncCounterRegistration(); if (!NAF.utils.isMine(networkedEl)) { this.el.setAttribute("body", { type: "static" }); } @@ -69,7 +69,7 @@ AFRAME.registerComponent("super-networked-interactable", { if (this.networkedEl && !NAF.utils.isMine(this.networkedEl)) { if (NAF.utils.takeOwnership(this.networkedEl)) { this.el.setAttribute("body", { type: "dynamic" }); - this._syncCounterRegistration(this.networkedEl); + this._syncCounterRegistration(); } else { this.el.emit("grab-end", { hand: this.hand }); this.hand = null; @@ -99,7 +99,7 @@ AFRAME.registerComponent("super-networked-interactable", { _syncCounterRegistration: function() { const el = this.networkedEl; - if (!el) return; + if (!el || !el.components["networked"]) return; const isPinned = el.components["pinnable"] && el.components["pinnable"].data.pinned; diff --git a/src/components/visible-while-frozen.js b/src/components/visible-while-frozen.js index 4acff3d420136fee0393dc84ae32e14c6779553b..c9c50eab3ba640486d42c65873cecf656912adc4 100644 --- a/src/components/visible-while-frozen.js +++ b/src/components/visible-while-frozen.js @@ -4,12 +4,51 @@ * @component visible-while-frozen */ AFRAME.registerComponent("visible-while-frozen", { + schema: { + withinDistance: { type: "number" } + }, + init() { + this.updateVisibility = this.updateVisibility.bind(this); + this.camWorldPos = new THREE.Vector3(); + this.objWorldPos = new THREE.Vector3(); + this.cam = this.el.sceneEl.camera.el.object3D; this.onStateChange = evt => { if (!evt.detail === "frozen") return; - this.el.setAttribute("visible", this.el.sceneEl.is("frozen")); + this.updateVisibility(); }; - this.el.setAttribute("visible", this.el.sceneEl.is("frozen")); + this.updateVisibility(); + }, + + tick() { + if (!this.data.withinDistance) return; + + const isFrozen = this.el.sceneEl.is("frozen"); + const isVisible = this.el.getAttribute("visible"); + if (!isFrozen && !isVisible) return; + + this.updateVisibility(); + }, + + updateVisibility() { + const isFrozen = this.el.sceneEl.is("frozen"); + + let isWithinDistance = true; + + if (this.data.withinDistance !== undefined) { + this.cam.getWorldPosition(this.camWorldPos); + this.objWorldPos.copy(this.el.object3D.position); + this.el.object3D.localToWorld(this.objWorldPos); + + isWithinDistance = + this.camWorldPos.distanceToSquared(this.objWorldPos) < this.data.withinDistance * this.data.withinDistance; + } + + const shouldBeVisible = isFrozen && isWithinDistance; + + if (this.el.getAttribute("visible") !== shouldBeVisible) { + this.el.setAttribute("visible", shouldBeVisible); + } }, play() { diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js index 1bca96496e0eb7ee726e66c49af6b4406d4a61e4..15c2f584750e1bbf5a4ea81c6c37699369925876 100644 --- a/src/gltf-component-mappings.js +++ b/src/gltf-component-mappings.js @@ -89,4 +89,12 @@ AFRAME.GLTFModelPlus.registerComponent("media", "media", (el, componentName, com if (componentData.pageIndex) { el.setAttribute("media-pager", { index: componentData.pageIndex }); } + + if (componentData.paused !== undefined) { + el.setAttribute("media-video", { videoPaused: componentData.paused }); + } + + if (componentData.time) { + el.setAttribute("media-video", { time: componentData.time }); + } }); diff --git a/src/hub.html b/src/hub.html index 28f1bd1ebe8fa6a45953369036e30d411ea92734..9344cbe2c88cac105507a960fd62cb3c3bd7a1ec 100644 --- a/src/hub.html +++ b/src/hub.html @@ -104,8 +104,8 @@ <template data-name="Chest"> <a-entity personal-space-invader="radius: 0.2; useMaterial: true;" bone-visibility> <a-entity billboard> - <a-entity mixin="rounded-text-button" block-button visible-while-frozen ui-class-while-frozen position="0 0 .35"> </a-entity> - <a-entity visible-while-frozen text="value:Block; width:2.5; align:center;" position="0 0 0.36"></a-entity> + <a-entity mixin="rounded-text-button" block-button visible-while-frozen="withinDistance: 3;" ui-class-while-frozen position="0 0 .35"> </a-entity> + <a-entity visible-while-frozen="withinDistance: 3;" text="value:Block; width:2.5; align:center;" position="0 0 0.36"></a-entity> </a-entity> </a-entity> </template> @@ -149,13 +149,13 @@ hoverable-visuals="cursorController: #cursor-controller" auto-scale-cannon-physics-body sticky-object="autoLockOnRelease: true; autoLockOnLoad: true;" - position-at-box-shape-border="target:.freeze-menu" + position-at-box-shape-border="target:.freeze-menu;" destroy-at-extreme-distances set-yxz-order pinnable > <a-entity class="interactable-ui" stop-event-propagation__grab-start="event: grab-start" stop-event-propagation__grab-end="event: grab-end"> - <a-entity class="freeze-menu" visible-while-frozen> + <a-entity class="freeze-menu" visible-while-frozen="withinDistance: 3;"> <a-entity mixin="rounded-text-action-button" pin-networked-object-button="labelSelector:.pin-button-label; hideWhenPinnedSelector:.hide-when-pinned; uiSelector:.interactable-ui" position="0 0.125 0.01"> </a-entity> <a-entity class="pin-button-label" text=" value:pin; width:1.75; align:center;" text-raycast-hack position="0 0.125 0.02"></a-entity> <a-entity mixin="rounded-text-button" class="hide-when-pinned" remove-networked-object-button position="0 -0.125 0.01"> </a-entity> @@ -185,7 +185,7 @@ segments-width="16" segments-height="12" ></a-sphere> - <a-entity class="delete-button" visible-while-frozen> + <a-entity class="delete-button" visible-while-frozen="withinDistance: 3;"> <a-entity mixin="rounded-text-button" remove-networked-object-button position="0 0 0"> </a-entity> <a-entity text=" value:remove; width:2.5; align:center;" text-raycast-hack position="0 0 0.01"></a-entity> </a-entity> @@ -207,7 +207,7 @@ set-yxz-order auto-scale-cannon-physics-body > - <a-entity class="delete-button" visible-while-frozen> + <a-entity class="delete-button" visible-while-frozen="withinDistance: 3;"> <a-entity mixin="rounded-text-button" remove-networked-object-button position="0 0 0"> </a-entity> <a-entity text=" value:remove; width:2.5; align:center;" text-raycast-hack position="0 0 0.01"></a-entity> </a-entity> diff --git a/src/react-components/scene-ui.js b/src/react-components/scene-ui.js index 168c6d517c68ee4f3520c2ea7fb770c45417eb1d..0a49bd07bd7b3504957dea05ff318e5a6052863f 100644 --- a/src/react-components/scene-ui.js +++ b/src/react-components/scene-ui.js @@ -81,11 +81,12 @@ class SceneUI extends Component { const toAttributionSpan = a => { if (a.url) { - const source = a.url.indexOf("sketchfab.com") - ? "on Sketchfab" - : a.url.indexOf("poly.google.com") - ? "on Google Poly" - : ""; + const source = + a.url.indexOf("sketchfab.com") >= 0 + ? "on Sketchfab" + : a.url.indexOf("poly.google.com") >= 0 + ? "on Google Poly" + : ""; return ( <span key={a.url}> @@ -107,7 +108,7 @@ class SceneUI extends Component { if (!this.props.sceneAttributions.extras) { attributions = ( <span> - <span>by {this.props.sceneAttributions.creator}</span> + <span>{this.props.sceneAttributions.creator ? `by ${this.props.sceneAttributions.creator}` : ""}</span> <br /> {this.props.sceneAttributions.content && this.props.sceneAttributions.content.map(toAttributionSpan)} </span> diff --git a/src/utils/pinned-entity-to-gltf.js b/src/utils/pinned-entity-to-gltf.js index c820dd786c5d5714c2faf008b49182c08bc63513..092f47d354a920adbff306c1f3ec82628b97b622 100644 --- a/src/utils/pinned-entity-to-gltf.js +++ b/src/utils/pinned-entity-to-gltf.js @@ -25,6 +25,11 @@ export default function pinnedEntityToGltf(el) { if (components["media-pager"]) { gltfComponents.media.pageIndex = components["media-pager"].data.index; } + + if (components["media-video"] && components["media-video"].data.videoPaused) { + gltfComponents.media.paused = true; + gltfComponents.media.time = components["media-video"].data.time; + } } gltfComponents.pinnable = { pinned: true };