From ceecee37a711008e3f0a0a866c4ca1b8f8c8cecb Mon Sep 17 00:00:00 2001
From: Alexander Rose <alexander.rose@weirdbyte.de>
Date: Fri, 2 Sep 2022 22:19:45 -0700
Subject: [PATCH] add parentDisplay param to interactions repr

---
 CHANGELOG.md                                  |  1 +
 .../interactions-inter-unit-cylinder.ts       | 62 ++++++++++++++-----
 .../interactions-intra-unit-cylinder.ts       | 41 +++++++++---
 .../computed/representations/shared.ts        | 16 +++++
 4 files changed, 96 insertions(+), 24 deletions(-)
 create mode 100644 src/mol-model-props/computed/representations/shared.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 952ba6982..4192e9273 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ Note that since we don't clearly distinguish between a public and private interf
 ## [Unreleased]
 
 - [Fix] Clone ``Canvas3DParams`` when creating a ``Canvas3D`` instance to prevent shared state between multiple instances.
+- [Add] New ``parentDisplay`` param for interactions representation.
 
 ## [v3.16.0] - 2022-08-25
 
diff --git a/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts b/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts
index d07ee17b5..5bd9d6574 100644
--- a/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts
+++ b/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts
@@ -22,6 +22,8 @@ import { LocationIterator } from '../../../mol-geo/util/location-iterator';
 import { InteractionFlag } from '../interactions/common';
 import { Unit } from '../../../mol-model/structure/structure';
 import { Sphere3D } from '../../../mol-math/geometry';
+import { assertUnreachable } from '../../../mol-util/type-helpers';
+import { InteractionsSharedParams } from './shared';
 
 function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InteractionsInterUnitParams>, mesh?: Mesh) {
     if (!structure.hasAtomic) return Mesh.createEmpty(mesh);
@@ -31,7 +33,7 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
     const { contacts, unitsFeatures } = interactions;
 
     const { edgeCount, edges } = contacts;
-    const { sizeFactor } = props;
+    const { sizeFactor, parentDisplay } = props;
 
     if (!edgeCount) return Mesh.createEmpty(mesh);
 
@@ -70,14 +72,48 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
 
             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 { offsets, members } = unitsFeatures.get(b.unitA);
-                for (let i = offsets[b.indexA], il = offsets[b.indexA + 1]; i < il; ++i) {
-                    const eA = unitA.elements[members[i]];
-                    if (!SortedArray.has(childUnitA.elements, eA)) return true;
+
+                if (parentDisplay === 'stub') {
+                    const childUnitA = child.unitMap.get(b.unitA);
+                    if (!childUnitA) return true;
+
+                    const unitA = structure.unitMap.get(b.unitA);
+                    const { offsets, members } = unitsFeatures.get(b.unitA);
+                    for (let i = offsets[b.indexA], il = offsets[b.indexA + 1]; i < il; ++i) {
+                        const eA = unitA.elements[members[i]];
+                        if (!SortedArray.has(childUnitA.elements, eA)) return true;
+                    }
+                } else if (parentDisplay === 'full' || parentDisplay === 'between') {
+                    let flagA = false;
+                    let flagB = false;
+
+                    const childUnitA = child.unitMap.get(b.unitA);
+                    if (!childUnitA) {
+                        flagA = true;
+                    } else {
+                        const unitA = structure.unitMap.get(b.unitA);
+                        const { offsets, members } = unitsFeatures.get(b.unitA);
+                        for (let i = offsets[b.indexA], il = offsets[b.indexA + 1]; i < il; ++i) {
+                            const eA = unitA.elements[members[i]];
+                            if (!SortedArray.has(childUnitA.elements, eA)) flagA = true;
+                        }
+                    }
+
+                    const childUnitB = child.unitMap.get(b.unitB);
+                    if (!childUnitB) {
+                        flagB = true;
+                    } else {
+                        const unitB = structure.unitMap.get(b.unitB);
+                        const { offsets, members } = unitsFeatures.get(b.unitB);
+                        for (let i = offsets[b.indexB], il = offsets[b.indexB + 1]; i < il; ++i) {
+                            const eB = unitB.elements[members[i]];
+                            if (!SortedArray.has(childUnitB.elements, eB)) flagB = true;
+                        }
+                    }
+
+                    return parentDisplay === 'full' ? flagA && flagB : flagA !== flagB;
+                } else {
+                    assertUnreachable(parentDisplay);
                 }
             }
 
@@ -101,10 +137,7 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
 export const InteractionsInterUnitParams = {
     ...ComplexMeshParams,
     ...LinkCylinderParams,
-    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
-    dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }),
-    dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
-    includeParent: PD.Boolean(false),
+    ...InteractionsSharedParams,
 };
 export type InteractionsInterUnitParams = typeof InteractionsInterUnitParams
 
