Coverage for ase / gui / view.py: 69.26%
527 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 10:20 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 10:20 +0000
1# fmt: off
3from math import cos, sin, sqrt
4from os.path import basename
6import numpy as np
8from ase.calculators.calculator import PropertyNotImplementedError
9from ase.data import atomic_numbers
10from ase.data.colors import jmol_colors
11from ase.geometry import complete_cell
12from ase.gui.colors import ColorWindow
13from ase.gui.i18n import _, ngettext
14from ase.gui.render import Render
15from ase.gui.repeat import Repeat
16from ase.gui.rotate import Rotate
17from ase.gui.utils import get_magmoms
18from ase.utils import rotate
20GREEN = '#74DF00'
21PURPLE = '#AC58FA'
22BLACKISH = '#151515'
25def get_cell_coordinates(cell, shifted=False):
26 """Get start and end points of lines segments used to draw cell."""
27 nn = []
28 for c in range(3):
29 v = cell[c]
30 d = sqrt(np.dot(v, v))
31 if d < 1e-12:
32 n = 0
33 else:
34 n = max(2, int(d / 0.3))
35 nn.append(n)
36 B1 = np.zeros((2, 2, sum(nn), 3))
37 B2 = np.zeros((2, 2, sum(nn), 3))
38 n1 = 0
39 for c, n in enumerate(nn):
40 n2 = n1 + n
41 h = 1.0 / (2 * n - 1)
42 R = np.arange(n) * (2 * h)
44 for i, j in [(0, 0), (0, 1), (1, 0), (1, 1)]:
45 B1[i, j, n1:n2, c] = R
46 B1[i, j, n1:n2, (c + 1) % 3] = i
47 B1[i, j, n1:n2, (c + 2) % 3] = j
48 B2[:, :, n1:n2] = B1[:, :, n1:n2]
49 B2[:, :, n1:n2, c] += h
50 n1 = n2
51 B1.shape = (-1, 3)
52 B2.shape = (-1, 3)
53 if shifted:
54 B1 -= 0.5
55 B2 -= 0.5
56 return B1, B2
59def get_bonds(atoms, covalent_radii):
60 from ase.neighborlist import PrimitiveNeighborList
62 nl = PrimitiveNeighborList(
63 covalent_radii * 1.5,
64 skin=0.0,
65 self_interaction=False,
66 bothways=False,
67 )
68 nl.update(atoms.pbc, atoms.get_cell(complete=True), atoms.positions)
69 number_of_neighbors = sum(indices.size for indices in nl.neighbors)
70 number_of_pbc_neighbors = sum(
71 offsets.any(axis=1).sum() for offsets in nl.displacements
72 ) # sum up all neighbors that have non-zero supercell offsets
73 nbonds = number_of_neighbors + number_of_pbc_neighbors
75 bonds = np.empty((nbonds, 5), int)
76 if nbonds == 0:
77 return bonds
79 n1 = 0
80 for a in range(len(atoms)):
81 indices, offsets = nl.get_neighbors(a)
82 n2 = n1 + len(indices)
83 bonds[n1:n2, 0] = a
84 bonds[n1:n2, 1] = indices
85 bonds[n1:n2, 2:] = offsets
86 n1 = n2
88 i = bonds[:n2, 2:].any(1)
89 pbcbonds = bonds[:n2][i]
90 bonds[n2:, 0] = pbcbonds[:, 1]
91 bonds[n2:, 1] = pbcbonds[:, 0]
92 bonds[n2:, 2:] = -pbcbonds[:, 2:]
93 return bonds
96class View:
97 def __init__(self, rotations):
98 self.colormode = 'jmol' # The default colors
99 self.axes = rotate(rotations)
100 self.configured = False
101 self.frame = None
103 # XXX
104 self.colormode = 'jmol'
105 self.colors = {
106 i: ('#{:02X}{:02X}{:02X}'.format(*(int(x * 255) for x in rgb)))
107 for i, rgb in enumerate(jmol_colors)
108 }
109 # scaling factors for vectors
110 self.force_vector_scale = self.config['force_vector_scale']
111 self.velocity_vector_scale = self.config['velocity_vector_scale']
112 self.magmom_vector_scale = self.config['magmom_vector_scale']
114 # buttons
115 self.b1 = 1 # left
116 self.b3 = 3 # right
117 if self.config['swap_mouse']:
118 self.b1 = 3
119 self.b3 = 1
121 @property
122 def atoms(self):
123 return self.images[self.frame]
125 def set_frame(self, frame=None, focus=False):
126 if frame is None:
127 frame = self.frame
128 assert frame < len(self.images)
129 self.frame = frame
130 self.set_atoms(self.images[frame])
132 fname = self.images.filenames[frame]
133 if fname is None:
134 header = 'ase.gui'
135 else:
136 # fname is actually not necessarily the filename but may
137 # contain indexing like filename@0
138 header = basename(fname)
140 images_loaded_text = ngettext(
141 'one image loaded',
142 '{} images loaded',
143 len(self.images)
144 ).format(len(self.images))
146 self.window.title = f'{header} — {images_loaded_text}'
148 if self.movie_window is not None:
149 self.movie_window.frame_number.value = frame
151 if focus:
152 self.focus()
153 else:
154 self.draw()
156 def get_bonds(self, atoms):
157 # this method exists rather than just using the standalone function
158 # so that it can be overridden by external libraries
159 return get_bonds(atoms, self.get_covalent_radii(atoms))
161 def set_atoms(self, atoms):
162 natoms = len(atoms)
164 if self.showing_cell():
165 B1, B2 = get_cell_coordinates(atoms.cell,
166 self.config['shift_cell'])
167 else:
168 B1 = B2 = np.zeros((0, 3))
170 if self.showing_bonds():
171 atomscopy = atoms.copy()
172 atomscopy.cell *= self.images.repeat[:, np.newaxis]
173 bonds = self.get_bonds(atomscopy)
174 else:
175 bonds = np.empty((0, 5), int)
177 # X is all atomic coordinates, and starting points of vectors
178 # like bonds and cell segments.
179 # The reason to have them all in one big list is that we like to
180 # eventually rotate/sort it by Z-order when rendering.
182 # Also B are the end points of line segments.
184 self.X = np.empty((natoms + len(B1) + len(bonds), 3))
185 self.X_pos = self.X[:natoms]
186 self.X_pos[:] = atoms.positions
187 self.X_cell = self.X[natoms:natoms + len(B1)]
188 self.X_bonds = self.X[natoms + len(B1):]
190 cell = atoms.cell
191 ncellparts = len(B1)
192 nbonds = len(bonds)
194 self.X_cell[:] = np.dot(B1, cell)
195 self.B = np.empty((ncellparts + nbonds, 3))
196 self.B[:ncellparts] = np.dot(B2, cell)
198 if nbonds > 0:
199 P = atoms.positions
200 Af = self.images.repeat[:, np.newaxis] * cell
201 a = P[bonds[:, 0]]
202 b = P[bonds[:, 1]] + np.dot(bonds[:, 2:], Af) - a
203 d = (b**2).sum(1)**0.5
204 r = 0.65 * self.get_covalent_radii()
205 x0 = (r[bonds[:, 0]] / d).reshape((-1, 1))
206 x1 = (r[bonds[:, 1]] / d).reshape((-1, 1))
207 self.X_bonds[:] = a + b * x0
208 b *= 1.0 - x0 - x1
209 b[bonds[:, 2:].any(1)] *= 0.5
210 self.B[ncellparts:] = self.X_bonds + b
212 self.obs.set_atoms.notify()
214 def showing_bonds(self):
215 return self.window['toggle-show-bonds']
217 def showing_cell(self):
218 return self.window['toggle-show-unit-cell']
220 def toggle_show_unit_cell(self, key=None):
221 self.set_frame()
223 def get_labels(self):
224 index = self.window['show-labels']
225 if index == 0:
226 return None
228 if index == 1:
229 return list(range(len(self.atoms)))
231 if index == 2:
232 return list(get_magmoms(self.atoms))
234 if index == 4:
235 Q = self.atoms.get_initial_charges()
236 return [f'{q:.4g}' for q in Q]
238 return self.atoms.symbols
240 def show_labels(self):
241 self.draw()
243 def toggle_show_axes(self, key=None):
244 self.draw()
246 def toggle_show_bonds(self, key=None):
247 self.set_frame()
249 def toggle_show_velocities(self, key=None):
250 self.draw()
252 def get_forces(self):
253 if self.atoms.calc is not None:
254 try:
255 return self.atoms.get_forces()
256 except PropertyNotImplementedError:
257 pass
258 return np.zeros((len(self.atoms), 3))
260 def toggle_show_forces(self, key=None):
261 self.draw()
263 def toggle_show_magmoms(self, key=None):
264 self.draw()
266 def hide_selected(self):
267 self.images.visible[self.images.selected] = False
268 self.draw()
270 def show_selected(self):
271 self.images.visible[self.images.selected] = True
272 self.draw()
274 def repeat_window(self, key=None):
275 return Repeat(self)
277 def rotate_window(self):
278 return Rotate(self)
280 def colors_window(self, key=None):
281 win = ColorWindow(self)
282 self.obs.new_atoms.register(win.notify_atoms_changed)
283 return win
285 def focus(self, x=None):
286 cell = (self.window['toggle-show-unit-cell'] and
287 self.images[0].cell.any())
288 if (len(self.atoms) == 0 and not cell):
289 self.scale = 20.0
290 self.center = np.zeros(3)
291 self.draw()
292 return
294 # Get the min and max point of the projected atom positions
295 # including the covalent_radii used for drawing the atoms
296 P = np.dot(self.X, self.axes)
297 n = len(self.atoms)
298 covalent_radii = self.get_covalent_radii()
299 P[:n] -= covalent_radii[:, None]
300 P1 = P.min(0)
301 P[:n] += 2 * covalent_radii[:, None]
302 P2 = P.max(0)
303 self.center = np.dot(self.axes, (P1 + P2) / 2)
304 self.center += self.atoms.get_celldisp().reshape((3,)) / 2
305 # Add 30% of whitespace on each side of the atoms
306 S = 1.3 * (P2 - P1)
307 w, h = self.window.size
308 if S[0] * h < S[1] * w:
309 self.scale = h / S[1]
310 elif S[0] > 0.0001:
311 self.scale = w / S[0]
312 else:
313 self.scale = 1.0
314 self.draw()
316 def reset_view(self, menuitem):
317 self.axes = rotate('0.0x,0.0y,0.0z')
318 self.set_frame()
319 self.focus(self)
321 def set_view(self, key):
322 if key == 'Z':
323 self.axes = rotate('0.0x,0.0y,0.0z')
324 elif key == 'X':
325 self.axes = rotate('-90.0x,-90.0y,0.0z')
326 elif key == 'Y':
327 self.axes = rotate('90.0x,0.0y,90.0z')
328 elif key == 'Shift+Z':
329 self.axes = rotate('180.0x,0.0y,90.0z')
330 elif key == 'Shift+X':
331 self.axes = rotate('0.0x,90.0y,0.0z')
332 elif key == 'Shift+Y':
333 self.axes = rotate('-90.0x,0.0y,0.0z')
334 else:
335 if key == 'I':
336 i, j = 1, 2
337 elif key == 'J':
338 i, j = 2, 0
339 elif key == 'K':
340 i, j = 0, 1
341 elif key == 'Shift+I':
342 i, j = 2, 1
343 elif key == 'Shift+J':
344 i, j = 0, 2
345 elif key == 'Shift+K':
346 i, j = 1, 0
348 A = complete_cell(self.atoms.cell)
349 x1 = A[i]
350 x2 = A[j]
352 norm = np.linalg.norm
354 x1 = x1 / norm(x1)
355 x2 = x2 - x1 * np.dot(x1, x2)
356 x2 /= norm(x2)
357 x3 = np.cross(x1, x2)
359 self.axes = np.array([x1, x2, x3]).T
361 self.set_frame()
363 def get_colors(self, rgb=False):
364 if rgb:
365 return [tuple(int(_rgb[i:i + 2], 16) / 255 for i in range(1, 7, 2))
366 for _rgb in self.get_colors()]
368 if self.colormode == 'jmol':
369 return [self.colors.get(Z, BLACKISH) for Z in self.atoms.numbers]
371 if self.colormode == 'neighbors':
372 return [self.colors.get(Z, BLACKISH)
373 for Z in self.get_color_scalars()]
375 colorscale, cmin, cmax = self.colormode_data
376 N = len(colorscale)
377 colorswhite = colorscale + ['#ffffff']
378 if cmin == cmax:
379 indices = [N // 2] * len(self.atoms)
380 else:
381 scalars = np.ma.array(self.get_color_scalars())
382 indices = np.clip(((scalars - cmin) / (cmax - cmin) * N +
383 0.5).astype(int),
384 0, N - 1).filled(N)
385 return [colorswhite[i] for i in indices]
387 def get_color_scalars(self, frame=None):
388 if self.colormode == 'tag':
389 return self.atoms.get_tags()
390 if self.colormode == 'force':
391 f = (self.get_forces()**2).sum(1)**0.5
392 return f * self.images.get_dynamic(self.atoms)
393 elif self.colormode == 'velocity':
394 return (self.atoms.get_velocities()**2).sum(1)**0.5
395 elif self.colormode == 'initial charge':
396 return self.atoms.get_initial_charges()
397 elif self.colormode == 'magmom':
398 return get_magmoms(self.atoms)
399 elif self.colormode == 'neighbors':
400 from ase.neighborlist import NeighborList
401 n = len(self.atoms)
402 nl = NeighborList(self.get_covalent_radii(self.atoms) * 1.5,
403 skin=0, self_interaction=False, bothways=True)
404 nl.update(self.atoms)
405 return [len(nl.get_neighbors(i)[0]) for i in range(n)]
406 else:
407 scalars = np.array(self.atoms.get_array(self.colormode),
408 dtype=float)
409 return np.ma.array(scalars, mask=np.isnan(scalars))
411 def get_covalent_radii(self, atoms=None):
412 if atoms is None:
413 atoms = self.atoms
414 return self.images.get_radii(atoms)
416 def draw(self, status=True):
417 self.window.clear()
418 axes = self.scale * self.axes * (1, -1, 1)
419 offset = np.dot(self.center, axes)
420 offset[:2] -= 0.5 * self.window.size
421 X = np.dot(self.X, axes) - offset
422 n = len(self.atoms)
424 # The indices enumerate drawable objects in z order:
425 self.indices = X[:, 2].argsort()
426 r = self.get_covalent_radii() * self.scale
427 if self.window['toggle-show-bonds']:
428 r *= 0.65
429 P = self.P = X[:n, :2]
430 A = (P - r[:, None]).round().astype(int)
431 X1 = X[n:, :2].round().astype(int)
432 X2 = (np.dot(self.B, axes) - offset).round().astype(int)
433 disp = (np.dot(self.atoms.get_celldisp().reshape((3,)),
434 axes)).round().astype(int)
435 d = (2 * r).round().astype(int)
437 vector_arrays = []
438 if self.window['toggle-show-velocities']:
439 # Scale ugly?
440 v = self.atoms.get_velocities()
441 if v is not None:
442 vector_arrays.append(v * 10.0 * self.velocity_vector_scale)
443 if self.window['toggle-show-forces']:
444 f = self.get_forces()
445 vector_arrays.append(f * self.force_vector_scale)
447 if self.window['toggle-show-magmoms']:
448 magmom = get_magmoms(self.atoms)
449 # Turn this into a 3D vector if it is a scalar
450 magmom_vecs = []
451 for i in range(len(magmom)):
452 if isinstance(magmom[i], (int, float)):
453 magmom_vecs.append(np.array([0, 0, magmom[i]]))
454 elif isinstance(magmom[i], np.ndarray) and len(magmom[i]) == 3:
455 magmom_vecs.append(magmom[i])
456 else:
457 raise TypeError('Magmom is not a 3-component vector '
458 'or a scalar')
459 magmom_vecs = np.array(magmom_vecs)
460 vector_arrays.append(magmom_vecs * 0.5 * self.magmom_vector_scale)
462 for array in vector_arrays:
463 array[:] = np.dot(array, axes) + X[:n]
465 colors = self.get_colors()
466 circle = self.window.circle
467 arc = self.window.arc
468 line = self.window.line
469 constrained = ~self.images.get_dynamic(self.atoms)
471 selected = self.images.selected
472 visible = self.images.visible
473 ncell = len(self.X_cell)
474 bond_linewidth = self.scale * 0.15
476 labels = self.get_labels()
478 if self.arrowkey_mode == self.ARROWKEY_MOVE:
479 movecolor = GREEN
480 elif self.arrowkey_mode == self.ARROWKEY_ROTATE:
481 movecolor = PURPLE
483 for a in self.indices:
484 if a < n:
485 ra = d[a]
486 if visible[a]:
487 try:
488 kinds = self.atoms.arrays['spacegroup_kinds']
489 site_occ = self.atoms.info['occupancy'][str(kinds[a])]
490 # first an empty circle if a site is not fully occupied
491 if (np.sum([v for v in site_occ.values()])) < 1.0:
492 fill = '#ffffff'
493 circle(fill, selected[a],
494 A[a, 0], A[a, 1],
495 A[a, 0] + ra, A[a, 1] + ra)
496 start = 0
497 # start with the dominant species
498 for sym, occ in sorted(site_occ.items(),
499 key=lambda x: x[1],
500 reverse=True):
501 if np.round(occ, decimals=4) == 1.0:
502 circle(colors[a], selected[a],
503 A[a, 0], A[a, 1],
504 A[a, 0] + ra, A[a, 1] + ra)
505 else:
506 # jmol colors for the moment
507 extent = 360. * occ
508 arc(self.colors[atomic_numbers[sym]],
509 selected[a],
510 start, extent,
511 A[a, 0], A[a, 1],
512 A[a, 0] + ra, A[a, 1] + ra)
513 start += extent
514 except KeyError:
515 # legacy behavior
516 # Draw the atoms
517 if (self.moving and a < len(self.move_atoms_mask)
518 and self.move_atoms_mask[a]):
519 circle(movecolor, False,
520 A[a, 0] - 4, A[a, 1] - 4,
521 A[a, 0] + ra + 4, A[a, 1] + ra + 4)
523 circle(colors[a], selected[a],
524 A[a, 0], A[a, 1], A[a, 0] + ra, A[a, 1] + ra)
526 # Draw labels on the atoms
527 if labels is not None:
528 self.window.text(A[a, 0] + ra / 2,
529 A[a, 1] + ra / 2,
530 str(labels[a]))
532 # Draw cross on constrained atoms
533 if constrained[a]:
534 R1 = int(0.14644 * ra)
535 R2 = int(0.85355 * ra)
536 line((A[a, 0] + R1, A[a, 1] + R1,
537 A[a, 0] + R2, A[a, 1] + R2))
538 line((A[a, 0] + R2, A[a, 1] + R1,
539 A[a, 0] + R1, A[a, 1] + R2))
541 # Draw velocities and/or forces
542 for v in vector_arrays:
543 assert not np.isnan(v).any()
544 self.arrow((X[a, 0], X[a, 1], v[a, 0], v[a, 1]),
545 width=2)
546 else:
547 # Draw unit cell and/or bonds:
548 a -= n
549 if a < ncell:
550 line((X1[a, 0] + disp[0], X1[a, 1] + disp[1],
551 X2[a, 0] + disp[0], X2[a, 1] + disp[1]))
552 else:
553 line((X1[a, 0], X1[a, 1],
554 X2[a, 0], X2[a, 1]),
555 width=bond_linewidth)
557 if self.window['toggle-show-axes']:
558 self.draw_axes()
560 if self.arrowkey_mode != self.ARROWKEY_SCAN:
561 self.draw_arrowkey_hint()
562 else:
563 self.hide_arrowkey_hint()
564 if len(self.images) > 1:
565 self.draw_frame_number()
567 self.window.update()
569 if status:
570 self.status.status(self.atoms)
572 # Currently we change the atoms all over the place willy-nilly
573 # and then call draw(). For which reason we abuse draw() to notify
574 # the observers about general changes.
575 #
576 # We should refactor so change_atoms is only emitted
577 # when when atoms actually change, and maybe have a separate signal
578 # to listen to e.g. changes of view.
579 self.obs.change_atoms.notify()
581 def arrow(self, coords, width):
582 line = self.window.line
583 begin = np.array((coords[0], coords[1]))
584 end = np.array((coords[2], coords[3]))
585 line(coords, width)
587 vec = end - begin
588 length = np.sqrt((vec[:2]**2).sum())
589 length = min(length, 0.3 * self.scale)
591 angle = np.arctan2(end[1] - begin[1], end[0] - begin[0]) + np.pi
592 x1 = (end[0] + length * np.cos(angle - 0.3)).round().astype(int)
593 y1 = (end[1] + length * np.sin(angle - 0.3)).round().astype(int)
594 x2 = (end[0] + length * np.cos(angle + 0.3)).round().astype(int)
595 y2 = (end[1] + length * np.sin(angle + 0.3)).round().astype(int)
596 line((x1, y1, end[0], end[1]), width)
597 line((x2, y2, end[0], end[1]), width)
599 def draw_axes(self):
600 axes_length = 15
602 rgb = ['red', 'green', 'blue']
604 for i in self.axes[:, 2].argsort():
605 a = 20
606 b = self.window.size[1] - 20
607 c = int(self.axes[i][0] * axes_length + a)
608 d = int(-self.axes[i][1] * axes_length + b)
609 self.window.line((a, b, c, d))
610 self.window.text(c, d, 'XYZ'[i], color=rgb[i])
612 def draw_arrowkey_hint(self):
613 if self.arrowkey_mode == self.ARROWKEY_ROTATE:
614 hint = _('ROTATING')
615 bg = PURPLE
616 tip_text = _(
617 'Ctrl + Up/Down: rotate around the view axis\n'
618 'Shift + Arrow keys: rotate in smaller increments'
619 )
620 else:
621 hint = _('MOVING')
622 bg = GREEN
623 tip_text = _(
624 'Ctrl + Up/Down: move along the view axis\n'
625 'Shift + Arrow keys: move in smaller increments'
626 )
627 self.arrowkey_hint.label.configure(
628 text=hint,
629 padx=3,
630 bg=bg,
631 )
632 self.arrowkey_hint.tooltip.configure(
633 text=tip_text,
634 justify='right',
635 font='TkTooltipFont',
636 )
637 self.arrowkey_hint.place(relx=1.0, rely=1.0, anchor='se')
638 self.arrowkey_hint.exists = True
640 def hide_arrowkey_hint(self):
641 if self.arrowkey_hint.exists:
642 self.arrowkey_hint.place_forget()
643 self.arrowkey_hint.exists = False
645 def draw_frame_number(self):
646 x, y = self.window.size
647 self.window.text(x, y, '{}'.format(self.frame),
648 anchor='SE')
650 def release(self, event):
651 if event.button in [4, 5]:
652 self.scroll_event(event)
653 return
655 if event.button != self.b1:
656 return
658 selected = self.images.selected
659 selected_ordered = self.images.selected_ordered
661 if event.time < self.t0 + 200: # 200 ms
662 d = self.P - self.xy
663 r = self.get_covalent_radii()
664 hit = np.less((d**2).sum(1), (self.scale * r)**2)
665 for a in self.indices[::-1]:
666 if a < len(self.atoms) and hit[a]:
667 if event.modifier == 'ctrl':
668 selected[a] = not selected[a]
669 if selected[a]:
670 selected_ordered += [a]
671 elif len(selected_ordered) > 0:
672 if selected_ordered[-1] == a:
673 selected_ordered = selected_ordered[:-1]
674 else:
675 selected_ordered = []
676 else:
677 selected[:] = False
678 selected[a] = True
679 selected_ordered = [a]
680 break
681 else:
682 selected[:] = False
683 selected_ordered = []
684 self.draw()
685 else:
686 A = (event.x, event.y)
687 C1 = np.minimum(A, self.xy)
688 C2 = np.maximum(A, self.xy)
689 hit = np.logical_and(self.P > C1, self.P < C2)
690 indices = np.compress(hit.prod(1), np.arange(len(hit)))
691 if event.modifier != 'ctrl':
692 selected[:] = False
693 selected[indices] = True
694 if (len(indices) == 1 and
695 indices[0] not in self.images.selected_ordered):
696 selected_ordered += [indices[0]]
697 elif len(indices) > 1:
698 selected_ordered = []
699 self.draw()
701 # XXX check bounds
702 natoms = len(self.atoms)
703 indices = np.arange(natoms)[self.images.selected[:natoms]]
704 if len(indices) != len(selected_ordered):
705 selected_ordered = []
706 self.images.selected_ordered = selected_ordered
708 def press(self, event):
709 self.button = event.button
710 self.xy = (event.x, event.y)
711 self.t0 = event.time
712 self.axes0 = self.axes
713 self.center0 = self.center
715 def move(self, event):
716 x = event.x
717 y = event.y
718 x0, y0 = self.xy
719 if self.button == self.b1:
720 x0 = int(round(x0))
721 y0 = int(round(y0))
722 self.draw()
723 self.window.canvas.create_rectangle((x, y, x0, y0))
724 return
726 if event.modifier == 'shift':
727 self.center = (self.center0 -
728 np.dot(self.axes, (x - x0, y0 - y, 0)) / self.scale)
729 else:
730 # Snap mode: the a-b angle and t should multipla of 15 degrees ???
731 a = x - x0
732 b = y0 - y
733 t = sqrt(a * a + b * b)
734 if t > 0:
735 a /= t
736 b /= t
737 else:
738 a = 1.0
739 b = 0.0
740 c = cos(0.01 * t)
741 s = -sin(0.01 * t)
742 rotation = np.array([(c * a * a + b * b, (c - 1) * b * a, s * a),
743 ((c - 1) * a * b, c * b * b + a * a, s * b),
744 (-s * a, -s * b, c)])
745 self.axes = np.dot(self.axes0, rotation)
746 if len(self.atoms) > 0:
747 com = self.X_pos.mean(0)
748 else:
749 com = self.atoms.cell.mean(0)
750 self.center = com - np.dot(com - self.center0,
751 np.dot(self.axes0, self.axes.T))
752 self.draw(status=False)
754 def render_window(self):
755 return Render(self)
757 def resize(self, event):
758 w, h = self.window.size
759 self.scale *= (event.width * event.height / (w * h))**0.5
760 self.window.size[:] = [event.width, event.height]
761 self.draw()