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 - [x] Add "Select All" (Ctrl+A) shortcut
### Selection Visualization ### Selection Visualization
- [ ] Add selection outline effect (OutlinePass or custom) - [x] Add selection outline effect (OutlinePass or custom)
- [ ] Add hover highlight on mouseover - [x] Add hover highlight on mouseover
- [ ] Distinguish single vs multi-selection visually - [x] Distinguish single vs multi-selection visually
### Context Menu ### Context Menu
- [ ] Create right-click context menu component - [ ] Create right-click context menu component

View file

@ -2,6 +2,10 @@
import { ref, watch, onMounted, onBeforeUnmount } from 'vue' import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
import * as THREE from 'three' import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js' 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 { import {
HOME_POSITION, HOME_POSITION,
HOME_TARGET, HOME_TARGET,
@ -43,6 +47,10 @@ let axisHelper: THREE.AxesHelper
let controls: OrbitControls let controls: OrbitControls
let raycaster: THREE.Raycaster let raycaster: THREE.Raycaster
const mouse = new THREE.Vector2() const mouse = new THREE.Vector2()
let composer: EffectComposer
let selectionOutlinePass: OutlinePass
let hoverOutlinePass: OutlinePass
let hoveredObject: SceneObject | null = null
function initScene() { function initScene() {
if (!containerRef.value) return if (!containerRef.value) return
@ -91,16 +99,58 @@ function initScene() {
addLighting() addLighting()
addGridHelper() addGridHelper()
addAxisHelper() addAxisHelper()
addTestCube()
raycaster = new THREE.Raycaster() raycaster = new THREE.Raycaster()
sceneStore.setScene(scene) sceneStore.setScene(scene)
addTestCube()
setupPostProcessing()
setupMouseHandlers() setupMouseHandlers()
setupResizeObserver() setupResizeObserver()
animate() 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() { function addGridHelper() {
gridHelper = new THREE.GridHelper(10, 10, 0x444444, 0x333333) gridHelper = new THREE.GridHelper(10, 10, 0x444444, 0x333333)
gridHelper.visible = props.showGrid gridHelper.visible = props.showGrid
@ -147,19 +197,21 @@ function addTestCube() {
const geometry = new THREE.BoxGeometry(1, 1, 1) const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial({ color: 0x4a90d9 }) const material = new THREE.MeshStandardMaterial({ color: 0x4a90d9 })
const cube = new THREE.Mesh(geometry, material) const cube = new THREE.Mesh(geometry, material)
scene.add(cube) sceneStore.addObject(cube, 'Cube')
} }
function setupMouseHandlers() { function setupMouseHandlers() {
if (!renderer) return if (!renderer) return
renderer.domElement.addEventListener('click', onCanvasClick) renderer.domElement.addEventListener('click', onCanvasClick)
renderer.domElement.addEventListener('mousemove', onCanvasMouseMove)
} }
function cleanupMouseHandlers() { function cleanupMouseHandlers() {
if (!renderer) return if (!renderer) return
renderer.domElement.removeEventListener('click', onCanvasClick) renderer.domElement.removeEventListener('click', onCanvasClick)
renderer.domElement.removeEventListener('mousemove', onCanvasMouseMove)
} }
function updateMouseCoordinates(event: MouseEvent) { function updateMouseCoordinates(event: MouseEvent) {
@ -198,6 +250,14 @@ function onCanvasClick(event: MouseEvent) {
emit('pick', pickedObject, event) emit('pick', pickedObject, event)
} }
function onCanvasMouseMove(event: MouseEvent) {
const newHovered = pickObject(event)
if (newHovered !== hoveredObject) {
hoveredObject = newHovered
updateHoverOutline()
}
}
function setupResizeObserver() { function setupResizeObserver() {
if (!containerRef.value) return if (!containerRef.value) return
@ -218,6 +278,11 @@ function setupResizeObserver() {
orthographicCamera.updateProjectionMatrix() orthographicCamera.updateProjectionMatrix()
renderer.setSize(width, height) 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() { function animate() {
animationFrameId = requestAnimationFrame(animate) animationFrameId = requestAnimationFrame(animate)
controls.update() 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[]) { function fitToSelection(objects: THREE.Object3D[]) {
if (objects.length === 0) return if (objects.length === 0) return
@ -281,6 +371,10 @@ function setOrthographic(ortho: boolean) {
// Update controls to use new camera // Update controls to use new camera
controls.object = activeCamera controls.object = activeCamera
controls.update() controls.update()
// Update outline passes camera
if (selectionOutlinePass) selectionOutlinePass.renderCamera = activeCamera
if (hoverOutlinePass) hoverOutlinePass.renderCamera = activeCamera
} }
function toggleProjection() { function toggleProjection() {