Pārlūkot izejas kodu

Merge pull request #705 from molstar/camera-helper-params

add axes camera-helper customization options
Alexander Rose 2 gadi atpakaļ
vecāks
revīzija
fde395e2fa
2 mainītis faili ar 193 papildinājumiem un 70 dzēšanām
  1. 1 0
      CHANGELOG.md
  2. 192 70
      src/mol-canvas3d/helper/camera-helper.ts

+ 1 - 0
CHANGELOG.md

@@ -10,6 +10,7 @@ Note that since we don't clearly distinguish between a public and private interf
 - Add example `image-renderer`
 - Fix wrong offset when rendering text with orthographic projection
 - Update camera/handle helper when `devicePixelRatio` changes
+- Add various options to customize the axes camera-helper
 
 ## [v3.30.0] - 2023-01-29
 

+ 192 - 70
src/mol-canvas3d/helper/camera-helper.ts

@@ -11,6 +11,8 @@ import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
 import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
 import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
 import { PickingId } from '../../mol-geo/geometry/picking';
+import { Text } from '../../mol-geo/geometry/text/text';
+import { TextBuilder } from '../../mol-geo/geometry/text/text-builder';
 import { GraphicsRenderObject } from '../../mol-gl/render-object';
 import { Scene } from '../../mol-gl/scene';
 import { WebGLContext } from '../../mol-gl/webgl/context';
@@ -23,19 +25,34 @@ import { Visual } from '../../mol-repr/visual';
 import { ColorNames } from '../../mol-util/color/names';
 import { MarkerAction, MarkerActions } from '../../mol-util/marker-action';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { assertUnreachable } from '../../mol-util/type-helpers';
 import { Camera, ICamera } from '../camera';
 import { Viewport } from '../camera/util';
 
 // TODO add scale line/grid
 
 const AxesParams = {
-    ...Mesh.Params,
-    alpha: { ...Mesh.Params.alpha, defaultValue: 0.51 },
-    ignoreLight: { ...Mesh.Params.ignoreLight, defaultValue: true },
+    alpha: PD.Numeric(0.51, { min: 0, max: 1, step: 0.01 }, { isEssential: true, label: 'Opacity' }),
     colorX: PD.Color(ColorNames.red, { isEssential: true }),
     colorY: PD.Color(ColorNames.green, { isEssential: true }),
     colorZ: PD.Color(ColorNames.blue, { isEssential: true }),
     scale: PD.Numeric(0.33, { min: 0.1, max: 2, step: 0.1 }, { isEssential: true }),
+    location: PD.Select('bottom-left', PD.arrayToOptions(['bottom-left', 'bottom-right', 'top-left', 'top-right'] as const)),
+    originColor: PD.Color(ColorNames.grey),
+    radiusScale: PD.Numeric(0.075, { min: 0.01, max: 0.3, step: 0.001 }),
+    showPlanes: PD.Boolean(true),
+    planeColorXY: PD.Color(ColorNames.grey, { label: 'Plane Color XY' }),
+    planeColorXZ: PD.Color(ColorNames.grey, { label: 'Plane Color XZ' }),
+    planeColorYZ: PD.Color(ColorNames.grey, { label: 'Plane Color YZ' }),
+    showLabels: PD.Boolean(false),
+    labelX: PD.Text('X'),
+    labelY: PD.Text('Y'),
+    labelZ: PD.Text('Z'),
+    labelColorX: PD.Color(ColorNames.grey),
+    labelColorY: PD.Color(ColorNames.grey),
+    labelColorZ: PD.Color(ColorNames.grey),
+    labelOpacity: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
+    labelScale: PD.Numeric(0.25, { min: 0.1, max: 1.0, step: 0.01 }),
 };
 type AxesParams = typeof AxesParams
 type AxesProps = PD.Values<AxesParams>
@@ -56,7 +73,8 @@ export class CameraHelper {
         axes: { name: 'off', params: {} }
     };
 
