diff --git a/src/assets/stylesheets/hub-create.scss b/src/assets/stylesheets/hub-create.scss index c2e3c94114f87d0663fb85baf1278e4c4625a13c..b9a69e4bc03dfdc4436426af55dcd0fa300cef70 100644 --- a/src/assets/stylesheets/hub-create.scss +++ b/src/assets/stylesheets/hub-create.scss @@ -143,15 +143,24 @@ top: 0; left: 0; background: rgb(2,0,36); - background: linear-gradient(0deg, rgba(2,0,36,0) 0%, rgba(1,0,11,0.2189076314119398) 60%, rgba(0,0,0,0.5242297602634804) 100%); + background: linear-gradient(0deg, rgba(2,0,36,0.324) 0%, rgba(1,0,11,0.1189076314119398) 60%, rgba(0,0,0,0.3242297602634804) 100%); - :local(.footer) { + :local(.customButton) { + @extend %default-font; + cursor: pointer; position: absolute; bottom: 14px; left: 12px; - font-size: 1.2em; + font-size: 1.3em; text-shadow: 0px 0px 6px #202020; color: $light-text; + appearance: none; + text-decoration: underline; + -moz-appearance: none; + -webkit-appearance: none; + background: transparent; + border: none; + pointer-events: auto; } :local(.header) { diff --git a/src/assets/stylesheets/info-dialog.scss b/src/assets/stylesheets/info-dialog.scss index 58b2259fc71f0097d8f2b032b1fcc9bfa5305251..05aec48c469e721d3c90406ee62606ba9535bd92 100644 --- a/src/assets/stylesheets/info-dialog.scss +++ b/src/assets/stylesheets/info-dialog.scss @@ -79,7 +79,7 @@ } } -.invite-form, .add-media-form { +.invite-form, .add-media-form, .custom-scene-form { display: flex; flex-direction: column; align-items: center; diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 49b6188286d236029a167b167bfea9f4b95f0810..0dfe136d19d4c1db8c78c4a591cf4c5130354c6f 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -45,7 +45,7 @@ "autoexit.title_units": " seconds", "autoexit.subtitle": "You have started another session.", "autoexit.cancel": "CANCEL", - "home.environment_picker_footer": "Choose a scene", + "home.room_create_options": "Options", "home.create_name.validation_warning": "Invalid name, limited to 4 to 64 characters and limited symbols.", "home.webvr_disclaimer_pre": "A ", "home.webvr_disclaimer_post": " experiment by ", diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js index 0b9755a4a8a449b2001a4d44180df73c3b9f7545..5e54e729e38c6d2360059e04f8fa4cb827d7b86f 100644 --- a/src/components/gltf-model-plus.js +++ b/src/components/gltf-model-plus.js @@ -80,7 +80,7 @@ function cloneGltf(gltf) { /// or templates associated with any of their nodes.) /// /// Returns the A-Frame entity associated with the given node, if one was constructed. -const inflateEntities = function(node, templates, gltfPath) { +const inflateEntities = function(node, templates, gltfPath, isRoot) { // inflate subtrees first so that we can determine whether or not this node needs to be inflated const childEntities = []; const children = node.children.slice(0); // setObject3D mutates the node's parent, so we have to copy @@ -92,7 +92,7 @@ const inflateEntities = function(node, templates, gltfPath) { } const nodeHasBehavior = node.userData.components || node.name in templates; - if (!nodeHasBehavior && !childEntities.length) { + if (!nodeHasBehavior && !childEntities.length && !isRoot) { return null; // we don't need an entity for this node } @@ -257,7 +257,7 @@ AFRAME.registerComponent("gltf-model-plus", { this.model.animations = model.animations; let object3DToSet = this.model; - if (this.data.inflate && (this.inflatedEl = inflateEntities(this.model, this.templates, gltfPath))) { + if (this.data.inflate && (this.inflatedEl = inflateEntities(this.model, this.templates, gltfPath, true))) { this.el.appendChild(this.inflatedEl); object3DToSet = this.inflatedEl.object3D; // TODO: Still don't fully understand the lifecycle here and how it differs between browsers, we should dig in more diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js index 0552e61c854ad7efe6ed5e97020b512ed5d0feac..cc22ef521f2ee74281f2de9e636d867c21d89b1d 100644 --- a/src/gltf-component-mappings.js +++ b/src/gltf-component-mappings.js @@ -11,6 +11,9 @@ AFRAME.GLTFModelPlus.registerComponent("gltf-model-plus", "gltf-model-plus"); AFRAME.GLTFModelPlus.registerComponent("body", "body"); AFRAME.GLTFModelPlus.registerComponent("hide-when-quality", "hide-when-quality"); AFRAME.GLTFModelPlus.registerComponent("light", "light"); +AFRAME.GLTFModelPlus.registerComponent("directional-light", "light"); +AFRAME.GLTFModelPlus.registerComponent("ambient-light", "light"); +AFRAME.GLTFModelPlus.registerComponent("point-light", "light"); AFRAME.GLTFModelPlus.registerComponent("skybox", "skybox"); AFRAME.GLTFModelPlus.registerComponent("layers", "layers"); AFRAME.GLTFModelPlus.registerComponent("shadow", "shadow"); diff --git a/src/hub.js b/src/hub.js index 94a423315a65ad7424ecb8ab77bb69735ac410fa..38b0019d53a7254c2f6d65e59dc2f8cc57f0b7ed 100644 --- a/src/hub.js +++ b/src/hub.js @@ -510,9 +510,21 @@ const onReady = async () => { .receive("ok", data => { const hub = data.hubs[0]; const defaultSpaceTopic = hub.topics[0]; - const gltfBundleUrl = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle").src; + const sceneUrl = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle").src; + + console.log(`Scene URL: ${sceneUrl}`); + + if (/\.gltf/i.test(sceneUrl) || /\.glb/i.test(sceneUrl)) { + const gltfEl = document.createElement("a-entity"); + gltfEl.setAttribute("gltf-model-plus", { src: sceneUrl, inflate: true }); + gltfEl.addEventListener("model-loaded", () => initialEnvironmentEl.emit("bundleloaded")); + initialEnvironmentEl.appendChild(gltfEl); + } else { + // TODO remove, and remove bundleloaded event + initialEnvironmentEl.setAttribute("gltf-bundle", `src: ${sceneUrl}`); + } + setRoom(hub.hub_id, hub.name); - initialEnvironmentEl.setAttribute("gltf-bundle", `src: ${gltfBundleUrl}`); hubChannel.setPhoenixChannel(channel); }) .receive("error", res => { diff --git a/src/react-components/hub-create-panel.js b/src/react-components/hub-create-panel.js index 9a107a54983fba8e44a71931cbda068e72a74ba9..8d83218341e07218aafb5126cf0cc75e4fe829ec 100644 --- a/src/react-components/hub-create-panel.js +++ b/src/react-components/hub-create-panel.js @@ -2,16 +2,17 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import { injectIntl, FormattedMessage } from "react-intl"; import { generateHubName } from "../utils/name-generation"; -import classNames from "classnames"; import { faAngleLeft } from "@fortawesome/free-solid-svg-icons/faAngleLeft"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { resolveURL, extractUrlBase } from "../utils/resolveURL"; +import InfoDialog from "./info-dialog.js"; import default_scene_preview_thumbnail from "../assets/images/default_thumbnail.png"; import styles from "../assets/stylesheets/hub-create.scss"; const HUB_NAME_PATTERN = "^[A-Za-z0-9-'\":!@#$%^&*(),.?~ ]{4,64}$"; +const dialogTypes = InfoDialog.dialogTypes; class HubCreatePanel extends Component { static propTypes = { @@ -34,7 +35,9 @@ class HubCreatePanel extends Component { this.state = { ready: false, name: generateHubName(), - environmentIndex + environmentIndex, + showCustomSceneDialog: false, + customSceneUrl: null }; // Optimisticly preload all environment thumbnails @@ -71,11 +74,15 @@ class HubCreatePanel extends Component { }; createHub = async e => { - e.preventDefault(); + if (e) { + e.preventDefault(); + } + const environment = this.props.environments[this.state.environmentIndex]; + const sceneUrl = this.state.customSceneUrl || environment.bundle_url; const payload = { - hub: { name: this.state.name, default_environment_gltf_bundle_url: environment.bundle_url } + hub: { name: this.state.name, default_environment_gltf_bundle_url: sceneUrl } }; let createUrl = "/api/v1/hubs"; @@ -129,6 +136,12 @@ class HubCreatePanel extends Component { this.setToEnvironmentOffset(-1); }; + showCustomSceneDialog = e => { + e.preventDefault(); + e.stopPropagation(); + this.setState({ showCustomSceneDialog: true }); + }; + shuffle = () => { this.setState({ name: generateHubName(), @@ -152,93 +165,106 @@ class HubCreatePanel extends Component { const environmentThumbnail = this._getEnvironmentThumbnail(this.state.environmentIndex); return ( - <form onSubmit={this.createHub}> - <div className={styles.createPanel}> - <div className={styles.form}> - <div - className={styles.leftContainer} - onClick={async () => { - this.shuffle(); - }} - > - <button type="button" tabIndex="3" className={styles.rotateButton}> - <img src="../assets/images/dice_icon.svg" /> - </button> - </div> - <div className={styles.rightContainer}> - <button type="submit" tabIndex="5" className={styles.submitButton}> - {this.isHubNameValid() ? ( - <img src="../assets/images/hub_create_button_enabled.svg" /> - ) : ( - <img src="../assets/images/hub_create_button_disabled.svg" /> - )} - </button> - </div> - <div className={styles.environment}> - <div className={styles.picker}> - <img className={styles.image} srcSet={environmentThumbnail.srcset} /> - <div className={styles.labels}> - <div className={styles.header}> - {meta.url ? ( - <a href={meta.url} rel="noopener noreferrer" className={styles.title}> - {environmentTitle} - </a> - ) : ( - <span className={styles.itle}>environmentTitle</span> - )} - {environmentAuthor && - environmentAuthor.name && - (environmentAuthor.url ? ( - <a href={environmentAuthor.url} rel="noopener noreferrer" className={styles.author}> - <FormattedMessage id="home.environment_author_by" /> - <span>{environmentAuthor.name}</span> - </a> - ) : ( - <span className={styles.author}> - <FormattedMessage id="home.environment_author_by" /> - <span>{environmentAuthor.name}</span> - </span> - ))} - {environmentAuthor && - environmentAuthor.organization && - (environmentAuthor.organization.url ? ( - <a href={environmentAuthor.organization.url} rel="noopener noreferrer" className={styles.org}> - <span>{environmentAuthor.organization.name}</span> + <div> + <form onSubmit={this.createHub}> + <div className={styles.createPanel}> + <div className={styles.form}> + <div + className={styles.leftContainer} + onClick={async () => { + this.shuffle(); + }} + > + <button type="button" tabIndex="3" className={styles.rotateButton}> + <img src="../assets/images/dice_icon.svg" /> + </button> + </div> + <div className={styles.rightContainer}> + <button type="submit" tabIndex="5" className={styles.submitButton}> + {this.isHubNameValid() ? ( + <img src="../assets/images/hub_create_button_enabled.svg" /> + ) : ( + <img src="../assets/images/hub_create_button_disabled.svg" /> + )} + </button> + </div> + <div className={styles.environment}> + <div className={styles.picker}> + <img className={styles.image} srcSet={environmentThumbnail.srcset} /> + <div className={styles.labels}> + <div className={styles.header}> + {meta.url ? ( + <a href={meta.url} rel="noopener noreferrer" className={styles.title}> + {environmentTitle} </a> ) : ( - <span className={styles.org}> - <span>{environmentAuthor.organization.name}</span> - </span> - ))} + <span className={styles.itle}>environmentTitle</span> + )} + {environmentAuthor && + environmentAuthor.name && + (environmentAuthor.url ? ( + <a href={environmentAuthor.url} rel="noopener noreferrer" className={styles.author}> + <FormattedMessage id="home.environment_author_by" /> + <span>{environmentAuthor.name}</span> + </a> + ) : ( + <span className={styles.author}> + <FormattedMessage id="home.environment_author_by" /> + <span>{environmentAuthor.name}</span> + </span> + ))} + {environmentAuthor && + environmentAuthor.organization && + (environmentAuthor.organization.url ? ( + <a href={environmentAuthor.organization.url} rel="noopener noreferrer" className={styles.org}> + <span>{environmentAuthor.organization.name}</span> + </a> + ) : ( + <span className={styles.org}> + <span>{environmentAuthor.organization.name}</span> + </span> + ))} + </div> + <div className={styles.footer}> + <button onClick={this.showCustomSceneDialog} className={styles.customButton}> + <FormattedMessage id="home.room_create_options" /> + </button> + </div> </div> - <div className={styles.footer}> - <FormattedMessage id="home.environment_picker_footer" /> + <div className={styles.controls}> + <button className={styles.prev} type="button" tabIndex="1" onClick={this.setToPreviousEnvironment}> + <FontAwesomeIcon icon={faAngleLeft} /> + </button> + + <button className={styles.next} type="button" tabIndex="2" onClick={this.setToNextEnvironment}> + <FontAwesomeIcon icon={faAngleRight} /> + </button> </div> </div> - <div className={styles.controls}> - <button className={styles.prev} type="button" tabIndex="1" onClick={this.setToPreviousEnvironment}> - <FontAwesomeIcon icon={faAngleLeft} /> - </button> - - <button className={styles.next} type="button" tabIndex="2" onClick={this.setToNextEnvironment}> - <FontAwesomeIcon icon={faAngleRight} /> - </button> - </div> </div> + <input + tabIndex="4" + className={styles.name} + value={this.state.name} + onChange={e => this.setState({ name: e.target.value })} + onFocus={e => e.target.select()} + required + pattern={HUB_NAME_PATTERN} + title={formatMessage({ id: "home.create_name.validation_warning" })} + /> </div> - <input - tabIndex="4" - className={styles.name} - value={this.state.name} - onChange={e => this.setState({ name: e.target.value })} - onFocus={e => e.target.select()} - required - pattern={HUB_NAME_PATTERN} - title={formatMessage({ id: "home.create_name.validation_warning" })} - /> </div> - </div> - </form> + </form> + {this.state.showCustomSceneDialog && ( + <InfoDialog + dialogType={dialogTypes.custom_scene} + onCloseDialog={() => this.setState({ showCustomSceneDialog: false })} + onCustomScene={url => { + this.setState({ showCustomSceneDialog: false, customSceneUrl: url }, () => this.createHub()); + }} + /> + )} + </div> ); } } diff --git a/src/react-components/info-dialog.js b/src/react-components/info-dialog.js index ff9b4da1b86f19c864bcab89b2eb997fc2eb6a1f..2111626fb344714eb7ddc7599f2d05bff3bf73a0 100644 --- a/src/react-components/info-dialog.js +++ b/src/react-components/info-dialog.js @@ -20,13 +20,15 @@ class InfoDialog extends Component { help: Symbol("help"), link: Symbol("link"), webvr_recommend: Symbol("webvr_recommend"), - add_media: Symbol("add_media") + add_media: Symbol("add_media"), + custom_scene: Symbol("custom_scene") }; static propTypes = { dialogType: PropTypes.oneOf(Object.values(InfoDialog.dialogTypes)), onCloseDialog: PropTypes.func, onSubmittedEmail: PropTypes.func, onAddMedia: PropTypes.func, + onCustomScene: PropTypes.func, linkCode: PropTypes.string }; @@ -53,11 +55,16 @@ class InfoDialog extends Component { } } - onContainerClicked(e) { + onContainerClicked = e => { if (e.currentTarget === e.target) { this.props.onCloseDialog(); } - } + }; + + onCustomSceneClicked = () => { + this.props.onCustomScene(this.state.customSceneUrl); + this.props.onCloseDialog(); + }; shareLinkClicked = () => { navigator.share({ @@ -74,7 +81,9 @@ class InfoDialog extends Component { state = { mailingListEmail: "", mailingListPrivacy: false, - copyLinkButtonText: "Copy" + copyLinkButtonText: "Copy", + addMediaUrl: "", + customSceneUrl: "" }; signUpForMailingList = async e => { @@ -191,6 +200,31 @@ class InfoDialog extends Component { dialogTitle = "Add Media"; dialogBody = <MediaToolsDialog onAddMedia={this.props.onAddMedia} onCloseDialog={this.props.onCloseDialog} />; break; + case InfoDialog.dialogTypes.custom_scene: + dialogTitle = "Use Custom Scene"; + dialogBody = ( + <div> + <div>Enter a URL to a GLTF file to use for your room's scene:</div> + <form onSubmit={this.onCustomSceneClicked}> + <div className="custom-scene-form"> + <input + type="url" + placeholder="URL to Scene GLTF or GLB" + className="custom-scene-form__link_field" + value={this.state.customSceneUrl} + onChange={e => this.setState({ customSceneUrl: e.target.value })} + required + /> + <div className="custom-scene-form__buttons"> + <button className="custom-scene-form__action-button"> + <span>Create Room</span> + </button> + </div> + </div> + </form> + </div> + ); + break; case InfoDialog.dialogTypes.updates: dialogTitle = ""; dialogBody = (