| @@ -35,6 +35,7 @@ export {EditorViewWidgetPlugin} from './interaction/EditorViewWidgetPlugin' | |||
| export {DeviceOrientationControlsPlugin} from './interaction/DeviceOrientationControlsPlugin' | |||
| export {PointerLockControlsPlugin} from './interaction/PointerLockControlsPlugin' | |||
| export {ThreeFirstPersonControlsPlugin} from './interaction/ThreeFirstPersonControlsPlugin' | |||
| export {UndoManagerPlugin} from './interaction/UndoManagerPlugin' | |||
| // import | |||
| export {Rhino3dmLoadPlugin} from './import/Rhino3dmLoadPlugin' | |||
| @@ -5,6 +5,7 @@ import {BoxSelectionWidget, ObjectPicker, SelectionWidget} from '../../three' | |||
| import {IObject3D, IObject3DEvent, ISceneEvent} from '../../core' | |||
| import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | |||
| import {FrameFadePlugin} from '../pipeline/FrameFadePlugin' | |||
| import {type UndoManagerPlugin} from './UndoManagerPlugin' | |||
| export class PickingPlugin extends AViewerPluginSync<'selectedObjectChanged'|'hoverObjectChanged'|'hitObject'> { | |||
| @serialize() | |||
| @@ -139,6 +140,14 @@ export class PickingPlugin extends AViewerPluginSync<'selectedObjectChanged'|'ho | |||
| viewer.scene.addEventListener('sceneUpdate', this._onSceneUpdate) | |||
| 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) { | |||
| @@ -154,6 +163,7 @@ export class PickingPlugin extends AViewerPluginSync<'selectedObjectChanged'|'ho | |||
| this._picker.removeEventListener('hoverObjectChanged', this._hoverObjectChanged) | |||
| this._picker.removeEventListener('hitObject', this._onObjectHit) | |||
| this._picker.dispose() | |||
| this._picker.undoManager = undefined // because setting above | |||
| this._picker = undefined | |||
| } | |||
| super.onRemove(viewer) | |||
| @@ -2,9 +2,11 @@ import {uiButton, uiConfig, uiPanelContainer, uiToggle} from 'uiconfig.js' | |||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||
| import {OrbitControls3, TransformControls2} from '../../three' | |||
| import {PickingPlugin} from './PickingPlugin' | |||
| import {onChange} from 'ts-browser-helpers' | |||
| import {JSUndoManager, onChange} from 'ts-browser-helpers' | |||
| import {TransformControls} from '../../three/controls/TransformControls' | |||
| import {UnlitLineMaterial, UnlitMaterial} from '../../core' | |||
| import {Euler, Object3D, Vector3} from 'three' | |||
| import type {UndoManagerPlugin} from './UndoManagerPlugin' | |||
| @uiPanelContainer('Transform Controls') | |||
| export class TransformControlsPlugin extends AViewerPluginSync<''> { | |||
| @@ -55,6 +57,15 @@ 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) { | |||
| super.onAdded(viewer) | |||
| this.setDirty() | |||
| @@ -86,6 +97,51 @@ export class TransformControlsPlugin extends AViewerPluginSync<''> { | |||
| 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) { | |||
| @@ -0,0 +1,43 @@ | |||
| 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) | |||
| } | |||
| } | |||
| @@ -15,6 +15,7 @@ export class TransformControls2 extends TransformControls implements IWidget, IO | |||
| private _keyDownListener(event: KeyboardEvent) { | |||
| if (!this.enabled) return | |||
| if (!this.object) return | |||
| if (event.metaKey || event.ctrlKey) return | |||
| switch (event.code) { | |||
| @@ -1,5 +1,5 @@ | |||
| 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' | |||
| export class ObjectPicker extends EventDispatcher<Event, 'hoverObjectChanged'|'selectedObjectChanged'|'hitObject'> { | |||
| @@ -15,6 +15,7 @@ export class ObjectPicker extends EventDispatcher<Event, 'hoverObjectChanged'|'s | |||
| */ | |||
| static PointerClickMaxDistance = 0.1 // 1/20 of the canvas | |||
| undoManager?: JSUndoManager | |||
| private _root: IObject3D | |||
| private _camera: ICamera | |||
| private _mouseDownTime: number | |||
| @@ -92,9 +93,18 @@ export class ObjectPicker extends EventDispatcher<Event, 'hoverObjectChanged'|'s | |||
| } | |||
| 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 | |||
| const current = [...this._selected] | |||
| this._selected = object ? Array.isArray(object) ? [...object] : [object] : [] | |||
| 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 { | |||
| @@ -65,7 +65,7 @@ export type AnimateResult = ReturnType<typeof animate> | |||
| export function makeSetterFor<V>(target: any, key: string, setDirty?: ()=>void) { | |||
| const v = target[key] as any | |||
| const dirty = ()=>{ | |||
| if (typeof target?.setDirty === 'function') target.setDirty() | |||
| // if (typeof target?.setDirty === 'function') target.setDirty() | |||
| setDirty?.() | |||
| } | |||
| const isBool = typeof v === 'boolean' | |||