Skip to content

Commit

Permalink
Initial support for rendering interactive 3d structures (#28)
Browse files Browse the repository at this point in the history
* pnpm i -D @threlte/core @threlte/preprocess @types/three svelte-sequential-preprocessor three

* add preprocessThrelte to svelte.config.js

* change jmol_colors from strings to number[]

* add 1st draft for Structure.svelte component powered by threlte

lots still missing like hover tooltips, fullscreen mode, more customization controls besides atomic radii, better rendering of unit cell, etc.

* add /structure demo page

* fix demo nav current route highlighting

* add StructureCard.svelte shown on /structure demo page
  • Loading branch information
janosh authored Apr 22, 2023
1 parent 83c8351 commit 7644f55
Show file tree
Hide file tree
Showing 10 changed files with 1,004 additions and 118 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,15 @@
"@playwright/test": "^1.32.3",
"@sveltejs/adapter-static": "2.0.2",
"@sveltejs/package": "^2.0.2",
"@threlte/core": "^5.0.9",
"@threlte/preprocess": "^0.0.2",
"@types/d3-array": "^3.0.4",
"@types/d3-color": "^3.1.0",
"@types/d3-interpolate-path": "^2.0.0",
"@types/d3-scale": "^4.0.3",
"@types/d3-scale-chromatic": "^3.0.0",
"@types/d3-shape": "^3.1.1",
"@types/three": "^0.150.2",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitest/coverage-c8": "^0.30.1",
Expand All @@ -63,9 +66,11 @@
"sharp": "^0.32.0",
"svelte-check": "^3.2.0",
"svelte-preprocess": "^5.0.3",
"svelte-sequential-preprocessor": "^1.0.0",
"svelte-toc": "^0.5.4",
"svelte-zoo": "^0.4.3",
"svelte2tsx": "^0.6.11",
"three": "^0.151.3",
"typescript": "5.0.4",
"vite": "^4.3.0",
"vitest": "^0.30.1"
Expand Down
153 changes: 153 additions & 0 deletions src/lib/Structure.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<script lang="ts">
import { Canvas, OrbitControls, T } from '@threlte/core'
import { atomic_colors, atomic_radii, type Structure } from './structure'
// output of pymatgen.core.Structure.as_dict()
export let structure: Structure
// scale factor for atomic radii
export let atom_radius: number = 0.5
// whether to use the same radius for all atoms. if not, the radius will be
// determined by the atomic radius of the element
export let same_size_atoms: boolean = true
// initial camera position from which to render the scene
export let camera_position: [number, number, number] = [10, 10, 10]
// zoom level of the camera
export let zoom: number = 60
// whether to show the controls panel
export let show_controls: boolean = false
// TODO whether to make the canvas fill the whole screen
// export let fullscreen: boolean = false
function on_keydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
show_controls = false
}
}
let tooltip = { visible: false, content: '', x: 0, y: 0 }
function on_mouse_enter(event, position, element) {
console.log(`event`, event)
const { clientX, clientY } = event.detail.event
tooltip.visible = true
tooltip.content = `${element} - (${position.join(', ')})`
tooltip.x = clientX
tooltip.y = clientY
}
function on_mouse_leave() {
tooltip.visible = false
}
const lattice = structure.lattice.matrix
$: ({ a, b, c } = structure.lattice)
</script>

<svelte:window on:keydown={on_keydown} />

<div>
<button class="controls-toggle" on:click={() => (show_controls = !show_controls)}>
{show_controls ? 'Hide' : 'Show'} controls
</button>
<section class="controls" class:open={show_controls}>
<label>
Atom radius
<input type="range" min="0.1" max="2" step="0.05" bind:value={atom_radius} />
</label>
<label>
<input type="checkbox" bind:checked={same_size_atoms} />
Scale atoms according to atomic radius (if false, all atoms have same size)
</label>
</section>

<div
class="tooltip"
style="top: {tooltip.y}px; left: {tooltip.x}px; display: {tooltip.visible
? 'block'
: 'none'};"
>
{tooltip.content}
</div>

<Canvas>
<T.PerspectiveCamera makeDefault position={camera_position} fov={zoom}>
<OrbitControls enableZoom enablePan target={{ x: a / 2, y: b / 2, z: c / 2 }} />
</T.PerspectiveCamera>

