Browse Source

Merge branch 'master' of https://github.com/molstar/molstar-proto into plugin

David Sehnal 6 years ago
parent
commit
e5e2b0263d
33 changed files with 356 additions and 84 deletions
  1. 18 12
      src/mol-canvas3d/canvas3d.ts
  2. 1 0
      src/mol-geo/geometry/geometry.ts
  3. 1 0
      src/mol-gl/_spec/renderer.spec.ts
  4. 8 1
      src/mol-gl/renderable.ts
  5. 2 1
      src/mol-gl/renderable/direct-volume.ts
  6. 2 1
      src/mol-gl/renderable/lines.ts
  7. 2 1
      src/mol-gl/renderable/mesh.ts
  8. 2 1
      src/mol-gl/renderable/points.ts
  9. 3 0
      src/mol-gl/renderable/schema.ts
  10. 11 2
      src/mol-gl/renderer.ts
  11. 1 1
      src/mol-gl/shader/chunks/assign-material-color.glsl
  12. 3 1
      src/mol-gl/shader/chunks/common-frag-params.glsl
  13. 5 0
      src/mol-gl/shader/direct-volume.frag
  14. 2 0
      src/mol-gl/shader/lines.frag
  15. 2 1
      src/mol-gl/shader/mesh.frag
  16. 2 0
      src/mol-gl/shader/points.frag
  17. 1 0
      src/mol-math/geometry/gaussian-density/gpu.ts
  18. 27 5
      src/mol-model/structure/structure/carbohydrates/compute.ts
  19. 4 2
      src/mol-model/structure/structure/carbohydrates/data.ts
  20. 12 0
      src/mol-plugin/ui/controls/parameters.tsx
  21. 14 5
      src/mol-repr/representation.ts
  22. 7 3
      src/mol-repr/shape/representation.ts
  23. 9 3
      src/mol-repr/structure/complex-representation.ts
  24. 4 1
      src/mol-repr/structure/complex-visual.ts
  25. 4 1
      src/mol-repr/structure/representation/carbohydrate.ts
  26. 12 4
      src/mol-repr/structure/units-representation.ts
  27. 7 4
      src/mol-repr/structure/units-visual.ts
  28. 6 17
      src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts
  29. 5 5
      src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
  30. 147 0
      src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts
  31. 5 3
      src/mol-repr/structure/visual/inter-unit-link-cylinder.ts
  32. 13 4
      src/mol-repr/volume/representation.ts
  33. 14 5
      src/mol-util/param-definition.ts

+ 18 - 12
src/mol-canvas3d/canvas3d.ts

@@ -26,15 +26,17 @@ import { MarkerAction } from 'mol-geo/geometry/marker-data';
 import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci';
 import { Color } from 'mol-util/color';
 import { Camera } from './camera';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
 
-export const DefaultCanvas3DProps = {
+export const Canvas3DParams = {
     // TODO: FPS cap?
-    // maxFps: 30,
-    cameraPosition: Vec3.create(0, 0, 50),
-    cameraMode: 'perspective' as Camera.Mode,
-    backgroundColor: Color(0x000000),
+    // maxFps: PD.Numeric(30),
+    cameraPosition: PD.Vec3(Vec3.create(0, 0, 50)), // TODO or should it be in a seperate 'state' property?
+    cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]),
+    backgroundColor: PD.Color(Color(0x000000)),
+    pickingAlphaThreshold: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'The minimum opacity value needed for an object to be pickable.' }),
 }
-export type Canvas3DProps = typeof DefaultCanvas3DProps
+export type Canvas3DParams = typeof Canvas3DParams
 
 export { Canvas3D }
 
