Browse Source

wip, polymerElements, gapElements as unit props

Alexander Rose 6 years ago
parent
commit
e4565da6bb

+ 2 - 2
src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts

@@ -9,7 +9,7 @@ import { UnitsVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
 import { Mesh } from '../../../mesh/mesh';
 import { MeshBuilder } from '../../../mesh/mesh-builder';
-import { getPolymerElementCount, PolymerBackboneIterator } from './util/polymer';
+import { PolymerBackboneIterator } from './util/polymer';
 import { getElementLoci, markElement, StructureElementIterator } from './util/element';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { DefaultUnitsMeshProps, UnitsMeshVisual } from '../units-visual';
@@ -24,7 +24,7 @@ export interface PolymerBackboneCylinderProps {
 }
 
 async function createPolymerBackboneCylinderMesh(ctx: RuntimeContext, unit: Unit, props: PolymerBackboneCylinderProps, mesh?: Mesh) {
-    const polymerElementCount = getPolymerElementCount(unit)
+    const polymerElementCount = unit.polymerElements.length
     if (!polymerElementCount) return Mesh.createEmpty(mesh)
 
     const sizeTheme = SizeTheme(props.sizeTheme)

+ 4 - 5
src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts

@@ -7,10 +7,9 @@
 import { Unit } from 'mol-model/structure';
 import { UnitsVisual } from '..';
 import { RuntimeContext } from 'mol-task'
-import { markElement, getElementLoci } from './util/element';
 import { Mesh } from '../../../mesh/mesh';
 import { MeshBuilder } from '../../../mesh/mesh-builder';
-import { getPolymerElementCount, PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator } from './util/polymer';
+import { PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator, getPolymerElementLoci, markPolymerElement } from './util/polymer';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import { SecondaryStructureType, MoleculeType } from 'mol-model/structure/model/types';
 import { DefaultUnitsMeshProps, UnitsMeshVisual } from '../units-visual';
@@ -34,7 +33,7 @@ export interface PolymerDirectionWedgeProps {
 }
 
 async function createPolymerDirectionWedgeMesh(ctx: RuntimeContext, unit: Unit, props: PolymerDirectionWedgeProps, mesh?: Mesh) {
-    const polymerElementCount = getPolymerElementCount(unit)
+    const polymerElementCount = unit.polymerElements.length
     if (!polymerElementCount) return Mesh.createEmpty(mesh)
 
     const sizeTheme = SizeTheme(props.sizeTheme)
@@ -95,8 +94,8 @@ export function PolymerDirectionVisual(): UnitsVisual<PolymerDirectionProps> {
         defaultProps: DefaultPolymerDirectionProps,
         createMesh: createPolymerDirectionWedgeMesh,
         createLocationIterator: PolymerLocationIterator.fromGroup,
-        getLoci: getElementLoci,
-        mark: markElement,
+        getLoci: getPolymerElementLoci,
+        mark: markPolymerElement,
         setUpdateState: () => {}
     })
 }

+ 2 - 2
src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts

@@ -9,7 +9,7 @@ import { UnitsVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
 import { Mesh } from '../../../mesh/mesh';
 import { MeshBuilder } from '../../../mesh/mesh-builder';
-import { getPolymerGapCount, PolymerGapIterator, PolymerGapLocationIterator } from './util/polymer';
+import { PolymerGapIterator, PolymerGapLocationIterator } from './util/polymer';
 import { getElementLoci, markElement } from './util/element';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { UnitsMeshVisual, DefaultUnitsMeshProps } from '../units-visual';
@@ -26,7 +26,7 @@ export interface PolymerGapCylinderProps {
 }
 
 async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, props: PolymerGapCylinderProps, mesh?: Mesh) {
-    const polymerGapCount = getPolymerGapCount(unit)
+    const polymerGapCount = unit.gapElements.length
     if (!polymerGapCount) return Mesh.createEmpty(mesh)
 
     const sizeTheme = SizeTheme(props.sizeTheme)

+ 4 - 5
src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts

@@ -7,10 +7,9 @@
 import { Unit } from 'mol-model/structure';
 import { UnitsVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
-import { markElement, getElementLoci } from './util/element';
 import { Mesh } from '../../../mesh/mesh';
 import { MeshBuilder } from '../../../mesh/mesh-builder';
-import { getPolymerElementCount, PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator } from './util/polymer';
+import { PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator, getPolymerElementLoci, markPolymerElement } from './util/polymer';
 import { SecondaryStructureType, isNucleic } from 'mol-model/structure/model/types';
 import { UnitsMeshVisual, DefaultUnitsMeshProps } from '../units-visual';
 import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
@@ -28,7 +27,7 @@ export interface PolymerTraceMeshProps {
 // TODO handle polymer ends properly
 
 async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, props: PolymerTraceMeshProps, mesh?: Mesh) {
-    const polymerElementCount = getPolymerElementCount(unit)
+    const polymerElementCount = unit.polymerElements.length
     if (!polymerElementCount) return Mesh.createEmpty(mesh)
 
     const sizeTheme = SizeTheme(props.sizeTheme)
@@ -95,8 +94,8 @@ export function PolymerTraceVisual(): UnitsVisual<PolymerTraceProps> {
         defaultProps: DefaultPolymerTraceProps,
         createMesh: createPolymerTraceMesh,
         createLocationIterator: PolymerLocationIterator.fromGroup,
-        getLoci: getElementLoci,
-        mark: markElement,
+        getLoci: getPolymerElementLoci,
+        mark: markPolymerElement,
         setUpdateState: (state: MeshUpdateState, newProps: PolymerTraceProps, currentProps: PolymerTraceProps) => {
             state.createMesh = (
                 newProps.linearSegments !== currentProps.linearSegments ||

+ 44 - 94
src/mol-geo/representation/structure/visual/util/polymer.ts

@@ -5,11 +5,11 @@
  */
 
 import { Unit, ElementIndex, StructureElement } from 'mol-model/structure';
-import { Segmentation, OrderedSet, Interval } from 'mol-data/int';
 import SortedRanges from 'mol-data/int/sorted-ranges';
 import { LocationIterator } from '../../../../util/location-iterator';
-import { getElementIndexForAtomRole } from 'mol-model/structure/util';
-import { PolymerGapIterator } from './polymer/gap-iterator';
+import { PickingId } from '../../../../util/picking';
+import { OrderedSet, Interval } from 'mol-data/int';
+import { EmptyLoci, Loci } from 'mol-model/loci';
 
 export * from './polymer/backbone-iterator'
 export * from './polymer/gap-iterator'
@@ -32,119 +32,69 @@ export function getGapRanges(unit: Unit): SortedRanges<ElementIndex> {
     }
 }
 
-// polymer element
-
-export function getPolymerElementCount(unit: Unit) {
-    let count = 0
-    const { elements } = unit
-    const polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), elements)
-    switch (unit.kind) {
-        case Unit.Kind.Atomic:
-            const residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, elements)
-            while (polymerIt.hasNext) {
-                const polymerSegment = polymerIt.move()
-                residueIt.setSegment(polymerSegment)
-                while (residueIt.hasNext) {
-                    const residueSegment = residueIt.move()
-                    const { start, end } = residueSegment
-                    if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) ++count
-                }
-            }
-            break
-        case Unit.Kind.Spheres:
-        case Unit.Kind.Gaussians:
-            while (polymerIt.hasNext) {
-                const { start, end } = polymerIt.move()
-                count += OrderedSet.intersectionSize(Interval.ofBounds(elements[start], elements[end - 1]), elements)
-            }
-            break
-    }
-    return count
-}
-
-export function getPolymerElementIndices(unit: Unit) {
-    const indices: ElementIndex[] = []
-    const { elements, model } = unit
-    const { residueAtomSegments } = unit.model.atomicHierarchy
-    const polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), elements)
-    switch (unit.kind) {
-        case Unit.Kind.Atomic:
-            const residueIt = Segmentation.transientSegments(residueAtomSegments, elements)
-            while (polymerIt.hasNext) {
-                const polymerSegment = polymerIt.move()
-                residueIt.setSegment(polymerSegment)
-                while (residueIt.hasNext) {
-                    const residueSegment = residueIt.move()
-                    const { start, end, index } = residueSegment
-                    if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) {
-                        const elementIndex = getElementIndexForAtomRole(model, index, 'trace')
-                        indices.push(elementIndex === -1 ? residueAtomSegments.offsets[index] : elementIndex)
-                    }
-                }
-            }
-            break
-        case Unit.Kind.Spheres:
-        case Unit.Kind.Gaussians:
-            while (polymerIt.hasNext) {
-                const { start, end } = polymerIt.move()
-                for (let i = start; i < end; ++i) { indices.push(elements[i]) }
-            }
-            break
-    }
-    return indices
-}
-
 export namespace PolymerLocationIterator {
     export function fromGroup(group: Unit.SymmetryGroup): LocationIterator {
-        const polymerElementIndices = getPolymerElementIndices(group.units[0])
-        const groupCount = polymerElementIndices.length
+        const polymerElements = group.units[0].polymerElements
+        const groupCount = polymerElements.length
         const instanceCount = group.units.length
         const location = StructureElement.create()
         const getLocation = (groupIndex: number, instanceIndex: number) => {
             const unit = group.units[instanceIndex]
             location.unit = unit
-            location.element = polymerElementIndices[groupIndex]
+            location.element = polymerElements[groupIndex]
             return location
         }
         return LocationIterator(groupCount, instanceCount, getLocation)
     }
 }
 
