Implement selection visual effects

This commit is contained in:
Nettika 2026-01-29 01:34:49 -08:00
parent 3b42ce9f88
commit aafcade099
No known key found for this signature in database
2 changed files with 100 additions and 6 deletions

View file

@ -95,9 +95,9 @@ A step-by-step checklist for porting MatterControl's design features to a Vue +
- [x] Add "Select All" (Ctrl+A) shortcut
### Selection Visualization
- [ ] Add selection outline effect (OutlinePass or custom)
- [ ] Add hover highlight on mouseover
- [ ] Distinguish single vs multi-selection visually
- [x] Add selection outline effect (OutlinePass or custom)
- [x] Add hover highlight on mouseover
- [x] Distinguish single vs multi-selection visually
### Context Menu
- [ ] Create right-click context menu component

View file

@ -2,6 +2,10 @@
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'
import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js'
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'
import {
HOME_POSITION,
HOME_TARGET,
@ -43,6 +47,10 @@ let axisHelper: THREE.AxesHelper
let controls: OrbitControls
let raycaster: THREE.Raycaster
const mouse = new THREE.Vector2()
let composer: EffectComposer
let selectionOutlinePass: OutlinePass
let hoverOutlinePass: OutlinePass
let hoveredObject: SceneObject | null = null
function initScene() {
if (!containerRef.value) return
@ -91,16 +99,58 @@ function initScene() {
addLighting()
addGridHelper()
addAxisHelper()
addTestCube()
raycaster = new THREE.Raycaster()
sceneStore.setScene(scene)
addTestCube()
setupPostProcessing()
setupMouseHandlers()
setupResizeObserver()
animate()
}
function setupPostProcessing() {
if (!containerRef.value) return
const { clientWidth: width, clientHeight: height } = containerRef.value
composer = new EffectComposer(renderer)
const renderPass = new RenderPass(scene, activeCamera)
composer.addPass(renderPass)
// Selection outline - cyan color
selectionOutlinePass = new OutlinePass(
new THREE.Vector2(width, height),
scene,
activeCamera
)
selectionOutlinePass.visibleEdgeColor.set(0x00ffff)
selectionOutlinePass.hiddenEdgeColor.set(0x004444)
selectionOutlinePass.edgeStrength = 3
selectionOutlinePass.edgeThickness = 1
selectionOutlinePass.edgeGlow = 0.5
composer.addPass(selectionOutlinePass)
// Hover outline - yellow color, thinner
hoverOutlinePass = new OutlinePass(
new THREE.Vector2(width, height),
scene,
activeCamera
)
hoverOutlinePass.visibleEdgeColor.set(0xffff00)
hoverOutlinePass.hiddenEdgeColor.set(0x444400)
hoverOutlinePass.edgeStrength = 2
hoverOutlinePass.edgeThickness = 1
hoverOutlinePass.edgeGlow = 0
composer.addPass(hoverOutlinePass)
const outputPass = new OutputPass()
composer.addPass(outputPass)
}
function addGridHelper() {
gridHelper = new THREE.GridHelper(10, 10, 0x444444, 0x333333)
gridHelper.visible = props.showGrid
@ -147,19 +197,21 @@ function addTestCube() {
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial({ color: 0x4a90d9 })
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
sceneStore.addObject(cube, 'Cube')
}
function setupMouseHandlers() {
if (!renderer) return
renderer.domElement.addEventListener('click', onCanvasClick)
renderer.domElement.addEventListener('mousemove', onCanvasMouseMove)
}
function cleanupMouseHandlers() {
if (!renderer) return
renderer.domElement.removeEventListener('click', onCanvasClick)
renderer.domElement.removeEventListener('mousemove', onCanvasMouseMove)
}
function updateMouseCoordinates(event: MouseEvent) {
@ -198,6 +250,14 @@ function onCanvasClick(event: MouseEvent) {
emit('pick', pickedObject, event)
}
function onCanvasMouseMove(event: MouseEvent) {
const newHovered = pickObject(event)
if (newHovered !== hoveredObject) {
hoveredObject = newHovered
updateHoverOutline()
}
}
function setupResizeObserver() {
if (!containerRef.value) return
@ -218,6 +278,11 @@ function setupResizeObserver() {
orthographicCamera.updateProjectionMatrix()
renderer.setSize(width, height)
composer.setSize(width, height)
const resolution = new THREE.Vector2(width, height)
selectionOutlinePass.resolution = resolution
hoverOutlinePass.resolution = resolution
}
}
})
@ -228,9 +293,34 @@ function setupResizeObserver() {
function animate() {
animationFrameId = requestAnimationFrame(animate)
controls.update()
renderer.render(scene, activeCamera)
composer.render()
}
function updateSelectionOutline() {
if (!selectionOutlinePass) return
selectionOutlinePass.selectedObjects = sceneStore.selectedObjects.map(
(obj) => obj.mesh
)
}
function updateHoverOutline() {
if (!hoverOutlinePass) return
if (hoveredObject && !sceneStore.selectedIds.has(hoveredObject.id)) {
hoverOutlinePass.selectedObjects = [hoveredObject.mesh]
} else {
hoverOutlinePass.selectedObjects = []
}
}
watch(
() => sceneStore.selectedObjects,
() => {
updateSelectionOutline()
updateHoverOutline()
},
{ deep: true }
)
function fitToSelection(objects: THREE.Object3D[]) {
if (objects.length === 0) return
@ -281,6 +371,10 @@ function setOrthographic(ortho: boolean) {
// Update controls to use new camera
controls.object = activeCamera
controls.update()
// Update outline passes camera
if (selectionOutlinePass) selectionOutlinePass.renderCamera = activeCamera
if (hoverOutlinePass) hoverOutlinePass.renderCamera = activeCamera
}
function toggleProjection() {