Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow displaying multiple shapes simultaneously #290

Merged
merged 2 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Settings } from './dataset';
* Possible HTML attributes to attach to a setting
*/
// this is mostly to catch typo early. Feel free to add more!
type Attribute = 'value' | 'checked' | 'innerText';
type Attribute = 'value' | 'checked' | 'innerText' | 'options';

/// Type mapping for options
interface OptionsTypeMap {
Expand Down Expand Up @@ -146,8 +146,17 @@ export class HTMLOption<T extends OptionsType> {
*/
public changed(origin: OptionModificationOrigin) {
for (const bound of this._boundList) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
(bound.element as any)[bound.attribute] = this._value;
if (bound.attribute === 'options') {
// options take a list of comma-separated values to allow multiple settings
const values = (this._value as string).split(',');
const element = bound.element as HTMLSelectElement;
for (const option of element.options) {
option.selected = values.includes(option.value);
}
} else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
(bound.element as any)[bound.attribute] = this._value;
}
}

for (const callback of this.onchange) {
Expand Down Expand Up @@ -175,14 +184,27 @@ export class HTMLOption<T extends OptionsType> {
}
element = element as HTMLElement;

const listener = (event: Event) => {
assert(event.target !== null);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
this._update((event.target as any)[attribute].toString(), 'DOM');
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
(element as any)[attribute] = this._value;
let listener: (event: Event) => void;
if (attribute === 'options') {
listener = (event: Event) => {
// we need a special handler for multi-select options
assert(event.target !== null);
const element = event.target as HTMLSelectElement;
const values: string[] = Array.from(element.options)
.filter((option) => option.selected)
.map((option) => option.value);

this._update(values.toString(), 'DOM');
};
} else {
listener = (event: Event) => {
assert(event.target !== null);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
this._update((event.target as any)[attribute].toString(), 'DOM');
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
(element as any)[attribute] = this._value;
}
element.addEventListener('change', listener);

this._boundList.push({ element, attribute, listener });
Expand Down
14 changes: 12 additions & 2 deletions src/structure/options.html.in
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<div class="modal-body">
<h5 class="chsp-settings-section-title">Representation</h5>
<div class="chsp-settings-representation">
<div class="form-check form-switch">
<input class="form-check-input" id="atoms" type="checkbox" />
<label class="form-check-label" for="atoms" title="show atoms as spheres">atoms</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" id="bonds" type="checkbox" />
<label class="form-check-label" for="bonds" title="show bonds between atoms">bonds</label>
Expand All @@ -31,8 +35,14 @@
</div>
<div class="chsp-hide-if-no-shapes" style="margin-top: 1em">
<div class="input-group input-group-sm">
<label class="input-group-text" for="shapes" style="width: 5em">Shapes:</label>
<select id="shapes" class="form-select" title="which shape to show for the particle"></select>
<label
class="input-group-text"
for="shapes"
style="width: 5em"
title="which shapes to display; hold CTRL/CMD to select multiple shapes"
>Shapes:</label
>
<select id="shapes" class="form-select" multiple></select>
</div>
</div>

Expand Down
4 changes: 4 additions & 0 deletions src/structure/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import HTML_OPTIONS from './options.html.in';
export class StructureOptions extends OptionsGroup {
/// should we show bonds
public bonds: HTMLOption<'boolean'>;
/// should we show atoms
public atoms: HTMLOption<'boolean'>;
/// should we use space filling representation
public spaceFilling: HTMLOption<'boolean'>;
/// should we show atoms labels
Expand Down Expand Up @@ -61,6 +63,7 @@ export class StructureOptions extends OptionsGroup {
super();

this.bonds = new HTMLOption('boolean', true);
this.atoms = new HTMLOption('boolean', true);
this.spaceFilling = new HTMLOption('boolean', false);
this.atomLabels = new HTMLOption('boolean', false);
this.shape = new HTMLOption('string', '');
Expand Down Expand Up @@ -229,6 +232,7 @@ export class StructureOptions extends OptionsGroup {

this.spaceFilling.bind(this.getModalElement('space-filling'), 'checked');
this.bonds.bind(this.getModalElement('bonds'), 'checked');
this.atoms.bind(this.getModalElement('atoms'), 'checked');

this.rotation.bind(this.getModalElement('rotation'), 'checked');
this.unitCell.bind(this.getModalElement('unit-cell'), 'checked');
Expand Down
100 changes: 55 additions & 45 deletions src/structure/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,12 +433,15 @@ export class MoleculeViewer {

const selectShape = this._options.getModalElement<HTMLSelectElement>('shapes');
selectShape.options.length = 0;
selectShape.options.add(new Option('off', ''));
for (const key of Object.keys(structure['shapes'])) {
selectShape.options.add(new Option(key, key));
}

this._options.shape.bind(selectShape, 'value');
// leave space for up to 3 shapes in the settings, the other one
// will be accessible with a scrollbar
selectShape.size = Math.min(Object.keys(structure['shapes']).length, 3);

this._options.shape.bind(selectShape, 'options');
}

this._updateStyle();
Expand Down Expand Up @@ -608,6 +611,7 @@ export class MoleculeViewer {

this._options.spaceFilling.onchange.push(restyleAndRender);
this._options.bonds.onchange.push(restyleAndRender);
this._options.atoms.onchange.push(restyleAndRender);

this._options.atomLabels.onchange.push((showLabels) => {
if (this._current === undefined) {
Expand Down Expand Up @@ -915,52 +919,58 @@ export class MoleculeViewer {
assert(this._current.atomLabels.length === 0);

const structure = this._current.structure;

assert(!(structure.shapes === undefined));
const current_shape = structure.shapes[this._options.shape.value];
assert(!(current_shape === undefined));

const supercell_a = this._options.supercell[0].value;
const supercell_b = this._options.supercell[1].value;
const supercell_c = this._options.supercell[2].value;
let cell = this._current.structure.cell;
const active_shapes = this._options.shape.value.split(',');

if ((supercell_a > 1 || supercell_b > 1 || supercell_c > 1) && cell === undefined) {
return;
} else if (cell === undefined) {
cell = [1, 0, 0, 0, 1, 0, 0, 0, 1];
}
for (const shape of active_shapes) {
if (shape === '') {
continue;
}
assert(shape in structure.shapes);
const current_shape = structure.shapes[shape];
const supercell_a = this._options.supercell[0].value;
const supercell_b = this._options.supercell[1].value;
const supercell_c = this._options.supercell[2].value;
let cell = this._current.structure.cell;

if ((supercell_a > 1 || supercell_b > 1 || supercell_c > 1) && cell === undefined) {
return;
} else if (cell === undefined) {
cell = [1, 0, 0, 0, 1, 0, 0, 0, 1];
}

for (let a = 0; a < supercell_a; a++) {
for (let b = 0; b < supercell_b; b++) {
for (let c = 0; c < supercell_c; c++) {
for (let i = 0; i < structure.size; i++) {
const name = structure.names[i];
const position: [number, number, number] = [
structure.x[i] + a * cell[0] + b * cell[3] + c * cell[6],
structure.y[i] + a * cell[1] + b * cell[4] + c * cell[7],
structure.z[i] + a * cell[2] + b * cell[5] + c * cell[8],
];

if (current_shape[i].kind === 'ellipsoid') {
const data = current_shape[i] as unknown as EllipsoidData;
const shape = new Ellipsoid(position, data);
this._viewer.addCustom(
shape.outputTo3Dmol($3Dmol.elementColors.Jmol[name] || 0x000000)
);
} else if (current_shape[i].kind === 'custom') {
const data = current_shape[i] as unknown as CustomShapeData;
const shape = new CustomShape(position, data);
this._viewer.addCustom(
shape.outputTo3Dmol($3Dmol.elementColors.Jmol[name] || 0x000000)
);
} else {
assert(current_shape[i].kind === 'sphere');
const data = current_shape[i] as unknown as SphereData;
const shape = new Sphere(position, data);
this._viewer.addCustom(
shape.outputTo3Dmol($3Dmol.elementColors.Jmol[name] || 0x000000)
);
for (let a = 0; a < supercell_a; a++) {
for (let b = 0; b < supercell_b; b++) {
for (let c = 0; c < supercell_c; c++) {
for (let i = 0; i < structure.size; i++) {
const name = structure.names[i];
const position: [number, number, number] = [
structure.x[i] + a * cell[0] + b * cell[3] + c * cell[6],
structure.y[i] + a * cell[1] + b * cell[4] + c * cell[7],
structure.z[i] + a * cell[2] + b * cell[5] + c * cell[8],
];

if (current_shape[i].kind === 'ellipsoid') {
const data = current_shape[i] as unknown as EllipsoidData;
const shape = new Ellipsoid(position, data);
this._viewer.addCustom(
shape.outputTo3Dmol($3Dmol.elementColors.Jmol[name] || 0x000000)
);
} else if (current_shape[i].kind === 'custom') {
const data = current_shape[i] as unknown as CustomShapeData;
const shape = new CustomShape(position, data);
this._viewer.addCustom(
shape.outputTo3Dmol($3Dmol.elementColors.Jmol[name] || 0x000000)
);
} else {
assert(current_shape[i].kind === 'sphere');
const data = current_shape[i] as unknown as SphereData;
const shape = new Sphere(position, data);
this._viewer.addCustom(
shape.outputTo3Dmol($3Dmol.elementColors.Jmol[name] || 0x000000)
);
}
}
}
}
Expand All @@ -975,7 +985,7 @@ export class MoleculeViewer {
*/
private _mainStyle(): Partial<$3Dmol.AtomStyleSpec> {
const style: Partial<$3Dmol.AtomStyleSpec> = {};
if (this._options.shape.value === '') {
if (this._options.atoms.value) {
style.sphere = {
scale: this._options.spaceFilling.value ? 1.0 : 0.22,
};
Expand Down