Disable view control menu items and gizmo clicks conditionally (#5537)
* Disable view control menu items and gizmo clicks conditionally * Actually just turn off all mouse events, moves too * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * fix gizmo flickering * fmt * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * Add test step for disabling gizmo in sketch mode * Use grayscale to indicate disabled state? @max-mrgrsk feel free to discard if you prefer the opacity, I don't have a strong opinion at the moment. * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: max-mrgrsk <156543465+max-mrgrsk@users.noreply.github.com>
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
@ -243,7 +243,7 @@ test.describe('Testing Gizmo', { tag: ['@skipWin'] }, () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test.describe(`Testing gizmo, fixture-based`, () => {
|
test.describe(`Testing gizmo, fixture-based`, () => {
|
||||||
test('Center on selection from menu', async ({
|
test('Center on selection from menu, disable interaction in sketch mode', async ({
|
||||||
context,
|
context,
|
||||||
page,
|
page,
|
||||||
homePage,
|
homePage,
|
||||||
@ -318,5 +318,26 @@ test.describe(`Testing gizmo, fixture-based`, () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await test.step(`Gizmo should be disabled when in sketch mode`, async () => {
|
||||||
|
const sketchModeButton = page.getByRole('button', {
|
||||||
|
name: 'Edit sketch',
|
||||||
|
})
|
||||||
|
const exitSketchButton = page.getByRole('button', {
|
||||||
|
name: 'Exit sketch',
|
||||||
|
})
|
||||||
|
|
||||||
|
await sketchModeButton.click()
|
||||||
|
await expect(exitSketchButton).toBeVisible()
|
||||||
|
const gizmoPopoverButton = page.getByRole('button', {
|
||||||
|
name: 'view settings',
|
||||||
|
})
|
||||||
|
await gizmoPopoverButton.click()
|
||||||
|
const buttonToTest = page.getByRole('button', {
|
||||||
|
name: 'right view',
|
||||||
|
})
|
||||||
|
await expect(buttonToTest).toBeVisible()
|
||||||
|
await expect(buttonToTest).toBeDisabled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -27,6 +27,8 @@ import {
|
|||||||
ViewControlContextMenu,
|
ViewControlContextMenu,
|
||||||
} from './ViewControlMenu'
|
} from './ViewControlMenu'
|
||||||
import { AxisNames } from 'lib/constants'
|
import { AxisNames } from 'lib/constants'
|
||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
const CANVAS_SIZE = 80
|
const CANVAS_SIZE = 80
|
||||||
const FRUSTUM_SIZE = 0.5
|
const FRUSTUM_SIZE = 0.5
|
||||||
@ -40,12 +42,33 @@ enum AxisColors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Gizmo() {
|
export default function Gizmo() {
|
||||||
|
const { state: modelingState } = useModelingContext()
|
||||||
|
const settings = useSettings()
|
||||||
const menuItems = useViewControlMenuItems()
|
const menuItems = useViewControlMenuItems()
|
||||||
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||||
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
||||||
const cameraPassiveUpdateTimer = useRef(0)
|
const cameraPassiveUpdateTimer = useRef(0)
|
||||||
const raycasterPassiveUpdateTimer = useRef(0)
|
const raycasterPassiveUpdateTimer = useRef(0)
|
||||||
|
const disableOrbitRef = useRef(false)
|
||||||
|
|
||||||
|
// Temporary fix for #4040:
|
||||||
|
// Disable gizmo orbiting in sketch mode
|
||||||
|
// This effect updates disableOrbitRef whenever the user
|
||||||
|
// toggles between Sketch mode and 3D mode
|
||||||
|
useEffect(() => {
|
||||||
|
disableOrbitRef.current =
|
||||||
|
modelingState.matches('Sketch') &&
|
||||||
|
!settings.app.allowOrbitInSketchMode.current
|
||||||
|
if (wrapperRef.current) {
|
||||||
|
wrapperRef.current.style.filter = disableOrbitRef.current
|
||||||
|
? 'grayscale(100%)'
|
||||||
|
: 'none'
|
||||||
|
wrapperRef.current.style.cursor = disableOrbitRef.current
|
||||||
|
? 'not-allowed'
|
||||||
|
: 'auto'
|
||||||
|
}
|
||||||
|
}, [modelingState, settings.app.allowOrbitInSketchMode.current])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canvasRef.current) return
|
if (!canvasRef.current) return
|
||||||
@ -64,7 +87,8 @@ export default function Gizmo() {
|
|||||||
const { mouse, disposeMouseEvents } = initializeMouseEvents(
|
const { mouse, disposeMouseEvents } = initializeMouseEvents(
|
||||||
canvas,
|
canvas,
|
||||||
raycasterIntersect,
|
raycasterIntersect,
|
||||||
sceneInfra
|
sceneInfra,
|
||||||
|
disableOrbitRef
|
||||||
)
|
)
|
||||||
const raycasterObjects = [...gizmoAxisHeads]
|
const raycasterObjects = [...gizmoAxisHeads]
|
||||||
|
|
||||||
@ -81,6 +105,8 @@ export default function Gizmo() {
|
|||||||
delta,
|
delta,
|
||||||
cameraPassiveUpdateTimer
|
cameraPassiveUpdateTimer
|
||||||
)
|
)
|
||||||
|
// If orbits are disabled, skip click logic
|
||||||
|
if (!disableOrbitRef.current) {
|
||||||
updateRayCaster(
|
updateRayCaster(
|
||||||
raycasterObjects,
|
raycasterObjects,
|
||||||
raycaster,
|
raycaster,
|
||||||
@ -90,6 +116,10 @@ export default function Gizmo() {
|
|||||||
delta,
|
delta,
|
||||||
raycasterPassiveUpdateTimer
|
raycasterPassiveUpdateTimer
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
raycasterObjects.forEach((object) => object.scale.set(1, 1, 1)) // Reset scales
|
||||||
|
raycasterIntersect.current = null // Clear intersection
|
||||||
|
}
|
||||||
renderer.render(scene, camera)
|
renderer.render(scene, camera)
|
||||||
requestAnimationFrame(animate)
|
requestAnimationFrame(animate)
|
||||||
}
|
}
|
||||||
@ -246,7 +276,8 @@ const quaternionsEqual = (
|
|||||||
const initializeMouseEvents = (
|
const initializeMouseEvents = (
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
raycasterIntersect: MutableRefObject<Intersection<Object3D> | null>,
|
raycasterIntersect: MutableRefObject<Intersection<Object3D> | null>,
|
||||||
sceneInfra: SceneInfra
|
sceneInfra: SceneInfra,
|
||||||
|
disableOrbitRef: MutableRefObject<boolean>
|
||||||
): { mouse: Vector2; disposeMouseEvents: () => void } => {
|
): { mouse: Vector2; disposeMouseEvents: () => void } => {
|
||||||
const mouse = new Vector2()
|
const mouse = new Vector2()
|
||||||
mouse.x = 1 // fix initial mouse position issue
|
mouse.x = 1 // fix initial mouse position issue
|
||||||
@ -258,11 +289,11 @@ const initializeMouseEvents = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (raycasterIntersect.current) {
|
// If orbits are disabled, skip click logic
|
||||||
|
if (disableOrbitRef.current || !raycasterIntersect.current) return
|
||||||
const axisName = raycasterIntersect.current.object.name as AxisNames
|
const axisName = raycasterIntersect.current.object.name as AxisNames
|
||||||
sceneInfra.camControls.updateCameraToAxis(axisName).catch(reportRejection)
|
sceneInfra.camControls.updateCameraToAxis(axisName).catch(reportRejection)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('mousemove', handleMouseMove)
|
window.addEventListener('mousemove', handleMouseMove)
|
||||||
window.addEventListener('click', handleClick)
|
window.addEventListener('click', handleClick)
|
||||||
|
@ -10,9 +10,14 @@ import { AxisNames, VIEW_NAMES_SEMANTIC } from 'lib/constants'
|
|||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { sceneInfra } from 'lib/singletons'
|
import { sceneInfra } from 'lib/singletons'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
export function useViewControlMenuItems() {
|
export function useViewControlMenuItems() {
|
||||||
const { send: modelingSend } = useModelingContext()
|
const { state: modelingState, send: modelingSend } = useModelingContext()
|
||||||
|
const settings = useSettings()
|
||||||
|
const shouldLockView =
|
||||||
|
modelingState.matches('Sketch') &&
|
||||||
|
!settings.app.allowOrbitInSketchMode.current
|
||||||
const menuItems = useMemo(
|
const menuItems = useMemo(
|
||||||
() => [
|
() => [
|
||||||
...Object.entries(VIEW_NAMES_SEMANTIC).map(([axisName, axisSemantic]) => (
|
...Object.entries(VIEW_NAMES_SEMANTIC).map(([axisName, axisSemantic]) => (
|
||||||
@ -23,6 +28,7 @@ export function useViewControlMenuItems() {
|
|||||||
.updateCameraToAxis(axisName as AxisNames)
|
.updateCameraToAxis(axisName as AxisNames)
|
||||||
.catch(reportRejection)
|
.catch(reportRejection)
|
||||||
}}
|
}}
|
||||||
|
disabled={shouldLockView}
|
||||||
>
|
>
|
||||||
{axisSemantic} view
|
{axisSemantic} view
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
@ -32,6 +38,7 @@ export function useViewControlMenuItems() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||||
}}
|
}}
|
||||||
|
disabled={shouldLockView}
|
||||||
>
|
>
|
||||||
Reset view
|
Reset view
|
||||||
</ContextMenuItem>,
|
</ContextMenuItem>,
|
||||||
@ -39,13 +46,14 @@ export function useViewControlMenuItems() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
modelingSend({ type: 'Center camera on selection' })
|
modelingSend({ type: 'Center camera on selection' })
|
||||||
}}
|
}}
|
||||||
|
disabled={shouldLockView}
|
||||||
>
|
>
|
||||||
Center view on selection
|
Center view on selection
|
||||||
</ContextMenuItem>,
|
</ContextMenuItem>,
|
||||||
<ContextMenuDivider />,
|
<ContextMenuDivider />,
|
||||||
<ContextMenuItemRefresh />,
|
<ContextMenuItemRefresh />,
|
||||||
],
|
],
|
||||||
[VIEW_NAMES_SEMANTIC]
|
[VIEW_NAMES_SEMANTIC, shouldLockView]
|
||||||
)
|
)
|
||||||
return menuItems
|
return menuItems
|
||||||
}
|
}
|
||||||
|