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.

206 lines 9.54 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { AViewerPluginSync } from '../../viewer'; import { getUrlQueryParam, JSUndoManager, onChange, recordUndoCommand, setValueUndoCommand, } from 'ts-browser-helpers'; /** * UndoManagerPlugin is a plugin for ThreeViewer that provides undo/redo functionality. * It uses the JSUndoManager(from ts-browser-helpers) library to maintain a common undo/redo history across the viewer and other plugins. */ // @uiPanelContainer('Undo Manager') export class UndoManagerPlugin extends AViewerPluginSync { constructor(enabled = true, limit = 1000) { super(); // @uiToggle() this.enabled = true; this.limit = 1000; this.toJSON = undefined; this.undoEditingWaitTime = 2000; // todo sync time with any ui plugins this.undoCommandTypes = { setValue: 'ThreeViewerUM_set', action: 'ThreeViewerUM_action', }; this.undoPresets = { [this.undoCommandTypes.setValue]: (c) => { const ref = () => { c.onUndoRedo && c.onUndoRedo(c); // c.uid.uiRefresh?.(false) }; return { undo: () => { console.log('undo', c.lastVal); if (!c.binding) return; this.setValue(c.binding, c.lastVal, c.props, c.uid, undefined, false); // .then(ref) ref(); }, redo: () => { // console.log('redo', c.val) if (!c.binding) return; this.setValue(c.binding, c.val, c.props, c.uid, undefined, false); // .then(ref) ref(); }, }; }, [this.undoCommandTypes.action]: (c) => { const ref = () => { c.onUndoRedo && c.onUndoRedo(c); }; return { undo: async () => { await c.undo.call(c.target, ...c.args); ref(); }, redo: async () => { await c.redo.call(c.target, ...c.args); ref(); }, }; }, }; this.enabled = enabled; this.limit = limit; } _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; if (this.undoManager) Object.assign(this.undoManager.presets, this.undoPresets); } onAdded(viewer) { super.onAdded(viewer); this.undoManager = new JSUndoManager({ bindHotKeys: true, limit: this.limit, debug: viewer.debug || getUrlQueryParam('debugUndo') !== null, hotKeyRoot: document }); this._refresh(); } onRemove(viewer) { this.undoManager?.dispose(); this.undoManager = undefined; super.onRemove(viewer); } recordUndo(com) { return recordUndoCommand(this.undoManager, com, this.undoCommandTypes.setValue, this.undoEditingWaitTime); } /** * Performs an action with undo/redo support. * @param targ - the target object to call the action on * @param action - a function that returns - 1. an undo function, 2. an object with undo and redo functions (and optional action) * @param args - the arguments to pass to the action function * @param uid - unique identifier for the command, not really used in actions * @param onUndoRedo - optional callback function to be called on undo/redo of the command. Not called on first action execution, only on undo/redo. */ async performAction(targ, action, args, uid, onUndoRedo) { const ac = () => targ === undefined ? action(...args) : action.call(targ, ...args); // if a function is returned, it is treated as undo function let res = await ac(); const undo = typeof res === 'function' ? res : res?.undo?.bind(res); const resAction = typeof res !== 'function' ? res?.action?.bind(res) : null; const redo = typeof res === 'function' ? ac : res?.redo?.bind(res) ?? resAction; if (typeof resAction === 'function') { res = await resAction(); // execute the action now. adding await just in case } if (typeof undo === 'function') { this.recordUndo({ type: 'UiConfigMethods_action', uid: uid, target: targ, undo: undo, redo: redo, args, onUndoRedo, }); } } /** * Sets a value in the target object with undo/redo support. * @param binding - a tuple of target object and key to set the value on * @param value - the value to set * @param props - properties for the undo command, including last, and lastValue(optional) * @param uid - unique identifier for the command, used to merge commands * @param forceOnChange * @param trackUndo - whether to track the undo command or not, defaults to true * @param onUndoRedo - optional callback function to be called on undo/redo of the command * @returns true if the value was set and the command was recorded, false if the command was not recorded (e.g. if it was not undoable or forceOnChange was false) */ setValue(binding, value, props, uid, forceOnChange, trackUndo = true, onUndoRedo) { const ev = setValueUndoCommand(this.undoManager, binding, value, props, uid, this.undoCommandTypes.setValue, trackUndo, this.undoEditingWaitTime, true, onUndoRedo); if (!ev.undoable && !forceOnChange) return false; // this.dispatchOnChangeSync({...props, ...ev}) // todo return true; } setValues(bindings, defs, v, props, uid, forceOnChange, trackUndo = true, onUndoRedo) { // array proxy for bindings, this is required because undo modifies arrays in place, and it's better as we only update the bindings that are actually changed. const proxy = createBindingsProxy(bindings, defs); return this.setValue([proxy, 'value'], v, props, uid, forceOnChange, trackUndo, onUndoRedo); } } UndoManagerPlugin.PluginType = 'UndoManagerPlugin'; __decorate([ onChange(UndoManagerPlugin.prototype._refresh) ], UndoManagerPlugin.prototype, "enabled", void 0); __decorate([ onChange(UndoManagerPlugin.prototype._refresh) ], UndoManagerPlugin.prototype, "limit", void 0); /** * Creates a proxy for an array of bindings, allowing to access and set values in the target objects by editing the value. * Useful for updating multiple properties in a single undo/redo command when dragging. * @param bindings * @param defs */ export function createBindingsProxy(bindings, defs) { return { p: new Proxy([], { get(_target, p, ...rest) { if (p === 'length') { return bindings.length; } const index = Number(p); if (isNaN(index) || index < 0 || index >= bindings.length) { return Reflect.get(Array.prototype, p, ...rest) || Reflect.get(_target, p, ...rest); } const [target, key] = bindings[index]; return target?.[key] ?? defs[index]; }, set(_target, p, newValue, ...rest) { const index = Number(p); if (isNaN(index) || index < 0 || index >= bindings.length) { return Reflect.set(_target, p, newValue, ...rest); } const [target, key] = bindings[index]; if (target) { target[key] = newValue; return true; } return false; }, // for every etc. has(_target, p, ...rest) { const index = Number(p); if (isNaN(index) || index < 0 || index >= bindings.length) { return Reflect.has(Array.prototype, p, ...rest) || Reflect.has(_target, p, ...rest); } return true; }, }), get value() { return this.p; }, set value(va) { if (bindings.length !== va.length) { console.error(`UndoManager - setValues: bindings length (${bindings.length}) does not match value length (${va.length})`); } for (let i = 0; i < Math.min(va.length, bindings.length); i++) { this.p[i] = va[i]; } }, }; } //# sourceMappingURL=UndoManagerPlugin.js.map