Compare commits

...

5 Commits

8 changed files with 266 additions and 50 deletions

1
interface.d.ts vendored
View File

@ -69,6 +69,7 @@ export interface IElectronAPI {
kittycad: (access: string, args: any) => any kittycad: (access: string, args: any) => any
listMachines: () => Promise<MachinesListing> listMachines: () => Promise<MachinesListing>
getMachineApiIp: () => Promise<string | null> getMachineApiIp: () => Promise<string | null>
readNaturalScrollDirection: () => Promise<boolean>
onUpdateDownloaded: ( onUpdateDownloaded: (
callback: (value: string) => void callback: (value: string) => void
) => Electron.IpcRenderer ) => Electron.IpcRenderer

View File

@ -22,7 +22,7 @@ import {
UnreliableSubscription, UnreliableSubscription,
} from 'lang/std/engineConnection' } from 'lang/std/engineConnection'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'
import { toSync, uuidv4 } from 'lib/utils' import { cachedNaturalScrollDirection, toSync, uuidv4 } from 'lib/utils'
import { deg2Rad } from 'lib/utils2d' import { deg2Rad } from 'lib/utils2d'
import { isReducedMotion, roundOff, throttle } from 'lib/utils' import { isReducedMotion, roundOff, throttle } from 'lib/utils'
import * as TWEEN from '@tweenjs/tween.js' import * as TWEEN from '@tweenjs/tween.js'
@ -78,8 +78,13 @@ export class CameraControls {
enablePan = true enablePan = true
enableZoom = true enableZoom = true
zoomDataFromLastFrame?: number = undefined zoomDataFromLastFrame?: number = undefined
// holds coordinates, and interaction // Holds event type, coordinates (for wheel, it's delta), and interaction
moveDataFromLastFrame?: [number, number, string] = undefined moveDataFromLastFrame?: [
'pointer' | 'wheel',
number,
number,
interactionType
] = undefined
lastPerspectiveFov: number = 45 lastPerspectiveFov: number = 45
pendingZoom: number | null = null pendingZoom: number | null = null
pendingRotation: Vector2 | null = null pendingRotation: Vector2 | null = null
@ -283,19 +288,75 @@ export class CameraControls {
const doMove = () => { const doMove = () => {
if (this.moveDataFromLastFrame !== undefined) { if (this.moveDataFromLastFrame !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises const interaction = this.moveDataFromLastFrame[3]
this.engineCommandManager.sendSceneCommand({ if (this.moveDataFromLastFrame[0] === 'pointer') {
type: 'modeling_cmd_req', this.engineCommandManager
cmd: { .sendSceneCommand({
type: 'camera_drag_move', type: 'modeling_cmd_req',
interaction: this.moveDataFromLastFrame[2] as any, cmd: {
window: { type: 'camera_drag_move',
x: this.moveDataFromLastFrame[0], interaction,
y: this.moveDataFromLastFrame[1], window: {
}, x: this.moveDataFromLastFrame[1],
}, y: this.moveDataFromLastFrame[2],
cmd_id: uuidv4(), },
}) },
cmd_id: uuidv4(),
})
.catch(reportRejection)
} else if (this.moveDataFromLastFrame[0] === 'wheel') {
const deltaX = this.moveDataFromLastFrame[1]
const deltaY = this.moveDataFromLastFrame[2]
this.isDragging = true
this.handleStart()
this.engineCommandManager
.sendSceneCommand({
type: 'modeling_cmd_batch_req',
batch_id: uuidv4(),
requests: [
{
cmd: {
type: 'camera_drag_start',
interaction,
window: { x: 0, y: 0 },
},
cmd_id: uuidv4(),
},
{
cmd: {
type: 'camera_drag_move',
interaction,
window: {
x: -deltaX,
y: -deltaY,
},
},
cmd_id: uuidv4(),
},
{
cmd: {
type: 'camera_drag_end',
interaction,
window: {
x: -deltaX,
y: -deltaY,
},
},
cmd_id: uuidv4(),
},
],
responses: false,
})
.catch(reportRejection)
this.isDragging = false
this.handleEnd()
} else {
console.error(
`Unknown moveDataFromLastFrame event type: ${this.moveDataFromLastFrame[0]}`
)
}
} }
this.moveDataFromLastFrame = undefined this.moveDataFromLastFrame = undefined
} }
@ -386,32 +447,16 @@ export class CameraControls {
if (interaction === 'none') return if (interaction === 'none') return
if (this.syncDirection === 'engineToClient') { if (this.syncDirection === 'engineToClient') {
this.moveDataFromLastFrame = [event.clientX, event.clientY, interaction] this.moveDataFromLastFrame = [
'pointer',
event.clientX,
event.clientY,
interaction,
]
return return
} }
// Implement camera movement logic here based on deltaMove this.moveCamera(interaction, deltaMove)
// For example, for rotating the camera around the target:
if (interaction === 'rotate') {
this.pendingRotation = this.pendingRotation
? this.pendingRotation
: new Vector2()
this.pendingRotation.x += deltaMove.x
this.pendingRotation.y += deltaMove.y
} else if (interaction === 'zoom') {
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
this.pendingZoom *= 1 + deltaMove.y * 0.01
} else if (interaction === 'pan') {
this.pendingPan = this.pendingPan ? this.pendingPan : new Vector2()
let distance = this.camera.position.distanceTo(this.target)
if (this.camera instanceof OrthographicCamera) {
const zoomFudgeFactor = 2280
distance = zoomFudgeFactor / (this.camera.zoom * 45)
}
const panSpeed = (distance / 1000 / 45) * this.perspectiveFovBeforeOrtho
this.pendingPan.x += -deltaMove.x * panSpeed
this.pendingPan.y += deltaMove.y * panSpeed
}
} else { } else {
/** /**
* If we're not in sketch mode and not dragging, we can highlight entities * If we're not in sketch mode and not dragging, we can highlight entities
@ -433,6 +478,31 @@ export class CameraControls {
} }
} }
moveCamera(interaction: interactionType, deltaMove: Vector2) {
// Implement camera movement logic here based on deltaMove
// For example, for rotating the camera around the target:
if (interaction === 'rotate') {
this.pendingRotation = this.pendingRotation
? this.pendingRotation
: new Vector2()
this.pendingRotation.x += deltaMove.x
this.pendingRotation.y += deltaMove.y
} else if (interaction === 'zoom') {
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
this.pendingZoom *= 1 + deltaMove.y * 0.01
} else if (interaction === 'pan') {
this.pendingPan = this.pendingPan ? this.pendingPan : new Vector2()
let distance = this.camera.position.distanceTo(this.target)
if (this.camera instanceof OrthographicCamera) {
const zoomFudgeFactor = 2280
distance = zoomFudgeFactor / (this.camera.zoom * 45)
}
const panSpeed = (distance / 1000 / 45) * this.perspectiveFovBeforeOrtho
this.pendingPan.x += -deltaMove.x * panSpeed
this.pendingPan.y += deltaMove.y * panSpeed
}
}
onMouseUp = (event: PointerEvent) => { onMouseUp = (event: PointerEvent) => {
this.domElement.releasePointerCapture(event.pointerId) this.domElement.releasePointerCapture(event.pointerId)
this.isDragging = false this.isDragging = false
@ -452,6 +522,20 @@ export class CameraControls {
} }
} }
zoomDirection = (event: WheelEvent): 1 | -1 => {
if (this.interactionGuards.zoom.pinchToZoom && isPinchToZoom(event)) {
return 1
}
if (!this.interactionGuards.zoom.scrollAllowInvertY) return 1
// Safari provides the updated user setting on every event, so it's more
// accurate than our cached value.
if ('webkitDirectionInvertedFromDevice' in event) {
return event.webkitDirectionInvertedFromDevice ? -1 : 1
}
return cachedNaturalScrollDirection ? -1 : 1
}
onMouseWheel = (event: WheelEvent) => { onMouseWheel = (event: WheelEvent) => {
const interaction = this.getInteractionType(event) const interaction = this.getInteractionType(event)
if (interaction === 'none') return if (interaction === 'none') return
@ -459,12 +543,15 @@ export class CameraControls {
if (this.syncDirection === 'engineToClient') { if (this.syncDirection === 'engineToClient') {
if (interaction === 'zoom') { if (interaction === 'zoom') {
this.zoomDataFromLastFrame = event.deltaY const zoomDir = this.zoomDirection(event)
this.zoomDataFromLastFrame = event.deltaY * zoomDir
} else { } else {
// This case will get handled when we add pan and rotate using Apple trackpad. this.moveDataFromLastFrame = [
console.error( 'wheel',
`Unexpected interaction type for engineToClient wheel event: ${interaction}` event.deltaX,
) event.deltaY,
interaction,
]
} }
return return
} }
@ -478,12 +565,20 @@ export class CameraControls {
this.handleStart() this.handleStart()
if (interaction === 'zoom') { if (interaction === 'zoom') {
this.pendingZoom = 1 + (event.deltaY / window.devicePixelRatio) * 0.001 const zoomDir = this.zoomDirection(event)
this.pendingZoom =
1 + (event.deltaY / window.devicePixelRatio) * 0.001 * zoomDir
} else { } else {
// This case will get handled when we add pan and rotate using Apple trackpad. this.isDragging = true
console.error( this.mouseDownPosition.set(event.clientX, event.clientY)
`Unexpected interaction type for wheel event: ${interaction}`
this.moveCamera(interaction, new Vector2(-event.deltaX, -event.deltaY))
this.mouseDownPosition.set(
event.clientX + event.deltaX,
event.clientY + event.deltaY
) )
this.isDragging = false
} }
this.handleEnd() this.handleEnd()
} }
@ -1266,8 +1361,17 @@ function _getInteractionType(
enableZoom: boolean enableZoom: boolean
): interactionType | 'none' { ): interactionType | 'none' {
if (event instanceof WheelEvent) { if (event instanceof WheelEvent) {
if (enableZoom && interactionGuards.zoom.scrollCallback(event)) // If the control scheme accepts pinch-to-zoom, and the event is
return 'zoom' // pinch-to-zoom, never consider other interaction types.
if (interactionGuards.zoom.pinchToZoom && isPinchToZoom(event)) {
if (enableZoom) return 'zoom'
} else {
if (enablePan && interactionGuards.pan.scrollCallback(event)) return 'pan'
if (enableRotate && interactionGuards.rotate.scrollCallback(event))
return 'rotate'
if (enableZoom && interactionGuards.zoom.scrollCallback(event))
return 'zoom'
}
} else { } else {
if (enablePan && interactionGuards.pan.callback(event)) return 'pan' if (enablePan && interactionGuards.pan.callback(event)) return 'pan'
if (enableRotate && interactionGuards.rotate.callback(event)) if (enableRotate && interactionGuards.rotate.callback(event))
@ -1277,6 +1381,18 @@ function _getInteractionType(
return 'none' return 'none'
} }
function isPinchToZoom(event: WheelEvent): boolean {
// Browsers do this hack. A couple issues:
//
// - According to MDN, it doesn't work on iOS.
// - It doesn't differentiate with a user actually holding Control and
// scrolling normally. It's possible to detect this by using onKeyDown and
// onKeyUp to track the state of the Control key. But we currently don't
// care about this since only the Apple Trackpad scheme looks for
// pinch-to-zoom events using interactionGuards.zoom.pinchToZoom.
return event.ctrlKey
}
/** /**
* Tells the engine to fire it's animation waits for it to finish and then requests camera settings * Tells the engine to fire it's animation waits for it to finish and then requests camera settings
* to ensure the client-side camera is synchronized with the engine's camera state. * to ensure the client-side camera is synchronized with the engine's camera state.

View File

@ -13,6 +13,7 @@ export type CameraSystem =
| 'KittyCAD' | 'KittyCAD'
| 'OnShape' | 'OnShape'
| 'Trackpad Friendly' | 'Trackpad Friendly'
| 'Apple Trackpad'
| 'Solidworks' | 'Solidworks'
| 'NX' | 'NX'
| 'Creo' | 'Creo'
@ -22,6 +23,7 @@ export const cameraSystems: CameraSystem[] = [
'KittyCAD', 'KittyCAD',
'OnShape', 'OnShape',
'Trackpad Friendly', 'Trackpad Friendly',
'Apple Trackpad',
'Solidworks', 'Solidworks',
'NX', 'NX',
'Creo', 'Creo',
@ -38,6 +40,8 @@ export function mouseControlsToCameraSystem(
return 'OnShape' return 'OnShape'
case 'trackpad_friendly': case 'trackpad_friendly':
return 'Trackpad Friendly' return 'Trackpad Friendly'
case 'apple_trackpad':
return 'Apple Trackpad'
case 'solidworks': case 'solidworks':
return 'Solidworks' return 'Solidworks'
case 'nx': case 'nx':
@ -54,6 +58,7 @@ export function mouseControlsToCameraSystem(
interface MouseGuardHandler { interface MouseGuardHandler {
description: string description: string
callback: (e: MouseEvent) => boolean callback: (e: MouseEvent) => boolean
scrollCallback: (e: WheelEvent) => boolean
lenientDragStartButton?: number lenientDragStartButton?: number
} }
@ -61,6 +66,8 @@ interface MouseGuardZoomHandler {
description: string description: string
dragCallback: (e: MouseEvent) => boolean dragCallback: (e: MouseEvent) => boolean
scrollCallback: (e: WheelEvent) => boolean scrollCallback: (e: WheelEvent) => boolean
scrollAllowInvertY?: boolean
pinchToZoom?: boolean
lenientDragStartButton?: number lenientDragStartButton?: number
} }
@ -83,6 +90,7 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
callback: (e) => callback: (e) =>
(btnName(e).middle && noModifiersPressed(e)) || (btnName(e).middle && noModifiersPressed(e)) ||
(btnName(e).right && e.shiftKey), (btnName(e).right && e.shiftKey),
scrollCallback: () => false,
}, },
zoom: { zoom: {
description: 'Scroll or Ctrl + Right click drag', description: 'Scroll or Ctrl + Right click drag',
@ -92,6 +100,7 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
rotate: { rotate: {
description: 'Right click drag', description: 'Right click drag',
callback: (e) => btnName(e).right && noModifiersPressed(e), callback: (e) => btnName(e).right && noModifiersPressed(e),
scrollCallback: () => false,
}, },
}, },
OnShape: { OnShape: {
@ -100,6 +109,7 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
callback: (e) => callback: (e) =>
(btnName(e).right && e.ctrlKey) || (btnName(e).right && e.ctrlKey) ||
(btnName(e).middle && noModifiersPressed(e)), (btnName(e).middle && noModifiersPressed(e)),
scrollCallback: () => false,
}, },
zoom: { zoom: {
description: 'Scroll', description: 'Scroll',
@ -109,6 +119,7 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
rotate: { rotate: {
description: 'Right click drag', description: 'Right click drag',
callback: (e) => btnName(e).right && noModifiersPressed(e), callback: (e) => btnName(e).right && noModifiersPressed(e),
scrollCallback: () => false,
}, },
}, },
'Trackpad Friendly': { 'Trackpad Friendly': {
@ -117,6 +128,7 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
callback: (e) => callback: (e) =>
(btnName(e).left && e.altKey && e.shiftKey && !e.metaKey) || (btnName(e).left && e.altKey && e.shiftKey && !e.metaKey) ||
(btnName(e).middle && noModifiersPressed(e)), (btnName(e).middle && noModifiersPressed(e)),
scrollCallback: () => false,
}, },
zoom: { zoom: {
description: `Scroll or ${ALT} + ${META} + Left click drag`, description: `Scroll or ${ALT} + ${META} + Left click drag`,
@ -126,13 +138,45 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
rotate: { rotate: {
description: `${ALT} + Left click drag`, description: `${ALT} + Left click drag`,
callback: (e) => btnName(e).left && e.altKey && !e.shiftKey && !e.metaKey, callback: (e) => btnName(e).left && e.altKey && !e.shiftKey && !e.metaKey,
scrollCallback: () => false,
lenientDragStartButton: 0, lenientDragStartButton: 0,
}, },
}, },
'Apple Trackpad': {
pan: {
description: `Scroll or one finger drag`,
callback: (e) => btnName(e).left && noModifiersPressed(e),
scrollCallback: (e) => e.deltaMode === 0 && noModifiersPressed(e),
lenientDragStartButton: 0,
},
zoom: {
description: `Shift + Scroll`,
dragCallback: (e) => false,
scrollCallback: (e) =>
e.deltaMode === 0 &&
e.shiftKey &&
!e.ctrlKey &&
!e.altKey &&
!e.metaKey,
scrollAllowInvertY: true,
pinchToZoom: true,
},
rotate: {
description: `${ALT} + Scroll`,
callback: (e) => false,
scrollCallback: (e) =>
e.deltaMode === 0 &&
e.altKey &&
!e.ctrlKey &&
!e.shiftKey &&
!e.metaKey,
},
},
Solidworks: { Solidworks: {
pan: { pan: {
description: 'Ctrl + Right click drag', description: 'Ctrl + Right click drag',
callback: (e) => btnName(e).right && e.ctrlKey, callback: (e) => btnName(e).right && e.ctrlKey,
scrollCallback: () => false,
lenientDragStartButton: 2, lenientDragStartButton: 2,
}, },
zoom: { zoom: {
@ -143,12 +187,14 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
rotate: { rotate: {
description: 'Middle click drag', description: 'Middle click drag',
callback: (e) => btnName(e).middle && noModifiersPressed(e), callback: (e) => btnName(e).middle && noModifiersPressed(e),
scrollCallback: () => false,
}, },
}, },
NX: { NX: {
pan: { pan: {
description: 'Shift + Middle click drag', description: 'Shift + Middle click drag',
callback: (e) => btnName(e).middle && e.shiftKey, callback: (e) => btnName(e).middle && e.shiftKey,
scrollCallback: () => false,
}, },
zoom: { zoom: {
description: 'Scroll or Ctrl + Middle click drag', description: 'Scroll or Ctrl + Middle click drag',
@ -158,12 +204,14 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
rotate: { rotate: {
description: 'Middle click drag', description: 'Middle click drag',
callback: (e) => btnName(e).middle && noModifiersPressed(e), callback: (e) => btnName(e).middle && noModifiersPressed(e),
scrollCallback: () => false,
}, },
}, },
Creo: { Creo: {
pan: { pan: {
description: 'Ctrl + Left click drag', description: 'Ctrl + Left click drag',
callback: (e) => btnName(e).left && !btnName(e).right && e.ctrlKey, callback: (e) => btnName(e).left && !btnName(e).right && e.ctrlKey,
scrollCallback: () => false,
}, },
zoom: { zoom: {
description: 'Scroll or Ctrl + Right click drag', description: 'Scroll or Ctrl + Right click drag',
@ -176,12 +224,14 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
const b = btnName(e) const b = btnName(e)
return (b.middle || (b.left && b.right)) && e.ctrlKey return (b.middle || (b.left && b.right)) && e.ctrlKey
}, },
scrollCallback: () => false,
}, },
}, },
AutoCAD: { AutoCAD: {
pan: { pan: {
description: 'Middle click drag', description: 'Middle click drag',
callback: (e) => btnName(e).middle && noModifiersPressed(e), callback: (e) => btnName(e).middle && noModifiersPressed(e),
scrollCallback: () => false,
}, },
zoom: { zoom: {
description: 'Scroll', description: 'Scroll',
@ -191,6 +241,7 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
rotate: { rotate: {
description: 'Shift + Middle click drag', description: 'Shift + Middle click drag',
callback: (e) => btnName(e).middle && e.shiftKey, callback: (e) => btnName(e).middle && e.shiftKey,
scrollCallback: () => false,
}, },
}, },
} }

View File

@ -565,3 +565,7 @@ export const getUser = async (
} }
return Promise.reject(new Error('unreachable')) return Promise.reject(new Error('unreachable'))
} }
export async function readNaturalScrollDirection() {
return window.electron.readNaturalScrollDirection()
}

View File

@ -4,6 +4,7 @@ import { v4 } from 'uuid'
import { isDesktop } from './isDesktop' import { isDesktop } from './isDesktop'
import { AnyMachineSnapshot } from 'xstate' import { AnyMachineSnapshot } from 'xstate'
import { AsyncFn } from './types' import { AsyncFn } from './types'
import { readNaturalScrollDirection } from './desktop'
export const uuidv4 = v4 export const uuidv4 = v4
@ -262,6 +263,19 @@ export function isReducedMotion(): boolean {
) )
} }
/**
* True if Apple Trackpad scroll should move the content. I.e. if this is true,
* and the user scrolls down, the viewport moves up relative to the content.
*/
export let cachedNaturalScrollDirection = platform() === 'macos'
export async function refreshNaturalScrollDirection() {
if (!isDesktop()) return cachedNaturalScrollDirection
const isNatural = await readNaturalScrollDirection()
cachedNaturalScrollDirection = isNatural
return isNatural
}
export function XOR(bool1: boolean, bool2: boolean): boolean { export function XOR(bool1: boolean, bool2: boolean): boolean {
return (bool1 || bool2) && !(bool1 && bool2) return (bool1 || bool2) && !(bool1 && bool2)
} }

View File

@ -5,6 +5,7 @@ import os from 'node:os'
import fsSync from 'node:fs' import fsSync from 'node:fs'
import packageJson from '../package.json' import packageJson from '../package.json'
import { MachinesListing } from 'lib/machineManager' import { MachinesListing } from 'lib/machineManager'
import { exec } from 'child_process'
import chokidar from 'chokidar' import chokidar from 'chokidar'
const open = (args: any) => ipcRenderer.invoke('dialog.showOpenDialog', args) const open = (args: any) => ipcRenderer.invoke('dialog.showOpenDialog', args)
@ -81,6 +82,25 @@ const listMachines = async (): Promise<MachinesListing> => {
const getMachineApiIp = async (): Promise<String | null> => const getMachineApiIp = async (): Promise<String | null> =>
ipcRenderer.invoke('find_machine_api') ipcRenderer.invoke('find_machine_api')
async function readNaturalScrollDirection(): Promise<boolean> {
if (os.platform() !== 'darwin') {
// TODO: Detect this on other OS's.
return false
}
return new Promise((resolve, reject) => {
exec(
'defaults read -globalDomain com.apple.swipescrolldirection',
(err, stdout) => {
if (err) {
reject(err)
} else {
resolve(stdout.trim() === '1')
}
}
)
})
}
contextBridge.exposeInMainWorld('electron', { contextBridge.exposeInMainWorld('electron', {
startDeviceFlow, startDeviceFlow,
loginWithDeviceFlow, loginWithDeviceFlow,
@ -144,6 +164,7 @@ contextBridge.exposeInMainWorld('electron', {
kittycad, kittycad,
listMachines, listMachines,
getMachineApiIp, getMachineApiIp,
readNaturalScrollDirection,
onUpdateDownloaded, onUpdateDownloaded,
appRestart, appRestart,
}) })

View File

@ -37,6 +37,8 @@ import {
} from 'lib/desktop' } from 'lib/desktop'
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar' import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
import { Project } from 'lib/project' import { Project } from 'lib/project'
import { refreshNaturalScrollDirection } from 'lib/utils'
import { reportRejection } from 'lib/trap'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { useProjectsLoader } from 'hooks/useProjectsLoader' import { useProjectsLoader } from 'hooks/useProjectsLoader'
@ -61,6 +63,11 @@ const Home = () => {
kclManager.cancelAllExecutions() kclManager.cancelAllExecutions()
}, []) }, [])
useEffect(() => {
// Load OS setting.
refreshNaturalScrollDirection().catch(reportRejection)
}, [])
useHotkeys('backspace', (e) => { useHotkeys('backspace', (e) => {
e.preventDefault() e.preventDefault()
}) })

View File

@ -389,6 +389,8 @@ pub enum MouseControlType {
OnShape, OnShape,
#[serde(alias = "Trackpad Friendly")] #[serde(alias = "Trackpad Friendly")]
TrackpadFriendly, TrackpadFriendly,
#[serde(alias = "Apple Trackpad")]
AppleTrackpad,
#[serde(alias = "Solidworks")] #[serde(alias = "Solidworks")]
Solidworks, Solidworks,
#[serde(alias = "NX")] #[serde(alias = "NX")]