diff --git a/src/assets/hud/spawn_camera-hover.png b/src/assets/hud/spawn_camera-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..2352beaab00ce7ffccae97ac68be17d93f2e05db Binary files /dev/null and b/src/assets/hud/spawn_camera-hover.png differ diff --git a/src/assets/hud/spawn_camera.png b/src/assets/hud/spawn_camera.png new file mode 100644 index 0000000000000000000000000000000000000000..09b21ac68f1050223aaa224d1dea9cb647229ef6 Binary files /dev/null and b/src/assets/hud/spawn_camera.png differ diff --git a/src/assets/stylesheets/2d-hud.scss b/src/assets/stylesheets/2d-hud.scss index f8b56e42545b757b425135a2e7fb1a21abf29a62..b1cf61cf12a28b35bb1720d933c9b50f59d01988 100644 --- a/src/assets/stylesheets/2d-hud.scss +++ b/src/assets/stylesheets/2d-hud.scss @@ -22,6 +22,7 @@ &:local(.column) { flex-direction: column; bottom: 20px; + z-index: 1; } } @@ -120,6 +121,13 @@ background-image: url(../hud/spawn_pen-hover.png); } +:local(.iconButton.spawn_camera) { + background-image: url(../hud/spawn_camera.png); +} +:local(.iconButton.spawn_camera:hover) { + background-image: url(../hud/spawn_camera-hover.png); +} + :local(.iconButton.freeze) { background-image: url(../hud/freeze_off.png); } diff --git a/src/assets/stylesheets/ui-root.scss b/src/assets/stylesheets/ui-root.scss index e7234072461334f7e9edb973eb315324312d98a9..90f6e245efa030784d4a4733fa0010960c0a3c67 100644 --- a/src/assets/stylesheets/ui-root.scss +++ b/src/assets/stylesheets/ui-root.scss @@ -70,6 +70,10 @@ .ui-interactive { pointer-events: auto; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; } :local(.nag-button) { diff --git a/src/components/camera-tool.js b/src/components/camera-tool.js new file mode 100644 index 0000000000000000000000000000000000000000..2501dddc04dfcbf0c81e2d1c3426a40f6e939bb6 --- /dev/null +++ b/src/components/camera-tool.js @@ -0,0 +1,141 @@ +import { addMedia } from "../utils/media-utils"; +import { ObjectTypes } from "../object-types"; + +const snapCanvas = document.createElement("canvas"); +async function pixelsToPNG(pixels, width, height) { + snapCanvas.width = width; + snapCanvas.height = height; + const context = snapCanvas.getContext("2d"); + + const imageData = context.createImageData(width, height); + imageData.data.set(pixels); + const bitmap = await createImageBitmap(imageData); + context.scale(1, -1); + context.drawImage(bitmap, 0, -height); + const blob = await new Promise(resolve => snapCanvas.toBlob(resolve)); + return new File([blob], "snap.png", { type: "image/png" }); +} + +AFRAME.registerComponent("camera-tool", { + schema: { + previewFPS: { default: 6 }, + imageWidth: { default: 512 }, + imageHeight: { default: 512 } + }, + + init() { + this.stateAdded = this.stateAdded.bind(this); + + this.lastUpdate = performance.now(); + + this.renderTarget = new THREE.WebGLRenderTarget(this.data.imageWidth, this.data.imageHeight, { + format: THREE.RGBAFormat, + minFilter: THREE.LinearFilter, + magFilter: THREE.NearestFilter, + encoding: THREE.sRGBEncoding, + depth: false, + stencil: false + }); + + this.camera = new THREE.PerspectiveCamera(); + this.camera.rotation.set(0, Math.PI, 0); + this.el.setObject3D("camera", this.camera); + + const material = new THREE.MeshBasicMaterial({ + map: this.renderTarget.texture + }); + // Bit of a hack here to only update the renderTarget when the screens are in view and at a reduced FPS + material.map.isVideoTexture = true; + material.map.update = () => { + if (performance.now() - this.lastUpdate >= 1000 / this.data.previewFPS) { + this.updateRenderTargetNextTick = true; + } + }; + + this.el.addEventListener( + "model-loaded", + () => { + const geometry = new THREE.PlaneGeometry(0.25, 0.25); + + const screen = new THREE.Mesh(geometry, material); + screen.rotation.set(0, Math.PI, 0); + screen.position.set(0, -0.015, -0.08); + this.el.setObject3D("screen", screen); + + const selfieScreen = new THREE.Mesh(geometry, material); + selfieScreen.position.set(0, 0.3, 0); + selfieScreen.scale.set(-1, 1, 1); + this.el.setObject3D("selfieScreen", selfieScreen); + + this.updateRenderTargetNextTick = true; + }, + { once: true } + ); + }, + + play() { + this.el.addEventListener("stateadded", this.stateAdded); + }, + + pause() { + this.el.removeEventListener("stateadded", this.stateAdded); + }, + + stateAdded(evt) { + if (evt.detail === "activated") { + this.takeSnapshotNextTick = true; + } + }, + + tock: (function() { + const tempScale = new THREE.Vector3(); + return function tock() { + const sceneEl = this.el.sceneEl; + const renderer = this.renderer || sceneEl.renderer; + const now = performance.now(); + + if (!this.playerHead) { + const headEl = document.getElementById("player-head"); + this.playerHead = headEl && headEl.object3D; + } + + if (this.takeSnapshotNextTick || this.updateRenderTargetNextTick) { + if (this.playerHead) { + tempScale.copy(this.playerHead.scale); + this.playerHead.scale.set(1, 1, 1); + } + const tmpVRFlag = renderer.vr.enabled; + const tmpOnAfterRender = sceneEl.object3D.onAfterRender; + delete sceneEl.object3D.onAfterRender; + renderer.vr.enabled = false; + renderer.render(sceneEl.object3D, this.camera, this.renderTarget, true); + renderer.vr.enabled = tmpVRFlag; + sceneEl.object3D.onAfterRender = tmpOnAfterRender; + if (this.playerHead) { + this.playerHead.scale.copy(tempScale); + } + this.lastUpdate = now; + this.updateRenderTargetNextTick = false; + } + + if (this.takeSnapshotNextTick) { + const width = this.renderTarget.width; + const height = this.renderTarget.height; + if (!this.snapPixels) { + this.snapPixels = new Uint8Array(width * height * 4); + } + renderer.readRenderTargetPixels(this.renderTarget, 0, 0, width, height, this.snapPixels); + pixelsToPNG(this.snapPixels, width, height).then(file => { + const { entity, orientation } = addMedia(file, "#interactable-media", undefined, true); + orientation.then(() => { + entity.object3D.position.copy(this.el.object3D.position); + entity.object3D.rotation.copy(this.el.object3D.rotation); + entity.components["sticky-object"].setLocked(false); + sceneEl.emit("object_spawned", { objectType: ObjectTypes.CAMERA }); + }); + }); + this.takeSnapshotNextTick = false; + } + }; + })() +}); diff --git a/src/components/character-controller.js b/src/components/character-controller.js index 6edfd332a2fdc707d3d5a484b468218ff3b55000..dba2bc4dd66a7595dbef20ca85a8ead162f35bcf 100644 --- a/src/components/character-controller.js +++ b/src/components/character-controller.js @@ -1,6 +1,7 @@ const CLAMP_VELOCITY = 0.01; const MAX_DELTA = 0.2; const EPS = 10e-6; +const MAX_WARNINGS = 10; /** * Avatar movement controller that listens to move, rotate and teleportation events and moves the avatar accordingly. @@ -25,6 +26,8 @@ AFRAME.registerComponent("character-controller", { this.accelerationInput = new THREE.Vector3(0, 0, 0); this.pendingSnapRotationMatrix = new THREE.Matrix4(); this.angularVelocity = 0; // Scalar value because we only allow rotation around Y + this._withinWarningLimit = true; + this._warningCount = 0; this.setAccelerationInput = this.setAccelerationInput.bind(this); this.snapRotateLeft = this.snapRotateLeft.bind(this); this.snapRotateRight = this.snapRotateRight.bind(this); @@ -158,6 +161,16 @@ AFRAME.registerComponent("character-controller", { }; })(), + _warnWithWarningLimit: function(msg) { + if (!this._withinWarningLimit) return; + this._warningCount++; + if (this._warningCount > MAX_WARNINGS) { + this._withinWarningLimit = false; + msg = "Warning count exceeded. Will not log further warnings"; + } + console.warn("character-controller", msg); + }, + setPositionOnNavMesh: function(start, end, object3D) { const pathfinder = this.el.sceneEl.systems.nav.pathfinder; const zone = this.navZone; @@ -166,7 +179,16 @@ AFRAME.registerComponent("character-controller", { this.navGroup = pathfinder.getGroup(zone, end); } this.navNode = this.navNode || pathfinder.getClosestNode(end, zone, this.navGroup, true); - this.navNode = pathfinder.clampStep(start, end, this.navNode, zone, this.navGroup, object3D.position); + if (this.navNode) { + try { + this.navNode = pathfinder.clampStep(start, end, this.navNode, zone, this.navGroup, object3D.position); + } catch (e) { + // clampStep failed for whatever reason. Don't stop the main loop. + if (this._withinWarningLimit) this._warnWithWarningLimit(`setPositionOnNavMesh: clampStep failed. ${e}`); + } + } else { + if (this._withinWarningLimit) this._warnWithWarningLimit("setPositionOnNavMesh: navNode is null."); + } } }, @@ -176,7 +198,16 @@ AFRAME.registerComponent("character-controller", { if (zone in pathfinder.zones) { this.navGroup = pathfinder.getGroup(zone, position); this.navNode = pathfinder.getClosestNode(navPosition, zone, this.navGroup, true) || this.navNode; - this.navNode = pathfinder.clampStep(position, position, this.navNode, zone, this.navGroup, object3D.position); + if (this.navNode) { + try { + this.navNode = pathfinder.clampStep(position, position, this.navNode, zone, this.navGroup, object3D.position); + } catch (e) { + // clampStep failed for whatever reason. Don't stop the main loop. + if (this._withinWarningLimit) this._warnWithWarningLimit(`resetPositionOnNavMesh: clampStep failed. ${e}`); + } + } else { + if (this._withinWarningLimit) this._warnWithWarningLimit("resetPositionOnNavMesh: navNode is null."); + } } }, diff --git a/src/components/heightfield.js b/src/components/heightfield.js index b1ed8b46bee36c243544476a7f59fb33b575a5fb..57853a5ccb54ac0060212264e82e21b62a1ffbfe 100644 --- a/src/components/heightfield.js +++ b/src/components/heightfield.js @@ -9,59 +9,19 @@ AFRAME.registerComponent("heightfield", { this.el.setAttribute("static-body", { shape: "none", mass: 0 }); }, generateAndAddHeightfield(body) { - const mesh = this.el.object3D.getObjectByProperty("type", "Mesh"); - mesh.geometry.computeBoundingBox(); - const size = new THREE.Vector3(); - mesh.geometry.boundingBox.getSize(size); - - const minDistance = 0.25; - const resolution = (size.x + size.z) / 2 / minDistance; - const distance = Math.max(minDistance, (size.x + size.z) / 2 / resolution); - - const data = []; - const down = new THREE.Vector3(0, -1, 0); - const position = new THREE.Vector3(); - const raycaster = new THREE.Raycaster(); - const intersections = []; - const meshPos = new THREE.Vector3(); - mesh.getWorldPosition(meshPos); - const offsetX = -size.x / 2 + meshPos.x; - const offsetZ = -size.z / 2 + meshPos.z; - let min = Infinity; - for (let z = 0; z < resolution; z++) { - data[z] = []; - for (let x = 0; x < resolution; x++) { - position.set(offsetX + x * distance, size.y / 2, offsetZ + z * distance); - raycaster.set(position, down); - intersections.length = 0; - raycaster.intersectObject(mesh, false, intersections); - let val; - if (intersections.length) { - val = -intersections[0].distance + size.y / 2; - } else { - val = -size.y / 2; - } - data[z][x] = val; - if (val < min) { - min = data[z][x]; - } - } - } - // Cannon doesn't like heightfields with negative heights. - for (let z = 0; z < resolution; z++) { - for (let x = 0; x < resolution; x++) { - data[z][x] -= min; - } - } + const { offset, distance, data } = this.data; const orientation = new CANNON.Quaternion(); orientation.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2); + const rotation = new CANNON.Quaternion(); rotation.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), -Math.PI / 2); rotation.mult(orientation, orientation); - const offset = new CANNON.Vec3(-size.x / 2, min, -size.z / 2); + + const cannonOffset = new CANNON.Vec3(offset.x, offset.y, offset.z); const shape = new CANNON.Heightfield(data, { elementSize: distance }); - body.addShape(shape, offset, orientation); + + body.addShape(shape, cannonOffset, orientation); } }); diff --git a/src/components/in-world-hud.js b/src/components/in-world-hud.js index 1bee398de9521f9a12e7fd0eb12b2af035a89041..aa0d8f83f1bf2e57a6a884b16c154b41d22a0bb2 100644 --- a/src/components/in-world-hud.js +++ b/src/components/in-world-hud.js @@ -12,11 +12,13 @@ AFRAME.registerComponent("in-world-hud", { this.mic = this.el.querySelector(".mic"); this.freeze = this.el.querySelector(".freeze"); this.pen = this.el.querySelector(".pen"); + this.cameraBtn = this.el.querySelector(".cameraBtn"); this.background = this.el.querySelector(".bg"); const renderOrder = window.APP.RENDER_ORDER; this.mic.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS; this.freeze.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS; this.pen.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS; + this.cameraBtn.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS; this.background.object3DMap.mesh.renderORder = renderOrder.HUD_BACKGROUND; this.updateButtonStates = () => { @@ -42,6 +44,10 @@ AFRAME.registerComponent("in-world-hud", { this.onPenClick = () => { this.el.emit("spawn_pen"); }; + + this.onCameraClick = () => { + this.el.emit("action_spawn_camera"); + }; }, play() { @@ -51,6 +57,7 @@ AFRAME.registerComponent("in-world-hud", { this.mic.addEventListener("click", this.onMicClick); this.freeze.addEventListener("click", this.onFreezeClick); this.pen.addEventListener("mousedown", this.onPenClick); + this.cameraBtn.addEventListener("click", this.onCameraClick); }, pause() { @@ -60,5 +67,6 @@ AFRAME.registerComponent("in-world-hud", { this.mic.removeEventListener("click", this.onMicClick); this.freeze.removeEventListener("click", this.onFreezeClick); this.pen.removeEventListener("mousedown", this.onPenClick); + this.cameraBtn.removeEventListener("click", this.onCameraClick); } }); diff --git a/src/hub.html b/src/hub.html index 787984a1889fecaf5f2aaa53ad3efe53c539e8c4..edb18d4b95c430debb86f6e6ae11948b0db73b52 100644 --- a/src/hub.html +++ b/src/hub.html @@ -58,6 +58,8 @@ <img id="freeze-on-hover" crossorigin="anonymous" src="./assets/hud/freeze_on-hover.png"> <img id="spawn-pen" crossorigin="anonymous" src="./assets/hud/spawn_pen.png"> <img id="spawn-pen-hover" crossorigin="anonymous" src="./assets/hud/spawn_pen-hover.png"> + <img id="spawn-camera" crossorigin="anonymous" src="./assets/hud/spawn_camera.png"> + <img id="spawn-camera-hover" crossorigin="anonymous" src="./assets/hud/spawn_camera-hover.png"> <a-asset-item id="botdefault" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotDefault_Avatar-9f71f8ff22.gltf"></a-asset-item> <a-asset-item id="botbobo" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotBobo_Avatar-f9740a010b.gltf"></a-asset-item> @@ -201,6 +203,27 @@ </a-entity> </template> + <template id="interactable-camera"> + <a-entity + class="interactable toggle" + grabbable-toggle="maxGrabbers: 1;" + hoverable + activatable__snap-hand="buttonStartEvents: secondary_hand_grab; buttonEndEvents: secondary_hand_release;" + activatable__snap-cursor="buttonStartEvents: secondary-cursor-grab; buttonEndEvents: secondary-cursor-release;" + camera-tool + body="type: dynamic; shape: none; mass: 1;" + sticky-object="autoLockOnRelease: true; autoLockOnLoad: true;" + media-loader="src: https://sketchfab.com/models/2e8778db90e845888e38edd80be28283; resize: true" + super-networked-interactable="counter: #camera-counter;" + position-at-box-shape-border="target:.delete-button" + rotation + > + <a-entity class="delete-button" visible-while-frozen> + <a-entity mixin="rounded-text-button" remove-networked-object-button position="0 0 0"> </a-entity> + <a-entity text=" value:Delete; width:2.5; align:center;" text-raycast-hack position="0 0 0.01"></a-entity> + </a-entity> + </a-entity> + </template> <template id="interactable-drawing"> <a-entity @@ -264,6 +287,8 @@ <a-entity id="pen-counter" networked-counter="max: 10;"></a-entity> + <a-entity id="camera-counter" networked-counter="max: 1;"></a-entity> + <a-entity id="drawing-manager" drawing-manager></a-entity> <a-entity @@ -315,10 +340,11 @@ vr-mode-toggle-playing__hud-controller > <a-entity in-world-hud="haptic:#player-right-controller;raycaster:#player-right-controller;" rotation="30 0 0"> - <a-rounded height="0.13" width="0.48" color="#000000" position="-0.24 -0.065 0" radius="0.065" opacity="0.35" class="hud bg"></a-rounded> + <a-rounded height="0.13" width="0.59" color="#000000" position="-0.24 -0.065 0" radius="0.065" opacity="0.35" class="hud bg"></a-rounded> <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Mute Mic; activeTooltipText: Unmute Mic; image: #mute-off; hoverImage: #mute-off-hover; activeImage: #mute-on; activeHoverImage: #mute-on-hover" scale="0.1 0.1 0.1" position="-0.17 0 0.001" class="ui hud mic" material="alphaTest:0.1;"></a-image> <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Pause; activeTooltipText: Resume; image: #freeze-off; hoverImage: #freeze-off-hover; activeImage: #freeze-on; activeHoverImage: #freeze-on-hover" scale="0.2 0.2 0.2" position="0 0 0.005" class="ui hud freeze"></a-image> <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Spawn Pen; activeTooltipText: Spawn Pen; image: #spawn-pen; hoverImage: #spawn-pen-hover; activeImage: #spawn-pen; activeHoverImage: #spawn-pen-hover" scale="0.1 0.1 0.1" position="0.17 0 0.001" class="ui hud pen" material="alphaTest:0.1;"></a-image> + <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Spawn Camera; activeTooltipText: Spawn Camera; image: #spawn-camera; hoverImage: #spawn-camera-hover; activeImage: #spawn-camera; activeHoverImage: #spawn-camera-hover" scale="0.1 0.1 0.1" position="0.28 0 0.001" class="ui hud cameraBtn" material="alphaTest:0.1;"></a-image> <a-rounded visible="false" id="hud-tooltip" height="0.08" width="0.3" color="#000000" position="-0.15 -0.2 0" rotation="-20 0 0" radius="0.025" opacity="0.35" class="hud bg"> <a-entity text="value: Mute Mic; align:center;" position="0.15 0.04 0.001" ></a-entity> </a-rounded> @@ -414,7 +440,7 @@ </template> <template data-name="Head"> - <a-entity visible="false" bone-visibility></a-entity> + <a-entity id="player-head" visible="false" bone-visibility></a-entity> </template> <template data-name="LeftHand"> @@ -443,6 +469,7 @@ superHand: #player-right-controller; cursorSuperHand: #cursor;" ></a-entity> + </a-scene> <div id="ui-root"></div> diff --git a/src/hub.js b/src/hub.js index 7e22912fe9bc1fc2c9a1d8f2ef44b7c992a3f581..ce0e5dca5e8486bdc172e72562fda1367d3f36ac 100644 --- a/src/hub.js +++ b/src/hub.js @@ -85,6 +85,7 @@ import "./components/hemisphere-light"; import "./components/point-light"; import "./components/spot-light"; import "./components/visible-to-owner"; +import "./components/camera-tool"; import ReactDOM from "react-dom"; import React from "react"; @@ -344,7 +345,6 @@ const onReady = async () => { }); const offset = { x: 0, y: 0, z: -1.5 }; - const spawnMediaInfrontOfPlayer = (src, contentOrigin) => { const { entity, orientation } = addMedia(src, "#interactable-media", contentOrigin, true); @@ -363,6 +363,16 @@ const onReady = async () => { spawnMediaInfrontOfPlayer(e.detail, contentOrigin); }); + scene.addEventListener("action_spawn_camera", () => { + const entity = document.createElement("a-entity"); + entity.setAttribute("networked", { template: "#interactable-camera" }); + entity.setAttribute("offset-relative-to", { + target: "#player-camera", + offset: { x: 0, y: 0, z: -1.5 } + }); + scene.appendChild(entity); + }); + scene.addEventListener("object_spawned", e => { if (hubChannel) { hubChannel.sendObjectSpawnedEvent(e.detail.objectType); diff --git a/src/network-schemas.js b/src/network-schemas.js index 8cef8df1653e06a89e25b5ecf046748606a7eeb2..3670af7be79393c83ef0d3d76fdc775fa5bc3a73 100644 --- a/src/network-schemas.js +++ b/src/network-schemas.js @@ -127,6 +127,11 @@ function registerNetworkSchemas() { ] }); + NAF.schemas.add({ + template: "#interactable-camera", + components: ["position", "rotation"] + }); + NAF.schemas.add({ template: "#pen-interactable", components: [ diff --git a/src/object-types.js b/src/object-types.js index d8f959c2531a59fe713c85b2141a013eed785be5..8c9ecc581b7485f4ad24187986dfd885a5c5f8a3 100644 --- a/src/object-types.js +++ b/src/object-types.js @@ -12,7 +12,7 @@ export const ObjectContentOrigins = { // Enumeration of spawnable object types, used for telemetry, which encapsulates // both the origin of the content for the object and also the type of content // contained in the object. -const ObjectTypes = { +export const ObjectTypes = { URL_IMAGE: 0, URL_VIDEO: 1, URL_MODEL: 2, @@ -38,7 +38,7 @@ const ObjectTypes = { SPAWNER_PDF: 27, SPAWNER_AUDIO: 28, //SPAWNER_TEXT: 29, - //DRAWING: 30, + CAMERA: 30, UNKNOWN: 31 }; diff --git a/src/react-components/2d-hud.js b/src/react-components/2d-hud.js index 052817a81d6aac00a1b2881df7eb73a5268fc79c..6c31677f58655bec2bb4ef364bd83a078f9e3fc2 100644 --- a/src/react-components/2d-hud.js +++ b/src/react-components/2d-hud.js @@ -4,7 +4,7 @@ import cx from "classnames"; import styles from "../assets/stylesheets/2d-hud.scss"; -const TopHUD = ({ muted, frozen, onToggleMute, onToggleFreeze, onSpawnPen }) => ( +const TopHUD = ({ muted, frozen, onToggleMute, onToggleFreeze, onSpawnPen, onSpawnCamera }) => ( <div className={cx(styles.container, styles.top, styles.unselectable)}> <div className={cx("ui-interactive", styles.panel, styles.left)}> <div @@ -20,6 +20,7 @@ const TopHUD = ({ muted, frozen, onToggleMute, onToggleFreeze, onSpawnPen }) => /> <div className={cx("ui-interactive", styles.panel, styles.right)}> <div className={cx(styles.iconButton, styles.spawn_pen)} title={"Drawing Pen"} onClick={onSpawnPen} /> + <div className={cx(styles.iconButton, styles.spawn_camera)} title={"Camera"} onClick={onSpawnCamera} /> </div> </div> ); @@ -29,7 +30,8 @@ TopHUD.propTypes = { frozen: PropTypes.bool, onToggleMute: PropTypes.func, onToggleFreeze: PropTypes.func, - onSpawnPen: PropTypes.func + onSpawnPen: PropTypes.func, + onSpawnCamera: PropTypes.func }; const BottomHUD = ({ onCreateObject, showPhotoPicker, onMediaPicked }) => ( diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 66afe441730a37f377a75c454b144f0d2001bde9..347ddd7b9fec924ec3bd5e5af9a6ec439d7d21cf 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -918,6 +918,7 @@ class UIRoot extends Component { onToggleFreeze={this.toggleFreeze} onToggleSpaceBubble={this.toggleSpaceBubble} onSpawnPen={this.spawnPen} + onSpawnCamera={() => this.props.scene.emit("action_spawn_camera")} /> {!this.props.availableVREntryTypes.isInHMD && this.props.occupantCount <= 1 && ( diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js index a43568cf5f6bf11914b191cbbb1f391634df1f46..a51b40fc67725cef2837007e5f3dc3afca3e9cf4 100644 --- a/src/utils/media-utils.js +++ b/src/utils/media-utils.js @@ -118,10 +118,12 @@ export const addMedia = (src, template, contentOrigin, resize = false) => { }); } - entity.addEventListener("media_resolved", ({ detail }) => { - const objectType = objectTypeForOriginAndContentType(contentOrigin, detail.contentType); - scene.emit("object_spawned", { objectType }); - }); + if (contentOrigin) { + entity.addEventListener("media_resolved", ({ detail }) => { + const objectType = objectTypeForOriginAndContentType(contentOrigin, detail.contentType); + scene.emit("object_spawned", { objectType }); + }); + } return { entity, orientation }; };