Browse Source

basic support for bond stubs

- line and ball & stick repr
- stubs support in link visual helper
- getData and mustRecreate methods for structure repr provider
- Structure.WithChild helper (needs Proxy support)
Alexander Rose 4 years ago
parent
commit
3180d7c305
26 changed files with 353 additions and 193 deletions
  1. 2 1
      src/extensions/alpha-orbitals/transforms.ts
  2. 2 1
      src/extensions/anvil/behavior.ts
  3. 2 1
      src/extensions/rcsb/assembly-symmetry/behavior.ts
  4. 29 1
      src/mol-model/structure/structure/structure.ts
  5. 1 1
      src/mol-plugin-state/helpers/structure-clipping.ts
  6. 1 1
      src/mol-plugin-state/helpers/structure-overpaint.ts
  7. 1 1
      src/mol-plugin-state/helpers/structure-transparency.ts
  8. 6 6
      src/mol-plugin-state/objects.ts
  9. 72 67
      src/mol-plugin-state/transforms/representation.ts
  10. 8 8
      src/mol-plugin-ui/structure/measurements.tsx
  11. 2 1
      src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts
  12. 6 6
      src/mol-plugin/behavior/static/representation.ts
  13. 4 5
      src/mol-repr/representation.ts
  14. 3 1
      src/mol-repr/structure/complex-representation.ts
  15. 8 1
      src/mol-repr/structure/representation/ball-and-stick.ts
  16. 1 0
      src/mol-repr/structure/representation/ellipsoid.ts
  17. 7 1
      src/mol-repr/structure/representation/line.ts
  18. 3 1
      src/mol-repr/structure/units-representation.ts
  19. 34 4
      src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts
  20. 4 2
      src/mol-repr/structure/visual/bond-inter-unit-line.ts
  21. 35 7
      src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts
  22. 13 3
      src/mol-repr/structure/visual/bond-intra-unit-line.ts
  23. 6 3
      src/mol-repr/structure/visual/element-point.ts
  24. 56 44
      src/mol-repr/structure/visual/util/bond.ts
  25. 33 16
      src/mol-repr/structure/visual/util/element.ts
  26. 14 10
      src/mol-repr/structure/visual/util/link.ts

+ 2 - 1
src/extensions/alpha-orbitals/transforms.ts

@@ -190,7 +190,7 @@ export const CreateOrbitalRepresentation3D = PluginStateTransform.BuiltIn({
             repr.setTheme(Theme.create(plugin.representation.volume.themes, { volume: a.data }, params));
             await repr.createOrUpdate(props, a.data).runInContext(ctx);
             repr.setState({ pickable: srcParams.pickable });
-            return new PluginStateObject.Volume.Representation3D({ repr, source: a }, { label: provider.label, description: VolumeRepresentation3DHelpers.getDescription(props) });
+            return new PluginStateObject.Volume.Representation3D({ repr, sourceData: a.data }, { label: provider.label, description: VolumeRepresentation3DHelpers.getDescription(props) });
         });
     },
     update({ a, b, newParams: srcParams }, plugin: PluginContext) {
@@ -200,6 +200,7 @@ export const CreateOrbitalRepresentation3D = PluginStateTransform.BuiltIn({
             const props = { ...b.data.repr.props, ...newParams.type.params };
             b.data.repr.setTheme(Theme.create(plugin.representation.volume.themes, { volume: a.data }, newParams));
             await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
+            b.data.sourceData = a.data;
             b.data.repr.setState({ pickable: srcParams.pickable });
             b.description = VolumeRepresentation3DHelpers.getDescription(props);
             return StateTransformer.UpdateResult.Updated;

+ 2 - 1
src/extensions/anvil/behavior.ts

@@ -121,7 +121,7 @@ const MembraneOrientation3D = PluginStateTransform.BuiltIn({
             await MembraneOrientationProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
             const repr = MembraneOrientationRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => MembraneOrientationParams);
             await repr.createOrUpdate(params, a.data).runInContext(ctx);
-            return new PluginStateObject.Shape.Representation3D({ repr, source: a }, { label: 'Membrane Orientation' });
+            return new PluginStateObject.Shape.Representation3D({ repr, sourceData: a.data }, { label: 'Membrane Orientation' });
         });
     },
     update({ a, b, newParams }, plugin: PluginContext) {
@@ -129,6 +129,7 @@ const MembraneOrientation3D = PluginStateTransform.BuiltIn({
             await MembraneOrientationProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
             const props = { ...b.data.repr.props, ...newParams };
             await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
+            b.data.sourceData = a.data;
             return StateTransformer.UpdateResult.Updated;
         });
     },

+ 2 - 1
src/extensions/rcsb/assembly-symmetry/behavior.ts

@@ -124,7 +124,7 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
             const repr = AssemblySymmetryRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => AssemblySymmetryParams);
             await repr.createOrUpdate(params, a.data).runInContext(ctx);
             const { type, kind, symbol } = assemblySymmetry;
-            return new PluginStateObject.Shape.Representation3D({ repr, source: a }, { label: kind, description: `${type} (${symbol})` });
+            return new PluginStateObject.Shape.Representation3D({ repr, sourceData: a.data }, { label: kind, description: `${type} (${symbol})` });
         });
     },
     update({ a, b, newParams }, plugin: PluginContext) {
@@ -138,6 +138,7 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
             }
             const props = { ...b.data.repr.props, ...newParams };
             await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
+            b.data.sourceData = a.data;
             const { type, kind, symbol } = assemblySymmetry;
             b.label = kind;
             b.description = `${type} (${symbol})`;

+ 29 - 1
src/mol-model/structure/structure/structure.ts

@@ -645,7 +645,7 @@ namespace Structure {
         representativeModel?: Model
     }
 
-    /** Serial index of an element in the structure accross all units */
+    /** Serial index of an element in the structure across all units */
     export type SerialIndex = { readonly '@type': 'serial-index' } & number
 
     /** Represents a single structure */
@@ -1227,6 +1227,34 @@ namespace Structure {
 
     export type Index = number;
     export const Index = CustomStructureProperty.createSimple<Index>('index', 'root');
+
+    export const WithChild = {
+        getChild(structure: Structure): Structure | undefined {
+            return (structure as any).__child;
+        },
+        /** Get the proxy target. Usefull for equality checks. */
+        getTarget(structure: Structure): Structure {
+            return (structure as any).__parent || structure;
+        },
+        /**
+         * For `structure` with `parent` this returns a proxy that
+         * targets `parent` and has `structure` attached as a child.
+         */
+        fromStructure(structure: Structure): Structure {
+            if (!structure.parent) return structure;
+
+            return new Proxy(structure.parent, {
+                get: function(target, prop, receiver) {
+                    if (prop === '__child') {
+                        return structure;
+                    } else if (prop === '__parent') {
+                        return structure.parent;
+                    }
+                    return Reflect.get(target, prop, receiver);
+                }
+            });
+        }
+    };
 }
 
 export { Structure };

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

