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
|
- [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
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue