diff --git a/fission/bun.lock b/fission/bun.lock
index 41f3330c0c..6aaaff01a0 100644
--- a/fission/bun.lock
+++ b/fission/bun.lock
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
+ "configVersion": 0,
"workspaces": {
"": {
"name": "synthesis-fission",
diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx
index bad1eb5d69..1830f272e0 100644
--- a/fission/src/Synthesis.tsx
+++ b/fission/src/Synthesis.tsx
@@ -3,7 +3,7 @@ import { SnackbarProvider } from "notistack"
import Slide from "@mui/material/Slide"
import { useCallback, useEffect, useRef, useState } from "react"
import { globalAddToast } from "@/components/GlobalUIControls.ts"
-import MainHUD from "@/components/MainHUD"
+import MainHUD, { EVENT_KEY_OPEN_MAIN_HUD } from "@/components/MainHUD"
import MultiplayerHUD from "@/components/MultiplayerHUD.tsx"
import Scene from "@/components/Scene.tsx"
import MultiplayerStartModal from "@/modals/MultiplayerStartModal.tsx"
@@ -40,6 +40,8 @@ function Synthesis() {
World.updateWorld()
}
+ document.dispatchEvent(new Event(EVENT_KEY_OPEN_MAIN_HUD))
+
mainLoop()
}
useEffect(() => {
diff --git a/fission/src/ui/components/HelpPopover.tsx b/fission/src/ui/components/HelpPopover.tsx
new file mode 100644
index 0000000000..4ac64db7c6
--- /dev/null
+++ b/fission/src/ui/components/HelpPopover.tsx
@@ -0,0 +1,71 @@
+import { Button, List, ListItemButton, ListItemIcon, ListItemText, Popover } from "@mui/material"
+import { IoHelpCircle } from "react-icons/io5"
+import { useCallback, useState } from "react"
+import { type HelpOption, HelpOptionType } from "@/util/HelpOption"
+import { FaDiscord, FaYoutube } from "react-icons/fa6"
+import { MdArticle } from "react-icons/md"
+
+function helpOptionIcon(type: HelpOptionType) {
+ switch (type) {
+ case HelpOptionType.YouTube:
+ return
+ case HelpOptionType.Codelab:
+ return
+ case HelpOptionType.Discord:
+ return
+ case HelpOptionType.Website:
+ return
+ default:
+ return
+ }
+}
+
+const HelpPopover: React.FC<{ id: string; helpOptions?: HelpOption[] }> = ({ id, helpOptions }) => {
+ const [helpPopoverAnchor, setHelpPopoverAnchor] = useState(null)
+
+ const handleHelpButton = useCallback((event: React.MouseEvent) => {
+ setHelpPopoverAnchor(event.currentTarget)
+ }, [])
+
+ const handleCloseHelpPopover = useCallback(() => {
+ setHelpPopoverAnchor(null)
+ }, [])
+
+ const helpPopoverOpen = Boolean(helpPopoverAnchor)
+ const helpPopoverId = helpPopoverOpen ? id : undefined
+
+ return (
+ helpOptions && (
+ <>
+ } onClick={handleHelpButton}>
+ Help
+
+
+
+ {helpOptions.map(x => (
+ window.open(x.link, "_blank", "noopener, noreferrer")}>
+ {helpOptionIcon(x.type)}
+
+
+ ))}
+
+
+ >
+ )
+ )
+}
+
+export { HelpPopover }
diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx
index c17da72259..d92baddb30 100644
--- a/fission/src/ui/components/MainHUD.tsx
+++ b/fission/src/ui/components/MainHUD.tsx
@@ -1,4 +1,4 @@
-import { Box, ButtonGroup, ButtonProps, Stack } from "@mui/material"
+import { Box, ButtonGroup, type ButtonProps, Stack } from "@mui/material"
import { motion } from "framer-motion"
import type React from "react"
import { useEffect, useState } from "react"
@@ -22,6 +22,23 @@ import { setAddToast, setOpenModal, setOpenPanel } from "./GlobalUIControls"
import { Button, IconButton, SynthesisIcons } from "./StyledComponents"
import { TouchControlsEvent, TouchControlsEventKeys } from "./TouchControls"
import UserIcon from "./UserIcon"
+import { HelpPopover } from "./HelpPopover"
+import {
+ HELP_OPTION_ALL_TUTORIALS,
+ HELP_OPTION_DISCORD,
+ HELP_OPTION_EXPORT_CODELAB,
+ HELP_OPTION_SPAWN_ASSET_YOUTUBE,
+ type HelpOption,
+} from "@/util/HelpOption"
+
+const HELP_OPTIONS: HelpOption[] = [
+ HELP_OPTION_SPAWN_ASSET_YOUTUBE,
+ HELP_OPTION_EXPORT_CODELAB,
+ HELP_OPTION_ALL_TUTORIALS,
+ HELP_OPTION_DISCORD,
+]
+
+export const EVENT_KEY_OPEN_MAIN_HUD = "openMainHud"
const MainHUDButton: React.FC = ({ startIcon, endIcon, children, ...props }) => {
return (
@@ -61,6 +78,17 @@ const MainHUD: React.FC = () => {
const [userInfo, setUserInfo] = useState(APS.userInfo)
const [matchModeRunning, setMatchModeRunning] = useState(MatchMode.getInstance().isMatchEnabled())
+ useEffect(() => {
+ const handler = (event: Event) => {
+ if (event.type === EVENT_KEY_OPEN_MAIN_HUD) setIsOpen(true)
+ }
+
+ document.addEventListener(EVENT_KEY_OPEN_MAIN_HUD, handler)
+ return () => {
+ document.removeEventListener(EVENT_KEY_OPEN_MAIN_HUD, handler)
+ }
+ }, [])
+
useEffect(() => {
document.addEventListener(APS_USER_INFO_UPDATE_EVENT, () => {
setUserInfo(APS.userInfo)
@@ -267,6 +295,7 @@ const MainHUD: React.FC = () => {
Abort Match Mode
)}
+
>
)
diff --git a/fission/src/ui/components/Modal.tsx b/fission/src/ui/components/Modal.tsx
index 33e961d601..258c32f220 100644
--- a/fission/src/ui/components/Modal.tsx
+++ b/fission/src/ui/components/Modal.tsx
@@ -1,8 +1,9 @@
-import { Card, CardActions, CardContent, CardHeader, Modal as MUIModal } from "@mui/material"
-import React, { type ReactElement } from "react"
+import { Box, Card, CardActions, CardContent, CardHeader, Modal as MUIModal, Typography } from "@mui/material"
+import React, { useMemo, type ReactElement } from "react"
import type { Modal as ModalType, Panel as PanelType } from "../helpers/UIProviderHelpers"
import { CloseType, useUIContext } from "../helpers/UIProviderHelpers"
import { Button } from "./StyledComponents"
+import { HelpPopover } from "./HelpPopover"
export type ModalImplProps = Partial<{
modal: ModalType
@@ -22,6 +23,15 @@ export const Modal = ({ children, modal, parent }: ModalElementProps
const props = modal.props
+ const header = useMemo(() => {
+ return (
+
+ {props.title}
+
+
+ )
+ }, [props.title])
+
return (
({ children, modal, parent }: ModalElementProps
>
{props.title && (
= Partial<{
@@ -59,6 +60,23 @@ export const Panel = ({ children, panel, parent }: PanelElementProps
const props = panel.props
const nodeRef = useRef(null)
+ const header = useMemo(() => {
+ return (
+
+ {props.title}
+
+
+ )
+ }, [props.title])
+
// FIXME: sliders show up as so want to cancel drag on those
// however still can drag on dropdown but menu elements are left behind
return (
@@ -84,7 +102,7 @@ export const Panel = ({ children, panel, parent }: PanelElementProps
>
{props.title && (
{
disableAccept?: boolean
cancelText?: string
acceptText?: string
+ helpOptions?: HelpOption[]
custom: P
}
diff --git a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx
index bdcc3795d4..a7a9ac8e02 100644
--- a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx
+++ b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx
@@ -35,6 +35,7 @@ import { Tab, Tabs } from "@mui/material"
import { SoundPlayer } from "@/systems/sound/SoundPlayer"
import CommandRegistry, { type CommandDefinition, type CommandProvider } from "@/ui/components/CommandRegistry"
import { globalOpenPanel } from "@/ui/components/GlobalUIControls"
+import { HELP_OPTION_CONFIGURE_YOUTUBE } from "@/util/HelpOption"
// Register command: Configure Assets (module-scope side effect)
CommandRegistry.get().registerCommands([
@@ -314,7 +315,12 @@ const ConfigurePanel: React.FC>
configureScreen(
panel!,
- { title: "Configure Assets", acceptText: "Save", cancelText: "Cancel" },
+ {
+ title: "Configure Assets",
+ acceptText: "Save",
+ cancelText: "Cancel",
+ helpOptions: [HELP_OPTION_CONFIGURE_YOUTUBE],
+ },
{ onBeforeAccept, onCancel }
)
}, [selectedAssembly, pendingDeletes])
diff --git a/fission/src/ui/panels/configuring/initial-config/InitialConfigPanel.tsx b/fission/src/ui/panels/configuring/initial-config/InitialConfigPanel.tsx
index b9739dfa15..157e1affba 100644
--- a/fission/src/ui/panels/configuring/initial-config/InitialConfigPanel.tsx
+++ b/fission/src/ui/panels/configuring/initial-config/InitialConfigPanel.tsx
@@ -21,6 +21,7 @@ import { useUIContext } from "@/ui/helpers/UIProviderHelpers"
import NewInputSchemeModal from "@/ui/modals/configuring/inputs/NewInputSchemeModal"
import ConfigurePanel from "../assembly-config/ConfigurePanel"
import InputSchemeSelection from "./InputSchemeSelection"
+import { HELP_OPTION_SPAWN_ASSET_YOUTUBE } from "@/util/HelpOption"
const InitialConfigPanel: React.FC> = ({ panel }) => {
// TODO: can we pass these as custom props?
@@ -76,7 +77,12 @@ const InitialConfigPanel: React.FC> = ({ panel }) =>
configureScreen(
panel!,
- { title: "Assembly Setup", acceptText: "Finish", cancelText: "Remove" },
+ {
+ title: "Assembly Setup",
+ acceptText: "Finish",
+ cancelText: "Remove",
+ helpOptions: [HELP_OPTION_SPAWN_ASSET_YOUTUBE],
+ },
{
onBeforeAccept: () => {
closeFinish()
diff --git a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx
index 436ecbe7f6..54144ff180 100644
--- a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx
+++ b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx
@@ -45,6 +45,7 @@ import {
import InitialConfigPanel from "../configuring/initial-config/InitialConfigPanel"
import CommandRegistry from "@/ui/components/CommandRegistry"
import type { CustomOrbitControls } from "@/systems/scene/CameraControls"
+import { HELP_OPTION_SPAWN_ASSET_YOUTUBE } from "@/util/HelpOption"
// Register commands: Open import panel scoped to robots/fields (module-scope side effect)
CommandRegistry.get().registerCommands([
@@ -192,7 +193,16 @@ const ImportMirabufPanel: React.FC(undefined)
useEffect(() => {
- configureScreen(panel!, { title: "Spawn Asset", hideAccept: true, cancelText: "Back" }, {})
+ configureScreen(
+ panel!,
+ {
+ title: "Spawn Asset",
+ hideAccept: true,
+ cancelText: "Back",
+ helpOptions: [HELP_OPTION_SPAWN_ASSET_YOUTUBE],
+ },
+ {}
+ )
}, [])
useEffect(() => {
diff --git a/fission/src/util/HelpOption.ts b/fission/src/util/HelpOption.ts
new file mode 100644
index 0000000000..008752ffc9
--- /dev/null
+++ b/fission/src/util/HelpOption.ts
@@ -0,0 +1,42 @@
+export enum HelpOptionType {
+ YouTube = "YouTube",
+ Codelab = "Codelab",
+ Website = "Website",
+ Discord = "Discord",
+}
+
+export type HelpOption = {
+ text: string
+ link: string
+ type: HelpOptionType
+}
+
+export const HELP_OPTION_SPAWN_ASSET_YOUTUBE: HelpOption = {
+ text: "How to Spawn a Robot/Field",
+ link: "https://youtu.be/u608dgHAc2s?si=25tbYWZpHNxGdVzU",
+ type: HelpOptionType.YouTube,
+}
+
+export const HELP_OPTION_CONFIGURE_YOUTUBE: HelpOption = {
+ text: "How to Configure a Robot/Field",
+ link: "https://youtu.be/hHf-7Ojl-fE?si=6xpjRkiFwjrosDyd",
+ type: HelpOptionType.YouTube,
+}
+
+export const HELP_OPTION_DISCORD: HelpOption = {
+ text: "Synthesis Community",
+ link: "https://discord.gg/AnGhEyAn",
+ type: HelpOptionType.Discord,
+}
+
+export const HELP_OPTION_EXPORT_CODELAB: HelpOption = {
+ text: "Exporter Guide",
+ link: "https://synthesis.autodesk.com/codelab/FusionExporterCodelab/#0",
+ type: HelpOptionType.Codelab,
+}
+
+export const HELP_OPTION_ALL_TUTORIALS: HelpOption = {
+ text: "All Tutorials",
+ link: "https://synthesis.autodesk.com/tutorials.html",
+ type: HelpOptionType.Website,
+}