-    private renderObject: GraphicsRenderObject | undefined;
+    private meshRenderObject: GraphicsRenderObject | undefined;
+    private textRenderObject: GraphicsRenderObject | undefined;
     private pixelRatio = 1;
 
     constructor(private webgl: WebGLContext, props: Partial<CameraHelperProps> = {}) {
@@ -80,8 +98,14 @@ export class CameraHelper {
                         ...props.axes.params,
                         scale: props.axes.params.scale * this.webgl.pixelRatio
                     };
-                    this.renderObject = createAxesRenderObject(params);
-                    this.scene.add(this.renderObject);
+                    this.meshRenderObject = createMeshRenderObject(params);
+                    this.scene.add(this.meshRenderObject);
+                    if (props.axes.params.showLabels) {
+                        this.textRenderObject = createTextRenderObject(params);
+                        this.scene.add(this.textRenderObject);
+                    } else {
+                        this.textRenderObject = undefined;
+                    }
                     this.scene.commit();
 
                     Vec3.set(this.camera.position, 0, 0, params.scale * 200);
@@ -99,19 +123,29 @@ export class CameraHelper {
 
     getLoci(pickingId: PickingId) {
         const { objectId, groupId, instanceId } = pickingId;
-        if (!this.renderObject || objectId !== this.renderObject.id || groupId === CameraHelperAxis.None) return EmptyLoci;
+        if ((
+            (!this.meshRenderObject || objectId !== this.meshRenderObject.id) &&
+            (!this.textRenderObject || objectId !== this.textRenderObject.id)
+        ) || groupId === CameraHelperAxis.None) return EmptyLoci;
         return CameraAxesLoci(this, groupId, instanceId);
     }
 
     private eachGroup = (loci: Loci, apply: (interval: Interval) => boolean): boolean => {
-        if (!this.renderObject) return false;
         if (!isCameraAxesLoci(loci)) return false;
         let changed = false;
-        const groupCount = this.renderObject.values.uGroupCount.ref.value;
-        const { elements } = loci;
-        for (const { groupId, instanceId } of elements) {
-            const idx = instanceId * groupCount + groupId;
-            if (apply(Interval.ofSingleton(idx))) changed = true;
+        if (this.meshRenderObject) {
+            const groupCount = this.meshRenderObject.values.uGroupCount.ref.value;
+            for (const { groupId, instanceId } of loci.elements) {
+                const idx = instanceId * groupCount + groupId;
+                if (apply(Interval.ofSingleton(idx))) changed = true;
+            }
+        }
+        if (this.textRenderObject) {
+            const groupCount = this.textRenderObject.values.uGroupCount.ref.value;
+            for (const { groupId, instanceId } of loci.elements) {
+                const idx = instanceId * groupCount + groupId;
+                if (apply(Interval.ofSingleton(idx))) changed = true;
+            }
         }
         return changed;
     };
@@ -122,11 +156,14 @@ export class CameraHelper {
             if (!isCameraAxesLoci(loci)) return false;
             if (loci.data !== this) return false;
         }
-        return Visual.mark(this.renderObject, loci, action, this.eachGroup);
+        return (
+            Visual.mark(this.meshRenderObject, loci, action, this.eachGroup) ||
+            Visual.mark(this.textRenderObject, loci, action, this.eachGroup)
+        );
     }
 
     update(camera: ICamera) {
-        if (!this.renderObject) return;
+        if (!this.meshRenderObject || this.props.axes.name === 'off') return;
 
         if (this.pixelRatio !== this.webgl.pixelRatio) {
             this.setProps(this.props);
@@ -135,12 +172,37 @@ export class CameraHelper {
         updateCamera(this.camera, camera.viewport, camera.viewOffset);
         Mat4.extractRotation(this.scene.view, camera.view);
 
-        const r = this.renderObject.values.boundingSphere.ref.value.radius;
-        Mat4.setTranslation(this.scene.view, Vec3.create(
-            -camera.viewport.width / 2 + r,
-            -camera.viewport.height / 2 + r,
-            0
-        ));
+        const r = this.textRenderObject
+            ? this.textRenderObject.values.boundingSphere.ref.value.radius
+            : this.meshRenderObject.values.boundingSphere.ref.value.radius;
+        const l = this.props.axes.params.location;
+        if (l === 'bottom-left') {
+            Mat4.setTranslation(this.scene.view, Vec3.create(
+                -camera.viewport.width / 2 + r,
+                -camera.viewport.height / 2 + r,
+                0
+            ));
+        } else if (l === 'bottom-right') {
+            Mat4.setTranslation(this.scene.view, Vec3.create(
+                camera.viewport.width / 2 - r,
+                -camera.viewport.height / 2 + r,
+                0
+            ));
+        } else if (l === 'top-left') {
+            Mat4.setTranslation(this.scene.view, Vec3.create(
+                -camera.viewport.width / 2 + r,
+                camera.viewport.height / 2 - r,
+                0
+            ));
+        } else if (l === 'top-right') {
+            Mat4.setTranslation(this.scene.view, Vec3.create(
+                camera.viewport.width / 2 - r,
+                camera.viewport.height / 2 - r,
+                0
+            ));
+        } else {
+            assertUnreachable(l);
+        }
     }
 }
 
@@ -151,17 +213,23 @@ export enum CameraHelperAxis {
     Z,
     XY,
     XZ,
-    YZ
+    YZ,
+    Origin
 }
 
-function getAxisLabel(axis: number) {
+function getAxisLabel(axis: number, cameraHelper: CameraHelper) {
+    const a = cameraHelper.props.axes;
+    const x = a.name === 'on' ? a.params.labelX : 'X';
+    const y = a.name === 'on' ? a.params.labelY : 'Y';
+    const z = a.name === 'on' ? a.params.labelZ : 'Z';
     switch (axis) {
-        case CameraHelperAxis.X: return 'X Axis';
-        case CameraHelperAxis.Y: return 'Y Axis';
-        case CameraHelperAxis.Z: return 'Z Axis';
-        case CameraHelperAxis.XY: return 'XY Plane';
-        case CameraHelperAxis.XZ: return 'XZ Plane';
-        case CameraHelperAxis.YZ: return 'YZ Plane';
+        case CameraHelperAxis.X: return `${x} Axis`;
+        case CameraHelperAxis.Y: return `${y} Axis`;
+        case CameraHelperAxis.Z: return `${z} Axis`;
+        case CameraHelperAxis.XY: return `${x}${y} Plane`;
+        case CameraHelperAxis.XZ: return `${x}${z} Plane`;
+        case CameraHelperAxis.YZ: return `${y}${z} Plane`;
+        case CameraHelperAxis.Origin: return 'Origin';
         default: return 'Axes';
     }
 }
@@ -169,7 +237,7 @@ function getAxisLabel(axis: number) {
 function CameraAxesLoci(cameraHelper: CameraHelper, groupId: number, instanceId: number) {
     return DataLoci('camera-axes', cameraHelper, [{ groupId, instanceId }],
         void 0 /** bounding sphere */,
-        () => getAxisLabel(groupId));
+        () => getAxisLabel(groupId, cameraHelper));
 }
 export type CameraAxesLoci = ReturnType<typeof CameraAxesLoci>
 export function isCameraAxesLoci(x: Loci): x is CameraAxesLoci {
@@ -206,15 +274,17 @@ function updateCamera(camera: Camera, viewport: Viewport, viewOffset: Camera.Vie
     Mat4.ortho(camera.projection, left, right, top, bottom, near, far);
 }
 
-function createAxesMesh(scale: number, mesh?: Mesh) {
+function createAxesMesh(props: AxesProps, mesh?: Mesh) {
     const state = MeshBuilder.createState(512, 256, mesh);
-    const radius = 0.075 * scale;
-    const x = Vec3.scale(Vec3(), Vec3.unitX, scale);
-    const y = Vec3.scale(Vec3(), Vec3.unitY, scale);
-    const z = Vec3.scale(Vec3(), Vec3.unitZ, scale);
+    const scale = 100 * props.scale;
+    const radius = props.radiusScale * scale;
+    const textScale = props.showLabels ? 100 * props.labelScale / 3 : 0;
+    const x = Vec3.scale(Vec3(), Vec3.unitX, scale - textScale);
+    const y = Vec3.scale(Vec3(), Vec3.unitY, scale - textScale);
+    const z = Vec3.scale(Vec3(), Vec3.unitZ, scale - textScale);
     const cylinderProps = { radiusTop: radius, radiusBottom: radius, radialSegments: 32 };
 
-    state.currentGroup = CameraHelperAxis.None;
+    state.currentGroup = CameraHelperAxis.Origin;
     addSphere(state, Vec3.origin, radius, 2);
 
     state.currentGroup = CameraHelperAxis.X;
@@ -229,50 +299,102 @@ function createAxesMesh(scale: number, mesh?: Mesh) {
     addSphere(state, z, radius, 2);
     addCylinder(state, Vec3.origin, z, 1, cylinderProps);
 
-    Vec3.scale(x, x, 0.5);
-    Vec3.scale(y, y, 0.5);
-    Vec3.scale(z, z, 0.5);
-
-    state.currentGroup = CameraHelperAxis.XY;
-    MeshBuilder.addTriangle(state, Vec3.origin, x, y);
-    MeshBuilder.addTriangle(state, Vec3.origin, y, x);
-    const xy = Vec3.add(Vec3(), x, y);
-    MeshBuilder.addTriangle(state, xy, x, y);
-    MeshBuilder.addTriangle(state, xy, y, x);
-
-    state.currentGroup = CameraHelperAxis.XZ;
-    MeshBuilder.addTriangle(state, Vec3.origin, x, z);
-    MeshBuilder.addTriangle(state, Vec3.origin, z, x);
-    const xz = Vec3.add(Vec3(), x, z);
-    MeshBuilder.addTriangle(state, xz, x, z);
-    MeshBuilder.addTriangle(state, xz, z, x);
-
-    state.currentGroup = CameraHelperAxis.YZ;
-    MeshBuilder.addTriangle(state, Vec3.origin, y, z);
-    MeshBuilder.addTriangle(state, Vec3.origin, z, y);
-    const yz = Vec3.add(Vec3(), y, z);
-    MeshBuilder.addTriangle(state, yz, y, z);
-    MeshBuilder.addTriangle(state, yz, z, y);
+    if (props.showPlanes) {
+        Vec3.scale(x, x, 0.5);
+        Vec3.scale(y, y, 0.5);
+        Vec3.scale(z, z, 0.5);
+
+        state.currentGroup = CameraHelperAxis.XY;
+        MeshBuilder.addTriangle(state, Vec3.origin, x, y);
+        MeshBuilder.addTriangle(state, Vec3.origin, y, x);
+        const xy = Vec3.add(Vec3(), x, y);
+        MeshBuilder.addTriangle(state, xy, x, y);
+        MeshBuilder.addTriangle(state, xy, y, x);
+
+        state.currentGroup = CameraHelperAxis.XZ;
+        MeshBuilder.addTriangle(state, Vec3.origin, x, z);
+        MeshBuilder.addTriangle(state, Vec3.origin, z, x);
+        const xz = Vec3.add(Vec3(), x, z);
+        MeshBuilder.addTriangle(state, xz, x, z);
+        MeshBuilder.addTriangle(state, xz, z, x);
+
+        state.currentGroup = CameraHelperAxis.YZ;
+        MeshBuilder.addTriangle(state, Vec3.origin, y, z);
+        MeshBuilder.addTriangle(state, Vec3.origin, z, y);
+        const yz = Vec3.add(Vec3(), y, z);
+        MeshBuilder.addTriangle(state, yz, y, z);
+        MeshBuilder.addTriangle(state, yz, z, y);
+    }
 
     return MeshBuilder.getMesh(state);
 }
 
-function getAxesShape(props: AxesProps, shape?: Shape<Mesh>) {
+function getAxesMeshShape(props: AxesProps, shape?: Shape<Mesh>) {
     const scale = 100 * props.scale;
-    const mesh = createAxesMesh(scale, shape?.geometry);
+    const mesh = createAxesMesh(props, shape?.geometry);
     mesh.setBoundingSphere(Sphere3D.create(Vec3.create(scale / 2, scale / 2, scale / 2), scale + scale / 4));
     const getColor = (groupId: number) => {
         switch (groupId) {
-            case 1: return props.colorX;
-            case 2: return props.colorY;
-            case 3: return props.colorZ;
+            case CameraHelperAxis.X: return props.colorX;
+            case CameraHelperAxis.Y: return props.colorY;
+            case CameraHelperAxis.Z: return props.colorZ;
+            case CameraHelperAxis.XY: return props.planeColorXY;
+            case CameraHelperAxis.XZ: return props.planeColorXZ;
+            case CameraHelperAxis.YZ: return props.planeColorYZ;
+            case CameraHelperAxis.Origin: return props.originColor;
+            default: return ColorNames.grey;
+        }
+    };
+    return Shape.create('axes-mesh', {}, mesh, getColor, () => 1, () => '');
+}
+
+function createMeshRenderObject(props: AxesProps) {
+    const shape = getAxesMeshShape(props);
+    return Shape.createRenderObject(shape, {
+        ...PD.getDefaultValues(Mesh.Params),
+        ...props,
+        ignoreLight: true,
+    });
+}
+
+//
+
+function createAxesText(props: AxesProps, text?: Text) {
+    const builder = TextBuilder.create(props, 8, 8, text);
+    const scale = 100 * props.scale;
+
+    const x = Vec3.scale(Vec3(), Vec3.unitX, scale);
+    const y = Vec3.scale(Vec3(), Vec3.unitY, scale);
+    const z = Vec3.scale(Vec3(), Vec3.unitZ, scale);
+
+    const textScale = 100 * props.labelScale;
+    builder.add(props.labelX, x[0], x[1], x[2], 0.0, textScale, CameraHelperAxis.X);
+    builder.add(props.labelY, y[0], y[1], y[2], 0.0, textScale, CameraHelperAxis.Y);
+    builder.add(props.labelZ, z[0], z[1], z[2], 0.0, textScale, CameraHelperAxis.Z);
+
+    return builder.getText();
+}
+
+function getAxesTextShape(props: AxesProps, shape?: Shape<Text>) {
+    const scale = 100 * props.scale;
+    const text = createAxesText(props, shape?.geometry);
+    text.setBoundingSphere(Sphere3D.create(Vec3.create(scale / 2, scale / 2, scale / 2), scale));
+    const getColor = (groupId: number) => {
+        switch (groupId) {
+            case CameraHelperAxis.X: return props.labelColorX;
+            case CameraHelperAxis.Y: return props.labelColorY;
+            case CameraHelperAxis.Z: return props.labelColorZ;
             default: return ColorNames.grey;
         }
     };
-    return Shape.create('axes', {}, mesh, getColor, () => 1, () => '');
+    return Shape.create('axes-text', {}, text, getColor, () => 1, () => '');
 }
 
-function createAxesRenderObject(props: AxesProps) {
-    const shape = getAxesShape(props);
-    return Shape.createRenderObject(shape, props);
-}
+function createTextRenderObject(props: AxesProps) {
+    const shape = getAxesTextShape(props);
+    return Shape.createRenderObject(shape, {
+        ...PD.getDefaultValues(Text.Params),
+        ...props,
+        alpha: props.labelOpacity,
+    });
+}