-// polymer gap
-
-export function getPolymerGapCount(unit: Unit) {
-    let count = 0
-    const { elements } = unit
-    const gapIt = SortedRanges.transientSegments(getGapRanges(unit), elements)
-    while (gapIt.hasNext) {
-        const { start, end } = gapIt.move()
-        if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) ++count
-    }
-    return count
-}
-
-export function getPolymerGapElementIndices(unit: Unit) {
-    const indices: ElementIndex[] = []
-    const polymerGapIt = PolymerGapIterator(unit)
-    while (polymerGapIt.hasNext) {
-        const { centerA, centerB } = polymerGapIt.move()
-        indices.push(centerA.element, centerB.element)
-    }
-    return indices
-}
-
 export namespace PolymerGapLocationIterator {
     export function fromGroup(group: Unit.SymmetryGroup): LocationIterator {
-        const polymerGapElementIndices = getPolymerGapElementIndices(group.units[0])
-        const groupCount = polymerGapElementIndices.length
+        const gapElements = group.units[0].gapElements
+        const groupCount = gapElements.length
         const instanceCount = group.units.length
         const location = StructureElement.create()
         const getLocation = (groupIndex: number, instanceIndex: number) => {
             const unit = group.units[instanceIndex]
             location.unit = unit
-            location.element = polymerGapElementIndices[groupIndex]
+            location.element = gapElements[groupIndex]
             return location
         }
         return LocationIterator(groupCount, instanceCount, getLocation)
     }
+}
+
+export function getPolymerElementLoci(pickingId: PickingId, group: Unit.SymmetryGroup, id: number) {
+    const { objectId, instanceId, groupId } = pickingId
+    if (id === objectId) {
+        const unit = group.units[instanceId]
+        const unitIndex = OrderedSet.findPredecessorIndex(unit.elements, unit.polymerElements[groupId]) as StructureElement.UnitIndex
+        const indices = OrderedSet.ofSingleton(unitIndex);
+        return StructureElement.Loci([{ unit, indices }])
+    }
+    return EmptyLoci
+}
+
+export function markPolymerElement(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Interval) => boolean) {
+    const groupCount = group.units[0].polymerElements.length
+
+    let changed = false
+    if (StructureElement.isLoci(loci)) {
+        for (const e of loci.elements) {
+            const unitIdx = group.unitIndexMap.get(e.unit.id)
+            if (unitIdx !== undefined) {
+                if (Interval.is(e.indices)) {
+                    const start = unitIdx * groupCount + OrderedSet.findPredecessorIndex(e.unit.polymerElements, e.unit.elements[Interval.start(e.indices)])
+                    const end = unitIdx * groupCount + OrderedSet.findPredecessorIndex(e.unit.polymerElements, e.unit.elements[Interval.end(e.indices)])
+                    if (apply(Interval.ofBounds(start, end))) changed = true
+                } else {
+                    for (let i = 0, _i = e.indices.length; i < _i; i++) {
+                        const idx = unitIdx * groupCount + OrderedSet.findPredecessorIndex(e.unit.polymerElements, e.unit.elements[e.indices[i]])
+                        if (apply(Interval.ofSingleton(idx))) changed = true
+                    }
+                }
+            }
+        }
+    }
+    return changed
 }