@@ -21,7 +21,7 @@ export async function setStructureClipping(plugin: PluginContext, components: St
     await eachRepr(plugin, components, async (update, repr, clippingCell) => {
         if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
 
-        const structure = repr.obj!.data.source.data;
+        const structure = repr.obj!.data.sourceData;
         // always use the root structure to get the loci so the clipping
         // stays applicable as long as the root structure does not change
         const loci = await lociGetter(structure.root);

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

@@ -22,7 +22,7 @@ export async function setStructureOverpaint(plugin: PluginContext, components: S
     await eachRepr(plugin, components, async (update, repr, overpaintCell) => {
         if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
 
-        const structure = repr.obj!.data.source.data;
+        const structure = repr.obj!.data.sourceData;
         // always use the root structure to get the loci so the overpaint
         // stays applicable as long as the root structure does not change
         const loci = await lociGetter(structure.root);

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

@@ -21,7 +21,7 @@ export async function setStructureTransparency(plugin: PluginContext, components
     await eachRepr(plugin, components, async (update, repr, transparencyCell) => {
         if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
 
-        const structure = repr.obj!.data.source.data;
+        const structure = repr.obj!.data.sourceData;
         // always use the root structure to get the loci so the transparency
         // stays applicable as long as the root structure does not change
         const loci = await lociGetter(structure.root);

+ 6 - 6
src/mol-plugin-state/objects.ts

@@ -41,8 +41,8 @@ export namespace PluginStateObject {
         return !!o && o.type.typeClass === 'Behavior';
     }
 
-    export interface Representation3DData<T extends Representation.Any, S extends StateObject = StateObject> { repr: T, source: S }
-    export function CreateRepresentation3D<T extends Representation.Any, S extends StateObject = StateObject>(type: { name: string }) {
+    export interface Representation3DData<T extends Representation.Any, S = any> { repr: T, sourceData: S }
+    export function CreateRepresentation3D<T extends Representation.Any, S = any>(type: { name: string }) {
         return Create<Representation3DData<T, S>>({ ...type, typeClass: 'Representation3D' });
     }
 
@@ -102,10 +102,10 @@ export namespace PluginStateObject {
         export class Structure extends Create<_Structure>({ name: 'Structure', typeClass: 'Object' }) { }
 
         export namespace Structure {
-            export class Representation3D extends CreateRepresentation3D<StructureRepresentation<any> | ShapeRepresentation<any, any, any>, Structure>({ name: 'Structure 3D' }) { }
+            export class Representation3D extends CreateRepresentation3D<StructureRepresentation<any>, _Structure>({ name: 'Structure 3D' }) { }
 
             export interface Representation3DStateData {
-                source: Representation3D,
+                repr: StructureRepresentation<any>,
                 /** used to restore state when the obj is removed */
                 initialState: Partial<StructureRepresentationState>,
                 state: Partial<StructureRepresentationState>,
@@ -120,12 +120,12 @@ export namespace PluginStateObject {
 
     export namespace Volume {
         export class Data extends Create<_Volume>({ name: 'Volume', typeClass: 'Object' }) { }
-        export class Representation3D extends CreateRepresentation3D<VolumeRepresentation<any>>({ name: 'Volume 3D' }) { }
+        export class Representation3D extends CreateRepresentation3D<VolumeRepresentation<any>, _Volume>({ name: 'Volume 3D' }) { }
     }
 
     export namespace Shape {
         export class Provider extends Create<ShapeProvider<any, any, any>>({ name: 'Shape Provider', typeClass: 'Object' }) { }
-        export class Representation3D extends CreateRepresentation3D<ShapeRepresentation<any, any, any>>({ name: 'Shape 3D' }) { }
+        export class Representation3D extends CreateRepresentation3D<ShapeRepresentation<any, any, any>, unknown>({ name: 'Shape 3D' }) { }
     }
 }
 

+ 72 - 67
src/mol-plugin-state/transforms/representation.ts

@@ -126,14 +126,15 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
         return Task.create('Structure Representation', async ctx => {
             const propertyCtx = { runtime: ctx, assetManager: plugin.managers.asset };
             const provider = plugin.representation.structure.registry.get(params.type.name);
-            if (provider.ensureCustomProperties) await provider.ensureCustomProperties.attach(propertyCtx, a.data);
+            const data = provider.getData?.(a.data, params.type.params) || a.data;
+            if (provider.ensureCustomProperties) await provider.ensureCustomProperties.attach(propertyCtx, data);
             const repr = provider.factory({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, provider.getParams);
-            await Theme.ensureDependencies(propertyCtx, plugin.representation.structure.themes, { structure: a.data }, params);
-            repr.setTheme(Theme.create(plugin.representation.structure.themes, { structure: a.data }, params));
+            await Theme.ensureDependencies(propertyCtx, plugin.representation.structure.themes, { structure: data }, params);
+            repr.setTheme(Theme.create(plugin.representation.structure.themes, { structure: data }, params));
 
             const props = params.type.params || {};
-            await repr.createOrUpdate(props, a.data).runInContext(ctx);
-            return new SO.Molecule.Structure.Representation3D({ repr, source: a }, { label: provider.label });
+            await repr.createOrUpdate(props, data).runInContext(ctx);
+            return new SO.Molecule.Structure.Representation3D({ repr, sourceData: a.data }, { label: provider.label });
         });
     },
     update({ a, b, oldParams, newParams, cache }, plugin: PluginContext) {
@@ -141,26 +142,28 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
             if (newParams.type.name !== oldParams.type.name) return StateTransformer.UpdateResult.Recreate;
 
             const provider = plugin.representation.structure.registry.get(newParams.type.name);
+            if (provider.mustRecreate?.(oldParams.type.params, newParams.type.params)) return StateTransformer.UpdateResult.Recreate;
+
+            const data = provider.getData?.(a.data, newParams.type.params) || a.data;
             const propertyCtx = { runtime: ctx, assetManager: plugin.managers.asset };
-            if (provider.ensureCustomProperties) await provider.ensureCustomProperties.attach(propertyCtx, a.data);
+            if (provider.ensureCustomProperties) await provider.ensureCustomProperties.attach(propertyCtx, data);
 
             // TODO: if themes had a .needsUpdate method the following block could
             //       be optimized and only executed conditionally
-            // dispose isn't called on update so we need to handle it manually
-            Theme.releaseDependencies(plugin.representation.structure.themes, { structure: b.data.source.data }, oldParams);
-            await Theme.ensureDependencies(propertyCtx, plugin.representation.structure.themes, { structure: a.data }, newParams);
-            b.data.repr.setTheme(Theme.create(plugin.representation.structure.themes, { structure: a.data }, newParams));
+            Theme.releaseDependencies(plugin.representation.structure.themes, { structure: b.data.sourceData }, oldParams);
+            await Theme.ensureDependencies(propertyCtx, plugin.representation.structure.themes, { structure: data }, newParams);
+            b.data.repr.setTheme(Theme.create(plugin.representation.structure.themes, { structure: data }, newParams));
 
             const props = { ...b.data.repr.props, ...newParams.type.params };
-            await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
-            b.data.source = a;
+            await b.data.repr.createOrUpdate(props, data).runInContext(ctx);
+            b.data.sourceData = a.data;
             return StateTransformer.UpdateResult.Updated;
         });
     },
     dispose({ b, params }, plugin: PluginContext) {
         if (!b || !params) return;
 
-        const structure = b.data.source.data;
+        const structure = b.data.sourceData;
         const provider = plugin.representation.structure.registry.get(params.type.name);
         if (provider.ensureCustomProperties) provider.ensureCustomProperties.detach(structure);
         Theme.releaseDependencies(plugin.representation.structure.themes, { structure }, params);
@@ -191,26 +194,26 @@ const UnwindStructureAssemblyRepresentation3D = PluginStateTransform.BuiltIn({
         return true;
     },
     apply({ a, params }) {
-        const structure = a.data.source.data;
+        const structure = a.data.sourceData;
         const unitTransforms = new StructureUnitTransforms(structure);
         unwindStructureAssembly(structure, unitTransforms, params.t);
         return new SO.Molecule.Structure.Representation3DState({
             state: { unitTransforms },
             initialState: { unitTransforms: new StructureUnitTransforms(structure) },
             info: structure,
-            source: a
+            repr: a.data.repr
         }, { label: `Unwind T = ${params.t.toFixed(2)}` });
     },
     update({ a, b, newParams, oldParams }) {
         const structure = b.data.info as Structure;
-        if (a.data.source.data !== structure) return StateTransformer.UpdateResult.Recreate;
-        if (a.data.repr !== b.data.source.data.repr) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.sourceData !== structure) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
 
         if (oldParams.t === newParams.t) return StateTransformer.UpdateResult.Unchanged;
         const unitTransforms = b.data.state.unitTransforms!;
         unwindStructureAssembly(structure, unitTransforms, newParams.t);
         b.label = `Unwind T = ${newParams.t.toFixed(2)}`;
-        b.data.source = a;
+        b.data.repr = a.data.repr;
         return StateTransformer.UpdateResult.Updated;
     }
 });
@@ -228,26 +231,26 @@ const ExplodeStructureRepresentation3D = PluginStateTransform.BuiltIn({
         return true;
     },
     apply({ a, params }) {
-        const structure = a.data.source.data;
+        const structure = a.data.sourceData;
         const unitTransforms = new StructureUnitTransforms(structure.root);
         explodeStructure(structure, unitTransforms, params.t);
         return new SO.Molecule.Structure.Representation3DState({
             state: { unitTransforms },
             initialState: { unitTransforms: new StructureUnitTransforms(structure.root) },
             info: structure.root,
-            source: a
+            repr: a.data.repr
         }, { label: `Explode T = ${params.t.toFixed(2)}` });
     },
     update({ a, b, newParams, oldParams }) {
-        const structure = a.data.source.data;
+        const structure = a.data.sourceData;
         if (b.data.info !== structure.root) return StateTransformer.UpdateResult.Recreate;
-        if (a.data.repr !== b.data.source.data.repr) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
 
         if (oldParams.t === newParams.t) return StateTransformer.UpdateResult.Unchanged;
         const unitTransforms = b.data.state.unitTransforms!;
         explodeStructure(structure.root, unitTransforms, newParams.t);
         b.label = `Explode T = ${newParams.t.toFixed(2)}`;
-        b.data.source = a;
+        b.data.repr = a.data.repr;
         return StateTransformer.UpdateResult.Updated;
     }
 });
@@ -276,28 +279,28 @@ const OverpaintStructureRepresentation3DFromScript = PluginStateTransform.BuiltI
         return true;
     },
     apply({ a, params }) {
-        const structure = a.data.source.data;
+        const structure = a.data.sourceData;
         const overpaint = Overpaint.ofScript(params.layers, structure);
 
         return new SO.Molecule.Structure.Representation3DState({
             state: { overpaint },
             initialState: { overpaint: Overpaint.Empty },
             info: structure,
-            source: a
+            repr: a.data.repr
         }, { label: `Overpaint (${overpaint.layers.length} Layers)` });
     },
     update({ a, b, newParams, oldParams }) {
         const oldStructure = b.data.info as Structure;
-        const newStructure = a.data.source.data;
+        const newStructure = a.data.sourceData;
         if (newStructure !== oldStructure) return StateTransformer.UpdateResult.Recreate;
-        if (a.data.repr !== b.data.source.data.repr) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
 
         const oldOverpaint = b.data.state.overpaint!;
         const newOverpaint = Overpaint.ofScript(newParams.layers, newStructure);
         if (Overpaint.areEqual(oldOverpaint, newOverpaint)) return StateTransformer.UpdateResult.Unchanged;
 
         b.data.state.overpaint = newOverpaint;
-        b.data.source = a;
+        b.data.repr = a.data.repr;
         b.label = `Overpaint (${newOverpaint.layers.length} Layers)`;
         return StateTransformer.UpdateResult.Updated;
     }
@@ -328,28 +331,28 @@ const OverpaintStructureRepresentation3DFromBundle = PluginStateTransform.BuiltI
         return true;
     },
     apply({ a, params }) {
-        const structure = a.data.source.data;
+        const structure = a.data.sourceData;
         const overpaint = Overpaint.ofBundle(params.layers, structure);
 
         return new SO.Molecule.Structure.Representation3DState({
             state: { overpaint },
             initialState: { overpaint: Overpaint.Empty },
             info: structure,
-            source: a
+            repr: a.data.repr
         }, { label: `Overpaint (${overpaint.layers.length} Layers)` });
     },
     update({ a, b, newParams, oldParams }) {
         const oldStructure = b.data.info as Structure;
-        const newStructure = a.data.source.data;
+        const newStructure = a.data.sourceData;
         if (newStructure !== oldStructure) return StateTransformer.UpdateResult.Recreate;
-        if (a.data.repr !== b.data.source.data.repr) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
 
         const oldOverpaint = b.data.state.overpaint!;
         const newOverpaint = Overpaint.ofBundle(newParams.layers, newStructure);
         if (Overpaint.areEqual(oldOverpaint, newOverpaint)) return StateTransformer.UpdateResult.Unchanged;
 
         b.data.state.overpaint = newOverpaint;
-        b.data.source = a;
+        b.data.repr = a.data.repr;
         b.label = `Overpaint (${newOverpaint.layers.length} Layers)`;
         return StateTransformer.UpdateResult.Updated;
     }
@@ -377,27 +380,27 @@ const TransparencyStructureRepresentation3DFromScript = PluginStateTransform.Bui
         return true;
     },
     apply({ a, params }) {
-        const structure = a.data.source.data;
+        const structure = a.data.sourceData;
         const transparency = Transparency.ofScript(params.layers, structure);
 
         return new SO.Molecule.Structure.Representation3DState({
             state: { transparency },
             initialState: { transparency: Transparency.Empty },
             info: structure,
-            source: a
+            repr: a.data.repr
         }, { label: `Transparency (${transparency.layers.length} Layers)` });
     },
     update({ a, b, newParams, oldParams }) {
         const structure = b.data.info as Structure;
-        if (a.data.source.data !== structure) return StateTransformer.UpdateResult.Recreate;
-        if (a.data.repr !== b.data.source.data.repr) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.sourceData !== structure) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
 
         const oldTransparency = b.data.state.transparency!;
         const newTransparency = Transparency.ofScript(newParams.layers, structure);
         if (Transparency.areEqual(oldTransparency, newTransparency)) return StateTransformer.UpdateResult.Unchanged;
 
         b.data.state.transparency = newTransparency;
-        b.data.source = a;
+        b.data.repr = a.data.repr;
         b.label = `Transparency (${newTransparency.layers.length} Layers)`;
         return StateTransformer.UpdateResult.Updated;
     }
@@ -426,27 +429,27 @@ const TransparencyStructureRepresentation3DFromBundle = PluginStateTransform.Bui
         return true;
     },
     apply({ a, params }) {
-        const structure = a.data.source.data;
+        const structure = a.data.sourceData;
         const transparency = Transparency.ofBundle(params.layers, structure);
 
         return new SO.Molecule.Structure.Representation3DState({
             state: { transparency },
             initialState: { transparency: Transparency.Empty },
             info: structure,
-            source: a
+            repr: a.data.repr
         }, { label: `Transparency (${transparency.layers.length} Layers)` });
     },
     update({ a, b, newParams, oldParams }) {
         const structure = b.data.info as Structure;
-        if (a.data.source.data !== structure) return StateTransformer.UpdateResult.Recreate;
-        if (a.data.repr !== b.data.source.data.repr) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.sourceData !== structure) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
 
         const oldTransparency = b.data.state.transparency!;
         const newTransparency = Transparency.ofBundle(newParams.layers, structure);
         if (Transparency.areEqual(oldTransparency, newTransparency)) return StateTransformer.UpdateResult.Unchanged;
 
         b.data.state.transparency = newTransparency;
-        b.data.source = a;
+        b.data.repr = a.data.repr;
         b.label = `Transparency (${newTransparency.layers.length} Layers)`;
         return StateTransformer.UpdateResult.Updated;
     }
@@ -474,27 +477,27 @@ const ClippingStructureRepresentation3DFromScript = PluginStateTransform.BuiltIn
         return true;
     },
     apply({ a, params }) {
-        const structure = a.data.source.data;
+        const structure = a.data.sourceData;
         const clipping = Clipping.ofScript(params.layers, structure);
 
         return new SO.Molecule.Structure.Representation3DState({
             state: { clipping },
             initialState: { clipping: Clipping.Empty },
             info: structure,
-            source: a
+            repr: a.data.repr
         }, { label: `Clipping (${clipping.layers.length} Layers)` });
     },
     update({ a, b, newParams, oldParams }) {
         const structure = b.data.info as Structure;
-        if (a.data.source.data !== structure) return StateTransformer.UpdateResult.Recreate;
-        if (a.data.repr !== b.data.source.data.repr) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.sourceData !== structure) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
 
         const oldClipping = b.data.state.clipping!;
         const newClipping = Clipping.ofScript(newParams.layers, structure);
         if (Clipping.areEqual(oldClipping, newClipping)) return StateTransformer.UpdateResult.Unchanged;
 
         b.data.state.clipping = newClipping;
-        b.data.source = a;
+        b.data.repr = a.data.repr;
         b.label = `Clipping (${newClipping.layers.length} Layers)`;
         return StateTransformer.UpdateResult.Updated;
     }
@@ -523,27 +526,27 @@ const ClippingStructureRepresentation3DFromBundle = PluginStateTransform.BuiltIn
         return true;
     },
     apply({ a, params }) {
-        const structure = a.data.source.data;
+        const structure = a.data.sourceData;
         const clipping = Clipping.ofBundle(params.layers, structure);
 
         return new SO.Molecule.Structure.Representation3DState({
             state: { clipping },
             initialState: { clipping: Clipping.Empty },
             info: structure,
-            source: a
+            repr: a.data.repr
         }, { label: `Clipping (${clipping.layers.length} Layers)` });
     },
     update({ a, b, newParams, oldParams }) {
         const structure = b.data.info as Structure;
-        if (a.data.source.data !== structure) return StateTransformer.UpdateResult.Recreate;
-        if (a.data.repr !== b.data.source.data.repr) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.sourceData !== structure) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
 
         const oldClipping = b.data.state.clipping!;
         const newClipping = Clipping.ofBundle(newParams.layers, structure);
         if (Clipping.areEqual(oldClipping, newClipping)) return StateTransformer.UpdateResult.Unchanged;
 
         b.data.state.clipping = newClipping;
