Преглед на файлове

Merge pull request #804 from giagitom/single-aromatic-dash-count

Enable odd dash count (1,3,5)
Alexander Rose преди 1 година
родител
ревизия
6925547b5f

+ 1 - 0
CHANGELOG.md

@@ -6,6 +6,7 @@ Note that since we don't clearly distinguish between a public and private interf
 
 ## [Unreleased]
 
+- Enable odd dash count (1,3,5)
 - Add principal axes spec and fix edge cases
 - Add a uniform color theme for NtC tube that still paints residue and segment dividers in a different color
 - Mesh exporter improvements

+ 1 - 1
src/extensions/dnatco/ntc-tube/representation.ts

@@ -316,7 +316,7 @@ function createNtCTubeMesh(ctx: VisualContext, unit: Unit, structure: Structure,
                 radiusTop: diameter / 2, radiusBottom: diameter / 2, topCap: true, bottomCap: true, radialSegments: segCount.radial,
             };
             mb.currentGroup = FirstBlockId;
-            addFixedCountDashedCylinder(mb, p_1, p1, 1, 2 * segCount.linear, cylinderProps);
+            addFixedCountDashedCylinder(mb, p_1, p1, 1, 2 * segCount.linear, false, cylinderProps);
         }
     }
 

+ 18 - 12
src/mol-geo/geometry/cylinders/cylinders-builder.ts