+ 65 - 14
src/mol-model/structure/structure/unit.ts

@@ -13,9 +13,10 @@ import { CoarseElements, CoarseSphereConformation, CoarseGaussianConformation }
 import { ValueRef } from 'mol-util';
 import { UnitRings } from './unit/rings';
 import StructureElement from './element'
-import { ChainIndex, ResidueIndex } from '../model/indexing';
+import { ChainIndex, ResidueIndex, ElementIndex } from '../model/indexing';
 import { IntMap, SortedArray } from 'mol-data/int';
 import { hash2 } from 'mol-data/util';
+import { getAtomicPolymerElements, getCoarsePolymerElements, getAtomicGapElements, getCoarseGapElements } from './util/polymer';
 
 // A building block of a structure that corresponds to an atomic or a coarse grained representation
 // 'conveniently grouped together'.
@@ -32,8 +33,8 @@ namespace Unit {
     export function create(id: number, invariantId: number, kind: Kind, model: Model, operator: SymmetryOperator, elements: StructureElement.Set): Unit {
         switch (kind) {
             case Kind.Atomic: return new Atomic(id, invariantId, model, elements, SymmetryOperator.createMapping(operator, model.atomicConformation, void 0), AtomicProperties());
-            case Kind.Spheres: return createCoarse(id, invariantId, model, Kind.Spheres, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.spheres, getSphereRadiusFunc(model)));
-            case Kind.Gaussians: return createCoarse(id, invariantId, model, Kind.Gaussians, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.gaussians, getGaussianRadiusFunc(model)));
+            case Kind.Spheres: return createCoarse(id, invariantId, model, Kind.Spheres, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.spheres, getSphereRadiusFunc(model)), CoarseProperties());
+            case Kind.Gaussians: return createCoarse(id, invariantId, model, Kind.Gaussians, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.gaussians, getGaussianRadiusFunc(model)), CoarseProperties());
         }
     }
 