-        b.data.source = a;
+        b.data.repr = a.data.repr;
         b.label = `Clipping (${newClipping.layers.length} Layers)`;
         return StateTransformer.UpdateResult.Updated;
     }
@@ -646,7 +649,7 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({
 
             const props = params.type.params || {};
             await repr.createOrUpdate(props, a.data).runInContext(ctx);
-            return new SO.Volume.Representation3D({ repr, source: a }, { label: provider.label, description: VolumeRepresentation3DHelpers.getDescription(props) });
+            return new SO.Volume.Representation3D({ repr, sourceData: a.data }, { label: provider.label, description: VolumeRepresentation3DHelpers.getDescription(props) });
         });
     },
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
@@ -659,6 +662,7 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({
             const props = { ...b.data.repr.props, ...newParams.type.params };
             b.data.repr.setTheme(Theme.create(plugin.representation.volume.themes, { volume: a.data }, newParams));
             await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
+            b.data.sourceData = a.data;
             b.description = VolumeRepresentation3DHelpers.getDescription(props);
             return StateTransformer.UpdateResult.Updated;
         });
@@ -686,13 +690,14 @@ const ShapeRepresentation3D = PluginStateTransform.BuiltIn({
             const props = { ...PD.getDefaultValues(a.data.params), ...params };
             const repr = ShapeRepresentation(a.data.getShape, a.data.geometryUtils);
             await repr.createOrUpdate(props, a.data.data).runInContext(ctx);
-            return new SO.Shape.Representation3D({ repr, source: a }, { label: a.data.label });
+            return new SO.Shape.Representation3D({ repr, sourceData: a.data }, { label: a.data.label });
         });
     },
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
         return Task.create('Shape Representation', async ctx => {
             const props = { ...b.data.repr.props, ...newParams };
             await b.data.repr.createOrUpdate(props, a.data.data).runInContext(ctx);
+            b.data.sourceData = a.data;
             return StateTransformer.UpdateResult.Updated;
         });
     }
@@ -720,7 +725,7 @@ const ModelUnitcell3D = PluginStateTransform.BuiltIn({
             const data = getUnitcellData(a.data, symmetry, params);
             const repr = UnitcellRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => UnitcellParams);
             await repr.createOrUpdate(params, data).runInContext(ctx);
-            return new SO.Shape.Representation3D({ repr, source: a }, { label: `Unit Cell`, description: symmetry.spacegroup.name });
+            return new SO.Shape.Representation3D({ repr, sourceData: data }, { label: `Unit Cell`, description: symmetry.spacegroup.name });
         });
     },
     update({ a, b, newParams }) {
@@ -730,7 +735,7 @@ const ModelUnitcell3D = PluginStateTransform.BuiltIn({
             const props = { ...b.data.repr.props, ...newParams };
             const data = getUnitcellData(a.data, symmetry, props);
             await b.data.repr.createOrUpdate(props, data).runInContext(ctx);
-            b.data.source = a;
+            b.data.sourceData = data;
             return StateTransformer.UpdateResult.Updated;
         });
     }
