Implement selection visual effects
This commit is contained in:
parent
3b42ce9f88
commit
aafcade099
2 changed files with 100 additions and 6 deletions
6
TODO.md
6
TODO.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue