matterviz 0.1.0
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.
- package/dist/BohrAtom.svelte +105 -0
- package/dist/BohrAtom.svelte.d.ts +21 -0
- package/dist/ControlPanel.svelte +158 -0
- package/dist/ControlPanel.svelte.d.ts +18 -0
- package/dist/Icon.svelte +23 -0
- package/dist/Icon.svelte.d.ts +8 -0
- package/dist/InfoCard.svelte +79 -0
- package/dist/InfoCard.svelte.d.ts +23 -0
- package/dist/Nucleus.svelte +64 -0
- package/dist/Nucleus.svelte.d.ts +16 -0
- package/dist/Spinner.svelte +44 -0
- package/dist/Spinner.svelte.d.ts +7 -0
- package/dist/api.d.ts +6 -0
- package/dist/api.js +30 -0
- package/dist/colors/alloy-colors.json +111 -0
- package/dist/colors/dark-mode-colors.json +111 -0
- package/dist/colors/index.d.ts +26 -0
- package/dist/colors/index.js +72 -0
- package/dist/colors/jmol-colors.json +111 -0
- package/dist/colors/muted-colors.json +111 -0
- package/dist/colors/pastel-colors.json +111 -0
- package/dist/colors/vesta-colors.json +111 -0
- package/dist/composition/BarChart.svelte +260 -0
- package/dist/composition/BarChart.svelte.d.ts +33 -0
- package/dist/composition/BubbleChart.svelte +166 -0
- package/dist/composition/BubbleChart.svelte.d.ts +30 -0
- package/dist/composition/Composition.svelte +73 -0
- package/dist/composition/Composition.svelte.d.ts +27 -0
- package/dist/composition/PieChart.svelte +236 -0
- package/dist/composition/PieChart.svelte.d.ts +36 -0
- package/dist/composition/index.d.ts +5 -0
- package/dist/composition/index.js +5 -0
- package/dist/composition/parse.d.ts +14 -0
- package/dist/composition/parse.js +307 -0
- package/dist/element/ElementHeading.svelte +21 -0
- package/dist/element/ElementHeading.svelte.d.ts +8 -0
- package/dist/element/ElementPhoto.svelte +56 -0
- package/dist/element/ElementPhoto.svelte.d.ts +9 -0
- package/dist/element/ElementStats.svelte +73 -0
- package/dist/element/ElementStats.svelte.d.ts +8 -0
- package/dist/element/ElementTile.svelte +449 -0
- package/dist/element/ElementTile.svelte.d.ts +25 -0
- package/dist/element/data.d.ts +4958 -0
- package/dist/element/data.js +5628 -0
- package/dist/element/index.d.ts +4 -0
- package/dist/element/index.js +4 -0
- package/dist/icons.d.ts +435 -0
- package/dist/icons.js +435 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +43 -0
- package/dist/io/decompress.d.ts +16 -0
- package/dist/io/decompress.js +78 -0
- package/dist/io/export.d.ts +9 -0
- package/dist/io/export.js +205 -0
- package/dist/io/parse.d.ts +53 -0
- package/dist/io/parse.js +747 -0
- package/dist/labels.d.ts +31 -0
- package/dist/labels.js +209 -0
- package/dist/material/MaterialCard.svelte +135 -0
- package/dist/material/MaterialCard.svelte.d.ts +10 -0
- package/dist/material/SymmetryCard.svelte +23 -0
- package/dist/material/SymmetryCard.svelte.d.ts +9 -0
- package/dist/material/index.d.ts +2 -0
- package/dist/material/index.js +2 -0
- package/dist/math.d.ts +24 -0
- package/dist/math.js +216 -0
- package/dist/periodic-table/PeriodicTable.svelte +284 -0
- package/dist/periodic-table/PeriodicTable.svelte.d.ts +50 -0
- package/dist/periodic-table/PropertySelect.svelte +20 -0
- package/dist/periodic-table/PropertySelect.svelte.d.ts +13 -0
- package/dist/periodic-table/TableInset.svelte +18 -0
- package/dist/periodic-table/TableInset.svelte.d.ts +9 -0
- package/dist/periodic-table/index.d.ts +9 -0
- package/dist/periodic-table/index.js +3 -0
- package/dist/plot/ColorBar.svelte +414 -0
- package/dist/plot/ColorBar.svelte.d.ts +22 -0
- package/dist/plot/ColorScaleSelect.svelte +31 -0
- package/dist/plot/ColorScaleSelect.svelte.d.ts +15 -0
- package/dist/plot/ElementScatter.svelte +38 -0
- package/dist/plot/ElementScatter.svelte.d.ts +14 -0
- package/dist/plot/Line.svelte +42 -0
- package/dist/plot/Line.svelte.d.ts +15 -0
- package/dist/plot/PlotLegend.svelte +206 -0
- package/dist/plot/PlotLegend.svelte.d.ts +18 -0
- package/dist/plot/ScatterPlot.svelte +1753 -0
- package/dist/plot/ScatterPlot.svelte.d.ts +114 -0
- package/dist/plot/ScatterPlotControls.svelte +505 -0
- package/dist/plot/ScatterPlotControls.svelte.d.ts +33 -0
- package/dist/plot/ScatterPoint.svelte +72 -0
- package/dist/plot/ScatterPoint.svelte.d.ts +17 -0
- package/dist/plot/index.d.ts +168 -0
- package/dist/plot/index.js +46 -0
- package/dist/state.svelte.d.ts +12 -0
- package/dist/state.svelte.js +11 -0
- package/dist/structure/Bond.svelte +68 -0
- package/dist/structure/Bond.svelte.d.ts +13 -0
- package/dist/structure/Lattice.svelte +115 -0
- package/dist/structure/Lattice.svelte.d.ts +15 -0
- package/dist/structure/Structure.svelte +298 -0
- package/dist/structure/Structure.svelte.d.ts +28 -0
- package/dist/structure/StructureCard.svelte +26 -0
- package/dist/structure/StructureCard.svelte.d.ts +9 -0
- package/dist/structure/StructureControls.svelte +383 -0
- package/dist/structure/StructureControls.svelte.d.ts +23 -0
- package/dist/structure/StructureLegend.svelte +130 -0
- package/dist/structure/StructureLegend.svelte.d.ts +17 -0
- package/dist/structure/StructureScene.svelte +331 -0
- package/dist/structure/StructureScene.svelte.d.ts +47 -0
- package/dist/structure/bonding.d.ts +16 -0
- package/dist/structure/bonding.js +150 -0
- package/dist/structure/index.d.ts +98 -0
- package/dist/structure/index.js +114 -0
- package/dist/structure/pbc.d.ts +6 -0
- package/dist/structure/pbc.js +72 -0
- package/dist/trajectory/Sidebar.svelte +412 -0
- package/dist/trajectory/Sidebar.svelte.d.ts +14 -0
- package/dist/trajectory/Trajectory.svelte +1084 -0
- package/dist/trajectory/Trajectory.svelte.d.ts +49 -0
- package/dist/trajectory/TrajectoryError.svelte +120 -0
- package/dist/trajectory/TrajectoryError.svelte.d.ts +12 -0
- package/dist/trajectory/extract.d.ts +5 -0
- package/dist/trajectory/extract.js +157 -0
- package/dist/trajectory/index.d.ts +16 -0
- package/dist/trajectory/index.js +49 -0
- package/dist/trajectory/parse.d.ts +13 -0
- package/dist/trajectory/parse.js +1093 -0
- package/dist/trajectory/plotting.d.ts +12 -0
- package/dist/trajectory/plotting.js +148 -0
- package/license +21 -0
- package/package.json +131 -0
- package/readme.md +95 -0
|
@@ -0,0 +1,1084 @@
|
|
|
1
|
+
<script lang="ts">import { Icon, Spinner, Structure } from '..';
|
|
2
|
+
import { decompress_file } from '../io/decompress';
|
|
3
|
+
import { format_num, trajectory_labels } from '../labels';
|
|
4
|
+
import { ScatterPlot } from '../plot';
|
|
5
|
+
import { scaleLinear } from 'd3-scale';
|
|
6
|
+
import { untrack } from 'svelte';
|
|
7
|
+
import { titles_as_tooltips } from 'svelte-zoo';
|
|
8
|
+
import { full_data_extractor } from './extract';
|
|
9
|
+
import { Sidebar, TrajectoryError } from './index';
|
|
10
|
+
import { data_url_to_array_buffer, get_unsupported_format_message, load_trajectory_from_url, parse_trajectory_data, } from './parse';
|
|
11
|
+
import { generate_plot_series, should_hide_plot } from './plotting';
|
|
12
|
+
let { trajectory = $bindable(undefined), trajectory_url, current_step_idx = $bindable(0), data_extractor = full_data_extractor, allow_file_drop = true, on_file_drop = handle_trajectory_file_drop, layout = `horizontal`, structure_props = {}, plot_props = {}, spinner_props = {}, trajectory_controls, error_snippet, show_controls = true, show_fullscreen_button = true, display_mode = $bindable(`both`), property_labels = trajectory_labels, units = {
|
|
13
|
+
energy: `eV`,
|
|
14
|
+
energy_per_atom: `eV/atom`,
|
|
15
|
+
force_max: `eV/Å`,
|
|
16
|
+
force_norm: `eV/Å`,
|
|
17
|
+
stress_max: `GPa`,
|
|
18
|
+
volume: `ų`,
|
|
19
|
+
density: `g/cm³`,
|
|
20
|
+
temperature: `K`,
|
|
21
|
+
pressure: `GPa`,
|
|
22
|
+
length: `Å`,
|
|
23
|
+
}, step_labels = 5, } = $props();
|
|
24
|
+
let dragover = $state(false);
|
|
25
|
+
let loading = $state(false);
|
|
26
|
+
let error_message = $state(null);
|
|
27
|
+
let is_playing = $state(false);
|
|
28
|
+
let frame_rate_fps = $state(5);
|
|
29
|
+
let play_interval = $state(undefined);
|
|
30
|
+
let current_filename = $state(null);
|
|
31
|
+
let current_file_path = $state(null);
|
|
32
|
+
let file_size = $state(null);
|
|
33
|
+
let file_object = $state(null);
|
|
34
|
+
let wrapper = $state(undefined);
|
|
35
|
+
let sidebar_open = $state(false);
|
|
36
|
+
// Current frame structure for display
|
|
37
|
+
let current_structure = $derived(trajectory && current_step_idx < trajectory.frames.length
|
|
38
|
+
? trajectory.frames[current_step_idx]?.structure
|
|
39
|
+
: undefined);
|
|
40
|
+
// Calculate step label positions using D3's pretty ticks for even distribution
|
|
41
|
+
let step_label_positions = $derived.by(() => {
|
|
42
|
+
if (!trajectory || !step_labels)
|
|
43
|
+
return [];
|
|
44
|
+
const total_frames = trajectory.frames.length;
|
|
45
|
+
if (total_frames <= 1)
|
|
46
|
+
return [];
|
|
47
|
+
if (Array.isArray(step_labels)) {
|
|
48
|
+
// Exact positions provided as array
|
|
49
|
+
return step_labels.filter((idx) => idx >= 0 && idx < total_frames);
|
|
50
|
+
}
|
|
51
|
+
if (typeof step_labels === `number`) {
|
|
52
|
+
if (step_labels > 0) {
|
|
53
|
+
// Use D3's pretty ticks for even distribution
|
|
54
|
+
const scale = scaleLinear().domain([0, total_frames - 1]);
|
|
55
|
+
const ticks = scale.nice().ticks(Math.min(step_labels, total_frames));
|
|
56
|
+
// Round and filter to valid frame indices
|
|
57
|
+
return ticks
|
|
58
|
+
.map((t) => Math.round(t))
|
|
59
|
+
.filter((t) => t >= 0 && t < total_frames)
|
|
60
|
+
.filter((t, idx, arr) => arr.indexOf(t) === idx); // Remove duplicates
|
|
61
|
+
}
|
|
62
|
+
else if (step_labels < 0) {
|
|
63
|
+
// Negative number: spacing between ticks
|
|
64
|
+
const spacing = Math.abs(step_labels);
|
|
65
|
+
const positions = [];
|
|
66
|
+
for (let idx = 0; idx < total_frames; idx += spacing) {
|
|
67
|
+
positions.push(idx);
|
|
68
|
+
}
|
|
69
|
+
// Always include the last frame if it's not already included
|
|
70
|
+
if (positions[positions.length - 1] !== total_frames - 1) {
|
|
71
|
+
positions.push(total_frames - 1);
|
|
72
|
+
}
|
|
73
|
+
return positions;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
77
|
+
});
|
|
78
|
+
// Generate plot data using extracted plotting utilities
|
|
79
|
+
let plot_series = $derived.by(() => {
|
|
80
|
+
if (!trajectory)
|
|
81
|
+
return [];
|
|
82
|
+
// Filter out undefined values from units
|
|
83
|
+
const filtered_units = {};
|
|
84
|
+
for (const [key, value] of Object.entries(units)) {
|
|
85
|
+
if (value !== undefined) {
|
|
86
|
+
filtered_units[key] = value;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return generate_plot_series(trajectory, data_extractor, {
|
|
90
|
+
property_labels,
|
|
91
|
+
units: filtered_units,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
// Check if all plotted values are constant (no variation) using extracted utility
|
|
95
|
+
let show_plot = $derived(display_mode !== `structure` && !should_hide_plot(trajectory, plot_series));
|
|
96
|
+
// Determine what to show based on display mode
|
|
97
|
+
let show_structure = $derived(display_mode !== `plot`);
|
|
98
|
+
let actual_show_plot = $derived(display_mode !== `structure` && show_plot);
|
|
99
|
+
// Generate intelligent axis labels based on first series on each axis
|
|
100
|
+
let y_axis_labels = $derived.by(() => {
|
|
101
|
+
if (plot_series.length === 0)
|
|
102
|
+
return { y1: `Value`, y2: `Value` };
|
|
103
|
+
const y1_series = plot_series.filter((s) => (s.y_axis ?? `y1`) === `y1`);
|
|
104
|
+
const y2_series = plot_series.filter((s) => s.y_axis === `y2`);
|
|
105
|
+
const get_axis_label = (series) => {
|
|
106
|
+
if (series.length === 0)
|
|
107
|
+
return `Value`;
|
|
108
|
+
// Use the first series label as the axis label
|
|
109
|
+
const first_series = series[0];
|
|
110
|
+
return first_series?.label || `Value`;
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
y1: get_axis_label(y1_series),
|
|
114
|
+
y2: get_axis_label(y2_series),
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
// Check if there are any Y2 series to determine padding
|
|
118
|
+
let has_y2_series = $derived(plot_series.some((s) => s.y_axis === `y2` && s.visible));
|
|
119
|
+
// Handle file drop events
|
|
120
|
+
async function handle_file_drop(event) {
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
dragover = false;
|
|
123
|
+
if (!allow_file_drop)
|
|
124
|
+
return;
|
|
125
|
+
// Check for our custom internal file format first
|
|
126
|
+
const internal_data = event.dataTransfer?.getData(`application/x-matterviz-file`);
|
|
127
|
+
if (internal_data) {
|
|
128
|
+
try {
|
|
129
|
+
const file_info = JSON.parse(internal_data);
|
|
130
|
+
// Check if this is a binary file
|
|
131
|
+
if (file_info.is_binary) {
|
|
132
|
+
const array_buffer = data_url_to_array_buffer(file_info.content);
|
|
133
|
+
await handle_trajectory_binary_drop(array_buffer, file_info.name);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
await on_file_drop(file_info.content, file_info.name);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
console.warn(`Failed to parse internal file data:`, error);
|
|
142
|
+
// Fall through to other methods
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Check for plain text data (fallback)
|
|
146
|
+
const text_data = event.dataTransfer?.getData(`text/plain`);
|
|
147
|
+
if (text_data) {
|
|
148
|
+
file_size = null; // Size not available for text drops
|
|
149
|
+
await on_file_drop(text_data, `trajectory.json`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Handle actual file drops from file system
|
|
153
|
+
const file = event.dataTransfer?.files[0];
|
|
154
|
+
if (!file)
|
|
155
|
+
return;
|
|
156
|
+
loading = true;
|
|
157
|
+
file_size = file.size; // Capture file size
|
|
158
|
+
current_file_path = file.webkitRelativePath || file.name; // Capture full path if available
|
|
159
|
+
file_object = file;
|
|
160
|
+
try {
|
|
161
|
+
// Check if this is an HDF5 file (handle as binary)
|
|
162
|
+
if (file.name.toLowerCase().endsWith(`.h5`) ||
|
|
163
|
+
file.name.toLowerCase().endsWith(`.hdf5`)) {
|
|
164
|
+
const buffer = await file.arrayBuffer();
|
|
165
|
+
await handle_trajectory_binary_drop(buffer, file.name);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Check for known unsupported binary formats before trying to read
|
|
169
|
+
const unsupported_message = get_unsupported_format_message(file.name, ``);
|
|
170
|
+
if (unsupported_message) {
|
|
171
|
+
error_message = unsupported_message;
|
|
172
|
+
current_filename = null;
|
|
173
|
+
file_size = null;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const { content, filename } = await decompress_file(file);
|
|
177
|
+
if (content)
|
|
178
|
+
await on_file_drop(content, filename);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
error_message = `Failed to read file: ${error}`;
|
|
182
|
+
current_filename = null;
|
|
183
|
+
file_size = null;
|
|
184
|
+
console.error(`File reading error:`, error);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
loading = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Step navigation functions
|
|
191
|
+
function next_step() {
|
|
192
|
+
if (trajectory && current_step_idx < trajectory.frames.length - 1) {
|
|
193
|
+
current_step_idx++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function prev_step() {
|
|
197
|
+
if (current_step_idx > 0) {
|
|
198
|
+
current_step_idx--;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function go_to_step(idx) {
|
|
202
|
+
if (trajectory && idx >= 0 && idx < trajectory.frames.length) {
|
|
203
|
+
current_step_idx = idx;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Handle plot point clicks to jump to that step
|
|
207
|
+
function handle_plot_change(data) {
|
|
208
|
+
if (data?.x !== undefined && typeof data.x === `number`) {
|
|
209
|
+
const step_idx = Math.round(data.x);
|
|
210
|
+
go_to_step(step_idx);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Play/pause functionality
|
|
214
|
+
function toggle_play() {
|
|
215
|
+
if (is_playing) {
|
|
216
|
+
pause_playback();
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
start_playback();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function start_playback() {
|
|
223
|
+
if (!trajectory || trajectory.frames.length <= 1)
|
|
224
|
+
return;
|
|
225
|
+
is_playing = true;
|
|
226
|
+
}
|
|
227
|
+
function pause_playback() {
|
|
228
|
+
is_playing = false;
|
|
229
|
+
}
|
|
230
|
+
// Effect to manage playback interval
|
|
231
|
+
$effect(() => {
|
|
232
|
+
// Only watch is_playing and frame_rate_ms, not play_interval itself
|
|
233
|
+
const playing = is_playing;
|
|
234
|
+
const rate_ms = 1000 / frame_rate_fps;
|
|
235
|
+
if (playing) {
|
|
236
|
+
// Clear existing interval if it exists - use untrack to avoid circular dependency
|
|
237
|
+
const current_interval = untrack(() => play_interval);
|
|
238
|
+
if (current_interval !== undefined)
|
|
239
|
+
clearInterval(current_interval);
|
|
240
|
+
// Create new interval with current frame rate
|
|
241
|
+
play_interval = setInterval(() => {
|
|
242
|
+
if (current_step_idx >= trajectory.frames.length - 1)
|
|
243
|
+
go_to_step(0); // Loop back to 1st step
|
|
244
|
+
else
|
|
245
|
+
next_step();
|
|
246
|
+
}, rate_ms);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
// Clear interval when not playing - use untrack to avoid circular dependency
|
|
250
|
+
const current_interval = untrack(() => play_interval);
|
|
251
|
+
if (current_interval !== undefined) {
|
|
252
|
+
clearInterval(current_interval);
|
|
253
|
+
play_interval = undefined;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
// Cleanup interval on component destroy
|
|
258
|
+
$effect(() => {
|
|
259
|
+
return () => {
|
|
260
|
+
if (play_interval !== undefined)
|
|
261
|
+
clearInterval(play_interval);
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
// Load trajectory from URL when trajectory_url is provided
|
|
265
|
+
$effect(() => {
|
|
266
|
+
if (trajectory_url && !trajectory) {
|
|
267
|
+
loading = true;
|
|
268
|
+
error_message = null;
|
|
269
|
+
load_trajectory_from_url(trajectory_url)
|
|
270
|
+
.then((loaded_trajectory) => {
|
|
271
|
+
trajectory = loaded_trajectory;
|
|
272
|
+
current_step_idx = 0;
|
|
273
|
+
// Extract filename from URL
|
|
274
|
+
current_filename = trajectory_url.split(`/`).pop() || trajectory_url;
|
|
275
|
+
file_size = null; // Size not available for URL loads
|
|
276
|
+
loading = false;
|
|
277
|
+
})
|
|
278
|
+
.catch((err) => {
|
|
279
|
+
console.error(`Failed to load trajectory from URL:`, err);
|
|
280
|
+
error_message = `Failed to load trajectory: ${err.message}`;
|
|
281
|
+
current_filename = null;
|
|
282
|
+
file_size = null;
|
|
283
|
+
loading = false;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
async function handle_trajectory_file_drop(content, filename) {
|
|
288
|
+
loading = true;
|
|
289
|
+
error_message = null;
|
|
290
|
+
try {
|
|
291
|
+
// Check for unsupported formats first
|
|
292
|
+
const unsupported_message = get_unsupported_format_message(filename, content);
|
|
293
|
+
if (unsupported_message) {
|
|
294
|
+
error_message = unsupported_message;
|
|
295
|
+
current_filename = null;
|
|
296
|
+
file_size = null;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// Use the new parser that can handle multiple formats including XDATCAR
|
|
300
|
+
trajectory = await parse_trajectory_data(content, filename);
|
|
301
|
+
current_step_idx = 0;
|
|
302
|
+
current_filename = filename;
|
|
303
|
+
// Note: file_size remains as set by the caller (could be null for text drops)
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
// Check if this might be an unsupported format even if not detected initially
|
|
307
|
+
const unsupported_message = get_unsupported_format_message(filename, content);
|
|
308
|
+
if (unsupported_message) {
|
|
309
|
+
error_message = unsupported_message;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
error_message = `Failed to parse trajectory file: ${err}`;
|
|
313
|
+
}
|
|
314
|
+
current_filename = null;
|
|
315
|
+
file_size = null;
|
|
316
|
+
console.error(`Trajectory parsing error:`, err);
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
loading = false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function handle_trajectory_binary_drop(buffer, filename) {
|
|
323
|
+
loading = true;
|
|
324
|
+
error_message = null;
|
|
325
|
+
try {
|
|
326
|
+
// Parse binary data (e.g., HDF5 files)
|
|
327
|
+
trajectory = await parse_trajectory_data(buffer, filename);
|
|
328
|
+
current_step_idx = 0;
|
|
329
|
+
current_filename = filename;
|
|
330
|
+
// Note: file_size should already be set by the caller
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
error_message = `Failed to parse binary trajectory file: ${err}`;
|
|
334
|
+
current_filename = null;
|
|
335
|
+
file_size = null;
|
|
336
|
+
console.error(`Binary trajectory parsing error:`, err);
|
|
337
|
+
}
|
|
338
|
+
finally {
|
|
339
|
+
loading = false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Fullscreen functionality
|
|
343
|
+
function toggle_fullscreen() {
|
|
344
|
+
if (!document.fullscreenElement && wrapper) {
|
|
345
|
+
wrapper.requestFullscreen().catch(console.error);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
document.exitFullscreen();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Display mode cycling
|
|
352
|
+
function cycle_display_mode() {
|
|
353
|
+
const modes = [`both`, `structure`, `plot`];
|
|
354
|
+
const current_index = modes.indexOf(display_mode);
|
|
355
|
+
const next_index = (current_index + 1) % modes.length;
|
|
356
|
+
display_mode = modes[next_index];
|
|
357
|
+
}
|
|
358
|
+
// Display mode labels and icons
|
|
359
|
+
const display_mode_config = {
|
|
360
|
+
both: {
|
|
361
|
+
label: `Show Both`,
|
|
362
|
+
icon: `TwoColumns`,
|
|
363
|
+
title: `Show both structure and plot`,
|
|
364
|
+
},
|
|
365
|
+
structure: {
|
|
366
|
+
label: `Structure Only`,
|
|
367
|
+
icon: `Atom`,
|
|
368
|
+
title: `Show structure only`,
|
|
369
|
+
},
|
|
370
|
+
plot: { label: `Plot Only`, icon: `ScatterPlot`, title: `Show plot only` },
|
|
371
|
+
};
|
|
372
|
+
// Handle click outside sidebar to close it
|
|
373
|
+
function handle_click_outside(event) {
|
|
374
|
+
if (!sidebar_open)
|
|
375
|
+
return;
|
|
376
|
+
const target = event.target;
|
|
377
|
+
const sidebar = target.closest(`.info-sidebar`);
|
|
378
|
+
const info_button = target.closest(`.info-button`);
|
|
379
|
+
// Don't close if clicking on sidebar or info button
|
|
380
|
+
if (!sidebar && !info_button)
|
|
381
|
+
sidebar_open = false;
|
|
382
|
+
}
|
|
383
|
+
// Handle keyboard shortcuts
|
|
384
|
+
function onkeydown(event) {
|
|
385
|
+
if (!trajectory)
|
|
386
|
+
return;
|
|
387
|
+
// Don't handle shortcuts if user is typing in an input field (but allow if it's our step input and not focused)
|
|
388
|
+
const target = event.target;
|
|
389
|
+
const is_step_input = target.classList.contains(`step-input`);
|
|
390
|
+
const is_input_focused = target.tagName === `INPUT` ||
|
|
391
|
+
target.tagName === `TEXTAREA`;
|
|
392
|
+
// Skip if typing in an input that's not our step input
|
|
393
|
+
if (is_input_focused && !is_step_input)
|
|
394
|
+
return;
|
|
395
|
+
// If typing in step input, only handle certain navigation keys
|
|
396
|
+
if (is_step_input && is_input_focused) {
|
|
397
|
+
// Allow normal typing, but handle special navigation keys
|
|
398
|
+
if ([`Escape`, `Enter`].includes(event.key))
|
|
399
|
+
target.blur(); // Remove focus from input
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const total_frames = trajectory.frames.length;
|
|
403
|
+
const is_cmd_or_ctrl = event.metaKey || event.ctrlKey;
|
|
404
|
+
// Navigation shortcuts
|
|
405
|
+
if (event.key === ` `)
|
|
406
|
+
toggle_play();
|
|
407
|
+
else if (event.key === `ArrowLeft`) {
|
|
408
|
+
if (is_cmd_or_ctrl)
|
|
409
|
+
go_to_step(0);
|
|
410
|
+
else
|
|
411
|
+
prev_step();
|
|
412
|
+
}
|
|
413
|
+
else if (event.key === `ArrowRight`) {
|
|
414
|
+
if (is_cmd_or_ctrl)
|
|
415
|
+
go_to_step(total_frames - 1);
|
|
416
|
+
else
|
|
417
|
+
next_step();
|
|
418
|
+
}
|
|
419
|
+
else if (event.key === `Home`)
|
|
420
|
+
go_to_step(0);
|
|
421
|
+
else if (event.key === `End`)
|
|
422
|
+
go_to_step(total_frames - 1);
|
|
423
|
+
else if (event.key === `j`) {
|
|
424
|
+
go_to_step(Math.max(0, current_step_idx - 10));
|
|
425
|
+
}
|
|
426
|
+
else if (event.key === `l`) {
|
|
427
|
+
go_to_step(Math.min(total_frames - 1, current_step_idx + 10));
|
|
428
|
+
}
|
|
429
|
+
else if (event.key === `PageUp`) {
|
|
430
|
+
go_to_step(Math.max(0, current_step_idx - 25));
|
|
431
|
+
}
|
|
432
|
+
else if (event.key === `PageDown`) {
|
|
433
|
+
go_to_step(Math.min(total_frames - 1, current_step_idx + 25));
|
|
434
|
+
} // Interface shortcuts
|
|
435
|
+
else if (event.key === `f`)
|
|
436
|
+
toggle_fullscreen();
|
|
437
|
+
else if (event.key === `i`)
|
|
438
|
+
sidebar_open = !sidebar_open;
|
|
439
|
+
else if (event.key === `d` && plot_series.length > 0) {
|
|
440
|
+
cycle_display_mode();
|
|
441
|
+
} // Playback speed shortcuts (only when playing)
|
|
442
|
+
else if ((event.key === `=` || event.key === `+`) && is_playing) {
|
|
443
|
+
frame_rate_fps = Math.min(5, frame_rate_fps + 0.2);
|
|
444
|
+
}
|
|
445
|
+
else if (event.key === `-` && is_playing) {
|
|
446
|
+
frame_rate_fps = Math.max(0.2, frame_rate_fps - 0.2);
|
|
447
|
+
} // System shortcuts
|
|
448
|
+
else if (event.key === `Escape`) {
|
|
449
|
+
if (document.fullscreenElement)
|
|
450
|
+
document.exitFullscreen();
|
|
451
|
+
else
|
|
452
|
+
sidebar_open = false;
|
|
453
|
+
} // Number keys 0-9 - jump to percentage of trajectory
|
|
454
|
+
else if (event.key >= `0` && event.key <= `9`) {
|
|
455
|
+
go_to_step(Math.floor((parseInt(event.key, 10) / 10) * (total_frames - 1)));
|
|
456
|
+
}
|
|
457
|
+
else if (event.key === `Escape` && sidebar_open) { // Escape key to close sidebar
|
|
458
|
+
event.stopPropagation();
|
|
459
|
+
sidebar_open = false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
</script>
|
|
463
|
+
|
|
464
|
+
<div
|
|
465
|
+
class="trajectory-viewer"
|
|
466
|
+
class:horizontal={layout === `horizontal`}
|
|
467
|
+
class:vertical={layout === `vertical`}
|
|
468
|
+
class:dragover
|
|
469
|
+
bind:this={wrapper}
|
|
470
|
+
role="button"
|
|
471
|
+
tabindex="0"
|
|
472
|
+
aria-label="Drop trajectory file here to load"
|
|
473
|
+
ondrop={handle_file_drop}
|
|
474
|
+
ondragover={(event) => {
|
|
475
|
+
event.preventDefault()
|
|
476
|
+
if (!allow_file_drop) return
|
|
477
|
+
dragover = true
|
|
478
|
+
}}
|
|
479
|
+
ondragleave={(event) => {
|
|
480
|
+
event.preventDefault()
|
|
481
|
+
dragover = false
|
|
482
|
+
}}
|
|
483
|
+
onclick={handle_click_outside}
|
|
484
|
+
{onkeydown}
|
|
485
|
+
>
|
|
486
|
+
{#if loading}
|
|
487
|
+
<Spinner text="Loading trajectory..." {...spinner_props} />
|
|
488
|
+
{:else if error_message}
|
|
489
|
+
<TrajectoryError
|
|
490
|
+
{error_message}
|
|
491
|
+
on_dismiss={() => (error_message = null)}
|
|
492
|
+
{error_snippet}
|
|
493
|
+
/>
|
|
494
|
+
{:else if trajectory}
|
|
495
|
+
<!-- Trajectory Controls -->
|
|
496
|
+
{#if show_controls}
|
|
497
|
+
<div class="trajectory-controls">
|
|
498
|
+
{#if trajectory_controls}
|
|
499
|
+
{@render trajectory_controls({
|
|
500
|
+
trajectory,
|
|
501
|
+
current_step_idx,
|
|
502
|
+
total_frames: trajectory.frames.length,
|
|
503
|
+
on_step_change: go_to_step,
|
|
504
|
+
})}
|
|
505
|
+
{:else}
|
|
506
|
+
{@const input_width = Math.max(25, String(current_step_idx).length * 8 + 6)}
|
|
507
|
+
{#if current_filename}
|
|
508
|
+
<div class="filename-section">
|
|
509
|
+
<button
|
|
510
|
+
use:titles_as_tooltips
|
|
511
|
+
title="Click to copy filename"
|
|
512
|
+
onclick={() => {
|
|
513
|
+
if (current_filename) navigator.clipboard.writeText(current_filename)
|
|
514
|
+
}}
|
|
515
|
+
>
|
|
516
|
+
{current_filename}
|
|
517
|
+
</button>
|
|
518
|
+
</div>
|
|
519
|
+
{/if}
|
|
520
|
+
|
|
521
|
+
<!-- Navigation controls -->
|
|
522
|
+
<div class="nav-section">
|
|
523
|
+
<button
|
|
524
|
+
onclick={prev_step}
|
|
525
|
+
disabled={current_step_idx === 0 || is_playing}
|
|
526
|
+
title="Previous step"
|
|
527
|
+
class="nav-button"
|
|
528
|
+
>
|
|
529
|
+
⏮
|
|
530
|
+
</button>
|
|
531
|
+
|
|
532
|
+
<button
|
|
533
|
+
onclick={toggle_play}
|
|
534
|
+
disabled={trajectory.frames.length <= 1}
|
|
535
|
+
title={is_playing ? `Pause playback` : `Play trajectory`}
|
|
536
|
+
class="play-button nav-button"
|
|
537
|
+
class:playing={is_playing}
|
|
538
|
+
>
|
|
539
|
+
{is_playing ? `⏸` : `▶`}
|
|
540
|
+
</button>
|
|
541
|
+
|
|
542
|
+
<button
|
|
543
|
+
onclick={next_step}
|
|
544
|
+
disabled={current_step_idx === trajectory.frames.length - 1 || is_playing}
|
|
545
|
+
title="Next step"
|
|
546
|
+
class="nav-button"
|
|
547
|
+
>
|
|
548
|
+
⏭
|
|
549
|
+
</button>
|
|
550
|
+
</div>
|
|
551
|
+
|
|
552
|
+
<!-- Frame slider and counter -->
|
|
553
|
+
<div class="step-section">
|
|
554
|
+
<input
|
|
555
|
+
type="number"
|
|
556
|
+
min="0"
|
|
557
|
+
max={trajectory.frames.length - 1}
|
|
558
|
+
bind:value={current_step_idx}
|
|
559
|
+
oninput={(event) => {
|
|
560
|
+
const target = event.target as HTMLInputElement
|
|
561
|
+
const width = Math.max(25, Math.min(80, target.value.length * 8 + 6))
|
|
562
|
+
target.style.width = `${width}px`
|
|
563
|
+
}}
|
|
564
|
+
style:width="{input_width}px"
|
|
565
|
+
class="step-input"
|
|
566
|
+
title="Enter step number to jump to"
|
|
567
|
+
/>
|
|
568
|
+
<span>/ {trajectory.frames.length}</span>
|
|
569
|
+
<div class="slider-container">
|
|
570
|
+
<input
|
|
571
|
+
type="range"
|
|
572
|
+
min="0"
|
|
573
|
+
max={trajectory.frames.length - 1}
|
|
574
|
+
bind:value={current_step_idx}
|
|
575
|
+
class="step-slider"
|
|
576
|
+
title="Drag to navigate steps"
|
|
577
|
+
/>
|
|
578
|
+
{#if step_label_positions.length > 0}
|
|
579
|
+
<div class="step-labels">
|
|
580
|
+
{#each step_label_positions as step_idx (step_idx)}
|
|
581
|
+
{@const position_percent = (step_idx / (trajectory.frames.length - 1)) *
|
|
582
|
+
100}
|
|
583
|
+
{@const adjusted_position = 1.5 + (position_percent * (100 - 2)) / 100}
|
|
584
|
+
<div class="step-tick" style:left="{adjusted_position}%"></div>
|
|
585
|
+
<div class="step-label" style:left="{adjusted_position}%">
|
|
586
|
+
{step_idx}
|
|
587
|
+
</div>
|
|
588
|
+
{/each}
|
|
589
|
+
</div>
|
|
590
|
+
{/if}
|
|
591
|
+
</div>
|
|
592
|
+
</div>
|
|
593
|
+
|
|
594
|
+
<!-- Frame rate control - only shown when playing -->
|
|
595
|
+
{#if is_playing}
|
|
596
|
+
<div class="speed-section">
|
|
597
|
+
<label for="step-rate-slider" style="font-weight: 500; white-space: nowrap"
|
|
598
|
+
>Speed:</label>
|
|
599
|
+
<input
|
|
600
|
+
id="step-rate-slider"
|
|
601
|
+
type="range"
|
|
602
|
+
min="0.2"
|
|
603
|
+
max="5"
|
|
604
|
+
step="0.1"
|
|
605
|
+
bind:value={frame_rate_fps}
|
|
606
|
+
class="speed-slider"
|
|
607
|
+
title="Frame rate: {format_num(frame_rate_fps, `.2~s`)} fps"
|
|
608
|
+
/>
|
|
609
|
+
<input
|
|
610
|
+
type="number"
|
|
611
|
+
min="0.2"
|
|
612
|
+
max="5"
|
|
613
|
+
step="0.1"
|
|
614
|
+
bind:value={frame_rate_fps}
|
|
615
|
+
class="speed-input"
|
|
616
|
+
title="Enter precise FPS value"
|
|
617
|
+
/>
|
|
618
|
+
fps
|
|
619
|
+
</div>
|
|
620
|
+
{/if}
|
|
621
|
+
|
|
622
|
+
<!-- Frame info section -->
|
|
623
|
+
<div class="info-section">
|
|
624
|
+
<!-- Info button to open sidebar -->
|
|
625
|
+
{#if trajectory}
|
|
626
|
+
<button
|
|
627
|
+
onclick={() => (sidebar_open = !sidebar_open)}
|
|
628
|
+
title={sidebar_open ? `Close info panel` : `Open info panel`}
|
|
629
|
+
aria-label={sidebar_open ? `Close info panel` : `Open info panel`}
|
|
630
|
+
class="info-button nav-button"
|
|
631
|
+
class:active={sidebar_open}
|
|
632
|
+
>
|
|
633
|
+
<Icon icon="Info" style="width: 22px; height: 22px" />
|
|
634
|
+
</button>
|
|
635
|
+
{/if}
|
|
636
|
+
<!-- Display mode button - after info button -->
|
|
637
|
+
{#if plot_series.length > 0}
|
|
638
|
+
<button
|
|
639
|
+
onclick={cycle_display_mode}
|
|
640
|
+
title={display_mode_config[display_mode].title}
|
|
641
|
+
class="display-mode nav-button"
|
|
642
|
+
>
|
|
643
|
+
<Icon icon={display_mode_config[display_mode].icon} />
|
|
644
|
+
</button>
|
|
645
|
+
{/if}
|
|
646
|
+
<!-- Fullscreen button - rightmost position -->
|
|
647
|
+
{#if show_fullscreen_button}
|
|
648
|
+
<button
|
|
649
|
+
onclick={toggle_fullscreen}
|
|
650
|
+
title="Toggle fullscreen"
|
|
651
|
+
aria-label="Toggle fullscreen"
|
|
652
|
+
class="fullscreen-button nav-button"
|
|
653
|
+
>
|
|
654
|
+
<Icon icon="Fullscreen" />
|
|
655
|
+
</button>
|
|
656
|
+
{/if}
|
|
657
|
+
</div>
|
|
658
|
+
{/if}
|
|
659
|
+
</div>
|
|
660
|
+
{/if}
|
|
661
|
+
|
|
662
|
+
<div
|
|
663
|
+
class="content-area"
|
|
664
|
+
class:hide-plot={!actual_show_plot}
|
|
665
|
+
class:hide-structure={!show_structure}
|
|
666
|
+
class:show-both={display_mode === `both`}
|
|
667
|
+
class:show-structure-only={display_mode === `structure`}
|
|
668
|
+
class:show-plot-only={display_mode === `plot`}
|
|
669
|
+
>
|
|
670
|
+
{#if show_structure}
|
|
671
|
+
<Structure
|
|
672
|
+
structure={current_structure}
|
|
673
|
+
allow_file_drop={false}
|
|
674
|
+
style="height: 100%; border-radius: 0"
|
|
675
|
+
enable_tips={false}
|
|
676
|
+
fullscreen_toggle={false}
|
|
677
|
+
{...{ show_image_atoms: false, ...structure_props }}
|
|
678
|
+
/>
|
|
679
|
+
{/if}
|
|
680
|
+
|
|
681
|
+
{#if actual_show_plot}
|
|
682
|
+
<ScatterPlot
|
|
683
|
+
series={plot_series}
|
|
684
|
+
x_label="Step"
|
|
685
|
+
y_label={y_axis_labels.y1}
|
|
686
|
+
y_label_shift={{ y: 20 }}
|
|
687
|
+
y_format=".2~s"
|
|
688
|
+
y2_format=".2~s"
|
|
689
|
+
y2_label={y_axis_labels.y2}
|
|
690
|
+
y2_label_shift={{ y: 80 }}
|
|
691
|
+
current_x_value={current_step_idx}
|
|
692
|
+
change={handle_plot_change}
|
|
693
|
+
markers="line"
|
|
694
|
+
x_ticks={step_label_positions}
|
|
695
|
+
show_controls
|
|
696
|
+
legend={{
|
|
697
|
+
responsive: true,
|
|
698
|
+
layout: `horizontal`,
|
|
699
|
+
layout_tracks: 3,
|
|
700
|
+
item_gap: 0,
|
|
701
|
+
padding: { t: 5, b: 5, l: 5, r: 5 },
|
|
702
|
+
}}
|
|
703
|
+
padding={{ t: 20, b: 60, l: 100, r: has_y2_series ? 100 : 20 }}
|
|
704
|
+
range_padding={0}
|
|
705
|
+
style="height: 100%"
|
|
706
|
+
{...plot_props}
|
|
707
|
+
>
|
|
708
|
+
{#snippet tooltip({ x, y, metadata })}
|
|
709
|
+
{#if metadata?.series_label}
|
|
710
|
+
Step: {Math.round(x)}<br />
|
|
711
|
+
{@html metadata.series_label}: {typeof y === `number` ? format_num(y) : y}
|
|
712
|
+
{:else}
|
|
713
|
+
Step: {Math.round(x)}<br />
|
|
714
|
+
Value: {typeof y === `number` ? format_num(y) : y}
|
|
715
|
+
{/if}
|
|
716
|
+
{/snippet}
|
|
717
|
+
</ScatterPlot>
|
|
718
|
+
{/if}
|
|
719
|
+
</div>
|
|
720
|
+
{:else}
|
|
721
|
+
<div class="empty-state">
|
|
722
|
+
<div class="drop-zone">
|
|
723
|
+
<h3>Load Trajectory</h3>
|
|
724
|
+
<p>
|
|
725
|
+
Drop a trajectory file here (.xyz, .extxyz, .json, .json.gz, XDATCAR) or provide
|
|
726
|
+
trajectory data via props
|
|
727
|
+
</p>
|
|
728
|
+
<div class="supported-formats">
|
|
729
|
+
<strong>Supported formats:</strong>
|
|
730
|
+
<ul>
|
|
731
|
+
<li>Multi-frame XYZ trajectory files (.xyz, .extxyz)</li>
|
|
732
|
+
<li>Pymatgen trajectory JSON</li>
|
|
733
|
+
<li>Array of structures with metadata</li>
|
|
734
|
+
<li>VASP XDATCAR files</li>
|
|
735
|
+
<li>Compressed files (.gz)</li>
|
|
736
|
+
</ul>
|
|
737
|
+
</div>
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
{/if}
|
|
741
|
+
|
|
742
|
+
<!-- Info Sidebar -->
|
|
743
|
+
{#if trajectory}
|
|
744
|
+
<Sidebar
|
|
745
|
+
{trajectory}
|
|
746
|
+
{current_step_idx}
|
|
747
|
+
{current_filename}
|
|
748
|
+
{current_file_path}
|
|
749
|
+
{file_size}
|
|
750
|
+
{file_object}
|
|
751
|
+
is_open={sidebar_open}
|
|
752
|
+
onclose={() => (sidebar_open = false)}
|
|
753
|
+
/>
|
|
754
|
+
{/if}
|
|
755
|
+
</div>
|
|
756
|
+
|
|
757
|
+
<style>
|
|
758
|
+
.trajectory-viewer {
|
|
759
|
+
display: flex;
|
|
760
|
+
flex-direction: column;
|
|
761
|
+
height: 100%;
|
|
762
|
+
width: 100%;
|
|
763
|
+
position: relative;
|
|
764
|
+
min-height: 500px;
|
|
765
|
+
border-radius: 4px;
|
|
766
|
+
border: 2px dashed transparent;
|
|
767
|
+
transition: border-color 0.2s ease;
|
|
768
|
+
box-sizing: border-box;
|
|
769
|
+
overflow: hidden;
|
|
770
|
+
contain: layout;
|
|
771
|
+
}
|
|
772
|
+
.trajectory-viewer:fullscreen {
|
|
773
|
+
height: 100vh !important;
|
|
774
|
+
width: 100vw !important;
|
|
775
|
+
border-radius: 0;
|
|
776
|
+
border: none;
|
|
777
|
+
}
|
|
778
|
+
/* Content area - grid container for equal sizing */
|
|
779
|
+
.content-area {
|
|
780
|
+
display: grid;
|
|
781
|
+
flex: 1;
|
|
782
|
+
min-height: 0;
|
|
783
|
+
overflow: visible;
|
|
784
|
+
}
|
|
785
|
+
.trajectory-viewer.horizontal .content-area {
|
|
786
|
+
grid-template-columns: 1fr 1fr;
|
|
787
|
+
}
|
|
788
|
+
.trajectory-viewer.vertical .content-area {
|
|
789
|
+
grid-template-rows: 1fr 1fr;
|
|
790
|
+
}
|
|
791
|
+
/* When plot is hidden, structure takes full space */
|
|
792
|
+
.content-area.hide-plot {
|
|
793
|
+
grid-template-columns: 1fr !important;
|
|
794
|
+
}
|
|
795
|
+
/* When structure is hidden, plot takes full space */
|
|
796
|
+
.content-area.hide-structure {
|
|
797
|
+
grid-template-columns: 1fr !important;
|
|
798
|
+
grid-template-rows: 1fr !important;
|
|
799
|
+
}
|
|
800
|
+
/* Display mode specific layouts */
|
|
801
|
+
.content-area.show-structure-only {
|
|
802
|
+
grid-template-columns: 1fr !important;
|
|
803
|
+
grid-template-rows: 1fr !important;
|
|
804
|
+
}
|
|
805
|
+
.content-area.show-plot-only {
|
|
806
|
+
grid-template-columns: 1fr !important;
|
|
807
|
+
grid-template-rows: 1fr !important;
|
|
808
|
+
}
|
|
809
|
+
.trajectory-viewer.dragover {
|
|
810
|
+
border-color: var(--trajectory-dragover-border, #007acc);
|
|
811
|
+
background-color: var(--trajectory-dragover-bg, rgba(0, 122, 204, 0.1));
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.trajectory-controls {
|
|
815
|
+
display: flex;
|
|
816
|
+
align-items: center;
|
|
817
|
+
gap: 1rem;
|
|
818
|
+
padding: 0.5rem;
|
|
819
|
+
background: var(--traj-surface, rgba(45, 55, 72, 0.95));
|
|
820
|
+
backdrop-filter: blur(4px);
|
|
821
|
+
border-bottom: var(--trajectory-border, 1px solid #e1e4e8);
|
|
822
|
+
color: var(--trajectory-text-color, #24292e);
|
|
823
|
+
font-size: 0.8rem;
|
|
824
|
+
}
|
|
825
|
+
.nav-section {
|
|
826
|
+
display: flex;
|
|
827
|
+
align-items: center;
|
|
828
|
+
gap: 0.25rem;
|
|
829
|
+
}
|
|
830
|
+
.nav-button {
|
|
831
|
+
min-width: 28px;
|
|
832
|
+
height: 28px;
|
|
833
|
+
padding: 0.125rem 0.25rem;
|
|
834
|
+
font-size: 0.8rem;
|
|
835
|
+
}
|
|
836
|
+
.step-section {
|
|
837
|
+
display: flex;
|
|
838
|
+
align-items: center;
|
|
839
|
+
gap: 0.5rem;
|
|
840
|
+
flex: 1;
|
|
841
|
+
min-width: 0;
|
|
842
|
+
}
|
|
843
|
+
.step-input {
|
|
844
|
+
border: 1px solid rgba(99, 179, 237, 0.3);
|
|
845
|
+
border-radius: 3px;
|
|
846
|
+
text-align: center;
|
|
847
|
+
margin: 1.5px -5px 0 0;
|
|
848
|
+
}
|
|
849
|
+
.slider-container {
|
|
850
|
+
position: relative;
|
|
851
|
+
flex: 1;
|
|
852
|
+
min-width: 80px;
|
|
853
|
+
}
|
|
854
|
+
.step-slider {
|
|
855
|
+
width: 100%;
|
|
856
|
+
accent-color: var(--traj-accent, #63b3ed);
|
|
857
|
+
}
|
|
858
|
+
.step-labels {
|
|
859
|
+
position: absolute;
|
|
860
|
+
top: 100%;
|
|
861
|
+
left: 0;
|
|
862
|
+
right: 0;
|
|
863
|
+
height: 16px;
|
|
864
|
+
pointer-events: none;
|
|
865
|
+
}
|
|
866
|
+
.step-tick {
|
|
867
|
+
position: absolute;
|
|
868
|
+
transform: translateX(-50%);
|
|
869
|
+
width: 2px;
|
|
870
|
+
height: 4px;
|
|
871
|
+
background: var(--traj-muted, rgba(148, 163, 184, 0.8));
|
|
872
|
+
top: -10px;
|
|
873
|
+
}
|
|
874
|
+
.step-label {
|
|
875
|
+
position: absolute;
|
|
876
|
+
transform: translateX(-50%);
|
|
877
|
+
font-size: 0.65rem;
|
|
878
|
+
color: var(--traj-muted, rgba(148, 163, 184, 0.85));
|
|
879
|
+
white-space: nowrap;
|
|
880
|
+
text-align: center;
|
|
881
|
+
top: -6px;
|
|
882
|
+
}
|
|
883
|
+
.speed-slider {
|
|
884
|
+
width: 90px;
|
|
885
|
+
accent-color: var(--traj-accent, #63b3ed);
|
|
886
|
+
}
|
|
887
|
+
.speed-input {
|
|
888
|
+
width: 45px;
|
|
889
|
+
text-align: center;
|
|
890
|
+
background: var(--traj-bg, rgba(26, 32, 44, 0.8));
|
|
891
|
+
border: var(--traj-border, 1px solid rgba(74, 85, 104, 0.5));
|
|
892
|
+
border-radius: 3px;
|
|
893
|
+
color: var(--traj-text, #e2e8f0);
|
|
894
|
+
font-size: 0.8rem;
|
|
895
|
+
padding: 0.125rem 0.25rem;
|
|
896
|
+
box-sizing: border-box;
|
|
897
|
+
}
|
|
898
|
+
.speed-section {
|
|
899
|
+
display: flex;
|
|
900
|
+
align-items: center;
|
|
901
|
+
gap: 0.25rem;
|
|
902
|
+
color: var(--traj-text, #e2e8f0);
|
|
903
|
+
}
|
|
904
|
+
.filename-section {
|
|
905
|
+
display: flex;
|
|
906
|
+
align-items: center;
|
|
907
|
+
color: var(--traj-text, #e2e8f0);
|
|
908
|
+
}
|
|
909
|
+
.filename-section button {
|
|
910
|
+
white-space: nowrap;
|
|
911
|
+
padding: 0.125rem 0.375rem;
|
|
912
|
+
background: var(--traj-bg, rgba(26, 32, 44, 0.8));
|
|
913
|
+
border-radius: 2px;
|
|
914
|
+
border: var(--traj-border, 1px solid rgba(74, 85, 104, 0.5));
|
|
915
|
+
max-width: 200px;
|
|
916
|
+
overflow: hidden;
|
|
917
|
+
text-overflow: ellipsis;
|
|
918
|
+
cursor: pointer;
|
|
919
|
+
line-height: inherit;
|
|
920
|
+
}
|
|
921
|
+
.display-mode {
|
|
922
|
+
min-width: 28px;
|
|
923
|
+
height: 28px;
|
|
924
|
+
background: var(--trajectory-display-mode-bg, rgba(255, 255, 255, 0.05));
|
|
925
|
+
}
|
|
926
|
+
.display-mode:hover:not(:disabled) {
|
|
927
|
+
background: var(--trajectory-display-mode-hover-bg, #6b7280);
|
|
928
|
+
}
|
|
929
|
+
.fullscreen-button {
|
|
930
|
+
min-width: 28px;
|
|
931
|
+
height: 28px;
|
|
932
|
+
background: var(--trajectory-fullscreen-bg, rgba(255, 255, 255, 0.05));
|
|
933
|
+
}
|
|
934
|
+
.fullscreen-button:hover:not(:disabled) {
|
|
935
|
+
background: var(--trajectory-fullscreen-hover-bg, rgba(255, 255, 255, 0.1));
|
|
936
|
+
}
|
|
937
|
+
.info-button {
|
|
938
|
+
width: 28px;
|
|
939
|
+
height: 28px;
|
|
940
|
+
min-width: 28px;
|
|
941
|
+
border-radius: 50%;
|
|
942
|
+
background: var(--trajectory-info-bg, #4b5563);
|
|
943
|
+
display: flex;
|
|
944
|
+
align-items: center;
|
|
945
|
+
justify-content: center;
|
|
946
|
+
padding: 0;
|
|
947
|
+
}
|
|
948
|
+
.info-button:hover:not(:disabled) {
|
|
949
|
+
background: var(--trajectory-info-hover-bg, #6b7280);
|
|
950
|
+
}
|
|
951
|
+
.info-button.active {
|
|
952
|
+
background: var(--trajectory-info-active-bg, #3b82f6);
|
|
953
|
+
}
|
|
954
|
+
.info-section {
|
|
955
|
+
display: flex;
|
|
956
|
+
align-items: center;
|
|
957
|
+
gap: 0.5rem;
|
|
958
|
+
color: var(--traj-text, #e2e8f0);
|
|
959
|
+
margin-left: auto;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
.play-button {
|
|
963
|
+
background: var(--trajectory-play-button-bg, #6b7280);
|
|
964
|
+
min-width: 36px;
|
|
965
|
+
font-size: 0.9rem;
|
|
966
|
+
}
|
|
967
|
+
.play-button:hover:not(:disabled) {
|
|
968
|
+
background: var(--trajectory-play-button-hover-bg, #7f8793);
|
|
969
|
+
}
|
|
970
|
+
.play-button.playing {
|
|
971
|
+
background: var(--trajectory-pause-button-bg, #6b7280);
|
|
972
|
+
}
|
|
973
|
+
.play-button.playing:hover:not(:disabled) {
|
|
974
|
+
background: var(--trajectory-pause-button-hover-bg, #9ca3af);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
.empty-state {
|
|
978
|
+
display: flex;
|
|
979
|
+
align-items: center;
|
|
980
|
+
justify-content: center;
|
|
981
|
+
height: 100%;
|
|
982
|
+
padding: 2rem;
|
|
983
|
+
color: var(--trajectory-text-color, #24292e);
|
|
984
|
+
}
|
|
985
|
+
.drop-zone {
|
|
986
|
+
text-align: center;
|
|
987
|
+
padding: 3rem;
|
|
988
|
+
border: 2px dashed var(--trajectory-dropzone-border, #ccc);
|
|
989
|
+
border-radius: 8px;
|
|
990
|
+
background: var(--trajectory-dropzone-bg, #f9f9f9);
|
|
991
|
+
color: var(--trajectory-text-color, #24292e);
|
|
992
|
+
max-width: 500px;
|
|
993
|
+
}
|
|
994
|
+
.drop-zone h3 {
|
|
995
|
+
color: var(--trajectory-heading-color, var(--trajectory-text-color, #24292e));
|
|
996
|
+
margin-bottom: 1rem;
|
|
997
|
+
}
|
|
998
|
+
.supported-formats {
|
|
999
|
+
margin-top: 1.5rem;
|
|
1000
|
+
text-align: left;
|
|
1001
|
+
}
|
|
1002
|
+
.supported-formats strong {
|
|
1003
|
+
color: var(--trajectory-text-color, #24292e);
|
|
1004
|
+
}
|
|
1005
|
+
.supported-formats ul {
|
|
1006
|
+
margin: 0.5rem 0;
|
|
1007
|
+
padding-left: 1.5rem;
|
|
1008
|
+
}
|
|
1009
|
+
.supported-formats li {
|
|
1010
|
+
color: var(--trajectory-text-muted, #586069);
|
|
1011
|
+
}
|
|
1012
|
+
button {
|
|
1013
|
+
background: var(--traj-border-bg, #4a5568);
|
|
1014
|
+
color: var(--traj-text, #e2e8f0);
|
|
1015
|
+
border: none;
|
|
1016
|
+
border-radius: 4px;
|
|
1017
|
+
padding: 0.25rem 0.5rem;
|
|
1018
|
+
cursor: pointer;
|
|
1019
|
+
font-size: 0.85rem;
|
|
1020
|
+
min-width: 2rem;
|
|
1021
|
+
transition: background-color 0.2s;
|
|
1022
|
+
}
|
|
1023
|
+
button:hover:not(:disabled) {
|
|
1024
|
+
background: var(--traj-surface-hover, #4a5568);
|
|
1025
|
+
}
|
|
1026
|
+
button:disabled {
|
|
1027
|
+
background: var(--traj-text-muted, #a0aec0);
|
|
1028
|
+
color: var(--traj-border-bg, #4a5568);
|
|
1029
|
+
cursor: not-allowed;
|
|
1030
|
+
}
|
|
1031
|
+
/* Hide number input spinner arrows */
|
|
1032
|
+
.trajectory-controls input[type='number']::-webkit-outer-spin-button,
|
|
1033
|
+
.trajectory-controls input[type='number']::-webkit-inner-spin-button {
|
|
1034
|
+
-webkit-appearance: none;
|
|
1035
|
+
margin: 0;
|
|
1036
|
+
}
|
|
1037
|
+
/* Responsive design */
|
|
1038
|
+
@media (max-width: 768px) {
|
|
1039
|
+
.trajectory-viewer.horizontal {
|
|
1040
|
+
flex-direction: column;
|
|
1041
|
+
}
|
|
1042
|
+
.trajectory-controls {
|
|
1043
|
+
flex-wrap: wrap;
|
|
1044
|
+
gap: 0.375rem;
|
|
1045
|
+
}
|
|
1046
|
+
.step-section {
|
|
1047
|
+
order: 1;
|
|
1048
|
+
width: 100%;
|
|
1049
|
+
min-width: 0;
|
|
1050
|
+
}
|
|
1051
|
+
.speed-section {
|
|
1052
|
+
justify-content: center;
|
|
1053
|
+
}
|
|
1054
|
+
.filename-section {
|
|
1055
|
+
order: -1;
|
|
1056
|
+
width: 100%;
|
|
1057
|
+
justify-content: center;
|
|
1058
|
+
}
|
|
1059
|
+
.info-section {
|
|
1060
|
+
margin-left: 0;
|
|
1061
|
+
justify-content: center;
|
|
1062
|
+
gap: 0.375rem;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
@media (max-width: 480px) {
|
|
1066
|
+
.trajectory-controls {
|
|
1067
|
+
padding: 0.125rem 0.375rem;
|
|
1068
|
+
font-size: 0.75rem;
|
|
1069
|
+
gap: 0.25rem;
|
|
1070
|
+
}
|
|
1071
|
+
.nav-button {
|
|
1072
|
+
min-width: 24px;
|
|
1073
|
+
height: 24px;
|
|
1074
|
+
font-size: 0.7rem;
|
|
1075
|
+
}
|
|
1076
|
+
.info-section {
|
|
1077
|
+
flex-direction: column;
|
|
1078
|
+
gap: 0.125rem;
|
|
1079
|
+
}
|
|
1080
|
+
.speed-section {
|
|
1081
|
+
font-size: 0.65rem;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
</style>
|