@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { ChunkedArray } from '../../../mol-data/util';
@@ -10,7 +11,7 @@ import { Vec3 } from '../../../mol-math/linear-algebra';
 
 export interface CylindersBuilder {
     add(startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
-    addFixedCountDashes(start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
+    addFixedCountDashes(start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, stubCap: boolean, group: number): void
     addFixedLengthDashes(start: Vec3, end: Vec3, segmentLength: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
     getCylinders(): Cylinders
 }
@@ -41,19 +42,24 @@ export namespace CylindersBuilder {
             }
         };
 
-        const addFixedCountDashes = (start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number) => {
+        const addFixedCountDashes = (start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, stubCap: boolean, group: number) => {
             const d = Vec3.distance(start, end);
-            const s = Math.floor(segmentCount / 2);
-            const step = 1 / segmentCount;
+            const isOdd = segmentCount % 2 !== 0;
+            const s = Math.floor((segmentCount + 1) / 2);
+            const step = d / (segmentCount + 0.5);
 
-            Vec3.sub(tmpDir, end, start);
+            Vec3.setMagnitude(tmpDir, Vec3.sub(tmpDir, end, start), step);
+            Vec3.copy(tmpVecA, start);
             for (let j = 0; j < s; ++j) {
-                const f = step * (j * 2 + 1);
-                Vec3.setMagnitude(tmpDir, tmpDir, d * f);
-                Vec3.add(tmpVecA, start, tmpDir);
-                Vec3.setMagnitude(tmpDir, tmpDir, d * step * ((j + 1) * 2));
-                Vec3.add(tmpVecB, start, tmpDir);
+                Vec3.add(tmpVecA, tmpVecA, tmpDir);
+                if (isOdd && j === s - 1) {
+                    Vec3.copy(tmpVecB, end);
+                    if (!stubCap) bottomCap = false;
+                } else {
+                    Vec3.add(tmpVecB, tmpVecA, tmpDir);
+                }
                 add(tmpVecA[0], tmpVecA[1], tmpVecA[2], tmpVecB[0], tmpVecB[1], tmpVecB[2], radiusScale, topCap, bottomCap, group);
+                Vec3.add(tmpVecA, tmpVecA, tmpDir);
             }
         };
 
@@ -62,7 +68,7 @@ export namespace CylindersBuilder {
             addFixedCountDashes,
             addFixedLengthDashes: (start: Vec3, end: Vec3, segmentLength: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number) => {
                 const d = Vec3.distance(start, end);
-                addFixedCountDashes(start, end, d / segmentLength, radiusScale, topCap, bottomCap, group);
+                addFixedCountDashes(start, end, d / segmentLength, radiusScale, topCap, bottomCap, true, group);
             },
             getCylinders: () => {
                 const cylinderCount = groups.elementCount / 6;

+ 15 - 10
src/mol-geo/geometry/lines/lines-builder.ts

@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { ChunkedArray } from '../../../mol-data/util';
@@ -50,17 +51,21 @@ export namespace LinesBuilder {
 
         const addFixedCountDashes = (start: Vec3, end: Vec3, segmentCount: number, group: number) => {
             const d = Vec3.distance(start, end);
-            const s = Math.floor(segmentCount / 2);
-            const step = 1 / segmentCount;
+            const isOdd = segmentCount % 2 !== 0;
+            const s = Math.floor((segmentCount + 1) / 2);
+            const step = d / (segmentCount + 0.5);
 
-            Vec3.sub(tmpDir, end, start);
+            Vec3.setMagnitude(tmpDir, Vec3.sub(tmpDir, end, start), step);
+            Vec3.copy(tmpVecA, start);
             for (let j = 0; j < s; ++j) {
-                const f = step * (j * 2 + 1);
-                Vec3.setMagnitude(tmpDir, tmpDir, d * f);
-                Vec3.add(tmpVecA, start, tmpDir);
-                Vec3.setMagnitude(tmpDir, tmpDir, d * step * ((j + 1) * 2));
-                Vec3.add(tmpVecB, start, tmpDir);
+                Vec3.add(tmpVecA, tmpVecA, tmpDir);
+                if (isOdd && j === s - 1) {
+                    Vec3.copy(tmpVecB, end);
+                } else {
+                    Vec3.add(tmpVecB, tmpVecA, tmpDir);
+                }
                 add(tmpVecA[0], tmpVecA[1], tmpVecA[2], tmpVecB[0], tmpVecB[1], tmpVecB[2], group);
+                Vec3.add(tmpVecA, tmpVecA, tmpDir);
             }
         };
 
@@ -111,4 +116,4 @@ function fillMappingAndIndices(n: number, mb: Float32Array, ib: Uint32Array) {
         ib[io] = o; ib[io + 1] = o + 1; ib[io + 2] = o + 2;
         ib[io + 3] = o + 1; ib[io + 4] = o + 3; ib[io + 5] = o + 2;
     }
-}
+}

+ 21 - 19
src/mol-geo/geometry/mesh/builder/cylinder.ts

@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { Vec3, Mat4 } from '../../../../mol-math/linear-algebra';
@@ -97,26 +98,27 @@ export function addDoubleCylinder(state: MeshBuilder.State, start: Vec3, end: Ve
     MeshBuilder.addPrimitive(state, tmpCylinderMat, cylinder);
 }
 
-export function addFixedCountDashedCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, segmentCount: number, props: BasicCylinderProps) {
-    const s = Math.floor(segmentCount / 2);
-    const step = 1 / segmentCount;
-
-    // automatically adjust length so links/bonds that are rendered as two half cylinders
-    // have evenly spaced dashed cylinders
-    if (lengthScale < 1) {
-        const bias = lengthScale / 2 / segmentCount;
-        lengthScale += segmentCount % 2 === 1 ? bias : -bias;
-    }
-
+export function addFixedCountDashedCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, segmentCount: number, stubCap: boolean, props: BasicCylinderProps) {
     const d = Vec3.distance(start, end) * lengthScale;
-    const cylinder = getCylinder(props);
-    Vec3.sub(tmpCylinderDir, end, start);
+    const isOdd = segmentCount % 2 !== 0;
+    const s = Math.floor((segmentCount + 1) / 2);
+    let step = d / (segmentCount + 0.5);
 
+    let cylinder = getCylinder(props);
+    Vec3.setMagnitude(tmpCylinderDir, Vec3.sub(tmpCylinderDir, end, start), step);
+    Vec3.copy(tmpCylinderStart, start);
     for (let j = 0; j < s; ++j) {
-        const f = step * (j * 2 + 1);
-        Vec3.setMagnitude(tmpCylinderDir, tmpCylinderDir, d * f);
-        Vec3.add(tmpCylinderStart, start, tmpCylinderDir);
-        setCylinderMat(tmpCylinderMat, tmpCylinderStart, tmpCylinderDir, d * step, false);
+        Vec3.add(tmpCylinderStart, tmpCylinderStart, tmpCylinderDir);
+        if (isOdd && j === s - 1) {
+            if (!stubCap && props.topCap) {
+                props.topCap = false;
+                cylinder = getCylinder(props);
+            }
+            step /= 2;
+        }
+        setCylinderMat(tmpCylinderMat, tmpCylinderStart, tmpCylinderDir, step, false);
         MeshBuilder.addPrimitive(state, tmpCylinderMat, cylinder);
+
+        Vec3.add(tmpCylinderStart, tmpCylinderStart, tmpCylinderDir);
     }
-}
+}

+ 2 - 1
src/mol-model-props/computed/representations/shared.ts

@@ -2,13 +2,14 @@
  * Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 
 export const InteractionsSharedParams = {
     sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
-    dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }),
+    dashCount: PD.Numeric(6, { min: 1, max: 10, step: 1 }),
     dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
     ignoreHydrogens: PD.Boolean(false),
     ignoreHydrogensVariant: PD.Select('all', PD.arrayToOptions(['all', 'non-polar'] as const)),

+ 5 - 4
src/mol-repr/structure/visual/polymer-gap-cylinder.ts

@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -68,11 +69,11 @@ function createPolymerGapCylinderMesh(ctx: VisualContext, unit: Unit, structure:
 
             cylinderProps.radiusTop = cylinderProps.radiusBottom = theme.size.size(centerA) * sizeFactor;
             builderState.currentGroup = i;
-            addFixedCountDashedCylinder(builderState, pA, pB, 0.5, segmentCount, cylinderProps);
+            addFixedCountDashedCylinder(builderState, pA, pB, 0.5, segmentCount, false, cylinderProps);
 
             cylinderProps.radiusTop = cylinderProps.radiusBottom = theme.size.size(centerB) * sizeFactor;
             builderState.currentGroup = i + 1;
-            addFixedCountDashedCylinder(builderState, pB, pA, 0.5, segmentCount, cylinderProps);
+            addFixedCountDashedCylinder(builderState, pB, pA, 0.5, segmentCount, false, cylinderProps);
         }
 
         i += 2;
@@ -106,4 +107,4 @@ export function PolymerGapVisual(materialId: number): UnitsVisual<PolymerGapPara
             );
         }
     }, materialId);
