import type { IconDefinition } from '@fortawesome/free-solid-svg-icons' import { Resizable } from 're-resizable' import type { MouseEventHandler } from 'react' import { useCallback, useContext, useEffect, useMemo } from 'react' import { useHotkeys } from 'react-hotkeys-hook' import { useAppState } from '@src/AppState' import { ActionIcon } from '@src/components/ActionIcon' import type { CustomIconName } from '@src/components/CustomIcon' import { MachineManagerContext } from '@src/components/MachineManagerProvider' import { ModelingPane } from '@src/components/ModelingSidebar/ModelingPane' import type { SidebarAction, SidebarType, } from '@src/components/ModelingSidebar/ModelingPanes' import { sidebarPanes } from '@src/components/ModelingSidebar/ModelingPanes' import Tooltip from '@src/components/Tooltip' import { useModelingContext } from '@src/hooks/useModelingContext' import { useNetworkContext } from '@src/hooks/useNetworkContext' import { NetworkHealthState } from '@src/hooks/useNetworkStatus' import { useKclContext } from '@src/lang/KclProvider' import { EngineConnectionStateType } from '@src/lang/std/engineConnection' import { SIDEBAR_BUTTON_SUFFIX } from '@src/lib/constants' import { isDesktop } from '@src/lib/isDesktop' import { useSettings } from '@src/lib/singletons' import { commandBarActor } from '@src/lib/singletons' import { onboardingPaths } from '@src/routes/Onboarding/paths' import { reportRejection } from '@src/lib/trap' import { refreshPage } from '@src/lib/utils' import { hotkeyDisplay } from '@src/lib/hotkeyWrapper' import usePlatform from '@src/hooks/usePlatform' interface ModelingSidebarProps { paneOpacity: '' | 'opacity-20' | 'opacity-40' } interface BadgeInfoComputed { value: number | boolean | string onClick?: MouseEventHandler className?: string title?: string } function getPlatformString(): 'web' | 'desktop' { return isDesktop() ? 'desktop' : 'web' } export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { const machineManager = useContext(MachineManagerContext) const kclContext = useKclContext() const settings = useSettings() const onboardingStatus = settings.app.onboardingStatus const { send, context } = useModelingContext() const pointerEventsCssClass = onboardingStatus.current === onboardingPaths.CAMERA || context.store?.openPanes.length === 0 ? 'pointer-events-none ' : 'pointer-events-auto ' const showDebugPanel = settings.app.showDebugPanel const { overallState, immediateState } = useNetworkContext() const { isExecuting } = useKclContext() const { isStreamReady } = useAppState() const reliesOnEngine = (overallState !== NetworkHealthState.Ok && overallState !== NetworkHealthState.Weak) || isExecuting || immediateState.type !== EngineConnectionStateType.ConnectionEstablished || !isStreamReady const paneCallbackProps = useMemo( () => ({ kclContext, settings, platform: getPlatformString(), }), [kclContext.diagnostics, settings] ) const sidebarActions: SidebarAction[] = [ { id: 'load-external-model', title: 'Load external model', sidebarName: 'Load external model', icon: 'importFile', keybinding: 'Mod + Alt + L', action: () => commandBarActor.send({ type: 'Find and select command', data: { name: 'load-external-model', groupId: 'code' }, }), }, { id: 'export', title: 'Export part', sidebarName: 'Export part', icon: 'floppyDiskArrow', keybinding: 'Ctrl + Shift + E', disable: () => reliesOnEngine ? 'Need engine connection to export' : undefined, action: () => commandBarActor.send({ type: 'Find and select command', data: { name: 'Export', groupId: 'modeling' }, }), }, { id: 'make', title: 'Make part', sidebarName: 'Make part', icon: 'printer3d', keybinding: 'Ctrl + Shift + M', // eslint-disable-next-line @typescript-eslint/no-misused-promises action: async () => { commandBarActor.send({ type: 'Find and select command', data: { name: 'Make', groupId: 'modeling' }, }) }, hide: () => !isDesktop(), disable: () => { return machineManager.noMachinesReason() }, }, { id: 'refresh', title: 'Refresh app', sidebarName: 'Refresh app', icon: 'arrowRotateRight', keybinding: 'Mod + R', // eslint-disable-next-line @typescript-eslint/no-misused-promises action: async () => { refreshPage('Sidebar button').catch(reportRejection) }, }, ] const filteredActions: SidebarAction[] = sidebarActions.filter( (action) => !action.hide || (action.hide instanceof Function && !action.hide(paneCallbackProps)) ) // // Filter out the debug panel if it's not supposed to be shown // // TODO: abstract out for allowing user to configure which panes to show const filteredPanes = useMemo( () => (showDebugPanel.current ? sidebarPanes : sidebarPanes.filter((pane) => pane.id !== 'debug') ).filter( (pane) => !pane.hide || (pane.hide instanceof Function && !pane.hide(paneCallbackProps)) ), [sidebarPanes, paneCallbackProps] ) const paneBadgeMap: Record = useMemo(() => { return filteredPanes.reduce( (acc, pane) => { if (pane.showBadge) { acc[pane.id] = { value: pane.showBadge.value(paneCallbackProps), onClick: pane.showBadge.onClick, className: pane.showBadge.className, title: pane.showBadge.title, } } return acc }, {} as Record ) }, [paneCallbackProps]) // Clear any hidden panes from the `openPanes` array useEffect(() => { const panesToReset: SidebarType[] = [] sidebarPanes.forEach((pane) => { if ( pane.hide === true || (pane.hide instanceof Function && pane.hide(paneCallbackProps)) ) { panesToReset.push(pane.id) } }) if (panesToReset.length > 0) { send({ type: 'Set context', data: { openPanes: context.store?.openPanes.filter( (pane) => !panesToReset.includes(pane) ), }, }) } }, [settings.app.showDebugPanel]) const togglePane = useCallback( (newPane: SidebarType) => { send({ type: 'Set context', data: { openPanes: context.store?.openPanes.includes(newPane) ? context.store?.openPanes.filter((pane) => pane !== newPane) : [...context.store?.openPanes, newPane], }, }) }, [context.store?.openPanes, send] ) return (
      = 1 ? 'pr-0.5' : '') } > {filteredPanes.map((pane) => ( togglePane(pane.id)} aria-pressed={context.store?.openPanes.includes(pane.id)} showBadge={paneBadgeMap[pane.id]} /> ))}
    {filteredActions.length > 0 && ( <>
    )}
    = 1 ? `w-full` : `hidden`) } > {filteredPanes .filter((pane) => context?.store.openPanes.includes(pane.id)) .map((pane) => ( {}} id={`${pane.id}-pane`} > {pane.Content instanceof Function ? ( togglePane(pane.id)} /> ) : ( pane.Content )} ))}
) } interface ModelingPaneButtonProps extends React.HTMLAttributes { paneConfig: { id: string sidebarName: string icon: CustomIconName | IconDefinition keybinding: string iconClassName?: string iconSize?: 'sm' | 'md' | 'lg' } onClick: () => void paneIsOpen?: boolean showBadge?: BadgeInfoComputed disabledText?: string } function ModelingPaneButton({ paneConfig, onClick, paneIsOpen, showBadge, disabledText, ...props }: ModelingPaneButtonProps) { const platform = usePlatform() useHotkeys(paneConfig.keybinding, onClick, { scopes: ['modeling'], }) return (
{!!showBadge?.value && (

1 ? 's' : '' }` } >  has  {typeof showBadge.value === 'number' || typeof showBadge.value === 'string' ? ( {showBadge.value} ) : ( a )} {typeof showBadge.value === 'number' && (  notification{Number(showBadge.value) > 1 ? 's' : ''} )}

)}
) }