Add Raycaster for mouse picking
This commit is contained in:
parent
a6a7f5ba17
commit
08cdf0d41a
5 changed files with 85 additions and 11 deletions
2
TODO.md
2
TODO.md
|
|
@ -86,7 +86,7 @@ A step-by-step checklist for porting MatterControl's design features to a Vue +
|
|||
## Phase 4: Selection System
|
||||
|
||||
### Object Picking
|
||||
- [ ] Add Raycaster for mouse picking
|
||||
- [x] Add Raycaster for mouse picking
|
||||
- [ ] Implement click-to-select single object
|
||||
- [ ] Implement click-on-empty to deselect
|
||||
- [ ] Add `selectedObjects` array to Pinia store
|
||||
|
|
|
|||
|
|
@ -2,12 +2,20 @@ import js from '@eslint/js'
|
|||
import tseslint from 'typescript-eslint'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import eslintConfigPrettier from 'eslint-config-prettier'
|
||||
import globals from 'globals'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist', 'node_modules'] },
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...pluginVue.configs['flat/recommended'],
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.vue'],
|
||||
languageOptions: {
|
||||
|
|
@ -15,6 +23,9 @@ export default tseslint.config(
|
|||
parser: tseslint.parser,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
},
|
||||
},
|
||||
eslintConfigPrettier
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-vue": "^10.7.0",
|
||||
"globals": "^17.2.0",
|
||||
"happy-dom": "^20.4.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
|
|
|
|||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
|
@ -45,6 +45,9 @@ importers:
|
|||
eslint-plugin-vue:
|
||||
specifier: ^10.7.0
|
||||
version: 10.7.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(vue-eslint-parser@10.2.0(eslint@9.39.2))
|
||||
globals:
|
||||
specifier: ^17.2.0
|
||||
version: 17.2.0
|
||||
happy-dom:
|
||||
specifier: ^20.4.0
|
||||
version: 20.4.0
|
||||
|
|
@ -917,6 +920,10 @@ packages:
|
|||
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
globals@17.2.0:
|
||||
resolution: {integrity: sha512-tovnCz/fEq+Ripoq+p/gN1u7l6A7wwkoBT9pRCzTHzsD/LvADIzXZdjmRymh5Ztf0DYC3Rwg5cZRYjxzBmzbWg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
happy-dom@20.4.0:
|
||||
resolution: {integrity: sha512-RDeQm3dT9n0A5f/TszjUmNCLEuPnMGv3Tv4BmNINebz/h17PA6LMBcxJ5FrcqltNBMh9jA/8ufgDdBYUdBt+eg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
|
@ -2214,6 +2221,8 @@ snapshots:
|
|||
|
||||
globals@14.0.0: {}
|
||||
|
||||
globals@17.2.0: {}
|
||||
|
||||
happy-dom@20.4.0:
|
||||
dependencies:
|
||||
'@types/node': 24.10.9
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
getViewUpVector,
|
||||
type ViewPreset,
|
||||
} from '../utils/camera'
|
||||
import { useSceneStore, type SceneObject } from '../stores/scene'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
|
@ -22,6 +23,11 @@ const props = withDefaults(
|
|||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
pick: [object: SceneObject | null, event: MouseEvent]
|
||||
}>()
|
||||
|
||||
const sceneStore = useSceneStore()
|
||||
const containerRef = ref<HTMLDivElement | null>(null)
|
||||
|
||||
let scene: THREE.Scene
|
||||
|
|
@ -35,6 +41,8 @@ let resizeObserver: ResizeObserver
|
|||
let gridHelper: THREE.GridHelper
|
||||
let axisHelper: THREE.AxesHelper
|
||||
let controls: OrbitControls
|
||||
let raycaster: THREE.Raycaster
|
||||
const mouse = new THREE.Vector2()
|
||||
|
||||
function initScene() {
|
||||
if (!containerRef.value) return
|
||||
|
|
@ -80,22 +88,16 @@ function initScene() {
|
|||
controls.enableDamping = true
|
||||
controls.dampingFactor = 0.05
|
||||
|
||||
// Add lighting
|
||||
addLighting()
|
||||
|
||||
// Add grid helper
|
||||
addGridHelper()
|
||||
|
||||
// Add axis helper
|
||||
addAxisHelper()
|
||||
|
||||
// Add a test cube to verify rendering
|
||||
addTestCube()
|
||||
|
||||
// Setup resize observer
|
||||
setupResizeObserver()
|
||||
raycaster = new THREE.Raycaster()
|
||||
sceneStore.setScene(scene)
|
||||
|
||||
// Start render loop
|
||||
setupMouseHandlers()
|
||||
setupResizeObserver()
|
||||
animate()
|
||||
}
|
||||
|
||||
|
|
@ -148,6 +150,54 @@ function addTestCube() {
|
|||
scene.add(cube)
|
||||
}
|
||||
|
||||
function setupMouseHandlers() {
|
||||
if (!renderer) return
|
||||
|
||||
renderer.domElement.addEventListener('click', onCanvasClick)
|
||||
}
|
||||
|
||||
function cleanupMouseHandlers() {
|
||||
if (!renderer) return
|
||||
|
||||
renderer.domElement.removeEventListener('click', onCanvasClick)
|
||||
}
|
||||
|
||||
function updateMouseCoordinates(event: MouseEvent) {
|
||||
if (!containerRef.value) return
|
||||
|
||||
const rect = renderer.domElement.getBoundingClientRect()
|
||||
// Convert to normalized device coordinates (-1 to +1)
|
||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
|
||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
|
||||
}
|
||||
|
||||
function pickObject(event: MouseEvent): SceneObject | null {
|
||||
updateMouseCoordinates(event)
|
||||
|
||||
raycaster.setFromCamera(mouse, activeCamera)
|
||||
|
||||
// Get all meshes from scene objects in the store
|
||||
const meshes = sceneStore.objects.map((obj) => obj.mesh)
|
||||
|
||||
if (meshes.length === 0) return null
|
||||
|
||||
const intersects = raycaster.intersectObjects(meshes, false)
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const hitMesh = intersects[0]!.object as THREE.Mesh
|
||||
// Find the SceneObject that owns this mesh
|
||||
const sceneObject = sceneStore.objects.find((obj) => obj.mesh === hitMesh)
|
||||
return sceneObject ?? null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function onCanvasClick(event: MouseEvent) {
|
||||
const pickedObject = pickObject(event)
|
||||
emit('pick', pickedObject, event)
|
||||
}
|
||||
|
||||
function setupResizeObserver() {
|
||||
if (!containerRef.value) return
|
||||
|
||||
|
|
@ -246,6 +296,8 @@ function cleanup() {
|
|||
resizeObserver.disconnect()
|
||||
}
|
||||
|
||||
cleanupMouseHandlers()
|
||||
|
||||
if (controls) {
|
||||
controls.dispose()
|
||||
}
|
||||
|
|
@ -270,6 +322,7 @@ defineExpose({
|
|||
setViewPreset,
|
||||
setOrthographic,
|
||||
toggleProjection,
|
||||
pickObject,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue