Skip to content
Snippets Groups Projects
Commit 4cd7088e authored by Alexander Rose's avatar Alexander Rose
Browse files

add axes camera-helper customization options

parent 8e1876fc
No related branches found
No related tags found
No related merge requests found
......@@ -8,6 +8,7 @@ Note that since we don't clearly distinguish between a public and private interf
- 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
......
......@@ -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,20 +123,30 @@ 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) {
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;
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,6 +299,7 @@ function createAxesMesh(scale: number, mesh?: Mesh) {
addSphere(state, z, radius, 2);
addCylinder(state, Vec3.origin, z, 1, cylinderProps);
if (props.showPlanes) {
Vec3.scale(x, x, 0.5);
Vec3.scale(y, y, 0.5);
Vec3.scale(z, z, 0.5);
......@@ -253,26 +324,77 @@ function createAxesMesh(scale: number, mesh?: Mesh) {
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, getColor, () => 1, () => '');
return Shape.create('axes-mesh', {}, mesh, getColor, () => 1, () => '');
}
function createAxesRenderObject(props: AxesProps) {
const shape = getAxesShape(props);
return Shape.createRenderObject(shape, props);
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-text', {}, text, getColor, () => 1, () => '');
}
function createTextRenderObject(props: AxesProps) {
const shape = getAxesTextShape(props);
return Shape.createRenderObject(shape, {
...PD.getDefaultValues(Text.Params),
...props,
alpha: props.labelOpacity,
});
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment