浏览代码

move clip variant and objects to repr state

Alexander Rose 3 年之前
父节点
当前提交
504406eb22

+ 36 - 8
src/mol-geo/geometry/clipping-data.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -16,12 +16,16 @@ export type ClippingData = {
     tClipping: ValueCell<TextureImage<Uint8Array>>
     uClippingTexDim: ValueCell<Vec2>
     dClipping: ValueCell<boolean>,
+
+    uClipObjectType: ValueCell<number[]>,
+    uClipObjectInvert: ValueCell<boolean[]>,
+    uClipObjectPosition: ValueCell<number[]>,
+    uClipObjectRotation: ValueCell<number[]>,
+    uClipObjectScale: ValueCell<number[]>,
 }
 
 export function applyClippingGroups(array: Uint8Array, start: number, end: number, groups: Clipping.Groups) {
-    for (let i = start; i < end; ++i) {
-        array[i] = groups;
-    }
+    array.fill(groups, start, end);
     return true;
 }
 
@@ -29,21 +33,36 @@ export function clearClipping(array: Uint8Array, start: number, end: number) {
     array.fill(0, start, end);
 }
 
-export function createClipping(count: number, clippingData?: ClippingData): ClippingData {
+export function createClipping(count: number, variant: Clipping.Variant, objects: Clipping.Objects, clippingData?: ClippingData): ClippingData {
     const clipping = createTextureImage(Math.max(1, count), 1, Uint8Array, clippingData && clippingData.tClipping.ref.value.array);
     if (clippingData) {
+        ValueCell.update(clippingData.dClipObjectCount, objects.count);
+        ValueCell.update(clippingData.dClipVariant, variant);
+
         ValueCell.update(clippingData.tClipping, clipping);
         ValueCell.update(clippingData.uClippingTexDim, Vec2.create(clipping.width, clipping.height));
         ValueCell.updateIfChanged(clippingData.dClipping, count > 0);
+
+        ValueCell.update(clippingData.uClipObjectType, objects.type);
+        ValueCell.update(clippingData.uClipObjectInvert, objects.invert);
+        ValueCell.update(clippingData.uClipObjectPosition, objects.position);
+        ValueCell.update(clippingData.uClipObjectRotation, objects.rotation);
+        ValueCell.update(clippingData.uClipObjectScale, objects.scale);
         return clippingData;
     } else {
         return {
-            dClipObjectCount: ValueCell.create(0),
-            dClipVariant: ValueCell.create('instance'),
+            dClipObjectCount: ValueCell.create(objects.count),
+            dClipVariant: ValueCell.create(variant),
 
             tClipping: ValueCell.create(clipping),
             uClippingTexDim: ValueCell.create(Vec2.create(clipping.width, clipping.height)),
             dClipping: ValueCell.create(count > 0),
+
+            uClipObjectType: ValueCell.create(objects.type),
+            uClipObjectInvert: ValueCell.create(objects.invert),
+            uClipObjectPosition: ValueCell.create(objects.position),
+            uClipObjectRotation: ValueCell.create(objects.rotation),
+            uClipObjectScale: ValueCell.create(objects.scale),
         };
     }
 }
@@ -51,17 +70,26 @@ export function createClipping(count: number, clippingData?: ClippingData): Clip
 const emptyClippingTexture = { array: new Uint8Array(1), width: 1, height: 1 };
 export function createEmptyClipping(clippingData?: ClippingData): ClippingData {
     if (clippingData) {
+        ValueCell.update(clippingData.dClipObjectCount, 0);
         ValueCell.update(clippingData.tClipping, emptyClippingTexture);
         ValueCell.update(clippingData.uClippingTexDim, Vec2.create(1, 1));
+        ValueCell.updateIfChanged(clippingData.dClipping, false);
         return clippingData;
     } else {
+        const { objects, variant } = Clipping.Empty;
         return {
             dClipObjectCount: ValueCell.create(0),
-            dClipVariant: ValueCell.create('instance'),
+            dClipVariant: ValueCell.create(variant),
 
             tClipping: ValueCell.create(emptyClippingTexture),
             uClippingTexDim: ValueCell.create(Vec2.create(1, 1)),
             dClipping: ValueCell.create(false),
+
+            uClipObjectType: ValueCell.create(objects.type),
+            uClipObjectInvert: ValueCell.create(objects.invert),
+            uClipObjectPosition: ValueCell.create(objects.position),
+            uClipObjectRotation: ValueCell.create(objects.rotation),
+            uClipObjectScale: ValueCell.create(objects.scale),
         };
     }
 }

+ 6 - 6
src/mol-gl/renderable/schema.ts

@@ -137,12 +137,6 @@ export const GlobalUniformSchema = {
 
     uTransparentBackground: UniformSpec('b'),
 
-    uClipObjectType: UniformSpec('i[]'),
-    uClipObjectInvert: UniformSpec('b[]'),
-    uClipObjectPosition: UniformSpec('v3[]'),
-    uClipObjectRotation: UniformSpec('v4[]'),
-    uClipObjectScale: UniformSpec('v3[]'),
-
     // all the following could in principle be per object
     // as a kind of 'material' parameter set
     // would need to test performance implications
@@ -238,6 +232,12 @@ export const ClippingSchema = {
     uClippingTexDim: UniformSpec('v2'),
     tClipping: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
     dClipping: DefineSpec('boolean'),
+
+    uClipObjectType: UniformSpec('i[]'),
+    uClipObjectInvert: UniformSpec('b[]'),
+    uClipObjectPosition: UniformSpec('v3[]'),
+    uClipObjectRotation: UniformSpec('v4[]'),
+    uClipObjectScale: UniformSpec('v3[]'),
 } as const;
 export type ClippingSchema = typeof ClippingSchema
 export type ClippingValues = Values<ClippingSchema>

+ 3 - 73
src/mol-gl/renderer.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -8,16 +8,15 @@ import { Viewport } from '../mol-canvas3d/camera/util';
 import { ICamera } from '../mol-canvas3d/camera';
 import { Scene } from './scene';
 import { WebGLContext } from './webgl/context';
-import { Mat4, Vec3, Vec4, Vec2, Quat } from '../mol-math/linear-algebra';
+import { Mat4, Vec3, Vec4, Vec2 } from '../mol-math/linear-algebra';
 import { GraphicsRenderable } from './renderable';
 import { Color } from '../mol-util/color';
-import { ValueCell, deepEqual } from '../mol-util';
+import { ValueCell } from '../mol-util';
 import { GlobalUniformValues } from './renderable/schema';
 import { GraphicsRenderVariant } from './webgl/render-item';
 import { ParamDefinition as PD } from '../mol-util/param-definition';
 import { Clipping } from '../mol-theme/clipping';
 import { stringToWords } from '../mol-util/string';
-import { degToRad } from '../mol-math/misc';
 import { createNullTexture, Texture, Textures } from './webgl/texture';
 import { arrayMapUpsert } from '../mol-util/array';
 import { clamp } from '../mol-math/interpolate';
@@ -150,47 +149,11 @@ export function getStyle(props: RendererProps['style']): Style {
     }
 }
 
-type Clip = {
-    variant: Clipping.Variant
-    objects: {
-        count: number
-        type: number[]
-        invert: boolean[]
-        position: number[]
-        rotation: number[]
-        scale: number[]
-    }
-}
-
-const tmpQuat = Quat();
-function getClip(props: RendererProps['clip'], clip?: Clip): Clip {
-    const { type, invert, position, rotation, scale } = clip?.objects || {
-        type: (new Array(5)).fill(1),
-        invert: (new Array(5)).fill(false),
-        position: (new Array(5 * 3)).fill(0),
-        rotation: (new Array(5 * 4)).fill(0),
-        scale: (new Array(5 * 3)).fill(1),
-    };
-    for (let i = 0, il = props.objects.length; i < il; ++i) {
-        const p = props.objects[i];
-        type[i] = Clipping.Type[p.type];
-        invert[i] = p.invert;
-        Vec3.toArray(p.position, position, i * 3);
-        Quat.toArray(Quat.setAxisAngle(tmpQuat, p.rotation.axis, degToRad(p.rotation.angle)), rotation, i * 4);
-        Vec3.toArray(p.scale, scale, i * 3);
-    }
-    return {
-        variant: props.variant,
-        objects: { count: props.objects.length, type, invert, position, rotation, scale }
-    };
-}
-
 namespace Renderer {
     export function create(ctx: WebGLContext, props: Partial<RendererProps> = {}): Renderer {
         const { gl, state, stats, extensions: { fragDepth } } = ctx;
         const p = PD.merge(RendererParams, PD.getDefaultValues(RendererParams), props);
         const style = getStyle(p.style);
-        const clip = getClip(p.clip);
 
         const viewport = Viewport();
         const drawingBufferSize = Vec2.create(gl.drawingBufferWidth, gl.drawingBufferHeight);
@@ -245,12 +208,6 @@ namespace Renderer {
 
             uTransparentBackground: ValueCell.create(false),
 
-            uClipObjectType: ValueCell.create(clip.objects.type),
-            uClipObjectInvert: ValueCell.create(clip.objects.invert),
-            uClipObjectPosition: ValueCell.create(clip.objects.position),
-            uClipObjectRotation: ValueCell.create(clip.objects.rotation),
-            uClipObjectScale: ValueCell.create(clip.objects.scale),
-
             // the following are general 'material' uniforms
             uLightIntensity: ValueCell.create(style.lightIntensity),
             uAmbientIntensity: ValueCell.create(style.ambientIntensity),
@@ -279,24 +236,6 @@ namespace Renderer {
                 return;
             }
 
-            let definesNeedUpdate = false;
-            if (r.state.noClip) {
-                if (r.values.dClipObjectCount.ref.value !== 0) {
-                    ValueCell.update(r.values.dClipObjectCount, 0);
-                    definesNeedUpdate = true;
-                }
-            } else {
-                if (r.values.dClipObjectCount.ref.value !== clip.objects.count) {
-                    ValueCell.update(r.values.dClipObjectCount, clip.objects.count);
-                    definesNeedUpdate = true;
-                }
-                if (r.values.dClipVariant.ref.value !== clip.variant) {
-                    ValueCell.update(r.values.dClipVariant, clip.variant);
-                    definesNeedUpdate = true;
-                }
-            }
-            if (definesNeedUpdate) r.update();
-
             const program = r.getProgram(variant);
             if (state.currentProgramId !== program.id) {
                 // console.log('new program')
@@ -633,15 +572,6 @@ namespace Renderer {
                     ValueCell.updateIfChanged(globalUniforms.uRoughness, style.roughness);
                     ValueCell.updateIfChanged(globalUniforms.uReflectivity, style.reflectivity);
                 }
-
-                if (props.clip !== undefined && !deepEqual(props.clip, p.clip)) {
-                    p.clip = props.clip;
-                    Object.assign(clip, getClip(props.clip, clip));
-                    ValueCell.update(globalUniforms.uClipObjectPosition, clip.objects.position);
-                    ValueCell.update(globalUniforms.uClipObjectRotation, clip.objects.rotation);
-                    ValueCell.update(globalUniforms.uClipObjectScale, clip.objects.scale);
-                    ValueCell.update(globalUniforms.uClipObjectType, clip.objects.type);
-                }
             },
             setViewport: (x: number, y: number, width: number, height: number) => {
                 gl.viewport(x, y, width, height);

+ 20 - 1
src/mol-math/linear-algebra/3d/quat.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -346,6 +346,25 @@ namespace Quat {
         return out;
     }
 
+    /**
+     * Returns whether or not the quaternions have exactly the same elements in the same position (when compared with ===)
+     */
+    export function exactEquals(a: Quat, b: Quat) {
+        return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
+    }
+
+    /**
+     * Returns whether or not the quaternions have approximately the same elements in the same position.
+     */
+    export function equals(a: Quat, b: Quat) {
+        const a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3];
+        const b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
+        return (Math.abs(a0 - b0) <= EPSILON * Math.max(1.0, Math.abs(a0), Math.abs(b0)) &&
+                Math.abs(a1 - b1) <= EPSILON * Math.max(1.0, Math.abs(a1), Math.abs(b1)) &&
+                Math.abs(a2 - b2) <= EPSILON * Math.max(1.0, Math.abs(a2), Math.abs(b2)) &&
+                Math.abs(a3 - b3) <= EPSILON * Math.max(1.0, Math.abs(a3), Math.abs(b3)));
+    }
+
     export function add(out: Quat, a: Quat, b: Quat) {
         out[0] = a[0] + b[0];
         out[1] = a[1] + b[1];

+ 2 - 1
src/mol-plugin-state/helpers/structure-clipping.ts

@@ -59,7 +59,8 @@ async function eachRepr(plugin: PluginContext, components: StructureComponentRef
 
 /** filter clipping layers for given structure */
 function getFilteredBundle(layers: Clipping.BundleLayer[], structure: Structure) {
-    const clipping = Clipping.ofBundle(layers, structure.root);
+    // TODO
+    const clipping = Clipping.Empty; // ofBundle(layers, structure.root);
     const merged = Clipping.merge(clipping);
     return Clipping.filter(merged, structure);
 }

+ 7 - 5
src/mol-plugin-state/transforms/representation.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -518,6 +518,7 @@ const ClippingStructureRepresentation3DFromScript = PluginStateTransform.BuiltIn
                 groups: Clipping.Groups.Flag.None,
             }]
         }),
+        ...Clipping.Params
     })
 })({
     canAutoUpdate() {
@@ -525,7 +526,7 @@ const ClippingStructureRepresentation3DFromScript = PluginStateTransform.BuiltIn
     },
     apply({ a, params }) {
         const structure = a.data.sourceData;
-        const clipping = Clipping.ofScript(params.layers, structure);
+        const clipping = Clipping.ofScript(params.layers, structure, Clipping.getClip(params));
 
         return new SO.Molecule.Structure.Representation3DState({
             state: { clipping },
@@ -540,7 +541,7 @@ const ClippingStructureRepresentation3DFromScript = PluginStateTransform.BuiltIn
         if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
 
         const oldClipping = b.data.state.clipping!;
-        const newClipping = Clipping.ofScript(newParams.layers, structure);
+        const newClipping = Clipping.ofScript(newParams.layers, structure, Clipping.getClip(newParams));
         if (Clipping.areEqual(oldClipping, newClipping)) return StateTransformer.UpdateResult.Unchanged;
 
         b.data.state.clipping = newClipping;
@@ -567,6 +568,7 @@ const ClippingStructureRepresentation3DFromBundle = PluginStateTransform.BuiltIn
             }],
             isHidden: true
         }),
+        ...Clipping.Params
     })
 })({
     canAutoUpdate() {
@@ -574,7 +576,7 @@ const ClippingStructureRepresentation3DFromBundle = PluginStateTransform.BuiltIn
     },
     apply({ a, params }) {
         const structure = a.data.sourceData;
-        const clipping = Clipping.ofBundle(params.layers, structure);
+        const clipping = Clipping.ofBundle(params.layers, structure, Clipping.getClip(params));
 
         return new SO.Molecule.Structure.Representation3DState({
             state: { clipping },
@@ -589,7 +591,7 @@ const ClippingStructureRepresentation3DFromBundle = PluginStateTransform.BuiltIn
         if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
 
         const oldClipping = b.data.state.clipping!;
-        const newClipping = Clipping.ofBundle(newParams.layers, structure);
+        const newClipping = Clipping.ofBundle(newParams.layers, structure, Clipping.getClip(newParams));
         if (Clipping.areEqual(oldClipping, newClipping)) return StateTransformer.UpdateResult.Unchanged;
 
         b.data.state.clipping = newClipping;

+ 1 - 0
src/mol-plugin/spec.ts

@@ -101,6 +101,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({
         PluginSpec.Action(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.OverpaintStructureRepresentation3DFromScript),
         PluginSpec.Action(StateTransforms.Representation.TransparencyStructureRepresentation3DFromScript),
+        PluginSpec.Action(StateTransforms.Representation.ClippingStructureRepresentation3DFromScript),
 
         PluginSpec.Action(AssignColorVolume),
         PluginSpec.Action(StateTransforms.Volume.VolumeFromCcp4),

+ 3 - 2
src/mol-repr/visual.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -142,9 +142,10 @@ namespace Visual {
 
         const { tClipping, uGroupCount, instanceCount } = renderObject.values;
         const count = uGroupCount.ref.value * instanceCount.ref.value;
+        const { layers, variant, objects } = clipping;
 
         // ensure texture has right size
-        createClipping(clipping.layers.length ? count : 0, renderObject.values);
+        createClipping(layers.length ? count : 0, variant, objects, renderObject.values);
         const { array } = tClipping.ref.value;
 
         // clear if requested

+ 111 - 14
src/mol-theme/clipping.ts

@@ -1,25 +1,48 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
+import { Quat, Vec3 } from '../mol-math/linear-algebra';
+import { degToRad } from '../mol-math/misc';
 import { Loci } from '../mol-model/loci';
 import { StructureElement, Structure } from '../mol-model/structure';
 import { Script } from '../mol-script/script';
 import { BitFlags } from '../mol-util/bit-flags';
+import { ParamDefinition as PD } from '../mol-util/param-definition';
+import { stringToWords } from '../mol-util/string';
 
 export { Clipping };
 
-type Clipping = { readonly layers: ReadonlyArray<Clipping.Layer> }
+type Clipping = {
+    readonly layers: ReadonlyArray<Clipping.Layer>
+    readonly variant: Clipping.Variant
+    readonly objects: Clipping.Objects
+}
+
+function Clipping(layers: Clipping['layers'], variant: Clipping['variant'], objects: Clipping['objects']): Clipping {
+    return { layers, variant, objects };
+}
 
-function Clipping(layers: ReadonlyArray<Clipping.Layer>): Clipping {
-    return { layers };
+function createClipObjects() {
+    return {
+        count: 0,
+        type: (new Array(5)).fill(1),
+        invert: (new Array(5)).fill(false),
+        position: (new Array(5 * 3)).fill(0),
+        rotation: (new Array(5 * 4)).fill(0),
+        scale: (new Array(5 * 3)).fill(1),
+    };
 }
 
 namespace Clipping {
     export type Layer = { readonly loci: StructureElement.Loci, readonly groups: Groups }
-    export const Empty: Clipping = { layers: [] };
+    export const Empty: Clipping = {
+        layers: [],
+        variant: 'pixel',
+        objects: createClipObjects()
+    };
 
     export type Groups = BitFlags<Groups.Flag>
     export namespace Groups {
@@ -95,21 +118,93 @@ namespace Clipping {
 
     export type Variant = 'instance' | 'pixel'
 
+    export type Objects = {
+        count: number
+        type: number[]
+        invert: boolean[]
+        position: number[]
+        rotation: number[]
+        scale: number[]
+    }
+
+    export const Params = {
+        variant: PD.Select('instance', PD.arrayToOptions<Variant>(['instance', 'pixel'])),
+        objects: PD.ObjectList({
+            type: PD.Select('plane', PD.objectToOptions(Type, t => stringToWords(t))),
+            invert: PD.Boolean(false),
+            position: PD.Vec3(Vec3()),
+            rotation: PD.Group({
+                axis: PD.Vec3(Vec3.create(1, 0, 0)),
+                angle: PD.Numeric(0, { min: -180, max: 180, step: 1 }, { description: 'Angle in Degrees' }),
+            }, { isExpanded: true }),
+            scale: PD.Vec3(Vec3.create(1, 1, 1)),
+        }, o => stringToWords(o.type))
+    };
+    export type Params = typeof Params
+    export type Props = PD.Values<Params>
+
+    export type Clip = {
+        variant: Clipping['variant'],
+        objects: Clipping['objects']
+    }
+
+    const qA = Quat();
+    const qB = Quat();
+    const vA = Vec3();
+    const vB = Vec3();
+
+    export function getClip(props: Props, clip?: Clip): Clip {
+        const { type, invert, position, rotation, scale } = clip?.objects || createClipObjects();
+        for (let i = 0, il = props.objects.length; i < il; ++i) {
+            const p = props.objects[i];
+            type[i] = Type[p.type];
+            invert[i] = p.invert;
+            Vec3.toArray(p.position, position, i * 3);
+            Quat.toArray(Quat.setAxisAngle(qA, p.rotation.axis, degToRad(p.rotation.angle)), rotation, i * 4);
+            Vec3.toArray(p.scale, scale, i * 3);
+        }
+        return {
+            variant: props.variant,
+            objects: { count: props.objects.length, type, invert, position, rotation, scale }
+        };
+    }
+
     export function areEqual(cA: Clipping, cB: Clipping) {
-        if (cA.layers.length === 0 && cB.layers.length === 0) return true;
         if (cA.layers.length !== cB.layers.length) return false;
         for (let i = 0, il = cA.layers.length; i < il; ++i) {
             if (cA.layers[i].groups !== cB.layers[i].groups) return false;
             if (!Loci.areEqual(cA.layers[i].loci, cB.layers[i].loci)) return false;
         }
+        if (cA.variant !== cB.variant) return false;
+        if (cA.objects.count !== cB.objects.count) return false;
+
+        const oA = cA.objects, oB = cB.objects;
+        for (let i = 0, il = oA.count; i < il; ++i) {
+            if (oA.invert[i] !== oB.invert[i]) return false;
+            if (oA.type[i] !== oB.type[i]) return false;
+
+            Vec3.fromArray(vA, oA.position, i * 3);
+            Vec3.fromArray(vB, oB.position, i * 3);
+            if (!Vec3.equals(vA, vB)) return false;
+
+            Vec3.fromArray(vA, oA.scale, i * 3);
+            Vec3.fromArray(vB, oB.scale, i * 3);
+            if (!Vec3.equals(vA, vB)) return false;
+
+            Quat.fromArray(qA, oA.rotation, i * 4);
+            Quat.fromArray(qB, oB.rotation, i * 4);
+            if (!Quat.equals(qA, qB)) return false;
+        }
         return true;
     }
 
+    /** Check if layers empty */
     export function isEmpty(clipping: Clipping) {
         return clipping.layers.length === 0;
     }
 
-    export function remap(clipping: Clipping, structure: Structure) {
+    /** Remap layers */
+    export function remap(clipping: Clipping, structure: Structure): Clipping {
         const layers: Clipping.Layer[] = [];
         for (const layer of clipping.layers) {
             let { loci, groups } = layer;
@@ -118,9 +213,10 @@ namespace Clipping {
                 layers.push({ loci, groups });
             }
         }
-        return { layers };
+        return { layers, variant: clipping.variant, objects: clipping.objects };
     }
 
+    /** Merge layers */
     export function merge(clipping: Clipping): Clipping {
         if (isEmpty(clipping)) return clipping;
         const { structure } = clipping.layers[0].loci;
@@ -141,9 +237,10 @@ namespace Clipping {
         map.forEach((loci, groups) => {
             layers.push({ loci, groups });
         });
-        return { layers };
+        return { layers, variant: clipping.variant, objects: clipping.objects };
     }
 
+    /** Filter layers */
     export function filter(clipping: Clipping, filter: Structure): Clipping {
         if (isEmpty(clipping)) return clipping;
         const { structure } = clipping.layers[0].loci;
@@ -158,11 +255,11 @@ namespace Clipping {
                 layers.push({ loci, groups });
             }
         }
-        return { layers };
+        return { layers, variant: clipping.variant, objects: clipping.objects };
     }
 
     export type ScriptLayer = { script: Script, groups: Groups }
-    export function ofScript(scriptLayers: ScriptLayer[], structure: Structure): Clipping {
+    export function ofScript(scriptLayers: ScriptLayer[], structure: Structure, clip: Clip): Clipping {
         const layers: Clipping.Layer[] = [];
         for (let i = 0, il = scriptLayers.length; i < il; ++i) {
             const { script, groups } = scriptLayers[i];
@@ -171,18 +268,18 @@ namespace Clipping {
                 layers.push({ loci, groups });
             }
         }
-        return { layers };
+        return { layers, variant: clip.variant, objects: clip.objects };
     }
 
     export type BundleLayer = { bundle: StructureElement.Bundle, groups: Groups }
-    export function ofBundle(bundleLayers: BundleLayer[], structure: Structure): Clipping {
+    export function ofBundle(bundleLayers: BundleLayer[], structure: Structure, clip: Clip): Clipping {
         const layers: Clipping.Layer[] = [];
         for (let i = 0, il = bundleLayers.length; i < il; ++i) {
             const { bundle, groups } = bundleLayers[i];
             const loci = StructureElement.Bundle.toLoci(bundle, structure.root);
             layers.push({ loci, groups });
         }
-        return { layers };
+        return { layers, variant: clip.variant, objects: clip.objects };
     }
 
     export function toBundle(clipping: Clipping) {