Add "Trackpad Friendly" camera control setting inspired by Blender (#431)

* Refactor: rename CADProgram to CameraSystem

* Fix buttonDownInStream always set to 0
This is problematic because the left mouse
button ID is actually 0. If no button is
pressed we should set back to undefined.

* Fix: middle mouse button ID is 1, not 3

* Add "Trackpad Friendly" camera system setting

Signed off by Frank Noirot <frank@kittycad.io>

* Allow camera configs to be lenient on first click
This commit is contained in:
Frank Noirot
2023-09-11 16:21:23 -04:00
committed by GitHub
parent 9e2a94fcd9
commit c5cb0e2fd4
6 changed files with 67 additions and 32 deletions

View File

@ -288,7 +288,7 @@ export function App() {
const newCmdId = uuidv4() const newCmdId = uuidv4()
if (buttonDownInStream) { if (buttonDownInStream !== undefined) {
const interactionGuards = cameraMouseDragGuards[cameraControls] const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type let interaction: CameraDragInteractionType_type
@ -303,6 +303,7 @@ export function App() {
} else { } else {
return return
} }
debounceSocketSend({ debounceSocketSend({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {

View File

@ -66,11 +66,20 @@ export const Stream = ({ className = '' }) => {
const interactionGuards = cameraMouseDragGuards[cameraControls] const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type let interaction: CameraDragInteractionType_type
if (interactionGuards.pan.callback(e)) { if (
interactionGuards.pan.callback(e) ||
interactionGuards.pan.lenientDragStartButton === e.button
) {
interaction = 'pan' interaction = 'pan'
} else if (interactionGuards.rotate.callback(e)) { } else if (
interactionGuards.rotate.callback(e) ||
interactionGuards.rotate.lenientDragStartButton === e.button
) {
interaction = 'rotate' interaction = 'rotate'
} else if (interactionGuards.zoom.dragCallback(e)) { } else if (
interactionGuards.zoom.dragCallback(e) ||
interactionGuards.zoom.lenientDragStartButton === e.button
) {
interaction = 'zoom' interaction = 'zoom'
} else { } else {
return return
@ -93,7 +102,6 @@ export const Stream = ({ className = '' }) => {
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => { const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
e.preventDefault()
engineCommandManager?.sendSceneCommand({ engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
@ -130,7 +138,7 @@ export const Stream = ({ className = '' }) => {
cmd_id: newCmdId, cmd_id: newCmdId,
}) })
setButtonDownInStream(0) setButtonDownInStream(undefined)
if (!didDragInStream) { if (!didDragInStream) {
engineCommandManager?.sendSceneCommand({ engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',

View File

@ -1,17 +1,19 @@
const noModifiersPressed = (e: React.MouseEvent) => const noModifiersPressed = (e: React.MouseEvent) =>
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
export type CADProgram = export type CameraSystem =
| 'KittyCAD' | 'KittyCAD'
| 'OnShape' | 'OnShape'
| 'Trackpad Friendly'
| 'Solidworks' | 'Solidworks'
| 'NX' | 'NX'
| 'Creo' | 'Creo'
| 'AutoCAD' | 'AutoCAD'
export const cadPrograms: CADProgram[] = [ export const cameraSystems: CameraSystem[] = [
'KittyCAD', 'KittyCAD',
'OnShape', 'OnShape',
'Trackpad Friendly',
'Solidworks', 'Solidworks',
'NX', 'NX',
'Creo', 'Creo',
@ -21,12 +23,14 @@ export const cadPrograms: CADProgram[] = [
interface MouseGuardHandler { interface MouseGuardHandler {
description: string description: string
callback: (e: React.MouseEvent) => boolean callback: (e: React.MouseEvent) => boolean
lenientDragStartButton?: number
} }
interface MouseGuardZoomHandler { interface MouseGuardZoomHandler {
description: string description: string
dragCallback: (e: React.MouseEvent) => boolean dragCallback: (e: React.MouseEvent) => boolean
scrollCallback: (e: React.MouseEvent) => boolean scrollCallback: (e: React.MouseEvent) => boolean
lenientDragStartButton?: number
} }
interface MouseGuard { interface MouseGuard {
@ -35,12 +39,12 @@ interface MouseGuard {
rotate: MouseGuardHandler rotate: MouseGuardHandler
} }
export const cameraMouseDragGuards: Record<CADProgram, MouseGuard> = { export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
KittyCAD: { KittyCAD: {
pan: { pan: {
description: 'Right click + Shift + drag or middle click + drag', description: 'Right click + Shift + drag or middle click + drag',
callback: (e) => callback: (e) =>
(e.button === 3 && noModifiersPressed(e)) || (e.button === 1 && noModifiersPressed(e)) ||
(e.button === 2 && e.shiftKey), (e.button === 2 && e.shiftKey),
}, },
zoom: { zoom: {
@ -58,7 +62,7 @@ export const cameraMouseDragGuards: Record<CADProgram, MouseGuard> = {
description: 'Right click + Ctrl + drag or middle click + drag', description: 'Right click + Ctrl + drag or middle click + drag',
callback: (e) => callback: (e) =>
(e.button === 2 && e.ctrlKey) || (e.button === 2 && e.ctrlKey) ||
(e.button === 3 && noModifiersPressed(e)), (e.button === 1 && noModifiersPressed(e)),
}, },
zoom: { zoom: {
description: 'Scroll wheel', description: 'Scroll wheel',
@ -70,55 +74,74 @@ export const cameraMouseDragGuards: Record<CADProgram, MouseGuard> = {
callback: (e) => e.button === 2 && noModifiersPressed(e), callback: (e) => e.button === 2 && noModifiersPressed(e),
}, },
}, },
'Trackpad Friendly': {
pan: {
description: 'Left click + Alt + Shift + drag or middle click + drag',
callback: (e) =>
(e.button === 0 && e.altKey && e.shiftKey && !e.metaKey) ||
(e.button === 1 && noModifiersPressed(e)),
},
zoom: {
description: 'Scroll wheel or Left click + Alt + OS + drag',
dragCallback: (e) => e.button === 0 && e.altKey && e.metaKey,
scrollCallback: () => true,
},
rotate: {
description: 'Left click + Alt + drag',
callback: (e) => e.button === 0 && e.altKey && !e.shiftKey && !e.metaKey,
lenientDragStartButton: 0,
},
},
Solidworks: { Solidworks: {
pan: { pan: {
description: 'Right click + Ctrl + drag', description: 'Right click + Ctrl + drag',
callback: (e) => e.button === 2 && e.ctrlKey, callback: (e) => e.button === 2 && e.ctrlKey,
lenientDragStartButton: 2,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Middle click + Shift + drag', description: 'Scroll wheel or Middle click + Shift + drag',
dragCallback: (e) => e.button === 3 && e.shiftKey, dragCallback: (e) => e.button === 1 && e.shiftKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle click + drag', description: 'Middle click + drag',
callback: (e) => e.button === 3 && noModifiersPressed(e), callback: (e) => e.button === 1 && noModifiersPressed(e),
}, },
}, },
NX: { NX: {
pan: { pan: {
description: 'Middle click + Shift + drag', description: 'Middle click + Shift + drag',
callback: (e) => e.button === 3 && e.shiftKey, callback: (e) => e.button === 1 && e.shiftKey,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Middle click + Ctrl + drag', description: 'Scroll wheel or Middle click + Ctrl + drag',
dragCallback: (e) => e.button === 3 && e.ctrlKey, dragCallback: (e) => e.button === 1 && e.ctrlKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle click + drag', description: 'Middle click + drag',
callback: (e) => e.button === 3 && noModifiersPressed(e), callback: (e) => e.button === 1 && noModifiersPressed(e),
}, },
}, },
Creo: { Creo: {
pan: { pan: {
description: 'Middle click + Shift + drag', description: 'Middle click + Shift + drag',
callback: (e) => e.button === 3 && e.shiftKey, callback: (e) => e.button === 1 && e.shiftKey,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Middle click + Ctrl + drag', description: 'Scroll wheel or Middle click + Ctrl + drag',
dragCallback: (e) => e.button === 3 && e.ctrlKey, dragCallback: (e) => e.button === 1 && e.ctrlKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle click + drag', description: 'Middle click + drag',
callback: (e) => e.button === 3 && noModifiersPressed(e), callback: (e) => e.button === 1 && noModifiersPressed(e),
}, },
}, },
AutoCAD: { AutoCAD: {
pan: { pan: {
description: 'Middle click + drag', description: 'Middle click + drag',
callback: (e) => e.button === 3 && noModifiersPressed(e), callback: (e) => e.button === 1 && noModifiersPressed(e),
}, },
zoom: { zoom: {
description: 'Scroll wheel', description: 'Scroll wheel',
@ -127,7 +150,7 @@ export const cameraMouseDragGuards: Record<CADProgram, MouseGuard> = {
}, },
rotate: { rotate: {
description: 'Middle click + Shift + drag', description: 'Middle click + Shift + drag',
callback: (e) => e.button === 3 && e.shiftKey, callback: (e) => e.button === 1 && e.shiftKey,
}, },
}, },
} }

View File

@ -1,7 +1,7 @@
import { assign, createMachine } from 'xstate' import { assign, createMachine } from 'xstate'
import { CommandBarMeta } from '../lib/commands' import { CommandBarMeta } from '../lib/commands'
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme' import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
import { CADProgram, cadPrograms } from 'lib/cameraControls' import { CameraSystem, cameraSystems } from 'lib/cameraControls'
export const DEFAULT_PROJECT_NAME = 'project-$nnn' export const DEFAULT_PROJECT_NAME = 'project-$nnn'
@ -42,7 +42,7 @@ export const settingsCommandBarMeta: CommandBarMeta = {
name: 'cameraControls', name: 'cameraControls',
type: 'select', type: 'select',
defaultValue: 'cameraControls', defaultValue: 'cameraControls',
options: Object.values(cadPrograms).map((v) => ({ name: v })), options: Object.values(cameraSystems).map((v) => ({ name: v })),
}, },
], ],
}, },
@ -109,7 +109,7 @@ export const settingsMachine = createMachine(
predictableActionArguments: true, predictableActionArguments: true,
context: { context: {
baseUnit: 'in' as BaseUnit, baseUnit: 'in' as BaseUnit,
cameraControls: 'KittyCAD' as CADProgram, cameraControls: 'KittyCAD' as CameraSystem,
defaultDirectory: '', defaultDirectory: '',
defaultProjectName: DEFAULT_PROJECT_NAME, defaultProjectName: DEFAULT_PROJECT_NAME,
onboardingStatus: '', onboardingStatus: '',
@ -232,7 +232,10 @@ export const settingsMachine = createMachine(
schema: { schema: {
events: {} as events: {} as
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } } | { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
| { type: 'Set Camera Controls'; data: { cameraControls: CADProgram } } | {
type: 'Set Camera Controls'
data: { cameraControls: CameraSystem }
}
| { type: 'Set Default Directory'; data: { defaultDirectory: string } } | { type: 'Set Default Directory'; data: { defaultDirectory: string } }
| { | {
type: 'Set Default Project Name' type: 'Set Default Project Name'

View File

@ -18,8 +18,8 @@ import { IndexLoaderData, paths } from '../Router'
import { Themes } from '../lib/theme' import { Themes } from '../lib/theme'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { import {
CADProgram, CameraSystem,
cadPrograms, cameraSystems,
cameraMouseDragGuards, cameraMouseDragGuards,
} from 'lib/cameraControls' } from 'lib/cameraControls'
import { UnitSystem } from 'machines/settingsMachine' import { UnitSystem } from 'machines/settingsMachine'
@ -103,11 +103,11 @@ export const Settings = () => {
onChange={(e) => { onChange={(e) => {
send({ send({
type: 'Set Camera Controls', type: 'Set Camera Controls',
data: { cameraControls: e.target.value as CADProgram }, data: { cameraControls: e.target.value as CameraSystem },
}) })
}} }}
> >
{cadPrograms.map((program) => ( {cameraSystems.map((program) => (
<option key={program} value={program}> <option key={program} value={program}>
{program} {program}
</option> </option>

View File

@ -160,8 +160,8 @@ export interface StoreState {
setIsStreamReady: (isStreamReady: boolean) => void setIsStreamReady: (isStreamReady: boolean) => void
isLSPServerReady: boolean isLSPServerReady: boolean
setIsLSPServerReady: (isLSPServerReady: boolean) => void setIsLSPServerReady: (isLSPServerReady: boolean) => void
buttonDownInStream: number buttonDownInStream: number | undefined
setButtonDownInStream: (buttonDownInStream: number) => void setButtonDownInStream: (buttonDownInStream: number | undefined) => void
didDragInStream: boolean didDragInStream: boolean
setDidDragInStream: (didDragInStream: boolean) => void setDidDragInStream: (didDragInStream: boolean) => void
fileId: string fileId: string
@ -356,7 +356,7 @@ export const useStore = create<StoreState>()(
setIsStreamReady: (isStreamReady) => set({ isStreamReady }), setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
isLSPServerReady: false, isLSPServerReady: false,
setIsLSPServerReady: (isLSPServerReady) => set({ isLSPServerReady }), setIsLSPServerReady: (isLSPServerReady) => set({ isLSPServerReady }),
buttonDownInStream: 0, buttonDownInStream: undefined,
setButtonDownInStream: (buttonDownInStream) => { setButtonDownInStream: (buttonDownInStream) => {
set({ buttonDownInStream }) set({ buttonDownInStream })
}, },