-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial support for rendering interactive 3d structures (#28)
* 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
Showing
10 changed files
with
1,004 additions
and
118 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
({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> |
Oops, something went wrong.