Skip to content

Commit

Permalink
Added cylinder shape type
Browse files Browse the repository at this point in the history
  • Loading branch information
ceriottm committed Dec 8, 2023
1 parent 41619ff commit 5f8ead2
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 6 deletions.
27 changes: 24 additions & 3 deletions python/chemiscope/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,13 @@ def create_input(
"semiaxes": [float, float, float],
}
# "kind" : "cylinder"
{ # "orientation" is redundant and hence ignored
"vector" : [float, float, float], # orientation and shape of the cylinder
# the tip of the cylinder is at the end of the segment.
"radius" : float,
}
# "kind" : "arrow"
{ # "orientation" is redundant and hence ignored
"vector" : [float, float, float], # orientation and shape of the arrow
Expand Down Expand Up @@ -982,20 +989,34 @@ def _check_valid_shape(shape):
raise ValueError(
"'simplices' must be an Nx3 array values for 'custom' shape kind"
)
elif shape["kind"] == "cylinder":
if not isinstance(parameters["radius"], float):
raise TypeError(
"cylinder shape 'radius' must be a float, "
f"got {type(parameters['radius'])}"
)
vector_array = np.asarray(parameters["vector"]).astype(
np.float64, casting="safe", subok=False, copy=False
)

if not vector_array.shape == (3,):
raise ValueError(
"'vector' must be an array with 3 values for 'cylinder' shape kind"
)
elif shape["kind"] == "arrow":
if not isinstance(parameters["baseRadius"], float):
raise TypeError(
"sphere shape 'baseRadius' must be a float, "
"arrow shape 'baseRadius' must be a float, "
f"got {type(parameters['baseRadius'])}"
)
if not isinstance(parameters["headRadius"], float):
raise TypeError(
"sphere shape 'headRadius' must be a float, "
"arrow shape 'headRadius' must be a float, "
f"got {type(parameters['headRadius'])}"
)
if not isinstance(parameters["headLength"], float):
raise TypeError(
"sphere shape 'headLength' must be a float, "
"arrow shape 'headLength' must be a float, "
f"got {type(parameters['headLength'])}"
)

Expand Down
4 changes: 3 additions & 1 deletion src/dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @module main
*/

import { Arrow, CustomShape, Ellipsoid, Sphere } from './structure/shapes';
import { Arrow, CustomShape, Cylinder, Ellipsoid, Sphere } from './structure/shapes';
import { ShapeParameters } from './structure/shapes';

/** A dataset containing all the data to be displayed. */
Expand Down Expand Up @@ -403,6 +403,8 @@ function validateShape(kind: string, parameters: Record<string, unknown>): strin
return Ellipsoid.validateParameters(parameters);
} else if (kind === 'arrow') {
return Arrow.validateParameters(parameters);
} else if (kind === 'cylinder') {
return Cylinder.validateParameters(parameters);
} else if (kind === 'custom') {
return CustomShape.validateParameters(parameters);
}
Expand Down
149 changes: 148 additions & 1 deletion src/structure/shapes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ export interface EllipsoidParameters extends BaseShapeParameters<EllipsoidData>
kind: 'ellipsoid';
}

// Interface for cylinder data (avoids orientation options, since it's redundant)
export interface CylinderData extends BaseShapeData {
vector: [number, number, number];
radius?: number;
}

/** Parameters for an arrow shape */
export interface CylinderParameters extends BaseShapeParameters<CylinderData> {
kind: 'cylinder';
}

// Interface for arrow data (avoids orientation options, since it's redundant)
export interface ArrowData extends BaseShapeData {
vector: [number, number, number];
Expand All @@ -87,7 +98,7 @@ export interface CustomShapeParameters extends BaseShapeParameters<CustomShapeDa
kind: 'custom';
}

export type ShapeData = SphereData | EllipsoidData | ArrowData | CustomShapeData;
export type ShapeData = SphereData | EllipsoidData | ArrowData | CylinderData | CustomShapeData;

/**
* Describes a shape, to be displayed alongside an atomic structure.
Expand All @@ -104,6 +115,7 @@ export type ShapeData = SphereData | EllipsoidData | ArrowData | CustomShapeData
export type ShapeParameters =
| SphereParameters
| EllipsoidParameters
| CylinderParameters
| ArrowParameters
| CustomShapeParameters;

Expand Down Expand Up @@ -574,6 +586,141 @@ export class Arrow extends Shape {
}
}

function triangulateCylinder(
vector: [number, number, number],
radius: number,
resolution: number = 20
): { vertices: XYZ[]; indices: number[] } {
const [x, y, z] = vector;
const tip: XYZ = { x, y, z };
const v_len = Math.sqrt(x * x + y * y + z * z);

// generates a unit circle oriented in the right direction
const n_vec: XYZ = multXYZ(tip, 1.0 / v_len);

// Generate an arbitrary vector not collinear with n
let vx: XYZ;
if (n_vec.x !== 0.0 || n_vec.y !== 0.0) {
vx = { x: 0, y: 0, z: 1 };
} else {
vx = { x: 0, y: 1, z: 0 };
}

// generate orthogonal vectors in the plane defined by nvec
let u: XYZ = addXYZ(vx, multXYZ(n_vec, -dotXYZ(vx, n_vec)));
u = multXYZ(u, 1.0 / Math.sqrt(dotXYZ(u, u)));
const v: XYZ = crossXYZ(u, n_vec);

// generate n_points in the plane defined by nvec, centered at vec
const circle_points: XYZ[] = [];
for (let i = 0; i < resolution; i++) {
circle_points.push(
addXYZ(
multXYZ(u, Math.cos((i * 2 * Math.PI) / resolution)),
multXYZ(v, Math.sin((i * 2 * Math.PI) / resolution))
)
);
}

let indices: number[] = [];
const vertices: XYZ[] = [];

vertices.push({ x: 0, y: 0, z: 0 });
vertices.push(tip);
// the cylinder is built as a surface of revolution, by stacking |_| motifs
for (let i = 0; i < resolution; i++) {
// nb replicated points are needed to get sharp edges
vertices.push(multXYZ(circle_points[i], radius));
vertices.push(multXYZ(circle_points[i], radius));
vertices.push(addXYZ(multXYZ(circle_points[i], radius), tip));
vertices.push(addXYZ(multXYZ(circle_points[i], radius), tip));
const i_seg = 2 + i * 4;
const i_next = 2 + ((i + 1) % resolution) * 4;
indices = [
...indices,
...[
0,
i_seg,
i_next, // cylinder base
i_seg + 1,
i_seg + 2,
i_next + 1,
i_next + 1,
i_seg + 2,
i_next + 2, // cylinder side
i_seg + 3,
1,
i_next + 3, // cylinder top
],
];
}
return {
vertices: vertices,
indices: indices,
};
}

export class Cylinder extends Shape {
public vector: [number, number, number];
public radius: number;

constructor(data: Partial<CylinderData>) {
super(data);
assert(data.vector);
this.vector = [
this.scale * data.vector[0],
this.scale * data.vector[1],
this.scale * data.vector[2],
];
this.radius = this.scale * (data.radius || 0.1);
}

public static validateParameters(parameters: Record<string, unknown>): string {
if (!('vector' in parameters)) {
return '"vector" is required for "arrow" shapes';
}

if (!Array.isArray(parameters.vector) || parameters.vector.length !== 3) {
return '"vector" must be an array with 3 elements for "vector" shapes';
}

const [ax, ay, az] = parameters.vector as unknown[];
if (typeof ax !== 'number' || typeof ay !== 'number' || typeof az !== 'number') {
return '"vector" elements must be numbers for "vector" shapes';
}

if ('orientation' in parameters) {
return '"orientation" cannot be used on "cylinder" shapes. define "vector" instead';
}

return '';
}

public outputTo3Dmol(color: $3Dmol.ColorSpec, resolution: number = 20): $3Dmol.CustomShapeSpec {
const triangulation = triangulateCylinder(this.vector, this.radius, resolution);
const rawVertices = triangulation.vertices;
const indices = triangulation.indices;
const vertices: XYZ[] = [];
const simplices: [number, number, number][] = [];

for (const v of rawVertices) {
const newVertex: XYZ = addXYZ(v, this.position);
vertices.push(newVertex);
}

for (let i = 0; i < indices.length / 3; i++) {
simplices.push([indices[3 * i], indices[3 * i + 1], indices[3 * i + 2]]);
}

return {
vertexArr: vertices,
normalArr: determineNormals(vertices, simplices),
faceArr: indices,
color: color,
};
}
}

export class CustomShape extends Shape {
public vertices: XYZ[];
public simplices: [number, number, number][];
Expand Down
7 changes: 6 additions & 1 deletion src/structure/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { arrayMaxMin, getElement, sendWarning, unreachable } from '../utils';
import { PositioningCallback } from '../utils';
import { Environment, Settings, Structure } from '../dataset';

import { Arrow, CustomShape, Ellipsoid, ShapeData, Sphere } from './shapes';
import { Arrow, CustomShape, Cylinder, Ellipsoid, ShapeData, Sphere } from './shapes';

import { StructureOptions } from './options';

Expand Down Expand Up @@ -1169,6 +1169,11 @@ export class MoleculeViewer {
this._viewer.addCustom(
shape.outputTo3Dmol(shape_data.color || 0xffffff)
);
} else if (current_shape.kind === 'cylinder') {
const shape = new Cylinder(shape_data);
this._viewer.addCustom(
shape.outputTo3Dmol(shape_data.color || 0xffffff)
);
} else if (current_shape.kind === 'arrow') {
const shape = new Arrow(shape_data);
this._viewer.addCustom(
Expand Down

0 comments on commit 5f8ead2

Please sign in to comment.