Compare commits
	
		
			5 Commits
		
	
	
		
			v1.0.0
			...
			jtran/appl
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 01cbd9533b | |||
| a8d12a35cd | |||
| 24c2fe996f | |||
| 4da6298e2a | |||
| 7021be8360 | 
							
								
								
									
										1
									
								
								interface.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								interface.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -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 | ||||||
|  | |||||||
| @ -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. | ||||||
|  | |||||||
| @ -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, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| } | } | ||||||
|  | |||||||
| @ -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() | ||||||
|  | } | ||||||
|  | |||||||
| @ -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) | ||||||
| } | } | ||||||
|  | |||||||
| @ -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, | ||||||
| }) | }) | ||||||
|  | |||||||
| @ -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() | ||||||
|   }) |   }) | ||||||
|  | |||||||
| @ -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")] | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	