This repository has been archived by the owner on Aug 21, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'dev' into IR-3575-playmode-on-scene-change
- Loading branch information
Showing
4 changed files
with
1,003 additions
and
785 deletions.
There are no files selected for viewing
283 changes: 283 additions & 0 deletions
283
packages/spatial/src/input/functions/ClientInputFunctions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,283 @@ | ||
/* | ||
CPAL-1.0 License | ||
The contents of this file are subject to the Common Public Attribution License | ||
Version 1.0. (the "License"); you may not use this file except in compliance | ||
with the License. You may obtain a copy of the License at | ||
https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. | ||
The License is based on the Mozilla Public License Version 1.1, but Sections 14 | ||
and 15 have been added to cover use of software over a computer network and | ||
provide for limited attribution for the Original Developer. In addition, | ||
Exhibit A has been modified to be consistent with Exhibit B. | ||
Software distributed under the License is distributed on an "AS IS" basis, | ||
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the | ||
specific language governing rights and limitations under the License. | ||
The Original Code is Ethereal Engine. | ||
The Original Developer is the Initial Developer. The Initial Developer of the | ||
Original Code is the Ethereal Engine team. | ||
All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 | ||
Ethereal Engine. All Rights Reserved. | ||
*/ | ||
|
||
/** | ||
* @fileoverview | ||
* Contains function definitions used by ClientInputSystem and other ClientInput related modules | ||
*/ | ||
|
||
import { | ||
defineQuery, | ||
Entity, | ||
getComponent, | ||
getMutableComponent, | ||
getOptionalComponent, | ||
hasComponent, | ||
Not, | ||
UndefinedEntity, | ||
UUIDComponent | ||
} from '@etherealengine/ecs' | ||
import { Quaternion, Vector3 } from 'three' | ||
import { PI, Q_IDENTITY, Vector3_Zero } from '../../common/constants/MathConstants' | ||
import { TransformComponent } from '../../SpatialModule' | ||
import { getAncestorWithComponent } from '../../transform/components/EntityTree' | ||
import { TransformGizmoTagComponent } from '../../transform/components/TransformComponent' | ||
import { XRSpaceComponent } from '../../xr/XRComponents' | ||
import { XRUIComponent } from '../../xrui/components/XRUIComponent' | ||
import { DefaultButtonAlias, InputComponent } from '../components/InputComponent' | ||
import { InputPointerComponent } from '../components/InputPointerComponent' | ||
import { InputSourceComponent } from '../components/InputSourceComponent' | ||
import { ButtonState, ButtonStateMap, createInitialButtonState, MouseButton } from '../state/ButtonState' | ||
import { HeuristicData, HeuristicFunctions, IntersectionData } from './ClientInputHeuristics' | ||
|
||
/** radian threshold for rotating state*/ | ||
const ROTATING_THRESHOLD = 1.5 * (PI / 180) | ||
|
||
/** squared distance threshold for dragging state */ | ||
const DRAGGING_THRESHOLD = 0.001 | ||
|
||
/** anti-garbage variable!! value not to be used unless you set values just before use*/ | ||
const _pointerPositionVector3 = new Vector3() | ||
|
||
export function preventDefault(e) { | ||
e.preventDefault() | ||
} | ||
|
||
export const preventDefaultKeyDown = (evt) => { | ||
if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') return | ||
if (evt.code === 'Tab') evt.preventDefault() | ||
// prevent DOM tab selection and spacebar/enter button toggling (since it interferes with avatar controls) | ||
if (evt.code === 'Space' || evt.code === 'Enter') evt.preventDefault() | ||
} | ||
|
||
export function updateGamepadInput(eid: Entity) { | ||
const inputSource = getComponent(eid, InputSourceComponent) | ||
const gamepad = inputSource.source.gamepad | ||
const buttons = inputSource.buttons | ||
// const buttonDownPos = inputSource.buttonDownPositions as WeakMap<AnyButton, Vector3> | ||
// log buttons | ||
// if (source.gamepad) { | ||
// for (let i = 0; i < source.gamepad.buttons.length; i++) { | ||
// const button = source.gamepad.buttons[i] | ||
// if (button.pressed) console.log('button ' + i + ' pressed: ' + button.pressed) | ||
// } | ||
// } | ||
|
||
if (!gamepad) return | ||
const gamepadButtons = gamepad.buttons | ||
if (!gamepadButtons.length) return | ||
|
||
const pointer = getOptionalComponent(eid, InputPointerComponent) | ||
const xrTransform = getOptionalComponent(eid, TransformComponent) | ||
|
||
for (let i = 0; i < gamepadButtons.length; i++) { | ||
const gamepadButton = gamepadButtons[i] | ||
if (!buttons[i] && (gamepadButton.pressed || gamepadButton.touched)) { | ||
buttons[i] = createInitialButtonState(eid, gamepadButton) | ||
} | ||
const buttonState = buttons[i] as ButtonState | ||
if (buttonState && (gamepadButton.pressed || gamepadButton.touched)) { | ||
if (!buttonState.pressed && gamepadButton.pressed) { | ||
buttonState.down = true | ||
buttonState.downPosition = new Vector3() | ||
buttonState.downRotation = new Quaternion() | ||
|
||
if (pointer) { | ||
buttonState.downPosition.set(pointer.position.x, pointer.position.y, 0) | ||
//TODO maybe map pointer rotation/swing/twist to downRotation here once we map the pointer events to that (think Apple pencil) | ||
} else if (hasComponent(eid, XRSpaceComponent) && xrTransform) { | ||
buttonState.downPosition.copy(xrTransform.position) | ||
buttonState.downRotation.copy(xrTransform.rotation) | ||
} | ||
} | ||
buttonState.pressed = gamepadButton.pressed | ||
buttonState.touched = gamepadButton.touched | ||
buttonState.value = gamepadButton.value | ||
|
||
if (buttonState.downPosition) { | ||
//if not yet dragging, compare distance to drag threshold and begin if appropriate | ||
if (!buttonState.dragging) { | ||
if (pointer) _pointerPositionVector3.set(pointer.position.x, pointer.position.y, 0) | ||
const squaredDistance = buttonState.downPosition.distanceToSquared( | ||
pointer ? _pointerPositionVector3 : xrTransform?.position ?? Vector3_Zero | ||
) | ||
|
||
if (squaredDistance > DRAGGING_THRESHOLD) { | ||
buttonState.dragging = true | ||
} | ||
} | ||
|
||
//if not yet rotating, compare distance to drag threshold and begin if appropriate | ||
if (!buttonState.rotating) { | ||
const angleRadians = buttonState.downRotation!.angleTo( | ||
pointer ? Q_IDENTITY : xrTransform?.rotation ?? Q_IDENTITY | ||
) | ||
if (angleRadians > ROTATING_THRESHOLD) { | ||
buttonState.rotating = true | ||
} | ||
} | ||
} | ||
} else if (buttonState) { | ||
buttonState.up = true | ||
} | ||
} | ||
} | ||
export const setInputSources = (startEntity: Entity, inputSources: Entity[]) => { | ||
const inputEntity = getAncestorWithComponent(startEntity, InputComponent) | ||
if (!inputEntity) return | ||
const inputComponent = getComponent(inputEntity, InputComponent) | ||
|
||
for (const sinkEntityUUID of inputComponent.inputSinks) { | ||
const sinkEntity = sinkEntityUUID === 'Self' ? inputEntity : UUIDComponent.getEntityByUUID(sinkEntityUUID) //TODO why is this not sending input to my sinks | ||
const sinkInputComponent = getMutableComponent(sinkEntity, InputComponent) | ||
sinkInputComponent.inputSources.merge(inputSources) | ||
} | ||
} | ||
|
||
export function updatePointerDragging(pointerEntity: Entity, event: PointerEvent) { | ||
const inputSourceComponent = getOptionalComponent(pointerEntity, InputSourceComponent) | ||
if (!inputSourceComponent) return | ||
|
||
const state = inputSourceComponent.buttons as ButtonStateMap<typeof DefaultButtonAlias> | ||
|
||
let button = MouseButton.PrimaryClick | ||
if (event.type === 'pointermove') { | ||
if ((event as MouseEvent).button === 1) button = MouseButton.AuxiliaryClick | ||
else if ((event as MouseEvent).button === 2) button = MouseButton.SecondaryClick | ||
} | ||
const btn = state[button] | ||
if (btn && !btn.dragging) { | ||
const pointer = getOptionalComponent(pointerEntity, InputPointerComponent) | ||
|
||
if (btn.pressed && btn.downPosition) { | ||
//if not yet dragging, compare distance to drag threshold and begin if appropriate | ||
if (!btn.dragging) { | ||
pointer | ||
? _pointerPositionVector3.set(pointer.position.x, pointer.position.y, 0) | ||
: _pointerPositionVector3.copy(Vector3_Zero) | ||
const squaredDistance = btn.downPosition.distanceToSquared(_pointerPositionVector3) | ||
|
||
if (squaredDistance > DRAGGING_THRESHOLD) { | ||
btn.dragging = true | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
export function cleanupButton( | ||
key: string, | ||
buttons: ButtonStateMap<Partial<Record<string | number | symbol, ButtonState | undefined>>>, | ||
hasFocus: boolean | ||
) { | ||
const button = buttons[key] | ||
if (button?.down) button.down = false | ||
if (button?.up || !hasFocus) delete buttons[key] | ||
} | ||
|
||
export const redirectPointerEventsToXRUI = (cameraEntity: Entity, evt: PointerEvent) => { | ||
const pointerEntity = InputPointerComponent.getPointerByID(cameraEntity, evt.pointerId) | ||
const inputSource = getOptionalComponent(pointerEntity, InputSourceComponent) | ||
if (!inputSource) return | ||
for (const i of inputSource.intersections) { | ||
const entity = i.entity | ||
const xrui = getOptionalComponent(entity, XRUIComponent) | ||
if (!xrui) continue | ||
xrui.updateWorldMatrix(true, true) | ||
const raycaster = inputSource.raycaster | ||
const hit = xrui.hitTest(raycaster.ray) | ||
if (hit && hit.intersection.object.visible) { | ||
hit.target.dispatchEvent(new (evt.constructor as any)(evt.type, evt)) | ||
hit.target.focus() | ||
return | ||
} | ||
} | ||
} | ||
|
||
const nonSpatialInputSource = defineQuery([InputSourceComponent, Not(TransformComponent)]) | ||
|
||
export function assignInputSources( | ||
sourceEid: Entity, | ||
capturedEntity: Entity, | ||
data: HeuristicData, | ||
heuristic: HeuristicFunctions | ||
) { | ||
const isSpatialInput = hasComponent(sourceEid, TransformComponent) | ||
|
||
const intersectionData = new Set([] as IntersectionData[]) | ||
|
||
if (isSpatialInput) heuristic.raycastedInput(sourceEid, intersectionData, data, heuristic) | ||
|
||
const sortedIntersections = Array.from(intersectionData).sort((a, b) => { | ||
// - if a < b | ||
// + if a > b | ||
// 0 if equal | ||
const aNum = hasComponent(a.entity, TransformGizmoTagComponent) ? -1 : 0 | ||
const bNum = hasComponent(b.entity, TransformGizmoTagComponent) ? -1 : 0 | ||
//aNum - bNum : 0 if equal, -1 if a has tag and b doesn't, 1 if a doesnt have tag and b does | ||
return Math.sign(a.distance - b.distance) + (aNum - bNum) | ||
}) | ||
const sourceState = getMutableComponent(sourceEid, InputSourceComponent) | ||
|
||
//TODO check all inputSources sorted by distance list of InputComponents from query, probably similar to the spatialInputQuery | ||
//Proximity check ONLY if we have no raycast results, as it is always lower priority | ||
if ( | ||
capturedEntity === UndefinedEntity && | ||
sortedIntersections.length === 0 && | ||
!hasComponent(sourceEid, InputPointerComponent) | ||
) { | ||
heuristic.proximity(isSpatialInput, sourceEid, sortedIntersections, intersectionData) | ||
} | ||
|
||
const inputPointerComponent = getOptionalComponent(sourceEid, InputPointerComponent) | ||
if (inputPointerComponent) { | ||
sortedIntersections.push({ entity: inputPointerComponent.cameraEntity, distance: 0 }) | ||
} | ||
|
||
sourceState.intersections.set(sortedIntersections) | ||
|
||
const finalInputSources = Array.from(new Set([sourceEid, ...nonSpatialInputSource()])) | ||
|
||
//if we have a capturedEntity, only run on the capturedEntity, not the sortedIntersections | ||
if (capturedEntity !== UndefinedEntity) { | ||
ClientInputFunctions.setInputSources(capturedEntity, finalInputSources) | ||
} else { | ||
for (const intersection of sortedIntersections) { | ||
ClientInputFunctions.setInputSources(intersection.entity, finalInputSources) | ||
} | ||
} | ||
} | ||
|
||
export const ClientInputFunctions = { | ||
preventDefault, | ||
preventDefaultKeyDown, | ||
updateGamepadInput, | ||
setInputSources, | ||
updatePointerDragging, | ||
cleanupButton, | ||
redirectPointerEventsToXRUI, | ||
assignInputSources | ||
} | ||
export default ClientInputFunctions |
Oops, something went wrong.