diff --git a/src/App.tsx b/src/App.tsx index be7fda9fe..7bb56d58b 100644 --- a/src/App.tsx +++ b/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((message) => { engineCommandManager?.sendSceneCommand(message) }, 16) - const handleMouseMove: MouseEventHandler = ({ - clientX, - clientY, - ctrlKey, - shiftKey, - currentTarget, - nativeEvent, - }) => { - nativeEvent.preventDefault() + const handleMouseMove: MouseEventHandler = (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() { diff --git a/src/components/Stream.tsx b/src/components/Stream.tsx index 0a7363b57..b5b1af076 100644 --- a/src/components/Stream.tsx +++ b/src/components/Stream.tsx @@ -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 = ({ - clientX, - clientY, - ctrlKey, - }) => { + const handleMouseDown: MouseEventHandler = (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 = (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', diff --git a/src/lib/cameraControls.ts b/src/lib/cameraControls.ts new file mode 100644 index 000000000..ca34fa460 --- /dev/null +++ b/src/lib/cameraControls.ts @@ -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 = { + 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, + }, + }, +} diff --git a/src/machines/settingsMachine.ts b/src/machines/settingsMachine.ts index 37aa3fa39..2f6c26c21 100644 --- a/src/machines/settingsMachine.ts +++ b/src/machines/settingsMachine.ts @@ -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' }, }, }, diff --git a/src/machines/settingsMachine.typegen.ts b/src/machines/settingsMachine.typegen.ts index 397fae0a9..eb279146c 100644 --- a/src/machines/settingsMachine.typegen.ts +++ b/src/machines/settingsMachine.typegen.ts @@ -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' diff --git a/src/routes/Onboarding/Camera.tsx b/src/routes/Onboarding/Camera.tsx index f2d5fa993..c518ae555 100644 --- a/src/routes/Onboarding/Camera.tsx +++ b/src/routes/Onboarding/Camera.tsx @@ -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() {

Camera

diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx index de1bd8b7a..cabb32be4 100644 --- a/src/routes/Settings.tsx +++ b/src/routes/Settings.tsx @@ -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.

+ + +
    +
  • + Pan:{' '} + {cameraMouseDragGuards[cameraControls].pan.description} +
  • +
  • + Zoom:{' '} + {cameraMouseDragGuards[cameraControls].zoom.description} +
  • +
  • + Rotate:{' '} + {cameraMouseDragGuards[cameraControls].rotate.description} +
  • +
+
{(window as any).__TAURI__ && ( <> 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()( 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) => {