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