Browse Source

simplified units-visual builder

Alexander Rose 6 years ago
parent
commit
f68d16b91a
1 changed files with 82 additions and 422 deletions
  1. 82 422
      src/mol-geo/representation/structure/units-visual.ts

+ 82 - 422
src/mol-geo/representation/structure/units-visual.ts

@@ -4,11 +4,9 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-// TODO refactor to make DRY
-
 import { Unit, Structure } from 'mol-model/structure';
 import { RepresentationProps, Visual } from '../';
-import { VisualUpdateState, StructureMeshParams, StructurePointsParams, StructureLinesParams, StructureDirectVolumeParams, StructureProps } from '.';
+import { VisualUpdateState, StructureMeshParams, StructurePointsParams, StructureLinesParams, StructureDirectVolumeParams, StructureProps, StructureParams } from '.';
 import { RuntimeContext } from 'mol-task';
 import { PickingId } from '../../geometry/picking';
 import { LocationIterator } from '../../util/location-iterator';
@@ -26,6 +24,7 @@ import { createSizes, SizeProps } from '../../geometry/size-data';
 import { Lines } from '../../geometry/lines/lines';
 import { MultiSelectParam, paramDefaultValues } from 'mol-view/parameter';
 import { DirectVolume } from '../../geometry/direct-volume/direct-volume';
+import { RenderableValues } from 'mol-gl/renderable/schema';
 
 export const UnitKindInfo = {
     'atomic': {},
@@ -72,8 +71,13 @@ function colorChanged(oldProps: ColorProps, newProps: ColorProps) {
 }
 
 const UnitsParams = {
+    ...StructureParams,
     unitKinds: MultiSelectParam<UnitKind>('Unit Kind', '', ['atomic', 'spheres'], UnitKindOptions),
 }
+const DefaultUnitsProps = paramDefaultValues(UnitsParams)
+type UnitsProps = typeof DefaultUnitsProps
+
+type UnitsRenderObject = MeshRenderObject | LinesRenderObject | PointsRenderObject | DirectVolumeRenderObject
 
 interface UnitsVisualBuilder<P extends StructureProps, G extends Geometry> {
     defaultProps: P
@@ -84,23 +88,20 @@ interface UnitsVisualBuilder<P extends StructureProps, G extends Geometry> {
     setUpdateState(state: VisualUpdateState, newProps: P, currentProps: P): void
 }
 
-// mesh
-
-export const UnitsMeshParams = {
-    ...StructureMeshParams,
-    ...UnitsParams,
+interface UnitsVisualGeometryBuilder<P extends StructureProps, G extends Geometry> extends UnitsVisualBuilder<P, G> {
+    createEmptyGeometry(geometry?: G): G
+    createRenderObject(ctx: RuntimeContext, group: Unit.SymmetryGroup, geometry: Geometry, locationIt: LocationIterator, currentProps: P): Promise<UnitsRenderObject>
+    updateValues(values: RenderableValues, newProps: P): void
 }
-export const DefaultUnitsMeshProps = paramDefaultValues(UnitsMeshParams)
-export type UnitsMeshProps = typeof DefaultUnitsMeshProps
-export interface UnitsMeshVisualBuilder<P extends UnitsMeshProps> extends UnitsVisualBuilder<P, Mesh> { }
 
-export function UnitsMeshVisual<P extends UnitsMeshProps>(builder: UnitsMeshVisualBuilder<P>): UnitsVisual<P> {
+export function UnitsVisual<P extends UnitsProps>(builder: UnitsVisualGeometryBuilder<P, Geometry>): UnitsVisual<P> {
     const { defaultProps, createGeometry, createLocationIterator, getLoci, mark, setUpdateState } = builder
+    const { createEmptyGeometry, createRenderObject, updateValues } = builder
     const updateState = VisualUpdateState.create()
 
-    let renderObject: MeshRenderObject | undefined
+    let renderObject: UnitsRenderObject | undefined
     let currentProps: P
-    let mesh: Mesh
+    let geometry: Geometry
     let currentGroup: Unit.SymmetryGroup
     let currentStructure: Structure
     let locationIt: LocationIterator
@@ -112,13 +113,13 @@ export function UnitsMeshVisual<P extends UnitsMeshProps>(builder: UnitsMeshVisu
 
         const unit = group.units[0]
         currentConformationId = Unit.conformationId(unit)
-        mesh = includesUnitKind(currentProps.unitKinds, unit)
-            ? await createGeometry(ctx, unit, currentStructure, currentProps, mesh)
-            : Mesh.createEmpty(mesh)
+        geometry = includesUnitKind(currentProps.unitKinds, unit)
+            ? await createGeometry(ctx, unit, currentStructure, currentProps, geometry)
+            : createEmptyGeometry(geometry)
 
         // TODO create empty location iterator when not in unitKinds
         locationIt = createLocationIterator(group)
-        renderObject = await createUnitsMeshRenderObject(ctx, group, mesh, locationIt, currentProps)
+        renderObject = await createRenderObject(ctx, group, geometry, locationIt, currentProps)
     }
 
     async function update(ctx: RuntimeContext, props: Partial<P> = {}) {
@@ -139,7 +140,6 @@ export function UnitsMeshVisual<P extends UnitsMeshProps>(builder: UnitsMeshVisu
 
         if (currentGroup.units.length !== locationIt.instanceCount) updateState.updateTransform = true
 
-        if (sizeChanged(currentProps, newProps)) updateState.createGeometry = true
         if (colorChanged(currentProps, newProps)) updateState.updateColor = true
         if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createGeometry = true
 
@@ -154,20 +154,26 @@ export function UnitsMeshVisual<P extends UnitsMeshProps>(builder: UnitsMeshVisu
         }
 
         if (updateState.createGeometry) {
-            mesh = includesUnitKind(newProps.unitKinds, unit)
-                ? await createGeometry(ctx, unit, currentStructure, newProps, mesh)
-                : Mesh.createEmpty(mesh)
-            ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+            geometry = includesUnitKind(newProps.unitKinds, unit)
+                ? await createGeometry(ctx, unit, currentStructure, newProps, geometry)
+                : createEmptyGeometry(geometry)
+            ValueCell.update(renderObject.values.drawCount, Geometry.getDrawCount(geometry))
             updateState.updateColor = true
         }
 
+        if (updateState.updateSize) {
+            // not all geometries have size data, so check here
+            if ('uSize' in renderObject.values) {
+                await createSizes(ctx, locationIt, newProps, renderObject.values)
+            }
+        }
+
         if (updateState.updateColor) {
             await createColors(ctx, locationIt, newProps, renderObject.values)
         }
 
-        // TODO why do I need to cast here?
-        Mesh.updateValues(renderObject.values, newProps as UnitsMeshProps)
-        updateRenderableState(renderObject.state, newProps as UnitsMeshProps)
+        updateValues(renderObject.values, newProps)
+        updateRenderableState(renderObject.state, newProps)
 
         currentProps = newProps
     }
@@ -226,6 +232,29 @@ export function UnitsMeshVisual<P extends UnitsMeshProps>(builder: UnitsMeshVisu
     }
 }
 
+// mesh
+
+export const UnitsMeshParams = {
+    ...StructureMeshParams,
+    ...UnitsParams,
+}
+export const DefaultUnitsMeshProps = paramDefaultValues(UnitsMeshParams)
+export type UnitsMeshProps = typeof DefaultUnitsMeshProps
+export interface UnitsMeshVisualBuilder<P extends UnitsMeshProps> extends UnitsVisualBuilder<P, Mesh> { }
+
+export function UnitsMeshVisual<P extends UnitsMeshProps>(builder: UnitsMeshVisualBuilder<P>): UnitsVisual<P> {
+    return UnitsVisual({
+        ...builder,
+        setUpdateState: (state: VisualUpdateState, newProps: P, currentProps: P) => {
+            builder.setUpdateState(state, newProps, currentProps)
+            if (sizeChanged(currentProps, newProps)) state.createGeometry = true
+        },
+        createEmptyGeometry: Mesh.createEmpty,
+        createRenderObject: createUnitsMeshRenderObject,
+        updateValues: Mesh.updateValues
+    })
+}
+
 // points
 
 export const UnitsPointsParams = {
@@ -237,139 +266,16 @@ export type UnitsPointsProps = typeof DefaultUnitsPointsProps
 export interface UnitsPointVisualBuilder<P extends UnitsPointsProps> extends UnitsVisualBuilder<P, Points> { }
 
 export function UnitsPointsVisual<P extends UnitsPointsProps>(builder: UnitsPointVisualBuilder<P>): UnitsVisual<P> {
-    const { defaultProps, createGeometry, createLocationIterator, getLoci, mark, setUpdateState } = builder
-    const updateState = VisualUpdateState.create()
-
-    let renderObject: PointsRenderObject | undefined
-    let currentProps: P
-    let points: Points
-    let currentGroup: Unit.SymmetryGroup
-    let currentStructure: Structure
-    let locationIt: LocationIterator
-    let currentConformationId: UUID
-
-    async function create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: Partial<P> = {}) {
-        currentProps = Object.assign({}, defaultProps, props, { structure: currentStructure })
-        currentGroup = group
-
-        const unit = group.units[0]
-        currentConformationId = Unit.conformationId(unit)
-        points = includesUnitKind(currentProps.unitKinds, unit)
-            ? await createGeometry(ctx, unit, currentStructure, currentProps, points)
-            : Points.createEmpty(points)
-
-        // TODO create empty location iterator when not in unitKinds
-        locationIt = createLocationIterator(group)
-        renderObject = await createUnitsPointsRenderObject(ctx, group, points, locationIt, currentProps)
-    }
-
-    async function update(ctx: RuntimeContext, props: Partial<P> = {}) {
-        if (!renderObject) return
-
-        const newProps = Object.assign({}, currentProps, props, { structure: currentStructure })
-        const unit = currentGroup.units[0]
-
-        locationIt.reset()
-        VisualUpdateState.reset(updateState)
-        setUpdateState(updateState, newProps, currentProps)
-
-        const newConformationId = Unit.conformationId(unit)
-        if (newConformationId !== currentConformationId) {
-            currentConformationId = newConformationId
-            updateState.createGeometry = true
-        }
-
-        if (currentGroup.units.length !== locationIt.instanceCount) updateState.updateTransform = true
-
-        if (sizeChanged(currentProps, newProps)) updateState.updateSize = true
-        if (colorChanged(currentProps, newProps)) updateState.updateColor = true
-        if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createGeometry = true
-
-        //
-
-        if (updateState.updateTransform) {
-            locationIt = createLocationIterator(currentGroup)
-            const { instanceCount, groupCount } = locationIt
-            createUnitsTransform(currentGroup, renderObject.values)
-            createMarkers(instanceCount * groupCount, renderObject.values)
-            updateState.updateColor = true
-        }
-
-        if (updateState.createGeometry) {
-            points = includesUnitKind(newProps.unitKinds, unit)
-                ? await createGeometry(ctx, unit, currentStructure, newProps, points)
-                : Points.createEmpty(points)
-            ValueCell.update(renderObject.values.drawCount, points.pointCount)
-            updateState.updateColor = true
-        }
-
-        if (updateState.updateSize) {
-            await createSizes(ctx, locationIt, newProps, renderObject.values)
-        }
-
-        if (updateState.updateColor) {
-            await createColors(ctx, locationIt, newProps, renderObject.values)
-        }
-
-        // TODO why do I need to cast here?
-        Points.updateValues(renderObject.values, newProps as UnitsPointsProps)
-        updateRenderableState(renderObject.state, newProps as UnitsPointsProps)
-
-        currentProps = newProps
-    }
-
-    return {
-        get renderObject () { return renderObject },
-        async createOrUpdate(ctx: RuntimeContext, props: Partial<P> = {}, structureGroup?: StructureGroup) {
-            if (structureGroup) currentStructure = structureGroup.structure
-            const group = structureGroup ? structureGroup.group : undefined
-            if (!group && !currentGroup) {
-                throw new Error('missing group')
-            } else if (group && (!currentGroup || !renderObject)) {
-                // console.log('unit-visual first create')
-                await create(ctx, group, props)
-            } else if (group && group.hashCode !== currentGroup.hashCode) {
-                // console.log('unit-visual group.hashCode !== currentGroup.hashCode')
-                await create(ctx, group, props)
-            } else {
-                // console.log('unit-visual update')
-                if (group && !sameGroupConformation(group, currentGroup)) {
-                    // console.log('unit-visual new conformation')
-                    currentGroup = group
-                }
-                await update(ctx, props)
-            }
-        },
-        getLoci(pickingId: PickingId) {
-            return renderObject ? getLoci(pickingId, currentGroup, renderObject.id) : EmptyLoci
+    return UnitsVisual({
+        ...builder,
+        createEmptyGeometry: Points.createEmpty,
+        createRenderObject: createUnitsPointsRenderObject,
+        setUpdateState: (state: VisualUpdateState, newProps: P, currentProps: P) => {
+            builder.setUpdateState(state, newProps, currentProps)
+            if (sizeChanged(currentProps, newProps)) state.updateSize = true
         },
-        mark(loci: Loci, action: MarkerAction) {
-            if (!renderObject) return false
-            const { tMarker } = renderObject.values
-            const { groupCount, instanceCount } = locationIt
-
-            function apply(interval: Interval) {
-                const start = Interval.start(interval)
-                const end = Interval.end(interval)
-                return applyMarkerAction(tMarker.ref.value.array, start, end, action)
-            }
-
-            let changed = false
-            if (isEveryLoci(loci)) {
-                changed = apply(Interval.ofBounds(0, groupCount * instanceCount))
-            } else {
-                changed = mark(loci, currentGroup, apply)
-            }
-            if (changed) {
-                ValueCell.update(tMarker, tMarker.ref.value)
-            }
-            return changed
-        },
-        destroy() {
-            // TODO
-            renderObject = undefined
-        }
-    }
+        updateValues: Points.updateValues
+    })
 }
 
 // lines
@@ -383,139 +289,16 @@ export type UnitsLinesProps = typeof DefaultUnitsLinesProps
 export interface UnitsLinesVisualBuilder<P extends UnitsLinesProps> extends UnitsVisualBuilder<P, Lines> { }
 
 export function UnitsLinesVisual<P extends UnitsLinesProps>(builder: UnitsLinesVisualBuilder<P>): UnitsVisual<P> {
-    const { defaultProps, createGeometry, createLocationIterator, getLoci, mark, setUpdateState } = builder
-    const updateState = VisualUpdateState.create()
-
-    let renderObject: LinesRenderObject | undefined
-    let currentProps: P
-    let lines: Lines
-    let currentGroup: Unit.SymmetryGroup
-    let currentStructure: Structure
-    let locationIt: LocationIterator
-    let currentConformationId: UUID
-
-    async function create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: Partial<P> = {}) {
-        currentProps = Object.assign({}, defaultProps, props, { structure: currentStructure })
-        currentGroup = group
-
-        const unit = group.units[0]
-        currentConformationId = Unit.conformationId(unit)
-        lines = includesUnitKind(currentProps.unitKinds, unit)
-            ? await createGeometry(ctx, unit, currentStructure, currentProps, lines)
-            : Lines.createEmpty(lines)
-
-        // TODO create empty location iterator when not in unitKinds
-        locationIt = createLocationIterator(group)
-        renderObject = await createUnitsLinesRenderObject(ctx, group, lines, locationIt, currentProps)
-    }
-
-    async function update(ctx: RuntimeContext, props: Partial<P> = {}) {
-        if (!renderObject) return
-
-        const newProps = Object.assign({}, currentProps, props, { structure: currentStructure })
-        const unit = currentGroup.units[0]
-
-        locationIt.reset()
-        VisualUpdateState.reset(updateState)
-        setUpdateState(updateState, newProps, currentProps)
-
-        const newConformationId = Unit.conformationId(unit)
-        if (newConformationId !== currentConformationId) {
-            currentConformationId = newConformationId
-            updateState.createGeometry = true
-        }
-
-        if (currentGroup.units.length !== locationIt.instanceCount) updateState.updateTransform = true
-
-        if (sizeChanged(currentProps, newProps)) updateState.updateSize = true
-        if (colorChanged(currentProps, newProps)) updateState.updateColor = true
-        if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createGeometry = true
-
-        //
-
-        if (updateState.updateTransform) {
-            locationIt = createLocationIterator(currentGroup)
-            const { instanceCount, groupCount } = locationIt
-            createUnitsTransform(currentGroup, renderObject.values)
-            createMarkers(instanceCount * groupCount, renderObject.values)
-            updateState.updateColor = true
-        }
-
-        if (updateState.createGeometry) {
-            lines = includesUnitKind(newProps.unitKinds, unit)
-                ? await createGeometry(ctx, unit, currentStructure, newProps, lines)
-                : Lines.createEmpty(lines)
-            ValueCell.update(renderObject.values.drawCount, lines.lineCount * 2 * 3)
-            updateState.updateColor = true
-        }
-
-        if (updateState.updateSize) {
-            await createSizes(ctx, locationIt, newProps, renderObject.values)
-        }
-
-        if (updateState.updateColor) {
-            await createColors(ctx, locationIt, newProps, renderObject.values)
-        }
-
-        // TODO why do I need to cast here?
-        Lines.updateValues(renderObject.values, newProps as UnitsLinesProps)
-        updateRenderableState(renderObject.state, newProps as UnitsLinesProps)
-
-        currentProps = newProps
-    }
-
-    return {
-        get renderObject () { return renderObject },
-        async createOrUpdate(ctx: RuntimeContext, props: Partial<P> = {}, structureGroup?: StructureGroup) {
-            if (structureGroup) currentStructure = structureGroup.structure
-            const group = structureGroup ? structureGroup.group : undefined
-            if (!group && !currentGroup) {
-                throw new Error('missing group')
-            } else if (group && (!currentGroup || !renderObject)) {
-                // console.log('unit-visual first create')
-                await create(ctx, group, props)
-            } else if (group && group.hashCode !== currentGroup.hashCode) {
-                // console.log('unit-visual group.hashCode !== currentGroup.hashCode')
-                await create(ctx, group, props)
-            } else {
-                // console.log('unit-visual update')
-                if (group && !sameGroupConformation(group, currentGroup)) {
-                    // console.log('unit-visual new conformation')
-                    currentGroup = group
-                }
-                await update(ctx, props)
-            }
-        },
-        getLoci(pickingId: PickingId) {
-            return renderObject ? getLoci(pickingId, currentGroup, renderObject.id) : EmptyLoci
+    return UnitsVisual({
+        ...builder,
+        createEmptyGeometry: Lines.createEmpty,
+        createRenderObject: createUnitsLinesRenderObject,
+        setUpdateState: (state: VisualUpdateState, newProps: P, currentProps: P) => {
+            builder.setUpdateState(state, newProps, currentProps)
+            if (sizeChanged(currentProps, newProps)) state.updateSize = true
         },
-        mark(loci: Loci, action: MarkerAction) {
-            if (!renderObject) return false
-            const { tMarker } = renderObject.values
-            const { groupCount, instanceCount } = locationIt
-
-            function apply(interval: Interval) {
-                const start = Interval.start(interval)
-                const end = Interval.end(interval)
-                return applyMarkerAction(tMarker.ref.value.array, start, end, action)
-            }
-
-            let changed = false
-            if (isEveryLoci(loci)) {
-                changed = apply(Interval.ofBounds(0, groupCount * instanceCount))
-            } else {
-                changed = mark(loci, currentGroup, apply)
-            }
-            if (changed) {
-                ValueCell.update(tMarker, tMarker.ref.value)
-            }
-            return changed
-        },
-        destroy() {
-            // TODO
-            renderObject = undefined
-        }
-    }
+        updateValues: Lines.updateValues
+    })
 }
 
 // direct-volume
@@ -529,137 +312,14 @@ export type UnitsDirectVolumeProps = typeof DefaultUnitsDirectVolumeProps
 export interface UnitsDirectVolumeVisualBuilder<P extends UnitsDirectVolumeProps> extends UnitsVisualBuilder<P, DirectVolume> { }
 
 export function UnitsDirectVolumeVisual<P extends UnitsDirectVolumeProps>(builder: UnitsDirectVolumeVisualBuilder<P>): UnitsVisual<P> {
-    const { defaultProps, createGeometry, createLocationIterator, getLoci, mark, setUpdateState } = builder
-    const updateState = VisualUpdateState.create()
-
-    let renderObject: DirectVolumeRenderObject | undefined
-    let currentProps: P
-    let directVolume: DirectVolume
-    let currentGroup: Unit.SymmetryGroup
-    let currentStructure: Structure
-    let locationIt: LocationIterator
-    let currentConformationId: UUID
-
-    async function create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: Partial<P> = {}) {
-        const { webgl } = props
-        if (webgl === undefined) throw new Error('UnitsDirectVolumeVisual requires `webgl` in props')
-
-        currentProps = Object.assign({}, defaultProps, props, { structure: currentStructure })
-        currentGroup = group
-
-        const unit = group.units[0]
-        currentConformationId = Unit.conformationId(unit)
-        directVolume = includesUnitKind(currentProps.unitKinds, unit)
-            ? await createGeometry(ctx, unit, currentStructure, currentProps, directVolume)
-            : DirectVolume.createEmpty(directVolume)
-
-        // TODO create empty location iterator when not in unitKinds
-        locationIt = createLocationIterator(group)
-        renderObject = await createUnitsDirectVolumeRenderObject(ctx, group, directVolume, locationIt, currentProps)
-    }
-
-    async function update(ctx: RuntimeContext, props: Partial<P> = {}) {
-        const { webgl } = props
-        if (webgl === undefined) throw new Error('UnitsDirectVolumeVisual requires `webgl` in props')
-
-        if (!renderObject) return
-
-        const newProps = Object.assign({}, currentProps, props, { structure: currentStructure })
-        const unit = currentGroup.units[0]
-
-        locationIt.reset()
-        VisualUpdateState.reset(updateState)
-        setUpdateState(updateState, newProps, currentProps)
-
-        const newConformationId = Unit.conformationId(unit)
-        if (newConformationId !== currentConformationId) {
-            currentConformationId = newConformationId
-            updateState.createGeometry = true
-        }
-
-        if (currentGroup.units.length !== locationIt.instanceCount) updateState.updateTransform = true
-
-        if (sizeChanged(currentProps, newProps)) updateState.createGeometry = true
-        if (colorChanged(currentProps, newProps)) updateState.updateColor = true
-        if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createGeometry = true
-
-        //
-
-        if (updateState.updateTransform) {
-            locationIt = createLocationIterator(currentGroup)
-            const { instanceCount, groupCount } = locationIt
-            createUnitsTransform(currentGroup, renderObject.values)
-            createMarkers(instanceCount * groupCount, renderObject.values)
-            updateState.updateColor = true
-        }
-
-        if (updateState.createGeometry) {
-            directVolume = includesUnitKind(newProps.unitKinds, unit)
-                ? await createGeometry(ctx, unit, currentStructure, newProps, directVolume)
-                : DirectVolume.createEmpty(directVolume)
-            updateState.updateColor = true
-        }
-
-        if (updateState.updateColor) {
-            await createColors(ctx, locationIt, newProps, renderObject.values)
-        }
-
-        DirectVolume.updateValues(renderObject.values, newProps)
-        updateRenderableState(renderObject.state, newProps)
-
-        currentProps = newProps
-    }
-
-    return {
-        get renderObject () { return renderObject },
-        async createOrUpdate(ctx: RuntimeContext, props: Partial<P> = {}, structureGroup?: StructureGroup) {
-            if (structureGroup) currentStructure = structureGroup.structure
-            const group = structureGroup ? structureGroup.group : undefined
-            if (!group && !currentGroup) {
-                throw new Error('missing group')
-            } else if (group && (!currentGroup || !renderObject)) {
-                // console.log('unit-visual first create')
-                await create(ctx, group, props)
-            } else if (group && group.hashCode !== currentGroup.hashCode) {
-                // console.log('unit-visual group.hashCode !== currentGroup.hashCode')
-                await create(ctx, group, props)
-            } else {
-                // console.log('unit-visual update')
-                if (group && !sameGroupConformation(group, currentGroup)) {
-                    // console.log('unit-visual new conformation')
-                    currentGroup = group
-                }
-                await update(ctx, props)
-            }
-        },
-        getLoci(pickingId: PickingId) {
-            return renderObject ? getLoci(pickingId, currentGroup, renderObject.id) : EmptyLoci
+    return UnitsVisual({
+        ...builder,
+        createEmptyGeometry: DirectVolume.createEmpty,
+        createRenderObject: createUnitsDirectVolumeRenderObject,
+        setUpdateState: (state: VisualUpdateState, newProps: P, currentProps: P) => {
+            builder.setUpdateState(state, newProps, currentProps)
+            if (sizeChanged(currentProps, newProps)) state.createGeometry = true
         },
-        mark(loci: Loci, action: MarkerAction) {
-            if (!renderObject) return false
-            const { tMarker } = renderObject.values
-            const { groupCount, instanceCount } = locationIt
-
-            function apply(interval: Interval) {
-                const start = Interval.start(interval)
-                const end = Interval.end(interval)
-                return applyMarkerAction(tMarker.ref.value.array, start, end, action)
-            }
-
-            let changed = false
-            if (isEveryLoci(loci)) {
-                changed = apply(Interval.ofBounds(0, groupCount * instanceCount))
-            } else {
-                changed = mark(loci, currentGroup, apply)
-            }
-            if (changed) {
-                ValueCell.update(tMarker, tMarker.ref.value)
-            }
-            return changed
-        },
-        destroy() {
-            // TODO
-            renderObject = undefined
-        }
-    }
+        updateValues: DirectVolume.updateValues
+    })
 }