-}
+}

+ 36 - 70
src/mol-repr/structure/visual/util/link.ts

@@ -1,8 +1,9 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Zhenyu Zhang <jump2cn@gmail.com>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { Vec3 } from '../../../../mol-math/linear-algebra';
@@ -25,8 +26,8 @@ export const LinkCylinderParams = {
     linkCap: PD.Boolean(false),
     aromaticScale: PD.Numeric(0.3, { min: 0, max: 1, step: 0.01 }),
     aromaticSpacing: PD.Numeric(1.5, { min: 0, max: 3, step: 0.01 }),
-    aromaticDashCount: PD.Numeric(2, { min: 2, max: 6, step: 2 }),
-    dashCount: PD.Numeric(4, { min: 0, max: 10, step: 2 }),
+    aromaticDashCount: PD.Numeric(2, { min: 1, max: 6, step: 1 }),
+    dashCount: PD.Numeric(4, { min: 0, max: 10, step: 1 }),
     dashScale: PD.Numeric(0.8, { min: 0, max: 2, step: 0.1 }),
     dashCap: PD.Boolean(true),
     stubCap: PD.Boolean(true),
@@ -38,8 +39,8 @@ export type LinkCylinderProps = typeof DefaultLinkCylinderProps
 export const LinkLineParams = {
     linkScale: PD.Numeric(0.5, { min: 0, max: 1, step: 0.1 }),
     linkSpacing: PD.Numeric(0.1, { min: 0, max: 2, step: 0.01 }),
-    aromaticDashCount: PD.Numeric(2, { min: 2, max: 6, step: 2 }),
-    dashCount: PD.Numeric(4, { min: 0, max: 10, step: 2 }),
+    aromaticDashCount: PD.Numeric(2, { min: 1, max: 6, step: 1 }),
+    dashCount: PD.Numeric(4, { min: 0, max: 10, step: 1 }),
 };
 export const DefaultLinkLineProps = PD.getDefaultValues(LinkLineParams);
 export type LinkLineProps = typeof DefaultLinkLineProps
@@ -133,8 +134,6 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
         bottomCap: linkCap
     };
 
-    const segmentCount = dashCount + 1;
-
     for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
         if (ignore && ignore(edgeIndex)) continue;
 
@@ -153,8 +152,6 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
         const [topCap, bottomCap] = dirFlag ? [linkStub, linkCap] : [linkCap, linkStub];
         builderState.currentGroup = edgeIndex;
 
-        const aromaticSegmentCount = aromaticDashCount + 1;
-
         if (linkStyle === LinkStyle.Solid) {
             cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
             cylinderProps.topCap = topCap;
@@ -164,12 +161,7 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
         } else if (linkStyle === LinkStyle.Dashed) {
             cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius * dashScale;
             cylinderProps.topCap = cylinderProps.bottomCap = dashCap;
-
-            if (segmentCount > 1) {
-                addFixedCountDashedCylinder(builderState, va, vb, 0.5, segmentCount, cylinderProps);
-            } else {
-                addCylinder(builderState, va, vb, 0.5, cylinderProps);
-            }
+            addFixedCountDashedCylinder(builderState, va, vb, 0.5, dashCount, linkStub, cylinderProps);
         } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
             const order = (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble) ? 2 :
                 (linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple) ? 3 : 1.5;
