Skip to content
Snippets Groups Projects
super-spawner.js 6.69 KiB
Newer Older
import { paths } from "../systems/userinput/paths";
import { addMedia } from "../utils/media-utils";
netpro2k's avatar
netpro2k committed
import { waitForEvent } from "../utils/async-utils";
import { ObjectContentOrigins } from "../object-types";

let nextGrabId = 0;
 * Spawns networked objects when grabbed or when a specified event is fired.
 * @namespace network
 * @component super-spawner
 */
AFRAME.registerComponent("super-spawner", {
  schema: {
netpro2k's avatar
netpro2k committed
    /**
     * Source of the media asset the spawner will spawn when grabbed. This can be a gltf, video, or image, or a url that the reticiulm media API can resolve to a gltf, video, or image.
     */
    /**
     * Whether to use the Reticulum media resolution API to interpret the src URL (e.g. find a video URL for Youtube videos.)
     */
    resolve: { default: false },

Kevin Lee's avatar
Kevin Lee committed
    /**
     * The template to use for this object
     */
Kevin Lee's avatar
Kevin Lee committed

netpro2k's avatar
netpro2k committed
    /**
     * Spawn the object at a custom position, rather than at the center of the spanwer.
     */
Kevin Lee's avatar
Kevin Lee committed
    useCustomSpawnPosition: { default: false },
    spawnPosition: { type: "vec3" },
netpro2k's avatar
netpro2k committed
    /**
     * Spawn the object with a custom orientation, rather than copying that of the spawner.
     */
    useCustomSpawnRotation: { default: false },
    spawnRotation: { type: "vec4" },
Kevin Lee's avatar
Kevin Lee committed
    /**
     * Spawn the object with a custom scale, rather than copying that of the spawner.
     */
    useCustomSpawnScale: { default: false },
    spawnScale: { type: "vec3" },

netpro2k's avatar
netpro2k committed
    /**
     * The events to emit for programmatically grabbing and releasing objects
     */
    grabEvents: { default: ["cursor-grab", "primary_hand_grab"] },
    releaseEvents: { default: ["cursor-release", "primary_hand_release"] },
netpro2k's avatar
netpro2k committed
    /**
     * The spawner will become invisible and ungrabbable for this ammount of time after being grabbed. This can prevent rapidly spawning objects.
     */
    spawnCooldown: { default: 1 },

    /**
     * Center the spawned object on the hand that grabbed it after it finishes loading. By default the object will be grabbed relative to where the spawner was grabbed
     */
     * Optional event to listen for to spawn an object on the preferred superHand
     * The superHand to use if an object is spawned via spawnEvent
    superHand: { type: "selector" },

    /**
     * The cursor superHand to use if an object is spawned via spawnEvent
     */
    cursorSuperHand: { type: "selector" }
  init() {
    this.heldEntities = new Map();
    this.cooldownTimeout = null;
netpro2k's avatar
netpro2k committed
    this.onGrabStart = this.onGrabStart.bind(this);
    this.onGrabEnd = this.onGrabEnd.bind(this);

    this.onSpawnEvent = this.onSpawnEvent.bind(this);

    this.sceneEl = document.querySelector("a-scene");

    this.el.setAttribute("hoverable-visuals", { cursorController: "#cursor-controller", enableSweepingEffect: false });
Kevin Lee's avatar
Kevin Lee committed
  },

netpro2k's avatar
netpro2k committed
    this.el.addEventListener("grab-start", this.onGrabStart);
    this.el.addEventListener("grab-end", this.onGrabEnd);
    if (this.data.spawnEvent) {
      this.sceneEl.addEventListener(this.data.spawnEvent, this.onSpawnEvent);
    }
Kevin Lee's avatar
Kevin Lee committed
  },

netpro2k's avatar
netpro2k committed
    this.el.removeEventListener("grab-start", this.onGrabStart);
    this.el.removeEventListener("grab-end", this.onGrabEnd);
    if (this.data.spawnEvent) {
      this.sceneEl.removeEventListener(this.data.spawnEvent, this.onSpawnEvent);
    }
    if (this.cooldownTimeout) {
      clearTimeout(this.cooldownTimeout);
      this.cooldownTimeout = null;
Kevin Lee's avatar
Kevin Lee committed
      this.el.setAttribute("visible", true);
      this.el.classList.add("interactable");
    }
  remove() {
    this.heldEntities.clear();
Kevin Lee's avatar
Kevin Lee committed
  },

  async onSpawnEvent() {
    const userinput = AFRAME.scenes[0].systems.userinput;
netpro2k's avatar
netpro2k committed
    const leftPose = userinput.get(paths.actions.leftHand.pose);
    const rightPose = userinput.get(paths.actions.rightHand.pose);
    const controllerCount = leftPose && rightPose ? 2 : leftPose || rightPose ? 1 : 0;
    const using6DOF = controllerCount > 1 && this.el.sceneEl.is("vr-mode");
    const hand = using6DOF ? this.data.superHand : this.data.cursorSuperHand;
    if (this.cooldownTimeout || !hand) {
      return;
    }
    const entity = addMedia(this.data.src, this.data.template, ObjectContentOrigins.SPAWNER, this.data.resolve).entity;
    hand.object3D.getWorldPosition(entity.object3D.position);
    hand.object3D.getWorldQuaternion(entity.object3D.quaternion);
    if (this.data.useCustomSpawnScale) {
      entity.object3D.scale.copy(this.data.spawnScale);
    }
    this.activateCooldown();

    await waitForEvent("body-loaded", entity);
    hand.object3D.getWorldPosition(entity.object3D.position);
    if (!using6DOF) {
      for (let i = 0; i < this.data.grabEvents.length; i++) {
        hand.emit(this.data.grabEvents[i], { targetEntity: entity });
netpro2k's avatar
netpro2k committed
  async onGrabStart(e) {
    if (this.cooldownTimeout) {
    // This tells super-hands we are handling this grab. The user is now "grabbing" the spawner
    e.preventDefault();
    const hand = e.detail.hand;
    const thisGrabId = nextGrabId++;
    this.heldEntities.set(hand, thisGrabId);

    const entity = addMedia(this.data.src, this.data.template, ObjectContentOrigins.SPAWNER, this.data.resolve).entity;
    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
    );
    entity.object3D.scale.copy(this.data.useCustomSpawnScale ? this.data.spawnScale : this.el.object3D.scale);

    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) {
netpro2k's avatar
netpro2k committed
      if (this.data.centerSpawnedObject) {
        entity.body.position.copy(hand.object3D.position);
      }
      for (let i = 0; i < this.data.grabEvents.length; i++) {
        hand.emit(this.data.releaseEvents[i]);
        hand.emit(this.data.grabEvents[i], { targetEntity: entity });
Kevin Lee's avatar
Kevin Lee committed
    }

    this.activateCooldown();
netpro2k's avatar
netpro2k committed
  },
netpro2k's avatar
netpro2k committed
  onGrabEnd(e) {
    this.heldEntities.delete(e.detail.hand);
netpro2k's avatar
netpro2k committed
    // This tells super-hands we are handling this release
netpro2k's avatar
netpro2k committed
    e.preventDefault();
  },
netpro2k's avatar
netpro2k committed
  activateCooldown() {
    if (this.data.spawnCooldown > 0) {
      this.el.setAttribute("visible", false);
      this.el.classList.remove("interactable");
netpro2k's avatar
netpro2k committed
      this.cooldownTimeout = setTimeout(() => {
        this.el.setAttribute("visible", true);
        this.el.classList.add("interactable");
netpro2k's avatar
netpro2k committed
        this.cooldownTimeout = null;
      }, this.data.spawnCooldown * 1000);
    }