@ccp-nc/crystvis-js 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.mocharc.yml +4 -0
  2. package/.vscode/settings.json +5 -1
  3. package/CHANGELOG.md +54 -16
  4. package/README.md +75 -2
  5. package/audit.txt +12 -25
  6. package/changes.txt +1 -26
  7. package/demo/demo.css +15 -10
  8. package/demo/index.html +32 -5
  9. package/demo/main.js +149 -42
  10. package/lib/formats/magres.js +156 -1
  11. package/lib/model.js +31 -0
  12. package/lib/modelview.js +24 -0
  13. package/lib/render.js +97 -2
  14. package/lib/selbox.js +18 -4
  15. package/lib/visualizer.js +237 -1
  16. package/outdated.txt +9 -12
  17. package/package.json +7 -5
  18. package/test/data/hf_mu_test.magres +62 -0
  19. package/test/data/hf_test.magres +61 -0
  20. package/test/data/optimized_muon_65-hf.magres +1424 -0
  21. package/test/loader.js +115 -1
  22. package/test/model.js +118 -0
  23. package/test/setup-dom.cjs +147 -0
  24. package/test/visualizer.js +475 -0
  25. package/docs/data/search.json +0 -1
  26. package/docs/fonts/Inconsolata-Regular.ttf +0 -0
  27. package/docs/fonts/OpenSans-Regular.ttf +0 -0
  28. package/docs/fonts/WorkSans-Bold.ttf +0 -0
  29. package/docs/index.html +0 -10
  30. package/docs/lib_model.module_js-AtomImage.html +0 -3
  31. package/docs/lib_model.module_js-BondImage.html +0 -3
  32. package/docs/lib_model.module_js-Model.html +0 -3
  33. package/docs/lib_model.module_js.html +0 -3
  34. package/docs/lib_modelview.module_js-ModelView.html +0 -12
  35. package/docs/lib_modelview.module_js.html +0 -3
  36. package/docs/lib_visualizer.module_js-CrystVis.html +0 -3
  37. package/docs/lib_visualizer.module_js.html +0 -3
  38. package/docs/model.js.html +0 -2160
  39. package/docs/modelview.js.html +0 -449
  40. package/docs/scripts/core.js +0 -726
  41. package/docs/scripts/core.min.js +0 -23
  42. package/docs/scripts/resize.js +0 -90
  43. package/docs/scripts/search.js +0 -265
  44. package/docs/scripts/search.min.js +0 -6
  45. package/docs/scripts/third-party/Apache-License-2.0.txt +0 -202
  46. package/docs/scripts/third-party/fuse.js +0 -9
  47. package/docs/scripts/third-party/hljs-line-num-original.js +0 -369
  48. package/docs/scripts/third-party/hljs-line-num.js +0 -1
  49. package/docs/scripts/third-party/hljs-original.js +0 -5171
  50. package/docs/scripts/third-party/hljs.js +0 -1
  51. package/docs/scripts/third-party/popper.js +0 -5
  52. package/docs/scripts/third-party/tippy.js +0 -1
  53. package/docs/scripts/third-party/tocbot.js +0 -672
  54. package/docs/scripts/third-party/tocbot.min.js +0 -1
  55. package/docs/styles/clean-jsdoc-theme-base.css +0 -1159
  56. package/docs/styles/clean-jsdoc-theme-dark.css +0 -412
  57. package/docs/styles/clean-jsdoc-theme-light.css +0 -482
  58. package/docs/styles/clean-jsdoc-theme-scrollbar.css +0 -30
  59. package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +0 -1
  60. package/docs/styles/clean-jsdoc-theme.min.css +0 -1
  61. package/docs/tutorial-Events.html +0 -13
  62. package/docs/tutorial-Queries.html +0 -16
  63. package/docs/tutorial-ThreejsMigration.html +0 -25
  64. package/docs/visualizer.js.html +0 -574
