Browse Source

fix issue with unit boundary reuse

- do at visual level instead
Alexander Rose 3 years ago
parent
commit
e0192ab5aa

+ 2 - 0
CHANGELOG.md

@@ -6,6 +6,8 @@ Note that since we don't clearly distinguish between a public and private interf
 
 ## [Unreleased]
 
+- Fix issue with unit boundary reuse (do at visual level instead)
+
 ## [v3.3.0] - 2022-02-27
 
 - Fix parsing contour-level from emdb v3 header files

+ 20 - 2
src/extensions/rcsb/validation-report/representation.ts

@@ -55,7 +55,16 @@ function createIntraUnitClashCylinderMesh(ctx: VisualContext, unit: Unit, struct
         radius: (edgeIndex: number) => magnitude[edgeIndex] * sizeFactor,
     };
 
-    return createLinkCylinderMesh(ctx, builderProps, props, mesh);
+    const { mesh: m, boundingSphere } = createLinkCylinderMesh(ctx, builderProps, props, mesh);
+
+    if (boundingSphere) {
+        m.setBoundingSphere(boundingSphere);
+    } else if (m.triangleCount > 0) {
+        const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, 1 * sizeFactor);
+        m.setBoundingSphere(sphere);
+    }
+
+    return m;
 }
 
 export const IntraUnitClashParams = {
@@ -169,7 +178,16 @@ function createInterUnitClashCylinderMesh(ctx: VisualContext, structure: Structu
         radius: (edgeIndex: number) => edges[edgeIndex].props.magnitude * sizeFactor
     };
 
-    return createLinkCylinderMesh(ctx, builderProps, props, mesh);
+    const { mesh: m, boundingSphere } = createLinkCylinderMesh(ctx, builderProps, props, mesh);
+
+    if (boundingSphere) {
+        m.setBoundingSphere(boundingSphere);
+    } else {
+        const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, 1 * sizeFactor);
+        m.setBoundingSphere(sphere);
+    }
+
+    return m;
 }
 
 export const InterUnitClashParams = {

+ 9 - 4
src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts

@@ -85,10 +85,15 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
         }
     };
 
-    const m = createLinkCylinderMesh(ctx, builderProps, props, mesh);
-
-    const sphere = Sphere3D.expand(Sphere3D(), (child ?? structure).boundary.sphere, 1 * sizeFactor);
-    m.setBoundingSphere(sphere);
+    const { mesh: m, boundingSphere } = createLinkCylinderMesh(ctx, builderProps, props, mesh);
+
+    if (boundingSphere) {
+        m.setBoundingSphere(boundingSphere);
+    } else if (m.triangleCount > 0) {
+        const { child } = structure;
+        const sphere = Sphere3D.expand(Sphere3D(), (child ?? structure).boundary.sphere, 1 * sizeFactor);
+        m.setBoundingSphere(sphere);
+    }
 
     return m;
 }

+ 7 - 3
src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts

@@ -71,10 +71,14 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
         }
     };
 
-    const m = createLinkCylinderMesh(ctx, builderProps, props, mesh);
+    const { mesh: m, boundingSphere } = createLinkCylinderMesh(ctx, builderProps, props, mesh);
 
-    const sphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, 1 * sizeFactor);
-    m.setBoundingSphere(sphere);
+    if (boundingSphere) {
+        m.setBoundingSphere(boundingSphere);
+    } else if (m.triangleCount > 0) {
+        const sphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, 1 * sizeFactor);
+        m.setBoundingSphere(sphere);
+    }
 
     return m;
 }

+ 11 - 1
src/mol-model-props/integrative/cross-link-restraint/representation.ts

@@ -22,6 +22,7 @@ import { VisualUpdateState } from '../../../mol-repr/util';
 import { ComplexRepresentation, StructureRepresentation, StructureRepresentationStateBuilder, StructureRepresentationProvider } from '../../../mol-repr/structure/representation';
 import { CustomProperty } from '../../common/custom-property';
 import { CrossLinkRestraintProvider, CrossLinkRestraint } from './property';
+import { Sphere3D } from '../../../mol-math/geometry';
 
 function createCrossLinkRestraintCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CrossLinkRestraintCylinderParams>, mesh?: Mesh) {
 
@@ -47,7 +48,16 @@ function createCrossLinkRestraintCylinderMesh(ctx: VisualContext, structure: Str
         },
     };
 
-    return createLinkCylinderMesh(ctx, builderProps, props, mesh);
+    const { mesh: m, boundingSphere } = createLinkCylinderMesh(ctx, builderProps, props, mesh);
+
+    if (boundingSphere) {
+        m.setBoundingSphere(boundingSphere);
+    } else if (m.triangleCount > 0) {
+        const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, 1 * sizeFactor);
+        m.setBoundingSphere(sphere);
+    }
+
+    return m;
 }
 
 export const CrossLinkRestraintCylinderParams = {

+ 1 - 22
src/mol-model/structure/structure/unit.ts

@@ -27,9 +27,6 @@ import { ElementSetIntraBondCache } from './unit/bonds/element-set-intra-bond-ca
 import { ModelSymmetry } from '../../../mol-model-formats/structure/property/symmetry';
 import { getResonance, UnitResonance } from './unit/resonance';
 
-// avoiding namespace lookup improved performance in Chrome (Aug 2020)
-const v3add = Vec3.add;
-
 /**
  * A building block of a structure that corresponds to an atomic or
  * a coarse grained representation 'conveniently grouped together'.
@@ -231,25 +228,7 @@ namespace Unit {
                         : tryRemapBonds(this, this.props.bonds, model, dynamicBonds)
                 };
                 if (!Unit.isSameConformation(this, model)) {
-                    const b = props.boundary;
-                    if (b) {
-                        const { elements } = this;
-                        const pos = this.conformation.invariantPosition;
-                        const v = Vec3();
-                        const center = Vec3();
-
-                        for (let i = 0, il = elements.length; i < il; i++) {
-                            pos(elements[i], v);
-                            v3add(center, center, v);
-                        }
-                        Vec3.scale(center, center, 1 / elements.length);
-
-                        // only invalidate boundary if sphere has changed too much
-                        if (Vec3.distance(center, b.sphere.center) / b.sphere.radius >= 1.0) {
-                            props.boundary = undefined;
-                        }
-                    }
-
+                    props.boundary = undefined;
                     props.lookup3d = undefined;
                     props.principalAxes = undefined;
                 }

+ 20 - 12
src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -162,24 +162,32 @@ function createInterUnitBondCylinderImpostors(ctx: VisualContext, structure: Str
     if (!structure.interUnitBonds.edgeCount) return Cylinders.createEmpty(cylinders);
 
     const builderProps = getInterUnitBondCylinderBuilderProps(structure, theme, props);
-    const m = createLinkCylinderImpostors(ctx, builderProps, props, cylinders);
-
-    const { child } = structure;
-    const sphere = Sphere3D.expand(Sphere3D(), (child ?? structure).boundary.sphere, 1 * props.sizeFactor);
-    m.setBoundingSphere(sphere);
+    const { cylinders: c, boundingSphere } = createLinkCylinderImpostors(ctx, builderProps, props, cylinders);
+
+    if (boundingSphere) {
+        c.setBoundingSphere(boundingSphere);
+    } else if (c.cylinderCount > 0) {
+        const { child } = structure;
+        const sphere = Sphere3D.expand(Sphere3D(), (child ?? structure).boundary.sphere, 1 * props.sizeFactor);
+        c.setBoundingSphere(sphere);
+    }
 
-    return m;
+    return c;
 }
 
 function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitBondCylinderParams>, mesh?: Mesh) {
     if (!structure.interUnitBonds.edgeCount) return Mesh.createEmpty(mesh);
 
     const builderProps = getInterUnitBondCylinderBuilderProps(structure, theme, props);
-    const m = createLinkCylinderMesh(ctx, builderProps, props, mesh);
-
-    const { child } = structure;
-    const sphere = Sphere3D.expand(Sphere3D(), (child ?? structure).boundary.sphere, 1 * props.sizeFactor);
-    m.setBoundingSphere(sphere);
+    const { mesh: m, boundingSphere } = createLinkCylinderMesh(ctx, builderProps, props, mesh);
+
+    if (boundingSphere) {
+        m.setBoundingSphere(boundingSphere);
+    } else if (m.triangleCount > 0) {
+        const { child } = structure;
+        const sphere = Sphere3D.expand(Sphere3D(), (child ?? structure).boundary.sphere, 1 * props.sizeFactor);
+        m.setBoundingSphere(sphere);
+    }
 
     return m;
 }

+ 9 - 5
src/mol-repr/structure/visual/bond-inter-unit-line.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -103,11 +103,15 @@ function createInterUnitBondLines(ctx: VisualContext, structure: Structure, them
         ignore: makeInterBondIgnoreTest(structure, props)
     };
 
-    const l = createLinkLines(ctx, builderProps, props, lines);
+    const { lines: l, boundingSphere } = createLinkLines(ctx, builderProps, props, lines);
 
-    const { child } = structure;
-    const sphere = Sphere3D.expand(Sphere3D(), (child ?? structure).boundary.sphere, 1 * props.sizeFactor);
-    l.setBoundingSphere(sphere);
+    if (boundingSphere) {
+        l.setBoundingSphere(boundingSphere);
+    } else if (l.lineCount > 0) {
+        const { child } = structure;
+        const sphere = Sphere3D.expand(Sphere3D(), (child ?? structure).boundary.sphere, 1 * sizeFactor);
+        l.setBoundingSphere(sphere);
+    }
 
     return l;
 }

+ 14 - 6
src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts

@@ -178,10 +178,14 @@ function createIntraUnitBondCylinderImpostors(ctx: VisualContext, unit: Unit, st
     if (child && !childUnit) return Cylinders.createEmpty(cylinders);
 
     const builderProps = getIntraUnitBondCylinderBuilderProps(unit, structure, theme, props);
-    const c = createLinkCylinderImpostors(ctx, builderProps, props, cylinders);
+    const { cylinders: c, boundingSphere } = createLinkCylinderImpostors(ctx, builderProps, props, cylinders);
 
-    const sphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, 1 * props.sizeFactor);
-    c.setBoundingSphere(sphere);
+    if (boundingSphere) {
+        c.setBoundingSphere(boundingSphere);
+    } else if (c.cylinderCount > 0) {
+        const sphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, 1 * props.sizeFactor);
+        c.setBoundingSphere(sphere);
+    }
 
     return c;
 }
@@ -195,10 +199,14 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
     if (child && !childUnit) return Mesh.createEmpty(mesh);
 
     const builderProps = getIntraUnitBondCylinderBuilderProps(unit, structure, theme, props);
-    const m = createLinkCylinderMesh(ctx, builderProps, props, mesh);
+    const { mesh: m, boundingSphere } = createLinkCylinderMesh(ctx, builderProps, props, mesh);
 
-    const sphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, 1 * props.sizeFactor);
-    m.setBoundingSphere(sphere);
+    if (boundingSphere) {
+        m.setBoundingSphere(boundingSphere);
+    } else if (m.triangleCount > 0) {
+        const sphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, 1 * props.sizeFactor);
+        m.setBoundingSphere(sphere);
+    }
 
     return m;
 }

+ 8 - 4
src/mol-repr/structure/visual/bond-intra-unit-line.ts

@@ -132,10 +132,14 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
         ignore: makeIntraBondIgnoreTest(structure, unit, props)
     };
 
-    const l = createLinkLines(ctx, builderProps, props, lines);
-
-    const sphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, 1 * sizeFactor);
-    l.setBoundingSphere(sphere);
+    const { lines: l, boundingSphere } = createLinkLines(ctx, builderProps, props, lines);
+
+    if (boundingSphere) {
+        l.setBoundingSphere(boundingSphere);
+    } else if (l.lineCount > 0) {
+        const sphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, 1 * sizeFactor);
+        l.setBoundingSphere(sphere);
+    }
 
     return l;
 }

+ 11 - 1
src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts

@@ -19,6 +19,7 @@ import { VisualUpdateState } from '../../util';
 import { VisualContext } from '../../../mol-repr/visual';
 import { Theme } from '../../../mol-theme/theme';
 import { getAltResidueLociFromId } from './util/common';
+import { Sphere3D } from '../../../mol-math/geometry';
 
 function createCarbohydrateLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CarbohydrateLinkParams>, mesh?: Mesh) {
     const { links, elements } = structure.carbohydrates;
@@ -43,7 +44,16 @@ function createCarbohydrateLinkCylinderMesh(ctx: VisualContext, structure: Struc
         },
     };
 
-    return createLinkCylinderMesh(ctx, builderProps, props, mesh);
+    const { mesh: m, boundingSphere } = createLinkCylinderMesh(ctx, builderProps, props, mesh);
+
+    if (boundingSphere) {
+        m.setBoundingSphere(boundingSphere);
+    } else if (m.triangleCount > 0) {
+        const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, 1 * linkSizeFactor);
+        m.setBoundingSphere(sphere);
+    }
+
+    return m;
 }
 
 export const CarbohydrateLinkParams = {

+ 11 - 1
src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts

@@ -20,6 +20,7 @@ import { PickingId } from '../../../mol-geo/geometry/picking';
 import { EmptyLoci, Loci } from '../../../mol-model/loci';
 import { getElementIdx, MetalsSet } from '../../../mol-model/structure/structure/unit/bonds/common';
 import { getAltResidueLociFromId, getAltResidueLoci } from './util/common';
+import { Sphere3D } from '../../../mol-math/geometry';
 
 function createCarbohydrateTerminalLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CarbohydrateTerminalLinkParams>, mesh?: Mesh) {
     const { terminalLinks, elements } = structure.carbohydrates;
@@ -60,7 +61,16 @@ function createCarbohydrateTerminalLinkCylinderMesh(ctx: VisualContext, structur
         }
     };
 
-    return createLinkCylinderMesh(ctx, builderProps, props, mesh);
+    const { mesh: m, boundingSphere } = createLinkCylinderMesh(ctx, builderProps, props, mesh);
+
+    if (boundingSphere) {
+        m.setBoundingSphere(boundingSphere);
+    } else if (m.triangleCount > 0) {
+        const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, 1 * terminalLinkSizeFactor);
+        m.setBoundingSphere(sphere);
+    }
+
+    return m;
 }
 
 export const CarbohydrateTerminalLinkParams = {

+ 21 - 3
src/mol-repr/structure/visual/element-point.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -16,6 +16,9 @@ import { ElementIterator, getElementLoci, eachElement, makeElementIgnoreTest } f
 import { VisualUpdateState } from '../../util';
 import { Sphere3D } from '../../../mol-math/geometry';
 
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3add = Vec3.add;
+
 export const ElementPointParams = {
     ...UnitsPointsParams,
     pointSizeAttenuation: PD.Boolean(false),
@@ -39,24 +42,39 @@ export function createElementPoint(ctx: VisualContext, unit: Unit, structure: St
     const p = Vec3();
     const pos = unit.conformation.invariantPosition;
     const ignore = makeElementIgnoreTest(structure, unit, props);
+    const center = Vec3();
+    let count = 0;
 
     if (ignore) {
         for (let i = 0; i < n; ++i) {
             if (ignore(elements[i])) continue;
             pos(elements[i], p);
+            v3add(center, center, p);
+            count += 1;
             builder.add(p[0], p[1], p[2], i);
         }
     } else {
         for (let i = 0; i < n; ++i) {
             pos(elements[i], p);
+            v3add(center, center, p);
+            count += 1;
             builder.add(p[0], p[1], p[2], i);
         }
     }
 
+    const oldBoundingSphere = points ? Sphere3D.clone(points.boundingSphere) : undefined;
     const pt = builder.getPoints();
+    if (count === 0) return pt;
 
-    const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor);
-    pt.setBoundingSphere(sphere);
+    // re-use boundingSphere if it has not changed much
+    let boundingSphere: Sphere3D;
+    Vec3.scale(center, center, 1 / count);
+    if (oldBoundingSphere && Vec3.distance(center, oldBoundingSphere.center) / oldBoundingSphere.radius < 1.0) {
+        boundingSphere = oldBoundingSphere;
+    } else {
+        boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor);
+    }
+    pt.setBoundingSphere(boundingSphere);
 
     return pt;
 }

+ 35 - 2
src/mol-repr/structure/visual/util/element.ts

@@ -22,6 +22,9 @@ import { SpheresBuilder } from '../../../../mol-geo/geometry/spheres/spheres-bui
 import { isTrace, isH, StructureGroup } from './common';
 import { Sphere3D } from '../../../../mol-math/geometry';
 
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3add = Vec3.add;
+
 type ElementProps = {
     ignoreHydrogens: boolean,
     traceOnly: boolean,
@@ -70,13 +73,17 @@ export function createElementSphereMesh(ctx: VisualContext, unit: Unit, structur
     const ignore = makeElementIgnoreTest(structure, unit, props);
     const l = StructureElement.Location.create(structure, unit);
     const themeSize = theme.size.size;
+    const center = Vec3();
     let maxSize = 0;
+    let count = 0;
 
     for (let i = 0; i < elementCount; i++) {
         if (ignore && ignore(elements[i])) continue;
 
         l.element = elements[i];
         pos(elements[i], v);
+        v3add(center, center, v);
+        count += 1;
 
         builderState.currentGroup = i;
         const size = themeSize(l);
@@ -85,8 +92,19 @@ export function createElementSphereMesh(ctx: VisualContext, unit: Unit, structur
         addSphere(builderState, v, size * sizeFactor, detail);
     }
 
+    const oldBoundingSphere = mesh ? Sphere3D.clone(mesh.boundingSphere) : undefined;
     const m = MeshBuilder.getMesh(builderState);
-    m.setBoundingSphere(Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, maxSize * sizeFactor + 0.05));
+    if (count === 0) return m;
+
+    // re-use boundingSphere if it has not changed much
+    let boundingSphere: Sphere3D;
+    Vec3.scale(center, center, 1 / count);
+    if (oldBoundingSphere && Vec3.distance(center, oldBoundingSphere.center) / oldBoundingSphere.radius < 1.0) {
+        boundingSphere = oldBoundingSphere;
+    } else {
+        boundingSphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, maxSize * sizeFactor + 0.05);
+    }
+    m.setBoundingSphere(boundingSphere);
 
     return m;
 }
@@ -110,21 +128,36 @@ export function createElementSphereImpostor(ctx: VisualContext, unit: Unit, stru
 
     const l = StructureElement.Location.create(structure, unit);
     const themeSize = theme.size.size;
+    const center = Vec3();
     let maxSize = 0;
+    let count = 0;
 
     for (let i = 0; i < elementCount; i++) {
         if (ignore?.(elements[i])) continue;
 
         pos(elements[i], v);
         builder.add(v[0], v[1], v[2], i);
+        v3add(center, center, v);
+        count += 1;
 
         l.element = elements[i];
         const size = themeSize(l);
         if (size > maxSize) maxSize = size;
     }
 
+    const oldBoundingSphere = spheres ? Sphere3D.clone(spheres.boundingSphere) : undefined;
     const s = builder.getSpheres();
-    s.setBoundingSphere(Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, maxSize * props.sizeFactor + 0.05));
+    if (count === 0) return s;
+
+    // re-use boundingSphere if it has not changed much
+    let boundingSphere: Sphere3D;
+    Vec3.scale(center, center, 1 / count);
+    if (oldBoundingSphere && Vec3.distance(center, oldBoundingSphere.center) / oldBoundingSphere.radius < 1.0) {
+        boundingSphere = oldBoundingSphere;
+    } else {
+        boundingSphere = Sphere3D.expand(Sphere3D(), (childUnit ?? unit).boundary.sphere, maxSize * props.sizeFactor + 0.05);
+    }
+    s.setBoundingSphere(boundingSphere);
 
     return s;
 }

+ 63 - 9
src/mol-repr/structure/visual/util/link.ts

@@ -16,6 +16,7 @@ import { Lines } from '../../../../mol-geo/geometry/lines/lines';
 import { LinesBuilder } from '../../../../mol-geo/geometry/lines/lines-builder';
 import { Cylinders } from '../../../../mol-geo/geometry/cylinders/cylinders';
 import { CylindersBuilder } from '../../../../mol-geo/geometry/cylinders/cylinders-builder';
+import { Sphere3D } from '../../../../mol-math/geometry/primitives/sphere3d';
 
 export const LinkCylinderParams = {
     linkScale: PD.Numeric(0.45, { min: 0, max: 1, step: 0.01 }),
@@ -106,10 +107,10 @@ const v3dot = Vec3.dot;
  * Each edge is included twice to allow for coloring/picking
  * the half closer to the first vertex, i.e. vertex a.
  */
-export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuilderProps, props: LinkCylinderProps, mesh?: Mesh) {
+export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuilderProps, props: LinkCylinderProps, mesh?: Mesh): { mesh: Mesh, boundingSphere?: Sphere3D } {
     const { linkCount, referencePosition, position, style, radius, ignore, stub } = linkBuilder;
 
-    if (!linkCount) return Mesh.createEmpty(mesh);
+    if (!linkCount) return { mesh: Mesh.createEmpty(mesh) };
 
     const { linkScale, linkSpacing, radialSegments, linkCap, aromaticScale, aromaticSpacing, aromaticDashCount, dashCount, dashScale, dashCap, stubCap } = props;
 
@@ -119,6 +120,10 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
     const va = Vec3();
     const vb = Vec3();
     const vShift = Vec3();
+
+    const center = Vec3();
+    let count = 0;
+
     const cylinderProps: CylinderProps = {
         radiusTop: 1,
         radiusBottom: 1,
@@ -133,6 +138,11 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
         if (ignore && ignore(edgeIndex)) continue;
 
         position(va, vb, edgeIndex);
+
+        v3add(center, center, va);
+        v3add(center, center, vb);
+        count += 2;
+
         v3sub(tmpV12, vb, va);
         const dirFlag = v3dot(tmpV12, up) > 0;
 
@@ -234,17 +244,27 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
         }
     }
 
-    return MeshBuilder.getMesh(builderState);
+    const oldBoundingSphere = mesh ? Sphere3D.clone(mesh.boundingSphere) : undefined;
+    const m = MeshBuilder.getMesh(builderState);
+    if (count === 0) return { mesh: m };
+
+    // re-use boundingSphere if it has not changed much
+    Vec3.scale(center, center, 1 / count);
+    if (oldBoundingSphere && Vec3.distance(center, oldBoundingSphere.center) / oldBoundingSphere.radius < 1.0) {
+        return { mesh: m, boundingSphere: oldBoundingSphere };
+    } else {
+        return { mesh: m };
+    }
 }
 
 /**
  * Each edge is included twice to allow for coloring/picking
  * the half closer to the first vertex, i.e. vertex a.
  */
-export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: LinkBuilderProps, props: LinkCylinderProps, cylinders?: Cylinders) {
+export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: LinkBuilderProps, props: LinkCylinderProps, cylinders?: Cylinders): { cylinders: Cylinders, boundingSphere?: Sphere3D } {
     const { linkCount, referencePosition, position, style, radius, ignore, stub } = linkBuilder;
 
-    if (!linkCount) return Cylinders.createEmpty(cylinders);
+    if (!linkCount) return { cylinders: Cylinders.createEmpty(cylinders) };
 
     const { linkScale, linkSpacing, linkCap, aromaticScale, aromaticSpacing, aromaticDashCount, dashCount, dashScale, dashCap, stubCap } = props;
 
@@ -256,6 +276,9 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
     const vm = Vec3();
     const vShift = Vec3();
 
+    const center = Vec3();
+    let count = 0;
+
     // automatically adjust length for evenly spaced dashed cylinders
     const segmentCount = dashCount % 2 === 1 ? dashCount : dashCount + 1;
     const lengthScale = 0.5 - (0.5 / 2 / segmentCount);
@@ -268,6 +291,10 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
 
         position(va, vb, edgeIndex);
 
+        v3add(center, center, va);
+        v3add(center, center, vb);
+        count += 2;
+
         const linkRadius = radius(edgeIndex);
         const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
         const linkStub = stubCap && (stub ? stub(edgeIndex) : false);
@@ -337,17 +364,27 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
         }
     }
 
