Compare commits

..

17 Commits

Author SHA1 Message Date
dsehnal
1bd162b977 tweaks 2025-01-04 11:07:55 +01:00
dsehnal
c7fb71738e header 2025-01-04 11:03:01 +01:00
dsehnal
9413481253 Fix plugin interactions when CSS scale is applied 2025-01-04 11:02:37 +01:00
Alexander Rose
9f3c617945 Merge pull request #1397 from molstar/color-external-structure
add external-structure theme
2025-01-02 14:45:50 -08:00
Alexander Rose
f920188cdc Merge pull request #1398 from ventura-rivera/master
fix hyperlink typo
2025-01-02 14:45:33 -08:00
Ventura Rivera
68b73503bb update changelog with doc updates 2025-01-02 12:59:35 -08:00
Ventura Rivera
e776138ecd fix hyperlink typo 2025-01-02 12:55:42 -08:00
Alexander Rose
bbacd5a9dd Merge branch 'master' into color-external-structure 2024-12-31 13:53:04 -08:00
Alexander Rose
289ecef1d7 add approxNearest to lookup3d 2024-12-31 13:31:26 -08:00
Alexander Rose
52fc3ef750 Merge pull request #1396 from molstar/float-volume-data
Support float and half-float data type
2024-12-29 18:41:53 -08:00
Alexander Rose
dffe40ac1d simplify isosurface property handling 2024-12-29 17:54:23 -08:00
Alexander Rose
f834e39ce4 add external-structure theme
- colors any geometry by structure properties
2024-12-28 17:38:49 -08:00
Alexander Rose
2bc9c6fb57 Support float and half-float data type
- direct-volume rendering
- GPU isosurface extraction
2024-12-28 14:29:17 -08:00
Alexander Rose
6e42c11f5e comments regarding webgl1 consistent bit plane counts 2024-12-28 13:08:05 -08:00
midlik
d48feeaa94 Mvs union params (#1388)
* MVS: define union params

* MVS: include defaults in param schema

* MVS: removed mvs-defaults

* MVS: union params validation

* MVS: use new param schema

* MVS: Nicely format ColorName

* MVS: primitive uses UnionParamsSchema

* MVS: mesh remove triangle_groups

* MVS: remove dead code

* MVS: reorg files and add docs for parameter system

* MVS: print-schema for union params

* MVS: remove line_colors

* MVS: Rename many primitives params

* MVS: update primitive params descriptions

* MVS: Refactor primitive Builders

* Volumes and segmentations: avoid parsing non-success response

* MVS: refactor primitive params types

* MVS: avoid repeating MVS defaults in primitives.ts

* MVS: update builder

* MVS: primitive params docstrings
2024-12-18 18:54:39 +01:00
David Sehnal
fd0ca75fc1 Volume UI improvements (#1379)
* improvements to Volumes UI

* support wheel scroll on sliders

* headers

* changelog
2024-12-17 17:55:06 +01:00
Alexander Rose
a270dcb5f5 error handling in deploy script 2024-12-15 13:49:54 -08:00
46 changed files with 1703 additions and 803 deletions

View File

@@ -5,6 +5,19 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
- Volume UI improvements
- Render all volume entries instead of selecting them one-by-one
- Toggle visibility of all volumes
- More accessible iso value control
- Support wheel event on sliders
- MolViewSpec extension:
- Add validation for discriminated union params
- Primitives: remove triangle_colors, line_colors, have implicit grouping instead; rename many parameters
- Add `external-structure` theme that colors any geometry by structure properties
- Support float and half-float data type for direct-volume rendering and GPU isosurface extraction
- Minor documentation updates
- Fix plugin mouse interactions when CSS `scale` transform is applied
## [v4.10.0] - 2024-12-15
- Add `ModelWithCoordinates` decorator transform.

View File

@@ -6,7 +6,7 @@
What is a plugin? A plugin is a collection of modules that provide functionality to the `Mol*` UI. The plugin is responsible for managing the state of the viewer, internal and user interactions. It has been a previous point of confusion for new users of `Mol*` to associate the __viewer__ part of the library with what is further referred to as the __plugin__. These two are closely connected in the `molstar-plugin-ui` module, which is the user-facing part of the library and ultimately provides the viewer, but they are ultimately distinct.
It is recommended that you inspect the general class structure of [`PluginInitWrapper`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/plugin.tsx#L41), [`PluginUIContext`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin/context.ts#L71) and [`PluginUIComponent`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/base.tsx#L16) to better understand the flow of data and events in the plugin.
It is recommended that you inspect the general class structure of [`PluginInitWrapper`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/plugin.tsx#L41), [`PluginUIContext`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/context.ts#L12) and [`PluginUIComponent`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/base.tsx#L16) to better understand the flow of data and events in the plugin.
A passing analogy is that a [ `PluginContext` ](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin/context.ts#L71) is the engine that powers computation, rendering, events and subscriptions inside the molstar UI. All UI components depend on `PluginContext`.

View File

@@ -61,9 +61,13 @@ function copyDemos() {
}
function copyFiles() {
copyViewer();
copyMe();
copyDemos();
try {
copyViewer();
copyMe();
copyDemos();
} catch (e) {
console.error(e);
}
}
if (!fs.existsSync(localPath)) {

View File

@@ -12,7 +12,6 @@
import { ArgumentParser } from 'argparse';
import { treeSchemaToMarkdown, treeSchemaToString } from '../../extensions/mvs/tree/generic/tree-schema';
import { MVSDefaults } from '../../extensions/mvs/tree/mvs/mvs-defaults';
import { MVSTreeSchema } from '../../extensions/mvs/tree/mvs/mvs-tree';
@@ -32,9 +31,9 @@ function parseArguments(): Args {
/** Main workflow for printing MolViewSpec tree schema. */
function main(args: Args) {
if (args.markdown) {
console.log(treeSchemaToMarkdown(MVSTreeSchema, MVSDefaults));
console.log(treeSchemaToMarkdown(MVSTreeSchema));
} else {
console.log(treeSchemaToString(MVSTreeSchema, MVSDefaults));
console.log(treeSchemaToString(MVSTreeSchema));
}
}

View File

@@ -18,7 +18,7 @@ import { decodeColor } from './helpers/utils';
import { MolstarLoadingContext } from './load';
import { SnapshotMetadata } from './mvs-data';
import { MolstarNodeParams } from './tree/molstar/molstar-tree';
import { MVSDefaults } from './tree/mvs/mvs-defaults';
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
const DefaultFocusOptions = {
@@ -68,7 +68,9 @@ export async function setFocus(plugin: PluginContext, focuses: { target: StateOb
}
function snapshotFocusInfoFromMvsFocuses(focuses: { target: StateObjectSelector, params: MolstarNodeParams<'focus'> }[]): PluginState.SnapshotFocusInfo {
const { direction, up } = (focuses.length > 0) ? focuses[focuses.length - 1].params : MVSDefaults.focus;
const lastFocus = (focuses.length > 0) ? focuses[focuses.length - 1] : undefined;
const direction = lastFocus?.params.direction ?? MVSTreeSchema.nodes.focus.params.fields.direction.default;
const up = lastFocus?.params.up ?? MVSTreeSchema.nodes.focus.params.fields.up.default;
return {
targets: focuses.map<PluginState.SnapshotFocusTargetInfo>(f => ({
targetRef: f.target.ref === '-=root=-' ? undefined : f.target.ref, // need to treat root separately so it does not include invisible structure parts etc.
@@ -81,7 +83,6 @@ function snapshotFocusInfoFromMvsFocuses(focuses: { target: StateObjectSelector,
};
}
/** Adjust `sceneRadiusFactor` property so that the current scene is not cropped */
function adjustSceneRadiusFactor(plugin: PluginContext, cameraTarget: Vec3 | undefined) {
if (!cameraTarget) return;

View File

@@ -2,6 +2,7 @@
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Adam Midlik <midlik@gmail.com>
*/
import { Lines } from '../../../mol-geo/geometry/lines/lines';
@@ -22,6 +23,7 @@ import { Expression } from '../../../mol-script/language/expression';
import { StateObject } from '../../../mol-state';
import { Task } from '../../../mol-task';
import { round } from '../../../mol-util';
import { range } from '../../../mol-util/array';
import { Asset } from '../../../mol-util/assets';
import { Color } from '../../../mol-util/color';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -29,17 +31,23 @@ import { capitalize } from '../../../mol-util/string';
import { rowsToExpression, rowToExpression } from '../helpers/selections';
import { collectMVSReferences, decodeColor } from '../helpers/utils';
import { MolstarNode, MolstarSubtree } from '../tree/molstar/molstar-tree';
import { MVSPrimitive, MVSPrimitiveOptions, MVSPrimitiveParams } from '../tree/mvs/mvs-primitives';
import { MVSNode } from '../tree/mvs/mvs-tree';
import { isComponentExpression, isPrimitiveComponentExpressions, isVector3, PrimitivePositionT } from '../tree/mvs/param-types';
import { MVSTransform } from './annotation-structure-component';
type PrimitivesParams = MolstarNode<'primitives'>['params']
type _PrimitiveParams = MolstarNode<'primitive'>['params']
type PrimitiveKind = _PrimitiveParams['kind']
type PrimitiveParams<T extends PrimitiveKind = PrimitiveKind> = Extract<_PrimitiveParams, { kind: T }>
export function getPrimitiveStructureRefs(primitives: MolstarSubtree<'primitives'>) {
const refs = new Set<string>();
for (const c of primitives.children ?? []) {
if (c.kind !== 'primitive') continue;
const p = c.params as unknown as MVSPrimitive;
Builders[p.kind]?.[3].refs?.(p, refs);
const p = c.params;
Builders[p.kind].resolveRefs?.(p, refs);
}
return refs;
}
@@ -201,7 +209,7 @@ interface PrimitiveBuilderContext {
defaultStructure?: Structure;
structureRefs: Record<string, Structure | undefined>;
primitives: MolstarNode<'primitive'>[];
options: MVSPrimitiveOptions;
options: PrimitivesParams;
positionCache: Map<string, [Sphere3D, Box3D]>;
instances: Mat4[] | undefined;
}
@@ -234,30 +242,60 @@ const BaseLabelProps: PD.Values<Text.Params> = {
};
const DefaultLabelParams = PD.withDefaults(Text.Params, BaseLabelProps);
const Builders: Record<MVSPrimitive['kind'], [
mesh: (context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: any) => void,
line: (context: PrimitiveBuilderContext, state: LineBuilderState, node: MVSNode<'primitive'>, params: any) => void,
label: (context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: any) => void,
features: {
mesh?: boolean | ((primitive: any, context: PrimitiveBuilderContext) => boolean),
line?: boolean | ((primitive: any, context: PrimitiveBuilderContext) => boolean),
label?: boolean | ((primitive: any, context: PrimitiveBuilderContext) => boolean),
refs?: (params: any, refs: Set<string>) => void
interface PrimitiveBuilder {
builders: {
mesh?: (context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: any) => void,
line?: (context: PrimitiveBuilderContext, state: LineBuilderState, node: MVSNode<'primitive'>, params: any) => void,
label?: (context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: any) => void,
}
isApplicable?: {
mesh?: ((primitive: any, context: PrimitiveBuilderContext) => boolean),
line?: ((primitive: any, context: PrimitiveBuilderContext) => boolean),
label?: ((primitive: any, context: PrimitiveBuilderContext) => boolean),
},
resolveRefs?: (params: any, refs: Set<string>) => void,
}
const Builders: Record<PrimitiveParams['kind'], PrimitiveBuilder> = {
mesh: {
builders: {
mesh: addMesh,
line: addMeshWireframe,
},
isApplicable: {
mesh: (m: PrimitiveParams<'mesh'>) => m.show_triangles,
line: (m: PrimitiveParams<'mesh'>) => m.show_wireframe,
},
},
lines: {
builders: {
line: addLines,
},
},
tube: {
builders: {
mesh: addTubeMesh,
},
resolveRefs: resolveLineRefs,
},
label: {
builders: {
label: addPrimitiveLabel,
},
resolveRefs: resolveLabelRefs,
},
distance_measurement: {
builders: {
mesh: addDistanceMesh,
label: addDistanceLabel,
},
resolveRefs: resolveLineRefs,
},
]> = {
mesh: [addMesh, addMeshWireframe, noOp, {
mesh: (m: MVSPrimitiveParams<'mesh'>) => m.show_triangles ?? true,
line: (m: MVSPrimitiveParams<'mesh'>) => m.show_wireframe ?? false,
}],
lines: [addMesh, addLines, noOp, { line: true }],
line: [addLineMesh, noOp, noOp, { mesh: true, refs: resolveLineRefs }],
label: [noOp, noOp, addPrimitiveLabel, { label: true, refs: resolveLabelRefs }],
distance_measurement: [addDistanceMesh, noOp, addDistanceLabel, { mesh: true, label: true, refs: resolveLineRefs }],
};
function getPrimitives(primitives: MolstarSubtree<'primitives'>) {
return (primitives.children ?? []).filter(c => c.kind === 'primitive') as unknown as MolstarNode<'primitive'>[];
return (primitives.children ?? []).filter(c => c.kind === 'primitive') as MolstarNode<'primitive'>[];
}
function addRef(position: PrimitivePositionT, refs: Set<string>) {
@@ -268,12 +306,14 @@ function addRef(position: PrimitivePositionT, refs: Set<string>) {
function hasPrimitiveKind(context: PrimitiveBuilderContext, kind: 'mesh' | 'line' | 'label') {
for (const c of context.primitives) {
const p = c.params as unknown as MVSPrimitive;
const test = Builders[p.kind]?.[3]?.[kind];
if (typeof test === 'boolean') {
if (test) return true;
} else if (test?.(p, context)) {
return true;
const params = c.params;
const b = Builders[params.kind];
const builderFunction = b.builders[kind];
if (builderFunction) {
const test = b.isApplicable?.[kind];
if (test === undefined || test(params, context)) {
return true;
}
}
}
return false;
@@ -347,7 +387,7 @@ function resolvePosition(context: PrimitiveBuilderContext, position: PrimitivePo
context.positionCache.set(cackeKey, [sphere, box]);
}
function getInstances(options: MVSPrimitiveOptions | undefined): Mat4[] | undefined {
function getInstances(options: PrimitivesParams | undefined): Mat4[] | undefined {
if (!options?.instances?.length) return undefined;
return options.instances.map(i => Mat4.fromArray(Mat4(), i, 0));
}
@@ -359,18 +399,18 @@ function buildPrimitiveMesh(context: PrimitiveBuilderContext, prev?: Mesh): Shap
meshBuilder.currentGroup = -1;
for (const c of context.primitives) {
const p = c.params as unknown as MVSPrimitive;
const p = c.params;
const b = Builders[p.kind];
if (!b) {
console.warn(`Primitive ${p.kind} not supported`);
continue;
}
b[0](context, state, c, p);
b.builders.mesh?.(context, state, c, p);
}
const { colors, tooltips } = state.groups;
const tooltip = context.options?.tooltip ?? '';
const color = decodeColor(context.options?.color) ?? 0x0;
const color = decodeColor(context.options?.color) ?? Color(0);
return Shape.create(
'Mesh',
@@ -380,7 +420,7 @@ function buildPrimitiveMesh(context: PrimitiveBuilderContext, prev?: Mesh): Shap
groupToNode: state.groups.groupToNodeMap,
},
MeshBuilder.getMesh(meshBuilder),
(g) => colors.get(g) as Color ?? color as Color,
(g) => colors.get(g) as Color ?? color,
(g) => 1,
(g) => tooltips.get(g) ?? tooltip,
context.instances,
@@ -392,17 +432,18 @@ function buildPrimitiveLines(context: PrimitiveBuilderContext, prev?: Lines): Sh
const state: LineBuilderState = { groups: new GroupManager(), lines: linesBuilder };
for (const c of context.primitives) {
const p = c.params as unknown as MVSPrimitive;
const p = c.params;
const b = Builders[p.kind];
if (!b) {
console.warn(`Primitive ${p.kind} not supported`);
continue;
}
b[1](context, state, c, p);
b.builders.line?.(context, state, c, p);
}
const color = decodeColor(context.options?.color) ?? 0x0;
const { colors, sizes, tooltips } = state.groups;
const tooltip = context.options?.tooltip ?? '';
const color = decodeColor(context.options?.color) ?? Color(0);
return Shape.create(
'Lines',
@@ -412,9 +453,9 @@ function buildPrimitiveLines(context: PrimitiveBuilderContext, prev?: Lines): Sh
groupToNode: state.groups.groupToNodeMap,
},
linesBuilder.getLines(),
(g) => colors.get(g) as Color ?? color as Color,
(g) => colors.get(g) as Color ?? color,
(g) => sizes.get(g) ?? 1,
(g) => tooltips.get(g) ?? '',
(g) => tooltips.get(g) ?? tooltip,
context.instances,
);
}
@@ -424,16 +465,16 @@ function buildPrimitiveLabels(context: PrimitiveBuilderContext, prev?: Text): Sh
const state: LabelBuilderState = { groups: new GroupManager(), labels: labelsBuilder };
for (const c of context.primitives) {
const p = c.params as unknown as MVSPrimitive;
const p = c.params;
const b = Builders[p.kind];
if (!b) {
console.warn(`Primitive ${p.kind} not supported`);
continue;
}
b[2](context, state, c, p);
b.builders.label?.(context, state, c, p);
}
const color = decodeColor(context.options?.label_color) ?? 0x0;
const color = decodeColor(context.options?.label_color) ?? Color(0);
const { colors, sizes, tooltips } = state.groups;
return Shape.create(
@@ -444,114 +485,89 @@ function buildPrimitiveLabels(context: PrimitiveBuilderContext, prev?: Text): Sh
groupToNode: state.groups.groupToNodeMap,
},
labelsBuilder.getText(),
(g) => colors.get(g) as Color ?? color as Color,
(g) => colors.get(g) as Color ?? color,
(g) => sizes.get(g) ?? 1,
(g) => tooltips.get(g) ?? '',
context.instances,
);
}
function noOp() { }
function addMeshFaces(context: PrimitiveBuilderContext, groups: GroupManager, node: MVSNode<'primitive'>, params: PrimitiveParams<'mesh'>, addFace: (mvsGroup: number, builderGroup: number, a: Vec3, b: Vec3, c: Vec3) => void) {
const a = Vec3.zero();
const b = Vec3.zero();
const c = Vec3.zero();
function addMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBuilderState, node: MVSNode<'primitive'>, params: MVSPrimitiveParams<'mesh'>) {
let { indices, vertices, triangle_groups } = params;
const nTriangles = Math.floor(indices.length / 3);
triangle_groups ??= range(nTriangles); // implicit grouping (triangle i = group i)
const groupSet = groups.allocateMany(node, triangle_groups);
for (let i = 0; i < nTriangles; i++) {
const mvsGroup = triangle_groups[i];
const builderGroup = groupSet.get(mvsGroup)!;
Vec3.fromArray(a, vertices, 3 * indices[3 * i]);
Vec3.fromArray(b, vertices, 3 * indices[3 * i + 1]);
Vec3.fromArray(c, vertices, 3 * indices[3 * i + 2]);
addFace(mvsGroup, builderGroup, a, b, c);
}
}
function addMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'mesh'>) {
if (!params.show_triangles) return;
const a = Vec3.zero();
const b = Vec3.zero();
const c = Vec3.zero();
const { indices, vertices, triangle_colors, triangle_groups, group_colors, group_tooltips } = params;
const groupSet: Map<number, number> | undefined = triangle_groups?.length ? groups.allocateMany(node, triangle_groups) : undefined;
for (let i = 0, _i = indices.length / 3; i < _i; i++) {
if (groupSet) {
const grp = triangle_groups![i];
mesh.currentGroup = groupSet.get(grp)!;
groups.updateColor(mesh.currentGroup, group_colors?.[grp] ?? params.color);
groups.updateTooltip(mesh.currentGroup, group_tooltips?.[grp]);
} else {
mesh.currentGroup = groups.allocateSingle(node);
groups.updateColor(mesh.currentGroup, triangle_colors?.[i] ?? params.color);
groups.updateTooltip(mesh.currentGroup, params.tooltip);
}
Vec3.fromArray(a, vertices, 3 * indices[3 * i]);
Vec3.fromArray(b, vertices, 3 * indices[3 * i + 1]);
Vec3.fromArray(c, vertices, 3 * indices[3 * i + 2]);
const { group_colors, group_tooltips, color, tooltip } = params;
addMeshFaces(context, groups, node, params, (mvsGroup, builderGroup, a, b, c) => {
groups.updateColor(builderGroup, group_colors[mvsGroup] ?? color);
groups.updateTooltip(builderGroup, group_tooltips[mvsGroup] ?? tooltip);
mesh.currentGroup = builderGroup;
MeshBuilder.addTriangle(mesh, a, b, c);
}
});
// this could be slightly improved by only updating color and tooltip once per group instead of once per triangle
}
function addMeshWireframe(context: PrimitiveBuilderContext, { groups, lines }: LineBuilderState, node: MVSNode<'primitive'>, params: MVSPrimitiveParams<'mesh'>) {
function addMeshWireframe(context: PrimitiveBuilderContext, { groups, lines }: LineBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'mesh'>) {
if (!params.show_wireframe) return;
const width = params.wireframe_width;
const a = Vec3.zero();
const b = Vec3.zero();
const c = Vec3.zero();
const { group_colors, group_tooltips, wireframe_color, color, tooltip } = params;
const { indices, vertices, triangle_colors, triangle_groups, group_colors, group_tooltips } = params;
const groupSet: Map<number, number> | undefined = triangle_groups?.length ? groups.allocateMany(node, triangle_groups) : undefined;
const radius = params.wireframe_radius ?? 1;
for (let i = 0, _i = indices.length / 3; i < _i; i++) {
let group: number;
if (groupSet) {
const grp = triangle_groups![i];
group = groupSet.get(grp)!;
groups.updateColor(group, params.wireframe_color ?? group_colors?.[grp]);
groups.updateTooltip(group, group_tooltips?.[grp]);
} else {
group = groups.allocateSingle(node);
groups.updateColor(group, params.wireframe_color ?? triangle_colors?.[i]);
groups.updateTooltip(group, params.tooltip);
}
groups.updateSize(group, radius);
Vec3.fromArray(a, vertices, 3 * indices[3 * i]);
Vec3.fromArray(b, vertices, 3 * indices[3 * i + 1]);
Vec3.fromArray(c, vertices, 3 * indices[3 * i + 2]);
lines.add(a[0], a[1], a[2], b[0], b[1], b[2], group);
lines.add(b[0], b[1], b[2], c[0], c[1], c[2], group);
lines.add(c[0], c[1], c[2], a[0], a[1], a[2], group);
}
addMeshFaces(context, groups, node, params, (mvsGroup, builderGroup, a, b, c) => {
groups.updateColor(builderGroup, wireframe_color ?? group_colors[mvsGroup] ?? color);
groups.updateTooltip(builderGroup, group_tooltips[mvsGroup] ?? tooltip);
groups.updateSize(builderGroup, width);
lines.add(a[0], a[1], a[2], b[0], b[1], b[2], builderGroup);
lines.add(b[0], b[1], b[2], c[0], c[1], c[2], builderGroup);
lines.add(c[0], c[1], c[2], a[0], a[1], a[2], builderGroup);
});
}
function addLines(context: PrimitiveBuilderContext, { groups, lines }: LineBuilderState, node: MVSNode<'primitive'>, params: MVSPrimitiveParams<'lines'>) {
function addLines(context: PrimitiveBuilderContext, { groups, lines }: LineBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'lines'>) {
const a = Vec3.zero();
const b = Vec3.zero();
const { indices, vertices, line_colors, line_groups, group_colors, group_tooltips, group_radius } = params;
let { indices, vertices, line_groups, group_colors, group_tooltips, group_widths } = params;
const width = params.width;
const groupSet: Map<number, number> | undefined = line_groups?.length ? groups.allocateMany(node, line_groups) : undefined;
const radius = params.line_radius ?? 1;
const nLines = Math.floor(indices.length / 2);
line_groups ??= range(nLines); // implicit grouping (line i = group i)
const groupSet = groups.allocateMany(node, line_groups);
for (let i = 0, _i = indices.length / 2; i < _i; i++) {
let group: number;
if (groupSet) {
const grp = line_groups![i];
group = groupSet.get(grp)!;
groups.updateColor(group, group_colors?.[grp] ?? params.color);
groups.updateTooltip(group, group_tooltips?.[grp]);
groups.updateSize(group, group_radius?.[grp] ?? radius);
} else {
group = groups.allocateSingle(node);
groups.updateColor(group, line_colors?.[i] ?? params.color);
groups.updateSize(group, radius);
groups.updateTooltip(group, params.tooltip);
}
for (let i = 0; i < nLines; i++) {
const mvsGroup = line_groups[i];
const builderGroup = groupSet.get(mvsGroup)!;
groups.updateColor(builderGroup, group_colors[mvsGroup] ?? params.color);
groups.updateTooltip(builderGroup, group_tooltips[mvsGroup] ?? params.tooltip);
groups.updateSize(builderGroup, group_widths[mvsGroup] ?? width);
Vec3.fromArray(a, vertices, 3 * indices[2 * i]);
Vec3.fromArray(b, vertices, 3 * indices[2 * i + 1]);
lines.add(a[0], a[1], a[2], b[0], b[1], b[2], group);
lines.add(a[0], a[1], a[2], b[0], b[1], b[2], builderGroup);
}
}
function resolveLineRefs(params: MVSPrimitiveParams<'line' | 'distance_measurement'>, refs: Set<string>) {
function resolveLineRefs(params: PrimitiveParams<'tube' | 'distance_measurement'>, refs: Set<string>) {
addRef(params.start, refs);
addRef(params.end, refs);
}
@@ -559,12 +575,12 @@ function resolveLineRefs(params: MVSPrimitiveParams<'line' | 'distance_measureme
const lStart = Vec3.zero();
const lEnd = Vec3.zero();
function addLineMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBuilderState, node: MVSNode<'primitive'>, params: MVSPrimitiveParams<'line'>, options?: { skipResolvePosition?: boolean }) {
function addTubeMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'tube'>, options?: { skipResolvePosition?: boolean }) {
if (!options?.skipResolvePosition) {
resolveBasePosition(context, params.start, lStart);
resolveBasePosition(context, params.end, lEnd);
}
const radius = params.thickness ?? 0.05;
const radius = params.radius;
const cylinderProps: BasicCylinderProps = {
radiusBottom: radius,
@@ -586,7 +602,7 @@ function addLineMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBui
}
}
function getDistanceLabel(context: PrimitiveBuilderContext, params: MVSPrimitiveParams<'distance_measurement'>) {
function getDistanceLabel(context: PrimitiveBuilderContext, params: PrimitiveParams<'distance_measurement'>) {
resolveBasePosition(context, params.start, lStart);
resolveBasePosition(context, params.end, lEnd);
@@ -597,14 +613,14 @@ function getDistanceLabel(context: PrimitiveBuilderContext, params: MVSPrimitive
return label;
}
function addDistanceMesh(context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: MVSPrimitiveParams<'distance_measurement'>) {
function addDistanceMesh(context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'distance_measurement'>) {
const tooltip = getDistanceLabel(context, params);
addLineMesh(context, state, node, { ...params, tooltip } as any, { skipResolvePosition: true });
addTubeMesh(context, state, node, { ...params, tooltip } as any, { skipResolvePosition: true });
}
const labelPos = Vec3.zero();
function addDistanceLabel(context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: MVSPrimitiveParams<'distance_measurement'>) {
function addDistanceLabel(context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'distance_measurement'>) {
const { labels, groups } = state;
resolveBasePosition(context, params.start, lStart);
resolveBasePosition(context, params.end, lEnd);
@@ -614,10 +630,10 @@ function addDistanceLabel(context: PrimitiveBuilderContext, state: LabelBuilderS
const label = typeof params.label_template === 'string' ? params.label_template.replace('{{distance}}', distance) : distance;
let size: number | undefined;
if (params.label_size === 'auto') {
size = Math.max(dist * (params.label_auto_size_scale ?? 0.2), params.label_auto_size_min ?? 0.01);
} else if (typeof params.label_size === 'number') {
if (typeof params.label_size === 'number') {
size = params.label_size;
} else {
size = Math.max(dist * (params.label_auto_size_scale), params.label_auto_size_min);
}
Vec3.add(labelPos, lStart, lEnd);
@@ -627,14 +643,14 @@ function addDistanceLabel(context: PrimitiveBuilderContext, state: LabelBuilderS
groups.updateColor(group, params.label_color);
groups.updateSize(group, size);
labels.add(label, labelPos[0], labelPos[1], labelPos[2], 1.05 * (params.thickness ?? 0.05), 1, group);
labels.add(label, labelPos[0], labelPos[1], labelPos[2], 1.05 * (params.radius), 1, group);
}
function resolveLabelRefs(params: MVSPrimitiveParams<'label'>, refs: Set<string>) {
function resolveLabelRefs(params: PrimitiveParams<'label'>, refs: Set<string>) {
addRef(params.position, refs);
}
function addPrimitiveLabel(context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: MVSPrimitiveParams<'label'>) {
function addPrimitiveLabel(context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'label'>) {
const { labels, groups } = state;
resolveBasePosition(context, params.position, labelPos);
@@ -642,5 +658,5 @@ function addPrimitiveLabel(context: PrimitiveBuilderContext, state: LabelBuilder
groups.updateColor(group, params.label_color);
groups.updateSize(group, params.label_size);
labels.add(params.text, labelPos[0], labelPos[1], labelPos[2], params.label_offset ?? 0, 1, group);
labels.add(params.text, labelPos[0], labelPos[1], labelPos[2], params.label_offset, 1, group);
}

View File

@@ -128,6 +128,16 @@ export const HexColor = {
},
};
/** Named color string, e.g. 'red' */
export type ColorName = keyof ColorNames
export const ColorName = {
/** Decide if a string is a valid named color string */
is(str: any): str is ColorName {
return str in ColorNames;
},
};
export function collectMVSReferences<T extends StateObject.Ctor>(type: T[], dependencies: Record<string, StateObject>): Record<string, StateObject.From<T>['data']> {
const ret: any = {};

View File

@@ -27,7 +27,6 @@ import { MolstarLoadingContext } from './load';
import { Subtree, getChildren } from './tree/generic/tree-schema';
import { dfs, formatObject } from './tree/generic/tree-utils';
import { MolstarKind, MolstarNode, MolstarNodeParams, MolstarSubtree, MolstarTree } from './tree/molstar/molstar-tree';
import { DefaultColor } from './tree/mvs/mvs-defaults';
export const AnnotationFromUriKinds = new Set(['color_from_uri', 'component_from_uri', 'label_from_uri', 'tooltip_from_uri'] satisfies MolstarKind[]);
@@ -36,6 +35,9 @@ export type AnnotationFromUriKind = ElementOfSet<typeof AnnotationFromUriKinds>
export const AnnotationFromSourceKinds = new Set(['color_from_source', 'component_from_source', 'label_from_source', 'tooltip_from_source'] satisfies MolstarKind[]);
export type AnnotationFromSourceKind = ElementOfSet<typeof AnnotationFromSourceKinds>
/** Color to be used e.g. for representations without 'color' node */
export const DefaultColor = 'white';
/** Return a 4x4 matrix representing a rotation followed by a translation */
export function transformFromRotationTranslation(rotation: number[] | null | undefined, translation: number[] | null | undefined): Mat4 {

View File

@@ -303,7 +303,7 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
},
primitives_from_uri(updateParent: UpdateTarget, tree: MolstarNode<'primitives_from_uri'>, context: MolstarLoadingContext): UpdateTarget {
const data = UpdateTarget.apply(updateParent, MVSDownloadPrimitiveData, { uri: tree.params.uri, format: tree.params.format });
return applyPrimitiveVisuals(data, new Set(tree.params.references ?? []));
return applyPrimitiveVisuals(data, new Set(tree.params.references));
},
};

View File

@@ -0,0 +1,69 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { RequiredField, fieldValidationIssues, float, int, literal, nullable, str, union } from '../field-schema';
describe('fieldValidationIssues', () => {
it('fieldValidationIssues string', async () => {
const stringField = RequiredField(str, 'Testing required field stringField');
expect(fieldValidationIssues(stringField, 'hello')).toBeUndefined();
expect(fieldValidationIssues(stringField, '')).toBeUndefined();
expect(fieldValidationIssues(stringField, 5)).toBeTruthy();
expect(fieldValidationIssues(stringField, null)).toBeTruthy();
expect(fieldValidationIssues(stringField, undefined)).toBeTruthy();
});
it('fieldValidationIssues string choice', async () => {
const colorParam = RequiredField(literal('red', 'green', 'blue', 'yellow'), 'Testing required field colorParam');
expect(fieldValidationIssues(colorParam, 'red')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'green')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'blue')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'yellow')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'banana')).toBeTruthy();
expect(fieldValidationIssues(colorParam, 5)).toBeTruthy();
expect(fieldValidationIssues(colorParam, null)).toBeTruthy();
expect(fieldValidationIssues(colorParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues number choice', async () => {
const numberParam = RequiredField(literal(1, 2, 3, 4), 'Testing required field numberParam');
expect(fieldValidationIssues(numberParam, 1)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 2)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 3)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 4)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 5)).toBeTruthy();
expect(fieldValidationIssues(numberParam, '1')).toBeTruthy();
expect(fieldValidationIssues(numberParam, null)).toBeTruthy();
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues int', async () => {
const numberParam = RequiredField(int, 'Testing required field numberParam');
expect(fieldValidationIssues(numberParam, 1)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 0)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 0.5)).toBeTruthy();
expect(fieldValidationIssues(numberParam, '1')).toBeTruthy();
expect(fieldValidationIssues(numberParam, null)).toBeTruthy();
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues union', async () => {
const stringOrNumberParam = RequiredField(union([str, float]), 'Testing required field stringOrNumberParam');
expect(fieldValidationIssues(stringOrNumberParam, 1)).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, 2)).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, 'hello')).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, '')).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, true)).toBeTruthy();
expect(fieldValidationIssues(stringOrNumberParam, null)).toBeTruthy();
expect(fieldValidationIssues(stringOrNumberParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues nullable', async () => {
const stringOrNullParam = RequiredField(nullable(str), 'Testing required field stringOrNullParam');
expect(fieldValidationIssues(stringOrNullParam, 'hello')).toBeUndefined();
expect(fieldValidationIssues(stringOrNullParam, '')).toBeUndefined();
expect(fieldValidationIssues(stringOrNullParam, null)).toBeUndefined();
expect(fieldValidationIssues(stringOrNullParam, 1)).toBeTruthy();
expect(fieldValidationIssues(stringOrNullParam, true)).toBeTruthy();
expect(fieldValidationIssues(stringOrNullParam, undefined)).toBeTruthy();
});
});

View File

@@ -1,107 +1,85 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import * as iots from 'io-ts';
import { fieldValidationIssues, RequiredField, literal, nullable, paramsValidationIssues, OptionalField } from '../params-schema';
import { OptionalField, RequiredField, bool, float, int, str } from '../field-schema';
import { SimpleParamsSchema, UnionParamsSchema, paramsValidationIssues } from '../params-schema';
describe('fieldValidationIssues', () => {
it('fieldValidationIssues string', async () => {
const stringField = RequiredField(iots.string);
expect(fieldValidationIssues(stringField, 'hello')).toBeUndefined();
expect(fieldValidationIssues(stringField, '')).toBeUndefined();
expect(fieldValidationIssues(stringField, 5)).toBeTruthy();
expect(fieldValidationIssues(stringField, null)).toBeTruthy();
expect(fieldValidationIssues(stringField, undefined)).toBeTruthy();
});
it('fieldValidationIssues string choice', async () => {
const colorParam = RequiredField(literal('red', 'green', 'blue', 'yellow'));
expect(fieldValidationIssues(colorParam, 'red')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'green')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'blue')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'yellow')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'banana')).toBeTruthy();
expect(fieldValidationIssues(colorParam, 5)).toBeTruthy();
expect(fieldValidationIssues(colorParam, null)).toBeTruthy();
expect(fieldValidationIssues(colorParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues number choice', async () => {
const numberParam = RequiredField(literal(1, 2, 3, 4));
expect(fieldValidationIssues(numberParam, 1)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 2)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 3)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 4)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 5)).toBeTruthy();
expect(fieldValidationIssues(numberParam, '1')).toBeTruthy();
expect(fieldValidationIssues(numberParam, null)).toBeTruthy();
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues int', async () => {
const numberParam = RequiredField(iots.Integer);
expect(fieldValidationIssues(numberParam, 1)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 0)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 0.5)).toBeTruthy();
expect(fieldValidationIssues(numberParam, '1')).toBeTruthy();
expect(fieldValidationIssues(numberParam, null)).toBeTruthy();
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues union', async () => {
const stringOrNumberParam = RequiredField(iots.union([iots.string, iots.number]));
expect(fieldValidationIssues(stringOrNumberParam, 1)).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, 2)).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, 'hello')).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, '')).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, true)).toBeTruthy();
expect(fieldValidationIssues(stringOrNumberParam, null)).toBeTruthy();
expect(fieldValidationIssues(stringOrNumberParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues nullable', async () => {
const stringOrNullParam = RequiredField(nullable(iots.string));
expect(fieldValidationIssues(stringOrNullParam, 'hello')).toBeUndefined();
expect(fieldValidationIssues(stringOrNullParam, '')).toBeUndefined();
expect(fieldValidationIssues(stringOrNullParam, null)).toBeUndefined();
expect(fieldValidationIssues(stringOrNullParam, 1)).toBeTruthy();
expect(fieldValidationIssues(stringOrNullParam, true)).toBeTruthy();
expect(fieldValidationIssues(stringOrNullParam, undefined)).toBeTruthy();
});
const simpleSchema = SimpleParamsSchema({
name: OptionalField(str, 'Anonymous', 'Testing optional field name'),
surname: RequiredField(str, 'Testing optional field surname'),
lunch: RequiredField(bool, 'Testing optional field lunch'),
age: OptionalField(int, 0, 'Testing optional field age'),
});
const schema = {
name: OptionalField(iots.string),
surname: RequiredField(iots.string),
lunch: RequiredField(iots.boolean),
age: OptionalField(iots.number),
};
describe('validateParams', () => {
it('validateParams', async () => {
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(schema, {}, { noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', age: 29 }, { noExtra: true })).toBeTruthy(); // missing `lunch`
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { noExtra: true })).toBeTruthy(); // wrong type of `age`
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, married: false }, { noExtra: true })).toBeTruthy(); // extra param `married`
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, married: false })).toBeUndefined(); // extra param `married`
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(simpleSchema, {}, { noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', age: 29 }, { noExtra: true })).toBeTruthy(); // missing `lunch`
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { noExtra: true })).toBeTruthy(); // wrong type of `age`
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true, married: false }, { noExtra: true })).toBeTruthy(); // extra param `married`
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true, married: false })).toBeUndefined(); // extra param `married`
});
});
describe('validateFullParams', () => {
it('validateFullParams', async () => {
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true }, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true }, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, age: 29 }, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29 }, { requireAll: true, noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(schema, {}, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { requireAll: true, noExtra: true })).toBeTruthy(); // wrong type of `age`
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29, married: true }, { requireAll: true, noExtra: true })).toBeTruthy(); // extra param `married`
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29, married: true }, { requireAll: true, noExtra: false })).toBeUndefined(); // extra param `married`
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true }, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true }, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true, age: 29 }, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 29 }, { requireAll: true, noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(simpleSchema, {}, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { requireAll: true, noExtra: true })).toBeTruthy(); // wrong type of `age`
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 29, married: true }, { requireAll: true, noExtra: true })).toBeTruthy(); // extra param `married`
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 29, married: true }, { requireAll: true, noExtra: false })).toBeUndefined(); // extra param `married`
});
});
const unionSchema = UnionParamsSchema(
'kind',
'Description for "kind"',
{
person: SimpleParamsSchema({
name: OptionalField(str, 'Anonymous', 'Testing optional field name'),
surname: RequiredField(str, 'Testing optional field surname'),
lunch: RequiredField(bool, 'Testing optional field lunch'),
age: OptionalField(int, 0, 'Testing optional field age'),
}),
object: SimpleParamsSchema({
weight: RequiredField(float, 'Testing optional field weight'),
color: OptionalField(str, 'colorless', 'Testing optional field color'),
}),
},
);
describe('validateUnionParams', () => {
it('validateUnionParams', async () => {
expect(paramsValidationIssues(unionSchema, { surname: 'Doe', lunch: true }, { noExtra: true })).toBeTruthy(); // missing discriminator param `kind`
expect(paramsValidationIssues(unionSchema, { kind: 'person', surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(unionSchema, { kind: 'person', name: 'John', surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(unionSchema, { kind: 'person', surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(unionSchema, { kind: 'person', name: 'John', surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(unionSchema, { kind: 'person' }, { noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(unionSchema, { kind: 'person', name: 'John', surname: 'Doe', age: 29 }, { noExtra: true })).toBeTruthy(); // missing `lunch`
expect(paramsValidationIssues(unionSchema, { kind: 'person', name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { noExtra: true })).toBeTruthy(); // wrong type of `age`
expect(paramsValidationIssues(unionSchema, { kind: 'person', surname: 'Doe', lunch: true, married: false }, { noExtra: true })).toBeTruthy(); // extra param `married`
expect(paramsValidationIssues(unionSchema, { kind: 'person', surname: 'Doe', lunch: true, married: false })).toBeUndefined(); // extra param `married`
expect(paramsValidationIssues(unionSchema, { kind: 'object', weight: 42, color: 'black' }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(unionSchema, { kind: 'object', weight: 42 }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(unionSchema, { kind: 'object', color: 'black' }, { noExtra: true })).toBeTruthy(); // missing param `weight`
expect(paramsValidationIssues(unionSchema, { kind: 'object', weight: 42, name: 'John' }, { noExtra: true })).toBeTruthy(); // extra param `name`
expect(paramsValidationIssues(unionSchema, { kind: 'spanish_inquisition' }, { noExtra: true })).toBeTruthy(); // unexpected value for discriminator param `kind`
});
});

View File

@@ -0,0 +1,107 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as iots from 'io-ts';
import { PathReporter } from 'io-ts/PathReporter';
import { onelinerJsonString } from '../../../../mol-util/json';
/** All types that can be used in tree node params.
* Can be extended, this is just to list them all in one place and possibly catch some typing errors */
type AllowedValueTypes = string | number | boolean | null | [number, number, number] | string[] | number[] | {};
/** Type definition for a string */
export const str = iots.string;
/** Type definition for an integer */
export const int = iots.Integer;
/** Type definition for a float or integer number */
export const float = iots.number;
/** Type definition for a boolean */
export const bool = iots.boolean;
/** Type definition for a tuple, e.g. `tuple([str, int, int])` */
export const tuple = iots.tuple;
/** Type definition for a list/array, e.g. `list(str)` */
export const list = iots.array;
/** Type definition for union types, e.g. `union([str, int])` means string or integer */
export const union = iots.union;
/** Type definition used to create objects */
export const obj = iots.type;
/** Type definition used to create partial objects */
export const partial = iots.partial;
/** Type definition for nullable types, e.g. `nullable(str)` means string or `null` */
export function nullable<T extends iots.Type<any>>(type: T) {
return union([type, iots.null]);
}
/** Type definition for literal types, e.g. `literal('red', 'green', 'blue')` means 'red' or 'green' or 'blue' */
export function literal<V extends string | number | boolean>(...values: V[]) {
if (values.length === 0) {
throw new Error(`literal type must have at least one value`);
}
const typeName = `(${values.map(v => onelinerJsonString(v)).join(' | ')})`;
return new iots.Type<V>(
typeName,
((value: any) => values.includes(value)) as any,
(value, ctx) => values.includes(value as any) ? { _tag: 'Right', right: value as any } : { _tag: 'Left', left: [{ value: value, context: ctx, message: `"${value}" is not a valid value for literal type ${typeName}` }] },
value => value
);
}
/** Type definition for mapping between two types, e.g. `mapping(str, float)` means type `{ [key in string]: number }` */
export function mapping<A extends iots.Type<any>, B extends iots.Type<any>>(from: A, to: B) {
return iots.record(from, to);
}
interface FieldBase<V extends AllowedValueTypes = any, R extends boolean = boolean> {
/** Definition of allowed types for the field */
type: iots.Type<V>,
/** If `required===true`, the value must always be defined in molviewspec format (can be `null` if `type` allows it).
* If `required===false`, the value can be ommitted (meaning that a default should be used).
* If `type` allows `null`, the default must be `null`. */
required: R,
/** Description of what the field value means */
description: string,
}
/** Schema for param field which must always be provided (has no default value) */
export interface RequiredField<V extends AllowedValueTypes = any> extends FieldBase<V> {
required: true,
}
export function RequiredField<V extends AllowedValueTypes>(type: iots.Type<V>, description: string): RequiredField<V> {
return { type, required: true, description };
}
/** Schema for param field which can be dropped (meaning that a default value will be used) */
export interface OptionalField<V extends AllowedValueTypes = any> extends FieldBase<V> {
required: false,
/** Default value for optional field.
* If field type allows `null`, default must be `null` (this is to avoid issues in languages that do not distinguish `null` and `undefined`). */
default: DefaultValue<V>,
}
export function OptionalField<V extends AllowedValueTypes>(type: iots.Type<V>, defaultValue: DefaultValue<V>, description: string): OptionalField<V> {
return { type, required: false, description, default: defaultValue };
}
/** Schema for one field in params (i.e. a value in a top-level key-value pair) */
export type Field<V extends AllowedValueTypes = any> = RequiredField<V> | OptionalField<V>;
/** Type of valid default value for value type `V` (if the type allows `null`, the default must be `null`) */
type DefaultValue<V extends AllowedValueTypes> = null extends V ? null : V;
/** Type of valid value for field of type `F` (never includes `undefined`, even if field is optional) */
export type ValueFor<F extends Field | iots.Any> = F extends Field<infer V> ? V : F extends iots.Any ? iots.TypeOf<F> : never;
/** Return `undefined` if `value` has correct type for `field`, regardsless of if required or optional.
* Return description of validation issues, if `value` has wrong type. */
export function fieldValidationIssues<F extends Field>(field: F, value: any): string[] | undefined {
const validation = field.type.decode(value);
if (validation._tag === 'Right') {
return undefined;
} else {
return PathReporter.report(validation);
}
}

View File

@@ -5,153 +5,175 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as iots from 'io-ts';
import { PathReporter } from 'io-ts/PathReporter';
import { isPlainObject, mapObjectMap } from '../../../../mol-util/object';
import { onelinerJsonString } from '../../../../mol-util/json';
import { isPlainObject, mapObjectMap, omitObjectKeys } from '../../../../mol-util/object';
import { Field, fieldValidationIssues, OptionalField, RequiredField, ValueFor } from './field-schema';
/** All types that can be used in tree node params.
* Can be extended, this is just to list them all in one place and possibly catch some typing errors */
type AllowedValueTypes = string | number | boolean | null | [number, number, number] | string[] | number[] | {}
type Fields = { [key in string]: Field };
/** Type definition for a string */
export const str = iots.string;
/** Type definition for an integer */
export const int = iots.Integer;
/** Type definition for a float or integer number */
export const float = iots.number;
/** Type definition for a boolean */
export const bool = iots.boolean;
/** Type definition for a tuple, e.g. `tuple([str, int, int])` */
export const tuple = iots.tuple;
/** Type definition for a list/array, e.g. `list(str)` */
export const list = iots.array;
/** Type definition for union types, e.g. `union([str, int])` means string or integer */
export const union = iots.union;
/** Type definition used to create objects */
export const obj = iots.type;
/** Type definition used to create partial objects */
export const partial = iots.partial;
/** Type definition for nullable types, e.g. `nullable(str)` means string or `null` */
export function nullable<T extends iots.Type<any>>(type: T) {
return union([type, iots.null]);
/** Type of `ParamsSchema` where all fields are completely independent */
export interface SimpleParamsSchema<TFields extends Fields = Fields> {
type: 'simple',
/** Parameter fields */
fields: TFields,
}
/** Type definition for literal types, e.g. `literal('red', 'green', 'blue')` means 'red' or 'green' or 'blue' */
export function literal<V extends string | number | boolean>(...values: V[]) {
if (values.length === 0) {
throw new Error(`literal type must have at least one value`);
}
const typeName = `(${values.map(v => onelinerJsonString(v)).join(' | ')})`;
return new iots.Type<V>(
typeName,
((value: any) => values.includes(value)) as any,
(value, ctx) => values.includes(value as any) ? { _tag: 'Right', right: value as any } : { _tag: 'Left', left: [{ value: value, context: ctx, message: `"${value}" is not a valid value for literal type ${typeName}` }] },
value => value
);
}
/** Mapping between two types */
export function mapping<A extends iots.Type<any>, B extends iots.Type<any>>(from: A, to: B) {
return iots.record(from, to);
export function SimpleParamsSchema<TFields extends Fields>(fields: TFields): SimpleParamsSchema<TFields> {
return { type: 'simple', fields };
}
type ValuesForFields<F extends Fields> =
{ [key in keyof F as (F[key] extends RequiredField<any> ? key : never)]: ValueFor<F[key]> }
& { [key in keyof F as (F[key] extends OptionalField<any> ? key : never)]?: ValueFor<F[key]> };
/** Schema for one field in params (i.e. a value in a top-level key-value pair) */
interface Field<V extends AllowedValueTypes = any, R extends boolean = boolean> {
/** Definition of allowed types for the field */
type: iots.Type<V>,
/** If `required===true`, the value must always be defined in molviewspec format (can be `null` if `type` allows it).
* If `required===false`, the value can be ommitted (meaning that a default should be used).
* If `type` allows `null`, the default must be `null`. */
required: R,
/** Description of what the field value means */
description?: string,
type ValuesForSimpleParamsSchema<TSchema extends SimpleParamsSchema> = ValuesForFields<TSchema['fields']>;
type AllRequiredFields<F extends Fields>
= { [key in keyof F]: F[key] extends Field<infer V> ? RequiredField<V> : never };
type AllRequiredSimple<TSchema extends SimpleParamsSchema> = SimpleParamsSchema<AllRequiredFields<TSchema['fields']>>;
type Cases = { [case_ in string]: SimpleParamsSchema };
// Tried to have this recursive ({ [case_ in string]: ParamsSchema }) but ran into "ts(2589) Type instantiation is excessively deep..."
/** Type of `ParamsSchema` where one field (discriminator) determines what other fields are allowed (i.e. discriminated union type) */
export interface UnionParamsSchema<TDiscriminator extends string = string, TCases extends Cases = Cases> {
type: 'union',
/** Name of parameter field that determines the rest (allowed values are defined by keys of `cases`) */
discriminator: TDiscriminator,
/** Description for the discriminator parameter field */
discriminatorDescription: string,
/** `ParamsSchema` for the rest, for each case of discriminator value */
cases: TCases,
}
/** Schema for param field which must always be provided (has no default value) */
export interface RequiredField<V extends AllowedValueTypes = any> extends Field<V> {
required: true,
}
export function RequiredField<V extends AllowedValueTypes>(type: iots.Type<V>, description?: string): RequiredField<V> {
return { type, required: true, description };
export function UnionParamsSchema<TDiscriminator extends string, TCases extends Cases>(discriminator: TDiscriminator, discriminatorDescription: string, cases: TCases): UnionParamsSchema<TDiscriminator, TCases> {
return { type: 'union', discriminator, discriminatorDescription, cases };
}
/** Schema for param field which can be dropped (meaning that a default value will be used) */
export interface OptionalField<V extends AllowedValueTypes = any> extends Field<V> {
required: false,
}
export function OptionalField<V extends AllowedValueTypes>(type: iots.Type<V>, description?: string): OptionalField<V> {
return { type, required: false, description };
}
type ValuesForUnionParamsSchema<TSchema extends UnionParamsSchema, TCase extends keyof TSchema['cases'] = keyof TSchema['cases']>
= TCase extends keyof TSchema['cases'] ? { [discriminator in TSchema['discriminator']]: TCase } & ValuesFor<TSchema['cases'][TCase]> : never;
// `extends` clause seems superfluous here, but is needed to properly create discriminated union type
/** Type of valid value for field of type `F` (never includes `undefined`, even if field is optional) */
export type ValueFor<F extends Field | iots.Any> = F extends Field<infer V> ? V : F extends iots.Any ? iots.TypeOf<F> : never
/** Type of valid default value for field of type `F` (if the field's type allows `null`, the default must be `null`) */
export type DefaultFor<F extends Field> = F extends Field<infer V> ? (null extends V ? null : V) : never
/** Return `undefined` if `value` has correct type for `field`, regardsless of if required or optional.
* Return description of validation issues, if `value` has wrong type. */
export function fieldValidationIssues<F extends Field, V>(field: F, value: V): V extends ValueFor<F> ? undefined : string[] {
const validation = field.type.decode(value);
if (validation._tag === 'Right') {
return undefined as any;
} else {
return PathReporter.report(validation) as any;
}
}
type AllRequiredUnion<TSchema extends UnionParamsSchema>
= UnionParamsSchema<TSchema['discriminator'], { [case_ in keyof TSchema['cases']]: AllRequired<TSchema['cases'][case_]> }>;
/** Schema for "params", i.e. a flat collection of key-value pairs */
export type ParamsSchema<TKey extends string = string> = { [key in TKey]: Field }
/** Variation of a params schema where all fields are required */
export type AllRequired<TParamsSchema extends ParamsSchema> = { [key in keyof TParamsSchema]: TParamsSchema[key] extends Field<infer V> ? RequiredField<V> : never }
export function AllRequired<TParamsSchema extends ParamsSchema>(paramsSchema: TParamsSchema): AllRequired<TParamsSchema> {
return mapObjectMap(paramsSchema, field => RequiredField(field.type, field.description)) as AllRequired<TParamsSchema>;
}
export type ParamsSchema = SimpleParamsSchema | UnionParamsSchema;
/** Type of values for a params schema (optional fields can be missing) */
export type ValuesFor<P extends ParamsSchema> =
{ [key in keyof P as (P[key] extends RequiredField<any> ? key : never)]: ValueFor<P[key]> }
& { [key in keyof P as (P[key] extends OptionalField<any> ? key : never)]?: ValueFor<P[key]> }
export type ValuesFor<P extends ParamsSchema>
= P extends SimpleParamsSchema ? ValuesForSimpleParamsSchema<P> : P extends UnionParamsSchema ? ValuesForUnionParamsSchema<P> : never;
/** Variation of a params schema where all fields are required */
export type AllRequired<P extends ParamsSchema>
= P extends SimpleParamsSchema ? AllRequiredSimple<P> : P extends UnionParamsSchema ? AllRequiredUnion<P> : never;
function AllRequiredSimple<TSchema extends SimpleParamsSchema>(schema: TSchema): AllRequired<TSchema> {
const newFields = mapObjectMap(schema.fields, field => RequiredField(field.type, field.description));
return SimpleParamsSchema(newFields) as AllRequired<TSchema>;
}
function AllRequiredUnion<TSchema extends UnionParamsSchema>(schema: TSchema): AllRequired<TSchema> {
const newCases = mapObjectMap(schema.cases, AllRequired);
return UnionParamsSchema(schema.discriminator, schema.discriminatorDescription, newCases) as AllRequired<TSchema>;
}
export function AllRequired<TSchema extends ParamsSchema>(schema: TSchema): AllRequired<TSchema> {
if (schema.type === 'simple') {
return AllRequiredSimple(schema) as AllRequired<TSchema>;
} else {
return AllRequiredUnion(schema) as AllRequired<TSchema>;
}
}
/** Type of full values for a params schema, i.e. including all optional fields */
export type FullValuesFor<P extends ParamsSchema> = { [key in keyof P]: ValueFor<P[key]> }
export type FullValuesFor<P extends ParamsSchema> = ValuesFor<AllRequired<P>>;
/** Type of default values for a params schema, i.e. including only optional fields */
export type DefaultsFor<P extends ParamsSchema> = { [key in keyof P as (P[key] extends Field<any, false> ? key : never)]: ValueFor<P[key]> }
interface ValidationOptions {
/** Check that all parameters (including optional) have a value provided. */
requireAll?: boolean,
/** Check there are extra parameters other that those defined in the schema. */
noExtra?: boolean,
}
/** Return `undefined` if `values` contains correct value types for `schema`,
* return description of validation issues, if `values` have wrong type.
* If `options.requireAll`, all parameters (including optional) must have a value provided.
* If `options.noExtra` is true, presence of any extra parameters is treated as an issue.
*/
export function paramsValidationIssues<P extends ParamsSchema, V extends { [k: string]: any }>(schema: P, values: V, options: { requireAll?: boolean, noExtra?: boolean } = {}): string[] | undefined {
* If `options.noExtra` is true, presence of any extra parameters is treated as an issue. */
export function paramsValidationIssues<P extends ParamsSchema>(schema: P, values: { [k: string]: any }, options: ValidationOptions = {}): string[] | undefined {
if (!isPlainObject(values)) return [`Parameters must be an object, not ${values}`];
for (const key in schema) {
const paramDef = schema[key];
// Special handling of "union" param type
// TODO: figure out how to do this properly, ignoring the validation for now
if (key === '_union_') {
return undefined;
}
if (schema.type === 'simple') {
return simpleParamsValidationIssue(schema, values, options);
} else {
return unionParamsValidationIssues(schema, values, options);
}
}
function simpleParamsValidationIssue(schema: SimpleParamsSchema, values: { [k: string]: any }, options: ValidationOptions): string[] | undefined {
for (const key in schema.fields) {
const fieldSchema = schema.fields[key];
if (Object.hasOwn(values, key)) {
const value = values[key];
const issues = fieldValidationIssues(paramDef, value);
if (issues) return [`Invalid type for parameter "${key}":`, ...issues.map(s => ' ' + s)];
const issues = fieldValidationIssues(fieldSchema, value);
if (issues) return [`Invalid value for parameter "${key}":`, ...issues.map(s => ' ' + s)];
} else {
if (paramDef.required) return [`Missing required parameter "${key}".`];
if (fieldSchema.required) return [`Missing required parameter "${key}".`];
if (options.requireAll) return [`Missing optional parameter "${key}".`];
}
}
if (options.noExtra) {
for (const key in values) {
if (!Object.hasOwn(schema, key)) return [`Unknown parameter "${key}".`];
if (!Object.hasOwn(schema.fields, key)) return [`Unknown parameter "${key}".`];
}
}
return undefined;
}
function unionParamsValidationIssues(schema: UnionParamsSchema, values: { [k: string]: any }, options: ValidationOptions): string[] | undefined {
if (!Object.hasOwn(values, schema.discriminator)) {
return [`Missing required parameter "${schema.discriminator}".`];
}
const case_ = values[schema.discriminator];
const subschema = schema.cases[case_];
if (subschema === undefined) {
const allowedCases = Object.keys(schema.cases).map(x => `"${x}"`).join(' | ');
return [
`Invalid value for parameter "${schema.discriminator}":`,
`"${case_}" is not a valid value for literal type (${allowedCases})`,
];
}
const issues = paramsValidationIssues(subschema, omitObjectKeys(values, [schema.discriminator]), options);
if (issues) {
issues.unshift(`(case "${schema.discriminator}": "${case_}")`);
return issues.map(s => ' ' + s);
}
return undefined;
}
/** Add default parameter values to `values` based on a parameter schema (only for optional parameters) */
export function addParamDefaults<P extends ParamsSchema>(schema: P, values: ValuesFor<P>): FullValuesFor<P> {
if (schema.type === 'simple') {
return addSimpleParamsDefaults(schema, values);
} else {
return addUnionParamsDefaults(schema, values);
}
}
function addSimpleParamsDefaults(schema: SimpleParamsSchema, values: any): any {
const out = { ...values };
for (const key in schema.fields) {
const field = schema.fields[key];
if (!field.required && out[key] === undefined) {
out[key] = field.default;
}
}
return out;
}
function addUnionParamsDefaults(schema: UnionParamsSchema, values: any): any {
const case_ = values[schema.discriminator];
const subschema = schema.cases[case_];
return addParamDefaults(subschema, values);
}

View File

@@ -6,7 +6,8 @@
import { onelinerJsonString } from '../../../../mol-util/json';
import { isPlainObject, mapObjectMap } from '../../../../mol-util/object';
import { AllRequired, DefaultsFor, ParamsSchema, ValuesFor, paramsValidationIssues } from './params-schema';
import { Field } from './field-schema';
import { AllRequired, ParamsSchema, SimpleParamsSchema, ValuesFor, paramsValidationIssues } from './params-schema';
import { treeToString } from './tree-utils';
@@ -85,7 +86,7 @@ export interface TreeSchema<TParamsSchemas extends ParamsSchemas = ParamsSchemas
},
}
export function TreeSchema<P extends ParamsSchemas = ParamsSchemas, R extends keyof P = string>(schema: TreeSchema<P, R>): TreeSchema<P, R> {
return schema as any;
return schema;
}
/** ParamsSchemas per node kind */
@@ -114,9 +115,6 @@ export type NodeFor<TTreeSchema extends TreeSchema, TKind extends keyof ParamsSc
/** Type of tree which conforms to tree schema `TTreeSchema` */
export type TreeFor<TTreeSchema extends TreeSchema> = Tree<NodeFor<TTreeSchema>, RootFor<TTreeSchema> & NodeFor<TTreeSchema>>
/** Type of default parameter values for each node kind in a tree schema `TTreeSchema` */
export type DefaultsForTree<TTreeSchema extends TreeSchema> = { [kind in keyof TTreeSchema['nodes']]: DefaultsFor<TTreeSchema['nodes'][kind]['params']> }
/** Return `undefined` if a tree conforms to the given schema,
* return validation issues (as a list of lines) if it does not conform.
@@ -160,14 +158,14 @@ export function validateTree(schema: TreeSchema, tree: Tree, label: string): voi
}
/** Return documentation for a tree schema as plain text */
export function treeSchemaToString<S extends TreeSchema>(schema: S, defaults?: DefaultsForTree<S>): string {
return treeSchemaToString_(schema, defaults, false);
export function treeSchemaToString<S extends TreeSchema>(schema: S): string {
return treeSchemaToString_(schema, false);
}
/** Return documentation for a tree schema as markdown text */
export function treeSchemaToMarkdown<S extends TreeSchema>(schema: S, defaults?: DefaultsForTree<S>): string {
return treeSchemaToString_(schema, defaults, true);
export function treeSchemaToMarkdown<S extends TreeSchema>(schema: S): string {
return treeSchemaToString_(schema, true);
}
function treeSchemaToString_<S extends TreeSchema>(schema: S, defaults?: DefaultsForTree<S>, markdown: boolean = false): string {
function treeSchemaToString_<S extends TreeSchema>(schema: S, markdown: boolean = false): string {
const out: string[] = [];
const bold = (str: string) => markdown ? `**${str}**` : str;
const code = (str: string) => markdown ? `\`${str}\`` : str;
@@ -175,6 +173,8 @@ function treeSchemaToString_<S extends TreeSchema>(schema: S, defaults?: Default
const p1 = markdown ? '' : ' ';
const h2 = markdown ? '- ' : ' - ';
const p2 = markdown ? ' ' : ' ';
const h3 = markdown ? ' - ' : ' - ';
const p3 = markdown ? ' ' : ' ';
const newline = markdown ? '\n\n' : '\n';
out.push(`Tree schema:`);
for (const kind in schema.nodes) {
@@ -188,21 +188,46 @@ function treeSchemaToString_<S extends TreeSchema>(schema: S, defaults?: Default
}
out.push(`${p1}Parent: ${!parent ? 'any' : parent.length === 0 ? 'none' : parent.map(code).join(' or ')}`);
out.push(`${p1}Params:${Object.keys(params).length > 0 ? '' : ' none'}`);
for (const key in params) {
const field = params[key];
let typeString = field.type.name;
if (typeString.startsWith('(') && typeString.endsWith(')')) {
typeString = typeString.slice(1, -1);
if (params.type === 'simple') {
formatSimpleParams(out, params, { h: h2, p: p2, code, bold });
} else {
const key = params.discriminator;
const casesStr = Object.keys(params.cases).join(' | ');
out.push(`${h2}${bold(code(key + ': '))}${code(casesStr)}`);
if (params.discriminatorDescription) {
out.push(`${p2}${params.discriminatorDescription}`);
}
out.push(`${h2}${bold(code(key + (field.required ? ': ' : '?: ')))}${code(typeString)}`);
const defaultValue = (defaults?.[kind] as any)?.[key];
if (field.description) {
out.push(`${p2}${field.description}`);
}
if (defaultValue !== undefined) {
out.push(`${p2}Default: ${code(onelinerJsonString(defaultValue))}`);
out.push(`${p2}[This parameter determines the rest of parameters]`);
for (const case_ in params.cases) {
const caseStr = `${params.discriminator}: "${case_}"`;
out.push(`${p2}${bold(`Case ${code(caseStr)}:`)}`);
formatSimpleParams(out, params.cases[case_], { h: h3, p: p3, code, bold });
}
}
}
return out.join(newline);
}
function formatSimpleParams(out: string[], params: SimpleParamsSchema, formatting: { h: string, p: string, code: (str: string) => string, bold: (str: string) => string }): void {
const { h, p, code, bold } = formatting;
for (const key in params.fields) {
const field = params.fields[key];
out.push(`${h}${bold(code(key + (field.required ? ': ' : '?: ')))}${code(formatFieldType(field))}`);
const defaultValue = field.required ? undefined : field.default;
if (field.description) {
out.push(`${p}${field.description}`);
}
if (defaultValue !== undefined) {
out.push(`${p}Default: ${code(onelinerJsonString(defaultValue))}`);
}
}
}
function formatFieldType(field: Field): string {
const typeString = field.type.name;
if (typeString.startsWith('(') && typeString.endsWith(')')) {
return typeString.slice(1, -1);
} else {
return typeString;
}
}

View File

@@ -5,7 +5,8 @@
*/
import { canonicalJsonString } from '../../../../mol-util/json';
import { CustomProps, DefaultsForTree, Kind, Node, Subtree, SubtreeOfKind, Tree, TreeFor, TreeSchema, TreeSchemaWithAllRequired, getParams } from './tree-schema';
import { addParamDefaults } from './params-schema';
import { CustomProps, Kind, Node, Subtree, SubtreeOfKind, Tree, TreeFor, TreeSchema, TreeSchemaWithAllRequired, getParams } from './tree-schema';
/** Run DFS (depth-first search) algorithm on a rooted tree.
@@ -148,12 +149,13 @@ export function condenseTree<T extends Tree>(root: T, condenseNodes?: Set<Kind<T
}
/** Create a copy of the tree where missing optional params for each node are added based on `defaults`. */
export function addDefaults<S extends TreeSchema>(tree: TreeFor<S>, defaults: DefaultsForTree<S>): TreeFor<TreeSchemaWithAllRequired<S>> {
const rules: ConversionRules<TreeFor<S>, TreeFor<S>> = {};
for (const kind in defaults) {
rules[kind] = node => [{
export function addDefaults<S extends TreeSchema>(tree: TreeFor<S>, treeSchema: S): TreeFor<TreeSchemaWithAllRequired<S>> {
type TTree = TreeFor<S>;
const rules: ConversionRules<TTree, TTree> = {};
for (const kind in treeSchema.nodes) {
rules[kind as Kind<Subtree<TTree>>] = node => [{
kind: node.kind,
params: { ...defaults[kind], ...node.params },
params: addParamDefaults(treeSchema.nodes[kind].params, node.params as any),
custom: node.custom,
ref: node.ref,
} as Node as any];

View File

@@ -4,12 +4,11 @@
* @author Adam Midlik <midlik@gmail.com>
*/
import { ConversionRules, addDefaults, condenseTree, convertTree, dfs, resolveUris } from '../generic/tree-utils';
import { MolstarKind, MolstarNode, MolstarTree } from './molstar-tree';
import { FullMVSTree, MVSTree, MVSTreeSchema } from '../mvs/mvs-tree';
import { MVSDefaults } from '../mvs/mvs-defaults';
import { MolstarParseFormatT, ParseFormatT } from '../mvs/param-types';
import { omitObjectKeys, pickObjectKeys } from '../../../../mol-util/object';
import { ConversionRules, addDefaults, condenseTree, convertTree, dfs, resolveUris } from '../generic/tree-utils';
import { FullMVSTree, MVSTree, MVSTreeSchema } from '../mvs/mvs-tree';
import { MolstarParseFormatT, ParseFormatT } from '../mvs/param-types';
import { MolstarKind, MolstarNode, MolstarTree } from './molstar-tree';
/** Convert `format` parameter of `parse` node in `MolstarTree`
@@ -53,7 +52,7 @@ const molstarNodesToCondense = new Set<MolstarKind>(['download', 'parse', 'traje
/** Convert MolViewSpec tree into MolStar tree */
export function convertMvsToMolstar(mvsTree: MVSTree, sourceUrl: string | undefined): MolstarTree {
const full = addDefaults<typeof MVSTreeSchema>(mvsTree, MVSDefaults) as FullMVSTree;
const full = addDefaults<typeof MVSTreeSchema>(mvsTree, MVSTreeSchema) as FullMVSTree;
if (sourceUrl) resolveUris(full, sourceUrl, ['uri', 'url']);
const converted = convertTree<FullMVSTree, MolstarTree>(full, mvsToMolstarConversionRules);
if (converted.kind !== 'root') throw new Error("Root's type is not 'root' after conversion from MVS tree to Molstar tree.");

View File

@@ -5,7 +5,8 @@
*/
import { omitObjectKeys, pickObjectKeys } from '../../../../mol-util/object';
import { RequiredField, bool } from '../generic/params-schema';
import { RequiredField, bool } from '../generic/field-schema';
import { SimpleParamsSchema } from '../generic/params-schema';
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema } from '../generic/tree-schema';
import { FullMVSTreeSchema } from '../mvs/mvs-tree';
import { MolstarParseFormatT } from '../mvs/param-types';
@@ -18,37 +19,41 @@ export const MolstarTreeSchema = TreeSchema({
...FullMVSTreeSchema.nodes,
download: {
...FullMVSTreeSchema.nodes.download,
params: {
...FullMVSTreeSchema.nodes.download.params,
is_binary: RequiredField(bool),
},
params: SimpleParamsSchema({
...FullMVSTreeSchema.nodes.download.params.fields,
is_binary: RequiredField(bool, 'Specifies whether file is downloaded as bytes array or string'),
}),
},
parse: {
...FullMVSTreeSchema.nodes.parse,
params: {
format: RequiredField(MolstarParseFormatT),
},
params: SimpleParamsSchema({
format: RequiredField(MolstarParseFormatT, 'File format'),
}),
},
/** Auxiliary node corresponding to Molstar's TrajectoryFrom*. */
trajectory: {
description: "Auxiliary node corresponding to Molstar's TrajectoryFrom*.",
parent: ['parse'],
params: {
format: RequiredField(MolstarParseFormatT),
...pickObjectKeys(FullMVSTreeSchema.nodes.structure.params, ['block_header', 'block_index'] as const),
},
params: SimpleParamsSchema({
format: RequiredField(MolstarParseFormatT, 'File format'),
...pickObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index'] as const),
}),
},
/** Auxiliary node corresponding to Molstar's ModelFromTrajectory. */
model: {
description: "Auxiliary node corresponding to Molstar's ModelFromTrajectory.",
parent: ['trajectory'],
params: pickObjectKeys(FullMVSTreeSchema.nodes.structure.params, ['model_index'] as const),
params: SimpleParamsSchema(
pickObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['model_index'] as const)
),
},
/** Auxiliary node corresponding to Molstar's StructureFromModel. */
structure: {
...FullMVSTreeSchema.nodes.structure,
parent: ['model'],
params: omitObjectKeys(FullMVSTreeSchema.nodes.structure.params, ['block_header', 'block_index', 'model_index'] as const),
params: SimpleParamsSchema(
omitObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index', 'model_index'] as const)
),
},
}
});

View File

@@ -7,8 +7,7 @@
import { deepClone, pickObjectKeys } from '../../../../mol-util/object';
import { GlobalMetadata, MVSData_State, Snapshot, SnapshotMetadata } from '../../mvs-data';
import { CustomProps } from '../generic/tree-schema';
import { MVSDefaults } from './mvs-defaults';
import { MVSKind, MVSNode, MVSNodeParams, MVSSubtree, MVSTreeSchema } from './mvs-tree';
import { MVSKind, MVSNode, MVSNodeParams, MVSSubtree } from './mvs-tree';
/** Create a new MolViewSpec builder containing only a root node. Example of MVS builder usage:
@@ -28,17 +27,16 @@ export function createMVSBuilder(params: CustomAndRef = {}) {
/** Base class for MVS builder pointing to anything */
class _Base<TKind extends MVSKind> {
protected constructor(
constructor(
protected readonly _root: Root,
protected readonly _node: MVSSubtree<TKind>,
) { }
/** Create a new node, append as child to current _node, and return the new node */
protected addChild<TChildKind extends MVSKind>(kind: TChildKind, params_: MVSNodeParams<TChildKind> & CustomAndRef) {
const { params, custom, ref } = splitParams<MVSNodeParams<TChildKind>>(params_);
const allowedParamNames = Object.keys(MVSTreeSchema.nodes[kind].params) as (keyof MVSNodeParams<TChildKind>)[];
const node = {
kind,
params: pickObjectKeys(params, allowedParamNames) as unknown,
params,
custom,
ref,
} as MVSSubtree<TChildKind>;
@@ -50,7 +48,7 @@ class _Base<TKind extends MVSKind> {
/** MVS builder pointing to the 'root' node */
export class Root extends _Base<'root'> {
export class Root extends _Base<'root'> implements FocusMixin, PrimitivesMixin {
constructor(params_: CustomAndRef) {
const { custom, ref } = params_;
const node: MVSNode<'root'> = { kind: 'root', custom, ref };
@@ -64,6 +62,7 @@ export class Root extends _Base<'root'> {
metadata: GlobalMetadata.create(metadata),
};
}
// omitting `saveState`, filesystem operations are responsibility of the caller code (platform-dependent)
/** Return the current state of the builder as a snapshot object to be used in multi-state . */
getSnapshot(metadata: SnapshotMetadata): Snapshot {
return {
@@ -71,7 +70,6 @@ export class Root extends _Base<'root'> {
metadata: { ...metadata },
};
}
// omitting `saveState`, filesystem operations are responsibility of the caller code (platform-dependent)
/** Add a 'camera' node and return builder pointing to the root. 'camera' node instructs to set the camera position and orientation. */
camera(params: MVSNodeParams<'camera'> & CustomAndRef): Root {
@@ -87,6 +85,9 @@ export class Root extends _Base<'root'> {
download(params: MVSNodeParams<'download'> & CustomAndRef): Download {
return new Download(this._root, this.addChild('download', params));
}
focus = bindMethod(this, FocusMixinImpl, 'focus');
primitives = bindMethod(this, PrimitivesMixinImpl, 'primitives');
primitives_from_uri = bindMethod(this, PrimitivesMixinImpl, 'primitives_from_uri');
}
@@ -154,10 +155,10 @@ export class Parse extends _Base<'parse'> {
/** MVS builder pointing to a 'structure' node */
export class Structure extends _Base<'structure'> {
export class Structure extends _Base<'structure'> implements PrimitivesMixin {
/** Add a 'component' node and return builder pointing to it. 'component' node instructs to create a component (i.e. a subset of the parent structure). */
component(params: Partial<MVSNodeParams<'component'>> & CustomAndRef = {}): Component {
const fullParams = { ...params, selector: params.selector ?? MVSDefaults.component.selector };
const fullParams = { ...params, selector: params.selector ?? 'all' };
return new Component(this._root, this.addChild('component', fullParams));
}
/** Add a 'component_from_uri' node and return builder pointing to it. 'component_from_uri' node instructs to create a component defined by an external annotation resource. */
@@ -196,11 +197,13 @@ export class Structure extends _Base<'structure'> {
this.addChild('transform', params);
return this;
}
primitives = bindMethod(this, PrimitivesMixinImpl, 'primitives');
primitives_from_uri = bindMethod(this, PrimitivesMixinImpl, 'primitives_from_uri');
}
/** MVS builder pointing to a 'component' or 'component_from_uri' or 'component_from_source' node */
export class Component extends _Base<'component' | 'component_from_uri' | 'component_from_source'> {
export class Component extends _Base<'component' | 'component_from_uri' | 'component_from_source'> implements FocusMixin {
/** Add a 'representation' node and return builder pointing to it. 'representation' node instructs to create a visual representation of a component. */
representation(params: Partial<MVSNodeParams<'representation'>> & CustomAndRef = {}): Representation {
const fullParams: MVSNodeParams<'representation'> = { ...params, type: params.type ?? 'cartoon' };
@@ -216,11 +219,7 @@ export class Component extends _Base<'component' | 'component_from_uri' | 'compo
this.addChild('tooltip', params);
return this;
}
/** Add a 'focus' node and return builder pointing back to the component node. 'focus' node instructs to set the camera focus to a component (zoom in). */
focus(params: MVSNodeParams<'focus'> & CustomAndRef = {}): Component {
this.addChild('focus', params);
return this;
}
focus = bindMethod(this, FocusMixinImpl, 'focus');
}
@@ -241,9 +240,95 @@ export class Representation extends _Base<'representation'> {
this.addChild('color_from_source', params);
return this;
}
/** Add an 'opacity' node and return builder pointing back to the representation node. 'opacity' node instructs to customize opacity/transparency of a visual representation. */
opacity(params: MVSNodeParams<'opacity'> & CustomAndRef): Representation {
this.addChild('opacity', params);
return this;
}
}
type MVSPrimitiveSubparams<TKind extends MVSNodeParams<'primitive'>['kind']> = Omit<Extract<MVSNodeParams<'primitive'>, { kind: TKind }>, 'kind'>;
/** MVS builder pointing to a 'primitives' node */
class Primitives extends _Base<'primitives'> implements FocusMixin {
/** Construct custom meshes/shapes in a low-level fashion by providing vertices and indices. */
mesh(params: MVSPrimitiveSubparams<'mesh'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'mesh', ...params });
return this;
}
/** Construct custom set of lines in a low-level fashion by providing vertices and indices. */
lines(params: MVSPrimitiveSubparams<'lines'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'lines', ...params });
return this;
}
/** Defines a tube (3D cylinder), connecting a start and an end point. */
tube(params: MVSPrimitiveSubparams<'tube'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'tube', ...params });
return this;
}
/** Defines a tube, connecting a start and an end point, with label containing distance between start and end. */
distance(params: MVSPrimitiveSubparams<'distance_measurement'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'distance_measurement', ...params });
return this;
}
/** Defines a label. */
label(params: MVSPrimitiveSubparams<'label'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'label', ...params });
return this;
}
focus = bindMethod(this, FocusMixinImpl, 'focus');
}
/** MVS builder pointing to a 'primitives_from_uri' node */
class PrimitivesFromUri extends _Base<'primitives_from_uri'> implements FocusMixin {
focus = bindMethod(this, FocusMixinImpl, 'focus');
}
// MIXINS
type Constructor<T> = new (...args: any[]) => T;
/** Fake interface for typing tweaks */
interface Self { '@type': 'self' }
type ReplaceSelf<TFunction, TSelf> = TFunction extends (...args: infer TArgs) => Self ? (...args: TArgs) => TSelf : TFunction;
function bindMethod<O extends _Base<any>, C extends Constructor<_Base<any>>, M extends keyof InstanceType<C>>(thisObj: O, mixin: C, methodName: M): ReplaceSelf<InstanceType<C>[M], O> {
return mixin.prototype[methodName].bind(thisObj);
}
// This mixin implementation is really ugly but couldn't be bothered (running into TS2502: 'Root' is referenced directly or indirectly in its own type annotation)
interface FocusMixin {
/** Add a 'focus' node and return builder pointing back to the original node. 'focus' node instructs to set the camera focus to a component (zoom in). */
focus(params: MVSNodeParams<'focus'> & CustomAndRef): any,
}
class FocusMixinImpl extends _Base<MVSKind> implements FocusMixin {
focus(params: MVSNodeParams<'focus'> & CustomAndRef = {}): Self {
this.addChild('focus', params);
return this as unknown as Self;
}
};
interface PrimitivesMixin {
/** Allows the definition of a (group of) geometric primitives. You can add any number of primitives and then assign shared options (color, opacity etc.). */
primitives(params: MVSNodeParams<'primitives'> & CustomAndRef): Primitives,
/** Allows the definition of a (group of) geometric primitives provided dynamically. */
primitives_from_uri(params: MVSNodeParams<'primitives_from_uri'> & CustomAndRef): PrimitivesFromUri,
};
class PrimitivesMixinImpl extends _Base<MVSKind> implements PrimitivesMixin {
primitives(params: MVSNodeParams<'primitives'> & CustomAndRef = {}): Primitives {
return new Primitives(this._root, this.addChild('primitives', params));
}
primitives_from_uri(params: MVSNodeParams<'primitives_from_uri'> & CustomAndRef): PrimitivesFromUri {
return new PrimitivesFromUri(this._root, this.addChild('primitives_from_uri', params));
}
};
/** Demonstration of usage of MVS builder */
export function builderDemo() {
const builder = createMVSBuilder();

View File

@@ -1,123 +0,0 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { DefaultsForTree } from '../generic/tree-schema';
import { MVSTreeSchema } from './mvs-tree';
/** Default values for params in `MVSTree` */
export const MVSDefaults = {
root: {},
download: {
},
parse: {
},
structure: {
block_header: null,
block_index: 0,
model_index: 0,
assembly_id: null,
radius: 5,
ijk_min: [-1, -1, -1],
ijk_max: [1, 1, 1],
},
component: {
selector: 'all' as const,
},
component_from_uri: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'component',
field_values: null,
},
component_from_source: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'component',
field_values: null,
},
representation: {
},
color: {
selector: 'all' as const,
},
color_from_uri: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'color',
},
color_from_source: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'color',
},
opacity: {
},
label: {
},
label_from_uri: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'label',
},
label_from_source: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'label',
},
tooltip: {
},
tooltip_from_uri: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'tooltip',
},
tooltip_from_source: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'tooltip',
},
focus: {
direction: [0, 0, -1],
up: [0, 1, 0],
radius: null,
radius_factor: 1,
radius_extent: 0,
},
transform: {
rotation: [1, 0, 0, 0, 1, 0, 0, 0, 1], // 3x3 identitity matrix
translation: [0, 0, 0],
},
canvas: {
},
camera: {
up: [0, 1, 0],
},
primitives: {
color: null,
label_color: null,
tooltip: null,
opacity: null,
label_opacity: null,
instances: null,
},
primitives_from_uri: {
references: null,
},
primitive: { },
} satisfies DefaultsForTree<typeof MVSTreeSchema>;
/** Color to be used e.g. for representations without 'color' node */
export const DefaultColor = 'white';

View File

@@ -1,81 +0,0 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { bool, float, int, literal, mapping, nullable, obj, str, union, ValueFor } from '../generic/params-schema';
import type { MVSNode } from './mvs-tree';
import { ColorT, FloatList, IntList, PrimitivePositionT, StrList } from './param-types';
// TODO: Figure out validation and default values for these
const _LineBase = {
start: PrimitivePositionT,
end: PrimitivePositionT,
thickness: nullable(float),
color: nullable(ColorT),
dash_length: nullable(float),
};
const MeshParams = obj({
kind: literal('mesh'),
vertices: FloatList,
indices: IntList,
triangle_colors: nullable(StrList),
triangle_groups: nullable(IntList),
group_colors: nullable(mapping(int, ColorT)),
group_tooltips: nullable(mapping(int, str)),
tooltip: nullable(str),
show_triangles: nullable(bool),
show_wireframe: nullable(bool),
color: nullable(ColorT),
wireframe_radius: nullable(float),
wireframe_color: nullable(ColorT),
});
const LinesParams = obj({
kind: literal('lines'),
vertices: FloatList,
indices: IntList,
line_colors: nullable(StrList),
line_groups: nullable(IntList),
group_colors: nullable(mapping(int, ColorT)),
group_tooltips: nullable(mapping(int, str)),
group_radius: nullable(mapping(int, float)),
tooltip: nullable(str),
color: nullable(ColorT),
line_radius: nullable(float),
});
const LineParams = obj({
kind: literal('line'),
..._LineBase,
tooltip: nullable(str),
});
const DistanceMeasurementParams = obj({
kind: literal('distance_measurement'),
..._LineBase,
label_template: nullable(str),
label_size: nullable(union([float, literal('auto')])),
label_auto_size_scale: nullable(float),
label_auto_size_min: nullable(float),
label_color: nullable(ColorT),
});
const PrimitiveLabelParams = obj({
kind: literal('label'),
position: PrimitivePositionT,
text: str,
label_size: nullable(float),
label_color: nullable(ColorT),
label_offset: nullable(float),
});
export const MVSPrimitiveParams = union([MeshParams, LinesParams, LineParams, DistanceMeasurementParams, PrimitiveLabelParams]);
export type MVSPrimitive = ValueFor<typeof MVSPrimitiveParams>
export type MVSPrimitiveKind = MVSPrimitive['kind']
export type MVSPrimitiveOptions = MVSNode<'primitives'>['params']
export type MVSPrimitiveParams<T extends MVSPrimitiveKind = MVSPrimitiveKind> = Extract<MVSPrimitive, { kind: T }>

View File

@@ -0,0 +1,115 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Adam Midlik <midlik@gmail.com>
*/
import { bool, float, int, mapping, nullable, OptionalField, RequiredField, str } from '../generic/field-schema';
import { SimpleParamsSchema, UnionParamsSchema } from '../generic/params-schema';
import { ColorT, FloatList, IntList, PrimitivePositionT } from './param-types';
const _TubeBase = {
/** Start point of the tube. */
start: RequiredField(PrimitivePositionT, 'Start point of the tube.'),
/** End point of the tube. */
end: RequiredField(PrimitivePositionT, 'End point of the tube.'),
/** Tube radius (in Angstroms). */
radius: OptionalField(float, 0.05, 'Tube radius (in Angstroms).'),
/** Length of each dash and gap between dashes. If not specified (null), draw full line. */
dash_length: OptionalField(nullable(float), null, 'Length of each dash and gap between dashes. If not specified (null), draw full line.'),
/** Color of the tube. If not specified, uses the parent primitives group `color`. */
color: OptionalField(nullable(ColorT), null, 'Color of the tube. If not specified, uses the parent primitives group `color`.'),
};
const MeshParams = {
/** 3*n_vertices length array of floats with vertex position (x1, y1, z1, ...). */
vertices: RequiredField(FloatList, '3*n_vertices length array of floats with vertex position (x1, y1, z1, ...).'),
/** 3*n_triangles length array of indices into vertices that form triangles (t1_1, t1_2, t1_3, ...). */
indices: RequiredField(IntList, '3*n_triangles length array of indices into vertices that form triangles (t1_1, t1_2, t1_3, ...).'),
/** Assign a number to each triangle to group them. If not specified, each triangle is considered a separate group (triangle i = group i). */
triangle_groups: OptionalField(nullable(IntList), null, 'Assign a number to each triangle to group them. If not specified, each triangle is considered a separate group (triangle i = group i).'),
/** Assign a color to each group. Where not assigned, uses `color`. */
group_colors: OptionalField(mapping(int, ColorT), {}, 'Assign a color to each group. Where not assigned, uses `color`.'),
/** Assign a tooltip to each group. Where not assigned, uses `tooltip`. */
group_tooltips: OptionalField(mapping(int, str), {}, 'Assign a tooltip to each group. Where not assigned, uses `tooltip`.'),
/** Color of the triangles and wireframe. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`. */
color: OptionalField(nullable(ColorT), null, 'Color of the triangles and wireframe. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`.'),
/** Tooltip shown when hovering over the mesh. Can be overwritten by `group_tooltips`. If not specified, uses the parent primitives group `tooltip`. */
tooltip: OptionalField(nullable(str), null, 'Tooltip shown when hovering over the mesh. Can be overwritten by `group_tooltips`. If not specified, uses the parent primitives group `tooltip`.'),
/** Determine whether to render triangles of the mesh. */
show_triangles: OptionalField(bool, true, 'Determine whether to render triangles of the mesh.'),
/** Determine whether to render wireframe of the mesh. */
show_wireframe: OptionalField(bool, false, 'Determine whether to render wireframe of the mesh.'),
/** Wireframe line width (in screen-space units). */
wireframe_width: OptionalField(float, 1, 'Wireframe line width (in screen-space units).'),
/** Wireframe color. If not specified, uses `group_colors`. */
wireframe_color: OptionalField(nullable(ColorT), null, 'Wireframe color. If not specified, uses `group_colors`.'),
};
const LinesParams = {
/** 3*n_vertices length array of floats with vertex position (x1, y1, z1, ...). */
vertices: RequiredField(FloatList, '3*n_vertices length array of floats with vertex position (x1, y1, z1, ...).'),
/** 2*n_lines length array of indices into vertices that form lines (l1_1, l1_2, ...). */
indices: RequiredField(IntList, '2*n_lines length array of indices into vertices that form lines (l1_1, l1_2, ...).'),
/** Assign a number to each triangle to group them. If not specified, each line is considered a separate group (line i = group i). */
line_groups: OptionalField(nullable(IntList), null, 'Assign a number to each triangle to group them. If not specified, each line is considered a separate group (line i = group i).'),
/** Assign a color to each group. Where not assigned, uses `color`. */
group_colors: OptionalField(mapping(int, ColorT), {}, 'Assign a color to each group. Where not assigned, uses `color`.'),
/** Assign a tooltip to each group. Where not assigned, uses `tooltip`. */
group_tooltips: OptionalField(mapping(int, str), {}, 'Assign a tooltip to each group. Where not assigned, uses `tooltip`.'),
/** Assign a line width to each group. Where not assigned, uses `width`. */
group_widths: OptionalField(mapping(int, float), {}, 'Assign a line width to each group. Where not assigned, uses `width`.'),
/** Color of the lines. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`. */
color: OptionalField(nullable(ColorT), null, 'Color of the lines. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`.'),
/** Tooltip shown when hovering over the lines. Can be overwritten by `group_tooltips`. If not specified, uses the parent primitives group `tooltip`. */
tooltip: OptionalField(nullable(str), null, 'Tooltip shown when hovering over the lines. Can be overwritten by `group_tooltips`. If not specified, uses the parent primitives group `tooltip`.'),
/** Line width (in screen-space units). Can be overwritten by `group_widths`. */
width: OptionalField(float, 1, 'Line width (in screen-space units). Can be overwritten by `group_widths`.'),
};
const TubeParams = {
..._TubeBase,
/** Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`. */
tooltip: OptionalField(nullable(str), null, 'Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`.'),
};
const DistanceMeasurementParams = {
..._TubeBase,
/** Template used to construct the label. Use {{distance}} as placeholder for the distance. */
label_template: OptionalField(str, '{{distance}}', 'Template used to construct the label. Use {{distance}} as placeholder for the distance.'),
/** Size of the label (text height in Angstroms). If not specified, size will be relative to the distance (see label_auto_size_scale, label_auto_size_min). */
label_size: OptionalField(nullable(float), null, 'Size of the label (text height in Angstroms). If not specified, size will be relative to the distance (see label_auto_size_scale, label_auto_size_min).'),
/** Scaling factor for relative size. */
label_auto_size_scale: OptionalField(float, 0.1, 'Scaling factor for relative size.'),
/** Minimum size for relative size. */
label_auto_size_min: OptionalField(float, 0, 'Minimum size for relative size.'),
/** Color of the label. If not specified, uses the parent primitives group `label_color`. */
label_color: OptionalField(nullable(ColorT), null, 'Color of the label. If not specified, uses the parent primitives group `label_color`.'),
};
const PrimitiveLabelParams = {
/** Position of this label. */
position: RequiredField(PrimitivePositionT, 'Position of this label.'),
/** The label. */
text: RequiredField(str, 'The label.'),
/** Size of the label (text height in Angstroms). */
label_size: OptionalField(float, 1, 'Size of the label (text height in Angstroms).'),
/** Color of the label. If not specified, uses the parent primitives group `label_color`. */
label_color: OptionalField(nullable(ColorT), null, 'Color of the label. If not specified, uses the parent primitives group `label_color`.'),
/** Camera-facing offset to prevent overlap with geometry. */
label_offset: OptionalField(float, 0, 'Camera-facing offset to prevent overlap with geometry.'),
};
export const MVSPrimitiveParams = UnionParamsSchema(
'kind',
'Kind of geometrical primitive',
{
'mesh': SimpleParamsSchema(MeshParams),
'lines': SimpleParamsSchema(LinesParams),
'tube': SimpleParamsSchema(TubeParams),
'distance_measurement': SimpleParamsSchema(DistanceMeasurementParams),
'label': SimpleParamsSchema(PrimitiveLabelParams),
},
);

View File

@@ -5,9 +5,10 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { float, int, list, literal, nullable, OptionalField, RequiredField, str, tuple, union } from '../generic/params-schema';
import { float, int, list, literal, nullable, OptionalField, RequiredField, str, tuple, union } from '../generic/field-schema';
import { SimpleParamsSchema } from '../generic/params-schema';
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema, TreeSchemaWithAllRequired } from '../generic/tree-schema';
import { MVSPrimitiveParams } from './mvs-primitives';
import { MVSPrimitiveParams } from './mvs-tree-primitives';
import { ColorT, ComponentExpressionT, ComponentSelectorT, Matrix, ParseFormatT, RepresentationTypeT, SchemaFormatT, SchemaT, StrList, StructureTypeT, Vector3 } from './param-types';
@@ -19,26 +20,26 @@ const _DataFromUriParams = {
/** Annotation schema defines what fields in the annotation will be taken into account. */
schema: RequiredField(SchemaT, 'Annotation schema defines what fields in the annotation will be taken into account.'),
/** Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`. */
block_header: OptionalField(nullable(str), 'Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`.'),
block_header: OptionalField(nullable(str), null, 'Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`.'),
/** 0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`). */
block_index: OptionalField(int, '0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`).'),
block_index: OptionalField(int, 0, '0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`).'),
/** Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used. */
category_name: OptionalField(nullable(str), 'Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used.'),
category_name: OptionalField(nullable(str), null, 'Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used.'),
/** Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...). The default value is 'color'/'label'/'tooltip'/'component' depending on the node type */
field_name: OptionalField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
field_name: RequiredField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
};
const _DataFromSourceParams = {
/** Annotation schema defines what fields in the annotation will be taken into account. */
schema: RequiredField(SchemaT, 'Annotation schema defines what fields in the annotation will be taken into account.'),
/** Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`. */
block_header: OptionalField(nullable(str), 'Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`.'),
block_header: OptionalField(nullable(str), null, 'Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`.'),
/** 0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`). */
block_index: OptionalField(int, '0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`).'),
block_index: OptionalField(int, 0, '0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`).'),
/** Name of the CIF category to read annotation from. If `null`, the first category in the block is used. */
category_name: OptionalField(nullable(str), 'Name of the CIF category to read annotation from. If `null`, the first category in the block is used.'),
category_name: OptionalField(nullable(str), null, 'Name of the CIF category to read annotation from. If `null`, the first category in the block is used.'),
/** Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...). The default value is 'color'/'label'/'tooltip'/'component' depending on the node type */
field_name: OptionalField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
field_name: RequiredField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
};
/** Schema for `MVSTree` (MolViewSpec tree) */
@@ -49,252 +50,274 @@ export const MVSTreeSchema = TreeSchema({
root: {
description: 'Auxiliary node kind that only appears as the tree root.',
parent: [],
params: {
},
params: SimpleParamsSchema({
}),
},
/** This node instructs to retrieve a data resource. */
download: {
description: 'This node instructs to retrieve a data resource.',
parent: ['root'],
params: {
params: SimpleParamsSchema({
/** URL of the data resource. */
url: RequiredField(str, 'URL of the data resource.'),
},
}),
},
/** This node instructs to parse a data resource. */
parse: {
description: 'This node instructs to parse a data resource.',
parent: ['download'],
params: {
params: SimpleParamsSchema({
/** Format of the input data resource. */
format: RequiredField(ParseFormatT, 'Format of the input data resource.'),
},
}),
},
/** This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation. */
structure: {
description: 'This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation.',
parent: ['parse'],
params: {
params: SimpleParamsSchema({
/** Type of structure to be created (`"model"` for original model coordinates, `"assembly"` for assembly structure, `"symmetry"` for a set of crystal unit cells based on Miller indices, `"symmetry_mates"` for a set of asymmetric units within a radius from the original model). */
type: RequiredField(StructureTypeT, 'Type of structure to be created (`"model"` for original model coordinates, `"assembly"` for assembly structure, `"symmetry"` for a set of crystal unit cells based on Miller indices, `"symmetry_mates"` for a set of asymmetric units within a radius from the original model).'),
/** Header of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF). If `null`, block is selected based on `block_index`. */
block_header: OptionalField(nullable(str), 'Header of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF). If `null`, block is selected based on `block_index`.'),
block_header: OptionalField(nullable(str), null, 'Header of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF). If `null`, block is selected based on `block_index`.'),
/** 0-based index of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF and `block_header` is `null`). */
block_index: OptionalField(int, '0-based index of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF and `block_header` is `null`).'),
block_index: OptionalField(int, 0, '0-based index of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF and `block_header` is `null`).'),
/** 0-based index of model in case the input data contain multiple models. */
model_index: OptionalField(int, '0-based index of model in case the input data contain multiple models.'),
model_index: OptionalField(int, 0, '0-based index of model in case the input data contain multiple models.'),
/** Assembly identifier (only applies when `kind` is `"assembly"`). If `null`, the first assembly is selected. */
assembly_id: OptionalField(nullable(str), 'Assembly identifier (only applies when `kind` is `"assembly"`). If `null`, the first assembly is selected.'),
assembly_id: OptionalField(nullable(str), null, 'Assembly identifier (only applies when `kind` is `"assembly"`). If `null`, the first assembly is selected.'),
/** Distance (in Angstroms) from the original model in which asymmetric units should be included (only applies when `kind` is `"symmetry_mates"`). */
radius: OptionalField(float, 'Distance (in Angstroms) from the original model in which asymmetric units should be included (only applies when `kind` is `"symmetry_mates"`).'),
radius: OptionalField(float, 5, 'Distance (in Angstroms) from the original model in which asymmetric units should be included (only applies when `kind` is `"symmetry_mates"`).'),
/** Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`). */
ijk_min: OptionalField(tuple([int, int, int]), 'Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`).'),
ijk_min: OptionalField(tuple([int, int, int]), [-1, -1, -1], 'Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`).'),
/** Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`). */
ijk_max: OptionalField(tuple([int, int, int]), 'Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`).'),
},
ijk_max: OptionalField(tuple([int, int, int]), [1, 1, 1], 'Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`).'),
}),
},
/** This node instructs to rotate and/or translate structure coordinates. */
transform: {
description: 'This node instructs to rotate and/or translate structure coordinates.',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
/** Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation). */
rotation: OptionalField(Matrix, 'Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation).'),
rotation: OptionalField(Matrix, [1, 0, 0, 0, 1, 0, 0, 0, 1], 'Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation).'),
/** Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation). */
translation: OptionalField(Vector3, 'Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation).'),
},
translation: OptionalField(Vector3, [0, 0, 0], 'Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation).'),
}),
},
/** This node instructs to create a component (i.e. a subset of the parent structure). */
component: {
description: 'This node instructs to create a component (i.e. a subset of the parent structure).',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
/** Defines what part of the parent structure should be included in this component. */
selector: RequiredField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'Defines what part of the parent structure should be included in this component.'),
},
}),
},
/** This node instructs to create a component defined by an external annotation resource. */
component_from_uri: {
description: 'This node instructs to create a component defined by an external annotation resource.',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
..._DataFromUriParams,
/** Name of the column in CIF or field name (key) in JSON that contains the component identifier. */
field_name: OptionalField(str, 'component', 'Name of the column in CIF or field name (key) in JSON that contains the component identifier.'),
/** List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation. */
field_values: OptionalField(nullable(list(str)), 'List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.'),
},
field_values: OptionalField(nullable(list(str)), null, 'List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.'),
}),
},
/** This node instructs to create a component defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
component_from_source: {
description: 'This node instructs to create a component defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
..._DataFromSourceParams,
/** Name of the column in CIF or field name (key) in JSON that contains the component identifier. */
field_name: OptionalField(str, 'component', 'Name of the column in CIF or field name (key) in JSON that contains the component identifier.'),
/** List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation. */
field_values: OptionalField(nullable(list(str)), 'List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.'),
},
field_values: OptionalField(nullable(list(str)), null, 'List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.'),
}),
},
/** This node instructs to create a visual representation of a component. */
representation: {
description: 'This node instructs to create a visual representation of a component.',
parent: ['component', 'component_from_uri', 'component_from_source'],
params: {
params: SimpleParamsSchema({
/** Method of visual representation of the component. */
type: RequiredField(RepresentationTypeT, 'Method of visual representation of the component.'),
},
}),
},
/** This node instructs to apply color to a visual representation. */
color: {
description: 'This node instructs to apply color to a visual representation.',
parent: ['representation'],
params: {
params: SimpleParamsSchema({
/** Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). */
color: RequiredField(ColorT, 'Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
/** Defines to what part of the representation this color should be applied. */
selector: OptionalField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'Defines to what part of the representation this color should be applied.'),
},
selector: OptionalField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'all', 'Defines to what part of the representation this color should be applied.'),
}),
},
/** This node instructs to apply colors to a visual representation. The colors are defined by an external annotation resource. */
color_from_uri: {
description: 'This node instructs to apply colors to a visual representation. The colors are defined by an external annotation resource.',
parent: ['representation'],
params: {
params: SimpleParamsSchema({
..._DataFromUriParams,
},
/** Name of the column in CIF or field name (key) in JSON that contains the color. */
field_name: OptionalField(str, 'color', 'Name of the column in CIF or field name (key) in JSON that contains the color.'),
}),
},
/** This node instructs to apply colors to a visual representation. The colors are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
color_from_source: {
description: 'This node instructs to apply colors to a visual representation. The colors are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
parent: ['representation'],
params: {
params: SimpleParamsSchema({
..._DataFromSourceParams,
},
/** Name of the column in CIF or field name (key) in JSON that contains the color. */
field_name: OptionalField(str, 'color', 'Name of the column in CIF or field name (key) in JSON that contains the color.'),
}),
},
/** This node instructs to apply opacity/transparency to a visual representation. */
opacity: {
description: 'This node instructs to apply opacity/transparency to a visual representation.',
parent: ['representation'],
params: {
params: SimpleParamsSchema({
/** Opacity of a representation. 0.0: fully transparent, 1.0: fully opaque. */
opacity: RequiredField(float, 'Opacity of a representation. 0.0: fully transparent, 1.0: fully opaque.'),
},
}),
},
/** This node instructs to add a label (textual visual representation) to a component. */
label: {
description: 'This node instructs to add a label (textual visual representation) to a component.',
parent: ['component', 'component_from_uri', 'component_from_source'],
params: {
params: SimpleParamsSchema({
/** Content of the shown label. */
text: RequiredField(str, 'Content of the shown label.'),
},
}),
},
/** This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an external annotation resource. */
label_from_uri: {
description: 'This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an external annotation resource.',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
..._DataFromUriParams,
},
/** Name of the column in CIF or field name (key) in JSON that contains the label text. */
field_name: OptionalField(str, 'label', 'Name of the column in CIF or field name (key) in JSON that contains the label text.'),
}),
},
/** This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
label_from_source: {
description: 'This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
..._DataFromSourceParams,
},
/** Name of the column in CIF or field name (key) in JSON that contains the label text. */
field_name: OptionalField(str, 'label', 'Name of the column in CIF or field name (key) in JSON that contains the label text.'),
}),
},
/** This node instructs to add a tooltip to a component. "Tooltip" is a text which is not a part of the visualization but should be presented to the users when they interact with the component (typically, the tooltip will be shown somewhere on the screen when the user hovers over a visual representation of the component). */
tooltip: {
description: 'This node instructs to add a tooltip to a component. "Tooltip" is a text which is not a part of the visualization but should be presented to the users when they interact with the component (typically, the tooltip will be shown somewhere on the screen when the user hovers over a visual representation of the component).',
parent: ['component', 'component_from_uri', 'component_from_source'],
params: {
params: SimpleParamsSchema({
/** Content of the shown tooltip. */
text: RequiredField(str, 'Content of the shown tooltip.'),
},
}),
},
/** This node instructs to add tooltips to parts of a structure. The tooltips are defined by an external annotation resource. */
tooltip_from_uri: {
description: 'This node instructs to add tooltips to parts of a structure. The tooltips are defined by an external annotation resource.',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
..._DataFromUriParams,
},
/** Name of the column in CIF or field name (key) in JSON that contains the tooltip text. */
field_name: OptionalField(str, 'tooltip', 'Name of the column in CIF or field name (key) in JSON that contains the tooltip text.'),
}),
},
/** This node instructs to add tooltips to parts of a structure. The tooltips are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
tooltip_from_source: {
description: 'This node instructs to add tooltips to parts of a structure. The tooltips are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
..._DataFromSourceParams,
},
/** Name of the column in CIF or field name (key) in JSON that contains the tooltip text. */
field_name: OptionalField(str, 'tooltip', 'Name of the column in CIF or field name (key) in JSON that contains the tooltip text.'),
}),
},
/** This node instructs to set the camera focus to a component (zoom in). */
focus: {
description: 'This node instructs to set the camera focus to a component (zoom in).',
parent: ['root', 'component', 'component_from_uri', 'component_from_source', 'primitives', 'primitives_from_uri'],
params: {
params: SimpleParamsSchema({
/** Vector describing the direction of the view (camera position -> focused target). */
direction: OptionalField(Vector3, 'Vector describing the direction of the view (camera position -> focused target).'),
direction: OptionalField(Vector3, [0, 0, -1], 'Vector describing the direction of the view (camera position -> focused target).'),
/** Vector which will be aligned with the screen Y axis. */
up: OptionalField(Vector3, 'Vector which will be aligned with the screen Y axis.'),
up: OptionalField(Vector3, [0, 1, 0], 'Vector which will be aligned with the screen Y axis.'),
/** Radius of the focused sphere (overrides `radius_factor` and `radius_extra`. */
radius: OptionalField(nullable(float), 'Radius of the focused sphere (overrides `radius_factor` and `radius_extra`).'),
radius: OptionalField(nullable(float), null, 'Radius of the focused sphere (overrides `radius_factor` and `radius_extra`).'),
/** Radius of the focused sphere relative to the radius of parent component (default: 1). Focused radius = component_radius * radius_factor + radius_extent. */
radius_factor: OptionalField(float, 'Radius of the focused sphere relative to the radius of parent component (default: 1). Focused radius = component_radius * radius_factor + radius_extent.'),
radius_factor: OptionalField(float, 1, 'Radius of the focused sphere relative to the radius of parent component (default: 1). Focused radius = component_radius * radius_factor + radius_extent.'),
/** Addition to the radius of the focused sphere, if computed from the radius of parent component (default: 0). Focused radius = component_radius * radius_factor + radius_extent. */
radius_extent: OptionalField(float, 'Addition to the radius of the focused sphere, if computed from the radius of parent component (default: 0). Focused radius = component_radius * radius_factor + radius_extent.'),
},
radius_extent: OptionalField(float, 0, 'Addition to the radius of the focused sphere, if computed from the radius of parent component (default: 0). Focused radius = component_radius * radius_factor + radius_extent.'),
}),
},
/** This node instructs to set the camera position and orientation. */
camera: {
description: 'This node instructs to set the camera position and orientation.',
parent: ['root'],
params: {
params: SimpleParamsSchema({
/** Coordinates of the point in space at which the camera is pointing. */
target: RequiredField(Vector3, 'Coordinates of the point in space at which the camera is pointing.'),
/** Coordinates of the camera. */
position: RequiredField(Vector3, 'Coordinates of the camera.'),
/** Vector which will be aligned with the screen Y axis. */
up: OptionalField(Vector3, 'Vector which will be aligned with the screen Y axis.'),
},
up: OptionalField(Vector3, [0, 1, 0], 'Vector which will be aligned with the screen Y axis.'),
}),
},
/** This node sets canvas properties. */
canvas: {
description: 'This node sets canvas properties.',
parent: ['root'],
params: {
params: SimpleParamsSchema({
/** Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). */
background_color: RequiredField(ColorT, 'Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
},
}),
},
primitives: {
description: 'This node groups a list of geometrical primitives',
parent: ['structure', 'root'],
params: {
color: OptionalField(nullable(ColorT)),
label_color: OptionalField(nullable(ColorT)),
tooltip: OptionalField(nullable(str)),
opacity: OptionalField(nullable(float)),
label_opacity: OptionalField(nullable(float)),
instances: OptionalField(nullable(list(Matrix))),
},
params: SimpleParamsSchema({
/** Default color for primitives in this group. */
color: OptionalField(ColorT, 'white', 'Default color for primitives in this group.'),
/** Default label color for primitives in this group. */
label_color: OptionalField(ColorT, 'white', 'Default label color for primitives in this group.'),
/** Default tooltip for primitives in this group. */
tooltip: OptionalField(nullable(str), null, 'Default tooltip for primitives in this group.'),
/** Opacity of primitive geometry in this group. */
opacity: OptionalField(float, 1, 'Opacity of primitive geometry in this group.'),
/** Opacity of primitive labels in this group. */
label_opacity: OptionalField(float, 1, 'Opacity of primitive labels in this group.'),
/** Instances of this primitive group defined as 4x4 column major (j * 4 + i indexing) transformation matrices. */
instances: OptionalField(nullable(list(Matrix)), null, 'Instances of this primitive group defined as 4x4 column major (j * 4 + i indexing) transformation matrices.'),
}),
},
primitives_from_uri: {
description: 'This node loads a list of primitives from URI',
parent: ['structure', 'root'],
params: {
uri: RequiredField(str),
format: RequiredField(literal('mvs-node-json')),
references: OptionalField(nullable(StrList)),
},
params: SimpleParamsSchema({
/** Location of the resource. */
uri: RequiredField(str, 'Location of the resource.'),
/** Format of the data. */
format: RequiredField(literal('mvs-node-json'), 'Format of the data.'),
/** List of nodes the data are referencing. */
references: OptionalField(StrList, [], 'List of nodes the data are referencing.'),
}),
},
primitive: {
description: 'This node represents a geometrical primitive',
parent: ['primitives'],
params: {
// TODO: validation
_union_: RequiredField(MVSPrimitiveParams),
},
params: MVSPrimitiveParams,
},
}
});

View File

@@ -6,8 +6,8 @@
*/
import * as iots from 'io-ts';
import { HexColor } from '../../helpers/utils';
import { ValueFor, float, int, list, literal, str, tuple, union } from '../generic/params-schema';
import { HexColor, ColorName } from '../../helpers/utils';
import { ValueFor, float, int, list, literal, str, tuple, union } from '../generic/field-schema';
import { ColorNames } from '../../../../mol-util/color/names';
@@ -80,11 +80,19 @@ export const HexColorT = new iots.Type<HexColor>(
value => value
);
/** `color` parameter values for `color` node in MVS tree */
export const ColorNameT = new iots.Type<ColorName>(
'ColorName',
((value: any) => typeof value === 'string') as any,
(value, ctx) => ColorName.is(value) ? { _tag: 'Right', right: value } : { _tag: 'Left', left: [{ value: value, context: ctx, message: `"${value}" is not a valid hex color string` }] },
value => value
);
/** `color` parameter values for `color` node in MVS tree */
export const ColorNamesT = literal(...Object.keys(ColorNames) as (keyof ColorNames)[]);
/** `color` parameter values for `color` node in MVS tree */
export const ColorT = union([HexColorT, ColorNamesT]);
export const ColorT = union([ColorNameT, HexColorT]);
/** Type helpers */
export function isVector3(x: any): x is Vector3 {
@@ -97,4 +105,4 @@ export function isPrimitiveComponentExpressions(x: any): x is PrimitiveComponent
export function isComponentExpression(x: any): x is ComponentExpressionT {
return !!x && typeof x === 'object' && !x.expressions;
}
}

View File

@@ -53,7 +53,12 @@ export class VolumeApiV2 {
public async getEntryList(maxEntries: number, keyword?: string): Promise<{ [source: string]: string[] }> {
const response = await fetch(this.entryListUrl(maxEntries, keyword));
return await response.json();
if (response.ok) {
return await response.json();
} else {
console.error('Failed to fetch "Volume & Segmentation" entry list');
return {};
}
}
public async getMetadata(source: string, entryId: string): Promise<Metadata> {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -392,6 +392,8 @@ namespace Canvas3D {
let y = 0;
let width = 128;
let height = 128;
let canvasScaleRatioX = 1;
let canvasScaleRatioY = 1;
let forceNextRender = false;
let currentTime = 0;
@@ -645,7 +647,7 @@ namespace Canvas3D {
function identify(x: number, y: number): PickData | undefined {
const cam = p.camera.stereo.name === 'on' ? stereoCamera : camera;
return webgl.isContextLost ? undefined : pickHelper.identify(x, y, cam);
return webgl.isContextLost ? undefined : pickHelper.identify(x / canvasScaleRatioX, y / canvasScaleRatioY, cam);
}
function commit(isSynchronous: boolean = false) {
@@ -1158,6 +1160,12 @@ namespace Canvas3D {
function updateViewport() {
const oldX = x, oldY = y, oldWidth = width, oldHeight = height;
const canvasRect = canvas?.getBoundingClientRect();
canvasScaleRatioX = (canvasRect?.width ?? gl.drawingBufferWidth) / gl.drawingBufferWidth;
if (!canvasScaleRatioX) canvasScaleRatioX = 1;
canvasScaleRatioY = (canvasRect?.height ?? gl.drawingBufferHeight) / gl.drawingBufferHeight;
if (!canvasScaleRatioY) canvasScaleRatioY = 1;
if (p.viewport.name === 'canvas') {
x = 0;
y = 0;
@@ -1184,7 +1192,7 @@ namespace Canvas3D {
pickHelper.setViewport(x, y, width, height);
renderer.setViewport(x, y, width, height);
Viewport.set(camera.viewport, x, y, width, height);
Viewport.set(controls.viewport, x, y, width, height);
Viewport.set(controls.viewport, x, y, width * canvasScaleRatioX, height * canvasScaleRatioY);
hiZ.setViewport(x, y, width, height);
}
}

View File

@@ -267,7 +267,7 @@ export class DpoitPass {
resources.texture('image-float32', 'rgba', 'float', 'nearest')
];
} else {
// in webgl1 drawbuffers must be in the same format for some reason
// webgl1 requires consistent bit plane counts
this.depthTextures = [
resources.texture('image-float32', 'rgba', 'float', 'nearest'),

View File

@@ -49,6 +49,7 @@ export interface DirectVolume {
readonly cartnToUnit: ValueCell<Mat4>
readonly packedGroup: ValueCell<boolean>
readonly axisOrder: ValueCell<Vec3>
readonly dataType: ValueCell<'byte' | 'float' | 'halfFloat'>
/** Bounding sphere of the volume */
readonly boundingSphere: Sphere3D
@@ -57,10 +58,10 @@ export interface DirectVolume {
}
export namespace DirectVolume {
export function create(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, directVolume?: DirectVolume): DirectVolume {
export function create(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, dataType: 'byte' | 'float' | 'halfFloat', directVolume?: DirectVolume): DirectVolume {
return directVolume ?
update(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, directVolume) :
fromData(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder);
update(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, dataType, directVolume) :
fromData(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, dataType);
}
function hashCode(directVolume: DirectVolume) {
@@ -71,7 +72,7 @@ export namespace DirectVolume {
]);
}
function fromData(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3): DirectVolume {
function fromData(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, dataType: 'byte' | 'float' | 'halfFloat'): DirectVolume {
const boundingSphere = Sphere3D();
let currentHash = -1;
@@ -103,6 +104,7 @@ export namespace DirectVolume {
},
packedGroup: ValueCell.create(packedGroup),
axisOrder: ValueCell.create(axisOrder),
dataType: ValueCell.create(dataType),
setBoundingSphere(sphere: Sphere3D) {
Sphere3D.copy(boundingSphere, sphere);
currentHash = hashCode(directVolume);
@@ -111,7 +113,7 @@ export namespace DirectVolume {
return directVolume;
}
function update(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, directVolume: DirectVolume): DirectVolume {
function update(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, dataType: 'byte' | 'float' | 'halfFloat', directVolume: DirectVolume): DirectVolume {
const width = texture.getWidth();
const height = texture.getHeight();
const depth = texture.getDepth();
@@ -129,6 +131,7 @@ export namespace DirectVolume {
ValueCell.update(directVolume.cartnToUnit, Mat4.invert(Mat4(), unitToCartn));
ValueCell.updateIfChanged(directVolume.packedGroup, packedGroup);
ValueCell.updateIfChanged(directVolume.axisOrder, Vec3.fromArray(directVolume.axisOrder.ref.value, axisOrder, 0));
ValueCell.updateIfChanged(directVolume.dataType, dataType);
return directVolume;
}
@@ -142,7 +145,8 @@ export namespace DirectVolume {
const stats = Grid.One.stats;
const packedGroup = false;
const axisOrder = Vec3.create(0, 1, 2);
return create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, directVolume);
const dataType = 'byte';
return create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, dataType, directVolume);
}
export const Params = {

View File

@@ -285,7 +285,7 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
webgl.namedTextures[ColorCountName] = resources.texture('image-float32', 'alpha', 'float', 'nearest');
}
} else {
// in webgl1 drawbuffers must be in the same format for some reason
// webgl1 requires consistent bit plane counts
// this is quite wasteful but good enough for medium size meshes
if (!webgl.namedTextures[ColorAccumulateName]) {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -7,7 +7,7 @@
import { ComputeRenderable, createComputeRenderable } from '../../renderable';
import { WebGLContext } from '../../webgl/context';
import { createComputeRenderItem } from '../../webgl/render-item';
import { Values, TextureSpec, UniformSpec } from '../../renderable/schema';
import { Values, TextureSpec, UniformSpec, DefineSpec } from '../../renderable/schema';
import { Texture } from '../../../mol-gl/webgl/texture';
import { ShaderCode } from '../../../mol-gl/shader-code';
import { ValueCell } from '../../../mol-util';
@@ -17,12 +17,14 @@ import { getTriCount } from './tables';
import { quad_vert } from '../../../mol-gl/shader/quad.vert';
import { activeVoxels_frag } from '../../../mol-gl/shader/marching-cubes/active-voxels.frag';
import { isTimingMode } from '../../../mol-util/debug';
import { isWebGL2 } from '../../webgl/compat';
const ActiveVoxelsSchema = {
...QuadSchema,
tTriCount: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
tVolumeData: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
dValueChannel: DefineSpec('string', ['red', 'alpha']),
uIsoValue: UniformSpec('f'),
uGridDim: UniformSpec('v3'),
@@ -34,12 +36,17 @@ type ActiveVoxelsValues = Values<typeof ActiveVoxelsSchema>
const ActiveVoxelsName = 'active-voxels';
function valueChannel(ctx: WebGLContext, volumeData: Texture) {
return isWebGL2(ctx.gl) && volumeData.format === ctx.gl.RED ? 'red' : 'alpha';
}
function getActiveVoxelsRenderable(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, scale: Vec2): ComputeRenderable<ActiveVoxelsValues> {
if (ctx.namedComputeRenderables[ActiveVoxelsName]) {
const v = ctx.namedComputeRenderables[ActiveVoxelsName].values as ActiveVoxelsValues;
ValueCell.update(v.uQuadScale, scale);
ValueCell.update(v.tVolumeData, volumeData);
ValueCell.update(v.dValueChannel, valueChannel(ctx, volumeData));
ValueCell.updateIfChanged(v.uIsoValue, isoValue);
ValueCell.update(v.uGridDim, gridDim);
ValueCell.update(v.uGridTexDim, gridTexDim);
@@ -59,6 +66,7 @@ function createActiveVoxelsRenderable(ctx: WebGLContext, volumeData: Texture, gr
uQuadScale: ValueCell.create(scale),
tVolumeData: ValueCell.create(volumeData),
dValueChannel: ValueCell.create(valueChannel(ctx, volumeData)),
uIsoValue: ValueCell.create(isoValue),
uGridDim: ValueCell.create(gridDim),
uGridTexDim: ValueCell.create(gridTexDim),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -28,6 +28,7 @@ const IsosurfaceSchema = {
tActiveVoxelsPyramid: TextureSpec('texture', 'rgba', 'float', 'nearest'),
tActiveVoxelsBase: TextureSpec('texture', 'rgba', 'float', 'nearest'),
tVolumeData: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
dValueChannel: DefineSpec('string', ['red', 'alpha']),
uIsoValue: UniformSpec('f'),
uSize: UniformSpec('f'),
@@ -48,6 +49,10 @@ type IsosurfaceValues = Values<typeof IsosurfaceSchema>
const IsosurfaceName = 'isosurface';
function valueChannel(ctx: WebGLContext, volumeData: Texture) {
return isWebGL2(ctx.gl) && volumeData.format === ctx.gl.RED ? 'red' : 'alpha';
}
function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean): ComputeRenderable<IsosurfaceValues> {
if (ctx.namedComputeRenderables[IsosurfaceName]) {
const v = ctx.namedComputeRenderables[IsosurfaceName].values as IsosurfaceValues;
@@ -55,6 +60,7 @@ function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture
ValueCell.update(v.tActiveVoxelsPyramid, activeVoxelsPyramid);
ValueCell.update(v.tActiveVoxelsBase, activeVoxelsBase);
ValueCell.update(v.tVolumeData, volumeData);
ValueCell.update(v.dValueChannel, valueChannel(ctx, volumeData));
ValueCell.updateIfChanged(v.uIsoValue, isoValue);
ValueCell.updateIfChanged(v.uSize, Math.pow(2, levels));
@@ -87,6 +93,7 @@ function createIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Text
tActiveVoxelsPyramid: ValueCell.create(activeVoxelsPyramid),
tActiveVoxelsBase: ValueCell.create(activeVoxelsBase),
tVolumeData: ValueCell.create(volumeData),
dValueChannel: ValueCell.create(valueChannel(ctx, volumeData)),
uIsoValue: ValueCell.create(isoValue),
uSize: ValueCell.create(Math.pow(2, levels)),
@@ -155,7 +162,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
: resources.texture('image-float32', 'rgba', 'float', 'nearest');
}
} else {
// in webgl1 drawbuffers must be in the same format for some reason
// webgl1 requires consistent bit plane counts
// this is quite wasteful but good enough for medium size meshes
if (!vertexTexture) {

View File

@@ -38,9 +38,14 @@ vec4 texture3dFrom2dNearest(sampler2D tex, vec3 pos, vec3 gridDim, vec2 texDim)
return texture2D(tex, coord);
}
vec4 voxel(vec3 pos) {
float voxelValue(vec3 pos) {
pos = min(max(vec3(0.0), pos), uGridDim - vec3(1.0));
return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
vec4 v = texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
#ifdef dValueChannel_red
return v.r;
#else
return v.a;
#endif
}
void main(void) {
@@ -48,14 +53,14 @@ void main(void) {
vec3 posXYZ = index3dFrom2d(uv);
// get MC case as the sum of corners that are below the given iso level
float c = step(voxel(posXYZ).a, uIsoValue)
+ 2. * step(voxel(posXYZ + c1).a, uIsoValue)
+ 4. * step(voxel(posXYZ + c2).a, uIsoValue)
+ 8. * step(voxel(posXYZ + c3).a, uIsoValue)
+ 16. * step(voxel(posXYZ + c4).a, uIsoValue)
+ 32. * step(voxel(posXYZ + c5).a, uIsoValue)
+ 64. * step(voxel(posXYZ + c6).a, uIsoValue)
+ 128. * step(voxel(posXYZ + c7).a, uIsoValue);
float c = step(voxelValue(posXYZ), uIsoValue)
+ 2. * step(voxelValue(posXYZ + c1), uIsoValue)
+ 4. * step(voxelValue(posXYZ + c2), uIsoValue)
+ 8. * step(voxelValue(posXYZ + c3), uIsoValue)
+ 16. * step(voxelValue(posXYZ + c4), uIsoValue)
+ 32. * step(voxelValue(posXYZ + c5), uIsoValue)
+ 64. * step(voxelValue(posXYZ + c6), uIsoValue)
+ 128. * step(voxelValue(posXYZ + c7), uIsoValue);
c *= step(c, 254.);
// handle out of bounds positions

View File

@@ -59,9 +59,14 @@ vec4 voxel(vec3 pos) {
return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
}
vec4 voxelPadded(vec3 pos) {
float voxelValuePadded(vec3 pos) {
pos = min(max(vec3(0.0), pos), uGridDim - vec3(vec2(2.0), 1.0)); // remove xy padding
return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
vec4 v = texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
#ifdef dValueChannel_red
return v.r;
#else
return v.a;
#endif
}
int idot2(const in ivec2 a, const in ivec2 b) {
@@ -261,8 +266,13 @@ void main(void) {
vec4 d0 = voxel(b0);
vec4 d1 = voxel(b1);
float v0 = d0.a;
float v1 = d1.a;
#ifdef dValueChannel_red
float v0 = d0.r;
float v1 = d1.r;
#else
float v0 = d0.a;
float v1 = d1.a;
#endif
float t = (uIsoValue - v0) / (v0 - v1);
gl_FragData[0].xyz = (uGridTransform * vec4(b0 + t * (b0 - b1), 1.0)).xyz;
@@ -286,14 +296,14 @@ void main(void) {
// normals from gradients
vec3 n0 = -normalize(vec3(
voxelPadded(b0 - c1).a - voxelPadded(b0 + c1).a,
voxelPadded(b0 - c3).a - voxelPadded(b0 + c3).a,
voxelPadded(b0 - c4).a - voxelPadded(b0 + c4).a
voxelValuePadded(b0 - c1) - voxelValuePadded(b0 + c1),
voxelValuePadded(b0 - c3) - voxelValuePadded(b0 + c3),
voxelValuePadded(b0 - c4) - voxelValuePadded(b0 + c4)
));
vec3 n1 = -normalize(vec3(
voxelPadded(b1 - c1).a - voxelPadded(b1 + c1).a,
voxelPadded(b1 - c3).a - voxelPadded(b1 + c3).a,
voxelPadded(b1 - c4).a - voxelPadded(b1 + c4).a
voxelValuePadded(b1 - c1) - voxelValuePadded(b1 + c1),
voxelValuePadded(b1 - c3) - voxelValuePadded(b1 + c3),
voxelValuePadded(b1 - c4) - voxelValuePadded(b1 + c4)
));
gl_FragData[2].xyz = -vec3(
n0.x + t * (n0.x - n1.x),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
@@ -59,14 +59,14 @@ export function getTarget(gl: GLRenderingContext, kind: TextureKind): number {
export function getFormat(gl: GLRenderingContext, format: TextureFormat, type: TextureType): number {
switch (format) {
case 'alpha':
if (isWebGL2(gl) && type === 'float') return gl.RED;
if (isWebGL2(gl) && (type === 'float' || type === 'fp16')) return gl.RED;
else if (isWebGL2(gl) && type === 'int') return gl.RED_INTEGER;
else return gl.ALPHA;
case 'rgb':
if (isWebGL2(gl) && type === 'int') return gl.RGB_INTEGER;
return gl.RGB;
case 'rg':
if (isWebGL2(gl) && type === 'float') return gl.RG;
if (isWebGL2(gl) && (type === 'float' || type === 'fp16')) return gl.RG;
else if (isWebGL2(gl) && type === 'int') return gl.RG_INTEGER;
else throw new Error('texture format "rg" requires webgl2 and type "float" or int"');
case 'rgba':

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -43,6 +43,7 @@ export interface Lookup3D<T = number> {
find(x: number, y: number, z: number, radius: number, result?: Result<T>): Result<T>,
nearest(x: number, y: number, z: number, k: number, stopIf?: Function, result?: Result<T>): Result<T>,
check(x: number, y: number, z: number, radius: number): boolean,
approxNearest(x: number, y: number, z: number, radius: number, result?: Result<T>): Result<T>,
readonly boundary: { readonly box: Box3D, readonly sphere: Sphere3D }
/** transient result */
readonly result: Result<T>

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -14,6 +14,7 @@ import { Vec3 } from '../../linear-algebra';
import { OrderedSet } from '../../../mol-data/int';
import { Boundary } from '../boundary';
import { FibonacciHeap } from '../../../mol-util/fibonacci-heap';
import { memoize1 } from '../../../mol-util/memoize';
interface GridLookup3D<T = number> extends Lookup3D<T> {
readonly buckets: { readonly offset: ArrayLike<number>, readonly count: ArrayLike<number>, readonly array: ArrayLike<number> }
@@ -62,6 +63,17 @@ class GridLookup3DImpl<T extends number = number> implements GridLookup3D<T> {
return query(this.ctx, this.result);
}
approxNearest(x: number, y: number, z: number, radius: number, result?: Result<T>): Result<T> {
this.ctx.x = x;
this.ctx.y = y;
this.ctx.z = z;
this.ctx.radius = radius;
this.ctx.isCheck = false;
const ret = result ?? this.result;
approxQueryNearest(this.ctx, ret);
return ret;
}
constructor(data: PositionData, boundary: Boundary, cellSizeOrCount?: Vec3 | number) {
const structure = build(data, boundary, cellSizeOrCount);
this.ctx = createContext(structure);
@@ -294,6 +306,84 @@ function query<T extends number = number>(ctx: QueryContext, result: Result<T>):
return result.count > 0;
}
function _insideOut(r: number) {
const cells: Vec3[] = [];
const n = r * 2 + 1;
for (let x = 0; x < n; ++x) {
for (let y = 0; y < n; ++y) {
for (let z = 0; z < n; ++z) {
cells.push(Vec3.create(x - r, y - r, z - r));
}
}
}
cells.sort((a, b) => Vec3.squaredMagnitude(a) - Vec3.squaredMagnitude(b));
return cells.flat();
}
const insideOut = memoize1(_insideOut);
/**
* The maximum error is on the order of cell size + max radius (if the grid has radii).
*/
function approxQueryNearest<T extends number = number>(ctx: QueryContext, result: Result<T>): boolean {
const { min, size: [sX, sY, sZ], bucketOffset, bucketCounts, bucketArray, grid, data: { x: px, y: py, z: pz, indices }, delta } = ctx.grid;
const { radius, x, y, z } = ctx;
const rSq = radius * radius;
Result.reset(result);
const loX = Math.max(0, Math.floor((x - radius - min[0]) / delta[0]));
const loY = Math.max(0, Math.floor((y - radius - min[1]) / delta[1]));
const loZ = Math.max(0, Math.floor((z - radius - min[2]) / delta[2]));
const hiX = Math.min(sX - 1, Math.floor((x + radius - min[0]) / delta[0]));
const hiY = Math.min(sY - 1, Math.floor((y + radius - min[1]) / delta[1]));
const hiZ = Math.min(sZ - 1, Math.floor((z + radius - min[2]) / delta[2]));
if (loX > hiX || loY > hiY || loZ > hiZ) return false;
const miX = Math.floor((x - min[0]) / delta[0]);
const miY = Math.floor((y - min[1]) / delta[1]);
const miZ = Math.floor((z - min[2]) / delta[2]);
const cells = insideOut(Math.max(hiX - loX, hiY - loY, hiZ - loZ) + 1);
for (let i = 0, _i = cells.length; i < _i; i += 3) {
const ix = miX + cells[i];
const iy = miY + cells[i + 1];
const iz = miZ + cells[i + 2];
if (ix < loX || ix > hiX || iy < loY || iy > hiY || iz < loZ || iz > hiZ) continue;
const bucketIdx = grid[(((ix * sY) + iy) * sZ) + iz];
if (bucketIdx === 0) continue;
const k = bucketIdx - 1;
const offset = bucketOffset[k];
const count = bucketCounts[k];
const end = offset + count;
let minDistSq = Number.MAX_VALUE;
for (let i = offset; i < end; i++) {
const idx = OrderedSet.getAt(indices, bucketArray[i]);
const dx = px[idx] - x;
const dy = py[idx] - y;
const dz = pz[idx] - z;
const distSq = dx * dx + dy * dy + dz * dz;
if (distSq <= rSq && distSq < minDistSq) {
Result.add(result, bucketArray[i], distSq);
minDistSq = distSq;
}
}
if (minDistSq !== Number.MAX_VALUE) return true;
}
return result.count > 0;
}
const tmpDirVec = Vec3();
const tmpVec = Vec3();
const tmpSetG = new Set<number>();

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -240,6 +240,36 @@ export class StructureLookup3D {
return false;
}
approxNearest(x: number, y: number, z: number, radius: number, ctx?: StructureLookup3DResultContext): StructureResult {
return this._approxNearest(x, y, z, radius, ctx ?? this.findContext);
}
_approxNearest(x: number, y: number, z: number, radius: number, ctx: StructureLookup3DResultContext): StructureResult {
Result.reset(ctx.result);
const { units } = this.structure;
const closeUnits = this.unitLookup.find(x, y, z, radius, ctx.closeUnitsResult);
if (closeUnits.count === 0) return ctx.result;
let minDistSq = Number.MAX_VALUE;
for (let t = 0, _t = closeUnits.count; t < _t; t++) {
const unit = units[closeUnits.indices[t]];
Vec3.set(this.pivot, x, y, z);
if (!unit.conformation.operator.isIdentity) {
Vec3.transformMat4(this.pivot, this.pivot, unit.conformation.operator.inverse);
}
const unitLookup = unit.lookup3d;
const groupResult = unitLookup.approxNearest(this.pivot[0], this.pivot[1], this.pivot[2], radius, ctx.unitGroupResult);
for (let j = 0, _j = groupResult.count; j < _j; j++) {
if (groupResult.squaredDistances[j] < minDistSq) {
StructureResult.add(ctx.result, unit, groupResult.indices[j], groupResult.squaredDistances[j]);
minDistSq = groupResult.squaredDistances[j];
}
}
}
return ctx.result;
}
get boundary() {
return this.structure.boundary;
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -8,6 +8,7 @@
import * as React from 'react';
import { TextInput } from './common';
import { noop } from '../../mol-util';
import { normalizeWheel } from '../../mol-util/input/input-observer';
export class Slider extends React.Component<{
min: number,
@@ -57,6 +58,14 @@ export class Slider extends React.Component<{
this.props.onChange(this.state.current);
};
onMouseWheel = (e: WheelEvent) => {
const { dx, dy, dz } = normalizeWheel(e);
const sign = (dx >= 0 ? 1 : -1) * (dy >= 0 ? 1 : -1) * (dz >= 0 ? 1 : -1);
const shift = e.getModifierState('Shift');
const delta = sign * (this.props.max - this.props.min) / (shift ? 100 : 25);
this.updateCurrent(this.state.current + delta);
};
render() {
let step = this.props.step;
if (step === void 0) step = 1;
@@ -64,6 +73,7 @@ export class Slider extends React.Component<{
<div>
<SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled}
onBeforeChange={this.begin}
onWheel={this.onMouseWheel}
onChange={this.updateCurrent as any} onAfterChange={this.end as any} />
</div>
<div>
@@ -258,6 +268,7 @@ export interface SliderBaseProps {
vertical?: boolean,
allowCross?: boolean,
pushable?: boolean | number,
onWheel?: (ev: WheelEvent) => any,
}
export interface SliderBaseState {
@@ -750,6 +761,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
<div ref={this.sliderElement} className={sliderClassName}
onTouchStart={disabled ? noop : this.onTouchStart as any}
onMouseDown={disabled ? noop : this.onMouseDown as any}
onWheel={disabled ? noop : this.props.onWheel as any}
>
<div className={`${prefixCls}-rail`} />
{handles}

View File

@@ -1,33 +1,36 @@
/**
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import * as React from 'react';
import { Volume } from '../../mol-model/volume';
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
import { StructureHierarchyManager } from '../../mol-plugin-state/manager/structure/hierarchy';
import { VolumeHierarchyManager } from '../../mol-plugin-state/manager/volume/hierarchy';
import { LazyVolumeRef, VolumeRef, VolumeRepresentationRef } from '../../mol-plugin-state/manager/volume/hierarchy-state';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
import { VolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/behavior';
import { InitVolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/transformers';
import { PluginCommands } from '../../mol-plugin/commands';
import { State, StateObjectCell, StateObjectSelector, StateSelection, StateTransform } from '../../mol-state';
import { Color } from '../../mol-util/color';
import { memoizeLatest } from '../../mol-util/memoize';
import { ParamDefinition } from '../../mol-util/param-definition';
import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base';
import { ActionMenu } from '../controls/action-menu';
import { CombinedColorControl } from '../controls/color';
import { Button, ControlGroup, ExpandGroup, IconButton } from '../controls/common';
import { AddSvg, BlurOnSvg, CheckSvg, CloseSvg, DeleteOutlinedSvg, ErrorSvg, MoreHorizSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg } from '../controls/icons';
import { ParameterControls, ParamOnChange } from '../controls/parameters';
import { ApplyActionControl } from '../state/apply-action';
import { UpdateTransformControl } from '../state/update-transform';
import { BindingsHelp } from '../viewport/help';
import { PluginCommands } from '../../mol-plugin/commands';
import { BlurOnSvg, ErrorSvg, CheckSvg, AddSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, DeleteOutlinedSvg, MoreHorizSvg, CloseSvg } from '../controls/icons';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
import { Color } from '../../mol-util/color';
import { ParamDefinition } from '../../mol-util/param-definition';
import { CombinedColorControl } from '../controls/color';
import { ParamOnChange } from '../controls/parameters';
import { Subject, throttleTime } from 'rxjs';
interface VolumeStreamingControlState extends CollapsableState {
isBusy: boolean
@@ -243,36 +246,57 @@ export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceCo
toggleHierarchy = () => this.setState({ show: this.state.show !== 'hierarchy' ? 'hierarchy' : void 0 });
toggleAddRepr = () => this.setState({ show: this.state.show !== 'add-repr' ? 'add-repr' : void 0 });
toggleVisibility = () => {
const mng = this.plugin.managers.volume.hierarchy;
const { current } = mng;
const globalVisibility = !current.volumes[0]?.representations[0]?.cell.state.isHidden;
this.plugin.managers.volume.hierarchy.toggleVisibility(current.volumes.flatMap(v => v.representations), globalVisibility ? 'hide' : 'show');
};
renderControls() {
const disabled = this.state.isBusy || this.isEmpty;
const label = this.label;
const selected = this.plugin.managers.volume.hierarchy.selection;
const mng = this.plugin.managers.volume.hierarchy;
const { current } = mng;
return <>
<div className='msp-flex-row' style={{ marginTop: '1px' }}>
<Button noOverflow flex onClick={this.toggleHierarchy} disabled={disabled} title={label}>{label}</Button>
{!this.isEmpty && selected && <IconButton svg={AddSvg} onClick={this.toggleAddRepr} title='Apply a structure presets to the current hierarchy.' toggleState={this.state.show === 'add-repr'} disabled={disabled} />}
{!this.isEmpty && <IconButton svg={VisibilityOutlinedSvg} onClick={this.toggleVisibility} toggleState={false} title='Toggle visibility of all volumes.' disabled={disabled} />}
</div>
{this.state.show === 'hierarchy' && <ActionMenu items={this.hierarchyItems} onSelect={this.selectCurrent} />}
{this.state.show === 'add-repr' && <ActionMenu items={this.addActions} onSelect={this.selectAdd} />}
{selected && selected.representations.length > 0 && <div style={{ marginTop: '6px' }}>
{selected.representations.map(r => <VolumeRepresentationControls key={r.cell.transform.ref} representation={r} />)}
{current.volumes.length > 0 && <div style={{ marginTop: '6px' }}>
{current.volumes.map((volume) => <VolumeEntryControls volume={volume} key={volume.cell.transform.ref} />)}
</div>}
</>;
}
}
function VolumeEntryControls({ volume }: { volume: VolumeRef }) {
return <>
<div className='msp-control-group-header' style={{ marginTop: '1px' }}>
<div><b>{volume.cell.obj?.label ?? 'n/a'}</b></div>
</div>
{volume.representations.map(r => <VolumeRepresentationControls key={r.cell.transform.ref} volume={volume} representation={r} />)}
</>;
}
type VolumeRepresentationEntryActions = 'update' | 'select-color'
class VolumeRepresentationControls extends PurePluginUIComponent<{ representation: VolumeRepresentationRef }, { action?: VolumeRepresentationEntryActions }> {
class VolumeRepresentationControls extends PurePluginUIComponent<{ volume: VolumeRef, representation: VolumeRepresentationRef }, { action?: VolumeRepresentationEntryActions }> {
state = { action: void 0 as VolumeRepresentationEntryActions | undefined };
updateIsoValueEvent = new Subject<{ isoValue: Volume.IsoValue }>();
componentDidMount() {
this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
if (State.ObjectEvent.isCell(e, this.props.representation.cell)) this.forceUpdate();
});
this.subscribe(this.updateIsoValueEvent.pipe(throttleTime(100, undefined, { leading: false, trailing: true })), this.updateIsoValue);
}
remove = () => this.plugin.managers.volume.hierarchy.remove([this.props.representation], true);
@@ -325,9 +349,42 @@ class VolumeRepresentationControls extends PurePluginUIComponent<{ representatio
}).commit();
};
requestIsoValueUpdate = (values: { isoValue: Volume.IsoValue }) => {
this.updateIsoValueEvent.next(values);
};
updateIsoValue = (values: { isoValue: Volume.IsoValue }) => {
const t = this.props.representation.cell.transform;
return this.plugin.build().to(t.ref).update({
...t.params,
type: {
...t.params?.type,
params: {
...t.params?.type.params,
isoValue: values.isoValue
}
}
}).commit();
};
get isIsoSurface() {
const repr = this.props.representation.cell;
return repr.transform.params?.type.name === 'isosurface';
}
isoValueParams = memoizeLatest((_: any) => {
const repr = this.props.representation.cell;
const params = repr.transform.params;
if (params?.type.name !== 'isosurface') return undefined;
return { isoValue: Volume.createIsoValueParam(params.type.params.isoValue, this.props.volume.cell.obj?.data.grid?.stats) };
});
render() {
const repr = this.props.representation.cell;
const params = repr.transform.params;
const color = this.color;
const isoParams = this.isoValueParams(repr.transform.ref);
return <>
<div className='msp-flex-row'>
{color !== void 0 && <Button style={{ backgroundColor: Color.toStyle(color), minWidth: 32, width: 32 }} onClick={this.toggleColor} />}
@@ -340,7 +397,12 @@ class VolumeRepresentationControls extends PurePluginUIComponent<{ representatio
<IconButton svg={MoreHorizSvg} onClick={this.toggleUpdate} title='Actions' toggleState={this.state.action === 'update'} />
</div>
{this.state.action === 'update' && !!repr.parent && <div style={{ marginBottom: '6px' }} className='msp-accent-offset'>
<UpdateTransformControl state={repr.parent} transform={repr.transform} customHeader='none' noMargin />
<div>
{!!isoParams && <div style={{ marginBottom: '1px' }}>
<ParameterControls params={isoParams} values={{ isoValue: params?.type.params.isoValue }} onChangeValues={this.requestIsoValueUpdate} />
</div>}
<UpdateTransformControl state={repr.parent} transform={repr.transform} customHeader='none' noMargin />
</div>
</div>}
{this.state.action === 'select-color' && color !== void 0 && <div style={{ marginBottom: '6px', marginTop: 1 }} className='msp-accent-offset'>
<ControlGroup header='Select Color' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleColor}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -31,7 +31,7 @@ async function createGaussianDensityVolume(ctx: VisualContext, structure: Struct
const unitToCartn = Mat4.mul(Mat4(), transform, Mat4.fromScaling(Mat4(), gridDim));
const cellDim = Mat4.getScaling(Vec3(), transform);
const axisOrder = Vec3.create(0, 1, 2);
const vol = DirectVolume.create(bbox, gridDim, transform, unitToCartn, cellDim, texture, stats, true, axisOrder, directVolume);
const vol = DirectVolume.create(bbox, gridDim, transform, unitToCartn, cellDim, texture, stats, true, axisOrder, 'byte', directVolume);
const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, densityTextureData.maxRadius);
vol.setBoundingSphere(sphere);
@@ -89,7 +89,7 @@ async function createUnitsGaussianDensityVolume(ctx: VisualContext, unit: Unit,
const unitToCartn = Mat4.mul(Mat4(), transform, Mat4.fromScaling(Mat4(), gridDim));
const cellDim = Mat4.getScaling(Vec3(), transform);
const axisOrder = Vec3.create(0, 1, 2);
const vol = DirectVolume.create(bbox, gridDim, transform, unitToCartn, cellDim, texture, stats, true, axisOrder, directVolume);
const vol = DirectVolume.create(bbox, gridDim, transform, unitToCartn, cellDim, texture, stats, true, axisOrder, 'byte', directVolume);
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, densityTextureData.maxRadius);
vol.setBoundingSphere(sphere);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -22,6 +22,7 @@ import { Interval } from '../../mol-data/int';
import { Loci, EmptyLoci } from '../../mol-model/loci';
import { PickingId } from '../../mol-geo/geometry/picking';
import { createVolumeTexture2d, createVolumeTexture3d, eachVolumeLoci, getVolumeTexture2dLayout } from './util';
import { Texture } from '../../mol-gl/webgl/texture';
function getBoundingBox(gridDimension: Vec3, transform: Mat4) {
const bbox = Box3D();
@@ -32,24 +33,35 @@ function getBoundingBox(gridDimension: Vec3, transform: Mat4) {
// 2d volume texture
export function createDirectVolume2d(ctx: RuntimeContext, webgl: WebGLContext, volume: Volume, directVolume?: DirectVolume) {
export function createDirectVolume2d(ctx: RuntimeContext, webgl: WebGLContext, volume: Volume, props: PD.Values<DirectVolumeParams>, directVolume?: DirectVolume) {
const gridDimension = volume.grid.cells.space.dimensions as Vec3;
const { width, height } = getVolumeTexture2dLayout(gridDimension);
if (Math.max(width, height) > webgl.maxTextureSize / 2) {
throw new Error('volume too large for direct-volume rendering');
}
const textureImage = createVolumeTexture2d(volume, 'normals');
const dataType = props.dataType === 'halfFloat' && !webgl.extensions.textureHalfFloat ? 'float' : props.dataType;
const textureImage = createVolumeTexture2d(volume, 'normals', 0, dataType);
// debugTexture(createImageData(textureImage.array, textureImage.width, textureImage.height), 1/3)
const transform = Grid.getGridToCartesianTransform(volume.grid);
const bbox = getBoundingBox(gridDimension, transform);
const texture = directVolume ? directVolume.gridTexture.ref.value : webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
let texture: Texture;
if (directVolume && directVolume.dataType.ref.value === dataType) {
texture = directVolume.gridTexture.ref.value;
} else {
texture = dataType === 'byte'
? webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear')
: dataType === 'halfFloat'
? webgl.resources.texture('image-float16', 'rgba', 'fp16', 'linear')
: webgl.resources.texture('image-float32', 'rgba', 'float', 'linear');
}
texture.load(textureImage);
const { unitToCartn, cellDim } = getUnitToCartn(volume.grid);
const axisOrder = volume.grid.cells.space.axisOrderSlowToFast as Vec3;
return DirectVolume.create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, volume.grid.stats, false, axisOrder, directVolume);
return DirectVolume.create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, volume.grid.stats, false, axisOrder, dataType, directVolume);
}
// 3d volume texture
@@ -76,33 +88,44 @@ function getUnitToCartn(grid: Grid) {
};
}
export function createDirectVolume3d(ctx: RuntimeContext, webgl: WebGLContext, volume: Volume, directVolume?: DirectVolume) {
export function createDirectVolume3d(ctx: RuntimeContext, webgl: WebGLContext, volume: Volume, props: PD.Values<DirectVolumeParams>, directVolume?: DirectVolume) {
const gridDimension = volume.grid.cells.space.dimensions as Vec3;
if (Math.max(...gridDimension) > webgl.max3dTextureSize / 2) {
throw new Error('volume too large for direct-volume rendering');
}
const textureVolume = createVolumeTexture3d(volume);
const dataType = props.dataType === 'halfFloat' && !webgl.extensions.textureHalfFloat ? 'float' : props.dataType;
const textureVolume = createVolumeTexture3d(volume, dataType);
const transform = Grid.getGridToCartesianTransform(volume.grid);
const bbox = getBoundingBox(gridDimension, transform);
const texture = directVolume ? directVolume.gridTexture.ref.value : webgl.resources.texture('volume-uint8', 'rgba', 'ubyte', 'linear');
let texture: Texture;
if (directVolume && directVolume.dataType.ref.value === dataType) {
texture = directVolume.gridTexture.ref.value;
} else {
texture = dataType === 'byte'
? webgl.resources.texture('volume-uint8', 'rgba', 'ubyte', 'linear')
: dataType === 'halfFloat'
? webgl.resources.texture('volume-float16', 'rgba', 'fp16', 'linear')
: webgl.resources.texture('volume-float32', 'rgba', 'float', 'linear');
}
texture.load(textureVolume);
const { unitToCartn, cellDim } = getUnitToCartn(volume.grid);
const axisOrder = volume.grid.cells.space.axisOrderSlowToFast as Vec3;
return DirectVolume.create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, volume.grid.stats, false, axisOrder, directVolume);
return DirectVolume.create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, volume.grid.stats, false, axisOrder, dataType, directVolume);
}
//
export async function createDirectVolume(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: PD.Values<DirectVolumeParams>, directVolume?: DirectVolume) {
const { runtime, webgl } = ctx;
if (webgl === undefined) throw new Error('DirectVolumeVisual requires `webgl` in props');
if (webgl === undefined) throw new Error('DirectVolumeVisual requires `webgl` in VisualContext');
return webgl.isWebGL2 ?
createDirectVolume3d(runtime, webgl, volume, directVolume) :
createDirectVolume2d(runtime, webgl, volume, directVolume);
createDirectVolume3d(runtime, webgl, volume, props, directVolume) :
createDirectVolume2d(runtime, webgl, volume, props, directVolume);
}
function getLoci(volume: Volume, props: PD.Values<DirectVolumeParams>) {
@@ -126,6 +149,7 @@ export function eachDirectVolume(loci: Loci, volume: Volume, key: number, props:
export const DirectVolumeParams = {
...DirectVolume.Params,
quality: { ...DirectVolume.Params.quality, isEssential: false },
dataType: PD.Select('byte', PD.arrayToOptions(['byte', 'float', 'halfFloat'] as const)),
};
export type DirectVolumeParams = typeof DirectVolumeParams
export function getDirectVolumeParams(ctx: ThemeRegistryContext, volume: Volume) {
@@ -143,6 +167,7 @@ export function DirectVolumeVisual(materialId: number): VolumeVisual<DirectVolum
getLoci: getDirectVolumeLoci,
eachLocation: eachDirectVolume,
setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<DirectVolumeParams>, currentProps: PD.Values<DirectVolumeParams>) => {
state.createGeometry = newProps.dataType !== currentProps.dataType;
},
geometryUtils: DirectVolume.Utils,
dispose: (geometry: DirectVolume) => {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -32,11 +32,19 @@ import { BaseGeometry } from '../../mol-geo/geometry/base';
import { ValueCell } from '../../mol-util/value-cell';
export const VolumeIsosurfaceParams = {
isoValue: Volume.IsoValueParam
isoValue: Volume.IsoValueParam,
};
export type VolumeIsosurfaceParams = typeof VolumeIsosurfaceParams
export type VolumeIsosurfaceProps = PD.Values<VolumeIsosurfaceParams>
export const VolumeIsosurfaceTextureParams = {
isoValue: Volume.IsoValueParam,
tryUseGpu: PD.Boolean(true),
gpuDataType: PD.Select('byte', PD.arrayToOptions(['byte', 'float', 'halfFloat'] as const), { hideIf: p => !p.tryUseGpu }),
};
export type VolumeIsosurfaceGpuParams = typeof VolumeIsosurfaceTextureParams
export type VolumeIsosurfaceGpuProps = PD.Values<VolumeIsosurfaceGpuParams>
function gpuSupport(webgl: WebGLContext) {
return webgl.extensions.colorBufferFloat && webgl.extensions.textureFloat && webgl.extensions.drawBuffers;
}
@@ -117,8 +125,8 @@ export const IsosurfaceMeshParams = {
...Mesh.Params,
...TextureMesh.Params,
...VolumeIsosurfaceParams,
...VolumeIsosurfaceTextureParams,
quality: { ...Mesh.Params.quality, isEssential: false },
tryUseGpu: PD.Boolean(true),
};
export type IsosurfaceMeshParams = typeof IsosurfaceMeshParams
@@ -144,9 +152,7 @@ export function IsosurfaceMeshVisual(materialId: number): VolumeVisual<Isosurfac
namespace VolumeIsosurfaceTexture {
const name = 'volume-isosurface-texture';
export const descriptor = CustomPropertyDescriptor({ name });
export function get(volume: Volume, webgl: WebGLContext) {
const { resources } = webgl;
export function get(volume: Volume, webgl: WebGLContext, props: VolumeIsosurfaceGpuProps) {
const transform = Grid.getGridToCartesianTransform(volume.grid);
const gridDimension = Vec3.clone(volume.grid.cells.space.dimensions as Vec3);
const { width, height, powerOfTwoSize: texDim } = getVolumeTexture2dLayout(gridDimension, Padding);
@@ -158,12 +164,18 @@ namespace VolumeIsosurfaceTexture {
throw new Error('volume too large for gpu isosurface extraction');
}
if (!volume._propertyData[name]) {
volume._propertyData[name] = resources.texture('image-uint8', 'alpha', 'ubyte', 'linear');
const texture = volume._propertyData[name] as Texture;
const dataType = props.gpuDataType === 'halfFloat' && !webgl.extensions.textureHalfFloat ? 'float' : props.gpuDataType;
if (volume._propertyData[name]?.dataType !== dataType) {
const texture = dataType === 'byte'
? webgl.resources.texture('image-uint8', 'alpha', 'ubyte', 'linear')
: dataType === 'halfFloat'
? webgl.resources.texture('image-float16', 'alpha', 'fp16', 'linear')
: webgl.resources.texture('image-float32', 'alpha', 'float', 'linear');
volume._propertyData[name] = { texture, dataType };
texture.define(texDim, texDim);
// load volume into sub-section of texture
texture.load(createVolumeTexture2d(volume, 'data', Padding), true);
texture.load(createVolumeTexture2d(volume, 'data', Padding, dataType), true);
volume.customProperties.add(descriptor);
volume.customProperties.assets(descriptor, [{ dispose: () => texture.destroy() }]);
}
@@ -172,7 +184,7 @@ namespace VolumeIsosurfaceTexture {
gridDimension[1] += Padding;
return {
texture: volume._propertyData[name] as Texture,
texture: volume._propertyData[name].texture as Texture,
transform,
gridDimension,
gridTexDim,
@@ -181,7 +193,7 @@ namespace VolumeIsosurfaceTexture {
}
}
async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: VolumeIsosurfaceProps, textureMesh?: TextureMesh) {
async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: VolumeIsosurfaceGpuProps, textureMesh?: TextureMesh) {
if (!ctx.webgl) throw new Error('webgl context required to create volume isosurface texture-mesh');
if (volume.grid.cells.data.length <= 1) {
@@ -193,7 +205,7 @@ async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Vol
const value = Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue;
const isoLevel = ((value - min) / diff);
const { texture, gridDimension, gridTexDim, gridTexScale, transform } = VolumeIsosurfaceTexture.get(volume, ctx.webgl);
const { texture, gridDimension, gridTexDim, gridTexScale, transform } = VolumeIsosurfaceTexture.get(volume, ctx.webgl, props);
const axisOrder = volume.grid.cells.space.axisOrderSlowToFast as Vec3;
const buffer = textureMesh?.doubleBuffer.get();
@@ -216,6 +228,7 @@ export function IsosurfaceTextureMeshVisual(materialId: number): VolumeVisual<Is
eachLocation: eachIsosurface,
setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<IsosurfaceMeshParams>, currentProps: PD.Values<IsosurfaceMeshParams>) => {
if (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats)) state.createGeometry = true;
if (newProps.gpuDataType !== currentProps.gpuDataType) state.createGeometry = true;
},
geometryUtils: TextureMesh.Utils,
mustRecreate: (volumeKey: VolumeKey, props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) => {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -12,6 +12,8 @@ import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
import { packIntToRGBArray } from '../../mol-util/number-packing';
import { SetUtils } from '../../mol-util/set';
import { Box3D } from '../../mol-math/geometry';
import { toHalfFloat } from '../../mol-util/number-conversion';
import { clamp } from '../../mol-math/interpolate';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const v3set = Vec3.set;
@@ -97,14 +99,18 @@ export function getVolumeTexture2dLayout(dim: Vec3, padding = 0) {
return { width, height, columns, rows, powerOfTwoSize: height < powerOfTwoSize ? powerOfTwoSize : powerOfTwoSize * 2 };
}
export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'groups' | 'data', padding = 0) {
export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'groups' | 'data', padding = 0, type: 'byte' | 'float' | 'halfFloat' = 'byte') {
const { cells: { space, data }, stats: { max, min } } = volume.grid;
const dim = space.dimensions as Vec3;
const { dataOffset: o } = space;
const { width, height } = getVolumeTexture2dLayout(dim, padding);
const itemSize = variant === 'data' ? 1 : 4;
const array = new Uint8Array(width * height * itemSize);
const array = type === 'byte'
? new Uint8Array(width * height * itemSize)
: type === 'halfFloat'
? new Uint16Array(width * height * itemSize)
: new Float32Array(width * height * itemSize);
const textureImage = { array, width, height };
const diff = max - min;
@@ -128,11 +134,29 @@ export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'grou
const index = itemSize * ((row * ynp * width) + (y * width) + px);
const offset = o(x, y, z);
let value: number;
if (type === 'byte') {
value = Math.round(((data[offset] - min) / diff) * 255);
} else if (type === 'halfFloat') {
value = toHalfFloat((data[offset] - min) / diff);
} else {
value = (data[offset] - min) / diff;
}
if (variant === 'data') {
array[index] = Math.round(((data[offset] - min) / diff) * 255);
array[index] = value;
} else {
if (variant === 'groups') {
packIntToRGBArray(offset, array, index);
if (type === 'halfFloat') {
let group = clamp(Math.round(offset), 0, 16777216 - 1) + 1;
array[index + 2] = toHalfFloat(group % 256);
group = Math.floor(group / 256);
array[index + 1] = toHalfFloat(group % 256);
group = Math.floor(group / 256);
array[index] = toHalfFloat(group % 256);
} else {
packIntToRGBArray(offset, array, index);
}
} else {
v3set(n0,
data[o(Math.max(0, x - 1), y, z)],
@@ -146,10 +170,19 @@ export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'grou
);
v3normalize(n0, v3sub(n0, n0, n1));
v3addScalar(n0, v3scale(n0, n0, 0.5), 0.5);
v3toArray(v3scale(n0, n0, 255), array, index);
if (type === 'byte') {
v3toArray(v3scale(n0, n0, 255), array, index);
} else if (type === 'halfFloat') {
array[index] = toHalfFloat(n0[0]);
array[index + 1] = toHalfFloat(n0[1]);
array[index + 2] = toHalfFloat(n0[2]);
} else {
v3toArray(n0, array, index);
}
}
array[index + 3] = Math.round(((data[offset] - min) / diff) * 255);
array[index + 3] = value;
}
}
}
@@ -158,12 +191,16 @@ export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'grou
return textureImage;
}
export function createVolumeTexture3d(volume: Volume) {
export function createVolumeTexture3d(volume: Volume, type: 'byte' | 'float' | 'halfFloat' = 'byte') {
const { cells: { space, data }, stats: { max, min } } = volume.grid;
const [width, height, depth] = space.dimensions as Vec3;
const { dataOffset: o } = space;
const array = new Uint8Array(width * height * depth * 4);
const array = type === 'byte'
? new Uint8Array(width * height * depth * 4)
: type === 'halfFloat'
? new Uint16Array(width * height * depth * 4)
: new Float32Array(width * height * depth * 4);
const textureVolume = { array, width, height, depth };
const diff = max - min;
@@ -192,9 +229,19 @@ export function createVolumeTexture3d(volume: Volume) {
);
v3normalize(n0, v3sub(n0, n0, n1));
v3addScalar(n0, v3scale(n0, n0, 0.5), 0.5);
v3toArray(v3scale(n0, n0, 255), array, i);
array[i + 3] = Math.round(((data[offset] - min) / diff) * 255);
if (type === 'byte') {
v3toArray(v3scale(n0, n0, 255), array, i);
array[i + 3] = Math.round(((data[offset] - min) / diff) * 255);
} else if (type === 'halfFloat') {
array[i] = toHalfFloat(n0[0]);
array[i + 1] = toHalfFloat(n0[1]);
array[i + 2] = toHalfFloat(n0[2]);
array[i + 3] = toHalfFloat((data[offset] - min) / diff);
} else {
v3toArray(n0, array, i);
array[i + 3] = (data[offset] - min) / diff;
}
i += 4;
}
}

View File

@@ -45,6 +45,7 @@ import { ExternalVolumeColorThemeProvider } from './color/external-volume';
import { ColorThemeCategory } from './color/categories';
import { CartoonColorThemeProvider } from './color/cartoon';
import { FormalChargeColorThemeProvider } from './color/formal-charge';
import { ExternalStructureColorThemeProvider } from './color/external-structure';
export type LocationColor = (location: Location, isSecondary: boolean) => Color
@@ -94,7 +95,7 @@ namespace ColorTheme {
export interface Palette {
filter?: TextureFilter,
colors: Color[]
colors: Color[],
}
export const PaletteScale = (1 << 24) - 1;
@@ -131,6 +132,8 @@ namespace ColorTheme {
'element-symbol': ElementSymbolColorThemeProvider,
'entity-id': EntityIdColorThemeProvider,
'entity-source': EntitySourceColorThemeProvider,
'external-structure': ExternalStructureColorThemeProvider,
'external-volume': ExternalVolumeColorThemeProvider,
'formal-charge': FormalChargeColorThemeProvider,
'hydrophobicity': HydrophobicityColorThemeProvider,
'illustrative': IllustrativeColorThemeProvider,
@@ -153,7 +156,6 @@ namespace ColorTheme {
'uniform': UniformColorThemeProvider,
'volume-segment': VolumeSegmentColorThemeProvider,
'volume-value': VolumeValueColorThemeProvider,
'external-volume': ExternalVolumeColorThemeProvider,
};
type _BuiltIn = typeof BuiltIn
export type BuiltIn = keyof _BuiltIn

View File

@@ -0,0 +1,168 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Color } from '../../mol-util/color';
import { Location } from '../../mol-model/location';
import type { ColorTheme, LocationColor } from '../color';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { ThemeDataContext } from '../theme';
import { type PluginContext } from '../../mol-plugin/context';
import { isPositionLocation } from '../../mol-geo/util/location-iterator';
import { ColorThemeCategory } from './categories';
import { StateSelection } from '../../mol-state/state/selection';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { QueryContext, Structure, StructureElement, StructureSelection } from '../../mol-model/structure';
import { ChainIdColorTheme, ChainIdColorThemeParams } from './chain-id';
import { EntityIdColorTheme, EntityIdColorThemeParams } from './entity-id';
import { EntitySourceColorTheme, EntitySourceColorThemeParams } from './entity-source';
import { MoleculeTypeColorTheme, MoleculeTypeColorThemeParams } from './molecule-type';
import { ModelIndexColorTheme, ModelIndexColorThemeParams } from './model-index';
import { StructureIndexColorTheme, StructureIndexColorThemeParams } from './structure-index';
import { assertUnreachable } from '../../mol-util/type-helpers';
import { ScaleLegend, TableLegend } from '../../mol-util/legend';
import { StructureLookup3DResultContext } from '../../mol-model/structure/structure/util/lookup3d';
import { StructureSelectionQueries } from '../../mol-plugin-state/helpers/structure-selection-query';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
const Description = `Assigns a color based on structure property at a given vertex.`;
export const ExternalStructureColorThemeParams = {
structure: PD.ValueRef<Structure>(
(ctx: PluginContext) => {
const structures = ctx.state.data.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Structure)).filter(c => c.obj?.data);
return structures.map(v => [v.transform.ref, v.obj?.label ?? '<unknown>'] as [string, string]);
},
(ref, getData) => getData(ref),
),
style: PD.MappedStatic('chain-id', {
'chain-id': PD.Group(ChainIdColorThemeParams),
'entity-id': PD.Group(EntityIdColorThemeParams),
'entity-source': PD.Group(EntitySourceColorThemeParams),
'molecule-type': PD.Group(MoleculeTypeColorThemeParams),
'model-index': PD.Group(ModelIndexColorThemeParams),
'structure-index': PD.Group(StructureIndexColorThemeParams),
}),
defaultColor: PD.Color(Color(0xcccccc)),
maxDistance: PD.Numeric(8, { min: 0.1, max: 24, step: 0.1 }, { description: 'Maximum distance to search for the nearest structure element. This is done only if the approximate search fails.' }),
approxMaxDistance: PD.Numeric(4, { min: 0, max: 12, step: 0.1 }, { description: 'Maximum distance to search for an approximately nearest structure element. This is done before the extact search.' }),
normalOffset: PD.Numeric(0, { min: -10, max: 20, step: 0.1 }, { description: 'Offset vertex position along its normal by given amount.' }),
backboneOnly: PD.Boolean(false),
};
export type ExternalStructureColorThemeParams = typeof ExternalStructureColorThemeParams
type ExternalStructureColorThemeProps = PD.Values<ExternalStructureColorThemeParams>
function getStyleTheme(ctx: ThemeDataContext, props: ExternalStructureColorThemeProps['style']) {
switch (props.name) {
case 'chain-id': return ChainIdColorTheme(ctx, props.params);
case 'entity-id': return EntityIdColorTheme(ctx, props.params);
case 'entity-source': return EntitySourceColorTheme(ctx, props.params);
case 'molecule-type': return MoleculeTypeColorTheme(ctx, props.params);
case 'model-index': return ModelIndexColorTheme(ctx, props.params);
case 'structure-index': return StructureIndexColorTheme(ctx, props.params);
default: assertUnreachable(props);
}
}
export function ExternalStructureColorTheme(ctx: ThemeDataContext, props: PD.Values<ExternalStructureColorThemeParams>): ColorTheme<ExternalStructureColorThemeParams> {
let structure: Structure | undefined;
try {
structure = props.structure.getValue();
} catch {
// .getValue() is resolved during state reconciliation => would throw from UI
}
// NOTE: this will currently be slow for with GPU/texture meshes due to slow iteration
// TODO: create texture to be able to do the sampling on the GPU
let color: LocationColor;
let contextHash: number | undefined = undefined;
let legend: Readonly<ScaleLegend | TableLegend> | undefined = undefined;
const { maxDistance, approxMaxDistance: approxDistance, normalOffset, defaultColor, backboneOnly } = props;
if (structure) {
const styleTheme = getStyleTheme({ ...ctx, structure }, props.style);
const lookupFirstCtx = StructureLookup3DResultContext();
const lookupNearestCtx = StructureLookup3DResultContext();
let s = structure;
if (backboneOnly) {
s = StructureSelection.unionStructure(StructureSelectionQueries.backbone.query(new QueryContext(structure)));
}
const position = Vec3();
const l = StructureElement.Location.create(s);
color = (location: Location, isSecondary: boolean): Color => {
if (!isPositionLocation(location)) {
return defaultColor;
}
// Offset the vertex position along its normal
if (normalOffset !== 0) {
Vec3.scaleAndAdd(position, location.position, location.normal, normalOffset);
} else {
Vec3.copy(position, location.position);
}
const [x, y, z] = position;
if (approxDistance > 0) {
const rf = s.lookup3d.approxNearest(x, y, z, approxDistance, lookupFirstCtx);
if (rf.count > 0) {
l.unit = rf.units[0];
l.element = l.unit.elements[rf.indices[0]];
return styleTheme.color(l, isSecondary);
}
}
const rn = s.lookup3d.find(x, y, z, maxDistance, lookupNearestCtx);
if (rn.count > 0) {
let idx = 0;
let minD = rn.squaredDistances[0];
for (let i = 1; i < rn.count; ++i) {
if (rn.squaredDistances[i] < minD) {
minD = rn.squaredDistances[i];
idx = i;
}
}
l.unit = rn.units[idx];
l.element = l.unit.elements[rn.indices[idx]];
return styleTheme.color(l, isSecondary);
}
return defaultColor;
};
contextHash = styleTheme.contextHash;
legend = styleTheme.legend;
} else {
color = () => defaultColor;
}
return {
factory: ExternalStructureColorTheme,
granularity: 'vertex',
preferSmoothing: true,
color,
props,
contextHash,
description: Description,
legend,
};
}
export const ExternalStructureColorThemeProvider: ColorTheme.Provider<ExternalStructureColorThemeParams, 'external-structure'> = {
name: 'external-structure',
label: 'External Structure',
category: ColorThemeCategory.Misc,
factory: ExternalStructureColorTheme,
getParams: () => ExternalStructureColorThemeParams,
defaultValues: PD.getDefaultValues(ExternalStructureColorThemeParams),
isApplicable: (ctx: ThemeDataContext) => true,
};

View File

@@ -983,7 +983,7 @@ namespace InputObserver {
// Adapted from https://stackoverflow.com/a/30134826
// License: https://creativecommons.org/licenses/by-sa/3.0/
function normalizeWheel(event: any) {
export function normalizeWheel(event: any) {
// Reasonable defaults
const PIXEL_STEP = 10;
const LINE_HEIGHT = 40;

View File

@@ -0,0 +1,124 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*
* This code has been modified from https://github.com/mrdoob/three.js/,
* copyright (c) 2010-2024 three.js authors. MIT License
*/
// Fast Half Float Conversions, http://www.fox-toolkit.org/ftp/fasthalffloatconversion.pdf
import { clamp } from '../mol-math/interpolate';
const Tables = generateTables();
function generateTables() {
// float32 to float16 helpers
const buffer = new ArrayBuffer(4);
const floatView = new Float32Array(buffer);
const uint32View = new Uint32Array(buffer);
const baseTable = new Uint32Array(512);
const shiftTable = new Uint32Array(512);
for (let i = 0; i < 256; ++i) {
const e = i - 127;
if (e < -27) { // very small number (0, -0)
baseTable[i] = 0x0000;
baseTable[i | 0x100] = 0x8000;
shiftTable[i] = 24;
shiftTable[i | 0x100] = 24;
} else if (e < -14) { // small number (denorm)
baseTable[i] = 0x0400 >> (-e - 14);
baseTable[i | 0x100] = (0x0400 >> (-e - 14)) | 0x8000;
shiftTable[i] = - e - 1;
shiftTable[i | 0x100] = -e - 1;
} else if (e <= 15) { // normal number
baseTable[i] = (e + 15) << 10;
baseTable[i | 0x100] = ((e + 15) << 10) | 0x8000;
shiftTable[i] = 13;
shiftTable[i | 0x100] = 13;
} else if (e < 128) { // large number (Infinity, -Infinity)
baseTable[i] = 0x7c00;
baseTable[i | 0x100] = 0xfc00;
shiftTable[i] = 24;
shiftTable[i | 0x100] = 24;
} else { // stay (NaN, Infinity, -Infinity)
baseTable[i] = 0x7c00;
baseTable[i | 0x100] = 0xfc00;
shiftTable[i] = 13;
shiftTable[i | 0x100] = 13;
}
}
// float16 to float32 helpers
const mantissaTable = new Uint32Array(2048);
const exponentTable = new Uint32Array(64);
const offsetTable = new Uint32Array(64);
for (let i = 1; i < 1024; ++i) {
let m = i << 13; // zero pad mantissa bits
let e = 0; // zero exponent
// normalized
while ((m & 0x00800000) === 0) {
m <<= 1;
e -= 0x00800000; // decrement exponent
}
m &= ~ 0x00800000; // clear leading 1 bit
e += 0x38800000; // adjust bias
mantissaTable[i] = m | e;
}
for (let i = 1024; i < 2048; ++i) {
mantissaTable[i] = 0x38000000 + ((i - 1024) << 13);
}
for (let i = 1; i < 31; ++i) {
exponentTable[i] = i << 23;
}
exponentTable[31] = 0x47800000;
exponentTable[32] = 0x80000000;
for (let i = 33; i < 63; ++i) {
exponentTable[i] = 0x80000000 + ((i - 32) << 23);
}
exponentTable[63] = 0xc7800000;
for (let i = 1; i < 64; ++i) {
if (i !== 32) {
offsetTable[i] = 1024;
}
}
return {
floatView,
uint32View,
baseTable,
shiftTable,
mantissaTable,
exponentTable,
offsetTable
};
}
/** float32 to float16 */
export function toHalfFloat(val: number) {
val = clamp(val, -65504, 65504);
Tables.floatView[0] = val;
const f = Tables.uint32View[0];
const e = (f >> 23) & 0x1ff;
return Tables.baseTable[e] + ((f & 0x007fffff) >> Tables.shiftTable[e]);
}
/** float16 to float32 */
export function fromHalfFloat(val: number) {
const m = val >> 10;
Tables.uint32View[0] = Tables.mantissaTable[Tables.offsetTable[m] + (val & 0x3ff)] + Tables.exponentTable[m];
return Tables.floatView[0];
}