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:
Frank Noirot
2023-09-08 10:13:35 -04:00
committed by GitHub
parent 97a0b6a543
commit c68fbbd89d
8 changed files with 344 additions and 113 deletions

View File

@ -55,6 +55,8 @@ import { onboardingPaths } from 'routes/Onboarding'
import { LanguageServerClient } from 'editor/lsp' import { LanguageServerClient } from 'editor/lsp'
import kclLanguage from 'editor/lsp/language' import kclLanguage from 'editor/lsp/language'
import { CSSRuleObject } from 'tailwindcss/types/config' import { CSSRuleObject } from 'tailwindcss/types/config'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
export function App() { export function App() {
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
@ -88,7 +90,7 @@ export function App() {
isStreamReady, isStreamReady,
isLSPServerReady, isLSPServerReady,
setIsLSPServerReady, setIsLSPServerReady,
isMouseDownInStream, buttonDownInStream,
formatCode, formatCode,
openPanes, openPanes,
setOpenPanes, setOpenPanes,
@ -129,7 +131,7 @@ export function App() {
setIsStreamReady: s.setIsStreamReady, setIsStreamReady: s.setIsStreamReady,
isLSPServerReady: s.isLSPServerReady, isLSPServerReady: s.isLSPServerReady,
setIsLSPServerReady: s.setIsLSPServerReady, setIsLSPServerReady: s.setIsLSPServerReady,
isMouseDownInStream: s.isMouseDownInStream, buttonDownInStream: s.buttonDownInStream,
formatCode: s.formatCode, formatCode: s.formatCode,
addKCLError: s.addKCLError, addKCLError: s.addKCLError,
openPanes: s.openPanes, openPanes: s.openPanes,
@ -145,7 +147,13 @@ export function App() {
context: { token }, context: { token },
}, },
settings: { settings: {
context: { showDebugPanel, theme, onboardingStatus, textWrapping }, context: {
showDebugPanel,
theme,
onboardingStatus,
textWrapping,
cameraControls,
},
}, },
} = useGlobalStateContext() } = useGlobalStateContext()
@ -389,28 +397,33 @@ export function App() {
const debounceSocketSend = throttle<EngineCommand>((message) => { const debounceSocketSend = throttle<EngineCommand>((message) => {
engineCommandManager?.sendSceneCommand(message) engineCommandManager?.sendSceneCommand(message)
}, 16) }, 16)
const handleMouseMove: MouseEventHandler<HTMLDivElement> = ({ const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
clientX, e.nativeEvent.preventDefault()
clientY,
ctrlKey,
shiftKey,
currentTarget,
nativeEvent,
}) => {
nativeEvent.preventDefault()
const { x, y } = getNormalisedCoordinates({ const { x, y } = getNormalisedCoordinates({
clientX, clientX: e.clientX,
clientY, clientY: e.clientY,
el: currentTarget, el: e.currentTarget,
...streamDimensions, ...streamDimensions,
}) })
const interaction = ctrlKey ? 'zoom' : shiftKey ? 'pan' : 'rotate'
const newCmdId = uuidv4() 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({ debounceSocketSend({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
@ -500,7 +513,7 @@ export function App() {
className={ className={
'transition-opacity transition-duration-75 ' + 'transition-opacity transition-duration-75 ' +
paneOpacity + paneOpacity +
(isMouseDownInStream ? ' pointer-events-none' : '') (buttonDownInStream ? ' pointer-events-none' : '')
} }
project={project} project={project}
enableMenu={true} enableMenu={true}
@ -509,7 +522,7 @@ export function App() {
<Resizable <Resizable
className={ className={
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' + '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 ' ? ' pointer-events-none '
: ' ') + : ' ') +
paneOpacity paneOpacity
@ -588,7 +601,7 @@ export function App() {
className={ className={
'transition-opacity transition-duration-75 ' + 'transition-opacity transition-duration-75 ' +
paneOpacity + paneOpacity +
(isMouseDownInStream ? ' pointer-events-none' : '') (buttonDownInStream ? ' pointer-events-none' : '')
} }
open={openPanes.includes('debug')} open={openPanes.includes('debug')}
/> />

View File

@ -9,6 +9,9 @@ import { v4 as uuidv4 } from 'uuid'
import { useStore } from '../useStore' import { useStore } from '../useStore'
import { getNormalisedCoordinates } from '../lib/utils' import { getNormalisedCoordinates } from '../lib/utils'
import Loading from './Loading' 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 = '' }) => { export const Stream = ({ className = '' }) => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
@ -17,7 +20,7 @@ export const Stream = ({ className = '' }) => {
const { const {
mediaStream, mediaStream,
engineCommandManager, engineCommandManager,
setIsMouseDownInStream, setButtonDownInStream,
didDragInStream, didDragInStream,
setDidDragInStream, setDidDragInStream,
streamDimensions, streamDimensions,
@ -25,14 +28,18 @@ export const Stream = ({ className = '' }) => {
} = useStore((s) => ({ } = useStore((s) => ({
mediaStream: s.mediaStream, mediaStream: s.mediaStream,
engineCommandManager: s.engineCommandManager, engineCommandManager: s.engineCommandManager,
isMouseDownInStream: s.isMouseDownInStream, setButtonDownInStream: s.setButtonDownInStream,
setIsMouseDownInStream: s.setIsMouseDownInStream,
fileId: s.fileId, fileId: s.fileId,
didDragInStream: s.didDragInStream, didDragInStream: s.didDragInStream,
setDidDragInStream: s.setDidDragInStream, setDidDragInStream: s.setDidDragInStream,
streamDimensions: s.streamDimensions, streamDimensions: s.streamDimensions,
isExecuting: s.isExecuting, isExecuting: s.isExecuting,
})) }))
const {
settings: {
context: { cameraControls },
},
} = useGlobalStateContext()
useEffect(() => { useEffect(() => {
if ( if (
@ -45,23 +52,29 @@ export const Stream = ({ className = '' }) => {
videoRef.current.srcObject = mediaStream videoRef.current.srcObject = mediaStream
}, [mediaStream, engineCommandManager]) }, [mediaStream, engineCommandManager])
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = ({ const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
clientX,
clientY,
ctrlKey,
}) => {
if (!videoRef.current) return if (!videoRef.current) return
const { x, y } = getNormalisedCoordinates({ const { x, y } = getNormalisedCoordinates({
clientX, clientX: e.clientX,
clientY, clientY: e.clientY,
el: videoRef.current, el: videoRef.current,
...streamDimensions, ...streamDimensions,
}) })
console.log('click', x, y)
const newId = uuidv4() 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({ engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -73,11 +86,13 @@ export const Stream = ({ className = '' }) => {
cmd_id: newId, cmd_id: newId,
}) })
setIsMouseDownInStream(true) setButtonDownInStream(e.button)
setClickCoords({ x, y }) setClickCoords({ x, y })
} }
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => { const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
e.preventDefault() e.preventDefault()
engineCommandManager?.sendSceneCommand({ engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -115,7 +130,7 @@ export const Stream = ({ className = '' }) => {
cmd_id: newCmdId, cmd_id: newCmdId,
}) })
setIsMouseDownInStream(false) setButtonDownInStream(0)
if (!didDragInStream) { if (!didDragInStream) {
engineCommandManager?.sendSceneCommand({ engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',

133
src/lib/cameraControls.ts Normal file
View 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,
},
},
}

View File

@ -1,6 +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'
export const DEFAULT_PROJECT_NAME = 'project-$nnn' 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 SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
export const settingsCommandBarMeta: CommandBarMeta = { export const settingsCommandBarMeta: CommandBarMeta = {
'Set Theme': { 'Set Base Unit': {
displayValue: (args: string[]) => 'Change the app theme', displayValue: (args: string[]) => 'Set your default base unit',
args: [ args: [
{ {
name: 'theme', name: 'baseUnit',
type: 'select', type: 'select',
defaultValue: 'theme', defaultValue: 'baseUnit',
options: Object.values(Themes).map((v) => ({ name: v })) as { options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
name: string
}[],
}, },
], ],
}, },
'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': { 'Set Default Project Name': {
displayValue: (args: string[]) => 'Set a new default project name', displayValue: (args: string[]) => 'Set a new default project name',
hide: 'web', hide: 'web',
@ -49,31 +62,9 @@ export const settingsCommandBarMeta: CommandBarMeta = {
}, },
], ],
}, },
'Set Default Directory': { 'Set Onboarding Status': {
hide: 'both', 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': { 'Set Text Wrapping': {
displayValue: (args: string[]) => 'Set whether text in the editor wraps', displayValue: (args: string[]) => 'Set whether text in the editor wraps',
args: [ args: [
@ -85,8 +76,29 @@ export const settingsCommandBarMeta: CommandBarMeta = {
}, },
], ],
}, },
'Set Onboarding Status': { 'Set Theme': {
hide: 'both', 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', id: 'Settings',
predictableActionArguments: true, predictableActionArguments: true,
context: { context: {
theme: Themes.System,
defaultProjectName: DEFAULT_PROJECT_NAME,
unitSystem: UnitSystem.Imperial,
baseUnit: 'in' as BaseUnit, baseUnit: 'in' as BaseUnit,
cameraControls: 'KittyCAD' as CADProgram,
defaultDirectory: '', defaultDirectory: '',
textWrapping: 'On' as Toggle, defaultProjectName: DEFAULT_PROJECT_NAME,
showDebugPanel: false,
onboardingStatus: '', onboardingStatus: '',
showDebugPanel: false,
textWrapping: 'On' as Toggle,
theme: Themes.System,
unitSystem: UnitSystem.Imperial,
}, },
initial: 'idle', initial: 'idle',
states: { states: {
idle: { idle: {
entry: ['setThemeClass'], entry: ['setThemeClass'],
on: { on: {
'Set Theme': { 'Set Base Unit': {
actions: [ actions: [
assign({ assign({ baseUnit: (_, event) => event.data.baseUnit }),
theme: (_, event) => event.data.theme,
}),
'persistSettings', 'persistSettings',
'toastSuccess', 'toastSuccess',
'setThemeClass',
], ],
target: 'idle', target: 'idle',
internal: true, internal: true,
}, },
'Set Default Project Name': { 'Set Camera Controls': {
actions: [ actions: [
assign({ assign({
defaultProjectName: (_, event) => cameraControls: (_, event) => event.data.cameraControls,
event.data.defaultProjectName.trim() || DEFAULT_PROJECT_NAME,
}), }),
'persistSettings', 'persistSettings',
'toastSuccess', 'toastSuccess',
@ -145,12 +154,11 @@ export const settingsMachine = createMachine(
target: 'idle', target: 'idle',
internal: true, internal: true,
}, },
'Set Unit System': { 'Set Default Project Name': {
actions: [ actions: [
assign({ assign({
unitSystem: (_, event) => event.data.unitSystem, defaultProjectName: (_, event) =>
baseUnit: (_, event) => event.data.defaultProjectName.trim() || DEFAULT_PROJECT_NAME,
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
}), }),
'persistSettings', 'persistSettings',
'toastSuccess', 'toastSuccess',
@ -158,11 +166,12 @@ export const settingsMachine = createMachine(
target: 'idle', target: 'idle',
internal: true, internal: true,
}, },
'Set Base Unit': { 'Set Onboarding Status': {
actions: [ actions: [
assign({ baseUnit: (_, event) => event.data.baseUnit }), assign({
onboardingStatus: (_, event) => event.data.onboardingStatus,
}),
'persistSettings', 'persistSettings',
'toastSuccess',
], ],
target: 'idle', target: 'idle',
internal: true, internal: true,
@ -178,6 +187,31 @@ export const settingsMachine = createMachine(
target: 'idle', target: 'idle',
internal: true, 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': { 'Toggle Debug Panel': {
actions: [ actions: [
assign({ assign({
@ -191,35 +225,26 @@ export const settingsMachine = createMachine(
target: 'idle', target: 'idle',
internal: true, internal: true,
}, },
'Set Onboarding Status': {
actions: [
assign({
onboardingStatus: (_, event) => event.data.onboardingStatus,
}),
'persistSettings',
],
target: 'idle',
internal: true,
},
}, },
}, },
}, },
tsTypes: {} as import('./settingsMachine.typegen').Typegen0, tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
schema: { schema: {
events: {} as 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' type: 'Set Default Project Name'
data: { defaultProjectName: string } 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' type: 'Set Unit System'
data: { unitSystem: UnitSystem } 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' }, | { type: 'Toggle Debug Panel' },
}, },
}, },

View File

@ -15,6 +15,7 @@ export interface Typegen0 {
eventsCausingActions: { eventsCausingActions: {
persistSettings: persistSettings:
| 'Set Base Unit' | 'Set Base Unit'
| 'Set Camera Controls'
| 'Set Default Directory' | 'Set Default Directory'
| 'Set Default Project Name' | 'Set Default Project Name'
| 'Set Onboarding Status' | 'Set Onboarding Status'
@ -24,6 +25,7 @@ export interface Typegen0 {
| 'Toggle Debug Panel' | 'Toggle Debug Panel'
setThemeClass: setThemeClass:
| 'Set Base Unit' | 'Set Base Unit'
| 'Set Camera Controls'
| 'Set Default Directory' | 'Set Default Directory'
| 'Set Default Project Name' | 'Set Default Project Name'
| 'Set Onboarding Status' | 'Set Onboarding Status'
@ -34,6 +36,7 @@ export interface Typegen0 {
| 'xstate.init' | 'xstate.init'
toastSuccess: toastSuccess:
| 'Set Base Unit' | 'Set Base Unit'
| 'Set Camera Controls'
| 'Set Default Directory' | 'Set Default Directory'
| 'Set Default Project Name' | 'Set Default Project Name'
| 'Set Text Wrapping' | 'Set Text Wrapping'

View File

@ -4,8 +4,8 @@ import { onboardingPaths, useDismiss, useNextClick } from '.'
import { useStore } from '../../useStore' import { useStore } from '../../useStore'
export default function Units() { export default function Units() {
const { isMouseDownInStream } = useStore((s) => ({ const { buttonDownInStream } = useStore((s) => ({
isMouseDownInStream: s.isMouseDownInStream, buttonDownInStream: s.buttonDownInStream,
})) }))
const dismiss = useDismiss() const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.SKETCHING) const next = useNextClick(onboardingPaths.SKETCHING)
@ -15,7 +15,7 @@ export default function Units() {
<div <div
className={ className={
'max-w-2xl flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' + '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> <h1 className="text-2xl font-bold">Camera</h1>

View File

@ -17,6 +17,11 @@ import { useHotkeys } from 'react-hotkeys-hook'
import { IndexLoaderData, paths } from '../Router' 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 {
CADProgram,
cadPrograms,
cameraMouseDragGuards,
} from 'lib/cameraControls'
import { UnitSystem } from 'machines/settingsMachine' import { UnitSystem } from 'machines/settingsMachine'
export const Settings = () => { export const Settings = () => {
@ -29,12 +34,13 @@ export const Settings = () => {
send, send,
state: { state: {
context: { context: {
baseUnit,
cameraControls,
defaultDirectory,
defaultProjectName, defaultProjectName,
showDebugPanel, showDebugPanel,
defaultDirectory,
unitSystem,
baseUnit,
theme, theme,
unitSystem,
}, },
}, },
}, },
@ -86,6 +92,42 @@ export const Settings = () => {
, and start a discussion if you don't see it! Your feedback will help , and start a discussion if you don't see it! Your feedback will help
us prioritize what to build next. us prioritize what to build next.
</p> </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__ && ( {(window as any).__TAURI__ && (
<> <>
<SettingsSection <SettingsSection

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
isMouseDownInStream: boolean buttonDownInStream: number
setIsMouseDownInStream: (isMouseDownInStream: boolean) => void setButtonDownInStream: (buttonDownInStream: number) => void
didDragInStream: boolean didDragInStream: boolean
setDidDragInStream: (didDragInStream: boolean) => void setDidDragInStream: (didDragInStream: boolean) => void
fileId: string fileId: string
@ -356,9 +356,9 @@ 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 }),
isMouseDownInStream: false, buttonDownInStream: 0,
setIsMouseDownInStream: (isMouseDownInStream) => { setButtonDownInStream: (buttonDownInStream) => {
set({ isMouseDownInStream }) set({ buttonDownInStream })
}, },
didDragInStream: false, didDragInStream: false,
setDidDragInStream: (didDragInStream) => { setDidDragInStream: (didDragInStream) => {