diff --git a/fission/package.json b/fission/package.json index 3b5df05cd..0f97b9170 100644 --- a/fission/package.json +++ b/fission/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "init": "(bun run assetpack && bun run playwright:install) || (npm run assetpack && npm run playwright:install)", - "dev": "vite --open", + "dev": "vite --open --host", "build": "tsc && vite build", "build:prod": "tsc && vite build --base=/fission/ --outDir dist/prod", "build:dev": "tsc && vite build --base=/fission-closed/ --outDir dist/dev", @@ -56,6 +56,7 @@ "@types/three": "^0.160.0", "@typescript-eslint/eslint-plugin": "^7.0.2", "@typescript-eslint/parser": "^7.0.2", + "@vitejs/plugin-basic-ssl": "^1.1.0", "@vitejs/plugin-react": "^4.0.3", "@vitejs/plugin-react-swc": "^3.5.0", "autoprefixer": "^10.4.14", diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index badb33a28..52475c3f8 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -60,6 +60,7 @@ import AnalyticsConsent from "./ui/components/AnalyticsConsent.tsx" import PreferencesSystem from "./systems/preferences/PreferencesSystem.ts" import APSManagementModal from "./ui/modals/APSManagementModal.tsx" import ConfigurePanel from "./ui/panels/configuring/assembly-config/ConfigurePanel.tsx" +import TouchControls from "./ui/components/TouchControls.tsx" const worker = new Lazy(() => new WPILibWSWorker()) @@ -164,10 +165,11 @@ function Synthesis() { + {panelElements.length > 0 && panelElements} {modalElement && ( -
+
{modalElement}
)} diff --git a/fission/src/index.css b/fission/src/index.css index f3c04e2d1..5ab64fd0e 100644 --- a/fission/src/index.css +++ b/fission/src/index.css @@ -50,7 +50,7 @@ body { display: flex; place-items: center; min-width: 320px; - min-height: 100vh; + /* min-height: 100vh; */ } h1 { diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 2804f3ae1..664dd3510 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -21,6 +21,7 @@ import ScoringZoneSceneObject from "./ScoringZoneSceneObject" import { SceneOverlayTag } from "@/ui/components/SceneOverlayEvents" import { ProgressHandle } from "@/ui/components/ProgressNotificationData" import SynthesisBrain from "@/systems/simulation/synthesis_brain/SynthesisBrain" +import { MainHUD_AddToast } from "@/ui/components/MainHUD" const DEBUG_BODIES = false @@ -186,7 +187,7 @@ class MirabufSceneObject extends SceneObject { * This block of code should only be executed if the transform gizmo exists. */ if (this._transformGizmos) { - if (InputSystem.isKeyPressed("Enter")) { + if (InputSystem.isKeyPressed("ConfirmAssemblyButton") || InputSystem.isKeyPressed("Enter")) { // confirming placement of the mirabuf object this.DisableTransformControls() return @@ -397,6 +398,7 @@ class MirabufSceneObject extends SceneObject { this._transformGizmos?.RemoveGizmos() this._transformGizmos = undefined this.EnablePhysics() + World.SceneRenderer.isPlacingAssembly = false } /** @@ -440,6 +442,14 @@ export async function CreateMirabuf( assembly: mirabuf.Assembly, progressHandle?: ProgressHandle ): Promise { + // Cancel is there is another assembly being placed + console.log(World.SceneRenderer.isPlacingAssembly) + if (World.SceneRenderer.isPlacingAssembly) { + MainHUD_AddToast("error", "Error Placing Assembly.", "Place assembly before spawning another.") + return + } else { + World.SceneRenderer.isPlacingAssembly = true + } const parser = new MirabufParser(assembly, progressHandle) if (parser.maxErrorSeverity >= ParseErrorSeverity.Unimportable) { console.error(`Assembly Parser produced significant errors for '${assembly.info!.name!}'`) diff --git a/fission/src/systems/input/DefaultInputs.ts b/fission/src/systems/input/DefaultInputs.ts index 4a6a87a68..1122c7372 100644 --- a/fission/src/systems/input/DefaultInputs.ts +++ b/fission/src/systems/input/DefaultInputs.ts @@ -1,3 +1,4 @@ +import { TouchControlsAxes } from "@/ui/components/TouchControls" import { InputScheme } from "./InputSchemeManager" import { AxisInput, ButtonInput, EmptyModifierState } from "./InputSystem" @@ -9,6 +10,7 @@ class DefaultInputs { descriptiveName: "WASD", customized: false, usesGamepad: false, + usesTouchControls: false, inputs: [ new AxisInput("arcadeDrive", "KeyW", "KeyS"), new AxisInput("arcadeTurn", "KeyD", "KeyA"), @@ -16,36 +18,96 @@ class DefaultInputs { new ButtonInput("intake", "KeyE"), new ButtonInput("eject", "KeyQ"), - new AxisInput("joint 1", "Digit1", "Digit1", -1, false, false, -1, -1, EmptyModifierState, { - ctrl: false, - alt: false, - shift: true, - meta: false, - }), - new AxisInput("joint 2", "Digit2", "Digit2", -1, false, false, -1, -1, EmptyModifierState, { - ctrl: false, - alt: false, - shift: true, - meta: false, - }), - new AxisInput("joint 3", "Digit3", "Digit3", -1, false, false, -1, -1, EmptyModifierState, { - ctrl: false, - alt: false, - shift: true, - meta: false, - }), - new AxisInput("joint 4", "Digit4", "Digit4", -1, false, false, -1, -1, EmptyModifierState, { - ctrl: false, - alt: false, - shift: true, - meta: false, - }), - new AxisInput("joint 5", "Digit5", "Digit5", -1, false, false, -1, -1, EmptyModifierState, { - ctrl: false, - alt: false, - shift: true, - meta: false, - }), + new AxisInput( + "joint 1", + "Digit1", + "Digit1", + -1, + false, + false, + -1, + -1, + TouchControlsAxes.NONE, + EmptyModifierState, + { + ctrl: false, + alt: false, + shift: true, + meta: false, + } + ), + new AxisInput( + "joint 2", + "Digit2", + "Digit2", + -1, + false, + false, + -1, + -1, + TouchControlsAxes.NONE, + EmptyModifierState, + { + ctrl: false, + alt: false, + shift: true, + meta: false, + } + ), + new AxisInput( + "joint 3", + "Digit3", + "Digit3", + -1, + false, + false, + -1, + -1, + TouchControlsAxes.NONE, + EmptyModifierState, + { + ctrl: false, + alt: false, + shift: true, + meta: false, + } + ), + new AxisInput( + "joint 4", + "Digit4", + "Digit4", + -1, + false, + false, + -1, + -1, + TouchControlsAxes.NONE, + EmptyModifierState, + { + ctrl: false, + alt: false, + shift: true, + meta: false, + } + ), + new AxisInput( + "joint 5", + "Digit5", + "Digit5", + -1, + false, + false, + -1, + -1, + TouchControlsAxes.NONE, + EmptyModifierState, + { + ctrl: false, + alt: false, + shift: true, + meta: false, + } + ), new AxisInput("joint 6"), new AxisInput("joint 7"), new AxisInput("joint 8"), @@ -61,6 +123,7 @@ class DefaultInputs { descriptiveName: "Arrow Keys", customized: false, usesGamepad: false, + usesTouchControls: false, inputs: [ new AxisInput("arcadeDrive", "ArrowUp", "ArrowDown"), new AxisInput("arcadeTurn", "ArrowRight", "ArrowLeft"), @@ -68,36 +131,96 @@ class DefaultInputs { new ButtonInput("intake", "Semicolon"), new ButtonInput("eject", "KeyL"), - new AxisInput("joint 1", "Slash", "Slash", -1, false, false, -1, -1, EmptyModifierState, { - ctrl: true, - alt: false, - shift: false, - meta: false, - }), - new AxisInput("joint 2", "Period", "Period", -1, false, false, -1, -1, EmptyModifierState, { - ctrl: true, - alt: false, - shift: false, - meta: false, - }), - new AxisInput("joint 3", "Comma", "Comma", -1, false, false, -1, -1, EmptyModifierState, { - ctrl: true, - alt: false, - shift: false, - meta: false, - }), - new AxisInput("joint 4", "KeyM", "KeyM", -1, false, false, -1, -1, EmptyModifierState, { - ctrl: true, - alt: false, - shift: false, - meta: false, - }), - new AxisInput("joint 5", "KeyN", "KeyN", -1, false, false, -1, -1, EmptyModifierState, { - ctrl: true, - alt: false, - shift: false, - meta: false, - }), + new AxisInput( + "joint 1", + "Slash", + "Slash", + -1, + false, + false, + -1, + -1, + TouchControlsAxes.NONE, + EmptyModifierState, + { + ctrl: true, + alt: false, + shift: false, + meta: false, + } + ), + new AxisInput( + "joint 2", + "Period", + "Period", + -1, + false, + false, + -1, + -1, + TouchControlsAxes.NONE, + EmptyModifierState, + { + ctrl: true, + alt: false, + shift: false, + meta: false, + } + ), + new AxisInput( + "joint 3", + "Comma", + "Comma", + -1, + false, + false, + -1, + -1, + TouchControlsAxes.NONE, + EmptyModifierState, + { + ctrl: true, + alt: false, + shift: false, + meta: false, + } + ), + new AxisInput( + "joint 4", + "KeyM", + "KeyM", + -1, + false, + false, + -1, + -1, + TouchControlsAxes.NONE, + EmptyModifierState, + { + ctrl: true, + alt: false, + shift: false, + meta: false, + } + ), + new AxisInput( + "joint 5", + "KeyN", + "KeyN", + -1, + false, + false, + -1, + -1, + TouchControlsAxes.NONE, + EmptyModifierState, + { + ctrl: true, + alt: false, + shift: false, + meta: false, + } + ), new AxisInput("joint 6"), new AxisInput("joint 7"), new AxisInput("joint 8"), @@ -113,6 +236,7 @@ class DefaultInputs { descriptiveName: "Full Controller", customized: false, usesGamepad: true, + usesTouchControls: false, inputs: [ new AxisInput("arcadeDrive", "", "", 1, true), new AxisInput("arcadeTurn", "", "", 2, false), @@ -141,6 +265,7 @@ class DefaultInputs { descriptiveName: "Left Stick", customized: false, usesGamepad: true, + usesTouchControls: false, inputs: [ new AxisInput("arcadeDrive", "", "", 1, true), new AxisInput("arcadeTurn", "", "", 0, false), @@ -168,6 +293,7 @@ class DefaultInputs { descriptiveName: "Right Stick", customized: false, usesGamepad: true, + usesTouchControls: false, inputs: [ new AxisInput("arcadeDrive", "", "", 3, true), new AxisInput("arcadeTurn", "", "", 2, false), @@ -189,6 +315,40 @@ class DefaultInputs { } } + public static brandon = () => { + return { + schemeName: "Brandon", + descriptiveName: "Touch Controls", + customized: false, + usesGamepad: false, + usesTouchControls: true, + inputs: [ + new AxisInput( + "arcadeDrive", + "", + "", + undefined, + undefined, + undefined, + undefined, + undefined, + TouchControlsAxes.LEFT_Y + ), + new AxisInput( + "arcadeTurn", + "", + "", + undefined, + undefined, + undefined, + undefined, + undefined, + TouchControlsAxes.RIGHT_X + ), + ], + } + } + /** @returns {InputScheme[]} New copies of the default input schemes without reference to any others. */ public static get defaultInputCopies(): InputScheme[] { return [ @@ -197,6 +357,7 @@ class DefaultInputs { DefaultInputs.jax(), DefaultInputs.hunter(), DefaultInputs.carmela(), + DefaultInputs.brandon(), ] } @@ -207,6 +368,7 @@ class DefaultInputs { descriptiveName: "", customized: true, usesGamepad: false, + usesTouchControls: false, inputs: [ new AxisInput("arcadeDrive"), new AxisInput("arcadeTurn"), diff --git a/fission/src/systems/input/InputSchemeManager.ts b/fission/src/systems/input/InputSchemeManager.ts index 4e2d66d73..b855092d8 100644 --- a/fission/src/systems/input/InputSchemeManager.ts +++ b/fission/src/systems/input/InputSchemeManager.ts @@ -8,6 +8,7 @@ export type InputScheme = { descriptiveName: string customized: boolean usesGamepad: boolean + usesTouchControls: boolean inputs: Input[] } @@ -58,6 +59,7 @@ class InputSchemeManager { rawAxis.useGamepadButtons, rawAxis.posGamepadButton, rawAxis.negGamepadButton, + rawAxis.touchControlAxis, rawAxis.posKeyModifiers, rawAxis.negKeyModifiers ) diff --git a/fission/src/systems/input/InputSystem.ts b/fission/src/systems/input/InputSystem.ts index 509f46760..9cddbf2fe 100644 --- a/fission/src/systems/input/InputSystem.ts +++ b/fission/src/systems/input/InputSystem.ts @@ -1,3 +1,5 @@ +import { TouchControlsAxes } from "@/ui/components/TouchControls" +import Joystick from "../scene/Joystick" import WorldSystem from "../WorldSystem" import { InputScheme } from "./InputSchemeManager" @@ -18,8 +20,8 @@ abstract class Input { this.inputName = inputName } - /** @returns {number} a number between -1 and 1 for this input. */ - abstract getValue(useGamepad: boolean): number + // Returns the current value of the input. Range depends on input type + abstract getValue(useGamepad: boolean, useTouchControls: boolean): number } /** Represents any user input that is a single true/false button. */ @@ -67,6 +69,7 @@ class AxisInput extends Input { public negKeyModifiers: ModifierState public gamepadAxisNumber: number + public touchControlAxis: TouchControlsAxes public joystickInverted: boolean public useGamepadButtons: boolean public posGamepadButton: number @@ -95,6 +98,7 @@ class AxisInput extends Input { useGamepadButtons?: boolean, posGamepadButton?: number, negGamepadButton?: number, + touchControlAxis?: TouchControlsAxes, posKeyModifiers?: ModifierState, negKeyModifiers?: ModifierState ) { @@ -106,6 +110,7 @@ class AxisInput extends Input { this.negKeyModifiers = negKeyModifiers ?? EmptyModifierState this.gamepadAxisNumber = gamepadAxisNumber ?? -1 + this.touchControlAxis = touchControlAxis ?? TouchControlsAxes.NONE this.joystickInverted = joystickInverted ?? false this.useGamepadButtons = useGamepadButtons ?? false @@ -118,7 +123,7 @@ class AxisInput extends Input { * @returns {number} KEYBOARD: 1 if positive pressed, -1 if negative pressed, or 0 if none or both are pressed. * @returns {number} GAMEPAD: a number between -1 and 1 with a deadband in the middle. */ - getValue(useGamepad: boolean): number { + getValue(useGamepad: boolean, useTouchControls: boolean): number { if (useGamepad) { // Gamepad joystick axis if (!this.useGamepadButtons) @@ -131,6 +136,10 @@ class AxisInput extends Input { ) } + if (useTouchControls) { + return InputSystem.getTouchControlsAxis(this.touchControlAxis) * (this.joystickInverted ? -1 : 1) + } + // Keyboard button axis return ( (InputSystem.isKeyPressed(this.posKeyCode, this.posKeyModifiers) ? 1 : 0) - @@ -152,6 +161,9 @@ class InputSystem extends WorldSystem { private static _gpIndex: number | null public static gamepad: Gamepad | null + private static leftJoystick: Joystick + private static rightJoystick: Joystick + /** Maps a brain index to an input scheme. */ public static brainIndexSchemeMap: Map = new Map() @@ -171,6 +183,17 @@ class InputSystem extends WorldSystem { this.gamepadDisconnected = this.gamepadDisconnected.bind(this) window.addEventListener("gamepaddisconnected", this.gamepadDisconnected) + window.addEventListener("touchcontrolsloaded", () => { + InputSystem.leftJoystick = new Joystick( + document.getElementById("joystick-base-left")!, + document.getElementById("joystick-stick-left")! + ) + InputSystem.rightJoystick = new Joystick( + document.getElementById("joystick-base-right")!, + document.getElementById("joystick-stick-right")! + ) + }) + // Initialize an event that's triggered when the user exits/enters the page document.addEventListener("visibilitychange", () => { if (document.hidden) this.clearKeyData() @@ -270,7 +293,7 @@ class InputSystem extends WorldSystem { if (targetScheme == null || targetInput == null) return 0 - return targetInput.getValue(targetScheme.usesGamepad) + return targetInput.getValue(targetScheme.usesGamepad, targetScheme.usesTouchControls) } /** @@ -319,6 +342,18 @@ class InputSystem extends WorldSystem { return button.pressed } + + // Returns a number between -1 and 1 from the touch controls + public static getTouchControlsAxis(axisType: TouchControlsAxes): number { + let value: number + + if (axisType === TouchControlsAxes.LEFT_Y) value = -InputSystem.leftJoystick.y + else if (axisType === TouchControlsAxes.RIGHT_X) value = InputSystem.rightJoystick.x + else if (axisType === TouchControlsAxes.RIGHT_Y) value = -InputSystem.rightJoystick.y + else value = InputSystem.leftJoystick.x + + return value! + } } export default InputSystem diff --git a/fission/src/systems/preferences/PreferenceTypes.ts b/fission/src/systems/preferences/PreferenceTypes.ts index 8c99e60d1..8d80bb92f 100644 --- a/fission/src/systems/preferences/PreferenceTypes.ts +++ b/fission/src/systems/preferences/PreferenceTypes.ts @@ -14,6 +14,7 @@ export type GlobalPreference = | "RenderSceneTags" | "RenderScoreboard" | "SubsystemGravity" + | "TouchControls" export const RobotPreferencesKey: string = "Robots" export const FieldPreferencesKey: string = "Fields" @@ -34,6 +35,7 @@ export const DefaultGlobalPreferences: { [key: string]: unknown } = { RenderSceneTags: true, RenderScoreboard: true, SubsystemGravity: false, + TouchControls: false, } export type QualitySetting = "Low" | "Medium" | "High" diff --git a/fission/src/systems/scene/Joystick.ts b/fission/src/systems/scene/Joystick.ts new file mode 100644 index 000000000..3dc6b059d --- /dev/null +++ b/fission/src/systems/scene/Joystick.ts @@ -0,0 +1,78 @@ +import { MAX_JOYSTICK_RADIUS } from "@/ui/components/TouchControls" + +class Joystick { + private baseElement: HTMLElement + private stickElement: HTMLElement + private stickPosition: { x: number; y: number } = { x: 0, y: 0 } + private baseRect: DOMRect | null = null + private activePointerId: number | null = null // Track the active pointer ID + + public get x() { + return this.stickPosition.x / MAX_JOYSTICK_RADIUS + } + + public get y() { + return this.stickPosition.y / MAX_JOYSTICK_RADIUS + } + + constructor(baseElement: HTMLElement, stickElement: HTMLElement) { + this.baseElement = baseElement + this.stickElement = stickElement + + this.initialize() + } + + private initialize() { + this.baseElement.addEventListener("pointerdown", this.onPointerDown.bind(this)) + document.addEventListener("pointermove", this.onPointerMove.bind(this)) + document.addEventListener("pointerup", this.onPointerUp.bind(this)) + } + + private onPointerDown(event: PointerEvent) { + this.baseRect = this.baseElement.getBoundingClientRect() + this.activePointerId = event.pointerId + this.updateStickPosition(event.clientX, event.clientY) + } + + private onPointerMove(event: PointerEvent) { + if (this.activePointerId !== event.pointerId || !this.baseRect) return // Ensure only the initiating pointer controls the joystick + this.updateStickPosition(event.clientX, event.clientY) + } + + private onPointerUp(event: PointerEvent) { + if (this.activePointerId !== event.pointerId) return + this.stickPosition = { x: 0, y: 0 } + this.stickElement.style.transform = `translate(-50%, -50%)` + this.baseRect = null + } + + private updateStickPosition(clientX: number, clientY: number) { + if (!this.baseRect) return + + const w = this.baseRect.right - this.baseRect.left + const h = this.baseRect.bottom - this.baseRect.top + const x = clientX - (this.baseRect.left + w / 2) + const y = clientY - (this.baseRect.top + h / 2) + + // Calculate the distance from the center + const distance = Math.sqrt(x * x + y * y) + + // If the distance exceeds maxDistance, constrain it + if (distance > MAX_JOYSTICK_RADIUS) { + const angle = Math.atan2(y, x) + this.stickPosition.x = Math.cos(angle) * MAX_JOYSTICK_RADIUS + this.stickPosition.y = Math.sin(angle) * MAX_JOYSTICK_RADIUS + } else { + this.stickPosition.x = x + this.stickPosition.y = y + } + + this.stickElement.style.transform = `translate(${this.stickPosition.x - MAX_JOYSTICK_RADIUS / 2}px, ${this.stickPosition.y - MAX_JOYSTICK_RADIUS / 2}px)` + } + + public GetPosition(axis: "x" | "y") { + return this.stickPosition[axis] / MAX_JOYSTICK_RADIUS + } +} + +export default Joystick diff --git a/fission/src/systems/scene/SceneRenderer.ts b/fission/src/systems/scene/SceneRenderer.ts index 205aa4434..92b491705 100644 --- a/fission/src/systems/scene/SceneRenderer.ts +++ b/fission/src/systems/scene/SceneRenderer.ts @@ -15,6 +15,7 @@ import Jolt from "@barclah/jolt-physics" import { PixelSpaceCoord, SceneOverlayEvent, SceneOverlayEventKey } from "@/ui/components/SceneOverlayEvents" import PreferencesSystem from "../preferences/PreferencesSystem" import { CSM } from "three/examples/jsm/csm/CSM.js" +import { TouchControlsEvent, TouchControlsEventKeys } from "@/ui/components/TouchControls" const CLEAR_COLOR = 0x121212 const GROUND_COLOR = 0x4066c7 @@ -35,6 +36,8 @@ class SceneRenderer extends WorldSystem { private _orbitControls: OrbitControls private _transformControls: Map // maps all rendered transform controls to their size + private _isPlacingAssembly: boolean = false + private _light: THREE.DirectionalLight | CSM | undefined public get sceneObjects() { @@ -53,6 +56,15 @@ class SceneRenderer extends WorldSystem { return this._renderer } + public get isPlacingAssembly() { + return this._isPlacingAssembly + } + + public set isPlacingAssembly(value: boolean) { + new TouchControlsEvent(TouchControlsEventKeys.PLACE_BUTTON, value) + this._isPlacingAssembly = value + } + public constructor() { super() diff --git a/fission/src/ui/components/Checkbox.tsx b/fission/src/ui/components/Checkbox.tsx index 82dd9be3c..09161bdaf 100644 --- a/fission/src/ui/components/Checkbox.tsx +++ b/fission/src/ui/components/Checkbox.tsx @@ -36,7 +36,7 @@ const Checkbox: React.FC = ({ hideLabel, onClick, tooltipText, -}) => { +}: CheckboxProps): JSX.Element => { const [state] = useState(defaultState) return ( = ({ }, }} defaultChecked={stateOverride != null ? undefined : state} + // checked={state} id="checkbox-switch" /> diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index f7aca002f..c834e30ce 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -10,6 +10,7 @@ import { UserIcon } from "./UserIcon" import { ButtonIcon, SynthesisIcons } from "./StyledComponents" import { Button } from "@mui/base" import { Box } from "@mui/material" +import { TouchControlsEvent, TouchControlsEventKeys } from "./TouchControls" type ButtonProps = { value: string @@ -57,6 +58,8 @@ const MainHUD: React.FC = () => { const { addToast } = useToastContext() const [isOpen, setIsOpen] = useState(false) + const touchCompatibility = matchMedia("(hover: none)").matches + MainHUD_AddToast = addToast const [userInfo, setUserInfo] = useState(APS.userInfo) @@ -156,6 +159,15 @@ const MainHUD: React.FC = () => { openPanel("debug") }} /> + {touchCompatibility ? ( + new TouchControlsEvent(TouchControlsEventKeys.JOYSTICK)} + /> + ) : ( + <> + )} {userInfo ? ( (null) + + const [isPlaceButtonVisible, setIsPlaceButtonVisible] = useState(false) + const [isJoystickVisible, setIsJoystickVisible] = useState( + PreferencesSystem.getGlobalPreference("TouchControls") + ) + + useEffect(() => { + const handlePlaceButtonEvent = (e: Event) => { + setIsPlaceButtonVisible((e as TouchControlsEvent).value!) + } + + const handleJoystickEvent = () => { + PreferencesSystem.setGlobalPreference("TouchControls", !isJoystickVisible) + PreferencesSystem.savePreferences() + setIsJoystickVisible(!isJoystickVisible) + } + + TouchControlsEvent.Listen(TouchControlsEventKeys.PLACE_BUTTON, handlePlaceButtonEvent) + TouchControlsEvent.Listen(TouchControlsEventKeys.JOYSTICK, handleJoystickEvent) + + window.dispatchEvent(new Event("touchcontrolsloaded")) + + return () => { + TouchControlsEvent.RemoveListener(TouchControlsEventKeys.PLACE_BUTTON, handlePlaceButtonEvent) + TouchControlsEvent.RemoveListener(TouchControlsEventKeys.JOYSTICK, handleJoystickEvent) + } + }, [isJoystickVisible, isPlaceButtonVisible]) + + /** simulates an enter key press and release within a 100 millisecond succession */ + const PlaceMirabufAssembly = useCallback(() => { + if (inputRef.current) { + const pressEvent = new KeyboardEvent("keydown", { + key: "ConfirmAssemblyButton", + code: "ConfirmAssemblyButton", + bubbles: true, + }) + inputRef.current.dispatchEvent(pressEvent) + + setTimeout(() => { + const releaseEvent = new KeyboardEvent("keyup", { + key: "ConfirmAssemblyButton", + code: "ConfirmAssemblyButton", + bubbles: true, + }) + inputRef.current?.dispatchEvent(releaseEvent) + }, 100) + } + }, []) + + return ( +
+ +
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+ ) +} + +export default TouchControls + +export const MAX_JOYSTICK_RADIUS: number = 55 + +export const enum TouchControlsEventKeys { + PLACE_BUTTON = "PlaceButtonEvent", + JOYSTICK = "JoystickEvent", +} + +export class TouchControlsEvent extends Event { + public value: boolean | undefined + + constructor(eventKey: TouchControlsEventKeys, value?: boolean) { + super(eventKey) + + if (value) this.value = value + + window.dispatchEvent(this) + } + + public static Listen(eventKey: TouchControlsEventKeys, func: (e: Event) => void) { + window.addEventListener(eventKey, func) + } + + public static RemoveListener(eventKey: TouchControlsEventKeys, func: (e: Event) => void) { + window.removeEventListener(eventKey, func) + } +} + +/** Notates the left and right joysticks with their x and y axis */ +export const enum TouchControlsAxes { + NONE, + LEFT_X, + LEFT_Y, + RIGHT_X, + RIGHT_Y, +} diff --git a/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/ConfigureSchemeInterface.tsx b/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/ConfigureSchemeInterface.tsx index 220191cab..75885c9d4 100644 --- a/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/ConfigureSchemeInterface.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/ConfigureSchemeInterface.tsx @@ -12,6 +12,7 @@ interface ConfigSchemeProps { /** Interface to configure a specific input scheme */ const ConfigureSchemeInterface: React.FC = ({ selectedScheme }) => { const [useGamepad, setUseGamepad] = useState(selectedScheme.usesGamepad) + const [useTouchControls, setUseTouchControls] = useState(selectedScheme.usesTouchControls) const scrollRef = useRef(null) const saveEvent = useCallback(() => { @@ -58,6 +59,15 @@ const ConfigureSchemeInterface: React.FC = ({ selectedScheme }} tooltipText="Supported controllers: Xbox one, Xbox 360." /> + { + setUseTouchControls(val) + selectedScheme.usesTouchControls = val + }} + tooltipText="Enable on-screen touch controls (only for mobile devices)." + /> {/* Scroll view for inputs */} @@ -68,6 +78,7 @@ const ConfigureSchemeInterface: React.FC = ({ selectedScheme key={i.inputName} input={i} useGamepad={useGamepad} + useTouchControls={useTouchControls} onInputChanged={() => { selectedScheme.customized = true }} diff --git a/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/EditInputInterface.tsx b/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/EditInputInterface.tsx index 33ef12b7a..790048cb5 100644 --- a/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/EditInputInterface.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/EditInputInterface.tsx @@ -55,6 +55,7 @@ const gamepadButtons: string[] = [ ] const gamepadAxes: string[] = ["N/A", "Left X", "Left Y", "Right X", "Right Y"] +const touchControlsAxes: string[] = ["N/A", "Left X", "Left Y", "Right X", "Right Y"] // Converts a key code to displayable character (ex: KeyA -> "A") const keyCodeToCharacter = (code: string) => { @@ -87,12 +88,14 @@ const transformKeyName = (keyCode: string, keyModifiers: ModifierState) => { interface EditInputProps { input: Input useGamepad: boolean + useTouchControls: boolean onInputChanged: () => void } -const EditInputInterface: React.FC = ({ input, useGamepad, onInputChanged }) => { +const EditInputInterface: React.FC = ({ input, useGamepad, useTouchControls, onInputChanged }) => { const [selectedInput, setSelectedInput] = useState("") const [chosenGamepadAxis, setChosenGamepadAxis] = useState(-1) + const [chosenTouchControlsAxis, setChosenTouchControlsAxis] = useState(-1) const [chosenKey, setChosenKey] = useState("") const [modifierState, setModifierState] = useState(EmptyModifierState) const [chosenButton, setChosenButton] = useState(-1) @@ -305,18 +308,37 @@ const EditInputInterface: React.FC = ({ input, useGamepad, onInp ) } + const TouchControlsAxisSelection = () => { + if (!(input instanceof AxisInput)) throw new Error("Input not axis type") + + return ( + <> + + + { + setSelectedInput(input.inputName) + setChosenTouchControlsAxis(touchControlsAxes.indexOf(value)) + }} + /> + + + ) + } + /** Show the correct selection mode based on input type and how it's configured */ const inputConfig = () => { - if (!useGamepad) { - // Keyboard button - if (input instanceof ButtonInput) { - return KeyboardButtonSelection() - } - // Keyboard Axis - else if (input instanceof AxisInput) { - return KeyboardAxisSelection() - } - } else { + if (useGamepad) { // Joystick Button if (input instanceof ButtonInput) { return JoystickButtonSelection() @@ -352,6 +374,33 @@ const EditInputInterface: React.FC = ({ input, useGamepad, onInp
) } + } else if (useTouchControls) { + // here + if (input instanceof AxisInput) { + return ( +
+ {TouchControlsAxisSelection()} + {/* // Button to invert the joystick axis */} + { + input.joystickInverted = val + }} + /> + +
+ ) + } + } else { + // Keyboard button + if (input instanceof ButtonInput) { + return KeyboardButtonSelection() + } + // Keyboard Axis + else if (input instanceof AxisInput) { + return KeyboardAxisSelection() + } } } @@ -375,7 +424,7 @@ const EditInputInterface: React.FC = ({ input, useGamepad, onInp /** Input detection for setting inputs */ useEffect(() => { // // Assign keyboard inputs when a key is pressed - if (!useGamepad && selectedInput && chosenKey) { + if (!useGamepad && !useTouchControls && selectedInput && chosenKey) { if (selectedInput.startsWith("pos")) { if (!(input instanceof AxisInput)) return input.posKeyCode = chosenKey @@ -429,7 +478,28 @@ const EditInputInterface: React.FC = ({ input, useGamepad, onInp setChosenGamepadAxis(-1) setSelectedInput("") } - }, [chosenKey, chosenButton, chosenGamepadAxis, input, modifierState, onInputChanged, selectedInput, useGamepad]) + + if (useTouchControls && selectedInput && chosenTouchControlsAxis != -1) { + if (!(input instanceof AxisInput)) return + + input.touchControlAxis = chosenTouchControlsAxis + + onInputChanged() + setChosenTouchControlsAxis(-1) + setSelectedInput("") + } + }, [ + chosenKey, + chosenButton, + chosenGamepadAxis, + input, + modifierState, + onInputChanged, + selectedInput, + useGamepad, + useTouchControls, + chosenTouchControlsAxis, + ]) return ( void, + progressHandle?: ProgressHandle +) { if (!progressHandle) { progressHandle = new ProgressHandle(info.name ?? info.cacheKey) } @@ -95,6 +100,7 @@ function SpawnCachedMira(info: MirabufCacheInfo, type: MiraType, progressHandle? CreateMirabuf(assembly).then(x => { if (x) { World.SceneRenderer.RegisterSceneObject(x) + if (x.miraType == MiraType.ROBOT) openPanel("choose-scheme") progressHandle.Done() } else { progressHandle.Fail() @@ -193,7 +199,7 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { // Select a mirabuf assembly from the cache. const selectCache = useCallback( (info: MirabufCacheInfo, type: MiraType) => { - SpawnCachedMira(info, type) + SpawnCachedMira(info, type, openPanel) showTooltip("controls", [ { control: "WASD", description: "Drive" }, @@ -203,10 +209,7 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { closePanel(panelId) - if (type == MiraType.ROBOT) { - setSelectedBrainIndexGlobal(SynthesisBrain.brainIndexMap.size) - openPanel("choose-scheme") - } + if (type == MiraType.ROBOT) setSelectedBrainIndexGlobal(SynthesisBrain.brainIndexMap.size) }, [showTooltip, closePanel, panelId, openPanel] ) @@ -220,7 +223,7 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { MirabufCachingService.CacheRemote(info.src, type) .then(cacheInfo => { if (cacheInfo) { - SpawnCachedMira(cacheInfo, type, status) + SpawnCachedMira(cacheInfo, type, openPanel, status) } else { status.Fail("Failed to cache") } @@ -229,10 +232,7 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { closePanel(panelId) - if (type == MiraType.ROBOT) { - setSelectedBrainIndexGlobal(SynthesisBrain.brainIndexMap.size) - openPanel("choose-scheme") - } + if (type == MiraType.ROBOT) setSelectedBrainIndexGlobal(SynthesisBrain.brainIndexMap.size) }, [closePanel, panelId, openPanel] ) @@ -245,7 +245,7 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { MirabufCachingService.CacheAPS(data, type) .then(cacheInfo => { if (cacheInfo) { - SpawnCachedMira(cacheInfo, type, status) + SpawnCachedMira(cacheInfo, type, openPanel, status) } else { status.Fail("Failed to cache") } @@ -254,10 +254,7 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { closePanel(panelId) - if (type == MiraType.ROBOT) { - setSelectedBrainIndexGlobal(SynthesisBrain.brainIndexMap.size) - openPanel("choose-scheme") - } + if (type == MiraType.ROBOT) setSelectedBrainIndexGlobal(SynthesisBrain.brainIndexMap.size) }, [closePanel, panelId, openPanel] ) diff --git a/fission/vite.config.ts b/fission/vite.config.ts index 967c978ca..7cb095d68 100644 --- a/fission/vite.config.ts +++ b/fission/vite.config.ts @@ -1,36 +1,39 @@ -import { defineConfig } from "vitest/config" -import * as path from "path" -import react from "@vitejs/plugin-react-swc" -import glsl from "vite-plugin-glsl" +import { defineConfig } from 'vitest/config' +import * as path from 'path' +import react from '@vitejs/plugin-react-swc' +import basicSsl from '@vitejs/plugin-basic-ssl' +import glsl from 'vite-plugin-glsl' const basePath = "/fission/" const serverPort = 3000 const dockerServerPort = 80 -const useLocal = true +const useLocal = false +const useSsl = false + +const plugins = [ + react(), glsl({ + include: [ // Glob pattern, or array of glob patterns to import + '**/*.glsl', '**/*.wgsl', + '**/*.vert', '**/*.frag', + '**/*.vs', '**/*.fs' + ], + exclude: undefined, // Glob pattern, or array of glob patterns to ignore + warnDuplicatedImports: true, // Warn if the same chunk was imported multiple times + defaultExtension: 'glsl', // Shader suffix when no extension is specified + compress: false, // Compress output shader code + watch: true, // Recompile shader on change + root: '/' // Directory for root imports + }) +] + +if (useSsl) { + plugins.push(basicSsl()) +} // https://vitejs.dev/config/ export default defineConfig({ - plugins: [ - react(), - glsl({ - include: [ - // Glob pattern, or array of glob patterns to import - "**/*.glsl", - "**/*.wgsl", - "**/*.vert", - "**/*.frag", - "**/*.vs", - "**/*.fs", - ], - exclude: undefined, // Glob pattern, or array of glob patterns to ignore - warnDuplicatedImports: true, // Warn if the same chunk was imported multiple times - defaultExtension: "glsl", // Shader suffix when no extension is specified - compress: false, // Compress output shader code - watch: true, // Recompile shader on change - root: "/", // Directory for root imports - }), - ], + plugins: plugins, resolve: { alias: [ { find: "@/components", replacement: path.resolve(__dirname, "src", "ui", "components") },