diff --git a/src/components/auto-box-collider.js b/src/components/auto-box-collider.js index f0aeb29414cf89021801119b463172ee4a82ec1d..633fd0d446de630aff616e27f8a211f394c59f4d 100644 --- a/src/components/auto-box-collider.js +++ b/src/components/auto-box-collider.js @@ -24,13 +24,13 @@ AFRAME.registerComponent("auto-box-collider", { this.el.object3D.worldToLocal(center); this.el.object3DMap.mesh.position.sub(center); + if (this.data.resize) { + this.resize(min, max); + } this.el.setAttribute("shape", { shape: "box", halfExtents: halfExtents }); - if (this.data.resize) { - this.resize(min, max); - } this.el.object3D.rotation.copy(rotation); this.el.removeAttribute("auto-box-collider"); }, diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js index 08491ec0ea05f253c720b50e078c5895bd3f188f..d331c78269c937d69a29387756b25337f5c5d8b5 100644 --- a/src/components/super-spawner.js +++ b/src/components/super-spawner.js @@ -1,3 +1,12 @@ +import { addMedia } from "../utils/media-utils"; + +const waitForEvent = function(eventName, eventObj) { + return new Promise(resolve => { + eventObj.addEventListener(eventName, resolve, { once: true }); + }); +}; + +let nextGrabId = 0; /** * Spawns networked objects when grabbed. * @namespace network @@ -5,108 +14,98 @@ */ AFRAME.registerComponent("super-spawner", { schema: { - template: { default: "" }, + src: { default: "https://asset-bundles-prod.reticulum.io/interactables/Ducky/DuckyMesh-438ff8e022.gltf" }, + useCustomSpawnPosition: { default: false }, spawnPosition: { type: "vec3" }, + useCustomSpawnRotation: { default: false }, spawnRotation: { type: "vec4" }, - events: { default: ["cursor-grab", "hand_grab"] }, + + grabEvents: { default: ["cursor-grab", "hand_grab"] }, + releaseEvents: { default: ["cursor-release", "hand_release"] }, + spawnCooldown: { default: 1 } }, - init: function() { - this.entities = new Map(); - this.timeout = null; + init() { + this.heldEntities = new Map(); + this.cooldownTimeout = null; + this.handleGrabStart = this.handleGrabStart.bind(this); + this.onGrabEnd = this.onGrabEnd.bind(this); }, - play: function() { - this.handleGrabStart = this._handleGrabStart.bind(this); + play() { this.el.addEventListener("grab-start", this.handleGrabStart); + this.el.addEventListener("grab-end", this.onGrabEnd); }, - pause: function() { + pause() { this.el.removeEventListener("grab-start", this.handleGrabStart); + this.el.removeEventListener("grab-end", this.onGrabEnd); - if (this.timeout) { - clearTimeout(this.timeout); - this.timeout = null; + if (this.cooldownTimeout) { + clearTimeout(this.cooldownTimeout); + this.cooldownTimeout = null; this.el.setAttribute("visible", true); this.el.classList.add("interactable"); } }, - remove: function() { - for (const entity of this.entities.keys()) { - const data = this.entities.get(entity); - entity.removeEventListener("componentinitialized", data.componentinInitializedListener); - entity.removeEventListener("body-loaded", data.bodyLoadedListener); - } - - this.entities.clear(); + remove() { + this.heldEntities.clear(); }, - _handleGrabStart: function(e) { - if (this.timeout) { - return; - } - const hand = e.detail.hand; - const entity = document.createElement("a-entity"); - - entity.setAttribute("networked", "template:" + this.data.template); - - const componentinInitializedListener = this._handleComponentInitialzed.bind(this, entity); - const bodyLoadedListener = this._handleBodyLoaded.bind(this, entity); - this.entities.set(entity, { - hand: hand, - componentInitialized: false, - bodyLoaded: false, - componentinInitializedListener: componentinInitializedListener, - bodyLoadedListener: bodyLoadedListener - }); - - entity.addEventListener("componentinitialized", componentinInitializedListener); - entity.addEventListener("body-loaded", bodyLoadedListener); - - const pos = this.data.useCustomSpawnPosition ? this.data.spawnPosition : this.el.getAttribute("position"); - entity.setAttribute("position", pos); - const rot = this.data.useCustomSpawnRotation ? this.data.spawnRotation : this.el.getAttribute("rotation"); - entity.setAttribute("rotation", rot); - this.el.sceneEl.appendChild(entity); + onGrabEnd(e) { + this.heldEntities.delete(e.detail.hand); + // This tells super-hands we are handling this releae + e.preventDefault(); + }, + activateCooldown() { if (this.data.spawnCooldown > 0) { this.el.setAttribute("visible", false); this.el.classList.remove("interactable"); - this.timeout = setTimeout(() => { + this.cooldownTimeout = setTimeout(() => { this.el.setAttribute("visible", true); this.el.classList.add("interactable"); - this.timeout = null; + this.cooldownTimeout = null; }, this.data.spawnCooldown * 1000); } }, - _handleComponentInitialzed: function(entity, e) { - if (e.detail.name === "grabbable") { - this.entities.get(entity).componentInitialized = true; - this._emitEvents.call(this, entity); + async handleGrabStart(e) { + if (this.cooldownTimeout) { + return; } - }, - _handleBodyLoaded: function(entity) { - this.entities.get(entity).bodyLoaded = true; - this._emitEvents.call(this, entity); - }, + // This tells super-hands we are handling this grab. The user is now "grabbing" the spawner + e.preventDefault(); - _emitEvents: function(entity) { - const data = this.entities.get(entity); - if (data.componentInitialized && data.bodyLoaded) { - for (let i = 0; i < this.data.events.length; i++) { - data.hand.emit(this.data.events[i], { targetEntity: entity }); + const hand = e.detail.hand; + const thisGrabId = nextGrabId++; + this.heldEntities.set(hand, thisGrabId); + + const entity = await addMedia(this.data.src); + entity.object3D.position.copy( + this.data.useCustomSpawnPosition ? this.data.spawnPosition : this.el.object3D.position + ); + entity.object3D.rotation.copy( + this.data.useCustomSpawnRotation ? this.data.spawnRotation : this.el.object3D.rotation + ); + + this.activateCooldown(); + + await waitForEvent("body-loaded", entity); + + // If we are still holding the spawner with the hand that grabbed to create this entity, release the spawner and grab the entity + if (this.heldEntities.get(hand) === thisGrabId) { + entity.body.position.copy(hand.object3D.position); + entity.body.velocity.set(0, 0, 0); + for (let i = 0; i < this.data.grabEvents.length; i++) { + hand.emit(this.data.releaseEvents[i]); + hand.emit(this.data.grabEvents[i], { targetEntity: entity }); } - - entity.removeEventListener("componentinitialized", data.componentinInitializedListener); - entity.removeEventListener("body-loaded", data.bodyLoadedListener); - - this.entities.delete(entity); } } }); diff --git a/src/hub.html b/src/hub.html index 3a649ad3c2f3082ed57b4b61e265acb943d4dd9f..572ce73d400715cb1d9629d53bd426adaeb26471 100644 --- a/src/hub.html +++ b/src/hub.html @@ -159,21 +159,6 @@ </a-entity> </template> - <template id="interactable-template"> - <a-entity - gltf-model-plus="src: #interactable-duck; inflate: true;" - class="interactable" - super-networked-interactable="counter: #counter; mass: 1;" - body="type: dynamic; shape: none; mass: 1;" - auto-scale-cannon-physics-body - grabbable - stretchable="useWorldPosition: true; usePhysics: never" - hoverable - duck - sticky-object="autoLockOnRelease: true;" - ></a-entity> - </template> - <template id="interactable-model"> <a-entity gltf-model-plus="inflate: false;" diff --git a/src/network-schemas.js b/src/network-schemas.js index a67b0d02381f0accc53472818dfdcac56951171a..47995b4edc516dc4d7ffcd8be800ecd3e2fe83ee 100644 --- a/src/network-schemas.js +++ b/src/network-schemas.js @@ -81,21 +81,6 @@ function registerNetworkSchemas() { ] }); - NAF.schemas.add({ - template: "#interactable-template", - components: [ - { - component: "position", - requiresNetworkUpdate: vectorRequiresUpdate(0.001) - }, - { - component: "rotation", - requiresNetworkUpdate: vectorRequiresUpdate(0.5) - }, - "scale" - ] - }); - NAF.schemas.add({ template: "#interactable-image", components: [ diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js index 65c13bb0149ce9e6c6d2b4fd853958efd523f236..a9e3a5799fc090d728cf6875546b49b235a651b3 100644 --- a/src/utils/media-utils.js +++ b/src/utils/media-utils.js @@ -57,7 +57,7 @@ export const spawnNetworkedInteractable = src => { model.id = "interactable-model-" + interactableId++; model.setAttribute("networked", { template: "#interactable-model" }); model.setAttribute("offset-relative-to", { - on: "model-loaded", + // on: "model-loaded", target: "#player-camera", offset: offset, selfDestruct: true @@ -76,9 +76,9 @@ export const addMedia = async url => { const contentType = await fetchContentType(farsparkUrl); if (contentType.startsWith("image/") || contentType.startsWith("video/")) { - spawnNetworkedImage(farsparkUrl, contentType); + return spawnNetworkedImage(farsparkUrl, contentType); } else if (contentType.startsWith("model/gltf") || url.endsWith(".gltf") || url.endsWith(".glb")) { - spawnNetworkedInteractable(farsparkUrl); + return spawnNetworkedInteractable(farsparkUrl); } else { throw new Error(`Unsupported content type: ${contentType}`); }