@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
230 lines (202 loc) • 7.95 kB
text/typescript
import { Layers, Object3D } from "three";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { getParam } from "../engine/engine_utils.js";
import { BoxHelperComponent } from "./BoxHelperComponent.js"
import { Behaviour, GameObject } from "./Component.js";
import { EventList } from "./EventList.js";
const debug = getParam("debugspatialtrigger");
/** Layer instances used for mask comparison */
const layer1 = new Layers();
const layer2 = new Layers();
/**
* Tests if two layer masks intersect
* @param mask1 First layer mask
* @param mask2 Second layer mask
* @returns True if the layers intersect
*/
function testMask(mask1, mask2) {
layer1.mask = mask1;
layer2.mask = mask2;
return layer1.test(layer2);
}
/**
* Component that receives and responds to spatial events, like entering or exiting a trigger zone.
* Used in conjunction with {@link SpatialTrigger} to create interactive spatial events.
* @category Interactivity
* @group Components
*/
export class SpatialTriggerReceiver extends Behaviour {
/**
* Bitmask determining which triggers this receiver responds to
* Only triggers with matching masks will interact with this receiver
*/
triggerMask: number = 0;
/** Event invoked when this object enters a trigger zone */
onEnter?: EventList<any>;
/** Event invoked continuously while this object is inside a trigger zone */
onStay?: EventList<any>;
/** Event invoked when this object exits a trigger zone */
onExit?: EventList<any>;
/**
* Initializes the receiver and logs debug info if enabled
* @internal
*/
start() {
if (debug) console.log(this.name, this.triggerMask, this);
}
/**
* Checks for intersections with spatial triggers and fires appropriate events
* Handles enter, stay, and exit events for all relevant triggers
* @internal
*/
update(): void {
this.currentIntersected.length = 0;
for (const trigger of SpatialTrigger.triggers) {
if (testMask(trigger.triggerMask, this.triggerMask)) {
if (trigger.test(this.gameObject)) {
this.currentIntersected.push(trigger);
}
}
}
for (let i = this.lastIntersected.length - 1; i >= 0; i--) {
const last = this.lastIntersected[i]
if (this.currentIntersected.indexOf(last) < 0) {
this.onExitTrigger(last);
this.lastIntersected.splice(i, 1);
}
}
for (const cur of this.currentIntersected) {
if (this.lastIntersected.indexOf(cur) < 0)
this.onEnterTrigger(cur);
this.onStayTrigger(cur);
}
this.lastIntersected.length = 0;
this.lastIntersected.push(...this.currentIntersected);
}
/** Array of triggers currently intersecting with this receiver */
readonly currentIntersected: SpatialTrigger[] = [];
/** Array of triggers that intersected with this receiver in the previous frame */
readonly lastIntersected: SpatialTrigger[] = [];
/**
* Handles trigger enter events.
* @param trigger The spatial trigger that was entered
*/
onEnterTrigger(trigger: SpatialTrigger): void {
if(debug) console.log("ENTER TRIGGER", this.name, trigger.name, this, trigger);
trigger.raiseOnEnterEvent(this);
this.onEnter?.invoke();
}
/**
* Handles trigger exit events.
* @param trigger The spatial trigger that was exited
*/
onExitTrigger(trigger: SpatialTrigger): void {
if(debug) console.log("EXIT TRIGGER", this.name, trigger.name, );
trigger.raiseOnExitEvent(this);
this.onExit?.invoke();
}
/**
* Handles trigger stay events.
* @param trigger The spatial trigger that the receiver is staying in
*/
onStayTrigger(trigger: SpatialTrigger): void {
trigger.raiseOnStayEvent(this);
this.onStay?.invoke();
}
}
/**
* A spatial trigger component that detects objects within a box-shaped area.
* Used to trigger events when objects enter, stay in, or exit the defined area
* @category Interactivity
* @group Components
*/
export class SpatialTrigger extends Behaviour {
/** Global registry of all active spatial triggers in the scene */
static triggers: SpatialTrigger[] = [];
/**
* Bitmask determining which receivers this trigger affects.
* Only receivers with matching masks will be triggered.
*/
// currently Layers in unity but maybe this should be a string or plane number? Or should it be a bitmask to allow receivers use multiple triggers?
triggerMask?: number;
/** Box helper component used to visualize and calculate the trigger area */
private boxHelper?: BoxHelperComponent;
/**
* Initializes the trigger and logs debug info if enabled
*/
start() {
if (debug)
console.log(this.name, this.triggerMask, this);
}
/**
* Registers this trigger in the global registry and sets up debug visualization if enabled
*/
onEnable(): void {
SpatialTrigger.triggers.push(this);
if (!this.boxHelper) {
this.boxHelper = GameObject.addComponent(this.gameObject, BoxHelperComponent);
this.boxHelper?.showHelper(null, debug as boolean);
}
}
/**
* Removes this trigger from the global registry when disabled
*/
onDisable(): void {
SpatialTrigger.triggers.splice(SpatialTrigger.triggers.indexOf(this), 1);
}
/**
* Tests if an object is inside this trigger's box
* @param obj The object to test against this trigger
* @returns True if the object is inside the trigger box
*/
test(obj: Object3D): boolean {
if (!this.boxHelper) return false;
return this.boxHelper.isInBox(obj) ?? false;
}
// private args: SpatialTriggerEventArgs = new SpatialTriggerEventArgs();
/**
* Raises the onEnter event on any SpatialTriggerReceiver components attached to this trigger's GameObject
* @param rec The receiver that entered this trigger
*/
raiseOnEnterEvent(rec: SpatialTriggerReceiver) {
// this.args.trigger = this;
// this.args.source = rec;
GameObject.foreachComponent(this.gameObject, c => {
if (c === rec) return;
if(c instanceof SpatialTriggerReceiver) {
c.onEnterTrigger(this);
}
}, false);
}
/**
* Raises the onStay event on any SpatialTriggerReceiver components attached to this trigger's GameObject
* @param rec The receiver that is staying in this trigger
*/
raiseOnStayEvent(rec: SpatialTriggerReceiver) {
// this.args.trigger = this;
// this.args.source = rec;
GameObject.foreachComponent(this.gameObject, c => {
if (c === rec) return;
if(c instanceof SpatialTriggerReceiver) {
c.onStayTrigger(this);
}
}, false);
}
/**
* Raises the onExit event on any SpatialTriggerReceiver components attached to this trigger's GameObject
* @param rec The receiver that exited this trigger
*/
raiseOnExitEvent(rec: SpatialTriggerReceiver) {
GameObject.foreachComponent(this.gameObject, c => {
if (c === rec) return;
if(c instanceof SpatialTriggerReceiver) {
c.onExitTrigger(this);
}
}, false);
}
}