camera controls enhancement

This commit is contained in:
wass08
2026-02-17 08:52:58 +09:00
parent 06e6ccb88f
commit 144a2bd18f
6 changed files with 171 additions and 22 deletions
@@ -3,7 +3,7 @@
import { sceneRegistry, useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { CameraControls, CameraControlsImpl } from '@react-three/drei'
import { useEffect, useMemo, useRef } from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { Box3, Vector3 } from 'three'
const tempBox = new Box3()
@@ -28,7 +28,7 @@ export const ViewerCameraControls = () => {
: CameraControlsImpl.ACTION.DOLLY
return {
left: CameraControlsImpl.ACTION.NONE,
left: CameraControlsImpl.ACTION.SCREEN_PAN,
middle: CameraControlsImpl.ACTION.SCREEN_PAN,
right: CameraControlsImpl.ACTION.ROTATE,
wheel: wheelAction,
@@ -59,7 +59,7 @@ export const ViewerCameraControls = () => {
target[0],
target[1],
target[2],
true
true,
)
return
}
@@ -81,7 +81,7 @@ export const ViewerCameraControls = () => {
const cameraPos = new Vector3(
tempCenter.x + distance * 0.7,
tempCenter.y + distance * 0.5,
tempCenter.z + distance * 0.7
tempCenter.z + distance * 0.7,
)
controls.current.setLookAt(
@@ -91,10 +91,18 @@ export const ViewerCameraControls = () => {
tempCenter.x,
tempCenter.y,
tempCenter.z,
true
true,
)
}, [targetNodeId, nodes])
const onTransitionStart = useCallback(() => {
useViewer.getState().setCameraDragging(true)
}, [])
const onRest = useCallback(() => {
useViewer.getState().setCameraDragging(false)
}, [])
return (
<CameraControls
ref={controls}
@@ -103,6 +111,10 @@ export const ViewerCameraControls = () => {
maxPolarAngle={Math.PI / 2 - 0.1}
minPolarAngle={0}
mouseButtons={mouseButtons}
onTransitionStart={onTransitionStart}
onRest={onRest}
restThreshold={0.01}
/>
)
}
@@ -3,7 +3,7 @@
import { type CameraControlEvent, emitter, sceneRegistry, useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { CameraControls, CameraControlsImpl } from '@react-three/drei'
import { useEffect, useMemo, useRef } from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { Vector3 } from 'three'
const currentTarget = new Vector3()
@@ -51,6 +51,86 @@ export const CustomCameraControls = () => {
}
}, [cameraMode])
useEffect(() => {
console.log('ohla')
const keyState = {
shiftRight: false,
shiftLeft: false,
controlRight: false,
controlLeft: false,
space: false,
}
const updateConfig = () => {
if (!controls.current) return
const shift = keyState.shiftRight || keyState.shiftLeft
const control = keyState.controlRight || keyState.controlLeft
const space = keyState.space
const wheelAction =
cameraMode === 'orthographic'
? CameraControlsImpl.ACTION.ZOOM
: CameraControlsImpl.ACTION.DOLLY
controls.current.mouseButtons.wheel = wheelAction
controls.current.mouseButtons.left = CameraControlsImpl.ACTION.NONE
controls.current.mouseButtons.middle = CameraControlsImpl.ACTION.SCREEN_PAN
controls.current.mouseButtons.right = CameraControlsImpl.ACTION.ROTATE
if (space) {
controls.current.mouseButtons.left = CameraControlsImpl.ACTION.SCREEN_PAN
}
}
const onKeyDown = (event: KeyboardEvent) => {
if (event.code === 'Space') {
keyState.space = true
document.body.style.cursor = 'grab'
}
if (event.code === 'ShiftRight') {
keyState.shiftRight = true
}
if (event.code === 'ShiftLeft') {
keyState.shiftLeft = true
}
if (event.code === 'ControlRight') {
keyState.controlRight = true
}
if (event.code === 'ControlLeft') {
keyState.controlLeft = true
}
updateConfig()
}
const onKeyUp = (event: KeyboardEvent) => {
if (event.code === 'Space') {
keyState.space = false
document.body.style.cursor = ''
}
if (event.code === 'ShiftRight') {
keyState.shiftRight = false
}
if (event.code === 'ShiftLeft') {
keyState.shiftLeft = false
}
if (event.code === 'ControlRight') {
keyState.controlRight = false
}
if (event.code === 'ControlLeft') {
keyState.controlLeft = false
}
updateConfig()
}
document.addEventListener('keydown', onKeyDown)
document.addEventListener('keyup', onKeyUp)
updateConfig()
return () => {
document.removeEventListener('keydown', onKeyDown)
document.removeEventListener('keyup', onKeyUp)
}
}, [cameraMode])
useEffect(() => {
const handleNodeCapture = ({ nodeId }: CameraControlEvent) => {
if (!controls.current) return
@@ -139,6 +219,14 @@ export const CustomCameraControls = () => {
}
}, [])
const onTransitionStart = useCallback(() => {
useViewer.getState().setCameraDragging(true)
}, [])
const onRest = useCallback(() => {
useViewer.getState().setCameraDragging(false)
}, [])
return (
<CameraControls
makeDefault
@@ -148,6 +236,9 @@ export const CustomCameraControls = () => {
minPolarAngle={0}
ref={controls}
mouseButtons={mouseButtons}
onTransitionStart={onTransitionStart}
onRest={onRest}
restThreshold={0.01}
/>
)
}
+8 -1
View File
@@ -1,4 +1,5 @@
import { type EventSuffix, emitter, type GridEvent } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { useThree } from '@react-three/fiber'
import { useEffect, useRef } from 'react'
import { Plane, Raycaster, Vector2, Vector3 } from 'three'
@@ -53,29 +54,35 @@ export function useGridEvents(gridY: number) {
}
const handlePointerDown = (e: PointerEvent) => {
if (useViewer.getState().cameraDragging) return
if (e.button !== 0) return
emit('pointerdown', e)
}
const handlePointerUp = (e: PointerEvent) => {
if (useViewer.getState().cameraDragging) return
if (e.button !== 0) return
emit('pointerup', e)
}
const handleClick = (e: PointerEvent) => {
if (useViewer.getState().cameraDragging) return
if (e.button !== 0) return
emit('click', e)
}
const handlePointerMove = (e: PointerEvent) => {
if (useViewer.getState().cameraDragging) return
emit('move', e)
}
const handleDoubleClick = (e: MouseEvent) => {
if (useViewer.getState().cameraDragging) return
emit('double-click', e)
}
const handleContextMenu = (e: MouseEvent) => {
if (useViewer.getState().cameraDragging) return
emit('context-menu', e)
}
@@ -95,5 +102,5 @@ export function useGridEvents(gridY: number) {
canvas.removeEventListener('dblclick', handleDoubleClick)
canvas.removeEventListener('contextmenu', handleContextMenu)
}
}, [camera, gl, gridY])
}, [camera, gl])
}
@@ -3,15 +3,15 @@
import {
type AnyNode,
type BuildingNode,
emitter,
type ItemNode,
type LevelNode,
type NodeEvent,
type WallNode,
type ZoneNode,
emitter,
pointInPolygon,
sceneRegistry,
useScene,
type WallNode,
type ZoneNode,
} from '@pascal-app/core'
import { useThree } from '@react-three/fiber'
import { useEffect, useRef } from 'react'
@@ -23,14 +23,23 @@ const tempWorldPos = new Vector3()
// Tolerance for edge detection (in meters)
const EDGE_TOLERANCE = 0.5
type SelectableNodeType = 'building' | 'level' | 'zone' | 'wall' | 'item' | 'slab' | 'ceiling' | 'roof'
type SelectableNodeType =
| 'building'
| 'level'
| 'zone'
| 'wall'
| 'item'
| 'slab'
| 'ceiling'
| 'roof'
// Expand polygon outward by a small amount to include items on edges
const expandPolygon = (polygon: [number, number][], tolerance: number): [number, number][] => {
if (polygon.length < 3) return polygon
// Calculate centroid
let cx = 0, cz = 0
let cx = 0,
cz = 0
for (const [x, z] of polygon) {
cx += x
cz += z
@@ -50,7 +59,11 @@ const expandPolygon = (polygon: [number, number][], tolerance: number): [number,
}
// Check if point is in polygon with tolerance for edges
const pointInPolygonWithTolerance = (x: number, z: number, polygon: [number, number][]): boolean => {
const pointInPolygonWithTolerance = (
x: number,
z: number,
polygon: [number, number][],
): boolean => {
// First try exact check
if (pointInPolygon(x, z, polygon)) return true
// Then try with expanded polygon for edge tolerance
@@ -186,7 +199,9 @@ const getStrategy = (): SelectionStrategy | null => {
const { selectedIds } = useViewer.getState().selection
// Toggle selection - if already selected, deselect; otherwise select
if (selectedIds.includes(node.id)) {
useViewer.getState().setSelection({ selectedIds: selectedIds.filter((id) => id !== node.id) })
useViewer
.getState()
.setSelection({ selectedIds: selectedIds.filter((id) => id !== node.id) })
} else {
useViewer.getState().setSelection({ selectedIds: [node.id] })
}
@@ -244,7 +259,16 @@ export const SelectionManager = () => {
}
// Subscribe to all node types
const allTypes: SelectableNodeType[] = ['building', 'level', 'zone', 'wall', 'item', 'slab', 'ceiling', 'roof']
const allTypes: SelectableNodeType[] = [
'building',
'level',
'zone',
'wall',
'item',
'slab',
'ceiling',
'roof',
]
for (const type of allTypes) {
emitter.on(`${type}:enter`, onEnter)
emitter.on(`${type}:leave`, onLeave)
@@ -258,7 +282,7 @@ export const SelectionManager = () => {
emitter.off(`${type}:click`, onClick)
}
}
}, [selection])
}, [])
return (
<>
@@ -268,12 +292,17 @@ export const SelectionManager = () => {
)
}
const PointerMissedHandler = ({ clickHandledRef }: { clickHandledRef: React.MutableRefObject<boolean> }) => {
const PointerMissedHandler = ({
clickHandledRef,
}: {
clickHandledRef: React.MutableRefObject<boolean>
}) => {
const gl = useThree((s) => s.gl)
useEffect(() => {
const handleClick = (event: MouseEvent) => {
// Only handle left clicks
if (useViewer.getState().cameraDragging) return
if (event.button !== 0) return
// Use requestAnimationFrame to check after R3F event handlers
+9 -5
View File
@@ -21,6 +21,7 @@ import {
type ZoneNode,
} from '@pascal-app/core'
import type { ThreeEvent } from '@react-three/fiber'
import useViewer from '../store/use-viewer';
type NodeConfig = {
site: { node: SiteNode; event: SiteEvent }
@@ -54,21 +55,24 @@ export function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], t
return {
onPointerDown: (e: ThreeEvent<PointerEvent>) => {
if (useViewer.getState().cameraDragging) return
if (e.button !== 0) return
emit('pointerdown', e)
},
onPointerUp: (e: ThreeEvent<PointerEvent>) => {
if (useViewer.getState().cameraDragging) return
if (e.button !== 0) return
emit('pointerup', e)
},
onClick: (e: ThreeEvent<PointerEvent>) => {
if (useViewer.getState().cameraDragging) return
if (e.button !== 0) return
emit('click', e)
},
onPointerEnter: (e: ThreeEvent<PointerEvent>) => emit('enter', e),
onPointerLeave: (e: ThreeEvent<PointerEvent>) => emit('leave', e),
onPointerMove: (e: ThreeEvent<PointerEvent>) => emit('move', e),
onDoubleClick: (e: ThreeEvent<PointerEvent>) => emit('double-click', e),
onContextMenu: (e: ThreeEvent<PointerEvent>) => emit('context-menu', e),
onPointerEnter: (e: ThreeEvent<PointerEvent>) => {if (useViewer.getState().cameraDragging) return; emit('enter', e)},
onPointerLeave: (e: ThreeEvent<PointerEvent>) => {if (useViewer.getState().cameraDragging) return; emit('leave', e)},
onPointerMove: (e: ThreeEvent<PointerEvent>) => {if (useViewer.getState().cameraDragging) return; emit('move', e)},
onDoubleClick: (e: ThreeEvent<PointerEvent>) => {if (useViewer.getState().cameraDragging) return; emit('double-click', e)},
onContextMenu: (e: ThreeEvent<PointerEvent>) => {if (useViewer.getState().cameraDragging) return; emit('context-menu', e)},
}
}
+6
View File
@@ -52,6 +52,9 @@ type ViewerState = {
// Export functionality
exportScene: (() => Promise<void>) | null
setExportScene: (fn: (() => Promise<void>) | null) => void
cameraDragging: boolean
setCameraDragging: (dragging: boolean) => void
}
const useViewer = create<ViewerState>()((set, get) => ({
@@ -107,6 +110,9 @@ const useViewer = create<ViewerState>()((set, get) => ({
exportScene: null,
setExportScene: (fn) => set({ exportScene: fn }),
cameraDragging: false,
setCameraDragging: (dragging) => set({ cameraDragging: dragging }),
}));
export default useViewer;