<T.DirectionalLight position={[3, 10, 10]} />
<T.DirectionalLight position={[-3, 10, -10]} intensity={0.2} />
<T.AmbientLight intensity={0.2} />

{#each structure.sites as { xyz: position, species }}
{@const symbol = species[0].element}
{@const radius = (same_size_atoms ? 1 : atomic_radii[symbol]) * atom_radius}
<T.Mesh
{position}
interactive
on:pointerenter={(e) => on_mouse_enter(e, position, symbol)}
on:pointerleave={on_mouse_leave}
>
<T.SphereGeometry args={[radius, 20, 20]} />
<T.MeshStandardMaterial
color="rgb({atomic_colors[symbol].map((x) => Math.floor(x * 255)).join(',')})"
/>
</T.Mesh>
{/each}

<!-- Render lattice as a cuboid of white lines -->
<T.Mesh position={[lattice[0][0] / 2, lattice[1][1] / 2, lattice[2][2] / 2]}>
<T.BoxGeometry args={[lattice[0][0], lattice[1][1], lattice[2][2]]} />
<T.MeshBasicMaterial transparent opacity={0.2} />
<T.LineSegments>
<T.EdgesGeometry />
<T.LineBasicMaterial color="white" />
</T.LineSegments>
</T.Mesh>
</Canvas>
</div>

<style>
div {
height: 600px;
width: 100%;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 3pt;
position: relative;
container-type: inline-size;
}
.controls-toggle {
position: absolute;
top: 5pt;
right: 5pt;
z-index: 100;
}
section.controls {
display: grid;
position: absolute;
top: 5pt;
right: 5pt;
background-color: rgba(255, 255, 255, 0.2);
padding: 2em 5pt 0;
border-radius: 3pt;
visibility: hidden;
opacity: 0;
transition: visibility 0.1s, opacity 0.1s linear;
max-width: 40cqw;
}
section.controls.open {
visibility: visible;
opacity: 1;
}
.tooltip {
position: absolute;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 5px;
border-radius: 5px;
pointer-events: none;
}
</style>
78 changes: 78 additions & 0 deletions src/lib/StructureCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<script lang="ts">
import { pretty_num } from './labels'
import { alphabetical_formula, density, type Structure } from './structure'
export let structure: Structure
export let title: string = ''
$: ({ volume, a, b, c, alpha, beta, gamma } = structure?.lattice ?? {})
</script>

<div class="structure-card">
{#if title || $$slots.title}
<h2>
<slot name="title">
{title}
</slot>
</h2>
{/if}
<strong>
formula:
<span class="value">{alphabetical_formula(structure)}</span>
</strong>
<strong>
Number of atoms:
<span class="value">{structure?.sites.length}</span>
</strong>
<strong>
Volume:
<span class="value">
{pretty_num(volume, '.1f')} ų
<small>
&nbsp; ({pretty_num(volume / structure?.sites.length, '.1f')} ų/atom)
</small></span
>
</strong>
<strong>
Density:
<span class="value">{density(structure)} g/cm³</span>
</strong>
<strong>
Lattice lengths (a, b, c):
<span class="value">{pretty_num(a)} Å, {pretty_num(b)} Å, {pretty_num(c)} Å</span>
</strong>
<strong>
Lattice angles (α, β, γ):
<span class="value">
{pretty_num(alpha)}°, {pretty_num(beta)}°, {pretty_num(gamma)}°
</span>
</strong>
</div>

<style>
.structure-card {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
border-radius: var(--sc-radius, 3pt);
padding: var(--sc-padding, 1ex 1em);
gap: var(--sc-gap, 1ex 1em);
background-color: var(--sc-bg, rgba(255, 255, 255, 0.1));
font-size: var(--sc-font-size);
}
h2 {
grid-column: 1 / -1;
margin: 1ex 0;
text-align: center;
}
strong {
display: flex;
justify-content: space-between;
align-items: center;
}
.value {
margin-left: var(--sc-value-margin, 1ex);
background-color: var(--sc-value-bg, rgba(255, 255, 255, 0.2));
padding: var(--sc-value-padding, 0 4pt);
border-radius: var(--sc-value-radius, 3pt);
}
</style>
Loading

0 comments on commit 7644f55

Please sign in to comment.