UNPKG

threepipe

Version:

A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.

147 lines (136 loc) 5.9 kB
import {IObject3D} from '../../core' import {JSUndoManager} from 'ts-browser-helpers' import {Matrix4, Object3D, Quaternion, Vector3} from 'three' import type {ThreeViewer} from '../../viewer' /** * Shared helper for multi-object transform gizmo support. * Used by both TransformControlsPlugin and PivotControlsPlugin. */ export class MultiSelectHelper { multiObjects: IObject3D[] = [] private _dummy: Object3D | null = null private _dummyStart = new Matrix4() private _startStates: {position: Vector3, quaternion: Quaternion, scale: Vector3}[] = [] private _startWorldMatrices: Matrix4[] = [] get dummy(): Object3D | null { return this._dummy } get hasMultiSelect(): boolean { return this.multiObjects.length > 1 } get hasStartStates(): boolean { return this._startStates.length > 0 } setup(objects: IObject3D[], viewer: ThreeViewer): Object3D { this.multiObjects = objects if (!this._dummy) { this._dummy = new Object3D() this._dummy.userData.isMultiSelectDummy = true viewer.scene.addObject(this._dummy as any, {addToRoot: true}) } const median = new Vector3() for (const obj of objects) { obj.updateWorldMatrix(true, false) median.add(new Vector3().setFromMatrixPosition(obj.matrixWorld)) } median.divideScalar(objects.length) this._dummy.position.copy(median) this._dummy.quaternion.identity() this._dummy.scale.setScalar(1) this._dummy.updateMatrixWorld(true) return this._dummy } clear(viewer: ThreeViewer) { this.multiObjects = [] this._startStates = [] this._startWorldMatrices = [] if (this._dummy) { viewer.scene.remove(this._dummy) this._dummy = null } } captureStart() { if (!this._dummy || !this.multiObjects.length) return this._dummy.updateMatrixWorld(true) this._dummyStart.copy(this._dummy.matrixWorld) this._startStates = this.multiObjects.map(o => ({ position: o.position.clone(), quaternion: o.quaternion.clone(), scale: o.scale.clone(), })) this._startWorldMatrices = this.multiObjects.map(o => { o.updateWorldMatrix(true, false) return o.matrixWorld.clone() }) } applyDelta() { if (!this._dummy || !this.multiObjects.length || !this._startWorldMatrices.length) return this._dummy.updateMatrixWorld(true) const dummyStartInv = this._dummyStart.clone().invert() const deltaWorld = this._dummy.matrixWorld.clone().multiply(dummyStartInv) for (let i = 0; i < this.multiObjects.length; i++) { const obj = this.multiObjects[i] const newWorld = deltaWorld.clone().multiply(this._startWorldMatrices[i]) const parentInv = new Matrix4() if (obj.parent) parentInv.copy(obj.parent.matrixWorld).invert() const newLocal = parentInv.clone().multiply(newWorld) newLocal.decompose(obj.position, obj.quaternion, obj.scale) obj.updateMatrixWorld(true) ;(obj as IObject3D).setDirty?.({change: 'transform', frameFade: false}) } } /** Reposition the dummy to the median of all selected objects */ updateDummyPosition() { if (!this._dummy || !this.multiObjects.length) return const median = new Vector3() for (const obj of this.multiObjects) { obj.updateWorldMatrix(true, false) median.add(new Vector3().setFromMatrixPosition(obj.matrixWorld)) } median.divideScalar(this.multiObjects.length) this._dummy.position.copy(median) this._dummy.quaternion.identity() this._dummy.scale.setScalar(1) this._dummy.updateMatrixWorld(true) } recordUndo(undoManager: JSUndoManager) { if (!this.multiObjects.length || !this._startStates.length) return const objects = [...this.multiObjects] const startStates = this._startStates.map(s => ({ position: s.position.clone(), quaternion: s.quaternion.clone(), scale: s.scale.clone(), })) const endStates = objects.map(obj => ({ position: obj.position.clone(), quaternion: obj.quaternion.clone(), scale: obj.scale.clone(), })) let changed = false for (let i = 0; i < objects.length; i++) { if (!startStates[i].position.equals(endStates[i].position) || !startStates[i].quaternion.equals(endStates[i].quaternion) || !startStates[i].scale.equals(endStates[i].scale)) { changed = true break } } if (!changed) return undoManager.record({ undo: () => { for (let i = 0; i < objects.length; i++) { objects[i].position.copy(startStates[i].position) objects[i].quaternion.copy(startStates[i].quaternion) objects[i].scale.copy(startStates[i].scale) objects[i].updateMatrixWorld(true) ;(objects[i] as IObject3D).setDirty?.({change: 'transform'}) } this.updateDummyPosition() }, redo: () => { for (let i = 0; i < objects.length; i++) { objects[i].position.copy(endStates[i].position) objects[i].quaternion.copy(endStates[i].quaternion) objects[i].scale.copy(endStates[i].scale) objects[i].updateMatrixWorld(true) ;(objects[i] as IObject3D).setDirty?.({change: 'transform'}) } this.updateDummyPosition() }, }) } }