@@ -196,13 +188,13 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
                 v3setMagnitude(vShift, vShift, aromaticOffset);
                 v3sub(va, va, vShift);
                 v3sub(vb, vb, vShift);
-                addFixedCountDashedCylinder(builderState, va, vb, 0.5, aromaticSegmentCount, cylinderProps);
+                addFixedCountDashedCylinder(builderState, va, vb, 0.5, aromaticDashCount, linkStub, cylinderProps);
 
                 if (linkStyle === LinkStyle.MirroredAromatic) {
                     v3setMagnitude(vShift, vShift, aromaticOffset * 2);
                     v3add(va, va, vShift);
                     v3add(vb, vb, vShift);
-                    addFixedCountDashedCylinder(builderState, va, vb, 0.5, aromaticSegmentCount, cylinderProps);
+                    addFixedCountDashedCylinder(builderState, va, vb, 0.5, aromaticDashCount, linkStub, cylinderProps);
                 }
             } else if (linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.OffsetTriple) {
                 const multipleOffset = linkRadius + multiRadius + linkScale * linkRadius * linkSpacing;
@@ -284,13 +276,6 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
     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);
-
-    const aromaticSegmentCount = aromaticDashCount + 1;
-    const aromaticLengthScale = 0.5 - (0.5 / 2 / aromaticSegmentCount);
-
     for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
         if (ignore && ignore(edgeIndex)) continue;
 
@@ -308,14 +293,8 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
             v3scale(vm, v3add(vm, va, vb), 0.5);
             builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], 1, linkCap, linkStub, edgeIndex);
         } else if (linkStyle === LinkStyle.Dashed) {
-            if (segmentCount > 1) {
-                v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale);
-                v3sub(vb, vb, tmpV12);
-                builder.addFixedCountDashes(va, vb, segmentCount, dashScale, dashCap, dashCap, edgeIndex);
-            } else {
-                v3scale(vm, v3add(vm, va, vb), 0.5);
-                builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], dashScale, dashCap, dashCap, edgeIndex);
-            }
+            v3scale(vm, v3add(vm, va, vb), 0.5);
+            builder.addFixedCountDashes(va, vm, dashCount, dashScale, dashCap, dashCap, linkStub, edgeIndex);
         } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
             const order = (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble) ? 2 :
                 (linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple) ? 3 : 1.5;
@@ -330,22 +309,19 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
 
                 const aromaticOffset = linkRadius + aromaticScale * linkRadius + aromaticScale * linkRadius * aromaticSpacing;
 
-                v3scale(tmpV12, v3sub(tmpV12, vb, va), aromaticLengthScale);
-                v3sub(vb, vb, tmpV12);
-
                 v3setMagnitude(tmpV12, v3sub(tmpV12, vb, va), linkRadius * 0.5);
                 v3add(va, va, tmpV12);
 
                 v3setMagnitude(vShift, vShift, aromaticOffset);
                 v3sub(va, va, vShift);
-                v3sub(vb, vb, vShift);
-                builder.addFixedCountDashes(va, vb, aromaticSegmentCount, aromaticScale, dashCap, dashCap, edgeIndex);
+                v3sub(vm, vm, vShift);
+                builder.addFixedCountDashes(va, vm, aromaticDashCount, aromaticScale, dashCap, dashCap, linkStub, edgeIndex);
 
                 if (linkStyle === LinkStyle.MirroredAromatic) {
                     v3setMagnitude(vShift, vShift, aromaticOffset * 2);
                     v3add(va, va, vShift);
-                    v3add(vb, vb, vShift);
-                    builder.addFixedCountDashes(va, vb, aromaticSegmentCount, aromaticScale, dashCap, dashCap, edgeIndex);
+                    v3add(vm, vm, vShift);
+                    builder.addFixedCountDashes(va, vm, aromaticDashCount, aromaticScale, dashCap, dashCap, linkStub, edgeIndex);
                 }
             } else if (linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.OffsetTriple) {
                 const multipleOffset = linkRadius + multiScale * linkRadius + linkScale * linkRadius * linkSpacing;
@@ -353,7 +329,7 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
 
                 builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], 1, linkCap, linkStub, edgeIndex);
 
-                v3setMagnitude(tmpV12, v3sub(tmpV12, va, vb), linkRadius / 1.5);
+                v3setMagnitude(tmpV12, v3sub(tmpV12, va, vm), linkRadius / 1.5);
                 v3sub(va, va, tmpV12);
 
                 if (order === 3) builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], multiScale, linkCap, linkStub, edgeIndex);
@@ -366,11 +342,11 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
                 builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vm[0] - vShift[0], vm[1] - vShift[1], vm[2] - vShift[2], multiScale, linkCap, linkStub, edgeIndex);
             }
         } else if (linkStyle === LinkStyle.Disk) {
-            v3scale(tmpV12, v3sub(tmpV12, vb, va), 0.475);
+            v3scale(tmpV12, v3sub(tmpV12, vm, va), 0.475);
             v3add(va, va, tmpV12);
-            v3sub(vb, vb, tmpV12);
+            v3sub(vm, vm, tmpV12);
 
-            builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], 1, linkCap, linkStub, edgeIndex);
+            builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], 1, linkCap, linkStub, edgeIndex);
         }
     }
 
@@ -409,12 +385,6 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
     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);
-
-    const aromaticSegmentCount = aromaticDashCount + 1;
-    const aromaticLengthScale = 0.5 - (0.5 / 2 / aromaticSegmentCount);
     const aromaticOffsetFactor = 4.5;
     const multipleOffsetFactor = 3;
 
@@ -433,14 +403,8 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
             v3scale(vm, v3add(vm, va, vb), 0.5);
             builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex);
         } else if (linkStyle === LinkStyle.Dashed) {
-            if (segmentCount > 1) {
-                v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale);
-                v3sub(vb, vb, tmpV12);
-                builder.addFixedCountDashes(va, vb, segmentCount, edgeIndex);
-            } else {
-                v3scale(vm, v3add(vm, va, vb), 0.5);
-                builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex);
-            }
+            v3scale(vm, v3add(vm, va, vb), 0.5);
+            builder.addFixedCountDashes(va, vm, dashCount, edgeIndex);
         } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
             const order = linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble ? 2 :
                 linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple ? 3 : 1.5;
@@ -453,26 +417,28 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
             if (linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
                 builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex);
 
-                v3scale(tmpV12, v3sub(tmpV12, vb, va), aromaticLengthScale);
-                v3sub(vb, vb, tmpV12);
+                const aromaticOffset = absOffset * aromaticOffsetFactor;
 
-                v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor);
+                v3setMagnitude(tmpV12, v3sub(tmpV12, vb, va), aromaticOffset * 0.5);
+                v3add(va, va, tmpV12);
+
+                v3setMagnitude(vShift, vShift, aromaticOffset);
                 v3sub(va, va, vShift);
-                v3sub(vb, vb, vShift);
-                builder.addFixedCountDashes(va, vb, aromaticSegmentCount, edgeIndex);
+                v3sub(vm, vm, vShift);
+                builder.addFixedCountDashes(va, vm, aromaticDashCount, edgeIndex);
 
                 if (linkStyle === LinkStyle.MirroredAromatic) {
-                    v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor * 2);
+                    v3setMagnitude(vShift, vShift, aromaticOffset * 2);
                     v3add(va, va, vShift);
-                    v3add(vb, vb, vShift);
-                    builder.addFixedCountDashes(va, vb, aromaticSegmentCount, edgeIndex);
+                    v3add(vm, vm, vShift);
+                    builder.addFixedCountDashes(va, vm, aromaticDashCount, edgeIndex);
                 }
             } else if (linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.OffsetTriple) {
                 v3setMagnitude(vShift, vShift, absOffset * multipleOffsetFactor);
 
                 builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex);
 
-                v3scale(tmpV12, v3sub(tmpV12, va, vb), linkSpacing * linkScale);
+                v3scale(tmpV12, v3sub(tmpV12, va, vm), linkSpacing * linkScale);
                 v3sub(va, va, tmpV12);
 
                 if (order === 3) builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], edgeIndex);
@@ -485,12 +451,12 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
                 builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vm[0] - vShift[0], vm[1] - vShift[1], vm[2] - vShift[2], edgeIndex);
             }
         } else if (linkStyle === LinkStyle.Disk) {
-            v3scale(tmpV12, v3sub(tmpV12, vb, va), 0.475);
+            v3scale(tmpV12, v3sub(tmpV12, vm, va), 0.475);
             v3add(va, va, tmpV12);
-            v3sub(vb, vb, tmpV12);
+            v3sub(vm, vm, tmpV12);
 
             // TODO what to do here? Line as disk doesn't work well.
-            builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], edgeIndex);
+            builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex);
         }
     }
 
@@ -505,4 +471,4 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
     } else {
         return { lines: l };
     }
-}
+}