-    return builder.getCylinders();
+    const oldBoundingSphere = cylinders ? Sphere3D.clone(cylinders.boundingSphere) : undefined;
+    const c = builder.getCylinders();
+    if (count === 0) return { cylinders: c };
+
+    // re-use boundingSphere if it has not changed much
+    Vec3.scale(center, center, 1 / count);
+    if (oldBoundingSphere && Vec3.distance(center, oldBoundingSphere.center) / oldBoundingSphere.radius < 1.0) {
+        return { cylinders: c, boundingSphere: oldBoundingSphere };
+    } else {
+        return { cylinders: c };
+    }
 }
 
 /**
  * Each edge is included twice to allow for coloring/picking
  * the half closer to the first vertex, i.e. vertex a.
  */
-export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProps, props: LinkLineProps, lines?: Lines) {
+export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProps, props: LinkLineProps, lines?: Lines): { lines: Lines, boundingSphere?: Sphere3D } {
     const { linkCount, referencePosition, position, style, ignore } = linkBuilder;
 
-    if (!linkCount) return Lines.createEmpty(lines);
+    if (!linkCount) return { lines: Lines.createEmpty(lines) };
 
     const { linkScale, linkSpacing, aromaticDashCount, dashCount } = props;
 
@@ -359,6 +396,9 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
     const vm = Vec3();
     const vShift = Vec3();
 
+    const center = Vec3();
+    let count = 0;
+
     // automatically adjust length for evenly spaced dashed lines
     const segmentCount = dashCount % 2 === 1 ? dashCount : dashCount + 1;
     const lengthScale = 0.5 - (0.5 / 2 / segmentCount);
@@ -373,6 +413,10 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
 
         position(va, vb, edgeIndex);
 
+        v3add(center, center, va);
+        v3add(center, center, vb);
+        count += 2;
+
         const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
 
         if (linkStyle === LinkStyle.Solid) {
@@ -435,5 +479,15 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
         }
     }
 
-    return builder.getLines();
+    const oldBoundingSphere = lines ? Sphere3D.clone(lines.boundingSphere) : undefined;
+    const l = builder.getLines();
+    if (count === 0) return { lines: l };
+
+    // re-use boundingSphere if it has not changed much
+    Vec3.scale(center, center, 1 / count);
+    if (oldBoundingSphere && Vec3.distance(center, oldBoundingSphere.center) / oldBoundingSphere.radius < 1.0) {
+        return { lines: l, boundingSphere: oldBoundingSphere };
+    } else {
+        return { lines: l };
+    }
 }