Skip to content

Commit cef2c94

Browse files
committed
v0.1.2
- fix ASE ULM trajectory file handling in VSCode extension - add tests for trajectory viewer FPS slider + Structure canvas resizing - more extension file pattern matching and webview tests
1 parent ae171d4 commit cef2c94

File tree

11 files changed

+319
-30
lines changed

11 files changed

+319
-30
lines changed

changelog.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Changelog
22

3+
#### [v0.1.2](https://github.com/janosh/matterviz/compare/v0.1.2...v0.1.2)
4+
5+
> 4 July 2025
6+
7+
### 🛠 Enhancements
8+
9+
- Allow toggling between histogram and line plot of properties in Trajectory viewer by @janosh in https://github.com/janosh/matterviz/pull/85
10+
- VSCode extension for rendering structures and trajectories with MatterViz directly in editor tabs by @janosh in https://github.com/janosh/matterviz/pull/82
11+
12+
**Full Changelog**: https://github.com/janosh/matterviz/compare/v0.1.0...v0.1.2
13+
14+
#### [v0.1.1](https://github.com/janosh/matterviz/compare/v0.1.1...v0.1.2)
15+
16+
> 19 June 2025
17+
18+
### 🛠 Enhancements
19+
20+
- Big speedup of binary trajectory parsing by avoiding data-URI conversion, use ArrayBuffer directly by @janosh in https://github.com/janosh/matterviz/pull/81
21+
- Force vectors by @janosh in https://github.com/janosh/matterviz/pull/80
22+
323
## [v0.1.0](https://github.com/janosh/matterviz/compare/v0.1.0...v0.1.0)
424

525
> 19 June 2025