@@ -755,7 +760,7 @@ const StructureSelectionsDistance3D = PluginStateTransform.BuiltIn({
             const data = getDistanceDataFromStructureSelections(a.data);
             const repr = DistanceRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => DistanceParams);
             await repr.createOrUpdate(params, data).runInContext(ctx);
-            return new SO.Shape.Representation3D({ repr, source: a }, { label: `Distance` });
+            return new SO.Shape.Representation3D({ repr, sourceData: data }, { label: `Distance` });
         });
     },
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
@@ -763,7 +768,7 @@ const StructureSelectionsDistance3D = PluginStateTransform.BuiltIn({
             const props = { ...b.data.repr.props, ...newParams };
             const data = getDistanceDataFromStructureSelections(a.data);
             await b.data.repr.createOrUpdate(props, data).runInContext(ctx);
-            b.data.source = a;
+            b.data.sourceData = data;
             return StateTransformer.UpdateResult.Updated;
         });
     },
@@ -788,7 +793,7 @@ const StructureSelectionsAngle3D = PluginStateTransform.BuiltIn({
             const data = getAngleDataFromStructureSelections(a.data);
             const repr = AngleRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => AngleParams);
             await repr.createOrUpdate(params, data).runInContext(ctx);
-            return new SO.Shape.Representation3D({ repr, source: a }, { label: `Angle` });
+            return new SO.Shape.Representation3D({ repr, sourceData: data }, { label: `Angle` });
         });
     },
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
@@ -796,7 +801,7 @@ const StructureSelectionsAngle3D = PluginStateTransform.BuiltIn({
             const props = { ...b.data.repr.props, ...newParams };
             const data = getAngleDataFromStructureSelections(a.data);
             await b.data.repr.createOrUpdate(props, data).runInContext(ctx);
-            b.data.source = a;
+            b.data.sourceData = data;
             return StateTransformer.UpdateResult.Updated;
         });
     },
@@ -821,7 +826,7 @@ const StructureSelectionsDihedral3D = PluginStateTransform.BuiltIn({
             const data = getDihedralDataFromStructureSelections(a.data);
             const repr = DihedralRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => DihedralParams);
             await repr.createOrUpdate(params, data).runInContext(ctx);
-            return new SO.Shape.Representation3D({ repr, source: a }, { label: `Dihedral` });
+            return new SO.Shape.Representation3D({ repr, sourceData: data }, { label: `Dihedral` });
         });
     },
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
@@ -829,7 +834,7 @@ const StructureSelectionsDihedral3D = PluginStateTransform.BuiltIn({
             const props = { ...b.data.repr.props, ...newParams };
             const data = getDihedralDataFromStructureSelections(a.data);
             await b.data.repr.createOrUpdate(props, data).runInContext(ctx);
-            b.data.source = a;
+            b.data.sourceData = data;
             return StateTransformer.UpdateResult.Updated;
         });
     },
@@ -854,7 +859,7 @@ const StructureSelectionsLabel3D = PluginStateTransform.BuiltIn({
             const data = getLabelDataFromStructureSelections(a.data);
             const repr = LabelRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => LabelParams);
             await repr.createOrUpdate(params, data).runInContext(ctx);
-            return new SO.Shape.Representation3D({ repr, source: a }, { label: `Label` });
+            return new SO.Shape.Representation3D({ repr, sourceData: data }, { label: `Label` });
         });
     },
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
@@ -862,7 +867,7 @@ const StructureSelectionsLabel3D = PluginStateTransform.BuiltIn({
             const props = { ...b.data.repr.props, ...newParams };
             const data = getLabelDataFromStructureSelections(a.data);
             await b.data.repr.createOrUpdate(props, data).runInContext(ctx);
-            b.data.source = a;
+            b.data.sourceData = data;
             return StateTransformer.UpdateResult.Updated;
         });
     },
@@ -887,7 +892,7 @@ const StructureSelectionsOrientation3D = PluginStateTransform.BuiltIn({
             const data = getOrientationDataFromStructureSelections(a.data);
             const repr = OrientationRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => OrientationParams);
             await repr.createOrUpdate(params, data).runInContext(ctx);
-            return new SO.Shape.Representation3D({ repr, source: a }, { label: `Orientation` });
+            return new SO.Shape.Representation3D({ repr, sourceData: data }, { label: `Orientation` });
         });
     },
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
@@ -895,7 +900,7 @@ const StructureSelectionsOrientation3D = PluginStateTransform.BuiltIn({
             const props = { ...b.data.repr.props, ...newParams };
             const data = getOrientationDataFromStructureSelections(a.data);
             await b.data.repr.createOrUpdate(props, data).runInContext(ctx);
-            b.data.source = a;
+            b.data.sourceData = data;
             return StateTransformer.UpdateResult.Updated;
         });
     },

+ 8 - 8
src/mol-plugin-ui/structure/measurements.tsx

