Make camera mouse controls configurable (#411)
* Add camera handler config object Using definitions of camera controls of various CAD incumbents from Onshape's onboarding. Signed-off-by: Frank Noirot <frank@kittycad.io> * Refactor: alphabetize settingsMachine * Refactor: add descriptions to MouseGuards * Refactor: don't destructure mousemove event * Refactor: button down in stream as int, not bool * Honor current camera control settings * Add cameraControls to settings * Refactor: alphabetize settings imports * Refactor: break out cameraControls to own file * Fix camera control setting in command bar * Fix formatting on generated type file * dont use "as" in App.tsx guards Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch> * Don't use "as" in Stream.tsx Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch> * Don't use "as" in settingsMachine.ts Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch> * Add type to cadPrograms Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch> * Kurt review --------- Signed-off-by: Frank Noirot <frank@kittycad.io> Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
This commit is contained in:
		
							
								
								
									
										55
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								src/App.tsx
									
									
									
									
									
								
							| @ -55,6 +55,8 @@ import { onboardingPaths } from 'routes/Onboarding' | ||||
| import { LanguageServerClient } from 'editor/lsp' | ||||
| import kclLanguage from 'editor/lsp/language' | ||||
| import { CSSRuleObject } from 'tailwindcss/types/config' | ||||
| import { cameraMouseDragGuards } from 'lib/cameraControls' | ||||
| import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models' | ||||
|  | ||||
| export function App() { | ||||
|   const { code: loadedCode, project } = useLoaderData() as IndexLoaderData | ||||
| @ -88,7 +90,7 @@ export function App() { | ||||
|     isStreamReady, | ||||
|     isLSPServerReady, | ||||
|     setIsLSPServerReady, | ||||
|     isMouseDownInStream, | ||||
|     buttonDownInStream, | ||||
|     formatCode, | ||||
|     openPanes, | ||||
|     setOpenPanes, | ||||
| @ -129,7 +131,7 @@ export function App() { | ||||
|     setIsStreamReady: s.setIsStreamReady, | ||||
|     isLSPServerReady: s.isLSPServerReady, | ||||
|     setIsLSPServerReady: s.setIsLSPServerReady, | ||||
|     isMouseDownInStream: s.isMouseDownInStream, | ||||
|     buttonDownInStream: s.buttonDownInStream, | ||||
|     formatCode: s.formatCode, | ||||
|     addKCLError: s.addKCLError, | ||||
|     openPanes: s.openPanes, | ||||
| @ -145,7 +147,13 @@ export function App() { | ||||
|       context: { token }, | ||||
|     }, | ||||
|     settings: { | ||||
|       context: { showDebugPanel, theme, onboardingStatus, textWrapping }, | ||||
|       context: { | ||||
|         showDebugPanel, | ||||
|         theme, | ||||
|         onboardingStatus, | ||||
|         textWrapping, | ||||
|         cameraControls, | ||||
|       }, | ||||
|     }, | ||||
|   } = useGlobalStateContext() | ||||
|  | ||||
| @ -389,28 +397,33 @@ export function App() { | ||||
|   const debounceSocketSend = throttle<EngineCommand>((message) => { | ||||
|     engineCommandManager?.sendSceneCommand(message) | ||||
|   }, 16) | ||||
|   const handleMouseMove: MouseEventHandler<HTMLDivElement> = ({ | ||||
|     clientX, | ||||
|     clientY, | ||||
|     ctrlKey, | ||||
|     shiftKey, | ||||
|     currentTarget, | ||||
|     nativeEvent, | ||||
|   }) => { | ||||
|     nativeEvent.preventDefault() | ||||
|   const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => { | ||||
|     e.nativeEvent.preventDefault() | ||||
|  | ||||
|     const { x, y } = getNormalisedCoordinates({ | ||||
|       clientX, | ||||
|       clientY, | ||||
|       el: currentTarget, | ||||
|       clientX: e.clientX, | ||||
|       clientY: e.clientY, | ||||
|       el: e.currentTarget, | ||||
|       ...streamDimensions, | ||||
|     }) | ||||
|  | ||||
|     const interaction = ctrlKey ? 'zoom' : shiftKey ? 'pan' : 'rotate' | ||||
|  | ||||
|     const newCmdId = uuidv4() | ||||
|  | ||||
|     if (isMouseDownInStream) { | ||||
|     if (buttonDownInStream) { | ||||
|       const interactionGuards = cameraMouseDragGuards[cameraControls] | ||||
|       let interaction: CameraDragInteractionType_type | ||||
|  | ||||
|       const eWithButton = { ...e, button: buttonDownInStream } | ||||
|  | ||||
|       if (interactionGuards.pan.callback(eWithButton)) { | ||||
|         interaction = 'pan' | ||||
|       } else if (interactionGuards.rotate.callback(eWithButton)) { | ||||
|         interaction = 'rotate' | ||||
|       } else if (interactionGuards.zoom.dragCallback(eWithButton)) { | ||||
|         interaction = 'zoom' | ||||
|       } else { | ||||
|         return | ||||
|       } | ||||
|       debounceSocketSend({ | ||||
|         type: 'modeling_cmd_req', | ||||
|         cmd: { | ||||
| @ -500,7 +513,7 @@ export function App() { | ||||
|         className={ | ||||
|           'transition-opacity transition-duration-75 ' + | ||||
|           paneOpacity + | ||||
|           (isMouseDownInStream ? ' pointer-events-none' : '') | ||||
|           (buttonDownInStream ? ' pointer-events-none' : '') | ||||
|         } | ||||
|         project={project} | ||||
|         enableMenu={true} | ||||
| @ -509,7 +522,7 @@ export function App() { | ||||
|       <Resizable | ||||
|         className={ | ||||
|           'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' + | ||||
|           (isMouseDownInStream || onboardingStatus === 'camera' | ||||
|           (buttonDownInStream || onboardingStatus === 'camera' | ||||
|             ? ' pointer-events-none ' | ||||
|             : ' ') + | ||||
|           paneOpacity | ||||
| @ -588,7 +601,7 @@ export function App() { | ||||
|           className={ | ||||
|             'transition-opacity transition-duration-75 ' + | ||||
|             paneOpacity + | ||||
|             (isMouseDownInStream ? ' pointer-events-none' : '') | ||||
|             (buttonDownInStream ? ' pointer-events-none' : '') | ||||
|           } | ||||
|           open={openPanes.includes('debug')} | ||||
|         /> | ||||
|  | ||||
| @ -9,6 +9,9 @@ import { v4 as uuidv4 } from 'uuid' | ||||
| import { useStore } from '../useStore' | ||||
| import { getNormalisedCoordinates } from '../lib/utils' | ||||
| import Loading from './Loading' | ||||
| import { cameraMouseDragGuards } from 'lib/cameraControls' | ||||
| import { useGlobalStateContext } from 'hooks/useGlobalStateContext' | ||||
| import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models' | ||||
|  | ||||
| export const Stream = ({ className = '' }) => { | ||||
|   const [isLoading, setIsLoading] = useState(true) | ||||
| @ -17,7 +20,7 @@ export const Stream = ({ className = '' }) => { | ||||
|   const { | ||||
|     mediaStream, | ||||
|     engineCommandManager, | ||||
|     setIsMouseDownInStream, | ||||
|     setButtonDownInStream, | ||||
|     didDragInStream, | ||||
|     setDidDragInStream, | ||||
|     streamDimensions, | ||||
| @ -25,14 +28,18 @@ export const Stream = ({ className = '' }) => { | ||||
|   } = useStore((s) => ({ | ||||
|     mediaStream: s.mediaStream, | ||||
|     engineCommandManager: s.engineCommandManager, | ||||
|     isMouseDownInStream: s.isMouseDownInStream, | ||||
|     setIsMouseDownInStream: s.setIsMouseDownInStream, | ||||
|     setButtonDownInStream: s.setButtonDownInStream, | ||||
|     fileId: s.fileId, | ||||
|     didDragInStream: s.didDragInStream, | ||||
|     setDidDragInStream: s.setDidDragInStream, | ||||
|     streamDimensions: s.streamDimensions, | ||||
|     isExecuting: s.isExecuting, | ||||
|   })) | ||||
|   const { | ||||
|     settings: { | ||||
|       context: { cameraControls }, | ||||
|     }, | ||||
|   } = useGlobalStateContext() | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if ( | ||||
| @ -45,23 +52,29 @@ export const Stream = ({ className = '' }) => { | ||||
|     videoRef.current.srcObject = mediaStream | ||||
|   }, [mediaStream, engineCommandManager]) | ||||
|  | ||||
|   const handleMouseDown: MouseEventHandler<HTMLVideoElement> = ({ | ||||
|     clientX, | ||||
|     clientY, | ||||
|     ctrlKey, | ||||
|   }) => { | ||||
|   const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => { | ||||
|     if (!videoRef.current) return | ||||
|     const { x, y } = getNormalisedCoordinates({ | ||||
|       clientX, | ||||
|       clientY, | ||||
|       clientX: e.clientX, | ||||
|       clientY: e.clientY, | ||||
|       el: videoRef.current, | ||||
|       ...streamDimensions, | ||||
|     }) | ||||
|     console.log('click', x, y) | ||||
|  | ||||
|     const newId = uuidv4() | ||||
|  | ||||
|     const interaction = ctrlKey ? 'pan' : 'rotate' | ||||
|     const interactionGuards = cameraMouseDragGuards[cameraControls] | ||||
|     let interaction: CameraDragInteractionType_type | ||||
|  | ||||
|     if (interactionGuards.pan.callback(e)) { | ||||
|       interaction = 'pan' | ||||
|     } else if (interactionGuards.rotate.callback(e)) { | ||||
|       interaction = 'rotate' | ||||
|     } else if (interactionGuards.zoom.dragCallback(e)) { | ||||
|       interaction = 'zoom' | ||||
|     } else { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     engineCommandManager?.sendSceneCommand({ | ||||
|       type: 'modeling_cmd_req', | ||||
| @ -73,11 +86,13 @@ export const Stream = ({ className = '' }) => { | ||||
|       cmd_id: newId, | ||||
|     }) | ||||
|  | ||||
|     setIsMouseDownInStream(true) | ||||
|     setButtonDownInStream(e.button) | ||||
|     setClickCoords({ x, y }) | ||||
|   } | ||||
|  | ||||
|   const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => { | ||||
|     if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return | ||||
|  | ||||
|     e.preventDefault() | ||||
|     engineCommandManager?.sendSceneCommand({ | ||||
|       type: 'modeling_cmd_req', | ||||
| @ -115,7 +130,7 @@ export const Stream = ({ className = '' }) => { | ||||
|       cmd_id: newCmdId, | ||||
|     }) | ||||
|  | ||||
|     setIsMouseDownInStream(false) | ||||
|     setButtonDownInStream(0) | ||||
|     if (!didDragInStream) { | ||||
|       engineCommandManager?.sendSceneCommand({ | ||||
|         type: 'modeling_cmd_req', | ||||
|  | ||||
							
								
								
									
										133
									
								
								src/lib/cameraControls.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/lib/cameraControls.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,133 @@ | ||||
| const noModifiersPressed = (e: React.MouseEvent) => | ||||
|   !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey | ||||
|  | ||||
| export type CADProgram = | ||||
|   | 'KittyCAD' | ||||
|   | 'OnShape' | ||||
|   | 'Solidworks' | ||||
|   | 'NX' | ||||
|   | 'Creo' | ||||
|   | 'AutoCAD' | ||||
|  | ||||
| export const cadPrograms: CADProgram[] = [ | ||||
|   'KittyCAD', | ||||
|   'OnShape', | ||||
|   'Solidworks', | ||||
|   'NX', | ||||
|   'Creo', | ||||
|   'AutoCAD', | ||||
| ] | ||||
|  | ||||
| interface MouseGuardHandler { | ||||
|   description: string | ||||
|   callback: (e: React.MouseEvent) => boolean | ||||
| } | ||||
|  | ||||
| interface MouseGuardZoomHandler { | ||||
|   description: string | ||||
|   dragCallback: (e: React.MouseEvent) => boolean | ||||
|   scrollCallback: (e: React.MouseEvent) => boolean | ||||
| } | ||||
|  | ||||
| interface MouseGuard { | ||||
|   pan: MouseGuardHandler | ||||
|   zoom: MouseGuardZoomHandler | ||||
|   rotate: MouseGuardHandler | ||||
| } | ||||
|  | ||||
| export const cameraMouseDragGuards: Record<CADProgram, MouseGuard> = { | ||||
|   KittyCAD: { | ||||
|     pan: { | ||||
|       description: 'Right click + Shift + drag or middle click + drag', | ||||
|       callback: (e) => | ||||
|         (e.button === 3 && noModifiersPressed(e)) || | ||||
|         (e.button === 2 && e.shiftKey), | ||||
|     }, | ||||
|     zoom: { | ||||
|       description: 'Scroll wheel or Right click + Ctrl + drag', | ||||
|       dragCallback: (e) => e.button === 2 && e.ctrlKey, | ||||
|       scrollCallback: () => true, | ||||
|     }, | ||||
|     rotate: { | ||||
|       description: 'Right click + drag', | ||||
|       callback: (e) => e.button === 2 && noModifiersPressed(e), | ||||
|     }, | ||||
|   }, | ||||
|   OnShape: { | ||||
|     pan: { | ||||
|       description: 'Right click + Ctrl + drag or middle click + drag', | ||||
|       callback: (e) => | ||||
|         (e.button === 2 && e.ctrlKey) || | ||||
|         (e.button === 3 && noModifiersPressed(e)), | ||||
|     }, | ||||
|     zoom: { | ||||
|       description: 'Scroll wheel', | ||||
|       dragCallback: () => false, | ||||
|       scrollCallback: () => true, | ||||
|     }, | ||||
|     rotate: { | ||||
|       description: 'Right click + drag', | ||||
|       callback: (e) => e.button === 2 && noModifiersPressed(e), | ||||
|     }, | ||||
|   }, | ||||
|   Solidworks: { | ||||
|     pan: { | ||||
|       description: 'Right click + Ctrl + drag', | ||||
|       callback: (e) => e.button === 2 && e.ctrlKey, | ||||
|     }, | ||||
|     zoom: { | ||||
|       description: 'Scroll wheel or Middle click + Shift + drag', | ||||
|       dragCallback: (e) => e.button === 3 && e.shiftKey, | ||||
|       scrollCallback: () => true, | ||||
|     }, | ||||
|     rotate: { | ||||
|       description: 'Middle click + drag', | ||||
|       callback: (e) => e.button === 3 && noModifiersPressed(e), | ||||
|     }, | ||||
|   }, | ||||
|   NX: { | ||||
|     pan: { | ||||
|       description: 'Middle click + Shift + drag', | ||||
|       callback: (e) => e.button === 3 && e.shiftKey, | ||||
|     }, | ||||
|     zoom: { | ||||
|       description: 'Scroll wheel or Middle click + Ctrl + drag', | ||||
|       dragCallback: (e) => e.button === 3 && e.ctrlKey, | ||||
|       scrollCallback: () => true, | ||||
|     }, | ||||
|     rotate: { | ||||
|       description: 'Middle click + drag', | ||||
|       callback: (e) => e.button === 3 && noModifiersPressed(e), | ||||
|     }, | ||||
|   }, | ||||
|   Creo: { | ||||
|     pan: { | ||||
|       description: 'Middle click + Shift + drag', | ||||
|       callback: (e) => e.button === 3 && e.shiftKey, | ||||
|     }, | ||||
|     zoom: { | ||||
|       description: 'Scroll wheel or Middle click + Ctrl + drag', | ||||
|       dragCallback: (e) => e.button === 3 && e.ctrlKey, | ||||
|       scrollCallback: () => true, | ||||
|     }, | ||||
|     rotate: { | ||||
|       description: 'Middle click + drag', | ||||
|       callback: (e) => e.button === 3 && noModifiersPressed(e), | ||||
|     }, | ||||
|   }, | ||||
|   AutoCAD: { | ||||
|     pan: { | ||||
|       description: 'Middle click + drag', | ||||
|       callback: (e) => e.button === 3 && noModifiersPressed(e), | ||||
|     }, | ||||
|     zoom: { | ||||
|       description: 'Scroll wheel', | ||||
|       dragCallback: () => false, | ||||
|       scrollCallback: () => true, | ||||
|     }, | ||||
|     rotate: { | ||||
|       description: 'Middle click + Shift + drag', | ||||
|       callback: (e) => e.button === 3 && e.shiftKey, | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
| @ -1,6 +1,7 @@ | ||||
| import { assign, createMachine } from 'xstate' | ||||
| import { CommandBarMeta } from '../lib/commands' | ||||
| import { Themes, getSystemTheme, setThemeClass } from '../lib/theme' | ||||
| import { CADProgram, cadPrograms } from 'lib/cameraControls' | ||||
|  | ||||
| export const DEFAULT_PROJECT_NAME = 'project-$nnn' | ||||
|  | ||||
| @ -23,19 +24,31 @@ export type Toggle = 'On' | 'Off' | ||||
| export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY' | ||||
|  | ||||
| export const settingsCommandBarMeta: CommandBarMeta = { | ||||
|   'Set Theme': { | ||||
|     displayValue: (args: string[]) => 'Change the app theme', | ||||
|   'Set Base Unit': { | ||||
|     displayValue: (args: string[]) => 'Set your default base unit', | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'theme', | ||||
|         name: 'baseUnit', | ||||
|         type: 'select', | ||||
|         defaultValue: 'theme', | ||||
|         options: Object.values(Themes).map((v) => ({ name: v })) as { | ||||
|           name: string | ||||
|         }[], | ||||
|         defaultValue: 'baseUnit', | ||||
|         options: Object.values(baseUnitsUnion).map((v) => ({ name: v })), | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Set Camera Controls': { | ||||
|     displayValue: (args: string[]) => 'Set your camera controls', | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'cameraControls', | ||||
|         type: 'select', | ||||
|         defaultValue: 'cameraControls', | ||||
|         options: Object.values(cadPrograms).map((v) => ({ name: v })), | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Set Default Directory': { | ||||
|     hide: 'both', | ||||
|   }, | ||||
|   'Set Default Project Name': { | ||||
|     displayValue: (args: string[]) => 'Set a new default project name', | ||||
|     hide: 'web', | ||||
| @ -49,31 +62,9 @@ export const settingsCommandBarMeta: CommandBarMeta = { | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Set Default Directory': { | ||||
|   'Set Onboarding Status': { | ||||
|     hide: 'both', | ||||
|   }, | ||||
|   'Set Unit System': { | ||||
|     displayValue: (args: string[]) => 'Set your default unit system', | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'unitSystem', | ||||
|         type: 'select', | ||||
|         defaultValue: 'unitSystem', | ||||
|         options: [{ name: UnitSystem.Imperial }, { name: UnitSystem.Metric }], | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Set Base Unit': { | ||||
|     displayValue: (args: string[]) => 'Set your default base unit', | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'baseUnit', | ||||
|         type: 'select', | ||||
|         defaultValue: 'baseUnit', | ||||
|         options: Object.values(baseUnitsUnion).map((v) => ({ name: v })), | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Set Text Wrapping': { | ||||
|     displayValue: (args: string[]) => 'Set whether text in the editor wraps', | ||||
|     args: [ | ||||
| @ -85,8 +76,29 @@ export const settingsCommandBarMeta: CommandBarMeta = { | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Set Onboarding Status': { | ||||
|     hide: 'both', | ||||
|   'Set Theme': { | ||||
|     displayValue: (args: string[]) => 'Change the app theme', | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'theme', | ||||
|         type: 'select', | ||||
|         defaultValue: 'theme', | ||||
|         options: Object.values(Themes).map((v): { name: string } => ({ | ||||
|           name: v, | ||||
|         })), | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Set Unit System': { | ||||
|     displayValue: (args: string[]) => 'Set your default unit system', | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'unitSystem', | ||||
|         type: 'select', | ||||
|         defaultValue: 'unitSystem', | ||||
|         options: [{ name: UnitSystem.Imperial }, { name: UnitSystem.Metric }], | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
| } | ||||
|  | ||||
| @ -96,37 +108,34 @@ export const settingsMachine = createMachine( | ||||
|     id: 'Settings', | ||||
|     predictableActionArguments: true, | ||||
|     context: { | ||||
|       theme: Themes.System, | ||||
|       defaultProjectName: DEFAULT_PROJECT_NAME, | ||||
|       unitSystem: UnitSystem.Imperial, | ||||
|       baseUnit: 'in' as BaseUnit, | ||||
|       cameraControls: 'KittyCAD' as CADProgram, | ||||
|       defaultDirectory: '', | ||||
|       textWrapping: 'On' as Toggle, | ||||
|       showDebugPanel: false, | ||||
|       defaultProjectName: DEFAULT_PROJECT_NAME, | ||||
|       onboardingStatus: '', | ||||
|       showDebugPanel: false, | ||||
|       textWrapping: 'On' as Toggle, | ||||
|       theme: Themes.System, | ||||
|       unitSystem: UnitSystem.Imperial, | ||||
|     }, | ||||
|     initial: 'idle', | ||||
|     states: { | ||||
|       idle: { | ||||
|         entry: ['setThemeClass'], | ||||
|         on: { | ||||
|           'Set Theme': { | ||||
|           'Set Base Unit': { | ||||
|             actions: [ | ||||
|               assign({ | ||||
|                 theme: (_, event) => event.data.theme, | ||||
|               }), | ||||
|               assign({ baseUnit: (_, event) => event.data.baseUnit }), | ||||
|               'persistSettings', | ||||
|               'toastSuccess', | ||||
|               'setThemeClass', | ||||
|             ], | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|           'Set Default Project Name': { | ||||
|           'Set Camera Controls': { | ||||
|             actions: [ | ||||
|               assign({ | ||||
|                 defaultProjectName: (_, event) => | ||||
|                   event.data.defaultProjectName.trim() || DEFAULT_PROJECT_NAME, | ||||
|                 cameraControls: (_, event) => event.data.cameraControls, | ||||
|               }), | ||||
|               'persistSettings', | ||||
|               'toastSuccess', | ||||
| @ -145,12 +154,11 @@ export const settingsMachine = createMachine( | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|           'Set Unit System': { | ||||
|           'Set Default Project Name': { | ||||
|             actions: [ | ||||
|               assign({ | ||||
|                 unitSystem: (_, event) => event.data.unitSystem, | ||||
|                 baseUnit: (_, event) => | ||||
|                   event.data.unitSystem === 'imperial' ? 'in' : 'mm', | ||||
|                 defaultProjectName: (_, event) => | ||||
|                   event.data.defaultProjectName.trim() || DEFAULT_PROJECT_NAME, | ||||
|               }), | ||||
|               'persistSettings', | ||||
|               'toastSuccess', | ||||
| @ -158,11 +166,12 @@ export const settingsMachine = createMachine( | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|           'Set Base Unit': { | ||||
|           'Set Onboarding Status': { | ||||
|             actions: [ | ||||
|               assign({ baseUnit: (_, event) => event.data.baseUnit }), | ||||
|               assign({ | ||||
|                 onboardingStatus: (_, event) => event.data.onboardingStatus, | ||||
|               }), | ||||
|               'persistSettings', | ||||
|               'toastSuccess', | ||||
|             ], | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
| @ -178,6 +187,31 @@ export const settingsMachine = createMachine( | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|           'Set Theme': { | ||||
|             actions: [ | ||||
|               assign({ | ||||
|                 theme: (_, event) => event.data.theme, | ||||
|               }), | ||||
|               'persistSettings', | ||||
|               'toastSuccess', | ||||
|               'setThemeClass', | ||||
|             ], | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|           'Set Unit System': { | ||||
|             actions: [ | ||||
|               assign({ | ||||
|                 unitSystem: (_, event) => event.data.unitSystem, | ||||
|                 baseUnit: (_, event) => | ||||
|                   event.data.unitSystem === 'imperial' ? 'in' : 'mm', | ||||
|               }), | ||||
|               'persistSettings', | ||||
|               'toastSuccess', | ||||
|             ], | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|           'Toggle Debug Panel': { | ||||
|             actions: [ | ||||
|               assign({ | ||||
| @ -191,35 +225,26 @@ export const settingsMachine = createMachine( | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|           'Set Onboarding Status': { | ||||
|             actions: [ | ||||
|               assign({ | ||||
|                 onboardingStatus: (_, event) => event.data.onboardingStatus, | ||||
|               }), | ||||
|               'persistSettings', | ||||
|             ], | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     tsTypes: {} as import('./settingsMachine.typegen').Typegen0, | ||||
|     schema: { | ||||
|       events: {} as | ||||
|         | { type: 'Set Theme'; data: { theme: Themes } } | ||||
|         | { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } } | ||||
|         | { type: 'Set Camera Controls'; data: { cameraControls: CADProgram } } | ||||
|         | { type: 'Set Default Directory'; data: { defaultDirectory: string } } | ||||
|         | { | ||||
|             type: 'Set Default Project Name' | ||||
|             data: { defaultProjectName: string } | ||||
|           } | ||||
|         | { type: 'Set Default Directory'; data: { defaultDirectory: string } } | ||||
|         | { type: 'Set Onboarding Status'; data: { onboardingStatus: string } } | ||||
|         | { type: 'Set Text Wrapping'; data: { textWrapping: Toggle } } | ||||
|         | { type: 'Set Theme'; data: { theme: Themes } } | ||||
|         | { | ||||
|             type: 'Set Unit System' | ||||
|             data: { unitSystem: UnitSystem } | ||||
|           } | ||||
|         | { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } } | ||||
|         | { type: 'Set Text Wrapping'; data: { textWrapping: Toggle } } | ||||
|         | { type: 'Set Onboarding Status'; data: { onboardingStatus: string } } | ||||
|         | { type: 'Toggle Debug Panel' }, | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
| @ -15,6 +15,7 @@ export interface Typegen0 { | ||||
|   eventsCausingActions: { | ||||
|     persistSettings: | ||||
|       | 'Set Base Unit' | ||||
|       | 'Set Camera Controls' | ||||
|       | 'Set Default Directory' | ||||
|       | 'Set Default Project Name' | ||||
|       | 'Set Onboarding Status' | ||||
| @ -24,6 +25,7 @@ export interface Typegen0 { | ||||
|       | 'Toggle Debug Panel' | ||||
|     setThemeClass: | ||||
|       | 'Set Base Unit' | ||||
|       | 'Set Camera Controls' | ||||
|       | 'Set Default Directory' | ||||
|       | 'Set Default Project Name' | ||||
|       | 'Set Onboarding Status' | ||||
| @ -34,6 +36,7 @@ export interface Typegen0 { | ||||
|       | 'xstate.init' | ||||
|     toastSuccess: | ||||
|       | 'Set Base Unit' | ||||
|       | 'Set Camera Controls' | ||||
|       | 'Set Default Directory' | ||||
|       | 'Set Default Project Name' | ||||
|       | 'Set Text Wrapping' | ||||
|  | ||||
| @ -4,8 +4,8 @@ import { onboardingPaths, useDismiss, useNextClick } from '.' | ||||
| import { useStore } from '../../useStore' | ||||
|  | ||||
| export default function Units() { | ||||
|   const { isMouseDownInStream } = useStore((s) => ({ | ||||
|     isMouseDownInStream: s.isMouseDownInStream, | ||||
|   const { buttonDownInStream } = useStore((s) => ({ | ||||
|     buttonDownInStream: s.buttonDownInStream, | ||||
|   })) | ||||
|   const dismiss = useDismiss() | ||||
|   const next = useNextClick(onboardingPaths.SKETCHING) | ||||
| @ -15,7 +15,7 @@ export default function Units() { | ||||
|       <div | ||||
|         className={ | ||||
|           'max-w-2xl flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' + | ||||
|           (isMouseDownInStream ? '' : ' pointer-events-auto') | ||||
|           (buttonDownInStream ? '' : ' pointer-events-auto') | ||||
|         } | ||||
|       > | ||||
|         <h1 className="text-2xl font-bold">Camera</h1> | ||||
|  | ||||
| @ -17,6 +17,11 @@ import { useHotkeys } from 'react-hotkeys-hook' | ||||
| import { IndexLoaderData, paths } from '../Router' | ||||
| import { Themes } from '../lib/theme' | ||||
| import { useGlobalStateContext } from 'hooks/useGlobalStateContext' | ||||
| import { | ||||
|   CADProgram, | ||||
|   cadPrograms, | ||||
|   cameraMouseDragGuards, | ||||
| } from 'lib/cameraControls' | ||||
| import { UnitSystem } from 'machines/settingsMachine' | ||||
|  | ||||
| export const Settings = () => { | ||||
| @ -29,12 +34,13 @@ export const Settings = () => { | ||||
|       send, | ||||
|       state: { | ||||
|         context: { | ||||
|           baseUnit, | ||||
|           cameraControls, | ||||
|           defaultDirectory, | ||||
|           defaultProjectName, | ||||
|           showDebugPanel, | ||||
|           defaultDirectory, | ||||
|           unitSystem, | ||||
|           baseUnit, | ||||
|           theme, | ||||
|           unitSystem, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
| @ -86,6 +92,42 @@ export const Settings = () => { | ||||
|           , and start a discussion if you don't see it! Your feedback will help | ||||
|           us prioritize what to build next. | ||||
|         </p> | ||||
|         <SettingsSection | ||||
|           title="Camera Controls" | ||||
|           description="How you want to control the camera in the 3D view" | ||||
|         > | ||||
|           <select | ||||
|             id="camera-controls" | ||||
|             className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent" | ||||
|             value={cameraControls} | ||||
|             onChange={(e) => { | ||||
|               send({ | ||||
|                 type: 'Set Camera Controls', | ||||
|                 data: { cameraControls: e.target.value as CADProgram }, | ||||
|               }) | ||||
|             }} | ||||
|           > | ||||
|             {cadPrograms.map((program) => ( | ||||
|               <option key={program} value={program}> | ||||
|                 {program} | ||||
|               </option> | ||||
|             ))} | ||||
|           </select> | ||||
|           <ul className="text-sm my-2 mx-4 leading-relaxed"> | ||||
|             <li> | ||||
|               <strong>Pan:</strong>{' '} | ||||
|               {cameraMouseDragGuards[cameraControls].pan.description} | ||||
|             </li> | ||||
|             <li> | ||||
|               <strong>Zoom:</strong>{' '} | ||||
|               {cameraMouseDragGuards[cameraControls].zoom.description} | ||||
|             </li> | ||||
|             <li> | ||||
|               <strong>Rotate:</strong>{' '} | ||||
|               {cameraMouseDragGuards[cameraControls].rotate.description} | ||||
|             </li> | ||||
|           </ul> | ||||
|         </SettingsSection> | ||||
|         {(window as any).__TAURI__ && ( | ||||
|           <> | ||||
|             <SettingsSection | ||||
|  | ||||
| @ -160,8 +160,8 @@ export interface StoreState { | ||||
|   setIsStreamReady: (isStreamReady: boolean) => void | ||||
|   isLSPServerReady: boolean | ||||
|   setIsLSPServerReady: (isLSPServerReady: boolean) => void | ||||
|   isMouseDownInStream: boolean | ||||
|   setIsMouseDownInStream: (isMouseDownInStream: boolean) => void | ||||
|   buttonDownInStream: number | ||||
|   setButtonDownInStream: (buttonDownInStream: number) => void | ||||
|   didDragInStream: boolean | ||||
|   setDidDragInStream: (didDragInStream: boolean) => void | ||||
|   fileId: string | ||||
| @ -356,9 +356,9 @@ export const useStore = create<StoreState>()( | ||||
|         setIsStreamReady: (isStreamReady) => set({ isStreamReady }), | ||||
|         isLSPServerReady: false, | ||||
|         setIsLSPServerReady: (isLSPServerReady) => set({ isLSPServerReady }), | ||||
|         isMouseDownInStream: false, | ||||
|         setIsMouseDownInStream: (isMouseDownInStream) => { | ||||
|           set({ isMouseDownInStream }) | ||||
|         buttonDownInStream: 0, | ||||
|         setButtonDownInStream: (buttonDownInStream) => { | ||||
|           set({ buttonDownInStream }) | ||||
|         }, | ||||
|         didDragInStream: false, | ||||
|         setDidDragInStream: (didDragInStream) => { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user