| export {DeviceOrientationControlsPlugin} from './interaction/DeviceOrientationControlsPlugin' | export {DeviceOrientationControlsPlugin} from './interaction/DeviceOrientationControlsPlugin' | ||||
| export {PointerLockControlsPlugin} from './interaction/PointerLockControlsPlugin' | export {PointerLockControlsPlugin} from './interaction/PointerLockControlsPlugin' | ||||
| export {ThreeFirstPersonControlsPlugin} from './interaction/ThreeFirstPersonControlsPlugin' | export {ThreeFirstPersonControlsPlugin} from './interaction/ThreeFirstPersonControlsPlugin' | ||||
| export {UndoManagerPlugin} from './interaction/UndoManagerPlugin' | |||||
| // import | // import | ||||
| export {Rhino3dmLoadPlugin} from './import/Rhino3dmLoadPlugin' | export {Rhino3dmLoadPlugin} from './import/Rhino3dmLoadPlugin' |
| import {IObject3D, IObject3DEvent, ISceneEvent} from '../../core' | import {IObject3D, IObject3DEvent, ISceneEvent} from '../../core' | ||||
| import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | ||||
| import {FrameFadePlugin} from '../pipeline/FrameFadePlugin' | import {FrameFadePlugin} from '../pipeline/FrameFadePlugin' | ||||
| import {type UndoManagerPlugin} from './UndoManagerPlugin' | |||||
| export class PickingPlugin extends AViewerPluginSync<'selectedObjectChanged'|'hoverObjectChanged'|'hitObject'> { | export class PickingPlugin extends AViewerPluginSync<'selectedObjectChanged'|'hoverObjectChanged'|'hitObject'> { | ||||
| @serialize() | @serialize() | ||||
| viewer.scene.addEventListener('sceneUpdate', this._onSceneUpdate) | viewer.scene.addEventListener('sceneUpdate', this._onSceneUpdate) | ||||
| viewer.scene.addEventListener('mainCameraChange', this._mainCameraChange) | viewer.scene.addEventListener('mainCameraChange', this._mainCameraChange) | ||||
| viewer.forPlugin<UndoManagerPlugin>('UndoManagerPlugin', (um)=>{ | |||||
| if (!this._picker) return | |||||
| this._picker.undoManager = um.undoManager | |||||
| }, ()=>{ | |||||
| if (!this._picker) return | |||||
| this._picker.undoManager = undefined | |||||
| }) | |||||
| } | } | ||||
| onRemove(viewer: ThreeViewer) { | onRemove(viewer: ThreeViewer) { | ||||
| this._picker.removeEventListener('hoverObjectChanged', this._hoverObjectChanged) | this._picker.removeEventListener('hoverObjectChanged', this._hoverObjectChanged) | ||||
| this._picker.removeEventListener('hitObject', this._onObjectHit) | this._picker.removeEventListener('hitObject', this._onObjectHit) | ||||
| this._picker.dispose() | this._picker.dispose() | ||||
| this._picker.undoManager = undefined // because setting above | |||||
| this._picker = undefined | this._picker = undefined | ||||
| } | } | ||||
| super.onRemove(viewer) | super.onRemove(viewer) |
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | import {AViewerPluginSync, ThreeViewer} from '../../viewer' | ||||
| import {OrbitControls3, TransformControls2} from '../../three' | import {OrbitControls3, TransformControls2} from '../../three' | ||||
| import {PickingPlugin} from './PickingPlugin' | import {PickingPlugin} from './PickingPlugin' | ||||
| import {onChange} from 'ts-browser-helpers' | |||||
| import {JSUndoManager, onChange} from 'ts-browser-helpers' | |||||
| import {TransformControls} from '../../three/controls/TransformControls' | import {TransformControls} from '../../three/controls/TransformControls' | ||||
| import {UnlitLineMaterial, UnlitMaterial} from '../../core' | import {UnlitLineMaterial, UnlitMaterial} from '../../core' | ||||
| import {Euler, Object3D, Vector3} from 'three' | |||||
| import type {UndoManagerPlugin} from './UndoManagerPlugin' | |||||
| @uiPanelContainer('Transform Controls') | @uiPanelContainer('Transform Controls') | ||||
| export class TransformControlsPlugin extends AViewerPluginSync<''> { | export class TransformControlsPlugin extends AViewerPluginSync<''> { | ||||
| }, | }, | ||||
| } | } | ||||
| private _transformState = { | |||||
| obj: null as Object3D|null, | |||||
| position: new Vector3(), | |||||
| rotation: new Euler(), | |||||
| scale: new Vector3(), | |||||
| } | |||||
| undoManager?: JSUndoManager | |||||
| onAdded(viewer: ThreeViewer) { | onAdded(viewer: ThreeViewer) { | ||||
| super.onAdded(viewer) | super.onAdded(viewer) | ||||
| this.setDirty() | this.setDirty() | ||||
| event.object ? this.transformControls.attach(event.object) : this.transformControls.detach() | event.object ? this.transformControls.attach(event.object) : this.transformControls.detach() | ||||
| }) | }) | ||||
| viewer.forPlugin<UndoManagerPlugin>('UndoManagerPlugin', (um)=> { | |||||
| this.undoManager = um.undoManager | |||||
| }, ()=> this.undoManager = undefined) | |||||
| // same logic for undo as three.js editor. todo It can be made better by syncing with the UI so it supports the hotkeys and other properties inside TransformControls2 | |||||
| this.transformControls.addEventListener('mouseDown', ()=> { | |||||
| if (!this.transformControls) return | |||||
| const object = this.transformControls.object | |||||
| if (!object) return | |||||
| this._transformState.obj = object | |||||
| this._transformState.position = object.position.clone() | |||||
| this._transformState.rotation = object.rotation.clone() | |||||
| this._transformState.scale = object.scale.clone() | |||||
| }) | |||||
| this.transformControls.addEventListener('mouseUp', ()=> { | |||||
| if (!this.transformControls) return | |||||
| const object = this.transformControls.object | |||||
| if (!object) return | |||||
| if (this._transformState.obj !== object || !this.undoManager) return | |||||
| const key = ({ | |||||
| 'translate': 'position', | |||||
| 'rotate': 'rotation', | |||||
| 'scale': 'scale', | |||||
| } as const)[this.transformControls.getMode()] | |||||
| if (!key) return | |||||
| if (this._transformState[key].equals(object[key] as any)) return | |||||
| const command = { | |||||
| last: this._transformState[key].clone(), current: object[key].clone(), | |||||
| set: (value: any) => { | |||||
| object[key].copy(value) | |||||
| object.updateMatrixWorld(true) | |||||
| this.transformControls?.dispatchEvent({type: 'change'} as any) | |||||
| this.transformControls?.dispatchEvent({type: 'objectChange'} as any) | |||||
| }, | |||||
| undo: () => command.set(command.last), | |||||
| redo: () => command.set(command.current), | |||||
| } | |||||
| this.undoManager.record(command) | |||||
| }) | |||||
| } | } | ||||
| onRemove(viewer: ThreeViewer) { | onRemove(viewer: ThreeViewer) { |
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||||
| import {getUrlQueryParam, JSUndoManager, onChange} from 'ts-browser-helpers' | |||||
| // @uiPanelContainer('Undo Manager') | |||||
| export class UndoManagerPlugin extends AViewerPluginSync<''> { | |||||
| public static readonly PluginType = 'UndoManagerPlugin' | |||||
| // @uiToggle() | |||||
| @onChange(UndoManagerPlugin.prototype._refresh) | |||||
| enabled = true | |||||
| undoManager?: JSUndoManager | |||||
| @onChange(UndoManagerPlugin.prototype._refresh) | |||||
| limit = 1000 | |||||
| constructor(enabled = true, limit = 1000) { | |||||
| super() | |||||
| this.enabled = enabled | |||||
| this.limit = limit | |||||
| } | |||||
| protected _refresh() { | |||||
| if (!this.undoManager) return | |||||
| this.undoManager.enabled = this.enabled | |||||
| this.undoManager.limit = this.limit | |||||
| this.undoManager.options.debug = this._viewer?.debug || this.undoManager.options.debug | |||||
| } | |||||
| toJSON: any = undefined | |||||
| onAdded(viewer: ThreeViewer) { | |||||
| super.onAdded(viewer) | |||||
| this.undoManager = new JSUndoManager({bindHotKeys: true, limit: this.limit, debug: viewer.debug || getUrlQueryParam('debugUndo') !== null, hotKeyRoot: document as any}) | |||||
| } | |||||
| onRemove(viewer: ThreeViewer) { | |||||
| this.undoManager?.dispose() | |||||
| this.undoManager = undefined | |||||
| super.onRemove(viewer) | |||||
| } | |||||
| } |
| private _keyDownListener(event: KeyboardEvent) { | private _keyDownListener(event: KeyboardEvent) { | ||||
| if (!this.enabled) return | if (!this.enabled) return | ||||
| if (!this.object) return | if (!this.object) return | ||||
| if (event.metaKey || event.ctrlKey) return | |||||
| switch (event.code) { | switch (event.code) { | ||||
| import {Event, EventDispatcher, Intersection, Raycaster, Vector2} from 'three' | import {Event, EventDispatcher, Intersection, Raycaster, Vector2} from 'three' | ||||
| import {now} from 'ts-browser-helpers' | |||||
| import {JSUndoManager, now} from 'ts-browser-helpers' | |||||
| import {ICamera, IObject3D} from '../../core' | import {ICamera, IObject3D} from '../../core' | ||||
| export class ObjectPicker extends EventDispatcher<Event, 'hoverObjectChanged'|'selectedObjectChanged'|'hitObject'> { | export class ObjectPicker extends EventDispatcher<Event, 'hoverObjectChanged'|'selectedObjectChanged'|'hitObject'> { | ||||
| */ | */ | ||||
| static PointerClickMaxDistance = 0.1 // 1/20 of the canvas | static PointerClickMaxDistance = 0.1 // 1/20 of the canvas | ||||
| undoManager?: JSUndoManager | |||||
| private _root: IObject3D | private _root: IObject3D | ||||
| private _camera: ICamera | private _camera: ICamera | ||||
| private _mouseDownTime: number | private _mouseDownTime: number | ||||
| } | } | ||||
| set selectedObject(object) { | set selectedObject(object) { | ||||
| this._setSelected(object) | |||||
| } | |||||
| private _setSelected(object: IObject3D|null, record = true) { | |||||
| if (!this._selected.length && !object || this._selected.length === 1 && this._selected[0] === object) return | if (!this._selected.length && !object || this._selected.length === 1 && this._selected[0] === object) return | ||||
| const current = [...this._selected] | |||||
| this._selected = object ? Array.isArray(object) ? [...object] : [object] : [] | this._selected = object ? Array.isArray(object) ? [...object] : [object] : [] | ||||
| this.dispatchEvent({type: 'selectedObjectChanged', object: this.selectedObject}) | this.dispatchEvent({type: 'selectedObjectChanged', object: this.selectedObject}) | ||||
| record && this.undoManager?.record({ | |||||
| undo: () => this._setSelected(current.length ? current[0] : null, false), | |||||
| redo: () => this._setSelected(object, false), | |||||
| }) | |||||
| } | } | ||||
| get hoverObject(): IObject3D | null { | get hoverObject(): IObject3D | null { |
| export function makeSetterFor<V>(target: any, key: string, setDirty?: ()=>void) { | export function makeSetterFor<V>(target: any, key: string, setDirty?: ()=>void) { | ||||
| const v = target[key] as any | const v = target[key] as any | ||||
| const dirty = ()=>{ | const dirty = ()=>{ | ||||
| if (typeof target?.setDirty === 'function') target.setDirty() | |||||
| // if (typeof target?.setDirty === 'function') target.setDirty() | |||||
| setDirty?.() | setDirty?.() | ||||
| } | } | ||||
| const isBool = typeof v === 'boolean' | const isBool = typeof v === 'boolean' |