diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4a3a921e923e7e2505b94da6572e34422e07198..7cdd7d49d15fb76b35b401fb5615c33bb0a60d93 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ Note that since we don't clearly distinguish between a public and private interf
 - Add Model Export extension
 - Bugfix: Automatically treat empty string as "non-present" value in BinaryCIF writer.
 - Fix coarse model support in entity-id color theme
+- Fix marking of carbohydrate visuals (whole chain could get marked instead of single residue)
 
 ## [v3.0.0-dev.10] - 2022-01-17
 
diff --git a/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts b/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts
index be0a198f95543e05488052bc2203eefcff6780f0..b779f658d0ef84682b4db6253818b2a64cd23a22 100644
--- a/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts
+++ b/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 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>
  */
@@ -101,6 +101,8 @@ function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
     return EmptyLoci;
 }
 
+const __linkIndicesSet = new Set<number>();
+
 function eachCarbohydrateLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     let changed = false;
     if (!StructureElement.Loci.is(loci)) return false;
@@ -110,11 +112,14 @@ function eachCarbohydrateLink(loci: Loci, structure: Structure, apply: (interval
     for (const { unit, indices } of loci.elements) {
         if (!Unit.isAtomic(unit)) continue;
 
+        __linkIndicesSet.clear();
         OrderedSet.forEach(indices, v => {
-            // TODO avoid duplicate calls to apply
             const linkIndices = getLinkIndices(unit, unit.elements[v]);
             for (let i = 0, il = linkIndices.length; i < il; ++i) {
-                if (apply(Interval.ofSingleton(linkIndices[i]))) changed = true;
+                if (!__linkIndicesSet.has(linkIndices[i])) {
+                    __linkIndicesSet.add(linkIndices[i]);
+                    if (apply(Interval.ofSingleton(linkIndices[i]))) changed = true;
+                }
             }
         });
     }
diff --git a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
index 8c56b96b7ef54de11291b0f57982c3ea53e9742b..ab3cfd91a64de3ab5d7cb1ed8f274561797338ce 100644
--- a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
+++ b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 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>
  */
@@ -29,8 +29,8 @@ import { getAltResidueLociFromId } from './util/common';
 import { BaseGeometry } from '../../../mol-geo/geometry/base';
 
 const t = Mat4.identity();
-const sVec = Vec3.zero();
-const pd = Vec3.zero();
+const sVec = Vec3();
+const pd = Vec3();
 
 const SideFactor = 2 * 0.806; // 0.806 == Math.cos(Math.PI / 4)
 
@@ -212,6 +212,8 @@ function getCarbohydrateLoci(pickingId: PickingId, structure: Structure, id: num
     return EmptyLoci;
 }
 
+const __elementIndicesSet = new Set<number>();
+
 /** For each carbohydrate (usually a monosaccharide) when all its residue's elements are in a loci. */
 function eachCarbohydrate(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     const { getElementIndices } = structure.carbohydrates;
@@ -222,11 +224,14 @@ function eachCarbohydrate(loci: Loci, structure: Structure, apply: (interval: In
     for (const { unit, indices } of loci.elements) {
         if (!Unit.isAtomic(unit)) continue;
 
+        __elementIndicesSet.clear();
         OrderedSet.forEach(indices, v => {
-            // TODO avoid duplicate calls to apply
             const elementIndices = getElementIndices(unit, unit.elements[v]);
             for (let i = 0, il = elementIndices.length; i < il; ++i) {
-                if (apply(Interval.ofSingleton(elementIndices[i] * 2))) changed = true;
+                if (!__elementIndicesSet.has(elementIndices[i])) {
+                    __elementIndicesSet.add(elementIndices[i]);
+                    if (apply(Interval.ofSingleton(elementIndices[i] * 2))) changed = true;
+                }
             }
         });
     }
diff --git a/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts b/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts
index a95088d747e05eb4f851389752a7d17255c6dcbf..4620b031627009c82b1019f780845bc0d697797f 100644
--- a/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts
+++ b/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 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>
  */
@@ -123,6 +123,8 @@ function getTerminalLinkLoci(pickingId: PickingId, structure: Structure, id: num
     return EmptyLoci;
 }
 
+const __linkIndicesSet = new Set<number>();
+
 function eachTerminalLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     let changed = false;
     if (!StructureElement.Loci.is(loci)) return false;
@@ -132,11 +134,14 @@ function eachTerminalLink(loci: Loci, structure: Structure, apply: (interval: In
     for (const { unit, indices } of loci.elements) {
         if (!Unit.isAtomic(unit)) continue;
 
+        __linkIndicesSet.clear();
         OrderedSet.forEach(indices, v => {
-            // TODO avoid duplicate calls to apply
             const linkIndices = getTerminalLinkIndices(unit, unit.elements[v]);
             for (let i = 0, il = linkIndices.length; i < il; ++i) {
-                if (apply(Interval.ofSingleton(linkIndices[i]))) changed = true;
+                if (!__linkIndicesSet.has(linkIndices[i])) {
+                    __linkIndicesSet.add(linkIndices[i]);
+                    if (apply(Interval.ofSingleton(linkIndices[i]))) changed = true;
+                }
             }
         });
     }
diff --git a/src/mol-repr/visual.ts b/src/mol-repr/visual.ts
index 223b4bfcdb3c794c3af6d58553d48516bc4d084d..90e272b4cb925e35626927f7e9e40b40bc475eb3 100644
--- a/src/mol-repr/visual.ts
+++ b/src/mol-repr/visual.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>
  */
@@ -88,6 +88,7 @@ namespace Visual {
         const currentStatus = markerStatus.ref.value as MarkerInfo['status'];
 
         if (!isEveryLoci(loci)) {
+            // assume that all interval are non-overlapping
             let intervalSize = 0;
             lociApply(loci, interval => {
                 intervalSize += Interval.size(interval);