Pārlūkot izejas kodu

camera manager focus multiple loci/spheres
+ show Unitcell control for multiple structures
+ misc fixes

David Sehnal 5 gadi atpakaļ
vecāks
revīzija
352a20ac48

+ 7 - 7
src/mol-gl/renderable/util.ts

@@ -29,7 +29,7 @@ export interface TextureVolume<T extends Uint8Array | Float32Array> {
     readonly depth: number
 }
 
-export function createTextureImage<T extends Uint8Array | Float32Array>(n: number, itemSize: number, arrayCtor: new (length: number) => T, array?: T): TextureImage<T> {
+export function createTextureImage<T extends Uint8Array | Float32Array>(n: number, itemSize: number, arrayCtor: new (length: number)=> T, array?: T): TextureImage<T> {
     const { length, width, height } = calculateTextureInfo(n, itemSize)
     array = array && array.length >= length ? array : new arrayCtor(length)
     return { array, width, height }
@@ -95,12 +95,12 @@ export function calculateInvariantBoundingSphere(position: Float32Array, positio
     boundaryHelper.reset()
     for (let i = 0, _i = positionCount * 3; i < _i; i += step) {
         Vec3.fromArray(v, position, i)
-        boundaryHelper.includeStep(v)
+        boundaryHelper.includePosition(v)
     }
     boundaryHelper.finishedIncludeStep()
     for (let i = 0, _i = positionCount * 3; i < _i; i += step) {
         Vec3.fromArray(v, position, i)
-        boundaryHelper.radiusStep(v)
+        boundaryHelper.radiusPosition(v)
     }
 
     const sphere = boundaryHelper.getSphere()
@@ -126,25 +126,25 @@ export function calculateTransformBoundingSphere(invariantBoundingSphere: Sphere
         for (let i = 0, _i = transformCount; i < _i; ++i) {
             for (const e of extrema) {
                 Vec3.transformMat4Offset(v, e, transform, 0, 0, i * 16)
-                boundaryHelper.includeStep(v)
+                boundaryHelper.includePosition(v)
             }
         }
         boundaryHelper.finishedIncludeStep()
         for (let i = 0, _i = transformCount; i < _i; ++i) {
             for (const e of extrema) {
                 Vec3.transformMat4Offset(v, e, transform, 0, 0, i * 16)
-                boundaryHelper.radiusStep(v)
+                boundaryHelper.radiusPosition(v)
             }
         }
     } else {
         for (let i = 0, _i = transformCount; i < _i; ++i) {
             Vec3.transformMat4Offset(v, center, transform, 0, 0, i * 16)
-            boundaryHelper.includeSphereStep(v, radius)
+            boundaryHelper.includePositionRadius(v, radius)
         }
         boundaryHelper.finishedIncludeStep()
         for (let i = 0, _i = transformCount; i < _i; ++i) {
             Vec3.transformMat4Offset(v, center, transform, 0, 0, i * 16)
-            boundaryHelper.radiusSphereStep(v, radius)
+            boundaryHelper.radiusPositionRadius(v, radius)
         }
     }
 

+ 2 - 14
src/mol-gl/scene.ts

@@ -28,13 +28,7 @@ function calculateBoundingSphere(renderables: Renderable<RenderableValues & Base
         const boundingSphere = renderables[i].values.boundingSphere.ref.value
         if (!boundingSphere.radius) continue;
 
-        if (Sphere3D.hasExtrema(boundingSphere)) {
-            for (const e of boundingSphere.extrema) {
-                boundaryHelper.includeStep(e)
-            }
-        } else {
-            boundaryHelper.includeSphereStep(boundingSphere.center, boundingSphere.radius);
-        }
+        boundaryHelper.includeSphere(boundingSphere);
     }
     boundaryHelper.finishedIncludeStep();
     for (let i = 0, il = renderables.length; i < il; ++i) {
@@ -43,13 +37,7 @@ function calculateBoundingSphere(renderables: Renderable<RenderableValues & Base
         const boundingSphere = renderables[i].values.boundingSphere.ref.value
         if (!boundingSphere.radius) continue;
 
-        if (Sphere3D.hasExtrema(boundingSphere)) {
-            for (const e of boundingSphere.extrema) {
-                boundaryHelper.radiusStep(e)
-            }
-        } else {
-            boundaryHelper.radiusSphereStep(boundingSphere.center, boundingSphere.radius);
-        }
+        boundaryHelper.radiusSphere(boundingSphere);
     }
 
     return boundaryHelper.getSphere(boundingSphere);

+ 24 - 4
src/mol-math/geometry/boundary-helper.ts

@@ -45,13 +45,23 @@ export class BoundaryHelper {
         }
     }
 
-    includeStep(p: Vec3) {
+    includeSphere(s: Sphere3D) {
+        if (Sphere3D.hasExtrema(s)) {
+            for (const e of s.extrema) {
+                this.includePosition(e);
+            }
+        } else {
+            this.includePositionRadius(s.center, s.radius);
+        }
+    }
+
+    includePosition(p: Vec3) {
         for (let i = 0, il = this.dir.length; i < il; ++i) {
             this.computeExtrema(i, p)
         }
     }
 
-    includeSphereStep(center: Vec3, radius: number) {
+    includePositionRadius(center: Vec3, radius: number) {
         for (let i = 0, il = this.dir.length; i < il; ++i) {
             this.computeSphereExtrema(i, center, radius)
         }
@@ -64,11 +74,21 @@ export class BoundaryHelper {
         this.centroidHelper.finishedIncludeStep();
     }
 
-    radiusStep(p: Vec3) {
+    radiusSphere(s: Sphere3D) {
+        if (Sphere3D.hasExtrema(s)) {
+            for (const e of s.extrema) {
+                this.radiusPosition(e)
+            }
+        } else {
+            this.radiusPositionRadius(s.center, s.radius);
+        }
+    }
+
+    radiusPosition(p: Vec3) {
         this.centroidHelper.radiusStep(p);
     }
 
-    radiusSphereStep(center: Vec3, radius: number) {
+    radiusPositionRadius(center: Vec3, radius: number) {
         this.centroidHelper.radiusSphereStep(center, radius);
     }
 

+ 2 - 2
src/mol-math/geometry/boundary.ts

@@ -28,13 +28,13 @@ export function getBoundary(data: PositionData): Boundary {
     for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) {
         const i = OrderedSet.getAt(indices, t);
         Vec3.set(p, x[i], y[i], z[i]);
-        boundaryHelper.includeSphereStep(p, (radius && radius[i]) || 0);
+        boundaryHelper.includePositionRadius(p, (radius && radius[i]) || 0);
     }
     boundaryHelper.finishedIncludeStep();
     for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) {
         const i = OrderedSet.getAt(indices, t);
         Vec3.set(p, x[i], y[i], z[i]);
-        boundaryHelper.radiusSphereStep(p, (radius && radius[i]) || 0);
+        boundaryHelper.radiusPositionRadius(p, (radius && radius[i]) || 0);
     }
 
     const sphere = boundaryHelper.getSphere()

+ 2 - 2
src/mol-model/loci.ts

@@ -71,9 +71,9 @@ namespace Loci {
     export function getBundleBoundingSphere(bundle: Bundle<any>): Sphere3D {
         const spheres = bundle.loci.map(l => getBoundingSphere(l)).filter(s => !!s) as Sphere3D[]
         boundaryHelper.reset();
-        for (const s of spheres) boundaryHelper.includeSphereStep(s.center, s.radius);
+        for (const s of spheres) boundaryHelper.includePositionRadius(s.center, s.radius);
         boundaryHelper.finishedIncludeStep();
-        for (const s of spheres) boundaryHelper.radiusSphereStep(s.center, s.radius);
+        for (const s of spheres) boundaryHelper.radiusPositionRadius(s.center, s.radius);
         return boundaryHelper.getSphere();
     }
 

+ 2 - 2
src/mol-model/structure/structure/element/loci.ts

@@ -485,7 +485,7 @@ export namespace Loci {
             for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
                 const eI = elements[OrderedSet.getAt(indices, i)];
                 pos(eI, tempPosBoundary);
-                boundaryHelper.includeSphereStep(tempPosBoundary, r(eI));
+                boundaryHelper.includePositionRadius(tempPosBoundary, r(eI));
             }
         }
         boundaryHelper.finishedIncludeStep();
@@ -496,7 +496,7 @@ export namespace Loci {
             for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
                 const eI = elements[OrderedSet.getAt(indices, i)];
                 pos(eI, tempPosBoundary);
-                boundaryHelper.radiusSphereStep(tempPosBoundary, r(eI));
+                boundaryHelper.radiusPositionRadius(tempPosBoundary, r(eI));
             }
         }
 

+ 4 - 4
src/mol-model/structure/structure/util/boundary.ts

@@ -34,14 +34,14 @@ export function computeStructureBoundary(s: Structure): Boundary {
             Vec3.min(min, min, invariantBoundary.box.min);
             Vec3.max(max, max, invariantBoundary.box.max);
 
-            boundaryHelper.includeSphereStep(invariantBoundary.sphere.center, invariantBoundary.sphere.radius);
+            boundaryHelper.includePositionRadius(invariantBoundary.sphere.center, invariantBoundary.sphere.radius);
         } else {
             Box3D.transform(tmpBox, invariantBoundary.box, o.matrix);
             Vec3.min(min, min, tmpBox.min);
             Vec3.max(max, max, tmpBox.max);
 
             Sphere3D.transform(tmpSphere, invariantBoundary.sphere, o.matrix);
-            boundaryHelper.includeSphereStep(tmpSphere.center, tmpSphere.radius);
+            boundaryHelper.includePositionRadius(tmpSphere.center, tmpSphere.radius);
         }
     }
 
@@ -53,10 +53,10 @@ export function computeStructureBoundary(s: Structure): Boundary {
         const o = u.conformation.operator;
 
         if (o.isIdentity) {
-            boundaryHelper.radiusSphereStep(invariantBoundary.sphere.center, invariantBoundary.sphere.radius);
+            boundaryHelper.radiusPositionRadius(invariantBoundary.sphere.center, invariantBoundary.sphere.radius);
         } else {
             Sphere3D.transform(tmpSphere, invariantBoundary.sphere, o.matrix);
-            boundaryHelper.radiusSphereStep(tmpSphere.center, tmpSphere.radius);
+            boundaryHelper.radiusPositionRadius(tmpSphere.center, tmpSphere.radius);
         }
     }
 

+ 54 - 5
src/mol-plugin-state/manager/camera.ts

@@ -10,6 +10,7 @@ import { PluginContext } from '../../mol-plugin/context';
 import { PrincipalAxes } from '../../mol-math/linear-algebra/matrix/principal-axes';
 import { Camera } from '../../mol-canvas3d/camera';
 import { Loci } from '../../mol-model/loci';
+import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
 
 // TODO: make this customizable somewhere?
 const DefaultCameraFocusOptions = {
@@ -21,16 +22,64 @@ const DefaultCameraFocusOptions = {
 export type CameraFocusOptions = typeof DefaultCameraFocusOptions
 
 export class CameraManager {
-    focusLoci(loci: Loci, options?: Partial<CameraFocusOptions>) {
+    private boundaryHelper = new BoundaryHelper('98');
+
+    focusLoci(loci: Loci | Loci[], options?: Partial<CameraFocusOptions>) {
         // TODO: allow computation of principal axes here?
         // perhaps have an optimized function, that does exact axes small Loci and approximate/sampled from big ones?
 
-        const { extraRadius, minRadius, durationMs } = { ...DefaultCameraFocusOptions, ...options };
-        const sphere = Loci.getBoundingSphere(loci);
+        let sphere: Sphere3D | undefined;
+
+        if (Array.isArray(loci) && loci.length > 1) {
+            const spheres = [];
+            for (const l of loci) {
+                const s = Loci.getBoundingSphere(l);
+                if (s) spheres.push(s);
+            }
+
+            if (spheres.length === 0) return;
+
+            this.boundaryHelper.reset();
+            for (const s of spheres) {
+                this.boundaryHelper.includeSphere(s);
+            }
+            this.boundaryHelper.finishedIncludeStep();
+            for (const s of spheres) {
+                this.boundaryHelper.radiusSphere(s);
+            }
+            sphere = this.boundaryHelper.getSphere();
+        } else if (Array.isArray(loci)) {
+            if (loci.length === 0) return;
+            sphere = Loci.getBoundingSphere(loci[0]);
+        } else {
+            sphere = Loci.getBoundingSphere(loci);
+        }
+
         if (sphere) {
-            const radius = Math.max(sphere.radius + extraRadius, minRadius);
-            this.plugin.canvas3d?.camera.focus(sphere.center, radius, durationMs);
+            this.focusSphere(sphere, options);
+        }
+    }
+
+    focusSpheres<T>(xs: ReadonlyArray<T>, sphere: (t: T) => Sphere3D | undefined, options?: Partial<CameraFocusOptions>) {
+        const spheres = [];
+
+        for (const x of xs) {
+            const s = sphere(x);
+            if (s) spheres.push(s);
+        }
+
+        if (spheres.length === 0) return;
+        if (spheres.length === 1) return this.focusSphere(spheres[0], options);
+
+        this.boundaryHelper.reset();
+        for (const s of spheres) {
+            this.boundaryHelper.includeSphere(s);
+        }
+        this.boundaryHelper.finishedIncludeStep();
+        for (const s of spheres) {
+            this.boundaryHelper.radiusSphere(s);
         }
+        this.focusSphere(this.boundaryHelper.getSphere(), options);
     }
 
     focusSphere(sphere: Sphere3D, options?: Partial<CameraFocusOptions> & { principalAxes?: PrincipalAxes }) {

+ 10 - 0
src/mol-plugin-state/manager/structure/hierarchy.ts

@@ -10,6 +10,7 @@ import { PluginComponent } from '../../component';
 import { SetUtils } from '../../../mol-util/set';
 import { StateTransform } from '../../../mol-state';
 import { applyTrajectoryHierarchyPreset } from '../../builder/structure/hierarchy-preset';
+import { setSubtreeVisibility } from '../../../mol-plugin/behavior/static/state';
 
 interface StructureHierarchyManagerState {
     hierarchy: StructureHierarchy,
@@ -125,6 +126,15 @@ export class StructureHierarchyManager extends PluginComponent<StructureHierarch
         return this.plugin.updateDataState(deletes, { canUndo: canUndo ? 'Remove' : false });
     }
 
+    toggleVisibility(refs: ReadonlyArray<HierarchyRef>) {
+        if (refs.length === 0) return;
+
+        const isHidden = !refs[0].cell.state.isHidden;
+        for (const c of refs) {
+            setSubtreeVisibility(this.dataState, c.cell.transform.ref, isHidden);
+        }
+    }
+
     createModels(trajectories: ReadonlyArray<TrajectoryRef>, kind: 'single' | 'all' = 'single') {
         return this.plugin.dataTransaction(async () => {
             for (const trajectory of trajectories) {

+ 2 - 2
src/mol-plugin-state/manager/structure/selection.ts

@@ -346,12 +346,12 @@ export class StructureSelectionManager extends PluginComponent<StructureSelectio
             const { box, sphere } = boundaries[i];
             Vec3.min(min, min, box.min);
             Vec3.max(max, max, box.max);
-            boundaryHelper.includeSphereStep(sphere.center, sphere.radius)
+            boundaryHelper.includePositionRadius(sphere.center, sphere.radius)
         }
         boundaryHelper.finishedIncludeStep();
         for (let i = 0, il = boundaries.length; i < il; ++i) {
             const { sphere } = boundaries[i];
-            boundaryHelper.radiusSphereStep(sphere.center, sphere.radius);
+            boundaryHelper.radiusPositionRadius(sphere.center, sphere.radius);
         }
 
         return { box: { min, max }, sphere: boundaryHelper.getSphere() };

+ 4 - 2
src/mol-plugin-ui/structure/components.tsx

@@ -329,8 +329,10 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
     }
 
     focus = () => {
-        const sphere = this.pivot.cell.obj?.data.boundary.sphere;
-        if (sphere) this.plugin.managers.camera.focusSphere(sphere);
+        this.plugin.managers.camera.focusSpheres(this.props.group, e => {
+            if (e.cell.state.isHidden) return;
+            return e.cell.obj?.data.boundary.sphere;
+        });
     }
 
     render() {

+ 7 - 11
src/mol-plugin-ui/structure/source.tsx

@@ -248,20 +248,16 @@ export class StructureSourceControls extends CollapsableControls<{}, StructureSo
 
     get unitcell() {
         const { selection } = this.plugin.managers.structure.hierarchy;
-        if (selection.structures.length !== 1) return null;
-
-        const model = selection.structures[0].model
-        if (!model) return null
+        if (selection.structures.length === 0) return null;
 
-        const unitcell = model.unitcell
-        if (!unitcell) {
-            // this.plugin.builders.structure.createUnitcell(model.cell, undefined, { isHidden: true })
-            return null
-        } else if (!unitcell.cell.obj) {
-            return null;
+        const refs = [];
+        for (const s of selection.structures) {
+            const model = s.model;
+            if (model?.unitcell && model.unitcell?.cell.obj) refs.push(model.unitcell);
         }
+        if (refs.length === 0) return null;
 
-        return <UnitcellEntry key={unitcell.cell.obj.id} cell={unitcell.cell} />
+        return <UnitcellEntry refs={refs} />;
     }
 
     renderControls() {

+ 38 - 20
src/mol-plugin-ui/structure/unitcell.tsx

@@ -2,36 +2,40 @@
  * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
  */
 
 import * as React from 'react';
+import { ModelUnitcellRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
+import { PluginCommands } from '../../mol-plugin/commands';
+import { State } from '../../mol-state';
 import { PurePluginUIComponent } from '../base';
-import { StateTransformer, StateTransform, StateObjectCell } from '../../mol-state';
 import { IconButton } from '../controls/common';
 import { UpdateTransformControl } from '../state/update-transform';
-import { PluginStateObject } from '../../mol-plugin-state/objects';
-import { PluginCommands } from '../../mol-plugin/commands';
-
-export type UnitcellCell = StateObjectCell<PluginStateObject.Shape.Representation3D, StateTransform<StateTransformer<PluginStateObject.Molecule.Model, PluginStateObject.Shape.Representation3D, any>>>
 
-export class UnitcellEntry extends PurePluginUIComponent<{ cell: UnitcellCell }, { showOptions: boolean }> {
+export class UnitcellEntry extends PurePluginUIComponent<{ refs: ModelUnitcellRef[] }, { showOptions: boolean }> {
     state = { showOptions: false }
 
     componentDidMount() {
         this.subscribe(this.plugin.events.state.cell.stateUpdated, e => {
-            this.forceUpdate();
+            if (State.ObjectEvent.isCell(e, this.pivot?.cell)) this.forceUpdate();
         });
     }
 
+    get pivot() { return this.props.refs[0]; }
+
     toggleVisibility = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.ToggleVisibility(this.plugin, { state: this.props.cell.parent, ref: this.props.cell.transform.ref });
+        this.plugin.managers.structure.hierarchy.toggleVisibility(this.props.refs);
         e.currentTarget.blur();
     }
 
     highlight = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.Interactivity.Object.Highlight(this.plugin, { state: this.props.cell.parent, ref: this.props.cell.transform.ref });
+        PluginCommands.Interactivity.Object.Highlight(this.plugin, {
+            state: this.pivot.cell.parent,
+            ref: this.props.refs.map(c => c.cell.transform.ref)
+        });
     }
 
     clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
@@ -41,32 +45,46 @@ export class UnitcellEntry extends PurePluginUIComponent<{ cell: UnitcellCell },
 
     focus = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        if (!this.props.cell.state.isHidden) {
-            const loci = this.props.cell.obj?.data.repr.getLoci()
-            if (loci) this.plugin.managers.camera.focusLoci(loci);
+
+        const loci = [];
+        for (const uc of this.props.refs) {
+            if (uc.cell.state.isHidden) continue;
+
+            const l = uc.cell.obj?.data.repr.getLoci()
+            if (l) loci.push(l);
         }
+        this.plugin.managers.camera.focusLoci(loci);
     }
 
     toggleOptions = () => this.setState({ showOptions: !this.state.showOptions })
 
     render() {
-        const { cell } = this.props;
-        const { obj } = cell;
-        if (!obj) return null;
+        const { refs } = this.props;
+        if (refs.length === 0) return null;
 
-        const { label, description } = obj
+        const pivot = refs[0];
+
+        let label, description;
+        if (refs.length === 1) {
+            const { obj } = pivot.cell;
+            if (!obj) return null;
+            label = obj?.label;
+            description = obj?.description;
+        } else {
+            label = 'Unitcells';
+        }
 
         return <>
             <div className='msp-btn-row-group' style={{ marginTop: '6px' }}>
                 <button className='msp-form-control msp-control-button-label' title={`${label}. Click to focus.`} onClick={this.focus} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={{ textAlign: 'left' }}>
                     {label} <small>{description}</small>
                 </button>
-                <IconButton customClass='msp-form-control' onClick={this.toggleVisibility} icon='visual-visibility' toggleState={!cell.state.isHidden} title={`${cell.state.isHidden ? 'Show' : 'Hide'}`} small style={{ flex: '0 0 32px' }} />
-                <IconButton customClass='msp-form-control' onClick={this.toggleOptions} icon='dot-3' title='Options' toggleState={this.state.showOptions} style={{ flex: '0 0 32px', padding: '0px' }} />
+                <IconButton customClass='msp-form-control' onClick={this.toggleVisibility} icon='visual-visibility' toggleState={!pivot.cell.state.isHidden} title={`${pivot.cell.state.isHidden ? 'Show' : 'Hide'}`} small style={{ flex: '0 0 32px' }} />
+                {refs.length === 1 && <IconButton customClass='msp-form-control' onClick={this.toggleOptions} icon='dot-3' title='Options' toggleState={this.state.showOptions} style={{ flex: '0 0 32px', padding: '0px' }} />}
             </div>
-            {this.state.showOptions && <>
+            {(refs.length === 1 && this.state.showOptions) && <>
                 <div className='msp-control-offset'>
-                    <UpdateTransformControl state={cell.parent} transform={cell.transform} customHeader='none' autoHideApply />
+                    <UpdateTransformControl state={pivot.cell.parent} transform={pivot.cell.transform} customHeader='none' autoHideApply />
                 </div>
             </>}
         </>;

+ 2 - 2
src/mol-repr/structure/visual/label-text.ts

@@ -120,12 +120,12 @@ function createResidueText(ctx: VisualContext, structure: Structure, theme: Them
             boundaryHelper.reset();
             for (let eI = start; eI < j; eI++) {
                 pos(elements[eI], tmpVec);
-                boundaryHelper.includeStep(tmpVec);
+                boundaryHelper.includePosition(tmpVec);
             }
             boundaryHelper.finishedIncludeStep();
             for (let eI = start; eI < j; eI++) {
                 pos(elements[eI], tmpVec);
-                boundaryHelper.radiusStep(tmpVec);
+                boundaryHelper.radiusPosition(tmpVec);
             }
 
             l.element = elements[start];

+ 10 - 10
src/mol-state/state.ts

@@ -78,7 +78,7 @@ class State {
 
         this.events.historyUpdated.next({ state: this });
     }
-    
+
     private clearHistory() {
         if (this.history.length === 0) return;
         this.history = [];
@@ -243,13 +243,13 @@ class State {
                 return ret.cell;
             } finally {
                 this.updateQueue.handled(params);
-                if (this.inTransaction) return;
-                
-                this.events.isUpdating.next(false);
-                if (!options?.canUndo) {
-                    if (!this.undoingHistory) this.clearHistory();
-                } else if (!reverted) {
-                    this.addHistory(snapshot!, typeof options.canUndo === 'string' ? options.canUndo : void 0);
+                if (!this.inTransaction) {
+                    this.events.isUpdating.next(false);
+                    if (!options?.canUndo) {
+                        if (!this.undoingHistory) this.clearHistory();
+                    } else if (!reverted) {
+                        this.addHistory(snapshot!, typeof options.canUndo === 'string' ? options.canUndo : void 0);
+                    }
                 }
             }
         }, () => {
@@ -358,8 +358,8 @@ namespace State {
     }
 
     export namespace ObjectEvent {
-        export function isCell(e: ObjectEvent, cell: StateObjectCell) {
-            return e.ref === cell.transform.ref && e.state === cell.parent
+        export function isCell(e: ObjectEvent, cell?: StateObjectCell) {
+            return !!cell && e.ref === cell.transform.ref && e.state === cell.parent;
         }
     }