@@ -216,7 +216,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
     }
 
     get selections() {
-        return this.props.cell.obj?.data.source as PluginStateObject.Molecule.Structure.Selections | undefined;
+        return this.props.cell.obj?.data.sourceData as ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry> | undefined;
     }
 
     delete = () => {
@@ -234,7 +234,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
         if (!selections) return;
 
         this.plugin.managers.interactivity.lociHighlights.clearHighlights();
-        for (const d of selections.data) {
+        for (const d of selections) {
             this.plugin.managers.interactivity.lociHighlights.highlight({ loci: d.loci }, false);
         }
         this.plugin.managers.interactivity.lociHighlights.highlight({ loci: this.props.cell.obj?.data.repr.getLoci()! }, false);
@@ -250,7 +250,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
         const selections = this.selections;
         if (!selections) return;
 
-        const sphere = Loci.getBundleBoundingSphere(toLociBundle(selections.data));
+        const sphere = Loci.getBundleBoundingSphere(toLociBundle(selections));
         if (sphere) {
             this.plugin.managers.camera.focusSphere(sphere);
         }
@@ -258,11 +258,11 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
 
     get label() {
         const selections = this.selections;
-        switch (selections?.data.length) {
-            case 1: return lociLabel(selections.data[0].loci, { condensed: true });
-            case 2: return distanceLabel(toLociBundle(selections.data), { condensed: true, unitLabel: this.plugin.managers.structure.measurement.state.options.distanceUnitLabel });
-            case 3: return angleLabel(toLociBundle(selections.data), { condensed: true });
-            case 4: return dihedralLabel(toLociBundle(selections.data), { condensed: true });
+        switch (selections?.length) {
+            case 1: return lociLabel(selections[0].loci, { condensed: true });
+            case 2: return distanceLabel(toLociBundle(selections), { condensed: true, unitLabel: this.plugin.managers.structure.measurement.state.options.distanceUnitLabel });
+            case 3: return angleLabel(toLociBundle(selections), { condensed: true });
+            case 4: return dihedralLabel(toLociBundle(selections), { condensed: true });
             default: return '';
         }
     }

+ 2 - 1
src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts

@@ -267,7 +267,7 @@ const VolumeStreamingVisual = PluginStateTransform.BuiltIn({
         const transform = structure?.models.length === 0 ? void 0 : GlobalModelTransformInfo.get(structure?.models[0]!);
         await repr.createOrUpdate(props, channel.data).runInContext(ctx);
         if (transform) repr.setState({ transform });
-        return new SO.Volume.Representation3D({ repr, source: a }, { label: `${Math.round(channel.isoValue.relativeValue * 100) / 100} σ [${srcParams.channel}]` });
+        return new SO.Volume.Representation3D({ repr, sourceData: channel.data }, { label: `${Math.round(channel.isoValue.relativeValue * 100) / 100} σ [${srcParams.channel}]` });
     }),
     update: ({ a, b, newParams, spine }, plugin: PluginContext) => Task.create('Volume Representation', async ctx => {
         // TODO : check if params/underlying data/etc have changed; maybe will need to export "data" or some other "tag" in the Representation for this to work
@@ -280,6 +280,7 @@ const VolumeStreamingVisual = PluginStateTransform.BuiltIn({
         const props = { ...b.data.repr.props, ...params.type.params };
         b.data.repr.setTheme(Theme.create(plugin.representation.volume.themes, { volume: channel.data }, params));
         await b.data.repr.createOrUpdate(props, channel.data).runInContext(ctx);
+        b.data.sourceData = channel.data;
 
         // TODO: set the transform here as well in case the structure moves?
         //       doing this here now breaks the code for some reason...

+ 6 - 6
src/mol-plugin/behavior/static/representation.ts

@@ -54,20 +54,20 @@ export function SyncStructureRepresentation3DState(ctx: PluginContext) {
     events.object.created.subscribe(e => {
         if (!SO.Molecule.Structure.Representation3DState.is(e.obj)) return;
         const data = e.obj.data as SO.Molecule.Structure.Representation3DStateData;
-        data.source.data.repr.setState(data.state);
-        ctx.canvas3d?.update(data.source.data.repr);
+        data.repr.setState(data.state);
+        ctx.canvas3d?.update(data.repr);
     });
     events.object.updated.subscribe(e => {
         if (!SO.Molecule.Structure.Representation3DState.is(e.obj)) return;
         const data = e.obj.data as SO.Molecule.Structure.Representation3DStateData;
-        data.source.data.repr.setState(data.state);
-        ctx.canvas3d?.update(data.source.data.repr);
+        data.repr.setState(data.state);
+        ctx.canvas3d?.update(data.repr);
     });
     events.object.removed.subscribe(e => {
         if (!SO.Molecule.Structure.Representation3DState.is(e.obj)) return;
         const data = e.obj.data as SO.Molecule.Structure.Representation3DStateData;
-        data.source.data.repr.setState(data.initialState);
-        ctx.canvas3d?.update(data.source.data.repr);
+        data.repr.setState(data.initialState);
+        ctx.canvas3d?.update(data.repr);
     });
 }
 

+ 4 - 5
src/mol-repr/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 Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -24,9 +24,6 @@ import { Visual } from './visual';
 import { CustomProperty } from '../mol-model-props/common/custom-property';
 import { Clipping } from '../mol-theme/clipping';
 
-// export interface RepresentationProps {
-//     visuals?: string[]
-// }
 export type RepresentationProps = { [k: string]: any }
 
 export interface RepresentationContext {
@@ -54,6 +51,8 @@ export interface RepresentationProvider<D = any, P extends PD.Params = any, S ex
         attach: (ctx: CustomProperty.Context, data: D) => Promise<void>,
         detach: (data: D) => void
     }
+    readonly getData?: (data: D, props: PD.Values<P>) => D
+    readonly mustRecreate?: (oldProps: PD.Values<P>, newProps: PD.Values<P>) => boolean
 }
 
 export namespace RepresentationProvider {
@@ -66,7 +65,7 @@ export namespace RepresentationProvider {
 
 export type AnyRepresentationProvider = RepresentationProvider<any, {}, Representation.State>
 
-export const EmptyRepresentationProvider = {
+const EmptyRepresentationProvider = {
     label: '',
     description: '',
     factory: () => Representation.Empty,

+ 3 - 1
src/mol-repr/structure/complex-representation.ts

@@ -66,7 +66,9 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
     }
 
     function getLoci(pickingId?: PickingId) {
-        if (pickingId === undefined) return Structure.Loci(_structure);
+        if (pickingId === undefined) {
+            return Structure.Loci(Structure.WithChild.getTarget(_structure));
+        }
         return visual ? visual.getLoci(pickingId) : EmptyLoci;
     }
 

+ 8 - 1
src/mol-repr/structure/representation/ball-and-stick.ts

@@ -24,6 +24,7 @@ const BallAndStickVisuals = {
 
 export const BallAndStickParams = {
     ...ElementSphereParams,
+    traceOnly: PD.Boolean(false, { isHidden: true }), // not useful here
     ...IntraUnitBondCylinderParams,
     ...InterUnitBondCylinderParams,
     unitKinds: getUnitKindsParam(['atomic']),
@@ -50,5 +51,11 @@ export const BallAndStickRepresentationProvider = StructureRepresentationProvide
     defaultValues: PD.getDefaultValues(BallAndStickParams),
     defaultColorTheme: { name: 'element-symbol' },
     defaultSizeTheme: { name: 'physical' },
-    isApplicable: (structure: Structure) => structure.elementCount > 0
+    isApplicable: (structure: Structure) => structure.elementCount > 0,
+    getData: (structure: Structure, props: PD.Values<BallAndStickParams>) => {
+        return props.includeParent ? Structure.WithChild.fromStructure(structure) : structure;
+    },
+    mustRecreate: (oldProps: PD.Values<BallAndStickParams>, newProps: PD.Values<BallAndStickParams>) => {
+        return oldProps.includeParent !== newProps.includeParent;
+    }
 });

+ 1 - 0
src/mol-repr/structure/representation/ellipsoid.ts

@@ -25,6 +25,7 @@ export const EllipsoidParams = {
     ...EllipsoidMeshParams,
     ...IntraUnitBondCylinderParams,
     ...InterUnitBondCylinderParams,
+    includeParent: PD.Boolean(false, { isHidden: true }), // not yet supported here
     unitKinds: getUnitKindsParam(['atomic']),
     sizeFactor: PD.Numeric(1, { min: 0.01, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(0.1, { min: 0.01, max: 3, step: 0.01 }),

+ 7 - 1
src/mol-repr/structure/representation/line.ts

@@ -46,5 +46,11 @@ export const LineRepresentationProvider = StructureRepresentationProvider({
     defaultValues: PD.getDefaultValues(LineParams),
     defaultColorTheme: { name: 'element-symbol' },
     defaultSizeTheme: { name: 'uniform' },
-    isApplicable: (structure: Structure) => structure.elementCount > 0
+    isApplicable: (structure: Structure) => structure.elementCount > 0,
+    getData: (structure: Structure, props: PD.Values<LineParams>) => {
+        return props.includeParent ? Structure.WithChild.fromStructure(structure) : structure;
+    },
+    mustRecreate: (oldProps: PD.Values<LineParams>, newProps: PD.Values<LineParams>) => {
+        return oldProps.includeParent !== newProps.includeParent;
+    }
 });

+ 3 - 1
src/mol-repr/structure/units-representation.ts

@@ -180,7 +180,9 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
     }
 
     function getLoci(pickingId?: PickingId) {
-        if (pickingId === undefined) return Structure.Loci(_structure);
+        if (pickingId === undefined) {
+            return Structure.Loci(Structure.WithChild.getTarget(_structure));
+        }
         let loci: Loci = EmptyLoci;
         visuals.forEach(({ visual }) => {
             const _loci = visual.getLoci(pickingId);

+ 34 - 4
src/mol-repr/structure/visual/bond-inter-unit-cylinder.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>
  */
@@ -19,6 +19,7 @@ import { BondCylinderParams, BondIterator, getInterBondLoci, eachInterBond, make
 import { Sphere3D } from '../../../mol-math/geometry';
 import { Cylinders } from '../../../mol-geo/geometry/cylinders/cylinders';
 import { WebGLContext } from '../../../mol-gl/webgl/context';
+import { SortedArray } from '../../../mol-data/int/sorted-array';
 
 const tmpRefPosBondIt = new Bond.ElementBondIterator();
 function setRefPosition(pos: Vec3, structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
@@ -43,6 +44,29 @@ function getInterUnitBondCylinderBuilderProps(structure: Structure, theme: Theme
 
     const delta = Vec3();
 
+    let stub: undefined | ((edgeIndex: number) => boolean);
+
+    if (props.includeParent) {
+        const child = Structure.WithChild.getChild(structure);
+        if (!child) throw new Error('expected child to exist');
+
+        stub = (edgeIndex: number) => {
+            const b = edges[edgeIndex];
+            const childUnitA = child.unitMap.get(b.unitA);
+            const childUnitB = child.unitMap.get(b.unitB);
+
+            const unitA = structure.unitMap.get(b.unitA);
+            const eA = unitA.elements[b.indexA];
+            const unitB = structure.unitMap.get(b.unitB);
+            const eB = unitB.elements[b.indexB];
+
+            return (
+                childUnitA && SortedArray.has(childUnitA.elements, eA) &&
+                (!childUnitB || !SortedArray.has(childUnitB.elements, eB))
+            );
+        };
+    }
+
     const radius = (edgeIndex: number) => {
         const b = edges[edgeIndex];
         locB.aUnit = structure.unitMap.get(b.unitA);
@@ -123,7 +147,8 @@ function getInterUnitBondCylinderBuilderProps(structure: Structure, theme: Theme
         radius: (edgeIndex: number) => {
             return radius(edgeIndex) * sizeAspectRatio;
         },
-        ignore: makeInterBondIgnoreTest(structure, props)
+        ignore: makeInterBondIgnoreTest(structure, props),
+        stub
     };
 }
 
@@ -133,7 +158,8 @@ function createInterUnitBondCylinderImpostors(ctx: VisualContext, structure: Str
     const builderProps = getInterUnitBondCylinderBuilderProps(structure, theme, props);
     const m = createLinkCylinderImpostors(ctx, builderProps, props, cylinders);
 
-    const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, 1 * props.sizeFactor);
+    const child = Structure.WithChild.getChild(structure);
+    const sphere = Sphere3D.expand(Sphere3D(), (child ?? structure).boundary.sphere, 1 * props.sizeFactor);
     m.setBoundingSphere(sphere);
 
     return m;
@@ -145,7 +171,8 @@ function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structur
     const builderProps = getInterUnitBondCylinderBuilderProps(structure, theme, props);
     const m = createLinkCylinderMesh(ctx, builderProps, props, mesh);
 
-    const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, 1 * props.sizeFactor);
+    const child = Structure.WithChild.getChild(structure);
+    const sphere = Sphere3D.expand(Sphere3D(), (child ?? structure).boundary.sphere, 1 * props.sizeFactor);
     m.setBoundingSphere(sphere);
 
     return m;
@@ -158,6 +185,7 @@ export const InterUnitBondCylinderParams = {
     sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(2 / 3, { min: 0, max: 3, step: 0.01 }),
     tryUseImpostor: PD.Boolean(true),
+    includeParent: PD.Boolean(false),
 };
 export type InterUnitBondCylinderParams = typeof InterUnitBondCylinderParams
 
@@ -183,6 +211,7 @@ export function InterUnitBondCylinderImpostorVisual(materialId: number): Complex
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashCap !== currentProps.dashCap ||
+                newProps.stubCap !== currentProps.stubCap ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
             );
@@ -212,6 +241,7 @@ export function InterUnitBondCylinderMeshVisual(materialId: number): ComplexVisu
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashCap !== currentProps.dashCap ||
+                newProps.stubCap !== currentProps.stubCap ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
             );

+ 4 - 2
src/mol-repr/structure/visual/bond-inter-unit-line.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>
  */
@@ -97,7 +97,8 @@ function createInterUnitBondLines(ctx: VisualContext, structure: Structure, them
 
     const l = createLinkLines(ctx, builderProps, props, lines);
 
-    const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, 1 * sizeFactor);
+    const child = Structure.WithChild.getChild(structure);
+    const sphere = Sphere3D.expand(Sphere3D(), (child ?? structure).boundary.sphere, 1 * props.sizeFactor);
     l.setBoundingSphere(sphere);
 
     return l;
@@ -106,6 +107,7 @@ function createInterUnitBondLines(ctx: VisualContext, structure: Structure, them
 export const InterUnitBondLineParams = {
     ...ComplexLinesParams,
     ...BondLineParams,
+    includeParent: PD.Boolean(false),
 };
 export type InterUnitBondLineParams = typeof InterUnitBondLineParams
 

+ 35 - 7
src/mol-repr/structure/visual/bond-intra-unit-cylinder.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>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -21,14 +21,12 @@ import { Sphere3D } from '../../../mol-math/geometry';
 import { IntAdjacencyGraph } from '../../../mol-math/graph';
 import { WebGLContext } from '../../../mol-gl/webgl/context';
 import { Cylinders } from '../../../mol-geo/geometry/cylinders/cylinders';
+import { SortedArray } from '../../../mol-data/int';
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const isBondType = BondType.is;
 
 function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Structure, theme: Theme, props: PD.Values<IntraUnitBondCylinderParams>) {
-    const locE = StructureElement.Location.create(structure, unit);
-    const locB = Bond.Location(structure, unit, undefined, structure, unit, undefined);
-
     const elements = unit.elements;
     const bonds = unit.bonds;
     const { edgeCount, a, b, edgeProps, offset } = bonds;
@@ -38,6 +36,24 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
     const vRef = Vec3(), delta = Vec3();
     const pos = unit.conformation.invariantPosition;
 
+    let stub: undefined | ((edgeIndex: number) => boolean);
+
+    const locE = StructureElement.Location.create(structure, unit);
+    const locB = Bond.Location(structure, unit, undefined, structure, unit, undefined);
+
+    if (props.includeParent) {
+        const child = Structure.WithChild.getChild(structure);
+        if (!child) throw new Error('expected child to exist');
+        const childUnit = child.unitMap.get(unit.id);
+        if (!childUnit) throw new Error('expected childUnit to exist');
+
+        stub = (edgeIndex: number) => {
+            const eA = elements[a[edgeIndex]];
+            const eB = elements[b[edgeIndex]];
+            return SortedArray.has(childUnit.elements, eA) && !SortedArray.has(childUnit.elements, eB);
+        };
+    }
+
     const radius = (edgeIndex: number) => {
         locB.aIndex = a[edgeIndex];
         locB.bIndex = b[edgeIndex];
@@ -105,7 +121,8 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
         radius: (edgeIndex: number) => {
             return radius(edgeIndex) * sizeAspectRatio;
         },
-        ignore: makeIntraBondIgnoreTest(unit, props)
+        ignore: makeIntraBondIgnoreTest(structure, unit, props),
+        stub
     };
 }
 
@@ -113,10 +130,14 @@ function createIntraUnitBondCylinderImpostors(ctx: VisualContext, unit: Unit, st
     if (!Unit.isAtomic(unit)) return Cylinders.createEmpty(cylinders);
     if (!unit.bonds.edgeCount) return Cylinders.createEmpty(cylinders);
 
+    const child = Structure.WithChild.getChild(structure);
+    const childUnit = child?.unitMap.get(unit.id);
+    if (child && !childUnit) return Cylinders.createEmpty(cylinders);
+
     const builderProps = getIntraUnitBondCylinderBuilderProps(unit, structure, theme, props);
     const c = createLinkCylinderImpostors(ctx, builderProps, props, cylinders);
 
-    const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor);
+    const sphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, 1 * props.sizeFactor);
     c.setBoundingSphere(sphere);
 
     return c;
@@ -126,10 +147,14 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
     if (!unit.bonds.edgeCount) return Mesh.createEmpty(mesh);
 
+    const child = Structure.WithChild.getChild(structure);
+    const childUnit = child?.unitMap.get(unit.id);
+    if (child && !childUnit) return Mesh.createEmpty(mesh);
+
     const builderProps = getIntraUnitBondCylinderBuilderProps(unit, structure, theme, props);
     const m = createLinkCylinderMesh(ctx, builderProps, props, mesh);
 
-    const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor);
+    const sphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, 1 * props.sizeFactor);
     m.setBoundingSphere(sphere);
 
     return m;
@@ -142,6 +167,7 @@ export const IntraUnitBondCylinderParams = {
     sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(2 / 3, { min: 0, max: 3, step: 0.01 }),
     tryUseImpostor: PD.Boolean(true),
+    includeParent: PD.Boolean(false),
 };
 export type IntraUnitBondCylinderParams = typeof IntraUnitBondCylinderParams
 
@@ -167,6 +193,7 @@ export function IntraUnitBondCylinderImpostorVisual(materialId: number): UnitsVi
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashCap !== currentProps.dashCap ||
+                newProps.stubCap !== currentProps.stubCap ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
             );
@@ -207,6 +234,7 @@ export function IntraUnitBondCylinderMeshVisual(materialId: number): UnitsVisual
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashCap !== currentProps.dashCap ||
+                newProps.stubCap !== currentProps.stubCap ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
             );

+ 13 - 3
src/mol-repr/structure/visual/bond-intra-unit-line.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>
  */
@@ -25,6 +25,15 @@ const isBondType = BondType.is;
 function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<IntraUnitBondLineParams>, lines?: Lines) {
     if (!Unit.isAtomic(unit)) return Lines.createEmpty(lines);
 
+    const child = Structure.WithChild.getChild(structure);
+    const childUnit = child?.unitMap.get(unit.id);
+    if (child && !childUnit) return Lines.createEmpty(lines);
+
+    if (props.includeParent) {
+        const child = Structure.WithChild.getChild(structure);
+        if (!child) throw new Error('expected child to exist');
+    }
+
     const location = StructureElement.Location.create(structure, unit);
 
     const elements = unit.elements;
@@ -82,12 +91,12 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
             const sizeB = theme.size.size(location);
             return Math.min(sizeA, sizeB) * sizeFactor;
         },
-        ignore: makeIntraBondIgnoreTest(unit, props)
+        ignore: makeIntraBondIgnoreTest(structure, unit, props)
     };
 
     const l = createLinkLines(ctx, builderProps, props, lines);
 
-    const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * sizeFactor);
+    const sphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, 1 * sizeFactor);
     l.setBoundingSphere(sphere);
 
     return l;
@@ -96,6 +105,7 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
 export const IntraUnitBondLineParams = {
     ...UnitsLinesParams,
     ...BondLineParams,
+    includeParent: PD.Boolean(false),
 };
 export type IntraUnitBondLineParams = typeof IntraUnitBondLineParams
 

+ 6 - 3
src/mol-repr/structure/visual/element-point.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>
  */
@@ -29,17 +29,20 @@ export type ElementPointParams = typeof ElementPointParams
 export function createElementPoint(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<ElementPointParams>, points: Points) {
     // TODO sizeFactor
 
+    const child = Structure.WithChild.getChild(structure);
+    if (child && !child.unitMap.get(unit.id)) return Points.createEmpty(points);
+
     const elements = unit.elements;
     const n = elements.length;
     const builder = PointsBuilder.create(n, n / 10, points);
 
     const p = Vec3();
     const pos = unit.conformation.invariantPosition;
-    const ignore = makeElementIgnoreTest(unit, props);
+    const ignore = makeElementIgnoreTest(structure, unit, props);
 
     if (ignore) {
         for (let i = 0; i < n; ++i) {
-            if (ignore(unit, elements[i])) continue;
+            if (ignore(elements[i])) continue;
             pos(elements[i], p);
             builder.add(p[0], p[1], p[2], i);
         }

+ 56 - 44
src/mol-repr/structure/visual/util/bond.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>
  */
@@ -13,7 +13,7 @@ import { LinkCylinderParams, LinkLineParams } from './link';
 import { ObjectKeys } from '../../../../mol-util/type-helpers';
 import { PickingId } from '../../../../mol-geo/geometry/picking';
 import { EmptyLoci, Loci } from '../../../../mol-model/loci';
-import { Interval, OrderedSet } from '../../../../mol-data/int';
+import { Interval, OrderedSet, SortedArray } from '../../../../mol-data/int';
 import { isH, isHydrogen } from './common';
 
 export const BondParams = {
@@ -42,7 +42,7 @@ export function ignoreBondType(include: BondType.Flag, exclude: BondType.Flag, f
     return !BondType.is(include, f) || BondType.is(exclude, f);
 }
 
-export function makeIntraBondIgnoreTest(unit: Unit.Atomic, props: BondProps): undefined | ((edgeIndex: number) => boolean) {
+export function makeIntraBondIgnoreTest(structure: Structure, unit: Unit.Atomic, props: BondProps): undefined | ((edgeIndex: number) => boolean) {
     const elements = unit.elements;
     const { atomicNumber } = unit.model.atomicHierarchy.derived.atom;
     const bonds = unit.bonds;
@@ -53,16 +53,21 @@ export function makeIntraBondIgnoreTest(unit: Unit.Atomic, props: BondProps): un
 
     const include = BondType.fromNames(includeTypes);
     const exclude = BondType.fromNames(excludeTypes);
-
     const allBondTypes = BondType.isAll(include) && BondType.Flag.None === exclude;
 
-    if (!allBondTypes && ignoreHydrogens) {
-        return (edgeIndex: number) => isH(atomicNumber, elements[a[edgeIndex]]) || isH(atomicNumber, elements[b[edgeIndex]]) || ignoreBondType(include, exclude, _flags[edgeIndex]);
-    } else if (!allBondTypes) {
-        return (edgeIndex: number) => ignoreBondType(include, exclude, _flags[edgeIndex]);
-    } else if (ignoreHydrogens) {
-        return (edgeIndex: number) => isH(atomicNumber, elements[a[edgeIndex]]) || isH(atomicNumber, elements[b[edgeIndex]]);
-    }
+    const child = Structure.WithChild.getChild(structure);
+    const childUnit = child?.unitMap.get(unit.id);
+    if (child && !childUnit) throw new Error('expected childUnit to exist if child exists');
+
+    if (allBondTypes && !ignoreHydrogens && !child) return;
+
+    return (edgeIndex: number) => {
+        return (
+            (!!childUnit && !SortedArray.has(childUnit.elements, elements[a[edgeIndex]])) ||
+            (ignoreHydrogens && (isH(atomicNumber, elements[a[edgeIndex]]) || isH(atomicNumber, elements[b[edgeIndex]]))) ||
+            (!allBondTypes && ignoreBondType(include, exclude, _flags[edgeIndex]))
+        );
+    };
 }
 
 export function makeInterBondIgnoreTest(structure: Structure, props: BondProps): undefined | ((edgeIndex: number) => boolean) {
@@ -73,34 +78,45 @@ export function makeInterBondIgnoreTest(structure: Structure, props: BondProps):
 
     const include = BondType.fromNames(includeTypes);
     const exclude = BondType.fromNames(excludeTypes);
-
     const allBondTypes = BondType.isAll(include) && BondType.Flag.None === exclude;
 
-    const ignoreHydrogen = (edgeIndex: number) => {
-        const b = edges[edgeIndex];
-        const uA = structure.unitMap.get(b.unitA);
-        const uB = structure.unitMap.get(b.unitB);
-        return isHydrogen(uA, uA.elements[b.indexA]) || isHydrogen(uB, uB.elements[b.indexB]);
-    };
+    const child = Structure.WithChild.getChild(structure);
 
-    if (!allBondTypes && ignoreHydrogens) {
-        return (edgeIndex: number) => ignoreHydrogen(edgeIndex) || ignoreBondType(include, exclude, edges[edgeIndex].props.flag);
-    } else if (!allBondTypes) {
-        return (edgeIndex: number) => ignoreBondType(include, exclude, edges[edgeIndex].props.flag);
-    } else if (ignoreHydrogens) {
-        return (edgeIndex: number) => ignoreHydrogen(edgeIndex);
-    }
+    if (allBondTypes && !ignoreHydrogens && !child) return;
+
+    return (edgeIndex: number) => {
+        if (child) {
+            const b = edges[edgeIndex];
+            const childUnitA = child.unitMap.get(b.unitA);
+            if (!childUnitA) return true;
+
+            const unitA = structure.unitMap.get(b.unitA);
+            const eA = unitA.elements[b.indexA];
+            if (!SortedArray.has(childUnitA.elements, eA)) return true;
+        }
+
+        if (ignoreHydrogens) {
+            const b = edges[edgeIndex];
+            const uA = structure.unitMap.get(b.unitA);
+            const uB = structure.unitMap.get(b.unitB);
+            if(isHydrogen(uA, uA.elements[b.indexA]) || isHydrogen(uB, uB.elements[b.indexB])) return true;
+        }
+
+        if (!allBondTypes) {
+            if (ignoreBondType(include, exclude, edges[edgeIndex].props.flag)) return true;
+        }
+
+        return false;
+    };
 }
 
 export namespace BondIterator {
     export function fromGroup(structureGroup: StructureGroup): LocationIterator {
         const { group, structure } = structureGroup;
-        const unit = group.units[0];
+        const unit = group.units[0] as Unit.Atomic;
         const groupCount = Unit.isAtomic(unit) ? unit.bonds.edgeCount * 2 : 0;
         const instanceCount = group.units.length;
-        const location = Bond.Location();
-        location.aStructure = structure;
-        location.bStructure = structure;
+        const location = Bond.Location(structure, undefined, undefined, structure, undefined, undefined);
         const getLocation = (groupIndex: number, instanceIndex: number) => {
             const unit = group.units[instanceIndex] as Unit.Atomic;
             location.aUnit = unit;
@@ -115,9 +131,7 @@ export namespace BondIterator {
     export function fromStructure(structure: Structure): LocationIterator {
         const groupCount = structure.interUnitBonds.edgeCount;
         const instanceCount = 1;
-        const location = Bond.Location();
-        location.aStructure = structure;
-        location.bStructure = structure;
+        const location = Bond.Location(structure, undefined, undefined, structure, undefined, undefined);
         const getLocation = (groupIndex: number) => {
             const bond = structure.interUnitBonds.edges[groupIndex];
             location.aUnit = structure.unitMap.get(bond.unitA);
@@ -138,15 +152,12 @@ export function getIntraBondLoci(pickingId: PickingId, structureGroup: Structure
         const { structure, group } = structureGroup;
         const unit = group.units[instanceId];
         if (Unit.isAtomic(unit)) {
-            return Bond.Loci(structure, [
-                Bond.Location(
-                    structure, unit, unit.bonds.a[groupId] as StructureElement.UnitIndex,
-                    structure, unit, unit.bonds.b[groupId] as StructureElement.UnitIndex
-                ),
-                Bond.Location(
-                    structure, unit, unit.bonds.b[groupId] as StructureElement.UnitIndex,
-                    structure, unit, unit.bonds.a[groupId] as StructureElement.UnitIndex
-                )
+            const target = Structure.WithChild.getTarget(structure);
+            const iA = unit.bonds.a[groupId];
+            const iB = unit.bonds.b[groupId];
+            return Bond.Loci(target, [
+                Bond.Location(target, unit, iA, target, unit, iB),
+                Bond.Location(target, unit, iB, target, unit, iA)
             ]);
         }
     }
@@ -199,12 +210,13 @@ export function eachIntraBond(loci: Loci, structureGroup: StructureGroup, apply:
 export function getInterBondLoci(pickingId: PickingId, structure: Structure, id: number) {
     const { objectId, groupId } = pickingId;
     if (id === objectId) {
+        const target = Structure.WithChild.getTarget(structure);
         const b = structure.interUnitBonds.edges[groupId];
         const uA = structure.unitMap.get(b.unitA);
         const uB = structure.unitMap.get(b.unitB);
-        return Bond.Loci(structure, [
-            Bond.Location(structure, uA, b.indexA, structure, uB, b.indexB),
-            Bond.Location(structure, uB, b.indexB, structure, uA, b.indexA)
+        return Bond.Loci(target, [
+            Bond.Location(target, uA, b.indexA, target, uB, b.indexB),
+            Bond.Location(target, uB, b.indexB, target, uA, b.indexA)
         ]);
     }
     return EmptyLoci;

+ 33 - 16
src/mol-repr/structure/visual/util/element.ts

@@ -8,7 +8,7 @@
 import { Vec3 } from '../../../../mol-math/linear-algebra';
 import { Unit, StructureElement, Structure, ElementIndex } from '../../../../mol-model/structure';
 import { Loci, EmptyLoci } from '../../../../mol-model/loci';
-import { Interval, OrderedSet } from '../../../../mol-data/int';
+import { Interval, OrderedSet, SortedArray } from '../../../../mol-data/int';
 import { Mesh } from '../../../../mol-geo/geometry/mesh/mesh';
 import { sphereVertexCount } from '../../../../mol-geo/primitive/sphere';
 import { MeshBuilder } from '../../../../mol-geo/geometry/mesh/mesh-builder';
@@ -36,22 +36,32 @@ export type ElementSphereMeshProps = {
     sizeFactor: number,
 } & ElementProps
 
-export function makeElementIgnoreTest(unit: Unit, props: ElementProps): undefined | ((unit: Unit, i: ElementIndex) => boolean) {
+export function makeElementIgnoreTest(structure: Structure, unit: Unit, props: ElementProps): undefined | ((i: ElementIndex) => boolean) {
     const { ignoreHydrogens, traceOnly } = props;
 
     const { atomicNumber } = unit.model.atomicHierarchy.derived.atom;
     const isCoarse = Unit.isCoarse(unit);
 
-    if (!isCoarse && ignoreHydrogens && traceOnly) {
-        return (unit: Unit, element: ElementIndex) => isH(atomicNumber, element) && !isTrace(unit, element);
-    } else if (!isCoarse && ignoreHydrogens) {
-        return (unit: Unit, element: ElementIndex) => isH(atomicNumber, element);
-    } else if (!isCoarse && traceOnly) {
-        return (unit: Unit, element: ElementIndex) => !isTrace(unit, element);
-    }
+    const child = Structure.WithChild.getChild(structure);
+    const childUnit = child?.unitMap.get(unit.id);
+    if (child && !childUnit) throw new Error('expected childUnit to exist if child exists');
+
+    if (!child && ((!ignoreHydrogens && !traceOnly) || traceOnly)) return;
+
+    return (element: ElementIndex) => {
+        return (
+            (!!childUnit && !SortedArray.has(childUnit.elements, element)) ||
+            (!isCoarse && ignoreHydrogens && isH(atomicNumber, element)) ||
+            (traceOnly && !isTrace(unit, element))
+        );
+    };
 }
 
 export function createElementSphereMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: ElementSphereMeshProps, mesh?: Mesh): Mesh {
+    const child = Structure.WithChild.getChild(structure);
+    const childUnit = child?.unitMap.get(unit.id);
+    if (child && !childUnit) return Mesh.createEmpty(mesh);
+
     const { detail, sizeFactor } = props;
 
     const { elements } = unit;
@@ -61,7 +71,7 @@ export function createElementSphereMesh(ctx: VisualContext, unit: Unit, structur
 
     const v = Vec3();
     const pos = unit.conformation.invariantPosition;
-    const ignore = makeElementIgnoreTest(unit, props);
+    const ignore = makeElementIgnoreTest(structure, unit, props);
     const l = StructureElement.Location.create(structure, unit);
     const themeSize = theme.size.size;
     const center = Vec3();
@@ -69,7 +79,7 @@ export function createElementSphereMesh(ctx: VisualContext, unit: Unit, structur
     let count = 0;
 
     for (let i = 0; i < elementCount; i++) {
-        if (ignore && ignore(unit, elements[i])) continue;
+        if (ignore && ignore(elements[i])) continue;
 
         l.element = elements[i];
         pos(elements[i], v);
@@ -89,7 +99,7 @@ export function createElementSphereMesh(ctx: VisualContext, unit: Unit, structur
     if (mesh && Vec3.distance(center, mesh.boundingSphere.center) / mesh.boundingSphere.radius < 1.0) {
         boundingSphere = Sphere3D.clone(mesh.boundingSphere);
     } else {
-        boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, maxSize * sizeFactor + 0.05);
+        boundingSphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, maxSize * sizeFactor + 0.05);
     }
 
     const m = MeshBuilder.getMesh(builderState);
@@ -103,13 +113,17 @@ export type ElementSphereImpostorProps = {
 } & ElementProps
 
 export function createElementSphereImpostor(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: ElementSphereImpostorProps, spheres?: Spheres): Spheres {
+    const child = Structure.WithChild.getChild(structure);
+    const childUnit = child?.unitMap.get(unit.id);
+    if (child && !childUnit) return Spheres.createEmpty(spheres);
+
     const { elements } = unit;
     const elementCount = elements.length;
     const builder = SpheresBuilder.create(elementCount, elementCount / 2, spheres);
 
     const v = Vec3();
     const pos = unit.conformation.invariantPosition;
-    const ignore = makeElementIgnoreTest(unit, props);
+    const ignore = makeElementIgnoreTest(structure, unit, props);
 
     const l = StructureElement.Location.create(structure, unit);
     const themeSize = theme.size.size;
@@ -118,7 +132,7 @@ export function createElementSphereImpostor(ctx: VisualContext, unit: Unit, stru
     let count = 0;
 
     for (let i = 0; i < elementCount; i++) {
-        if (ignore?.(unit, elements[i])) continue;
+        if (ignore?.(elements[i])) continue;
 
         pos(elements[i], v);
         builder.add(v[0], v[1], v[2], i);
@@ -136,7 +150,7 @@ export function createElementSphereImpostor(ctx: VisualContext, unit: Unit, stru
     if (spheres && Vec3.distance(center, spheres.boundingSphere.center) / spheres.boundingSphere.radius < 1.0) {
         boundingSphere = Sphere3D.clone(spheres.boundingSphere);
     } else {
-        boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, maxSize * props.sizeFactor + 0.05);
+        boundingSphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, maxSize * props.sizeFactor + 0.05);
     }
 
     const s = builder.getSpheres();
@@ -180,7 +194,10 @@ export function getElementLoci(pickingId: PickingId, structureGroup: StructureGr
         const { structure, group } = structureGroup;
         const unit = group.units[instanceId];
         const indices = OrderedSet.ofSingleton(groupId as StructureElement.UnitIndex);
-        return StructureElement.Loci(structure, [{ unit, indices }]);
+        return StructureElement.Loci(
+            Structure.WithChild.getTarget(structure),
+            [{ unit, indices }]
+        );
     }
     return EmptyLoci;
 }

+ 14 - 10
src/mol-repr/structure/visual/util/link.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>
  */
@@ -24,6 +24,7 @@ export const LinkCylinderParams = {
     dashCount: PD.Numeric(4, { min: 2, max: 10, step: 2 }),
     dashScale: PD.Numeric(0.8, { min: 0, max: 2, step: 0.1 }),
     dashCap: PD.Boolean(true),
+    stubCap: PD.Boolean(true),
     radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo),
 };
 export const DefaultLinkCylinderProps = PD.getDefaultValues(LinkCylinderParams);
@@ -75,6 +76,7 @@ export interface LinkBuilderProps {
     referencePosition?: (edgeIndex: number) => Vec3 | null
     style?: (edgeIndex: number) => LinkStyle
     ignore?: (edgeIndex: number) => boolean
+    stub?: (edgeIndex: number) => boolean
 }
 
 export const enum LinkStyle {
@@ -97,11 +99,11 @@ const v3dot = Vec3.dot;
  * the half closer to the first vertex, i.e. vertex a.
  */
 export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuilderProps, props: LinkCylinderProps, mesh?: Mesh) {
-    const { linkCount, referencePosition, position, style, radius, ignore } = linkBuilder;
+    const { linkCount, referencePosition, position, style, radius, ignore, stub } = linkBuilder;
 
     if (!linkCount) return Mesh.createEmpty(mesh);
 
-    const { linkScale, linkSpacing, radialSegments, linkCap, dashCount, dashScale, dashCap } = props;
+    const { linkScale, linkSpacing, radialSegments, linkCap, dashCount, dashScale, dashCap, stubCap } = props;
 
     const vertexCountEstimate = radialSegments * 2 * linkCount * 2;
     const builderState = MeshBuilder.createState(vertexCountEstimate, vertexCountEstimate / 4, mesh);
@@ -127,7 +129,8 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
 
         const linkRadius = radius(edgeIndex);
         const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
-        const [topCap, bottomCap] = (v3dot(tmpV12, up) > 0) ? [false, linkCap] : [linkCap, false];
+        const linkStub = stubCap && (stub ? stub(edgeIndex) : false);
+        const [topCap, bottomCap] = (v3dot(tmpV12, up) > 0) ? [linkStub, linkCap] : [linkCap, linkStub];
         builderState.currentGroup = edgeIndex;
 
         if (linkStyle === LinkStyle.Solid) {
@@ -176,11 +179,11 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
  * the half closer to the first vertex, i.e. vertex a.
  */
 export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: LinkBuilderProps, props: LinkCylinderProps, cylinders?: Cylinders) {
-    const { linkCount, referencePosition, position, style, radius, ignore } = linkBuilder;
+    const { linkCount, referencePosition, position, style, radius, ignore, stub } = linkBuilder;
 
     if (!linkCount) return Cylinders.createEmpty(cylinders);
 
-    const { linkScale, linkSpacing, linkCap, dashCount, dashScale, dashCap } = props;
+    const { linkScale, linkSpacing, linkCap, dashCount, dashScale, dashCap, stubCap } = props;
 
     const cylindersCountEstimate = linkCount * 2;
     const builder = CylindersBuilder.create(cylindersCountEstimate, cylindersCountEstimate / 4, cylinders);
@@ -200,10 +203,11 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
 
         const linkRadius = radius(edgeIndex);
         const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
+        const linkStub = stubCap && (stub ? stub(edgeIndex) : false);
 
         if (linkStyle === LinkStyle.Solid) {
             v3scale(vb, v3add(vb, va, vb), 0.5);
-            builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], 1, linkCap, false, edgeIndex);
+            builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], 1, linkCap, linkStub, edgeIndex);
         } else if (linkStyle === LinkStyle.Dashed) {
             v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale);
             v3sub(vb, vb, tmpV12);
@@ -218,14 +222,14 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
             v3setMagnitude(vShift, vShift, absOffset);
 
             if (order === 3) builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], multiScale, linkCap, false, edgeIndex);
-            builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vb[0] + vShift[0], vb[1] + vShift[1], vb[2] + vShift[2], multiScale, linkCap, false, edgeIndex);
-            builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vb[0] - vShift[0], vb[1] - vShift[1], vb[2] - vShift[2], multiScale, linkCap, false, edgeIndex);
+            builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vb[0] + vShift[0], vb[1] + vShift[1], vb[2] + vShift[2], multiScale, linkCap, linkStub, edgeIndex);
+            builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vb[0] - vShift[0], vb[1] - vShift[1], vb[2] - vShift[2], multiScale, linkCap, linkStub, edgeIndex);
         } else if (linkStyle === LinkStyle.Disk) {
             v3scale(tmpV12, v3sub(tmpV12, vb, va), 0.475);
             v3add(va, va, tmpV12);
             v3sub(vb, vb, tmpV12);
 
-            builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], 1, linkCap, false, edgeIndex);
+            builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], 1, linkCap, linkStub, edgeIndex);
         }
     }