@@ -64,18 +66,18 @@ interface Canvas3D {
     readonly camera: Camera
     downloadScreenshot: () => void
     getImageData: (variant: RenderVariant) => ImageData
-    setProps: (props: Partial<Canvas3DProps>) => void
+    setProps: (props: Partial<PD.Values<Canvas3DParams>>) => void
 
     /** Returns a copy of the current Canvas3D instance props */
-    readonly props: Canvas3DProps
+    readonly props: PD.Values<Canvas3DParams>
     readonly input: InputObserver
     readonly stats: RendererStats
     dispose: () => void
 }
 
 namespace Canvas3D {
-    export function create(canvas: HTMLCanvasElement, container: Element, props: Partial<Canvas3DProps> = {}): Canvas3D {
-        const p = { ...props, ...DefaultCanvas3DProps }
+    export function create(canvas: HTMLCanvasElement, container: Element, props: Partial<PD.Values<Canvas3DParams>> = {}): Canvas3D {
+        const p = { ...PD.getDefaultValues(Canvas3DParams), ...props }
 
         const reprRenderObjects = new Map<Representation.Any, Set<RenderObject>>()
         const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>()
@@ -353,13 +355,16 @@ namespace Canvas3D {
                 }
             },
             didDraw,
-            setProps: (props: Partial<Canvas3DProps>) => {
+            setProps: (props: Partial<PD.Values<Canvas3DParams>>) => {
                 if (props.cameraMode !== undefined && props.cameraMode !== camera.state.mode) {
                     camera.setState({ mode: props.cameraMode })
                 }
                 if (props.backgroundColor !== undefined && props.backgroundColor !== renderer.props.clearColor) {
                     renderer.setClearColor(props.backgroundColor)
                 }
+                if (props.pickingAlphaThreshold !== undefined && props.pickingAlphaThreshold !== renderer.props.pickingAlphaThreshold) {
+                    renderer.setPickingAlphaThreshold(props.pickingAlphaThreshold)
+                }
                 requestDraw(true)
             },
 
@@ -367,7 +372,8 @@ namespace Canvas3D {
                 return {
                     cameraPosition: Vec3.clone(camera.position),
                     cameraMode: camera.state.mode,
-                    backgroundColor: renderer.props.clearColor
+                    backgroundColor: renderer.props.clearColor,
+                    pickingAlphaThreshold: renderer.props.pickingAlphaThreshold,
                 }
             },
             get input() {

+ 1 - 0
src/mol-geo/geometry/geometry.ts

@@ -104,6 +104,7 @@ export namespace Geometry {
 export function createRenderableState(props: PD.Values<Geometry.Params>): RenderableState {
     return {
         visible: true,
+        pickable: true,
         depthMask: props.depthMask
     }
 }

+ 1 - 0
src/mol-gl/_spec/renderer.spec.ts

@@ -85,6 +85,7 @@ function createPoints() {
     }
     const state: RenderableState = {
         visible: true,
+        pickable: true,
         depthMask: true,
     }
 

+ 8 - 1
src/mol-gl/renderable.ts

@@ -9,9 +9,11 @@ import { RenderableValues, Values, RenderableSchema } from './renderable/schema'
 import { RenderVariant, RenderItem } from './webgl/render-item';
 import { Sphere3D } from 'mol-math/geometry';
 import { Vec3 } from 'mol-math/linear-algebra';
+import { ValueCell } from 'mol-util';
 
 export type RenderableState = {
     visible: boolean
+    pickable: boolean
     depthMask: boolean
 }
 
@@ -41,7 +43,12 @@ export function createRenderable<T extends Values<RenderableSchema>>(renderItem:
         },
         get opaque () { return values.uAlpha && values.uAlpha.ref.value === 1 },
 
-        render: (variant: RenderVariant) => renderItem.render(variant),
+        render: (variant: RenderVariant) => {
+            if (values.uPickable) {
+                ValueCell.updateIfChanged(values.uPickable, state.pickable ? 1 : 0)
+            }
+            renderItem.render(variant)
+        },
         getProgram: (variant: RenderVariant) => renderItem.getProgram(variant),
         update: () => renderItem.update(),
         dispose: () => renderItem.destroy()

+ 2 - 1
src/mol-gl/renderable/direct-volume.ts

@@ -57,7 +57,8 @@ export type DirectVolumeValues = Values<DirectVolumeSchema>
 export function DirectVolumeRenderable(ctx: WebGLContext, id: number, values: DirectVolumeValues, state: RenderableState): Renderable<DirectVolumeValues> {
     const schema = { ...GlobalUniformSchema, ...InternalSchema, ...DirectVolumeSchema }
     const internalValues: InternalValues = {
-        uObjectId: ValueCell.create(id)
+        uObjectId: ValueCell.create(id),
+        uPickable: ValueCell.create(state.pickable ? 1 : 0)
     }
     const shaderCode = DirectVolumeShaderCode
     const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues })

+ 2 - 1
src/mol-gl/renderable/lines.ts

@@ -28,7 +28,8 @@ export type LinesValues = Values<LinesSchema>
 export function LinesRenderable(ctx: WebGLContext, id: number, values: LinesValues, state: RenderableState): Renderable<LinesValues> {
     const schema = { ...GlobalUniformSchema, ...InternalSchema, ...LinesSchema }
     const internalValues: InternalValues = {
-        uObjectId: ValueCell.create(id)
+        uObjectId: ValueCell.create(id),
+        uPickable: ValueCell.create(state.pickable ? 1 : 0)
     }
     const shaderCode = LinesShaderCode
     const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues })

+ 2 - 1
src/mol-gl/renderable/mesh.ts

@@ -26,7 +26,8 @@ export type MeshValues = Values<MeshSchema>
 export function MeshRenderable(ctx: WebGLContext, id: number, values: MeshValues, state: RenderableState): Renderable<MeshValues> {
     const schema = { ...GlobalUniformSchema, ...InternalSchema, ...MeshSchema }
     const internalValues: InternalValues = {
-        uObjectId: ValueCell.create(id)
+        uObjectId: ValueCell.create(id),
+        uPickable: ValueCell.create(state.pickable ? 1 : 0)
     }
     const shaderCode = MeshShaderCode
     const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues })

+ 2 - 1
src/mol-gl/renderable/points.ts

@@ -25,7 +25,8 @@ export type PointsValues = Values<PointsSchema>
 export function PointsRenderable(ctx: WebGLContext, id: number, values: PointsValues, state: RenderableState): Renderable<PointsValues> {
     const schema = { ...GlobalUniformSchema, ...InternalSchema, ...PointsSchema }
     const internalValues: InternalValues = {
-        uObjectId: ValueCell.create(id)
+        uObjectId: ValueCell.create(id),
+        uPickable: ValueCell.create(state.pickable ? 1 : 0)
     }
     const shaderCode = PointsShaderCode
     const renderItem = createRenderItem(ctx, 'points', shaderCode, schema, { ...values, ...internalValues })

+ 3 - 0
src/mol-gl/renderable/schema.ts

@@ -149,12 +149,15 @@ export const GlobalUniformSchema = {
     uFogNear: UniformSpec('f'),
     uFogFar: UniformSpec('f'),
     uFogColor: UniformSpec('v3'),
+
+    uPickingAlphaThreshold: UniformSpec('f'),
 }
 export type GlobalUniformSchema = typeof GlobalUniformSchema
 export type GlobalUniformValues = { [k in keyof GlobalUniformSchema]: ValueCell<any> }
 
 export const InternalSchema = {
     uObjectId: UniformSpec('i'),
+    uPickable: UniformSpec('i'),
 }
 export type InternalSchema = typeof InternalSchema
 export type InternalValues = { [k in keyof InternalSchema]: ValueCell<any> }

+ 11 - 2
src/mol-gl/renderer.ts

@@ -39,20 +39,22 @@ interface Renderer {
     render: (scene: Scene, variant: RenderVariant) => void
     setViewport: (x: number, y: number, width: number, height: number) => void
     setClearColor: (color: Color) => void
+    setPickingAlphaThreshold: (value: number) => void
     getImageData: () => ImageData
     dispose: () => void
 }
 
 export const DefaultRendererProps = {
     clearColor: Color(0x000000),
-    viewport: Viewport.create(0, 0, 0, 0)
+    viewport: Viewport.create(0, 0, 0, 0),
+    pickingAlphaThreshold: 0.5,
 }
 export type RendererProps = typeof DefaultRendererProps
 
 namespace Renderer {
     export function create(ctx: WebGLContext, camera: Camera, props: Partial<RendererProps> = {}): Renderer {
         const { gl } = ctx
-        let { clearColor, viewport: _viewport } = { ...DefaultRendererProps, ...props }
+        let { clearColor, viewport: _viewport, pickingAlphaThreshold } = { ...DefaultRendererProps, ...props }
 
         const viewport = Viewport.clone(_viewport)
         const viewportVec4 = Viewport.toVec4(Vec4.zero(), viewport)
@@ -98,6 +100,8 @@ namespace Renderer {
             uFogNear: ValueCell.create(camera.state.near),
             uFogFar: ValueCell.create(camera.state.far / 50),
             uFogColor: ValueCell.create(Vec3.clone(fogColor)),
+
+            uPickingAlphaThreshold: ValueCell.create(pickingAlphaThreshold),
         }
 
         let currentProgramId = -1
@@ -175,6 +179,10 @@ namespace Renderer {
             render,
 
             setClearColor,
+            setPickingAlphaThreshold: (value: number) => {
+                pickingAlphaThreshold = value
+                ValueCell.update(globalUniforms.uPickingAlphaThreshold, pickingAlphaThreshold)
+            },
             setViewport: (x: number, y: number, width: number, height: number) => {
                 Viewport.set(viewport, x, y, width, height)
                 gl.viewport(x, y, width, height)
@@ -192,6 +200,7 @@ namespace Renderer {
             get props() {
                 return {
                     clearColor,
+                    pickingAlphaThreshold,
                     viewport
                 }
             },

+ 1 - 1
src/mol-gl/shader/chunks/assign-material-color.glsl

@@ -3,5 +3,5 @@
 #elif defined(dColorType_attribute) || defined(dColorType_instance) || defined(dColorType_group) || defined(dColorType_groupInstance)
     vec4 material = vec4(vColor.rgb, uAlpha);
 #elif defined(dColorType_objectPicking) || defined(dColorType_instancePicking) || defined(dColorType_groupPicking)
-    vec4 material = vColor;
+    vec4 material = uPickable == 1 ? vColor : vec4(0.0, 0.0, 0.0, 1.0); // set to empty picking id
 #endif

+ 3 - 1
src/mol-gl/shader/chunks/common-frag-params.glsl

@@ -12,4 +12,6 @@ uniform float uFogNear;
 uniform float uFogFar;
 uniform vec3 uFogColor;
 
-uniform float uAlpha;
+uniform float uAlpha;
+uniform float uPickingAlphaThreshold;
+uniform int uPickable;

+ 5 - 0
src/mol-gl/shader/direct-volume.frag

@@ -123,6 +123,11 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 viewDir) {
                 tmp = ((prevValue - uIsoValue) / ((prevValue - uIsoValue) - (value - uIsoValue)));
                 isoPos = mix(pos - step, pos, tmp);
 
+                #if defined(dColorType_objectPicking) || defined(dColorType_instancePicking) || defined(dColorType_groupPicking)
+                    if (uAlpha < uPickingAlphaThreshold)
+                        discard; // ignore so the element below can be picked
+                #else
+
                 #if defined(dColorType_objectPicking)
                     return vec4(encodeIdRGB(float(uObjectId)), 1.0);
                 #elif defined(dColorType_instancePicking)

+ 2 - 0
src/mol-gl/shader/lines.frag

@@ -14,6 +14,8 @@ void main(){
     #pragma glslify: import('./chunks/assign-material-color.glsl')
 
     #if defined(dColorType_objectPicking) || defined(dColorType_instancePicking) || defined(dColorType_groupPicking)
+        if (uAlpha < uPickingAlphaThreshold)
+            discard; // ignore so the element below can be picked
         gl_FragColor = material;
     #else
         gl_FragColor = material;

+ 2 - 1
src/mol-gl/shader/mesh.frag

@@ -33,7 +33,8 @@ void main() {
     #pragma glslify: import('./chunks/assign-material-color.glsl')
 
     #if defined(dColorType_objectPicking) || defined(dColorType_instancePicking) || defined(dColorType_groupPicking)
-        // gl_FragColor = vec4(material.r, material.g, material.a, 1.0);
+        if (uAlpha < uPickingAlphaThreshold)
+            discard; // ignore so the element below can be picked
         gl_FragColor = material;
     #else
         // determine surface to light direction

+ 2 - 0
src/mol-gl/shader/points.frag

@@ -21,6 +21,8 @@ void main(){
     #pragma glslify: import('./chunks/assign-material-color.glsl')
 
     #if defined(dColorType_objectPicking) || defined(dColorType_instancePicking) || defined(dColorType_groupPicking)
+        if (uAlpha < uPickingAlphaThreshold)
+            discard; // ignore so the element below can be picked
         gl_FragColor = material;
     #else
         gl_FragColor = material;

+ 1 - 0
src/mol-math/geometry/gaussian-density/gpu.ts

@@ -232,6 +232,7 @@ function getGaussianDensityRenderObject(webgl: WebGLContext, drawCount: number,
     }
     const state: RenderableState = {
         visible: true,
+        pickable: false,
         depthMask: false
     }
 

+ 27 - 5
src/mol-model/structure/structure/carbohydrates/compute.ts

@@ -262,8 +262,9 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
 
     }
 
-    // get carbohydrate links induced by inter-unit bonds
-    // (e.g. for structures from the PDB archive __before__ carbohydrate remediation)
+    // get carbohydrate links induced by inter-unit bonds, that is
+    // terminal links plus inter monosaccharide links for structures from the
+    // PDB archive __before__ carbohydrate remediation
     for (let i = 0, il = structure.units.length; i < il; ++i) {
         const unit = structure.units[i]
         if (!Unit.isAtomic(unit)) continue
@@ -309,10 +310,10 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
         })
     }
 
-    return { links, terminalLinks, elements, partialElements, ...buildLookups(elements, links) }
+    return { links, terminalLinks, elements, partialElements, ...buildLookups(elements, links, terminalLinks) }
 }
 
-function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[]) {
+function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[], terminalLinks: CarbohydrateTerminalLink[]) {
     // element lookup
 
     function elementKey(unit: Unit, anomericCarbon: ElementIndex) {
@@ -347,6 +348,27 @@ function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[
         return linkMap.get(linkKey(unitA, anomericCarbonA, unitB, anomericCarbonB))
     }
 
+    // terminal link lookup
+
+    function terminalLinkKey(unitA: Unit, elementA: ElementIndex, unitB: Unit, elementB: ElementIndex) {
+        return `${unitA.id}|${elementA}|${unitB.id}|${elementB}`
+    }
+
+    const terminalLinkMap = new Map<string, number>()
+    for (let i = 0, il = terminalLinks.length; i < il; ++i) {
+        const { fromCarbohydrate, carbohydrateIndex, elementUnit, elementIndex } = terminalLinks[i]
+        const { unit, anomericCarbon } = elements[carbohydrateIndex]
+        if (fromCarbohydrate) {
+            terminalLinkMap.set(terminalLinkKey(unit, anomericCarbon, elementUnit, elementUnit.elements[elementIndex]), i)
+        } else {
+            terminalLinkMap.set(terminalLinkKey(elementUnit, elementUnit.elements[elementIndex], unit, anomericCarbon), i)
+        }
+    }
+
+    function getTerminalLinkIndex(unitA: Unit, elementA: ElementIndex, unitB: Unit, elementB: ElementIndex) {
+        return terminalLinkMap.get(terminalLinkKey(unitA, elementA, unitB, elementB))
+    }
+
     // anomeric carbon lookup
 
     function anomericCarbonKey(unit: Unit, residueIndex: ResidueIndex) {
@@ -364,5 +386,5 @@ function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[
         return anomericCarbonMap.get(anomericCarbonKey(unit, residueIndex))
     }
 
-    return { getElementIndex, getLinkIndex, getAnomericCarbon }
+    return { getElementIndex, getLinkIndex, getTerminalLinkIndex, getAnomericCarbon }
 }

+ 4 - 2
src/mol-model/structure/structure/carbohydrates/data.ts

@@ -8,6 +8,7 @@ import Unit from '../unit';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { ResidueIndex, ElementIndex } from '../../model';
 import { SaccharideComponent } from './constants';
+import StructureElement from '../element';
 
 export interface CarbohydrateLink {
     readonly carbohydrateIndexA: number
@@ -16,7 +17,7 @@ export interface CarbohydrateLink {
 
 export interface CarbohydrateTerminalLink {
     readonly carbohydrateIndex: number
-    readonly elementIndex: number
+    readonly elementIndex: StructureElement.UnitIndex
     readonly elementUnit: Unit
     /** specifies direction of the link */
     readonly fromCarbohydrate: boolean
@@ -31,7 +32,7 @@ export interface CarbohydrateElement {
     readonly ringAltId: string,
 }
 
-// partial carbohydrate with no ring present
+/** partial carbohydrate with no ring present */
 export interface PartialCarbohydrateElement {
     readonly unit: Unit.Atomic,
     readonly residueIndex: ResidueIndex,
@@ -45,5 +46,6 @@ export interface Carbohydrates {
     partialElements: ReadonlyArray<PartialCarbohydrateElement>
     getElementIndex: (unit: Unit, anomericCarbon: ElementIndex) => number | undefined
     getLinkIndex: (unitA: Unit, anomericCarbonA: ElementIndex, unitB: Unit, anomericCarbonB: ElementIndex) => number | undefined
+    getTerminalLinkIndex: (unitA: Unit, elementA: ElementIndex, unitB: Unit, elementB: ElementIndex) => number | undefined
     getAnomericCarbon: (unit: Unit, residueIndex: ResidueIndex) => ElementIndex | undefined
 }

+ 12 - 0
src/mol-plugin/ui/controls/parameters.tsx

@@ -44,6 +44,7 @@ function controlFor(param: PD.Any): ParamControl | undefined {
         case 'converted': return ConvertedControl;
         case 'multi-select': return MultiSelectControl;
         case 'color': return ColorControl;
+        case 'vec3': return Vec3Control;
         case 'select': return SelectControl;
         case 'text': return TextControl;
         case 'interval': return IntervalControl;
@@ -166,6 +167,17 @@ export class ColorControl extends SimpleParam<PD.Color> {
     }
 }
 
+export class Vec3Control extends SimpleParam<PD.Vec3> {
+    // onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
+    //     this.setState({ value: e.target.value });
+    //     this.props.onChange(e.target.value);
+    // }
+
+    renderControl() {
+        return <span>vec3 TODO</span>;
+    }
+}
+
 export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiSelect<any>>, { isExpanded: boolean }> {
     state = { isExpanded: false }
 

+ 14 - 5
src/mol-repr/representation.ts

@@ -15,7 +15,7 @@ import { getQualityProps } from './util';
 import { ColorTheme } from 'mol-theme/color';
 import { SizeTheme } from 'mol-theme/size';
 import { Theme, ThemeRegistryContext } from 'mol-theme/theme';
-import { BehaviorSubject } from 'rxjs';
+import { Subject } from 'rxjs';
 
 // export interface RepresentationProps {
 //     visuals?: string[]
@@ -80,7 +80,7 @@ export interface RepresentationContext {
 export { Representation }
 interface Representation<D, P extends PD.Params = {}> {
     readonly label: string
-    readonly updated: BehaviorSubject<number>
+    readonly updated: Subject<number>
     readonly renderObjects: ReadonlyArray<RenderObject>
     readonly props: Readonly<PD.Values<P>>
     readonly params: Readonly<P>
@@ -88,23 +88,26 @@ interface Representation<D, P extends PD.Params = {}> {
     getLoci: (pickingId: PickingId) => Loci
     mark: (loci: Loci, action: MarkerAction) => boolean
     setVisibility: (value: boolean) => void
+    setPickable: (value: boolean) => void
     destroy: () => void
 }
 namespace Representation {
     export type Any = Representation<any>
     export const Empty: Any = {
-        label: '', renderObjects: [], props: {}, params: {}, updated: new BehaviorSubject(0),
+        label: '', renderObjects: [], props: {}, params: {}, updated: new Subject(),
         createOrUpdate: () => Task.constant('', undefined),
         getLoci: () => EmptyLoci,
         mark: () => false,
         setVisibility: () => {},
+        setPickable: () => {},
         destroy: () => {}
     }
 
     export type Def<D, P extends PD.Params = {}> = { [k: string]: (getParams: RepresentationParamsGetter<D, P>) => Representation<any, P> }
 
     export function createMulti<D, P extends PD.Params = {}>(label: string, getParams: RepresentationParamsGetter<D, P>, reprDefs: Def<D, P>): Representation<D, P> {
-        const updated = new BehaviorSubject(0)
+        let version = 0
+        const updated = new Subject<number>()
 
         let currentParams: P
         let currentProps: PD.Values<P>
@@ -153,7 +156,7 @@ namespace Representation {
                             await reprList[i].createOrUpdate(ctx, currentProps, currentData).runInContext(runtime)
                         }
                     }
-                    updated.next(updated.getValue() + 1)
+                    updated.next(version++)
                 })
             },
             getLoci: (pickingId: PickingId) => {
@@ -175,6 +178,11 @@ namespace Representation {
                     reprList[i].setVisibility(value)
                 }
             },
+            setPickable: (value: boolean) => {
+                for (let i = 0, il = reprList.length; i < il; ++i) {
+                    reprList[i].setPickable(value)
+                }
+            },
             destroy() {
                 for (let i = 0, il = reprList.length; i < il; ++i) {
                     reprList[i].destroy()
@@ -197,5 +205,6 @@ export interface Visual<D, P extends PD.Params> {
     getLoci: (pickingId: PickingId) => Loci
     mark: (loci: Loci, action: MarkerAction) => boolean
     setVisibility: (value: boolean) => void
+    setPickable: (value: boolean) => void
     destroy: () => void
 }

+ 7 - 3
src/mol-repr/shape/representation.ts

@@ -19,7 +19,7 @@ import { PickingId } from 'mol-geo/geometry/picking';
 import { MarkerAction, applyMarkerAction } from 'mol-geo/geometry/marker-data';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
 import { createTheme } from 'mol-theme/theme';
-import { BehaviorSubject } from 'rxjs';
+import { Subject } from 'rxjs';
 
 export interface ShapeRepresentation<P extends ShapeParams> extends Representation<Shape, P> { }
 
@@ -31,7 +31,8 @@ export const ShapeParams = {
 export type ShapeParams = typeof ShapeParams
 
 export function ShapeRepresentation<P extends ShapeParams>(): ShapeRepresentation<P> {
-    const updated = new BehaviorSubject(0)
+    let version = 0
+    const updated = new Subject<number>()
     const renderObjects: RenderObject[] = []
     let _renderObject: MeshRenderObject | undefined
     let _shape: Shape
@@ -57,7 +58,7 @@ export function ShapeRepresentation<P extends ShapeParams>(): ShapeRepresentatio
 
             _renderObject = createMeshRenderObject(values, state)
             renderObjects.push(_renderObject)
-            updated.next(updated.getValue() + 1)
+            updated.next(version++)
         });
     }
 
@@ -103,6 +104,9 @@ export function ShapeRepresentation<P extends ShapeParams>(): ShapeRepresentatio
         setVisibility(value: boolean) {
             renderObjects.forEach(ro => ro.state.visible = value)
         },
+        setPickable(value: boolean) {
+            renderObjects.forEach(ro => ro.state.pickable = value)
+        },
         destroy() {
             // TODO
             renderObjects.length = 0

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

@@ -15,10 +15,11 @@ import { MarkerAction } from 'mol-geo/geometry/marker-data';
 import { RepresentationContext, RepresentationParamsGetter } from 'mol-repr/representation';
 import { Theme, createTheme } from 'mol-theme/theme';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { BehaviorSubject } from 'rxjs';
+import { Subject } from 'rxjs';
 
 export function ComplexRepresentation<P extends StructureParams>(label: string, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> {
-    const updated = new BehaviorSubject(0)
+    let version = 0
+    const updated = new Subject<number>()
     let visual: ComplexVisual<P> | undefined
 
     let _structure: Structure
@@ -38,7 +39,7 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         return Task.create('Creating or updating ComplexRepresentation', async runtime => {
             if (!visual) visual = visualCtor()
             await visual.createOrUpdate({ ...ctx, runtime }, _theme, _props, structure)
-            updated.next(updated.getValue() + 1)
+            updated.next(version++)
         });
     }
 
@@ -54,6 +55,10 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         if (visual) visual.setVisibility(value)
     }
 
+    function setPickable(value: boolean) {
+        if (visual) visual.setPickable(value)
+    }
+
     function destroy() {
         if (visual) visual.destroy()
     }
@@ -70,6 +75,7 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         getLoci,
         mark,
         setVisibility,
+        setPickable,
         destroy
     }
 }

+ 4 - 1
src/mol-repr/structure/complex-visual.ts

@@ -165,6 +165,9 @@ export function ComplexVisual<P extends ComplexParams>(builder: ComplexVisualGeo
         setVisibility(value: boolean) {
             if (renderObject) renderObject.state.visible = value
         },
+        setPickable(value: boolean) {
+            if (renderObject) renderObject.state.pickable = value
+        },
         destroy() {
             // TODO
             renderObject = undefined
@@ -187,7 +190,7 @@ export function ComplexMeshVisual<P extends ComplexMeshParams>(builder: ComplexM
         ...builder,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
             builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme)
-            if (SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true
+            if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true
         },
         createEmptyGeometry: Mesh.createEmpty,
         createRenderObject: createComplexMeshRenderObject,

+ 4 - 1
src/mol-repr/structure/representation/carbohydrate.ts

@@ -6,6 +6,7 @@
 
 import { CarbohydrateSymbolVisual, CarbohydrateSymbolParams } from '../visual/carbohydrate-symbol-mesh';
 import { CarbohydrateLinkVisual, CarbohydrateLinkParams } from '../visual/carbohydrate-link-cylinder';
+import { CarbohydrateTerminalLinkParams, CarbohydrateTerminalLinkVisual } from '../visual/carbohydrate-terminal-link-cylinder';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ComplexRepresentation } from '../complex-representation';
 import { StructureRepresentation, StructureRepresentationProvider } from '../representation';
@@ -17,6 +18,7 @@ import { BuiltInColorThemeOptions, getBuiltInColorThemeParams } from 'mol-theme/
 const CarbohydrateVisuals = {
     'carbohydrate-symbol': (getParams: RepresentationParamsGetter<Structure, CarbohydrateSymbolParams>) => ComplexRepresentation('Carbohydrate symbol mesh', getParams, CarbohydrateSymbolVisual),
     'carbohydrate-link': (getParams: RepresentationParamsGetter<Structure, CarbohydrateLinkParams>) => ComplexRepresentation('Carbohydrate link cylinder', getParams, CarbohydrateLinkVisual),
+    'carbohydrate-terminal-link': (getParams: RepresentationParamsGetter<Structure, CarbohydrateTerminalLinkParams>) => ComplexRepresentation('Carbohydrate terminal link cylinder', getParams, CarbohydrateTerminalLinkVisual),
 }
 type CarbohydrateVisualName = keyof typeof CarbohydrateVisuals
 const CarbohydrateVisualOptions = Object.keys(CarbohydrateVisuals).map(name => [name, name] as [CarbohydrateVisualName, string])
@@ -24,8 +26,9 @@ const CarbohydrateVisualOptions = Object.keys(CarbohydrateVisuals).map(name => [
 export const CarbohydrateParams = {
     ...CarbohydrateSymbolParams,
     ...CarbohydrateLinkParams,
+    ...CarbohydrateTerminalLinkParams,
     colorTheme: PD.Mapped('carbohydrate-symbol', BuiltInColorThemeOptions, getBuiltInColorThemeParams),
-    visuals: PD.MultiSelect<CarbohydrateVisualName>(['carbohydrate-symbol', 'carbohydrate-link'], CarbohydrateVisualOptions),
+    visuals: PD.MultiSelect<CarbohydrateVisualName>(['carbohydrate-symbol', 'carbohydrate-link', 'carbohydrate-terminal-link'], CarbohydrateVisualOptions),
 }
 PD.getDefaultValues(CarbohydrateParams).colorTheme.name
 export type CarbohydrateParams = typeof CarbohydrateParams

+ 12 - 4
src/mol-repr/structure/units-representation.ts

@@ -17,7 +17,7 @@ import { MarkerAction } from 'mol-geo/geometry/marker-data';
 import { Theme, createTheme } from 'mol-theme/theme';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { UnitKind, UnitKindOptions } from './visual/util/common';
-import { BehaviorSubject } from 'rxjs';
+import { Subject } from 'rxjs';
 
 export const UnitsParams = {
     ...StructureParams,
@@ -28,7 +28,8 @@ export type UnitsParams = typeof UnitsParams
 export interface UnitsVisual<P extends UnitsParams> extends Visual<StructureGroup, P> { }
 
 export function UnitsRepresentation<P extends UnitsParams>(label: string, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> {
-    const updated = new BehaviorSubject(0)
+    let version = 0
+    const updated = new Subject<number>()
     let visuals = new Map<number, { group: Unit.SymmetryGroup, visual: UnitsVisual<P> }>()
 
     let _structure: Structure
@@ -120,7 +121,7 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, getPar
                 }
             }
             if (structure) _structure = structure
-            updated.next(updated.getValue() + 1)
+            updated.next(version++)
         });
     }
 
@@ -147,6 +148,12 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, getPar
         })
     }
 
+    function setPickable(value: boolean) {
+        visuals.forEach(({ visual }) => {
+            visual.setPickable(value)
+        })
+    }
+
     function destroy() {
         visuals.forEach(({ visual }) => visual.destroy())
         visuals.clear()
@@ -163,11 +170,12 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, getPar
         },
         get props() { return _props },
         get params() { return _params },
-        get updated() { return updated },
+        updated,
         createOrUpdate,
         getLoci,
         mark,
         setVisibility,
+        setPickable,
         destroy
     }
 }

+ 7 - 4
src/mol-repr/structure/units-visual.ts

@@ -195,6 +195,9 @@ export function UnitsVisual<P extends UnitsParams>(builder: UnitsVisualGeometryB
         setVisibility(value: boolean) {
             if (renderObject) renderObject.state.visible = value
         },
+        setPickable(value: boolean) {
+            if (renderObject) renderObject.state.pickable = value
+        },
         destroy() {
             // TODO
             renderObject = undefined
@@ -216,7 +219,7 @@ export function UnitsMeshVisual<P extends UnitsMeshParams>(builder: UnitsMeshVis
         ...builder,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
             builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme)
-            if (SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true
+            if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true
         },
         createEmptyGeometry: Mesh.createEmpty,
         createRenderObject: createUnitsMeshRenderObject,
@@ -240,7 +243,7 @@ export function UnitsPointsVisual<P extends UnitsPointsParams>(builder: UnitsPoi
         createRenderObject: createUnitsPointsRenderObject,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
             builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme)
-            if (SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true
+            if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true
         },
         updateValues: Points.updateValues
     })
@@ -262,7 +265,7 @@ export function UnitsLinesVisual<P extends UnitsLinesParams>(builder: UnitsLines
         createRenderObject: createUnitsLinesRenderObject,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
             builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme)
-            if (SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true
+            if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true
         },
         updateValues: Lines.updateValues
     })
@@ -284,7 +287,7 @@ export function UnitsDirectVolumeVisual<P extends UnitsDirectVolumeParams>(build
         createRenderObject: createUnitsDirectVolumeRenderObject,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
             builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme)
-            if (SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true
+            if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true
         },
         updateValues: DirectVolume.updateValues
     })

+ 6 - 17
src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts

@@ -7,7 +7,7 @@
 import { Structure, Link, StructureElement } from 'mol-model/structure';
 import { Loci, EmptyLoci } from 'mol-model/loci';
 import { Vec3 } from 'mol-math/linear-algebra';
-import { createLinkCylinderMesh, LinkCylinderProps, LinkCylinderParams } from './util/link';
+import { createLinkCylinderMesh, LinkCylinderParams } from './util/link';
 import { OrderedSet, Interval } from 'mol-data/int';
 import { ComplexMeshVisual, ComplexVisual } from '../complex-visual';
 import { LinkType } from 'mol-model/structure/model/types';
@@ -21,22 +21,10 @@ import { VisualUpdateState } from '../../util';
 import { VisualContext } from 'mol-repr/representation';
 import { Theme } from 'mol-theme/theme';
 
-// TODO create seperate visual
-// for (let i = 0, il = carbohydrates.terminalLinks.length; i < il; ++i) {
-//     const tl = carbohydrates.terminalLinks[i]
-//     const center = carbohydrates.elements[tl.carbohydrateIndex].geometry.center
-//     tl.elementUnit.conformation.position(tl.elementUnit.elements[tl.elementIndex], p)
-//     if (tl.fromCarbohydrate) {
-//         builder.addCylinder(center, p, 0.5, linkParams)
-//     } else {
-//         builder.addCylinder(p, center, 0.5, linkParams)
-//     }
-// }
-
-const radiusFactor = 0.3
-
-async function createCarbohydrateLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: LinkCylinderProps, mesh?: Mesh) {
+async function createCarbohydrateLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CarbohydrateLinkParams>, mesh?: Mesh) {
     const { links, elements } = structure.carbohydrates
+    const { linkSizeFactor } = props
+
     const location = StructureElement.create()
 
     const builderProps = {
@@ -53,7 +41,7 @@ async function createCarbohydrateLinkCylinderMesh(ctx: VisualContext, structure:
             const l = links[edgeIndex]
             location.unit = elements[l.carbohydrateIndexA].unit
             location.element = elements[l.carbohydrateIndexA].anomericCarbon
-            return theme.size.size(location) * radiusFactor
+            return theme.size.size(location) * linkSizeFactor
         }
     }
 
@@ -64,6 +52,7 @@ export const CarbohydrateLinkParams = {
     ...UnitsMeshParams,
     ...LinkCylinderParams,
     detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }),
+    linkSizeFactor: PD.Numeric(0.3, { min: 0, max: 3, step: 0.01 }),
 }
 export type CarbohydrateLinkParams = typeof CarbohydrateLinkParams
 

+ 5 - 5
src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts

@@ -30,8 +30,7 @@ const t = Mat4.identity()
 const sVec = Vec3.zero()
 const pd = Vec3.zero()
 
-const sideFactor = 1.75 * 2 * 0.806; // 0.806 == Math.cos(Math.PI / 4)
-const radiusFactor = 1.75
+const SideFactor = 2 * 0.806; // 0.806 == Math.cos(Math.PI / 4)
 
 const box = Box()
 const perforatedBox = PerforatedBox()
@@ -47,7 +46,7 @@ const hexagonalPrism = HexagonalPrism()
 async function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CarbohydrateSymbolParams>, mesh?: Mesh) {
     const builder = MeshBuilder.create(256, 128, mesh)
 
-    const { detail } = props
+    const { detail, sizeFactor } = props
 
     const carbohydrates = structure.carbohydrates
     const n = carbohydrates.elements.length
@@ -60,8 +59,8 @@ async function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Struc
         l.unit = c.unit
         l.element = c.unit.elements[c.anomericCarbon]
         const size = theme.size.size(l)
-        const radius = size * radiusFactor
-        const side = size * sideFactor
+        const radius = size * sizeFactor
+        const side = size * sizeFactor * SideFactor
 
         const { center, normal, direction } = c.geometry
         Vec3.add(pd, center, direction)
@@ -148,6 +147,7 @@ async function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Struc
 export const CarbohydrateSymbolParams = {
     ...ComplexMeshParams,
     detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }),
+    sizeFactor: PD.Numeric(1.75, { min: 0, max: 10, step: 0.01 }),
 }
 export type CarbohydrateSymbolParams = typeof CarbohydrateSymbolParams
 

+ 147 - 0
src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts

@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Structure, Link, StructureElement } from 'mol-model/structure';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { createLinkCylinderMesh, LinkCylinderParams } from './util/link';
+import { OrderedSet, Interval } from 'mol-data/int';
+import { ComplexMeshVisual, ComplexVisual } from '../complex-visual';
+import { LinkType } from 'mol-model/structure/model/types';
+import { BitFlags } from 'mol-util';
+import { UnitsMeshParams } from '../units-visual';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { Mesh } from 'mol-geo/geometry/mesh/mesh';
+import { LocationIterator } from 'mol-geo/util/location-iterator';
+import { PickingId } from 'mol-geo/geometry/picking';
+import { VisualUpdateState } from '../../util';
+import { VisualContext } from 'mol-repr/representation';
+import { Theme } from 'mol-theme/theme';
+
+async function createCarbohydrateTerminalLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CarbohydrateTerminalLinkParams>, mesh?: Mesh) {
+    const { terminalLinks, elements } = structure.carbohydrates
+    const { linkSizeFactor } = props
+
+    const location = StructureElement.create()
+
+    const builderProps = {
+        linkCount: terminalLinks.length,
+        referencePosition: (edgeIndex: number) => null,
+        position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
+            const l = terminalLinks[edgeIndex]
+            if (l.fromCarbohydrate) {
+                Vec3.copy(posA, elements[l.carbohydrateIndex].geometry.center)
+                l.elementUnit.conformation.position(l.elementIndex, posB)
+            } else {
+                l.elementUnit.conformation.position(l.elementIndex, posA)
+                Vec3.copy(posB, elements[l.carbohydrateIndex].geometry.center)
+            }
+        },
+        order: (edgeIndex: number) => 1,
+        flags: (edgeIndex: number) => BitFlags.create(LinkType.Flag.None),
+        radius: (edgeIndex: number) => {
+            const l = terminalLinks[edgeIndex]
+            if (l.fromCarbohydrate) {
+                location.unit = elements[l.carbohydrateIndex].unit
+                location.element = elements[l.carbohydrateIndex].anomericCarbon
+            } else {
+                location.unit = l.elementUnit
+                location.element = l.elementUnit.elements[l.elementIndex]
+            }
+            return theme.size.size(location) * linkSizeFactor
+        }
+    }
+
+    return createLinkCylinderMesh(ctx, builderProps, props, mesh)
+}
+
+export const CarbohydrateTerminalLinkParams = {
+    ...UnitsMeshParams,
+    ...LinkCylinderParams,
+    detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }),
+    linkSizeFactor: PD.Numeric(0.3, { min: 0, max: 3, step: 0.01 }),
+}
+export type CarbohydrateTerminalLinkParams = typeof CarbohydrateTerminalLinkParams
+
+export function CarbohydrateTerminalLinkVisual(): ComplexVisual<CarbohydrateTerminalLinkParams> {
+    return ComplexMeshVisual<CarbohydrateTerminalLinkParams>({
+        defaultProps: PD.getDefaultValues(CarbohydrateTerminalLinkParams),
+        createGeometry: createCarbohydrateTerminalLinkCylinderMesh,
+        createLocationIterator: CarbohydrateTerminalLinkIterator,
+        getLoci: getTerminalLinkLoci,
+        mark: markTerminalLink,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CarbohydrateTerminalLinkParams>, currentProps: PD.Values<CarbohydrateTerminalLinkParams>) => {
+            state.createGeometry = newProps.radialSegments !== currentProps.radialSegments
+        }
+    })
+}
+
+function CarbohydrateTerminalLinkIterator(structure: Structure): LocationIterator {
+    const { elements, terminalLinks } = structure.carbohydrates
+    const groupCount = terminalLinks.length
+    const instanceCount = 1
+    const location = Link.Location()
+    const getLocation = (groupIndex: number) => {
+        const terminalLink = terminalLinks[groupIndex]
+        const carb = elements[terminalLink.carbohydrateIndex]
+        const indexCarb = OrderedSet.indexOf(carb.unit.elements, carb.anomericCarbon)
+        if (terminalLink.fromCarbohydrate) {
+            location.aUnit = carb.unit
+            location.aIndex = indexCarb as StructureElement.UnitIndex
+            location.bUnit = terminalLink.elementUnit
+            location.bIndex = terminalLink.elementIndex
+        } else {
+            location.aUnit = terminalLink.elementUnit
+            location.aIndex = terminalLink.elementIndex
+            location.bUnit = carb.unit
+            location.bIndex = indexCarb as StructureElement.UnitIndex
+        }
+        return location
+    }
+    return LocationIterator(groupCount, instanceCount, getLocation, true)
+}
+
+function getTerminalLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
+    const { objectId, groupId } = pickingId
+    if (id === objectId) {
+        const { terminalLinks, elements } = structure.carbohydrates
+        const l = terminalLinks[groupId]
+        const carb = elements[l.carbohydrateIndex]
+        const carbIndex = OrderedSet.indexOf(carb.unit.elements, carb.anomericCarbon)
+
+        if (l.fromCarbohydrate) {
+            return Link.Loci(structure, [
+                Link.Location(
+                    carb.unit, carbIndex as StructureElement.UnitIndex,
+                    l.elementUnit, l.elementIndex
+                )
+            ])
+        } else {
+            return Link.Loci(structure, [
+                Link.Location(
+                    l.elementUnit, l.elementIndex,
+                    carb.unit, carbIndex as StructureElement.UnitIndex
+                )
+            ])
+        }
+    }
+    return EmptyLoci
+}
+
+function markTerminalLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
+    const { getTerminalLinkIndex } = structure.carbohydrates
+
+    let changed = false
+    if (Link.isLoci(loci)) {
+        for (const l of loci.links) {
+            const idx = getTerminalLinkIndex(l.aUnit, l.aUnit.elements[l.aIndex], l.bUnit, l.bUnit.elements[l.bIndex])
+            if (idx !== undefined) {
+                if (apply(Interval.ofSingleton(idx))) changed = true
+            }
+        }
+    }
+    return changed
+}

+ 5 - 3
src/mol-repr/structure/visual/inter-unit-link-cylinder.ts

@@ -7,7 +7,7 @@
 import { Link, Structure, StructureElement } from 'mol-model/structure';
 import { ComplexVisual } from '../representation';
 import { VisualUpdateState } from '../../util';
-import { LinkCylinderProps, createLinkCylinderMesh, LinkIterator, LinkCylinderParams } from './util/link';
+import { createLinkCylinderMesh, LinkIterator, LinkCylinderParams } from './util/link';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { Loci, EmptyLoci } from 'mol-model/loci';
 import { ComplexMeshVisual, ComplexMeshParams } from '../complex-visual';
@@ -19,9 +19,10 @@ import { PickingId } from 'mol-geo/geometry/picking';
 import { VisualContext } from 'mol-repr/representation';
 import { Theme } from 'mol-theme/theme';
 
-async function createInterUnitLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: LinkCylinderProps, mesh?: Mesh) {
+async function createInterUnitLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitLinkParams>, mesh?: Mesh) {
     const links = structure.links
     const { bondCount, bonds } = links
+    const { sizeFactor } = props
 
     if (!bondCount) return Mesh.createEmpty(mesh)
 
@@ -42,7 +43,7 @@ async function createInterUnitLinkCylinderMesh(ctx: VisualContext, structure: St
             const b = bonds[edgeIndex]
             location.unit = b.unitA
             location.element = b.unitA.elements[b.indexA]
-            return theme.size.size(location)
+            return theme.size.size(location) * sizeFactor
         }
     }
 
@@ -52,6 +53,7 @@ async function createInterUnitLinkCylinderMesh(ctx: VisualContext, structure: St
 export const InterUnitLinkParams = {
     ...ComplexMeshParams,
     ...LinkCylinderParams,
+    sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
 }
 export type InterUnitLinkParams = typeof InterUnitLinkParams
 

+ 13 - 4
src/mol-repr/volume/representation.ts

@@ -20,7 +20,7 @@ import { NullLocation } from 'mol-model/location';
 import { VisualUpdateState } from 'mol-repr/util';
 import { ValueCell } from 'mol-util';
 import { Theme, createTheme } from 'mol-theme/theme';
-import { BehaviorSubject } from 'rxjs';
+import { Subject } from 'rxjs';
 
 export interface VolumeVisual<P extends VolumeParams> extends Visual<VolumeData, P> { }
 
@@ -39,7 +39,7 @@ interface VolumeVisualGeometryBuilder<P extends VolumeParams, G extends Geometry
     updateValues(values: RenderableValues, newProps: PD.Values<P>): void
 }
 
-export function VolumeVisual<P extends VolumeParams>(builder: VolumeVisualGeometryBuilder<P, Geometry>) {
+export function VolumeVisual<P extends VolumeParams>(builder: VolumeVisualGeometryBuilder<P, Geometry>): VolumeVisual<P> {
     const { defaultProps, createGeometry, getLoci, mark, setUpdateState } = builder
     const { createRenderObject, updateValues } = builder
     const updateState = VisualUpdateState.create()
@@ -120,6 +120,9 @@ export function VolumeVisual<P extends VolumeParams>(builder: VolumeVisualGeomet
         setVisibility(value: boolean) {
             if (renderObject) renderObject.state.visible = value
         },
+        setPickable(value: boolean) {
+            if (renderObject) renderObject.state.pickable = value
+        },
         destroy() {
             // TODO
             renderObject = undefined
@@ -140,7 +143,8 @@ export const VolumeParams = {
 export type VolumeParams = typeof VolumeParams
 
 export function VolumeRepresentation<P extends VolumeParams>(label: string, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: (volume: VolumeData) => VolumeVisual<P>): VolumeRepresentation<P> {
-    const updated = new BehaviorSubject(0)
+    let version = 0
+    const updated = new Subject<number>()
     let visual: VolumeVisual<P>
 
     let _volume: VolumeData
@@ -174,7 +178,7 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, getP
                 await visual.createOrUpdate({ ...ctx, runtime }, _theme, _props, volume)
                 busy = false
             }
-            updated.next(updated.getValue() + 1)
+            updated.next(version++)
         });
     }
 
@@ -194,6 +198,10 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, getP
         if (visual) visual.setVisibility(value)
     }
 
+    function setPickable(value: boolean) {
+        if (visual) visual.setPickable(value)
+    }
+
     return {
         label,
         get renderObjects() {
@@ -206,6 +214,7 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, getP
         getLoci,
         mark,
         setVisibility,
+        setPickable,
         destroy
     }
 }

+ 14 - 5
src/mol-util/param-definition.ts

@@ -7,7 +7,7 @@
 
 import { Color as ColorData } from './color';
 import { shallowEqual } from 'mol-util';
-import { Vec2 } from 'mol-math/linear-algebra';
+import { Vec2 as Vec2Data, Vec3 as Vec3Data } from 'mol-math/linear-algebra';
 import { deepClone } from './object';
 
 export namespace ParamDefinition {
@@ -75,6 +75,13 @@ export namespace ParamDefinition {
         return setInfo<Color>({ type: 'color', defaultValue }, info)
     }
 
+    export interface Vec3 extends Base<Vec3Data> {
+        type: 'vec3'
+    }
+    export function Vec3(defaultValue: Vec3Data, info?: Info): Vec3 {
+        return setInfo<Vec3>({ type: 'vec3', defaultValue }, info)
+    }
+
     export interface Range {
         /** If given treat as a range. */
         min?: number
@@ -108,10 +115,10 @@ export namespace ParamDefinition {
         return setInfo<Interval>(setRange({ type: 'interval', defaultValue }, range), info)
     }
 
-    export interface LineGraph extends Base<Vec2[]> {
+    export interface LineGraph extends Base<Vec2Data[]> {
         type: 'line-graph'
     }
-    export function LineGraph(defaultValue: Vec2[], info?: Info): LineGraph {
+    export function LineGraph(defaultValue: Vec2Data[], info?: Info): LineGraph {
         return setInfo<LineGraph>({ type: 'line-graph', defaultValue }, info)
     }
 
@@ -149,7 +156,7 @@ export namespace ParamDefinition {
         return { type: 'converted', defaultValue: toValue(converted.defaultValue), converted, fromValue, toValue };
     }
 
-    export type Any = Value<any> | Select<any> | MultiSelect<any> | Boolean | Text | Color | Numeric | Interval | LineGraph | Group<any> | Mapped<any> | Converted<any, any>
+    export type Any = Value<any> | Select<any> | MultiSelect<any> | Boolean | Text | Color | Vec3 | Numeric | Interval | LineGraph | Group<any> | Mapped<any> | Converted<any, any>
 
     export type Params = { [k: string]: Any }
     export type Values<T extends Params> = { [k in keyof T]: T[k]['defaultValue'] }
@@ -210,9 +217,11 @@ export namespace ParamDefinition {
             const u = a as LineGraph['defaultValue'], v = b as LineGraph['defaultValue'];
             if (u.length !== v.length) return false;
             for (let i = 0, _i = u.length; i < _i; i++) {
-                if (!Vec2.areEqual(u[i], v[i])) return false;
+                if (!Vec2Data.areEqual(u[i], v[i])) return false;
             }
             return true;
+        } else if (p.type === 'vec3') {
+            return Vec3Data.equals(a, b);
         } else if (typeof a === 'object' && typeof b === 'object') {
             return shallowEqual(a, b);
         }