@@ -121,7 +154,8 @@ export function InteractionsInterUnitVisual(materialId: number): ComplexVisual<I
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashCap !== currentProps.dashCap ||
-                newProps.radialSegments !== currentProps.radialSegments
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.parentDisplay !== currentProps.parentDisplay
             );
 
             const interactionsHash = InteractionsProvider.get(newStructure).version;
diff --git a/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts b/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts
index de3675163..64f180a9e 100644
--- a/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts
+++ b/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts
@@ -22,6 +22,8 @@ import { Interactions } from '../interactions/interactions';
 import { InteractionFlag } from '../interactions/common';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
+import { assertUnreachable } from '../../../mol-util/type-helpers';
+import { InteractionsSharedParams } from './shared';
 
 async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<InteractionsIntraUnitParams>, mesh?: Mesh) {
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
@@ -38,7 +40,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
 
     const { x, y, z, members, offsets } = features;
     const { edgeCount, a, b, edgeProps: { flag } } = contacts;
-    const { sizeFactor } = props;
+    const { sizeFactor, parentDisplay } = props;
 
     if (!edgeCount) return Mesh.createEmpty(mesh);
 
@@ -60,10 +62,31 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
             if (flag[edgeIndex] === InteractionFlag.Filtered) return true;
 
             if (childUnit) {
-                const f = a[edgeIndex];
-                for (let i = offsets[f], jl = offsets[f + 1]; i < jl; ++i) {
-                    const e = unit.elements[members[offsets[i]]];
-                    if (!SortedArray.has(childUnit.elements, e)) return true;
+                if (parentDisplay === 'stub') {
+                    const f = a[edgeIndex];
+                    for (let i = offsets[f], jl = offsets[f + 1]; i < jl; ++i) {
+                        const e = unit.elements[members[offsets[i]]];
+                        if (!SortedArray.has(childUnit.elements, e)) return true;
+                    }
+                } else if (parentDisplay === 'full' || parentDisplay === 'between') {
+                    let flagA = false;
+                    let flagB = false;
+
+                    const fA = a[edgeIndex];
+                    for (let i = offsets[fA], jl = offsets[fA + 1]; i < jl; ++i) {
+                        const eA = unit.elements[members[offsets[i]]];
+                        if (!SortedArray.has(childUnit.elements, eA)) flagA = true;
+                    }
+
+                    const fB = b[edgeIndex];
+                    for (let i = offsets[fB], jl = offsets[fB + 1]; i < jl; ++i) {
+                        const eB = unit.elements[members[offsets[i]]];
+                        if (!SortedArray.has(childUnit.elements, eB)) flagB = true;
+                    }
+
+                    return parentDisplay === 'full' ? flagA && flagB : flagA !== flagB;
+                } else {
+                    assertUnreachable(parentDisplay);
                 }
             }
 
@@ -86,10 +109,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
 export const InteractionsIntraUnitParams = {
     ...UnitsMeshParams,
     ...LinkCylinderParams,
-    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
-    dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }),
-    dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
-    includeParent: PD.Boolean(false),
+    ...InteractionsSharedParams,
 };
 export type InteractionsIntraUnitParams = typeof InteractionsIntraUnitParams
 
@@ -106,7 +126,8 @@ export function InteractionsIntraUnitVisual(materialId: number): UnitsVisual<Int
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashCap !== currentProps.dashCap ||
-                newProps.radialSegments !== currentProps.radialSegments
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.parentDisplay !== currentProps.parentDisplay
             );
 
             const interactionsHash = InteractionsProvider.get(newStructureGroup.structure).version;
diff --git a/src/mol-model-props/computed/representations/shared.ts b/src/mol-model-props/computed/representations/shared.ts
new file mode 100644
index 000000000..61d70bf07
--- /dev/null
+++ b/src/mol-model-props/computed/representations/shared.ts
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+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 }),
+    dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
+    includeParent: PD.Boolean(false),
+    parentDisplay: PD.Select('stub', PD.arrayToOptions(['stub', 'full', 'between'] as const), { description: 'Only has an effect when "includeParent" is enabled. "Stub" shows just the child side of interactions to the parent. "Full" shows both sides of interactions to the parent. "Between" shows only interactions to the parent.' }),
+};
+export type InteractionsSharedParams = typeof InteractionsSharedParams
-- 
GitLab