Skip to content

Commit 712b41a

Browse files
committed
update to time dependent animation
1 parent 6d0c9d6 commit 712b41a

8 files changed

Lines changed: 1516 additions & 229 deletions

File tree

src/components/crystal-toolkit/PhononAnimationScene/PhononAnimationScene.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { mount } from 'enzyme';
22
import * as React from 'react';
3-
import { CrystalToolkitAnimationScene } from './PhononAnimationScene';
3+
import { PhononAnimationScene } from './PhononAnimationScene';
44
import { phonon_scene as scene } from '../scene/phonon-animation-scene';
55
import { MOUNT_NODE_CLASS, Renderer } from '../scene/constants';
66
import Scene from '../scene/Scene';
@@ -10,7 +10,7 @@ const RENDERSCENE_CALLS_BY_REACT_RENDERING = 3; // goal is to reach 1 and stay t
1010

1111
// When we run test, three.js is bundled differently, and we encounter again the bug
1212
// where we have 2 different instances of three
13-
describe('<CrystalToolkitAnimationScene/>', () => {
13+
describe('<PhononAnimationScene/>', () => {
1414
it('should be rendered', () => {
1515
const wrapper = renderElement();
1616
expect(wrapper.find(`.${MOUNT_NODE_CLASS}`).length).toBe(1);
@@ -34,7 +34,7 @@ describe('<CrystalToolkitAnimationScene/>', () => {
3434
function renderElement() {
3535
// we use mount to test the rendering of the underlying elements
3636
return mount(
37-
<CrystalToolkitAnimationScene
37+
<PhononAnimationScene
3838
sceneSize={500}
3939
settings={{
4040
renderer: Renderer.SVG

src/components/crystal-toolkit/PhononAnimationScene/PhononAnimationScene.tsx

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import React, {
99
} from 'react';
1010
import Scene from '../scene/Scene';
1111
import { subscribe } from '../scene/download-event';
12-
import './CrystalToolkitAnimationScene.less';
12+
import './PhononAnimationScene.less';
1313
import {
1414
AnimationStyle,
1515
DEBUG_STYLE,
@@ -102,6 +102,13 @@ export interface PhononAnimationSceneProps {
102102
* defaultZoom: 1, // 1 will zoom to fit object exactly, <1 will add padding between object and box bounds
103103
* zoomToFit2D: false // if true, will zoom to fit object only along the X and Y axes (not Z)
104104
* extractAxis: false // will remove the axis from the main scene
105+
* omega: 0.0 // The eigen frequency
106+
* phases: [0, 0] // Per-atom phase theta_i = q · R_i (radians). Length should equal number of atoms.
107+
* amplitude: 150 //The amplitude of vibration
108+
* eigenVectors: [[[2.369e-7, 0], [-6.908e-7, 0], [-0.002, 0]], [[2.369e-7, 0], [-6.908e-7, 0], [-0.002, 0]]] // The eigenvectors for each atom with the given wave vector and frequency
109+
* velocity: 0.5 // The velocity coef, scale 0 to 1
110+
*
111+
*
105112
* }
106113
* There are several additional options used for debugging and testing,
107114
* please consult the source code directly for these.
@@ -235,23 +242,6 @@ export interface PhononAnimationSceneProps {
235242
* @default true
236243
*/
237244
showPositionButton?: boolean;
238-
/**
239-
* The eigen frequency
240-
*/
241-
omega: number;
242-
/**
243-
* Per-atom phase theta_i = q · R_i (radians).
244-
* Length should equal number of atoms.
245-
*/
246-
phases: number[];
247-
/**
248-
* The amplitude of vibration
249-
*/
250-
amplitude: number;
251-
/**
252-
* The eigenvectors for each atom with the given wave vector and frequency
253-
*/
254-
eigenVectors: number[];
255245
}
256246

257247
/**
@@ -410,6 +400,10 @@ export const PhononAnimationScene: React.FC<PhononAnimationSceneProps> = ({
410400

411401
// called after the component is mounted, so refs are correctly populated
412402
useEffect(() => {
403+
if (!props.data || !props.data.name || !props.data.contents) {
404+
return;
405+
}
406+
413407
const _s = (scene.current = new Scene(
414408
props.data,
415409
mountNodeRef.current!,
@@ -437,7 +431,9 @@ export const PhononAnimationScene: React.FC<PhononAnimationSceneProps> = ({
437431
mountNodeDebugRef.current!
438432
));
439433
_s.removeListener();
440-
_s.animate();
434+
if (props.data !== undefined && props.data !== null) {
435+
_s.animate();
436+
}
441437

442438
/**
443439
* I believe this can be removed because image requesting is now handled by
@@ -450,17 +446,18 @@ export const PhononAnimationScene: React.FC<PhononAnimationSceneProps> = ({
450446
subscription.unsubscribe();
451447
_s.onDestroy();
452448
};
453-
}, []);
449+
}, [props.data]);
454450

455451
// Note(chab) those hooks will be executed sequentially at mount time, and on change of the deps array elements
456-
useEffect(
457-
() => scene.current!.enableDebug(props.debug!, mountNodeDebugRef.current),
458-
[props.debug]
459-
);
452+
useEffect(() => {
453+
if (!scene.current) return;
454+
scene.current!.enableDebug(props.debug!, mountNodeDebugRef.current);
455+
}, [props.debug]);
460456
// An interesting classical react issue that we fixed : look at the stories, we do not pass anymore an empty object,
461457
// but a reference to an empty object, otherwise, it will be a different reference, and treated as a different object, thus
462458
// triggering the effect
463459
useEffect(() => {
460+
if (!scene.current) return;
464461
if (!props.data || !(props.data as any).name || !(props.data as any).contents) {
465462
console.warn(
466463
'no data passed ( or missing name /content ), scene will not be updated',
@@ -470,25 +467,28 @@ export const PhononAnimationScene: React.FC<PhononAnimationSceneProps> = ({
470467
}
471468

472469
//FIXME(chab) we have to much calls to renderScene
473-
!!props.data && scene.current!.addToScene(props.data, false);
470+
//!!props.data && scene.current!.addToScene(props.data, false);
471+
scene.current.addToScene(props.data, false);
474472
// !!props.data && scene.current!.addToScene(props.data, false);
475473
// scene.current!.animate();
476474
scene.current!.toggleVisibility(props.toggleVisibility as any);
477475
}, [props.data]);
478-
useEffect(
479-
() => scene.current!.toggleVisibility(props.toggleVisibility as any),
480-
[props.toggleVisibility]
481-
);
482-
useEffect(
483-
() => scene.current!.updateInsetSettings(props.inletSize!, props.inletPadding!, props.axisView),
484-
[props.inletSize, props.inletPadding, props.axisView]
485-
);
476+
useEffect(() => {
477+
if (!scene.current) return;
478+
scene.current!.toggleVisibility(props.toggleVisibility as any);
479+
}, [props.toggleVisibility]);
480+
useEffect(() => {
481+
if (!scene.current) return;
482+
scene.current!.updateInsetSettings(props.inletSize!, props.inletPadding!, props.axisView);
483+
}, [props.inletSize, props.inletPadding, props.axisView]);
486484

487485
useEffect(() => {
486+
if (!scene.current) return;
488487
scene.current!.resizeRendererToDisplaySize();
489488
}, [props.sceneSize]);
490489

491490
useEffect(() => {
491+
if (!scene.current) return;
492492
const { filetype } = props.imageRequest;
493493
if (filetype) {
494494
requestImage(filetype, scene.current!);
@@ -505,6 +505,7 @@ export const PhononAnimationScene: React.FC<PhononAnimationSceneProps> = ({
505505
const cameraDispatch = cameraContext ? cameraContext.dispatch : cameraReducerDispatch;
506506
if (cameraState) {
507507
useEffect(() => {
508+
if (!scene.current) return;
508509
props.setProps({ currentCameraState: cameraState });
509510

510511
if (cameraState && cameraState.position && cameraState.quaternion && cameraState.zoom) {
@@ -528,6 +529,7 @@ export const PhononAnimationScene: React.FC<PhononAnimationSceneProps> = ({
528529
* and save the new state into the cameraState
529530
*/
530531
useEffect(() => {
532+
if (!scene.current) return;
531533
if (props.customCameraState) {
532534
const { position: p, quaternion: q, zoom } = props.customCameraState;
533535
/**
@@ -554,6 +556,7 @@ export const PhononAnimationScene: React.FC<PhononAnimationSceneProps> = ({
554556
}, [props.customCameraState]);
555557

556558
useEffect(() => {
559+
if (!scene.current) return;
557560
props.animation && scene.current!.updateAnimationStyle(props.animation as AnimationStyle);
558561
}, [props.animation]);
559562

src/components/crystal-toolkit/scene/Scene.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export default class Scene {
103103
if (!renderer) {
104104
throw new Error('No renderer');
105105
}
106+
106107
this.renderer = renderer;
107108
this.renderer.setSize(this.cachedMountNodeSize.width, this.cachedMountNodeSize.height);
108109
//TODO(chab) This should be simpler
@@ -384,10 +385,10 @@ export default class Scene {
384385
sceneJson.amplitude,
385386
sceneJson.phases,
386387
sceneJson.omega,
387-
sceneJson.eigenVectors
388+
sceneJson.eigenVectors,
389+
sceneJson.velocity
388390
)
389391
: new AnimationHelper(this.objectBuilder);
390-
// this.animationHelper = new AnimationHelper(this.objectBuilder);
391392
window.addEventListener('resize', this.windowListener, false);
392393
this.inset = new InsetHelper(
393394
this.axis,
@@ -809,12 +810,21 @@ export default class Scene {
809810
this.inset.onDestroy();
810811
this.controls.dispose();
811812
disposeSceneHierarchy(this.scene);
812-
this.scene.dispose();
813+
// this.scene.dispose();
813814
if (this.renderer instanceof THREE.WebGLRenderer) {
814815
this.renderer.forceContextLoss();
815816
this.renderer.dispose();
816817
}
817-
this.renderer.domElement!.parentElement!.removeChild(this.renderer.domElement);
818+
// remove CSS2D overlay
819+
if (this.labelRenderer?.domElement?.parentElement) {
820+
this.labelRenderer.domElement.parentElement.removeChild(this.labelRenderer.domElement);
821+
}
822+
823+
// remove canvas/SVG
824+
if (this.renderer?.domElement?.parentElement) {
825+
this.renderer.domElement.parentElement.removeChild(this.renderer.domElement);
826+
}
827+
// this.renderer.domElement!.parentElement!.removeChild(this.renderer.domElement);
818828
this.renderer.domElement = undefined as any;
819829
this.renderer = null as any;
820830
this.stop();

src/components/crystal-toolkit/scene/phonon-animation-helper.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,25 @@ export class PhononAnimationHelper {
2727
private A: number,
2828
private phases: number[],
2929
private omega: number,
30-
private eigenVectors: number[]
30+
private eigenVectors: number[],
31+
private velocity: number
3132
) {
32-
this.atomNumber = phases.length;
33+
this.atomNumber = Array.isArray(phases) ? phases.length : 0;
3334
}
3435

3536
public reset() {
3637
this.atomMeshes = new Array(this.atomNumber);
37-
this.unitCellAtomIndexArray = new Array(this.atomNumber);
38+
this.unitCellAtomIndexArray = new Array<number>(this.atomNumber);
3839
this.bondMeshes = new Map<string, THREE.Mesh>();
3940
}
4041

4142
public buildAnimationSupport(json: SceneJsonObject, three: THREE.Object3D) {
4243
if (json.type === JSON3DObject.SPHERES) {
43-
const atomIndex = json._meta.atom_idx?.[0];
44+
if (json._meta === undefined) return;
45+
const atomIndex = json._meta[0].atom_idx?.[0];
4446
if (atomIndex === undefined) return;
45-
const unitCellAtomIndex = json._meta.unit_cell_atom_idx?.[0];
47+
const unitCellAtomIndex = json._meta[0].unit_cell_atom_idx?.[0];
48+
if (atomIndex === undefined || unitCellAtomIndex === undefined) return;
4649
this.unitCellAtomIndexArray[atomIndex] = unitCellAtomIndex;
4750

4851
const mesh = three.children[0] as THREE.Mesh;
@@ -52,7 +55,9 @@ export class PhononAnimationHelper {
5255
if (!meta) return;
5356

5457
for (let i = meta.length - 1; i >= 0; i--) {
55-
const [atomIndex1, atomIndex2] = meta[i].atom_idx;
58+
const pair = meta[i].atom_idx;
59+
if (!pair || pair.length < 2) return;
60+
const [atomIndex1, atomIndex2] = pair;
5661
const bondKey = PhononAnimationHelper.bondKey(atomIndex1, atomIndex2);
5762

5863
if (!this.bondMeshes.has(bondKey)) {
@@ -69,30 +74,30 @@ export class PhononAnimationHelper {
6974
public updateTime(time: number) {}
7075

7176
public animate() {
72-
// requestAnimationFrame(this.animate);
73-
7477
const delta = this.clock.getElapsedTime();
75-
const A = 100; // this.A;
78+
const velocity = this.velocity;
79+
const modified_delta = delta * velocity;
80+
const A = this.A;
7681
const omega = this.omega;
7782
const phases = this.phases;
7883

7984
const temAtomPosition = new Array(this.atomNumber);
8085

8186
// update atoms
8287
this.atomMeshes.forEach((mesh, atomIndex) => {
83-
let base = mesh.userData.basePos as THREE.Vector3 | undefined;
88+
let base = mesh.userData.basePos;
8489

8590
const unitCellAtomIndex = this.unitCellAtomIndexArray[atomIndex];
8691

87-
if (!base) {
92+
if (!(base && base.isVector3)) {
8893
base = mesh.position.clone();
8994
mesh.userData.basePos = base;
9095
}
9196

9297
const phase = phases[unitCellAtomIndex];
9398
const eigenVector = this.eigenVectors[unitCellAtomIndex];
9499

95-
const theta = omega * delta + phase;
100+
const theta = phase - omega * modified_delta;
96101

97102
const cos = Math.cos(theta);
98103
const sin = Math.sin(theta);

0 commit comments

Comments
 (0)