Files
fabrikanabytok/apps/fabrikanabytok/lib/three/advanced-selection.ts
2025-11-28 20:48:15 +01:00

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
}
}