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 { 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')}
|
||||||
/>
|
/>
|
||||||
|
@ -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
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 { 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' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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'
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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) => {
|
||||||
|
Reference in New Issue
Block a user