456 lines
10 KiB
TypeScript
456 lines
10 KiB
TypeScript
/**
|
|
* Advanced Selection Tools
|
|
* Lasso, magic wand, area select, and intelligent selection
|
|
*/
|
|
|
|
import * as THREE from 'three'
|
|
|
|
export interface SelectionBox {
|
|
min: THREE.Vector2
|
|
max: THREE.Vector2
|
|
}
|
|
|
|
export interface LassoPoint {
|
|
x: number
|
|
y: number
|
|
}
|
|
|
|
/**
|
|
* Lasso Selection Tool
|
|
*/
|
|
export class LassoSelection {
|
|
private points: LassoPoint[] = []
|
|
private closed: boolean = false
|
|
|
|
/**
|
|
* Add point to lasso
|
|
*/
|
|
addPoint(x: number, y: number): void {
|
|
this.points.push({ x, y })
|
|
}
|
|
|
|
/**
|
|
* Close lasso path
|
|
*/
|
|
close(): void {
|
|
this.closed = true
|
|
}
|
|
|
|
/**
|
|
* Check if point is inside lasso
|
|
*/
|
|
isPointInside(x: number, y: number): boolean {
|
|
if (!this.closed || this.points.length < 3) return false
|
|
|
|
let inside = false
|
|
const n = this.points.length
|
|
|
|
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
const xi = this.points[i].x
|
|
const yi = this.points[i].y
|
|
const xj = this.points[j].x
|
|
const yj = this.points[j].y
|
|
|
|
const intersect = ((yi > y) !== (yj > y)) &&
|
|
(x < (xj - xi) * (y - yi) / (yj - yi) + xi)
|
|
|
|
if (intersect) inside = !inside
|
|
}
|
|
|
|
return inside
|
|
}
|
|
|
|
/**
|
|
* Select objects within lasso
|
|
*/
|
|
selectObjects(
|
|
objects: THREE.Object3D[],
|
|
camera: THREE.Camera,
|
|
viewport: { width: number; height: number }
|
|
): THREE.Object3D[] {
|
|
const selected: THREE.Object3D[] = []
|
|
|
|
objects.forEach((obj) => {
|
|
// Project object position to screen space
|
|
const screenPos = obj.position.clone().project(camera)
|
|
|
|
const x = (screenPos.x + 1) * viewport.width / 2
|
|
const y = (-screenPos.y + 1) * viewport.height / 2
|
|
|
|
if (this.isPointInside(x, y)) {
|
|
selected.push(obj)
|
|
}
|
|
})
|
|
|
|
return selected
|
|
}
|
|
|
|
/**
|
|
* Get lasso path for rendering
|
|
*/
|
|
getPath(): LassoPoint[] {
|
|
return this.points
|
|
}
|
|
|
|
/**
|
|
* Clear lasso
|
|
*/
|
|
clear(): void {
|
|
this.points = []
|
|
this.closed = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Magic Wand Selection (Select similar)
|
|
*/
|
|
export class MagicWandSelection {
|
|
/**
|
|
* Select objects similar to target
|
|
*/
|
|
selectSimilar(
|
|
target: THREE.Object3D,
|
|
objects: THREE.Object3D[],
|
|
criteria: {
|
|
checkMaterial?: boolean
|
|
checkSize?: boolean
|
|
checkType?: boolean
|
|
tolerance?: number
|
|
} = {}
|
|
): THREE.Object3D[] {
|
|
const tolerance = criteria.tolerance ?? 0.1
|
|
const selected: THREE.Object3D[] = [target]
|
|
|
|
objects.forEach((obj) => {
|
|
if (obj === target) return
|
|
|
|
let similar = true
|
|
|
|
// Check material similarity
|
|
if (criteria.checkMaterial && obj instanceof THREE.Mesh && target instanceof THREE.Mesh) {
|
|
if (obj.material !== target.material) {
|
|
similar = false
|
|
}
|
|
}
|
|
|
|
// Check size similarity
|
|
if (criteria.checkSize) {
|
|
const targetBox = new THREE.Box3().setFromObject(target)
|
|
const objBox = new THREE.Box3().setFromObject(obj)
|
|
|
|
const targetSize = new THREE.Vector3()
|
|
const objSize = new THREE.Vector3()
|
|
targetBox.getSize(targetSize)
|
|
objBox.getSize(objSize)
|
|
|
|
const sizeDiff = targetSize.distanceTo(objSize) / targetSize.length()
|
|
if (sizeDiff > tolerance) {
|
|
similar = false
|
|
}
|
|
}
|
|
|
|
// Check type similarity
|
|
if (criteria.checkType) {
|
|
if (obj.type !== target.type) {
|
|
similar = false
|
|
}
|
|
}
|
|
|
|
if (similar) {
|
|
selected.push(obj)
|
|
}
|
|
})
|
|
|
|
return selected
|
|
}
|
|
|
|
/**
|
|
* Select by color
|
|
*/
|
|
selectByColor(
|
|
targetColor: THREE.Color,
|
|
objects: THREE.Mesh[],
|
|
tolerance: number = 0.1
|
|
): THREE.Mesh[] {
|
|
const selected: THREE.Mesh[] = []
|
|
|
|
objects.forEach((mesh) => {
|
|
if (!mesh.material) return
|
|
|
|
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
|
|
|
materials.forEach((mat) => {
|
|
if ('color' in mat && mat.color) {
|
|
const colorDist = mat.color.distanceTo(targetColor)
|
|
if (colorDist < tolerance) {
|
|
selected.push(mesh)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
return selected
|
|
}
|
|
|
|
/**
|
|
* Select by layer/tag
|
|
*/
|
|
selectByProperty(
|
|
objects: THREE.Object3D[],
|
|
property: string,
|
|
value: any
|
|
): THREE.Object3D[] {
|
|
return objects.filter((obj) => {
|
|
return obj.userData[property] === value
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Box Selection
|
|
*/
|
|
export class BoxSelection {
|
|
private startPoint: THREE.Vector2 | null = null
|
|
private endPoint: THREE.Vector2 | null = null
|
|
|
|
/**
|
|
* Start selection
|
|
*/
|
|
start(x: number, y: number): void {
|
|
this.startPoint = new THREE.Vector2(x, y)
|
|
this.endPoint = new THREE.Vector2(x, y)
|
|
}
|
|
|
|
/**
|
|
* Update selection
|
|
*/
|
|
update(x: number, y: number): void {
|
|
if (!this.startPoint) return
|
|
this.endPoint = new THREE.Vector2(x, y)
|
|
}
|
|
|
|
/**
|
|
* End selection
|
|
*/
|
|
end(): SelectionBox | null {
|
|
if (!this.startPoint || !this.endPoint) return null
|
|
|
|
const box: SelectionBox = {
|
|
min: new THREE.Vector2(
|
|
Math.min(this.startPoint.x, this.endPoint.x),
|
|
Math.min(this.startPoint.y, this.endPoint.y)
|
|
),
|
|
max: new THREE.Vector2(
|
|
Math.max(this.startPoint.x, this.endPoint.x),
|
|
Math.max(this.startPoint.y, this.endPoint.y)
|
|
)
|
|
}
|
|
|
|
this.clear()
|
|
return box
|
|
}
|
|
|
|
/**
|
|
* Get current box
|
|
*/
|
|
getCurrentBox(): SelectionBox | null {
|
|
if (!this.startPoint || !this.endPoint) return null
|
|
|
|
return {
|
|
min: new THREE.Vector2(
|
|
Math.min(this.startPoint.x, this.endPoint.x),
|
|
Math.min(this.startPoint.y, this.endPoint.y)
|
|
),
|
|
max: new THREE.Vector2(
|
|
Math.max(this.startPoint.x, this.endPoint.x),
|
|
Math.max(this.startPoint.y, this.endPoint.y)
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Select objects in box
|
|
*/
|
|
selectObjects(
|
|
objects: THREE.Object3D[],
|
|
camera: THREE.Camera,
|
|
viewport: { width: number; height: number }
|
|
): THREE.Object3D[] {
|
|
const box = this.getCurrentBox()
|
|
if (!box) return []
|
|
|
|
const selected: THREE.Object3D[] = []
|
|
|
|
objects.forEach((obj) => {
|
|
// Project to screen space
|
|
const screenPos = obj.position.clone().project(camera)
|
|
|
|
const x = (screenPos.x + 1) * viewport.width / 2
|
|
const y = (-screenPos.y + 1) * viewport.height / 2
|
|
|
|
if (x >= box.min.x && x <= box.max.x &&
|
|
y >= box.min.y && y <= box.max.y) {
|
|
selected.push(obj)
|
|
}
|
|
})
|
|
|
|
return selected
|
|
}
|
|
|
|
/**
|
|
* Clear selection
|
|
*/
|
|
clear(): void {
|
|
this.startPoint = null
|
|
this.endPoint = null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Advanced Selection Manager
|
|
* Combines all selection tools
|
|
*/
|
|
export class AdvancedSelectionManager {
|
|
private lassoSelection: LassoSelection = new LassoSelection()
|
|
private magicWandSelection: MagicWandSelection = new MagicWandSelection()
|
|
private boxSelection: BoxSelection = new BoxSelection()
|
|
private selectedObjects: Set<THREE.Object3D> = new Set()
|
|
private mode: 'normal' | 'add' | 'subtract' | 'intersect' = 'normal'
|
|
|
|
/**
|
|
* Start lasso selection
|
|
*/
|
|
startLasso(x: number, y: number): void {
|
|
this.lassoSelection.clear()
|
|
this.lassoSelection.addPoint(x, y)
|
|
}
|
|
|
|
/**
|
|
* Continue lasso
|
|
*/
|
|
continueLasso(x: number, y: number): void {
|
|
this.lassoSelection.addPoint(x, y)
|
|
}
|
|
|
|
/**
|
|
* End lasso
|
|
*/
|
|
endLasso(
|
|
objects: THREE.Object3D[],
|
|
camera: THREE.Camera,
|
|
viewport: { width: number; height: number }
|
|
): THREE.Object3D[] {
|
|
this.lassoSelection.close()
|
|
const selected = this.lassoSelection.selectObjects(objects, camera, viewport)
|
|
this.updateSelection(selected)
|
|
return selected
|
|
}
|
|
|
|
/**
|
|
* Box selection
|
|
*/
|
|
startBox(x: number, y: number): void {
|
|
this.boxSelection.start(x, y)
|
|
}
|
|
|
|
updateBox(x: number, y: number): void {
|
|
this.boxSelection.update(x, y)
|
|
}
|
|
|
|
endBox(
|
|
objects: THREE.Object3D[],
|
|
camera: THREE.Camera,
|
|
viewport: { width: number; height: number }
|
|
): THREE.Object3D[] {
|
|
const selected = this.boxSelection.selectObjects(objects, camera, viewport)
|
|
this.updateSelection(selected)
|
|
return selected
|
|
}
|
|
|
|
/**
|
|
* Magic wand selection
|
|
*/
|
|
selectSimilar(
|
|
target: THREE.Object3D,
|
|
objects: THREE.Object3D[],
|
|
criteria?: Parameters<typeof MagicWandSelection.prototype.selectSimilar>[2]
|
|
): THREE.Object3D[] {
|
|
const selected = this.magicWandSelection.selectSimilar(target, objects, criteria)
|
|
this.updateSelection(selected)
|
|
return selected
|
|
}
|
|
|
|
/**
|
|
* Update selection based on mode
|
|
*/
|
|
private updateSelection(newSelection: THREE.Object3D[]): void {
|
|
switch (this.mode) {
|
|
case 'normal':
|
|
this.selectedObjects.clear()
|
|
newSelection.forEach(obj => this.selectedObjects.add(obj))
|
|
break
|
|
|
|
case 'add':
|
|
newSelection.forEach(obj => this.selectedObjects.add(obj))
|
|
break
|
|
|
|
case 'subtract':
|
|
newSelection.forEach(obj => this.selectedObjects.delete(obj))
|
|
break
|
|
|
|
case 'intersect':
|
|
const intersection = new Set<THREE.Object3D>()
|
|
newSelection.forEach(obj => {
|
|
if (this.selectedObjects.has(obj)) {
|
|
intersection.add(obj)
|
|
}
|
|
})
|
|
this.selectedObjects = intersection
|
|
break
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set selection mode
|
|
*/
|
|
setMode(mode: 'normal' | 'add' | 'subtract' | 'intersect'): void {
|
|
this.mode = mode
|
|
}
|
|
|
|
/**
|
|
* Get selected objects
|
|
*/
|
|
getSelection(): THREE.Object3D[] {
|
|
return Array.from(this.selectedObjects)
|
|
}
|
|
|
|
/**
|
|
* Clear selection
|
|
*/
|
|
clearSelection(): void {
|
|
this.selectedObjects.clear()
|
|
}
|
|
|
|
/**
|
|
* Select all
|
|
*/
|
|
selectAll(objects: THREE.Object3D[]): void {
|
|
this.selectedObjects.clear()
|
|
objects.forEach(obj => this.selectedObjects.add(obj))
|
|
}
|
|
|
|
/**
|
|
* Invert selection
|
|
*/
|
|
invertSelection(objects: THREE.Object3D[]): void {
|
|
const newSelection = new Set<THREE.Object3D>()
|
|
objects.forEach(obj => {
|
|
if (!this.selectedObjects.has(obj)) {
|
|
newSelection.add(obj)
|
|
}
|
|
})
|
|
this.selectedObjects = newSelection
|
|
}
|
|
}
|
|
|