@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.
766 lines • 33.7 kB
JavaScript
import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
import { InputEvents, PointerType } from "../../engine/engine_input.js";
import { onInitialized } from "../../engine/engine_lifecycle_api.js";
import { Mathf } from "../../engine/engine_math.js";
import { RaycastOptions } from "../../engine/engine_physics.js";
import { Context } from "../../engine/engine_setup.js";
import { getParam } from "../../engine/engine_utils.js";
import { Behaviour, GameObject } from "../Component.js";
import { hasPointerEventComponent, PointerEventData } from "./PointerEvents.js";
import { ObjectRaycaster, Raycaster } from "./Raycaster.js";
import { UIRaycastUtils } from "./RaycastUtils.js";
import { $shadowDomOwner } from "./Symbols.js";
import { isUIObject } from "./Utils.js";
const debug = getParam("debugeventsystem");
export var EventSystemEvents;
(function (EventSystemEvents) {
EventSystemEvents["BeforeHandleInput"] = "BeforeHandleInput";
EventSystemEvents["AfterHandleInput"] = "AfterHandleInput";
})(EventSystemEvents || (EventSystemEvents = {}));
onInitialized((ctx) => {
EventSystem.createIfNoneExists(ctx);
});
/**
* @category User Interface
* @group Components
*/
export class EventSystem extends Behaviour {
//@ts-ignore
static ensureUpdateMeshUI(instance, context, force = false) {
MeshUIHelper.update(instance, context, force);
}
static markUIDirty(_context) {
MeshUIHelper.markDirty();
}
static createIfNoneExists(context) {
if (!context.scene.getComponent(EventSystem)) {
context.scene.addComponent(EventSystem);
}
}
static get(ctx) {
this.createIfNoneExists(ctx);
return ctx.scene.getComponent(EventSystem);
}
/** Get the currently active event system */
static get instance() {
return this.get(Context.Current);
}
raycaster = [];
register(rc) {
if (rc && this.raycaster && !this.raycaster.includes(rc))
this.raycaster?.push(rc);
}
unregister(rc) {
const i = this.raycaster?.indexOf(rc);
if (i !== undefined && i !== -1) {
this.raycaster?.splice(i, 1);
}
}
get hasActiveUI() {
return this.currentActiveMeshUIComponents.length > 0;
}
get isHoveringObjects() { return this.hoveredByID.size > 0; }
awake() {
// We only want ONE eventsystem on the root scene
// as long as this component is not implemented in core we need to check this here
if (this.gameObject !== this.context.scene) {
console.debug(`[Needle Engine] EventSystem is only allowed on the scene root. Disabling EventSystem on '${this.gameObject.name}'`);
this.enabled = false;
}
}
start() {
if (!this.context.scene.getComponent(Raycaster)) {
this.context.scene.addComponent(ObjectRaycaster);
}
}
onEnable() {
this.context.input.addEventListener(InputEvents.PointerDown, this.onPointerEvent);
this.context.input.addEventListener(InputEvents.PointerUp, this.onPointerEvent);
this.context.input.addEventListener(InputEvents.PointerMove, this.onPointerEvent);
}
onDisable() {
this.context.input.removeEventListener(InputEvents.PointerDown, this.onPointerEvent);
this.context.input.removeEventListener(InputEvents.PointerUp, this.onPointerEvent);
this.context.input.removeEventListener(InputEvents.PointerMove, this.onPointerEvent);
}
/**
* all pointers that have pressed something
*
* key: pointerId
* value: object that was pressed, data of the pointer event, handlers that are releavant to the event
*/
pressedByID = new Map();
/**
* all hovered objects
*
* key: pointerId
* value: object that is hovered, data of the pointer event
*/
hoveredByID = new Map();
onBeforeRender() {
this.resetMeshUIStates();
}
/**
* Handle an pointer event from the input system
*/
onPointerEvent = (pointerEvent) => {
if (pointerEvent === undefined)
return;
if (pointerEvent.propagationStopped)
return;
if (pointerEvent.defaultPrevented)
return;
if (pointerEvent.used)
return;
// Use the pointerID, this is the touch id or the mouse (always 0, could be index of the mouse DEVICE) or the index of the XRInputSource
const data = new PointerEventData(this.context.input, pointerEvent);
this._currentPointerEventName = pointerEvent.type;
data.inputSource = this.context.input;
data.isClick = pointerEvent.isClick;
data.isDoubleClick = pointerEvent.isDoubleClick;
// using the input type directly instead of input API -> otherwise onMove events can sometimes be getPointerUp() == true
data.isDown = pointerEvent.type == InputEvents.PointerDown;
data.isUp = pointerEvent.type == InputEvents.PointerUp;
data.isPressed = this.context.input.getPointerPressed(pointerEvent.pointerId);
if (debug) {
if (data.isDown)
console.log("DOWN", data.pointerId);
else if (data.isUp)
console.log("UP", data.pointerId);
if (data.isClick)
console.log("CLICK", data.pointerId);
}
// raycast
const options = new RaycastOptions();
if (pointerEvent.hasRay) {
options.ray = pointerEvent.ray;
}
else {
options.screenPoint = this.context.input.getPointerPositionRC(pointerEvent.pointerId);
}
const hits = this.performRaycast(options);
if (hits) {
for (const hit of hits) {
hit.event = pointerEvent;
pointerEvent.intersections.push(hit);
}
if (pointerEvent.origin.onPointerHits) {
pointerEvent.origin.onPointerHits({
sender: this,
event: pointerEvent,
hits
});
}
}
if (debug && data.isClick) {
showBalloonMessage("EventSystem: " + data.pointerId + " - " + this.context.time.frame + " - Up:" + data.isUp + ", Down:" + data.isDown);
}
const evt = {
sender: this,
args: data,
hasActiveUI: this.currentActiveMeshUIComponents.length > 0,
};
this.dispatchEvent(new CustomEvent(EventSystemEvents.BeforeHandleInput, { detail: evt }));
// then handle the intersections and call the callbacks on the regular objects
this.handleIntersections(hits, data);
this.dispatchEvent(new CustomEvent(EventSystemEvents.AfterHandleInput, { detail: evt }));
};
_sortedHits = [];
/**
* cache for objects that we want to raycast against. It's cleared before each call to performRaycast invoking raycasters
*/
_testObjectsCache = new Map();
/** that's the raycaster that is CURRENTLY being used for raycasting (the shouldRaycastObject method uses this) */
_currentlyActiveRaycaster = null;
_currentPointerEventName = null;
/**
* Checks if an object that we encounter has an event component and if it does, we add it to our objects cache
* If it doesnt we tell our raycasting system to ignore it and continue in the child hierarchy
* We do this to avoid raycasts against objects that are not going to be used by the event system
* Because there's no component callback to be invoked anyways.
* This is especially important to avoid expensive raycasts against SkinnedMeshes
*
* Further optimizations would be to check what type of event we're dealing with
* For example if an event component has only an onPointerClick method we don't need to raycast during movement events
* */
shouldRaycastObject = (obj) => {
// TODO: this implementation below should be removed and we should regularly raycast objects in the scene unless marked as "do not raycast"
// with the introduction of the mesh-bvh based raycasting the performance impact should be greatly reduced. But this needs further testing
const raycasterOnObject = obj && "getComponent" in obj ? obj.getComponent(Raycaster) : null;
if (raycasterOnObject && raycasterOnObject != this._currentlyActiveRaycaster) {
return false;
}
// if (this._currentPointerEventName == "pointermove") {
// console.log(this.context.time.frame, obj.name, obj.type, obj.guid)
// }
// check if this object is actually a UI shadow hierarchy object
let uiOwner = null;
const isUI = isUIObject(obj);
// if yes we want to grab the actual object that is the owner of the shadow dom
// and check that object for the event component
if (isUI) {
uiOwner = obj[$shadowDomOwner]?.gameObject;
}
// check if the object was seen previously
if (this._testObjectsCache.has(obj) || (uiOwner && this._testObjectsCache.has(uiOwner))) {
// if yes we check if it was previously stored as "YES WE NEED TO RAYCAST THIS"
const prev = this._testObjectsCache.get(obj);
if (prev === false)
return "continue in children";
return true;
}
else {
// if the object has another raycaster component than the one that is currently raycasting, we ignore this here
// because then this other raycaster is responsible for raycasting this object
// const rc = GameObject.getComponent(obj, Raycaster);
// if (rc?.activeAndEnabled && rc !== this._currentlyActiveRaycaster) return false;
// the object was not yet seen so we test if it has an event component
let hasEventComponent = hasPointerEventComponent(obj, this._currentPointerEventName);
if (!hasEventComponent && uiOwner)
hasEventComponent = hasPointerEventComponent(uiOwner, this._currentPointerEventName);
if (hasEventComponent) {
// it has an event component: we add it and all its children to the cache
// we don't need to do the same for the shadow component hierarchy
// because the next object that will be detecting that the shadow owner was already seen
this._testObjectsCache.set(obj, true);
for (const ch of obj.children)
this.shouldRaycastObject_AddToYesCache(ch);
return true;
}
this._testObjectsCache.set(obj, false);
return "continue in children";
}
};
shouldRaycastObject_AddToYesCache(obj) {
// if the object has another raycaster component than the one that is currently raycasting, we ignore this here
// because then this other raycaster is responsible for raycasting this object
// const rc = GameObject.getComponent(obj, Raycaster);
// if (rc?.activeAndEnabled && rc !== this._currentlyActiveRaycaster) return;
this._testObjectsCache.set(obj, true);
for (const ch of obj.children) {
this.shouldRaycastObject_AddToYesCache(ch);
}
}
/** the raycast filter is always overriden */
performRaycast(opts) {
if (!this.raycaster)
return null;
// we clear the cache of previously seen objects
this._testObjectsCache.clear();
this._sortedHits.length = 0;
if (!opts)
opts = new RaycastOptions();
opts.testObject = this.shouldRaycastObject;
for (const rc of this.raycaster) {
if (!rc.activeAndEnabled)
continue;
this._currentlyActiveRaycaster = rc;
const res = rc.performRaycast(opts);
this._currentlyActiveRaycaster = null;
if (res && res.length > 0) {
// console.log(res.length, res.map(r => r.object.name));
this._sortedHits.push(...res);
}
}
this._sortedHits.sort((a, b) => {
return a.distance - b.distance;
});
return this._sortedHits;
}
assignHitInformation(args, hit) {
if (!hit) {
args.intersection = undefined;
args.point = undefined;
args.normal = undefined;
args.face = undefined;
args.distance = undefined;
args.instanceId = undefined;
}
else {
args.intersection = hit;
args.point = hit.point;
args.normal = hit.normal;
args.face = hit.face;
args.distance = hit.distance;
args.instanceId = hit.instanceId;
}
}
handleIntersections(hits, args) {
if (hits?.length) {
hits = this.sortCandidates(hits);
for (const hit of hits) {
if (args.event.immediatePropagationStopped) {
return false;
}
this.assignHitInformation(args, hit);
if (this.handleEventOnObject(hit.object, args)) {
return true;
}
}
}
// first invoke captured pointers
this.assignHitInformation(args, hits?.[0]);
this.invokePointerCapture(args);
// pointer has not hit any object to handle
// thus is not hovering over anything
const hoveredData = this.hoveredByID.get(args.pointerId);
if (hoveredData) {
this.propagatePointerExit(hoveredData.obj, hoveredData.data, null);
}
this.hoveredByID.delete(args.pointerId);
// if it was up, it means it should notify things that it down on before
if (args.isUp) {
this.pressedByID.get(args.pointerId)?.handlers.forEach(h => this.invokeOnPointerUp(args, h));
this.pressedByID.delete(args.pointerId);
}
return false;
}
_sortingBuffer = [];
_noDepthTestingResults = [];
sortCandidates(hits) {
// iterate over all hits and filter for nodepth objects and normal hit objects
// the no-depth objects will be handled first starting from the closest
// assuming the hits array is sorted by distance (closest > furthest)
this._sortingBuffer.length = 0;
this._noDepthTestingResults.length = 0;
for (let i = 0; i < hits.length; i++) {
const hit = hits[i];
const object = hit.object;
if (object.material) {
if (object.material["depthTest"] === false) {
this._noDepthTestingResults.push(hit);
continue;
}
}
this._sortingBuffer.push(hit);
}
for (const obj of this._sortingBuffer) {
this._noDepthTestingResults.push(obj);
}
return this._noDepthTestingResults;
}
out = {};
/**
* Handle hit result by preparing all needed information before propagation.
* Then calling propagate.
*/
handleEventOnObject(object, args) {
// ensures that invisible objects are ignored
if (!this.testIsVisible(object)) {
if (args.isClick && debug)
console.log("not allowed", object);
return false;
}
// Event without pointer can't be handled
if (args.pointerId === undefined) {
if (debug)
console.error("Event without pointer can't be handled", args);
return false;
}
// Correct the handled object to match the relevant object in shadow dom (?)
args.object = object;
const parent = object.parent;
let isShadow = false;
const clicked = args.isClick ?? false;
let canvasGroup = null;
// handle potential shadow dom built from three mesh ui
if (parent && parent.isUI) {
const pressedOrClicked = (args.isPressed || args.isClick) ?? false;
if (parent[$shadowDomOwner]) {
const actualGo = parent[$shadowDomOwner].gameObject;
if (actualGo) {
const res = UIRaycastUtils.isInteractable(actualGo, this.out);
if (!res)
return false;
canvasGroup = this.out.canvasGroup ?? null;
const handled = this.handleMeshUIIntersection(object, pressedOrClicked);
if (!clicked && handled) {
// return true;
}
object = actualGo;
isShadow = true;
}
}
// adding this to have a way for allowing to receive events on TMUI elements without shadow hierarchy
// if(parent["needle:use_eventsystem"] == true){
// // if use_eventsystem is true, we want to handle the event
// }
// else if (!isShadow) {
// const obj = this.handleMeshUiObjectWithoutShadowDom(parent, pressedOrClicked);
// if (obj) return true;
// }
}
if (clicked && debug)
console.log(this.context.time.frame, object);
// Handle OnPointerExit -> in case when we are about to hover something new
// TODO: we need to keep track of the components that already received a PointerEnterEvent -> we can have a hierarchy where the hovered object changes but the component is on the parent and should not receive a PointerExit event because it's still hovered (just another child object)
const hovering = this.hoveredByID.get(args.pointerId);
const prevHovering = hovering?.obj;
const isNewlyHovering = prevHovering !== object;
// trigger onPointerExit
if (isNewlyHovering && prevHovering) {
this.propagatePointerExit(prevHovering, hovering.data, object);
}
// save hovered object
const entry = this.hoveredByID.get(args.pointerId);
if (!entry)
this.hoveredByID.set(args.pointerId, { obj: object, data: args });
else {
entry.obj = object;
entry.data = args;
}
// create / update pressed entry
if (args.isDown) {
const data = this.pressedByID.get(args.pointerId);
if (!data)
this.pressedByID.set(args.pointerId, { obj: object, data: args, handlers: new Set() });
else {
data.obj = object;
data.data = args;
}
}
if (canvasGroup === null || canvasGroup.interactable) {
this.handleMainInteraction(object, args, prevHovering ?? null);
}
return true;
}
/**
* Propagate up in hiearchy and call the callback for each component that is possibly a handler
*/
propagate(object, onComponent) {
while (true) {
if (!object)
break;
GameObject.foreachComponent(object, comp => {
// TODO: implement Stop Immediate Propagation
onComponent(comp);
}, false);
// walk up
object = object.parent;
}
}
/**
* Propagate up in hierarchy and call handlers based on the pointer event data
*/
handleMainInteraction(object, args, prevHovering) {
const pressedEvent = this.pressedByID.get(args.pointerId);
const hoveredObjectChanged = prevHovering !== object;
// TODO: should we not move this check up before we even raycast for "pointerMove" events? We dont need to do any processing if the pointer didnt move
let isMoving = true;
switch (args.event.pointerType) {
case "mouse":
case "touch":
const posLastFrame = this.context.input.getPointerPositionLastFrame(args.pointerId);
const posThisFrame = this.context.input.getPointerPosition(args.pointerId);
isMoving = posLastFrame && !Mathf.approximately(posLastFrame, posThisFrame);
break;
case "controller":
case "hand":
// for hands and controller we assume they are never totally still (except for simulated environments)
// we might want to add a threshold here (e.g. if a user holds their hand very still or controller)
// so maybe check the angle every frame?
break;
}
this.propagate(object, (behaviour) => {
const comp = behaviour;
if (comp.interactable === false)
return;
if (!comp.activeAndEnabled || !comp.enabled)
return;
if (comp.onPointerEnter) {
if (hoveredObjectChanged) {
this.handlePointerEnter(comp, args);
}
}
if (args.isDown) {
if (comp.onPointerDown) {
comp.onPointerDown(args);
// Set the handler that we called the down event on
// So we can call the up event on the same handler
// In a scenario where we Down on one object and Up on another
pressedEvent?.handlers.add(comp);
this.handlePointerCapture(args, comp);
}
}
if (comp.onPointerMove) {
if (isMoving)
comp.onPointerMove(args);
this.handlePointerCapture(args, comp);
}
if (args.isUp) {
if (comp.onPointerUp) {
this.invokeOnPointerUp(args, comp);
// We don't want to call Up twice if we Down and Up on the same object
// But if we Down on one and Up on another we want to call Up on the first one as well
// For example if the object was cloned by the Duplicatable
// The original component that received the down event SHOULD also receive the up event
pressedEvent?.handlers.delete(comp);
}
// handle touch onExit (touchUp) since the pointer stops existing
// mouse onExit (mouseUp) is handled when we hover over something else / on nothing
// Mouse 0 is always persistent
if (comp.onPointerExit && args.event?.pointerType === PointerType.Touch) {
this.handlePointerExit(comp, args);
this.hoveredByID.delete(args.pointerId);
}
}
if (args.isClick) {
if (comp.onPointerClick) {
comp.onPointerClick(args);
}
}
});
// after the propagation end, call UP on any objects that were DOWNED and didn't recieve an UP while propagating
// If user drags away from the object, then it doesn't get the UP event
if (args.isUp) {
pressedEvent?.handlers.forEach((handler) => {
this.invokeOnPointerUp(args, handler);
});
this.pressedByID.delete(args.pointerId);
}
}
/** Propagate up in hierarchy and call onPointerExit */
propagatePointerExit(object, args, newObject) {
this.propagate(object, (behaviour) => {
if (!behaviour.gameObject || behaviour.destroyed)
return;
const inst = behaviour;
if (inst.onPointerExit || inst.onPointerEnter) {
// if the newly hovered object is a child of the current object, we don't want to call onPointerExit
if (newObject && this.isChild(newObject, behaviour.gameObject)) {
return;
}
this.handlePointerExit(inst, args);
}
});
}
/** handles onPointerUp - this will also release the pointerCapture */
invokeOnPointerUp(evt, handler) {
handler.onPointerUp?.call(handler, evt);
this.releasePointerCapture(evt, handler);
}
/** Responsible for invoking onPointerEnter (and updating onPointerExit). We invoke onPointerEnter once per active pointerId */
handlePointerEnter(comp, args) {
if (comp.onPointerEnter) {
if (this.updatePointerState(comp, args.pointerId, this.pointerEnterSymbol, true)) {
comp.onPointerEnter(args);
}
}
this.updatePointerState(comp, args.pointerId, this.pointerExitSymbol, false);
}
/** Responsible for invoking onPointerExit (and updating onPointerEnter). We invoke onPointerExit once per active pointerId */
handlePointerExit(comp, evt) {
if (comp.onPointerExit) {
if (this.updatePointerState(comp, evt.pointerId, this.pointerExitSymbol, true)) {
comp.onPointerExit(evt);
}
}
this.updatePointerState(comp, evt.pointerId, this.pointerEnterSymbol, false);
}
/** updates the pointer state list for a component
* @param comp the component to update
* @param pointerId the pointerId to update
* @param symbol the symbol to use for the state
* @param add if true, the pointerId is added to the state list, if false the pointerId will be removed
*/
updatePointerState(comp, pointerId, symbol, add) {
let state = comp[symbol];
if (add) {
// the pointer is already in the state list
if (state && state.includes(pointerId))
return false;
state = state || [];
state.push(pointerId);
comp[symbol] = state;
return true;
}
else {
if (!state || !state.includes(pointerId))
return false;
const i = state.indexOf(pointerId);
if (i !== -1) {
state.splice(i, 1);
}
return true;
}
}
/** the list of component handlers that requested pointerCapture for a specific pointerId */
_capturedPointer = {};
/** check if the event was marked to be captured: if yes add the current component to the captured list */
handlePointerCapture(evt, comp) {
if (evt.z__pointer_ctured) {
evt.z__pointer_ctured = false;
const id = evt.pointerId;
// only the onPointerMove event is called with captured pointers so we don't need to add it to our list if it doesnt implement onPointerMove
if (comp.onPointerMove) {
const list = this._capturedPointer[id] || [];
list.push(comp);
this._capturedPointer[id] = list;
}
else {
if (isDevEnvironment() && !comp["z__warned_no_pointermove"]) {
comp["z__warned_no_pointermove"] = true;
console.warn("PointerCapture was requested but the component doesn't implement onPointerMove. It will not receive any pointer events");
}
}
}
else if (evt.z__pointer_cture_rleased) {
evt.z__pointer_cture_rleased = false;
this.releasePointerCapture(evt, comp);
}
}
/** removes the component from the pointer capture list */
releasePointerCapture(evt, component) {
const id = evt.pointerId;
if (this._capturedPointer[id]) {
const i = this._capturedPointer[id].indexOf(component);
if (i !== -1) {
this._capturedPointer[id].splice(i, 1);
if (debug)
console.log("released pointer capture", id, component, this._capturedPointer);
}
}
}
/** invoke the pointerMove event on all captured handlers */
invokePointerCapture(evt) {
if (evt.event.type === InputEvents.PointerMove) {
const id = evt.pointerId;
const captured = this._capturedPointer[id];
if (captured) {
if (debug)
console.log("Captured", id, captured);
for (let i = 0; i < captured.length; i++) {
const handler = captured[i];
// check if it was destroyed
const comp = handler;
if (comp.destroyed) {
captured.splice(i, 1);
i--;
continue;
}
// invoke pointer move
handler.onPointerMove?.call(handler, evt);
}
}
}
}
pointerEnterSymbol = Symbol("pointerEnter");
pointerExitSymbol = Symbol("pointerExit");
isChild(obj, possibleChild) {
if (!obj || !possibleChild)
return false;
if (obj === possibleChild)
return true;
if (!obj.parent)
return false;
return this.isChild(obj.parent, possibleChild);
}
handleMeshUiObjectWithoutShadowDom(obj, pressed) {
if (!obj || !obj.isUI)
return true;
const hit = this.handleMeshUIIntersection(obj, pressed);
return hit;
}
currentActiveMeshUIComponents = [];
handleMeshUIIntersection(meshUiObject, pressed) {
const res = MeshUIHelper.updateState(meshUiObject, pressed);
if (res) {
this.currentActiveMeshUIComponents.push(res);
}
return res !== null;
}
resetMeshUIStates() {
if (this.context.input.getPointerPressedCount() > 0) {
MeshUIHelper.resetLastSelected();
}
if (!this.currentActiveMeshUIComponents || this.currentActiveMeshUIComponents.length <= 0)
return;
for (let i = 0; i < this.currentActiveMeshUIComponents.length; i++) {
const comp = this.currentActiveMeshUIComponents[i];
MeshUIHelper.resetState(comp);
}
this.currentActiveMeshUIComponents.length = 0;
}
testIsVisible(obj) {
if (!obj)
return true;
if (!GameObject.isActiveSelf(obj))
return false;
return this.testIsVisible(obj.parent);
}
}
class MeshUIHelper {
static lastSelected = null;
static lastUpdateFrame = [];
static needsUpdate = false;
static markDirty() {
this.needsUpdate = true;
}
static update(threeMeshUI, context, force = false) {
if (force) {
threeMeshUI.update();
return;
}
const currentFrame = context.time.frameCount;
for (const lu of this.lastUpdateFrame) {
if (lu.context === context) {
if (currentFrame === lu.frame)
return;
lu.frame = currentFrame;
let shouldUpdate = this.needsUpdate || currentFrame < 1;
if (lu.nextUpdate <= currentFrame)
shouldUpdate = true;
// if (this.needsUpdate) lu.nextUpdate = currentFrame + 3;
if (shouldUpdate) {
// console.warn(currentFrame, lu.nextUpdate, this.needsUpdate)
if (debug)
console.log("Update threemeshui");
this.needsUpdate = false;
lu.nextUpdate = currentFrame + 60;
threeMeshUI.update();
}
return;
}
}
this.lastUpdateFrame = [{ context, frame: currentFrame, nextUpdate: currentFrame + 60 }];
threeMeshUI.update();
this.needsUpdate = false;
}
static updateState(intersect, _selectState) {
let foundBlock = null;
if (intersect) {
foundBlock = this.findBlockOrTextInParent(intersect);
// console.log(intersect, "-- found block:", foundBlock)
if (foundBlock && foundBlock !== this.lastSelected) {
const interactable = foundBlock["interactable"];
if (interactable === false)
return null;
this.needsUpdate = true;
}
}
return foundBlock;
}
static resetLastSelected() {
const last = this.lastSelected;
if (!last)
return;
this.lastSelected = null;
this.resetState(last);
}
static resetState(obj) {
if (!obj)
return;
this.needsUpdate = true;
}
static findBlockOrTextInParent(elem) {
if (!elem)
return null;
if (elem.isBlock || (elem.isText)) {
// @TODO : Replace states managements
// if (Object.keys(elem.states).length > 0)
return elem;
}
return this.findBlockOrTextInParent(elem.parent);
}
}
//# sourceMappingURL=EventSystem.js.map