Add Raycaster for mouse picking

This commit is contained in:
Nettika 2026-01-29 01:23:34 -08:00
parent a6a7f5ba17
commit 08cdf0d41a
No known key found for this signature in database
5 changed files with 85 additions and 11 deletions

View file

@ -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

View file

@ -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
)

View file

@ -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
View file

@ -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

View file

@ -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>