Skip to content

Commit

Permalink
Added pan/pinch features for touch controls, fixed context menu controls
Browse files Browse the repository at this point in the history
for mobile, added keys to context menu react components.
  • Loading branch information
HunterBarclay committed Aug 26, 2024
1 parent 1c70994 commit 654389f
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 46 deletions.
2 changes: 1 addition & 1 deletion fission/src/systems/scene/CameraControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export class CustomOrbitControls extends CameraControls {

public interactionStart(start: InteractionStart) {
// If primary button, make Pointer be down
if (this._activePointerType < 0) {
if (this._activePointerType < start.interactionType) {
switch (start.interactionType) {
case PRIMARY_MOUSE_INTERACTION:
this._activePointerType = PRIMARY_MOUSE_INTERACTION
Expand Down
1 change: 1 addition & 0 deletions fission/src/systems/scene/SceneRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ class SceneRenderer extends WorldSystem {
if (PreferencesSystem.getGlobalPreference<boolean>("RenderSceneTags"))
new SceneOverlayEvent(SceneOverlayEventKey.UPDATE)

this._screenInteractionHandler.update(deltaT)
this._cameraControls.update(deltaT)

this._composer.render(deltaT)
Expand Down
185 changes: 145 additions & 40 deletions fission/src/systems/scene/ScreenInteractionHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import * as THREE from "three"

export const PRIMARY_MOUSE_INTERACTION = 0
export const MIDDLE_MOUSE_INTERACTION = 1
export const SECONDARY_MOUSE_INTERACTION = 2
Expand All @@ -26,8 +24,6 @@ export interface InteractionEnd {

/**
* Handler for all screen interactions with Mouse, Pen, and Touch controls.
*
* Mouse and Pen controls are stateless, whereas Touch controls are stateful
*/
class ScreenInteractionHandler {
private _primaryTouch: number | undefined
Expand All @@ -38,6 +34,9 @@ class ScreenInteractionHandler {
private _doubleTapInteraction: boolean = false
private _pointerPosition: [number, number] | undefined

private _lastPinchSeparation: number | undefined
private _lastPinchPosition: [number, number] | undefined;

private _pointerMove: (ev: PointerEvent) => void
private _wheelMove: (ev: WheelEvent) => void
private _pointerDown: (ev: PointerEvent) => void
Expand All @@ -52,6 +51,44 @@ class ScreenInteractionHandler {
public interactionMove: ((i: InteractionMove) => void) | undefined
public contextMenu: ((i: InteractionEnd) => void) | undefined

/**
* Caculates the distance between the primary and secondary touch positions.
*
* @returns Distance in pixels. Undefined if primary or secondary touch positions are undefined.
*/
public get pinchSeparation(): number | undefined {
if (this._primaryTouchPosition == undefined || this._secondaryTouchPosition == undefined) {
return undefined
}

const diff = [
this._primaryTouchPosition[0] - this._secondaryTouchPosition[0],
this._primaryTouchPosition[1] - this._secondaryTouchPosition[1]
]
return Math.sqrt((diff[0] ** 2) + (diff[1] ** 2))
}

/**
* Gets the midpoint between the primary and secondary touch positions.
*
* @returns Midpoint between primary and secondary touch positions. Undefined if touch positions are undefined.
*/
public get pinchPosition(): [number, number] | undefined {
if (this._primaryTouchPosition == undefined || this._secondaryTouchPosition == undefined) {
return undefined
}

return [
(this._primaryTouchPosition[0] + this._secondaryTouchPosition[0]) / 2.0,
(this._primaryTouchPosition[1] + this._secondaryTouchPosition[1]) / 2.0
]
}

/**
* Adds event listeners to dom element and wraps interaction events around original dom events.
*
* @param domElement Element to attach events to. Generally canvas for our application.
*/
public constructor(domElement: HTMLElement) {
this._domElement = domElement

Expand Down Expand Up @@ -79,6 +116,9 @@ class ScreenInteractionHandler {
this._domElement.addEventListener("touchmove", this._touchMove)
}

/**
* Disposes attached event handlers on the selected dom element.
*/
public dispose() {
this._domElement.removeEventListener("pointermove", this._pointerMove)
this._domElement.removeEventListener("wheel", this._wheelMove)
Expand All @@ -91,6 +131,17 @@ class ScreenInteractionHandler {
this._domElement.removeEventListener("touchmove", this._touchMove)
}

/**
* This method intercepts pointer move events and translates them into interaction move events accordingly. Pen and mouse movements have
* very minimal parsing, while touch movements are split into two categories. Either you have only a primary touch on the screen, in which
* it has, again, very minimal parsing. However, if there is a secondary touch, it simply updates the tracked positions, without dispatching
* any events. The touches positions are then translated into pinch and pan movements inside the update method.
*
* Pointer movements need to move half the recorded pointers width or height (depending on direction of movement) in order to begin updating
* the position data and dispatch events.
*
* @param e Pointer Event data.
*/
private pointerMove(e: PointerEvent) {
if (!this.interactionMove) {
return
Expand All @@ -115,54 +166,40 @@ class ScreenInteractionHandler {
} else {
if (e.pointerId == this._primaryTouch) {
if (!this._movementThresholdMet) {
const delta = [
Math.abs(e.clientX - this._primaryTouchPosition![0]),
Math.abs(e.clientY - this._primaryTouchPosition![1]),
]
if (delta[0] > e.width || delta[1] > e.height) {
if (this.checkMovementThreshold(this._primaryTouchPosition!, e)) {
this._movementThresholdMet = true
} else {
return
}
}

this._primaryTouchPosition = [e.clientX, e.clientY]
this.interactionMove({
interactionType: PRIMARY_MOUSE_INTERACTION,
movement: [e.movementX, e.movementY],
})

if (this._secondaryTouch == undefined) {
this.interactionMove({
interactionType: PRIMARY_MOUSE_INTERACTION,
movement: [e.movementX, e.movementY],
})
}
} else if (e.pointerId == this._secondaryTouch) {
if (!this._movementThresholdMet) {
const delta = [
Math.abs(e.clientX - this._secondaryTouchPosition![0]),
Math.abs(e.clientY - this._secondaryTouchPosition![1]),
]
if (delta[0] > e.width || delta[1] > e.height) {
if (this.checkMovementThreshold(this._secondaryTouchPosition!, e)) {
this._movementThresholdMet = true
} else {
return
}
}

this._secondaryTouchPosition = [e.clientX, e.clientY]
if (this._primaryTouchPosition) {
// This shouldn't happen, but you never know
const scalingDir = new THREE.Vector2(
this._secondaryTouchPosition[0],
this._secondaryTouchPosition[1]
).sub(new THREE.Vector2(this._primaryTouchPosition[0], this._primaryTouchPosition[1]))

const scale = scalingDir.normalize().dot(new THREE.Vector2(e.movementX, e.movementY))

this.interactionMove({
interactionType: PRIMARY_MOUSE_INTERACTION,
scale: scale * -0.06,
})
}
}
}
}

/**
* Intercepts wheel events and passes them along via the interaction move event.
*
* @param e Wheel event data.
*/
private wheelMove(e: WheelEvent) {
if (!this.interactionMove) {
return
Expand All @@ -171,24 +208,66 @@ class ScreenInteractionHandler {
this.interactionMove({ interactionType: -1, scale: e.deltaY * 0.01 })
}

/**
* The primary role of update within screen interaction handler is to parse the double touches on the screen into
* pinch and pan movement, then dispatch the data via the interaction move events.
*
* @param _ Unused deltaT variable.
*/
public update(_: number) {
if (this._secondaryTouch != undefined && this._movementThresholdMet) {
// Calculate current pinch position and separation
const pinchSep = this.pinchSeparation!
const pinchPos = this.pinchPosition!

// If previous ones exist, determine delta and send events
if (this._lastPinchPosition != undefined && this._lastPinchSeparation != undefined) {
this.interactionMove?.({
interactionType: SECONDARY_MOUSE_INTERACTION,
scale: (pinchSep - this._lastPinchSeparation) * -0.03,
})

this.interactionMove?.({
interactionType: SECONDARY_MOUSE_INTERACTION,
movement: [
pinchPos[0] - this._lastPinchPosition[0],
pinchPos[1] - this._lastPinchPosition[1]
]
})
}

// Load current into last
this._lastPinchSeparation = pinchSep
this._lastPinchPosition = pinchPos
}
}

private pointerDown(e: PointerEvent) {
if (!this.interactionStart) {
return
}

if (e.pointerType == "touch") {
if (!this._primaryTouch) {
if (this._primaryTouch == undefined) {
this._primaryTouch = e.pointerId
this._primaryTouchPosition = [e.clientX, e.clientY]
this._movementThresholdMet = false
this.interactionStart({
interactionType: PRIMARY_MOUSE_INTERACTION,
position: this._primaryTouchPosition,
})
} else if (!this._secondaryTouch) {
} else if (this._secondaryTouch == undefined) {
this._secondaryTouch = e.pointerId
this._secondaryTouchPosition = [e.clientX, e.clientY]
this._doubleTapInteraction = true

this._lastPinchSeparation = undefined
this._lastPinchPosition = undefined

this.interactionStart({
interactionType: SECONDARY_MOUSE_INTERACTION,
position: this._secondaryTouchPosition,
})
}
} else {
if (e.button >= 0 && e.button <= 2) {
Expand All @@ -211,21 +290,31 @@ class ScreenInteractionHandler {
if (e.pointerId == this._primaryTouch) {
this._primaryTouch = this._secondaryTouch
this._secondaryTouch = undefined
if (!this._primaryTouch) {
const end: InteractionEnd = {
if (this._primaryTouch != undefined) {
this.interactionEnd({
interactionType: SECONDARY_MOUSE_INTERACTION,
position: [e.clientX, e.clientY],
})
} else {
this.interactionEnd({
interactionType: PRIMARY_MOUSE_INTERACTION,
position: [e.clientX, e.clientY],
}
this.interactionEnd(end)
})
if (this._doubleTapInteraction && !this._movementThresholdMet && this.contextMenu) {
this.contextMenu(end)
this.contextMenu({
interactionType: -1,
position: this.pinchPosition!,
})
}
this._doubleTapInteraction = false
}
// Reset continuous tracking
} else if (e.pointerId == this._secondaryTouch) {
this._secondaryTouch = undefined
// Reset continuous tracking
this.interactionEnd({
interactionType: SECONDARY_MOUSE_INTERACTION,
position: [e.clientX, e.clientY],
})
}
} else {
if (e.button >= 0 && e.button <= 2) {
Expand All @@ -240,6 +329,22 @@ class ScreenInteractionHandler {
}
}
}

/**
* Checks if a given position has moved from the origin given a specified threshold.
*
* @param origin Origin to move away from.
* @param ptr Pointer data.
* @returns True if latest is outside of the box around origin with sides the length of thresholds * 2.
*/
private checkMovementThreshold(origin: [number, number], ptr: PointerEvent): boolean {
const delta = [
Math.abs(ptr.clientX - origin[0]),
Math.abs(ptr.clientY - origin[1]),
]

return delta[0] > ptr.width / 2.0 || delta[1] > ptr.height / 2.0
}
}

export default ScreenInteractionHandler
9 changes: 4 additions & 5 deletions fission/src/ui/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ interface ContextMenuStateData {
function ContextMenu() {
const [state, setState] = useState<ContextMenuStateData | undefined>(undefined)

// const { currentTheme, themes } = useTheme()
// const theme = useMemo(() => themes[currentTheme], [currentTheme, themes])

useEffect(() => {
const func = (e: ContextSupplierEvent) => {
setState({ data: e.data, location: [e.mousePosition[0], e.mousePosition[1]] })
Expand All @@ -32,7 +29,7 @@ function ContextMenu() {
<></>
) : (
<Box
id="CANCEL"
key="CANCEL"
component="div"
display="flex"
sx={{
Expand All @@ -46,7 +43,7 @@ function ContextMenu() {
onContextMenu={e => e.preventDefault()}
>
<Box
id="MENU"
key="MENU"
component="div"
display="flex"
sx={{
Expand All @@ -64,6 +61,7 @@ function ContextMenu() {
onPointerDown={e => e.stopPropagation()}
>
<Box
key="CONTEXT-HEADER"
component="div"
display="flex"
sx={{
Expand All @@ -77,6 +75,7 @@ function ContextMenu() {
</Box>
{state.data.items.map(x => (
<Button
key={x.name}
size={ButtonSize.Small}
className={"w-full text-sm"}
value={x.name}
Expand Down

0 comments on commit 654389f

Please sign in to comment.