@@ -91,6 +92,8 @@ namespace Unit {
         applyOperator(id: number, operator: SymmetryOperator, dontCompose?: boolean /* = false */): Unit,
 
         readonly lookup3d: Lookup3D
+        readonly polymerElements: SortedArray<ElementIndex>
+        readonly gapElements: SortedArray<ElementIndex>
     }
 
     function getSphereRadiusFunc(model: Model) {
@@ -154,6 +157,18 @@ namespace Unit {
             return this.props.rings.ref;
         }
 
+        get polymerElements() {
+            if (this.props.polymerElements.ref) return this.props.polymerElements.ref;
+            this.props.polymerElements.ref = getAtomicPolymerElements(this);
+            return this.props.polymerElements.ref;
+        }
+
+        get gapElements() {
+            if (this.props.gapElements.ref) return this.props.gapElements.ref;
+            this.props.gapElements.ref = getAtomicGapElements(this);
+            return this.props.gapElements.ref;
+        }
+
         getResidueIndex(elementIndex: StructureElement.UnitIndex) {
             return this.model.atomicHierarchy.residueAtomSegments.index[this.elements[elementIndex]];
         }
@@ -175,10 +190,18 @@ namespace Unit {
         lookup3d: ValueRef<Lookup3D | undefined>,
         links: ValueRef<IntraUnitLinks | undefined>,
         rings: ValueRef<UnitRings | undefined>
+        polymerElements: ValueRef<SortedArray<ElementIndex> | undefined>
+        gapElements: ValueRef<SortedArray<ElementIndex> | undefined>
     }
 
     function AtomicProperties(): AtomicProperties {
-        return { lookup3d: ValueRef.create(void 0), links: ValueRef.create(void 0), rings: ValueRef.create(void 0) };
+        return {
+            lookup3d: ValueRef.create(void 0),
+            links: ValueRef.create(void 0),
+            rings: ValueRef.create(void 0),
+            polymerElements: ValueRef.create(void 0),
+            gapElements: ValueRef.create(void 0),
+        };
     }
 
     class Coarse<K extends Kind.Gaussians | Kind.Spheres, C extends CoarseSphereConformation | CoarseGaussianConformation> implements Base {
@@ -193,32 +216,45 @@ namespace Unit {
         readonly coarseElements: CoarseElements;
         readonly coarseConformation: C;
 
+        private props: CoarseProperties;
+
         getChild(elements: StructureElement.Set): Unit {
             if (elements.length === this.elements.length) return this as any as Unit /** lets call this an ugly temporary hack */;
-            return createCoarse(this.id, this.invariantId, this.model, this.kind, elements, this.conformation);
+            return createCoarse(this.id, this.invariantId, this.model, this.kind, elements, this.conformation, CoarseProperties());
         }
 
         applyOperator(id: number, operator: SymmetryOperator, dontCompose = false): Unit {
             const op = dontCompose ? operator : SymmetryOperator.compose(this.conformation.operator, operator);
-            const ret = createCoarse(id, this.invariantId, this.model, this.kind, this.elements, SymmetryOperator.createMapping(op, this.getCoarseElements(), this.conformation.r));
-            (ret as Coarse<K, C>)._lookup3d = this._lookup3d;
+            const ret = createCoarse(id, this.invariantId, this.model, this.kind, this.elements, SymmetryOperator.createMapping(op, this.getCoarseElements(), this.conformation.r), this.props);
+            // (ret as Coarse<K, C>)._lookup3d = this._lookup3d;
             return ret;
         }
 
-        private _lookup3d: ValueRef<Lookup3D | undefined> = ValueRef.create(void 0);
         get lookup3d() {
-            if (this._lookup3d.ref) return this._lookup3d.ref;
+            if (this.props.lookup3d.ref) return this.props.lookup3d.ref;
             // TODO: support sphere radius?
             const { x, y, z } = this.getCoarseElements();
-            this._lookup3d.ref = GridLookup3D({ x, y, z, indices: this.elements });
-            return this._lookup3d.ref;
+            this.props.lookup3d.ref = GridLookup3D({ x, y, z, indices: this.elements });
+            return this.props.lookup3d.ref;
+        }
+
+        get polymerElements() {
+            if (this.props.polymerElements.ref) return this.props.polymerElements.ref;
+            this.props.polymerElements.ref = getCoarsePolymerElements(this as Unit.Spheres | Unit.Gaussians); // TODO
+            return this.props.polymerElements.ref;
+        }
+
+        get gapElements() {
+            if (this.props.gapElements.ref) return this.props.gapElements.ref;
+            this.props.gapElements.ref = getCoarseGapElements(this as Unit.Spheres | Unit.Gaussians); // TODO
+            return this.props.gapElements.ref;
         }
 
         private getCoarseElements() {
             return this.kind === Kind.Spheres ? this.model.coarseConformation.spheres : this.model.coarseConformation.gaussians;
         }
 
-        constructor(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping) {
+        constructor(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping, props: CoarseProperties) {
             this.kind = kind;
             this.id = id;
             this.invariantId = invariantId;
@@ -227,11 +263,26 @@ namespace Unit {
             this.conformation = conformation;
             this.coarseElements = kind === Kind.Spheres ? model.coarseHierarchy.spheres : model.coarseHierarchy.gaussians;
             this.coarseConformation = (kind === Kind.Spheres ? model.coarseConformation.spheres : model.coarseConformation.gaussians) as C;
+            this.props = props;
         }
     }
 
-    function createCoarse<K extends Kind.Gaussians | Kind.Spheres>(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping): Unit {
-        return new Coarse(id, invariantId, model, kind, elements, conformation) as any as Unit /** lets call this an ugly temporary hack */;
+    interface CoarseProperties {
+        lookup3d: ValueRef<Lookup3D | undefined>,
+        polymerElements: ValueRef<SortedArray<ElementIndex> | undefined>
+        gapElements: ValueRef<SortedArray<ElementIndex> | undefined>
+    }
+
+    function CoarseProperties(): CoarseProperties {
+        return {
+            lookup3d: ValueRef.create(void 0),
+            polymerElements: ValueRef.create(void 0),
+            gapElements: ValueRef.create(void 0),
+        };
+    }
+
+    function createCoarse<K extends Kind.Gaussians | Kind.Spheres>(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping, props: CoarseProperties): Unit {
+        return new Coarse(id, invariantId, model, kind, elements, conformation, props) as any as Unit /** lets call this an ugly temporary hack */;
     }
 
     export class Spheres extends Coarse<Kind.Spheres, CoarseSphereConformation> { }

+ 75 - 0
src/mol-model/structure/structure/util/polymer.ts

@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, ElementIndex } from 'mol-model/structure';
+import { Segmentation, OrderedSet, Interval, SortedArray } from 'mol-data/int';
+import SortedRanges from 'mol-data/int/sorted-ranges';
+import { getElementIndexForAtomRole } from 'mol-model/structure/util';
+
+export function getAtomicPolymerElements(unit: Unit.Atomic) {
+    const indices: ElementIndex[] = []
+    const { elements, model } = unit
+    const { residueAtomSegments } = unit.model.atomicHierarchy
+    const polymerIt = SortedRanges.transientSegments(unit.model.atomicHierarchy.polymerRanges, elements)
+    const residueIt = Segmentation.transientSegments(residueAtomSegments, elements)
+    while (polymerIt.hasNext) {
+        const polymerSegment = polymerIt.move()
+        residueIt.setSegment(polymerSegment)
+        while (residueIt.hasNext) {
+            const residueSegment = residueIt.move()
+            const { start, end, index } = residueSegment
+            if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) {
+                const elementIndex = getElementIndexForAtomRole(model, index, 'trace')
+                indices.push(elementIndex === -1 ? residueAtomSegments.offsets[index] : elementIndex)
+            }
+        }
+    }
+    return SortedArray.ofSortedArray<ElementIndex>(indices)
+}
+
+export function getCoarsePolymerElements(unit: Unit.Spheres | Unit.Gaussians) {
+    const indices: ElementIndex[] = []
+    const { elements, model } = unit
+    const { spheres, gaussians } = model.coarseHierarchy
+    const polymerRanges = Unit.isSpheres(unit) ? spheres.polymerRanges : gaussians.polymerRanges
+    const polymerIt = SortedRanges.transientSegments(polymerRanges, elements)
+    while (polymerIt.hasNext) {
+        const { start, end } = polymerIt.move()
+        for (let i = start; i < end; ++i) { indices.push(elements[i]) }
+    }
+    return SortedArray.ofSortedArray<ElementIndex>(indices)
+}
+
+export function getAtomicGapElements(unit: Unit.Atomic) {
+    const indices: ElementIndex[] = []
+    const { elements, model, residueIndex } = unit
+    const { residueAtomSegments } = unit.model.atomicHierarchy
+    const gapIt = SortedRanges.transientSegments(unit.model.atomicHierarchy.gapRanges, unit.elements);
+    while (gapIt.hasNext) {
+        const gapSegment = gapIt.move();
+        const indexStart = residueIndex[elements[gapSegment.start]]
+        const indexEnd = residueIndex[elements[gapSegment.end - 1]]
+        const elementIndexStart = getElementIndexForAtomRole(model, indexStart, 'trace')
+        const elementIndexEnd = getElementIndexForAtomRole(model, indexEnd, 'trace')
+        indices.push(elementIndexStart === -1 ? residueAtomSegments.offsets[indexStart] : elementIndexStart)
+        indices.push(elementIndexEnd === -1 ? residueAtomSegments.offsets[indexEnd] : elementIndexEnd)
+
+    }
+    return SortedArray.ofSortedArray<ElementIndex>(indices)
+}
+
+export function getCoarseGapElements(unit: Unit.Spheres | Unit.Gaussians) {
+    const indices: ElementIndex[] = []
+    const { elements, model } = unit
+    const { spheres, gaussians } = model.coarseHierarchy
+    const gapRanges = Unit.isSpheres(unit) ? spheres.gapRanges : gaussians.gapRanges
+    const gapIt = SortedRanges.transientSegments(gapRanges, elements)
+    while (gapIt.hasNext) {
+        const { start, end } = gapIt.move()
+        indices.push(elements[start], elements[end - 1])
+    }
+    return SortedArray.ofSortedArray<ElementIndex>(indices)
+}