package/test/loader.js CHANGED
@@ -259,8 +259,122 @@ H 0.0 1.0 0.0`;
259
259
  expect(loader.error_message).to.equal('Invalid Magres file format: block opened without closing');
260
260
 
261
261
  });
262
- it('should load properly a CELL file', function() {
263
262
 
263
+ it('should parse hyperfine tensors from magres_old block', function() {
264
+
265
+ var loader = new Loader();
266
+
267
+ // Test with the synthetic HF tensor test file (non-zero values)
268
+ var magres = fs.readFileSync(path.join(__dirname, 'data', 'hf_test.magres'), "utf8");
269
+ var a = loader.load(magres, 'magres')['magres'];
270
+
271
+ expect(loader.status).to.equal(Loader.STATUS_SUCCESS);
272
+ expect(a.length()).to.equal(2);
273
+
274
+ // hf array should exist
275
+ var hf = a.get_array('hf');
276
+ expect(hf).to.have.lengthOf(2);
277
+
278
+ // Each entry should be a TensorData object
279
+ expect(hf[0]).to.have.property('data');
280
+ expect(hf[1]).to.have.property('data');
281
+
282
+ // Check H 1 tensor matrix values
283
+ expect(hf[0].data).to.deep.almost.equal([
284
+ [ 1.2345, -0.5678, 0.1234],
285
+ [-0.5678, 2.3456, 0.4567],
286
+ [ 0.1234, 0.4567, 3.4567]
287
+ ]);
288
+
289
+ // Check C 1 tensor matrix values
290
+ expect(hf[1].data).to.deep.almost.equal([
291
+ [5.0, 1.0, 0.0],
292
+ [1.0, 5.0, 0.0],
293
+ [0.0, 0.0, 3.0]
294
+ ]);
295
+
296
+ // Check isotropic values (trace/3)
297
+ expect(hf[0].isotropy).to.almost.equal((1.2345 + 2.3456 + 3.4567) / 3);
298
+ expect(hf[1].isotropy).to.almost.equal((5.0 + 5.0 + 3.0) / 3);
299
+
300
+ // Check eigenvalues of C 1 (symmetric: known values 3, 4, 6)
301
+ var c_evals = hf[1].eigenvalues;
302
+ expect(c_evals).to.deep.almost.equal([3.0, 4.0, 6.0]);
303
+
304
+ // Check gamma ratios parsed from the synthetic file
305
+ var ratios = a.info['hf-gyromagnetic-ratios'];
306
+ expect(ratios).to.exist;
307
+ expect(ratios['H']).to.exist;
308
+ expect(ratios['H'].isotope).to.equal(1);
309
+ expect(ratios['H'].gamma).to.almost.equal(2.6752e8);
310
+ expect(ratios['C']).to.exist;
311
+ expect(ratios['C'].isotope).to.equal(13);
312
+ expect(ratios['C'].gamma).to.almost.equal(6.7283e7);
313
+
314
+ // Test with optimized_muon_65-hf.magres (real CASTEP output with non-zero HF tensors and H:Mu)
315
+ var muon_magres = fs.readFileSync(path.join(__dirname, 'data', 'optimized_muon_65-hf.magres'), "utf8");
316
+ var am = loader.load(muon_magres, 'magres')['magres'];
317
+
318
+ expect(am.length()).to.equal(69); // 16 H + 1 H:Mu + 28 C + 4 Fe + 2 N + 12 O + 4 S + 2 Br
319
+
320
+ var hf_m = am.get_array('hf');
321
+ expect(hf_m).to.have.lengthOf(69);
322
+
323
+ // All entries should be TensorData objects (not null)
324
+ hf_m.forEach(function(t) {
325
+ expect(t).to.have.property('data');
326
+ expect(t.data).to.have.lengthOf(3);
327
+ });
328
+
329
+ // H 1 tensor values are non-trivial
330
+ expect(hf_m[0].data).to.deep.almost.equal([
331
+ [-2.5897, 2.2293, 0.4459],
332
+ [ 2.2293, 0.8275, -0.5108],
333
+ [ 0.4459, -0.5108, 0.9140]
334
+ ]);
335
+
336
+ // H:Mu 1 is the last atom; check its isotropy
337
+ expect(hf_m[68].isotropy).to.almost.equal(-431.5713);
338
+
339
+ // Gamma ratios should include H:Mu as user-defined
340
+ var ratios_m = am.info['hf-gyromagnetic-ratios'];
341
+ expect(ratios_m['H:Mu'].isotope).to.equal(null);
342
+ expect(ratios_m['H:Mu'].gamma).to.almost.equal(8.5162e8);
343
+ });
344
+ it('should handle custom species (H:Mu) in magres files', function() {
345
+
346
+ var loader = new Loader();
347
+
348
+ var magres = fs.readFileSync(path.join(__dirname, 'data', 'hf_mu_test.magres'), "utf8");
349
+ var a = loader.load(magres, 'magres')['magres'];
350
+
351
+ expect(loader.status).to.equal(Loader.STATUS_SUCCESS);
352
+ expect(a.length()).to.equal(2);
353
+
354
+ // H:Mu should be stored as element H (colon-suffix stripped)
355
+ expect(a.get_chemical_symbols()).to.deep.equal(['H', 'H']);
356
+
357
+ // Both atoms should have HF tensors
358
+ var hf = a.get_array('hf');
359
+ expect(hf).to.have.lengthOf(2);
360
+ expect(hf[0]).to.have.property('data');
361
+ expect(hf[1]).to.have.property('data');
362
+
363
+ // H 1 tensor is diagonal [1,2,3]
364
+ expect(hf[0].eigenvalues).to.deep.almost.equal([1.0, 2.0, 3.0]);
365
+
366
+ // H:Mu 1 tensor was stored (check isotropy ~ -20)
367
+ expect(hf[1].isotropy).to.almost.equal((-10 - 20 - 30) / 3);
368
+
369
+ // Gamma ratios: standard H and user-defined H:Mu
370
+ var ratios = a.info['hf-gyromagnetic-ratios'];
371
+ expect(ratios['H'].isotope).to.equal(1);
372
+ expect(ratios['H'].gamma).to.almost.equal(2.6752e8);
373
+ expect(ratios['H:Mu'].isotope).to.equal(null);
374
+ expect(ratios['H:Mu'].gamma).to.almost.equal(8.5162e8);
375
+ });
376
+
377
+ it('should load properly a CELL file', function() {
264
378
  var loader = new Loader();
265
379
 
266
380
  var cell = fs.readFileSync(path.join(__dirname, 'data', 'ethanol.cell'), "utf8");
package/test/model.js CHANGED
@@ -394,4 +394,122 @@ describe('#modelview', function() {
394
394
  expect(newMV.length).to.equal(1);
395
395
  expect(newMV._indices.sort()).to.deep.equal([72]);
396
396
  });
397
+ });
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // Tests: ModelView serialisation (§6.1, §6.3) and Model reconstruction (§6.2, §6.3)
401
+ // ---------------------------------------------------------------------------
402
+
403
+ describe('ModelView#toIndices', function() {
404
+
405
+ it('returns a plain array equal to the view indices', function() {
406
+ var mv = h2omodel.find({ elements: ['O'] });
407
+ var result = mv.toIndices();
408
+ expect(result).to.deep.equal(mv.indices);
409
+ });
410
+
411
+ it('returns a copy — mutating it does not affect the view', function() {
412
+ var mv = h2omodel.find({ elements: ['O'] });
413
+ var result = mv.toIndices();
414
+ result.push(999);
415
+ expect(mv.indices.length).to.equal(result.length - 1);
416
+ });
417
+
418
+ it('returns an empty array for an empty view', function() {
419
+ var mv = h2omodel.view([]);
420
+ expect(mv.toIndices()).to.deep.equal([]);
421
+ });
422
+
423
+ });
424
+
425
+ describe('ModelView#toLabels', function() {
426
+
427
+ it('returns one crystLabel per atom in the view', function() {
428
+ var mv = h2omodel.find({ elements: ['O'] });
429
+ var labels = mv.toLabels();
430
+ expect(labels).to.be.an('array');
431
+ expect(labels.length).to.equal(mv.length);
432
+ labels.forEach(l => expect(l).to.be.a('string').and.have.length.above(0));
433
+ });
434
+
435
+ it('all labels contain the element symbol', function() {
436
+ var mv = h2omodel.find({ elements: ['H'] });
437
+ mv.toLabels().forEach(l => expect(l).to.match(/^H/));
438
+ });
439
+
440
+ it('returns an empty array for an empty view', function() {
441
+ var mv = h2omodel.view([]);
442
+ expect(mv.toLabels()).to.deep.equal([]);
443
+ });
444
+
445
+ it('round-trips: toLabels then viewFromLabels gives the same indices', function() {
446
+ // Use a multi-site structure for a meaningful round-trip
447
+ var mv = chamodel.find({ elements: ['O'] });
448
+ var labels = mv.toLabels();
449
+ var restored = chamodel.viewFromLabels(labels);
450
+ expect(restored.indices.sort((a,b)=>a-b))
451
+ .to.deep.equal(mv.indices.slice().sort((a,b)=>a-b));
452
+ });
453
+
454
+ });
455
+
456
+ describe('Model#viewFromIndices', function() {
457
+
458
+ it('produces a view with exactly the specified indices', function() {
459
+ var indices = [0, 2, 4];
460
+ var mv = h2omodel.viewFromIndices(indices);
461
+ expect(mv.indices).to.deep.equal(indices);
462
+ });
463
+
464
+ it('is equivalent to model.view(indices)', function() {
465
+ var indices = [1, 3];
466
+ var a = h2omodel.viewFromIndices(indices);
467
+ var b = h2omodel.view(indices);
468
+ expect(a.indices).to.deep.equal(b.indices);
469
+ });
470
+
471
+ it('defaults to an empty view when called with no arguments', function() {
472
+ var mv = h2omodel.viewFromIndices();
473
+ expect(mv.length).to.equal(0);
474
+ });
475
+
476
+ it('round-trips with toIndices()', function() {
477
+ var original = h2omodel.find({ elements: ['H'] });
478
+ var restored = h2omodel.viewFromIndices(original.toIndices());
479
+ expect(restored.indices).to.deep.equal(original.indices);
480
+ });
481
+
482
+ });
483
+
484
+ describe('Model#viewFromLabels', function() {
485
+
486
+ it('finds atoms matching the given labels', function() {
487
+ var mv = chamodel.find({ elements: ['Si'] });
488
+ var labels = mv.toLabels();
489
+ var restored = chamodel.viewFromLabels(labels);
490
+ expect(restored.length).to.equal(mv.length);
491
+ });
492
+
493
+ it('returns an empty view for an empty label array', function() {
494
+ var mv = chamodel.viewFromLabels([]);
495
+ expect(mv.length).to.equal(0);
496
+ });
497
+
498
+ it('ignores unknown labels', function() {
499
+ // Grab a real label dynamically, then mix it with a nonsense one
500
+ var validLabel = chamodel._atom_images[0].crystLabel;
501
+ var mv = chamodel.viewFromLabels([validLabel, 'DOES_NOT_EXIST']);
502
+ expect(mv.length).to.be.greaterThan(0);
503
+ mv.toLabels().forEach(l => expect(l).to.not.equal('DOES_NOT_EXIST'));
504
+ });
505
+
506
+ it('round-trips with toLabels() across a different supercell model', function() {
507
+ // Use the unit-cell model to get labels, then restore on the same model
508
+ var mv = chamodel.find({ elements: ['O'] });
509
+ var labels = mv.toLabels();
510
+ var restored = chamodel.viewFromLabels(labels);
511
+ expect(restored.indices.sort((a,b)=>a-b))
512
+ .to.deep.equal(mv.indices.slice().sort((a,b)=>a-b));
513
+ });
514
+
397
515
  });
@@ -0,0 +1,147 @@
1
+ /**
2
+ * test/setup-dom.cjs
3
+ *
4
+ * Mocha --require hook (CommonJS) that installs a minimal jsdom window as
5
+ * global so that DOM-dependent ES modules (THREE.js texture loader, jQuery,
6
+ * etc.) can be imported in the Node.js test environment without crashing.
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { JSDOM } = require('jsdom');
12
+
13
+ const dom = new JSDOM('<!DOCTYPE html><html><body><div id="crystvis"></div></body></html>', {
14
+ pretendToBeVisual: true,
15
+ resources: 'usable',
16
+ });
17
+
18
+ // Helper: set a global, falling back to Object.defineProperty for read-only
19
+ // properties (e.g. navigator in Node.js 23+).
20
+ function setGlobal(name, value) {
21
+ try {
22
+ global[name] = value;
23
+ } catch (_) {
24
+ Object.defineProperty(global, name, { value, writable: true, configurable: true });
25
+ }
26
+ }
27
+
28
+ // Expose browser globals expected by THREE.js, jQuery, etc.
29
+ setGlobal('window', dom.window);
30
+ setGlobal('document', dom.window.document);
31
+ setGlobal('navigator', dom.window.navigator);
32
+ setGlobal('HTMLElement', dom.window.HTMLElement);
33
+ setGlobal('HTMLCanvasElement', dom.window.HTMLCanvasElement);
34
+ setGlobal('Image', dom.window.Image);
35
+ setGlobal('ImageData', dom.window.ImageData);
36
+ setGlobal('XMLHttpRequest', dom.window.XMLHttpRequest);
37
+ setGlobal('requestAnimationFrame', (cb) => setTimeout(cb, 16));
38
+ setGlobal('cancelAnimationFrame', (id) => clearTimeout(id));
39
+ setGlobal('ResizeObserver', class ResizeObserver {
40
+ observe() {}
41
+ unobserve() {}
42
+ disconnect() {}
43
+ });
44
+
45
+ // Stub WebGL context so THREE.WebGLRenderer does not crash on getContext()
46
+ const canvasProto = dom.window.HTMLCanvasElement.prototype;
47
+ const _origGetContext = canvasProto.getContext;
48
+ canvasProto.getContext = function (type, ...args) {
49
+ if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') {
50
+ // Return a minimal WebGL stub that THREE can interrogate without causing
51
+ // errors. Only the methods referenced during renderer initialisation
52
+ // need to be present.
53
+ return {
54
+ canvas: this,
55
+ drawingBufferWidth: 300,
56
+ drawingBufferHeight: 150,
57
+ getExtension: () => null,
58
+ getParameter: () => null,
59
+ getShaderPrecisionFormat: () => ({ rangeMin: 127, rangeMax: 127, precision: 23 }),
60
+ createTexture: () => ({}),
61
+ bindTexture: () => {},
62
+ texParameteri: () => {},
63
+ pixelStorei: () => {},
64
+ enable: () => {},
65
+ disable: () => {},
66
+ blendEquation: () => {},
67
+ blendFunc: () => {},
68
+ depthFunc: () => {},
69
+ depthMask: () => {},
70
+ colorMask: () => {},
71
+ clearColor: () => {},
72
+ clearDepth: () => {},
73
+ clearStencil: () => {},
74
+ scissor: () => {},
75
+ viewport: () => {},
76
+ clear: () => {},
77
+ useProgram: () => {},
78
+ frontFace: () => {},
79
+ cullFace: () => {},
80
+ createBuffer: () => ({}),
81
+ bindBuffer: () => {},
82
+ bufferData: () => {},
83
+ createFramebuffer: () => ({}),
84
+ bindFramebuffer: () => {},
85
+ createRenderbuffer: () => ({}),
86
+ bindRenderbuffer: () => {},
87
+ renderbufferStorage: () => {},
88
+ framebufferRenderbuffer: () => {},
89
+ framebufferTexture2D: () => {},
90
+ createProgram: () => ({}),
91
+ createShader: () => ({}),
92
+ shaderSource: () => {},
93
+ compileShader: () => {},
94
+ attachShader: () => {},
95
+ linkProgram: () => {},
96
+ getProgramParameter: (p, pname) => {
97
+ // LINK_STATUS
98
+ if (pname === 35714) return true;
99
+ return null;
100
+ },
101
+ getShaderParameter: () => true,
102
+ getUniformLocation: () => ({}),
103
+ getAttribLocation: () => 0,
104
+ uniform1i: () => {},
105
+ uniform1f: () => {},
106
+ uniform2f: () => {},
107
+ uniform3f: () => {},
108
+ uniform4f: () => {},
109
+ uniformMatrix3fv: () => {},
110
+ uniformMatrix4fv: () => {},
111
+ vertexAttribPointer: () => {},
112
+ enableVertexAttribArray: () => {},
113
+ disableVertexAttribArray: () => {},
114
+ drawArrays: () => {},
115
+ drawElements: () => {},
116
+ deleteBuffer: () => {},
117
+ deleteTexture: () => {},
118
+ deleteProgram: () => {},
119
+ deleteShader: () => {},
120
+ deleteFramebuffer: () => {},
121
+ deleteRenderbuffer: () => {},
122
+ isContextLost: () => false,
123
+ getError: () => 0,
124
+ // WebGL2 extras
125
+ createVertexArray: () => ({}),
126
+ bindVertexArray: () => {},
127
+ deleteVertexArray: () => {},
128
+ texImage2D: () => {},
129
+ texImage3D: () => {},
130
+ texStorage2D: () => {},
131
+ texStorage3D: () => {},
132
+ blitFramebuffer: () => {},
133
+ readBuffer: () => {},
134
+ drawBuffers: () => {},
135
+ renderbufferStorageMultisample: () => {},
136
+ createSampler: () => ({}),
137
+ deleteSampler: () => {},
138
+ bindSampler: () => {},
139
+ samplerParameteri: () => {},
140
+ samplerParameterf: () => {},
141
+ };
142
+ }
143
+ if (_origGetContext) {
144
+ return _origGetContext.call(this, type, ...args);
145
+ }
146
+ return null;
147
+ };