extensions/vscode/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "matterviz",
33
"displayName": "MatterViz",
44
"description": "Visualize crystal structures and MD trajectories in VSCode",
5-
"version": "0.1.0",
5+
"version": "0.1.2",
66
"publisher": "janosh",
77
"type": "module",
88
"icon": "icon.png",
@@ -55,7 +55,8 @@
5555
"scripts": {
5656
"build": "rm -rf dist && tsc && mv dist/extension.{,c}js && vite build",
5757
"dev": "tsc --watch & vite build --watch & mv dist/extension.{,c}js",
58-
"vitest": "vitest"
58+
"vitest": "vitest",
59+
"package": "rm -rf dist && tsc && mv dist/extension.{,c}js && vite build && vsce package"
5960
},
6061
"devDependencies": {
6162
"@sveltejs/vite-plugin-svelte": "^5.1.0",

extensions/vscode/src/extension.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,29 @@ interface MessageData {
2424
export function is_trajectory_file(filename: string): boolean {
2525
const name = filename.toLowerCase()
2626
return (
27+
// Standard trajectory file extensions
2728
name.match(/\.(traj|xyz|extxyz|h5|hdf5)$/) !== null ||
28-
/(xdatcar|trajectory|traj|md|relax)/.test(name) ||
29+
// Files with trajectory-related keywords
30+
/(xdatcar|trajectory|traj|md|relax|npt|nvt|nve)/.test(name) ||
31+
// Compressed trajectory files
2932
/\.(xyz|extxyz|traj)\.gz$/.test(name) ||
30-
(name.endsWith(`.gz`) && /(traj|xdatcar|trajectory|relax|xyz)/.test(name))
33+
(name.endsWith(`.gz`) &&
34+
/(traj|xdatcar|trajectory|relax|xyz|md|npt|nvt|nve)/.test(name))
3135
)
3236
}
3337

3438
// Read file from filesystem
3539
export const read_file = (file_path: string): FileData => {
3640
const filename = path.basename(file_path)
37-
const compressed = /\.(gz|traj|h5|hdf5)$/.test(filename)
41+
// Binary files that should be read as base64
42+
const is_binary = /\.(gz|traj|h5|hdf5)$/.test(filename)
43+
3844
return {
3945
filename,
40-
content: compressed
46+
content: is_binary
4147
? fs.readFileSync(file_path).toString(`base64`)
4248
: fs.readFileSync(file_path, `utf8`),
43-
isCompressed: compressed,
49+
isCompressed: is_binary,
4450
}
4551
}
4652

extensions/vscode/tests/extension.test.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ describe(`MatterViz Extension`, () => {
6969
}
7070
const mock_context = { extensionUri: { fsPath: `/test` }, subscriptions: [] }
7171

72+
// only checking filename recognition, files don't need to exist
7273
test.each([
7374
// Basic trajectory files
7475
[`test.traj`, true],
@@ -81,6 +82,13 @@ describe(`MatterViz Extension`, () => {
8182
[`trajectory.dat`, true],
8283
[`md.xyz.gz`, true],
8384
[`relax.extxyz`, true],
85+
// ASE ULM binary trajectory files (specific to this fix)
86+
[`md_npt_300K.traj`, true],
87+
[`ase-LiMnO2-chgnet-relax.traj`, true],
88+
[`simulation_nvt_250K.traj`, true],
89+
[`molecular_dynamics_nve.traj`, true],
90+
[`water_cluster_md.traj`, true],
91+
[`optimization_relax.traj`, true],
8492
// Case insensitive
8593
[`FILE.TRAJ`, true],
8694
[`TrAjEcToRy.XyZ`, true],
@@ -110,8 +118,10 @@ describe(`MatterViz Extension`, () => {
110118
test.each([
111119
[`test.gz`, true],
112120
[`test.h5`, true],
113-
[`test.traj`, true],
121+
[`test.traj`, true], // ASE binary files should be treated as compressed
114122
[`test.hdf5`, true],
123+
[`md_npt_300K.traj`, true], // Specific ASE ULM binary file
124+
[`ase-LiMnO2-chgnet-relax.traj`, true], // Another ASE ULM binary file
115125
[`test.cif`, false],
116126
[`test.xyz`, false],
117127
[`test.json`, false],
@@ -129,6 +139,67 @@ describe(`MatterViz Extension`, () => {
129139
}
130140
})
131141

142+
test.each([
143+
[`md_npt_300K.traj`, true, true], // ASE binary trajectory
144+
[`ase-LiMnO2-chgnet-relax.traj`, true, true], // ASE binary trajectory
145+
[`simulation_nvt_250K.traj`, true, true], // ASE binary trajectory
146+
[`water_cluster_md.traj`, true, true], // ASE binary trajectory
147+
[`optimization_relax.traj`, true, true], // ASE binary trajectory
148+
[`regular_text.traj`, true, true], // .traj files are always binary
149+
[`test.xyz`, true, false], // Text trajectory file
150+
[`test.extxyz`, true, false], // Text trajectory file
151+
[`test.cif`, false, false], // Not a trajectory file
152+
])(
153+
`ASE trajectory file handling: "%s" → trajectory:%s, binary:%s`,
154+
(filename, is_trajectory, is_binary) => {
155+
expect(is_trajectory_file(filename)).toBe(is_trajectory)
156+
157+
if (is_trajectory) {
158+
const result = read_file(`/test/${filename}`)
159+
expect(result.isCompressed).toBe(is_binary)
160+
}
161+
},
162+
)
163+
164+
// Integration test for ASE trajectory file processing (simulates the exact failing scenario)
165+
test(`ASE trajectory file end-to-end processing`, () => {
166+
const ase_filename = `ase-LiMnO2-chgnet-relax.traj`
167+
168+
// Step 1: Extension should detect this as a trajectory file
169+
expect(is_trajectory_file(ase_filename)).toBe(true)
170+
171+
// Step 2: Extension should read this as binary (compressed)
172+
const file_result = read_file(`/test/${ase_filename}`)
173+
expect(file_result.filename).toBe(ase_filename)
174+
expect(file_result.isCompressed).toBe(true)
175+
expect(file_result.content).toBe(`mock content`) // base64 encoded binary data
176+
177+
// Step 3: Verify webview data structure matches expected format
178+
const webview_data = {
179+
type: `trajectory` as const,
180+
data: file_result,
181+
}
182+
183+
// Step 4: HTML generation should work with this data
184+
const html = create_html(
185+
mock_webview as unknown as vscode.Webview,
186+
mock_context as unknown as vscode.ExtensionContext,
187+
webview_data,
188+
)
189+
190+
expect(html).toContain(`<!DOCTYPE html>`)
191+
expect(html).toContain(JSON.stringify(webview_data))
192+
193+
// Step 5: Verify the exact data structure that would be sent to webview
194+
const parsed_data = JSON.parse(
195+
html.match(/mattervizData=(.+?)</s)?.[1] || `{}`,
196+
)
197+
expect(parsed_data.type).toBe(`trajectory`)
198+
expect(parsed_data.data.filename).toBe(ase_filename)
199+
expect(parsed_data.data.isCompressed).toBe(true)
200+
expect(parsed_data.data.content).toBe(`mock content`)
201+
})
202+
132203
test.each([
133204
[{ fsPath: `/test/file.cif` }, `file.cif`],
134205
[{ fsPath: `/test/structure.xyz` }, `structure.xyz`],
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { describe, expect, test } from 'vitest'
2+
3+
// Local implementation of base64_to_array_buffer for testing
4+
// This matches the implementation in webview/src/main.ts
5+
function base64_to_array_buffer(base64: string): ArrayBuffer {
6+
const binary = atob(base64)
7+
const bytes = new Uint8Array(binary.length)
8+
for (let idx = 0; idx < binary.length; idx++) {
9+
bytes[idx] = binary.charCodeAt(idx)
10+
}
11+
return bytes.buffer
12+
}
13+
14+
describe(`Webview Integration - ASE Binary Trajectory Support`, () => {
15+
test.each([
16+
[`SGVsbG8gV29ybGQ=`, `Hello World`, 11], // Basic ASCII
17+
[`QUJDREVGR0g=`, `ABCDEFGH`, 8], // Another ASCII
18+
[``, ``, 0], // Empty string
19+
[`QQ==`, `A`, 1], // Single character
20+
[`QUI=`, `AB`, 2], // Two characters
21+
])(
22+
`base64_to_array_buffer: %s → %s (%i bytes)`,
23+
(base64, expected, byte_length) => {
24+
const result = base64_to_array_buffer(base64)
25+
expect(result).toBeInstanceOf(ArrayBuffer)
26+
expect(result.byteLength).toBe(byte_length)
27+
expect(new TextDecoder().decode(result)).toBe(expected)
28+
},
29+
)
30+
31+
test(`base64_to_array_buffer handles ASE trajectory file header correctly`, () => {
32+
const ase_header = new Uint8Array([
33+
0x2d,
34+
0x20,
35+
0x6f,
36+
0x66,
37+
0x20,
38+
0x55,
39+
0x6c,
40+
0x6d,
41+
0x41,
42+
0x53,
43+
0x45,
44+
0x2d,
45+
0x54,
46+
0x72,
47+
0x61,
48+
0x6a,
49+
])
50+
const base64 = btoa(String.fromCharCode(...ase_header))
51+
const result = base64_to_array_buffer(base64)
52+
53+
expect(result.byteLength).toBe(ase_header.length)
54+
expect(Array.from(new Uint8Array(result))).toEqual(Array.from(ase_header))
55+
expect(new TextDecoder().decode(result.slice(0, 8))).toBe(`- of Ulm`)
56+
})
57+
58+
test(`base64_to_array_buffer preserves byte order and handles performance`, () => {
59+
// Test byte order preservation
60+
const test_bytes = new Uint8Array([
61+
0x00,
62+
0x01,
63+
0x02,
64+
0x03,
65+
0xFF,
66+
0xFE,
67+
0xFD,
68+
0xFC,
69+
])
70+
const base64 = btoa(String.fromCharCode(...test_bytes))
71+
const result = base64_to_array_buffer(base64)
72+
expect(Array.from(new Uint8Array(result))).toEqual(Array.from(test_bytes))
73+
74+
// Test performance with large data
75+
const large_data = new Uint8Array(10000).fill(42)
76+
const large_base64 = btoa(String.fromCharCode(...large_data))
77+
const start = performance.now()
78+
const large_result = base64_to_array_buffer(large_base64)
79+
expect(performance.now() - start).toBeLessThan(100)
80+
expect(large_result.byteLength).toBe(large_data.length)
81+
const result_array = new Uint8Array(large_result)
82+
expect(result_array[0]).toBe(42)
83+
expect(result_array[result_array.length - 1]).toBe(42)
84+
})
85+
86+
test.each([1024, 8192, 32768])(
87+
`handles typical ASE trajectory file size: %i bytes`,
88+
(size) => {
89+
const data = new Uint8Array(size)
90+
for (let i = 0; i < size; i++) data[i] = i % 256
91+
92+
const base64 = btoa(String.fromCharCode(...data))
93+
const result = base64_to_array_buffer(base64)
94+
const result_array = new Uint8Array(result)
95+
96+
expect(result.byteLength).toBe(size)
97+
expect(result_array[0]).toBe(0)
98+
expect(result_array[255]).toBe(255)
99+
expect(result_array[size - 1]).toBe((size - 1) % 256)
100+
},
101+
)
102+
103+
test(`ASE trajectory file regression test - simulates VS Code extension flow`, () => {
104+
// ASE signature + tag
105+
const ase_data = new Uint8Array([
106+
0x2d,
107+
0x20,
108+
0x6f,
109+
0x66,
110+
0x20,
111+
0x55,
112+
0x6c,
113+
0x6d, // "- of Ulm"
114+
0x41,
115+
0x53,
116+
0x45,
117+
0x2d,
118+
0x54,
119+
0x72,
120+
0x61,
121+
0x6a,
122+
0x65,
123+
0x63,
124+
0x74,
125+
0x6f,
126+
0x72,
127+
0x79,
128+
0x00,
129+
0x00, // "ASE-Trajectory"
130+
...new Array(176).fill(0), // Mock trajectory data
131+
])
132+
133+
const base64_content = btoa(String.fromCharCode(...ase_data))
134+
const result = base64_to_array_buffer(base64_content)
135+
const result_array = new Uint8Array(result)
136+
137+
expect(result.byteLength).toBe(ase_data.length)
138+
expect(new TextDecoder().decode(result_array.slice(0, 8))).toBe(`- of Ulm`)
139+
expect(
140+
new TextDecoder().decode(result_array.slice(8, 24)).replace(/\0/g, ``),
141+
).toBe(`ASE-Trajectory`)
142+
143+
console.log(
144+
`✅ ASE trajectory file regression test passed - binary conversion works correctly`,
145+
)
146+
})
147+
})

extensions/vscode/webview/src/main.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const parse_file_content = async (
6262
filename: string,
6363
is_compressed: boolean = false,
6464
): Promise<ParseResult> => {
65-
// Handle compressed files by decompressing first
65+
// Handle compressed/binary files by converting from base64 first
6666
if (is_compressed) {
6767
const buffer = base64_to_array_buffer(content)
6868

@@ -72,6 +72,12 @@ const parse_file_content = async (
7272
return { type: `trajectory`, filename, data }
7373
}
7474

75+
// For ASE .traj files, pass buffer directly to trajectory parser
76+
if (/\.traj$/i.test(filename)) {
77+
const data = await parse_trajectory_data(buffer, filename)
78+
return { type: `trajectory`, filename, data }
79+
}
80+
7581
// For .gz files, decompress first
7682
if (filename.endsWith(`.gz`)) {
7783
const { decompress_data } = await import(`$lib/io/decompress`)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"homepage": "https://janosh.github.io/matterviz",
66
"repository": "https://github.com/janosh/matterviz",
77
"license": "MIT",
8-
"version": "0.1.1",
8+
"version": "0.1.2",
99
"type": "module",
1010
"svelte": "./dist/index.js",
1111
"bugs": "https://github.com/janosh/matterviz/issues",

readme.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
<h1 align="center">
2-
<sub><img src="static/favicon.svg" alt="Logo" height="40"></sub> MatterViz
2+
<sub><img src="static/favicon.svg" alt="Logo" width="40px"></sub> MatterViz
33
</h1>
44

55
<h4 align="center">
66

77
[![Tests](https://github.com/janosh/matterviz/actions/workflows/test.yml/badge.svg)](https://github.com/janosh/matterviz/actions/workflows/test.yml)
88
[![GH Pages](https://github.com/janosh/matterviz/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/janosh/matterviz/actions/workflows/gh-pages.yml)
99
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/janosh/matterviz/main.svg?badge_token=nUqJfPCFS4uyMwcFSDIfdQ)](https://results.pre-commit.ci/latest/github/janosh/matterviz/main?badge_token=nUqJfPCFS4uyMwcFSDIfdQ)
10-
[![VSCode Extension](https://img.shields.io/badge/Install%20VSCode-Extension-blue)](https://marketplace.visualstudio.com/items?itemName=janosh.matterviz)
11-
[![Docs](https://img.shields.io/badge/View-interactive%20docs-blue)](https://matterviz.janosh.dev)
12-
[![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-darkblue?logo=stackblitz)](https://stackblitz.com/github/janosh/matterviz)
10+
[![VSCode Extension](https://img.shields.io/badge/Install%20VSCode-Extension-blue?logo=typescript&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=janosh.matterviz)
11+
[![Docs](https://img.shields.io/badge/Read-the%20docs-blue?logo=googledocs&logoColor=white)](https://matterviz.janosh.dev)
12+
[![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-darkblue?logo=stackblitz&logoColor=white)](https://stackblitz.com/github/janosh/matterviz)
1313

1414
</h4>
1515

0 commit comments

Comments
 (0)