diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 5d21ef3a61..c6c7048b12 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -61,6 +61,7 @@ import { ProgressHandle } from "./ui/components/ProgressNotificationData.ts" import ConfigureRobotModal from "./ui/modals/configuring/ConfigureRobotModal.tsx" import ResetAllInputsModal from "./ui/modals/configuring/ResetAllInputsModal.tsx" import ZoneConfigPanel from "./ui/panels/configuring/scoring/ZoneConfigPanel.tsx" +import SceneOverlay from "./ui/components/SceneOverlay.tsx" import WPILibWSWorker from "@/systems/simulation/wpilib_brain/WPILibWSWorker.ts?worker" import WSViewPanel from "./ui/panels/WSViewPanel.tsx" @@ -191,6 +192,7 @@ function Synthesis() { > + {panelElements.length > 0 && panelElements} {modalElement && ( diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index f0bd7b5596..4c4cdd1414 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -17,6 +17,7 @@ import PreferencesSystem from "@/systems/preferences/PreferencesSystem" import { MiraType } from "./MirabufLoader" import IntakeSensorSceneObject from "./IntakeSensorSceneObject" import EjectableSceneObject from "./EjectableSceneObject" +import { SceneOverlayTag } from "@/ui/components/SceneOverlayEvents" import { ProgressHandle } from "@/ui/components/ProgressNotificationData" const DEBUG_BODIES = false @@ -45,6 +46,8 @@ class MirabufSceneObject extends SceneObject { private _intakeSensor?: IntakeSensorSceneObject private _ejectable?: EjectableSceneObject + private _nameTag: SceneOverlayTag | undefined + get mirabufInstance() { return this._mirabufInstance } @@ -99,6 +102,11 @@ class MirabufSceneObject extends SceneObject { this.EnableTransformControls() // adding transform gizmo to mirabuf object on its creation this.getPreferences() + + // creating nametag for robots + if (this.miraType === MiraType.ROBOT) { + this._nameTag = new SceneOverlayTag("Ernie") + } } public Setup(): void { @@ -208,6 +216,18 @@ class MirabufSceneObject extends SceneObject { x.computeBoundingBox() x.computeBoundingSphere() }) + + /* Updating the position of the name tag according to the robots position on screen */ + if (this._nameTag && PreferencesSystem.getGlobalPreference("RenderSceneTags")) { + const boundingBox = this.ComputeBoundingBox() + this._nameTag.position = World.SceneRenderer.WorldToPixelSpace( + new THREE.Vector3( + (boundingBox.max.x + boundingBox.min.x) / 2, + boundingBox.max.y + 0.1, + (boundingBox.max.z + boundingBox.min.z) / 2 + ) + ) + } } public Dispose(): void { @@ -225,6 +245,7 @@ class MirabufSceneObject extends SceneObject { World.PhysicsSystem.RemoveBodyAssocation(bodyId) }) + this._nameTag?.Dispose() this.DisableTransformControls() World.SimulationSystem.UnregisterMechanism(this._mechanism) World.PhysicsSystem.DestroyMechanism(this._mechanism) @@ -345,6 +366,19 @@ class MirabufSceneObject extends SceneObject { this.EnablePhysics() } + /** + * + * @returns The bounding box of the mirabuf object. + */ + private ComputeBoundingBox(): THREE.Box3 { + const box = new THREE.Box3() + this._mirabufInstance.batches.forEach(batch => { + if (batch.boundingBox) box.union(batch.boundingBox) + }) + + return box + } + private getPreferences(): void { this._intakePreferences = PreferencesSystem.getRobotPreferences(this.assemblyName)?.intake this._ejectorPreferences = PreferencesSystem.getRobotPreferences(this.assemblyName)?.ejector diff --git a/fission/src/systems/preferences/PreferenceTypes.ts b/fission/src/systems/preferences/PreferenceTypes.ts index abf7a702b7..9f088d7d97 100644 --- a/fission/src/systems/preferences/PreferenceTypes.ts +++ b/fission/src/systems/preferences/PreferenceTypes.ts @@ -10,6 +10,7 @@ export type GlobalPreference = | "ReportAnalytics" | "UseMetric" | "RenderScoringZones" + | "RenderSceneTags" export const RobotPreferencesKey: string = "Robots" export const FieldPreferencesKey: string = "Fields" @@ -23,6 +24,7 @@ export const DefaultGlobalPreferences: { [key: string]: unknown } = { ReportAnalytics: false, UseMetric: false, RenderScoringZones: true, + RenderSceneTags: true, } export type IntakePreferences = { diff --git a/fission/src/systems/scene/SceneRenderer.ts b/fission/src/systems/scene/SceneRenderer.ts index 2617401501..1590a15c9a 100644 --- a/fission/src/systems/scene/SceneRenderer.ts +++ b/fission/src/systems/scene/SceneRenderer.ts @@ -11,6 +11,10 @@ import fragmentShader from "@/shaders/fragment.glsl" import { Theme } from "@/ui/ThemeContext" import InputSystem from "../input/InputSystem" +import { PixelSpaceCoord, SceneOverlayEvent, SceneOverlayEventKey } from "@/ui/components/SceneOverlayEvents" +import {} from "@/ui/components/SceneOverlayEvents" +import PreferencesSystem from "../preferences/PreferencesSystem" + const CLEAR_COLOR = 0x121212 const GROUND_COLOR = 0x4066c7 @@ -151,6 +155,10 @@ class SceneRenderer extends WorldSystem { ) }) + // Update the tags each frame if they are enabled in preferences + if (PreferencesSystem.getGlobalPreference("RenderSceneTags")) + new SceneOverlayEvent(SceneOverlayEventKey.UPDATE) + this._composer.render(deltaT) } @@ -219,6 +227,18 @@ class SceneRenderer extends WorldSystem { return screenSpace.unproject(this.mainCamera) } + /** + * Convert world space coordinates to screen space coordinates + * + * @param world World space coordinates + * @returns Pixel space coordinates + */ + public WorldToPixelSpace(world: THREE.Vector3): PixelSpaceCoord { + this._mainCamera.updateMatrixWorld() + const screenSpace = world.project(this._mainCamera) + return [(window.innerWidth * (screenSpace.x + 1.0)) / 2.0, (window.innerHeight * (1.0 - screenSpace.y)) / 2.0] + } + /** * Updates the skybox colors based on the current theme diff --git a/fission/src/ui/components/SceneOverlay.tsx b/fission/src/ui/components/SceneOverlay.tsx new file mode 100644 index 0000000000..01e4ce605b --- /dev/null +++ b/fission/src/ui/components/SceneOverlay.tsx @@ -0,0 +1,107 @@ +import { Box } from "@mui/material" +import { useEffect, useReducer, useState } from "react" +import { + SceneOverlayTag, + SceneOverlayEvent, + SceneOverlayEventKey, + SceneOverlayTagEvent, + SceneOverlayTagEventKey, +} from "./SceneOverlayEvents" +import Label, { LabelSize } from "./Label" + +const tagMap = new Map() + +function SceneOverlay() { + /* State to determine if the overlay is disabled */ + const [isDisabled, setIsDisabled] = useState(false) + + /* h1 text for each tagMap tag */ + const [components, updateComponents] = useReducer(() => { + if (isDisabled) return <> // if the overlay is disabled, return nothing + + return [...tagMap.values()].map(x => ( +
+ +
+ )) + }, []) + + /* Creating listener for tag events to update tagMap and rerender overlay */ + useEffect(() => { + const onTagAdd = (e: Event) => { + tagMap.set((e as SceneOverlayTagEvent).tag.id, (e as SceneOverlayTagEvent).tag) + } + + const onTagRemove = (e: Event) => { + tagMap.delete((e as SceneOverlayTagEvent).tag.id) + } + + const onUpdate = (_: Event) => { + updateComponents() + } + + const onDisable = () => { + setIsDisabled(true) + updateComponents() + } + + const onEnable = () => { + setIsDisabled(false) + updateComponents() + } + + // listening for tags being added and removed + SceneOverlayTagEvent.Listen(SceneOverlayTagEventKey.ADD, onTagAdd) + SceneOverlayTagEvent.Listen(SceneOverlayTagEventKey.REMOVE, onTagRemove) + + // listening for updates to the overlay every frame + SceneOverlayEvent.Listen(SceneOverlayEventKey.UPDATE, onUpdate) + + // listening for disabling and enabling scene tags + SceneOverlayEvent.Listen(SceneOverlayEventKey.DISABLE, onDisable) + SceneOverlayEvent.Listen(SceneOverlayEventKey.ENABLE, onEnable) + + // disposing all the tags and listeners when the scene is destroyed + return () => { + SceneOverlayTagEvent.RemoveListener(SceneOverlayTagEventKey.ADD, onTagAdd) + SceneOverlayTagEvent.RemoveListener(SceneOverlayTagEventKey.REMOVE, onTagRemove) + SceneOverlayEvent.RemoveListener(SceneOverlayEventKey.UPDATE, onUpdate) + SceneOverlayEvent.RemoveListener(SceneOverlayEventKey.DISABLE, onDisable) + SceneOverlayEvent.RemoveListener(SceneOverlayEventKey.ENABLE, onEnable) + tagMap.clear() + } + }, []) + + /* Render the overlay as a box that spans the entire screen and does not intercept any user interaction */ + return ( + + {components ?? <>} + + ) +} + +export default SceneOverlay diff --git a/fission/src/ui/components/SceneOverlayEvents.ts b/fission/src/ui/components/SceneOverlayEvents.ts new file mode 100644 index 0000000000..feb6ef093e --- /dev/null +++ b/fission/src/ui/components/SceneOverlayEvents.ts @@ -0,0 +1,85 @@ +let nextTagId = 0 + +/* Coordinates for tags in world space */ +export type PixelSpaceCoord = [number, number] + +/** Contains the event keys for events that require a SceneOverlayTag as a parameter */ +export const enum SceneOverlayTagEventKey { + ADD = "SceneOverlayTagAddEvent", + REMOVE = "SceneOverlayTagRemoveEvent", +} + +/** Contains the event keys for other Scene Overlay Events */ +export const enum SceneOverlayEventKey { + UPDATE = "SceneOverlayUpdateEvent", + DISABLE = "SceneOverlayDisableEvent", + ENABLE = "SceneOverlayEnableEvent", +} + +/** + * Represents a tag that can be displayed on the screen + * + * @param text The text to display + * @param position The position of the tag in screen space (default: [0,0]) + */ +export class SceneOverlayTag { + private _id: number + public text: string + public position: PixelSpaceCoord // Screen Space + + public get id() { + return this._id + } + + /** Create a new tag */ + public constructor(text: string, position?: PixelSpaceCoord) { + this._id = nextTagId++ + + this.text = text + this.position = position ?? [0, 0] + new SceneOverlayTagEvent(SceneOverlayTagEventKey.ADD, this) + } + + /** Removing the tag */ + public Dispose() { + new SceneOverlayTagEvent(SceneOverlayTagEventKey.REMOVE, this) + } +} + +/** Event handler for events that use a SceneOverlayTag as a parameter */ +export class SceneOverlayTagEvent extends Event { + public tag: SceneOverlayTag + + public constructor(eventKey: SceneOverlayTagEventKey, tag: SceneOverlayTag) { + super(eventKey) + + this.tag = tag + + window.dispatchEvent(this) + } + + public static Listen(eventKey: SceneOverlayTagEventKey, func: (e: Event) => void) { + window.addEventListener(eventKey, func) + } + + public static RemoveListener(eventKey: SceneOverlayTagEventKey, func: (e: Event) => void) { + window.removeEventListener(eventKey, func) + } +} + +/** Event handler for other SceneOverlay events */ +export class SceneOverlayEvent extends Event { + public constructor(eventKey: SceneOverlayEventKey) { + super(eventKey) + + window.dispatchEvent(this) + } + + public static Listen(eventKey: SceneOverlayEventKey, func: (e: Event) => void) { + window.addEventListener(eventKey, func) + } + + public static RemoveListener(eventKey: SceneOverlayEventKey, func: (e: Event) => void) { + window.removeEventListener(eventKey, func) + } +} diff --git a/fission/src/ui/modals/configuring/SettingsModal.tsx b/fission/src/ui/modals/configuring/SettingsModal.tsx index 017c2d392e..4c001c5771 100644 --- a/fission/src/ui/modals/configuring/SettingsModal.tsx +++ b/fission/src/ui/modals/configuring/SettingsModal.tsx @@ -8,6 +8,7 @@ import Button from "@/components/Button" import Slider from "@/components/Slider" import Checkbox from "@/components/Checkbox" import PreferencesSystem from "@/systems/preferences/PreferencesSystem" +import { SceneOverlayEvent, SceneOverlayEventKey } from "@/ui/components/SceneOverlayEvents" const moveElementToTop = (arr: string[], element: string | undefined) => { if (element == undefined) { @@ -44,6 +45,9 @@ const SettingsModal: React.FC = ({ modalId }) => { const [renderScoringZones, setRenderScoringZones] = useState( PreferencesSystem.getGlobalPreference("RenderScoringZones") ) + const [renderSceneTags, setRenderSceneTags] = useState( + PreferencesSystem.getGlobalPreference("RenderSceneTags") + ) const saveSettings = () => { PreferencesSystem.setGlobalPreference("ScreenMode", screenMode) @@ -54,6 +58,7 @@ const SettingsModal: React.FC = ({ modalId }) => { PreferencesSystem.setGlobalPreference("ReportAnalytics", reportAnalytics) PreferencesSystem.setGlobalPreference("UseMetric", useMetric) PreferencesSystem.setGlobalPreference("RenderScoringZones", renderScoringZones) + PreferencesSystem.setGlobalPreference("RenderSceneTags", renderSceneTags) PreferencesSystem.savePreferences() } @@ -136,6 +141,15 @@ const SettingsModal: React.FC = ({ modalId }) => { setRenderScoringZones(checked) }} /> + ("RenderSceneTags")} + onClick={checked => { + setRenderSceneTags(checked) + if (!checked) new SceneOverlayEvent(SceneOverlayEventKey.DISABLE) + else new SceneOverlayEvent(SceneOverlayEventKey.ENABLE) + }} + /> ) }