ソースを参照

generic loci support for overpaint, substance, clipping

Alexander Rose 2 年 前
コミット
bdee4859f2

+ 1 - 0
CHANGELOG.md

@@ -23,6 +23,7 @@ Note that since we don't clearly distinguish between a public and private interf
     - Improve checks in in UnitsRepresentation setVisualState
 - Add StructureElement.Loci.forEachLocation
 - Add RepresentationRegistry.clear and ThemeRegistry.clear
+- Add generic Loci support for overpaint, substance, clipping themes
 
 ## [v3.28.0] - 2022-12-20
 

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

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2023 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>
@@ -61,5 +61,5 @@ async function eachRepr(plugin: PluginContext, components: StructureComponentRef
 function getFilteredBundle(layers: Clipping.BundleLayer[], structure: Structure) {
     const clipping = Clipping.ofBundle(layers, structure.root);
     const merged = Clipping.merge(clipping);
-    return Clipping.filter(merged, structure);
+    return Clipping.filter(merged, structure) as Clipping<StructureElement.Loci>;
 }

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

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2023 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>
@@ -72,5 +72,5 @@ async function eachRepr(plugin: PluginContext, components: StructureComponentRef
 function getFilteredBundle(layers: Overpaint.BundleLayer[], structure: Structure) {
     const overpaint = Overpaint.ofBundle(layers, structure.root);
     const merged = Overpaint.merge(overpaint);
-    return Overpaint.filter(merged, structure);
+    return Overpaint.filter(merged, structure) as Overpaint<StructureElement.Loci>;
 }

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

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2021-2023 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>
@@ -72,5 +72,5 @@ async function eachRepr(plugin: PluginContext, components: StructureComponentRef
 function getFilteredBundle(layers: Substance.BundleLayer[], structure: Structure) {
     const substance = Substance.ofBundle(layers, structure.root);
     const merged = Substance.merge(substance);
-    return Substance.filter(merged, structure);
+    return Substance.filter(merged, structure) as Substance<StructureElement.Loci>;
 }

+ 58 - 45
src/mol-theme/clipping.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -11,17 +11,18 @@ import { BitFlags } from '../mol-util/bit-flags';
 
 export { Clipping };
 
-type Clipping = {
-    readonly layers: ReadonlyArray<Clipping.Layer>
+type Clipping<T extends Loci = Loci> = {
+    readonly kind: T['kind']
+    readonly layers: ReadonlyArray<Clipping.Layer<T>>
 }
 
-function Clipping(layers: Clipping['layers']): Clipping {
-    return { layers };
+function Clipping<T extends Loci>(kind: T['kind'], layers: ReadonlyArray<Clipping.Layer<T>>): Clipping<T> {
+    return { kind, layers };
 }
 
 namespace Clipping {
-    export type Layer = { readonly loci: StructureElement.Loci, readonly groups: Groups }
-    export const Empty: Clipping = { layers: [] };
+    export type Layer<T extends Loci = Loci> = { readonly loci: T, readonly groups: Groups }
+    export const Empty: Clipping = { kind: 'empty-loci', layers: [] };
 
     export type Groups = BitFlags<Groups.Flag>
     export namespace Groups {
@@ -101,57 +102,69 @@ namespace Clipping {
 
     /** Remap layers */
     export function remap(clipping: Clipping, structure: Structure): Clipping {
-        const layers: Clipping.Layer[] = [];
-        for (const layer of clipping.layers) {
-            let { loci, groups } = layer;
-            loci = StructureElement.Loci.remap(loci, structure);
-            if (!StructureElement.Loci.isEmpty(loci)) {
-                layers.push({ loci, groups });
+        if (clipping.kind === 'element-loci') {
+            const layers: Clipping.Layer[] = [];
+            for (const layer of clipping.layers) {
+                let { loci, groups } = layer;
+                loci = StructureElement.Loci.remap(loci as StructureElement.Loci, structure);
+                if (!StructureElement.Loci.isEmpty(loci)) {
+                    layers.push({ loci, groups });
+                }
             }
+            return { kind: 'element-loci', layers };
+        } else {
+            return clipping;
         }
-        return { layers };
     }
 
     /** Merge layers */
     export function merge(clipping: Clipping): Clipping {
         if (isEmpty(clipping)) return clipping;
-        const { structure } = clipping.layers[0].loci;
-        const map = new Map<Groups, StructureElement.Loci>();
-        let shadowed = StructureElement.Loci.none(structure);
-        for (let i = 0, il = clipping.layers.length; i < il; ++i) {
-            let { loci, groups } = clipping.layers[il - i - 1]; // process from end
-            loci = StructureElement.Loci.subtract(loci, shadowed);
-            shadowed = StructureElement.Loci.union(loci, shadowed);
-            if (!StructureElement.Loci.isEmpty(loci)) {
-                if (map.has(groups)) {
-                    loci = StructureElement.Loci.union(loci, map.get(groups)!);
+        if (clipping.kind === 'element-loci') {
+            const { structure } = clipping.layers[0].loci as StructureElement.Loci;
+            const map = new Map<Groups, StructureElement.Loci>();
+            let shadowed = StructureElement.Loci.none(structure);
+            for (let i = 0, il = clipping.layers.length; i < il; ++i) {
+                let { loci, groups } = clipping.layers[il - i - 1]; // process from end
+                loci = StructureElement.Loci.subtract(loci as StructureElement.Loci, shadowed);
+                shadowed = StructureElement.Loci.union(loci, shadowed);
+                if (!StructureElement.Loci.isEmpty(loci)) {
+                    if (map.has(groups)) {
+                        loci = StructureElement.Loci.union(loci, map.get(groups)!);
+                    }
+                    map.set(groups, loci);
                 }
-                map.set(groups, loci);
             }
+            const layers: Clipping.Layer[] = [];
+            map.forEach((loci, groups) => {
+                layers.push({ loci, groups });
+            });
+            return { kind: 'element-loci', layers };
+        } else {
+            return clipping;
         }
-        const layers: Clipping.Layer[] = [];
-        map.forEach((loci, groups) => {
-            layers.push({ loci, groups });
-        });
-        return { layers };
     }
 
     /** Filter layers */
     export function filter(clipping: Clipping, filter: Structure): Clipping {
         if (isEmpty(clipping)) return clipping;
-        const { structure } = clipping.layers[0].loci;
-        const layers: Clipping.Layer[] = [];
-        for (const layer of clipping.layers) {
-            let { loci, groups } = layer;
-            // filter by first map to the `filter` structure and
-            // then map back to the original structure of the clipping loci
-            const filtered = StructureElement.Loci.remap(loci, filter);
-            loci = StructureElement.Loci.remap(filtered, structure);
-            if (!StructureElement.Loci.isEmpty(loci)) {
-                layers.push({ loci, groups });
+        if (clipping.kind === 'element-loci') {
+            const { structure } = clipping.layers[0].loci as StructureElement.Loci;
+            const layers: Clipping.Layer[] = [];
+            for (const layer of clipping.layers) {
+                let { loci, groups } = layer;
+                // filter by first map to the `filter` structure and
+                // then map back to the original structure of the clipping loci
+                const filtered = StructureElement.Loci.remap(loci as StructureElement.Loci, filter);
+                loci = StructureElement.Loci.remap(filtered, structure);
+                if (!StructureElement.Loci.isEmpty(loci)) {
+                    layers.push({ loci, groups });
+                }
             }
+            return { kind: 'element-loci', layers };
+        } else {
+            return clipping;
         }
-        return { layers };
     }
 
     export type ScriptLayer = { script: Script, groups: Groups }
@@ -164,7 +177,7 @@ namespace Clipping {
                 layers.push({ loci, groups });
             }
         }
-        return { layers };
+        return { kind: 'element-loci', layers };
     }
 
     export type BundleLayer = { bundle: StructureElement.Bundle, groups: Groups }
@@ -175,16 +188,16 @@ namespace Clipping {
             const loci = StructureElement.Bundle.toLoci(bundle, structure.root);
             layers.push({ loci, groups });
         }
-        return { layers };
+        return { kind: 'element-loci', layers };
     }
 
-    export function toBundle(clipping: Clipping) {
+    export function toBundle(clipping: Clipping<StructureElement.Loci>) {
         const layers: BundleLayer[] = [];
         for (let i = 0, il = clipping.layers.length; i < il; ++i) {
             const { loci, groups } = clipping.layers[i];
             const bundle = StructureElement.Bundle.fromLoci(loci);
             layers.push({ bundle, groups });
         }
-        return { layers };
+        return { kind: 'element-loci', layers };
     }
 }

+ 63 - 48
src/mol-theme/overpaint.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -11,15 +11,18 @@ import { Script } from '../mol-script/script';
 
 export { Overpaint };
 
-type Overpaint = { readonly layers: ReadonlyArray<Overpaint.Layer> }
+type Overpaint<T extends Loci = Loci> = {
+    readonly kind: T['kind']
+    readonly layers: ReadonlyArray<Overpaint.Layer<T>>
+}
 
-function Overpaint(layers: ReadonlyArray<Overpaint.Layer>): Overpaint {
-    return { layers };
+function Overpaint<T extends Loci>(kind: T['kind'], layers: ReadonlyArray<Overpaint.Layer<T>>): Overpaint<T> {
+    return { kind, layers };
 }
 
 namespace Overpaint {
-    export type Layer = { readonly loci: StructureElement.Loci, readonly color: Color, readonly clear: boolean }
-    export const Empty: Overpaint = { layers: [] };
+    export type Layer<T extends Loci = Loci> = { readonly loci: T, readonly color: Color, readonly clear: boolean }
+    export const Empty: Overpaint = { kind: 'empty-loci', layers: [] };
 
     export function areEqual(oA: Overpaint, oB: Overpaint) {
         if (oA.layers.length === 0 && oB.layers.length === 0) return true;
@@ -36,59 +39,71 @@ namespace Overpaint {
         return overpaint.layers.length === 0;
     }
 
-    export function remap(overpaint: Overpaint, structure: Structure) {
-        const layers: Overpaint.Layer[] = [];
-        for (const layer of overpaint.layers) {
-            let { loci, color, clear } = layer;
-            loci = StructureElement.Loci.remap(loci, structure);
-            if (!StructureElement.Loci.isEmpty(loci)) {
-                layers.push({ loci, color, clear });
+    export function remap(overpaint: Overpaint, structure: Structure): Overpaint {
+        if (overpaint.kind === 'element-loci') {
+            const layers: Overpaint.Layer[] = [];
+            for (const layer of overpaint.layers) {
+                let { loci, color, clear } = layer;
+                loci = StructureElement.Loci.remap(loci as StructureElement.Loci, structure);
+                if (!StructureElement.Loci.isEmpty(loci)) {
+                    layers.push({ loci, color, clear });
+                }
             }
+            return { kind: 'element-loci', layers };
+        } else {
+            return overpaint;
         }
-        return { layers };
     }
 
     export function merge(overpaint: Overpaint): Overpaint {
         if (isEmpty(overpaint)) return overpaint;
-        const { structure } = overpaint.layers[0].loci;
-        const map = new Map<Color | -1, StructureElement.Loci>();
-        let shadowed = StructureElement.Loci.none(structure);
-        for (let i = 0, il = overpaint.layers.length; i < il; ++i) {
-            let { loci, color, clear } = overpaint.layers[il - i - 1]; // process from end
-            loci = StructureElement.Loci.subtract(loci, shadowed);
-            shadowed = StructureElement.Loci.union(loci, shadowed);
-            if (!StructureElement.Loci.isEmpty(loci)) {
-                const colorOrClear = clear ? -1 : color;
-                if (map.has(colorOrClear)) {
-                    loci = StructureElement.Loci.union(loci, map.get(colorOrClear)!);
+        if (overpaint.kind === 'element-loci') {
+            const { structure } = overpaint.layers[0].loci as StructureElement.Loci;
+            const map = new Map<Color | -1, StructureElement.Loci>();
+            let shadowed = StructureElement.Loci.none(structure);
+            for (let i = 0, il = overpaint.layers.length; i < il; ++i) {
+                let { loci, color, clear } = overpaint.layers[il - i - 1]; // process from end
+                loci = StructureElement.Loci.subtract(loci as StructureElement.Loci, shadowed);
+                shadowed = StructureElement.Loci.union(loci, shadowed);
+                if (!StructureElement.Loci.isEmpty(loci)) {
+                    const colorOrClear = clear ? -1 : color;
+                    if (map.has(colorOrClear)) {
+                        loci = StructureElement.Loci.union(loci, map.get(colorOrClear)!);
+                    }
+                    map.set(colorOrClear, loci);
                 }
-                map.set(colorOrClear, loci);
             }
+            const layers: Overpaint.Layer[] = [];
+            map.forEach((loci, colorOrClear) => {
+                const clear = colorOrClear === -1;
+                const color = clear ? Color(0) : colorOrClear;
+                layers.push({ loci, color, clear });
+            });
+            return { kind: 'element-loci', layers };
+        } else {
+            return overpaint;
         }
-        const layers: Overpaint.Layer[] = [];
-        map.forEach((loci, colorOrClear) => {
-            const clear = colorOrClear === -1;
-            const color = clear ? Color(0) : colorOrClear;
-            layers.push({ loci, color, clear });
-        });
-        return { layers };
     }
 
     export function filter(overpaint: Overpaint, filter: Structure): Overpaint {
         if (isEmpty(overpaint)) return overpaint;
-        const { structure } = overpaint.layers[0].loci;
-        const layers: Overpaint.Layer[] = [];
-        for (const layer of overpaint.layers) {
-            let { loci, color, clear } = layer;
-            // filter by first map to the `filter` structure and
-            // then map back to the original structure of the overpaint loci
-            const filtered = StructureElement.Loci.remap(loci, filter);
-            loci = StructureElement.Loci.remap(filtered, structure);
-            if (!StructureElement.Loci.isEmpty(loci)) {
-                layers.push({ loci, color, clear });
+        if (overpaint.kind === 'element-loci') {
+            const { structure } = overpaint.layers[0].loci as StructureElement.Loci;
+            const layers: Overpaint.Layer[] = [];
+            for (const layer of overpaint.layers) {
+                let { loci, color, clear } = layer;
+                // filter by first map to the `filter` structure and
+                // then map back to the original structure of the overpaint loci
+                const filtered = StructureElement.Loci.remap(loci as StructureElement.Loci, filter);
+                loci = StructureElement.Loci.remap(filtered, structure);
+                if (!StructureElement.Loci.isEmpty(loci)) {
+                    layers.push({ loci, color, clear });
+                }
             }
+            return { kind: 'element-loci', layers };
+        } else {
+            return overpaint;
         }
-        return { layers };
     }
 
     export type ScriptLayer = { script: Script, color: Color, clear: boolean }
@@ -101,7 +116,7 @@ namespace Overpaint {
                 layers.push({ loci, color, clear });
             }
         }
-        return { layers };
+        return { kind: 'element-loci', layers };
     }
 
     export type BundleLayer = { bundle: StructureElement.Bundle, color: Color, clear: boolean }
@@ -112,16 +127,16 @@ namespace Overpaint {
             const loci = StructureElement.Bundle.toLoci(bundle, structure.root);
             layers.push({ loci, color, clear });
         }
-        return { layers };
+        return { kind: 'element-loci', layers };
     }
 
-    export function toBundle(overpaint: Overpaint) {
+    export function toBundle(overpaint: Overpaint<StructureElement.Loci>) {
         const layers: BundleLayer[] = [];
         for (let i = 0, il = overpaint.layers.length; i < il; ++i) {
             const { loci, color, clear } = overpaint.layers[i];
             const bundle = StructureElement.Bundle.fromLoci(loci);
             layers.push({ bundle, color, clear });
         }
-        return { layers };
+        return { kind: 'element-loci', layers };
     }
 }

+ 69 - 54
src/mol-theme/substance.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -12,15 +12,18 @@ import { shallowEqual } from '../mol-util/object';
 
 export { Substance };
 
-type Substance = { readonly layers: ReadonlyArray<Substance.Layer> }
+type Substance<T extends Loci = Loci> = {
+    readonly kind: T['kind']
+    readonly layers: ReadonlyArray<Substance.Layer<T>>
+}
 
-function Substance(layers: ReadonlyArray<Substance.Layer>): Substance {
-    return { layers };
+function Substance<T extends Loci>(kind: T['kind'], layers: ReadonlyArray<Substance.Layer<T>>): Substance {
+    return { kind, layers };
 }
 
 namespace Substance {
-    export type Layer = { readonly loci: StructureElement.Loci, readonly material: Material, readonly clear: boolean }
-    export const Empty: Substance = { layers: [] };
+    export type Layer<T extends Loci = Loci> = { readonly loci: T, readonly material: Material, readonly clear: boolean }
+    export const Empty: Substance = { kind: 'empty-loci', layers: [] };
 
     export function areEqual(sA: Substance, sB: Substance) {
         if (sA.layers.length === 0 && sB.layers.length === 0) return true;
@@ -37,66 +40,78 @@ namespace Substance {
         return overpaint.layers.length === 0;
     }
 
-    export function remap(substance: Substance, structure: Structure) {
-        const layers: Substance.Layer[] = [];
-        for (const layer of substance.layers) {
-            let { loci, material, clear } = layer;
-            loci = StructureElement.Loci.remap(loci, structure);
-            if (!StructureElement.Loci.isEmpty(loci)) {
-                layers.push({ loci, material, clear });
+    export function remap(substance: Substance, structure: Structure): Substance {
+        if (substance.kind === 'element-loci') {
+            const layers: Substance.Layer[] = [];
+            for (const layer of substance.layers) {
+                let { loci, material, clear } = layer;
+                loci = StructureElement.Loci.remap(loci as StructureElement.Loci, structure);
+                if (!StructureElement.Loci.isEmpty(loci)) {
+                    layers.push({ loci, material, clear });
+                }
             }
+            return { kind: 'element-loci', layers };
+        } else {
+            return substance;
         }
-        return { layers };
     }
 
     export function merge(substance: Substance): Substance {
         if (isEmpty(substance)) return substance;
-        const { structure } = substance.layers[0].loci;
-        let clearLoci: StructureElement.Loci | undefined = void 0;
-        const map = new Map<Material, StructureElement.Loci>();
-        let shadowed = StructureElement.Loci.none(structure);
-        for (let i = 0, il = substance.layers.length; i < il; ++i) {
-            let { loci, material, clear } = substance.layers[il - i - 1]; // process from end
-            loci = StructureElement.Loci.subtract(loci, shadowed);
-            shadowed = StructureElement.Loci.union(loci, shadowed);
-            if (!StructureElement.Loci.isEmpty(loci)) {
-                if (clear) {
-                    clearLoci = clearLoci
-                        ? StructureElement.Loci.union(loci, clearLoci)
-                        : loci;
-                } else {
-                    if (map.has(material)) {
-                        loci = StructureElement.Loci.union(loci, map.get(material)!);
+        if (substance.kind === 'element-loci') {
+            const { structure } = substance.layers[0].loci as StructureElement.Loci;
+            let clearLoci: StructureElement.Loci | undefined = void 0;
+            const map = new Map<Material, StructureElement.Loci>();
+            let shadowed = StructureElement.Loci.none(structure);
+            for (let i = 0, il = substance.layers.length; i < il; ++i) {
+                let { loci, material, clear } = substance.layers[il - i - 1]; // process from end
+                loci = StructureElement.Loci.subtract(loci as StructureElement.Loci, shadowed);
+                shadowed = StructureElement.Loci.union(loci, shadowed);
+                if (!StructureElement.Loci.isEmpty(loci)) {
+                    if (clear) {
+                        clearLoci = clearLoci
+                            ? StructureElement.Loci.union(loci, clearLoci)
+                            : loci;
+                    } else {
+                        if (map.has(material)) {
+                            loci = StructureElement.Loci.union(loci, map.get(material)!);
+                        }
+                        map.set(material, loci);
                     }
-                    map.set(material, loci);
                 }
             }
+            const layers: Substance.Layer[] = [];
+            if (clearLoci) {
+                layers.push({ loci: clearLoci, material: Material(), clear: true });
+            }
+            map.forEach((loci, material) => {
+                layers.push({ loci, material, clear: false });
+            });
+            return { kind: 'element-loci', layers };
+        } else {
+            return substance;
         }
-        const layers: Substance.Layer[] = [];
-        if (clearLoci) {
-            layers.push({ loci: clearLoci, material: Material(), clear: true });
-        }
-        map.forEach((loci, material) => {
-            layers.push({ loci, material, clear: false });
-        });
-        return { layers };
     }
 
     export function filter(substance: Substance, filter: Structure): Substance {
         if (isEmpty(substance)) return substance;
-        const { structure } = substance.layers[0].loci;
-        const layers: Substance.Layer[] = [];
-        for (const layer of substance.layers) {
-            let { loci, material, clear } = layer;
-            // filter by first map to the `filter` structure and
-            // then map back to the original structure of the substance loci
-            const filtered = StructureElement.Loci.remap(loci, filter);
-            loci = StructureElement.Loci.remap(filtered, structure);
-            if (!StructureElement.Loci.isEmpty(loci)) {
-                layers.push({ loci, material, clear });
+        if (substance.kind === 'element-loci') {
+            const { structure } = substance.layers[0].loci as StructureElement.Loci;
+            const layers: Substance.Layer[] = [];
+            for (const layer of substance.layers) {
+                let { loci, material, clear } = layer;
+                // filter by first map to the `filter` structure and
+                // then map back to the original structure of the substance loci
+                const filtered = StructureElement.Loci.remap(loci as StructureElement.Loci, filter);
+                loci = StructureElement.Loci.remap(filtered, structure);
+                if (!StructureElement.Loci.isEmpty(loci)) {
+                    layers.push({ loci, material, clear });
+                }
             }
+            return { kind: 'element-loci', layers };
+        } else {
+            return substance;
         }
-        return { layers };
     }
 
     export type ScriptLayer = { script: Script, material: Material, clear: boolean }
@@ -109,7 +124,7 @@ namespace Substance {
                 layers.push({ loci, material, clear });
             }
         }
-        return { layers };
+        return { kind: 'element-loci', layers };
     }
 
     export type BundleLayer = { bundle: StructureElement.Bundle, material: Material, clear: boolean }
@@ -120,16 +135,16 @@ namespace Substance {
             const loci = StructureElement.Bundle.toLoci(bundle, structure.root);
             layers.push({ loci, material, clear });
         }
-        return { layers };
+        return { kind: 'element-loci', layers };
     }
 
-    export function toBundle(overpaint: Substance) {
+    export function toBundle(overpaint: Substance<StructureElement.Loci>) {
         const layers: BundleLayer[] = [];
         for (let i = 0, il = overpaint.layers.length; i < il; ++i) {
             const { loci, material, clear } = overpaint.layers[i];
             const bundle = StructureElement.Bundle.fromLoci(loci);
             layers.push({ bundle, material, clear });
         }
-        return { layers };
+        return { kind: 'element-loci', layers };
     }
 }