###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or <http://www.boost.org/LICENSE_1_0.txt> # # # ###################################################################### """ Classes and functions for the visualization of layouts created with the gdspy Python module. """ from __future__ import division from __future__ import unicode_literals from __future__ import print_function from __future__ import absolute_import import sys if sys.version_info.major < 3: from builtins import zip from builtins import open from builtins import int from builtins import round from builtins import range from builtins import super from future import standard_library standard_library.install_aliases() else: # Python 3 doesn't have basestring, as unicode is type string # Python 2 doesn't equate unicode to string, but both are basestring # Now isinstance(s, basestring) will be True for any python version basestring = str import os import colorsys import warnings import numpy import tkinter import tkinter.messagebox import tkinter.colorchooser import gdspy _stipple = tuple( "@" + os.path.join(os.path.dirname(gdspy.__file__), "data", "{:02d}.xbm".format(n)) for n in range(10) ) _icon_up = "@" + os.path.join(os.path.dirname(gdspy.__file__), "data", "up.xbm") _icon_down = "@" + os.path.join(os.path.dirname(gdspy.__file__), "data", "down.xbm") _icon_outline = "@" + os.path.join( os.path.dirname(gdspy.__file__), "data", "outline.xbm" ) _invisible = 9 _tkinteranchors = [ tkinter.NW, tkinter.N, tkinter.NE, None, tkinter.W, tkinter.CENTER, tkinter.E, None, tkinter.SW, tkinter.S, tkinter.SE, ] class ColorDict(dict): def __init__(self, default): super(ColorDict, self).__init__() self.default = default def __missing__(self, key): if self.default is None: layer, datatype = key rgb = "#{0[0]:02x}{0[1]:02x}{0[2]:02x}".format( [ int(255 * c + 0.5) for c in colorsys.hsv_to_rgb( (layer % 3) / 3.0 + (layer % 6 // 3) / 6.0 + (layer // 6) / 11.0, 1 - ((layer + datatype) % 8) / 12.0, 1 - (datatype % 3) / 4.0, ) ] ) else: rgb = self.default self[key] = rgb return rgb class PatternDict(dict): def __init__(self, default): super(PatternDict, self).__init__() self.default = default def __missing__(self, key): if self.default is None: pat = (key[0] + key[1]) % 8 else: pat = self.default self[key] = pat return pat class LayoutViewer(tkinter.Frame): """ Provide a GUI where the layout can be viewed. The view can be scrolled vertically with the mouse wheel, and horizontally by holding the shift key and using the mouse wheel. Dragging the 2nd mouse button also scrolls the view, and if control is held down, it scrolls 10 times faster. You can zoom in or out using control plus the mouse wheel, or drag a rectangle on the window with the 1st mouse button to zoom into that area. A ruler is available by clicking the 1st mouse button anywhere on the view and moving the mouse around. The distance is shown in the status area. Double-clicking on any polygon gives some information about it. Color and pattern for each layer/datatype specification can be changed by left and right clicking on the icon in the layer/datatype list. Left and right clicking the text label changes the visibility. Parameters ---------- library : ``GdsLibrary`` GDSII library to display. cells : Cell or iterable The array of cells to be included in the view in addition to the library cells. hidden_types : array-like The array of tuples (layer, datatype) to start in hidden state. depth : integer Initial depth of referenced cells to be displayed. color : dictionary Dictionary of colors for each tuple (layer, datatype). The colors must be strings in the format ``#rrggbb``. A value with key ``default`` will be used as default color. pattern : dictionary Dictionary of patterns for each tuple (layer, datatype). The patterns must be integers between 0 and 9, inclusive. A value with key ``default`` will be used as default pattern. background : string Canvas background color in the format ``#rrggbb``. width : integer Horizontal size of the viewer canvas. height : integer Vertical size of the viewer canvas. Examples -------- White background, filled shapes: >>> gdspy.LayoutViewer(pattern={'default': 8}, ... background='#FFFFFF') No filling, black color for layer 0, datatype 1, automatic for others: >>> gdspy.LayoutViewer(pattern={'default': 9}, ... color={(0, 1): '#000000'}) """ def __init__( self, library=None, cells=None, hidden_types=[], depth=0, color={}, pattern={}, background="#202020", width=800, height=600, ): tkinter.Frame.__init__(self, None) self.cells = {} if library is None else dict(library.cells) if cells is not None: if isinstance(cells, gdspy.Cell): self.cells[cells.name] = cells else: for c in cells: self.cells[c.name] = c if len(self.cells) == 0: self.cells = dict(gdspy.current_library.cells) if len(self.cells) == 0: raise ValueError("[GDSPY] No cells to display in LayoutViewer.") else: warnings.warn( "[GDSPY] Use of the global library is deprecated. " "Pass LayoutViewer a GdsLibrary instance.", category=DeprecationWarning, stacklevel=2, ) cell_names = list(self.cells.keys()) self.cell_bb = dict([(s, None) for s in self.cells]) self.current_cell = tkinter.StringVar() self.current_cell.set(cell_names[0]) self.depth = tkinter.IntVar() self.depth.set(depth) self.hidden_layers = hidden_types self.color = ColorDict(color.get("default", None)) self.color.update(color) self.pattern = PatternDict(pattern.get("default", None)) self.pattern.update(pattern) # Setup resizable window self.grid(sticky="nsew") top = self.winfo_toplevel() top.rowconfigure(0, weight=1) top.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) # Setup canvas self.canvas = tkinter.Canvas( self, width=width, height=height, xscrollincrement=0, yscrollincrement=0 ) self.canvas.grid(row=0, column=0, sticky="nsew") self.canvas.configure(bg=background) bg = [int(c, 16) for c in (background[1:3], background[3:5], background[5:7])] self.default_outline = "#{0[0]:02x}{0[1]:02x}{0[2]:02x}".format( [(0 if c > 127 else 255) for c in bg] ) self.default_grey = "#{0[0]:02x}{0[1]:02x}{0[2]:02x}".format( [ ( (c + 256) // 2 if c < 85 else (c // 2 if c > 171 else (255 if c > 127 else 0)) ) for c in bg ] ) # Setup scrollbars self.xscroll = tkinter.Scrollbar( self, orient=tkinter.HORIZONTAL, command=self.canvas.xview ) self.xscroll.grid(row=1, column=0, sticky="ew") self.yscroll = tkinter.Scrollbar( self, orient=tkinter.VERTICAL, command=self.canvas.yview ) self.yscroll.grid(row=0, column=1, sticky="ns") self.canvas["xscrollcommand"] = self.xscroll.set self.canvas["yscrollcommand"] = self.yscroll.set # Setup toolbar self.frame = tkinter.Frame(self) self.frame.columnconfigure(6, weight=1) self.frame.grid(row=2, column=0, columnspan=2, padx=2, pady=2, sticky="ew") # Setup buttons self.home = tkinter.Button( self.frame, text="Extents", command=self._update_canvas ) self.zoom_in = tkinter.Button(self.frame, text="In", command=self._zoom_in) self.zoom_out = tkinter.Button(self.frame, text="Out", command=self._zoom_out) self.cell_menu = tkinter.OptionMenu(self.frame, self.current_cell, *cell_names) self.depth_spin = tkinter.Spinbox( self.frame, textvariable=self.depth, command=self._update_depth, from_=-1, to=128, increment=1, justify=tkinter.RIGHT, width=3, ) self.home.grid(row=0, column=0, sticky="w") self.zoom_in.grid(row=0, column=1, sticky="w") self.zoom_out.grid(row=0, column=2, sticky="w") self.cell_menu.grid(row=0, column=3, sticky="w") tkinter.Label(self.frame, text="Ref level:").grid(row=0, column=4, sticky="w") self.depth_spin.grid(row=0, column=5, sticky="w") self.bind_all("<KeyPress-Home>", self._update_canvas) self.bind_all("<KeyPress-A>", self._update_canvas) self.bind_all("<KeyPress-a>", self._update_canvas) # Setup coordinates box self.coords = tkinter.Label(self.frame, text="0, 0") self.coords.grid(row=0, column=6, sticky="e") # Layers self.l_canvas = tkinter.Canvas(self) self.l_canvas.grid(row=0, column=2, rowspan=3, sticky="nsew") self.l_scroll = tkinter.Scrollbar( self, orient=tkinter.VERTICAL, command=self.l_canvas.yview ) self.l_scroll.grid(row=0, column=3, rowspan=3, sticky="ns") self.l_canvas["yscrollcommand"] = self.l_scroll.set # Change current cell self.current_cell.trace_variable("w", self._update_canvas) # Update depth self.depth_spin.bind("<FocusOut>", self._update_depth) # Drag-scroll: button 2 self.canvas.bind("<Button-2>", lambda evt: self.canvas.scan_mark(evt.x, evt.y)) self.canvas.bind("<Motion>", self._mouse_move) # Y scroll: scroll wheel self.canvas.bind( "<MouseWheel>", lambda evt: self.canvas.yview( tkinter.SCROLL, 1 if evt.delta < 0 else -1, tkinter.UNITS ), ) self.canvas.bind( "<Button-4>", lambda evt: self.canvas.yview(tkinter.SCROLL, -1, tkinter.UNITS), ) self.canvas.bind( "<Button-5>", lambda evt: self.canvas.yview(tkinter.SCROLL, 1, tkinter.UNITS), ) self.l_canvas.bind( "<MouseWheel>", lambda evt: self.l_canvas.yview( tkinter.SCROLL, 1 if evt.delta < 0 else -1, tkinter.UNITS ), ) self.l_canvas.bind( "<Button-4>", lambda evt: self.l_canvas.yview(tkinter.SCROLL, -1, tkinter.UNITS), ) self.l_canvas.bind( "<Button-5>", lambda evt: self.l_canvas.yview(tkinter.SCROLL, 1, tkinter.UNITS), ) # X scroll: shift + scroll wheel self.bind_all( "<Shift-MouseWheel>", lambda evt: self.canvas.xview( tkinter.SCROLL, 1 if evt.delta < 0 else -1, tkinter.UNITS ), ) self.canvas.bind( "<Shift-Button-4>", lambda evt: self.canvas.xview(tkinter.SCROLL, -1, tkinter.UNITS), ) self.canvas.bind( "<Shift-Button-5>", lambda evt: self.canvas.xview(tkinter.SCROLL, 1, tkinter.UNITS), ) # Object properties: double button 1 # Zoom rectangle: drag button 1 # Measure tool: button 1 (click + click, no drag) self.canvas.bind("<Button-1>", self._zoom_rect_mark) self.canvas.bind("<ButtonRelease-1>", self._mouse_btn_1) self.canvas.bind("<Double-Button-1>", self._properties) # Zoom: control + scroll wheel self.bind_all("<Control-MouseWheel>", self._zoom) self.canvas.bind("<Control-Button-4>", self._zoom) self.canvas.bind("<Control-Button-5>", self._zoom) # Update the viewer self.shown_cell = None self.shown_depth = depth self.canvas_margins = None self._update_canvas() self.master.title("Gdspy - Layout Viewer") self.mainloop() def _update_depth(self, *args): try: d = self.depth.get() except: self.depth.set(self.shown_depth) return if d != self.shown_depth: self.shown_cell = self.current_cell.get() self.shown_depth = d self._update_data() def _update_canvas(self, *args): if self.shown_cell is None: width = float(self.canvas.cget("width")) height = float(self.canvas.cget("height")) else: width = float(self.canvas.winfo_width()) - self.canvas_margins[0] height = float(self.canvas.winfo_height()) - self.canvas_margins[1] self.shown_cell = self.current_cell.get() if self.cell_bb[self.current_cell.get()] is None: bb = [1e300, 1e300, -1e300, -1e300] pol_dict = self.cells[self.current_cell.get()].get_polygons( by_spec=True, depth=self.shown_depth ) for pols in pol_dict.values(): for pol in pols: bb[0] = min(bb[0], pol[:, 0].min()) bb[1] = min(bb[1], -pol[:, 1].max()) bb[2] = max(bb[2], pol[:, 0].max()) bb[3] = max(bb[3], -pol[:, 1].min()) self.cell_bb[self.current_cell.get()] = tuple(bb) else: bb = list(self.cell_bb[self.current_cell.get()]) if bb[2] < bb[0]: tkinter.messagebox.showwarning("Warning", "The selected cell is empty.") bb = [-1, -1, 1, 1] self.scale = ((bb[3] - bb[1]) / height, (bb[2] - bb[0]) / width) if self.scale[0] > self.scale[1]: self.scale = self.scale[0] * 1.05 add = (width * self.scale - bb[2] + bb[0]) * 0.5 bb[0] -= add bb[2] += add add = (bb[3] - bb[1]) * 0.025 bb[1] -= add bb[3] += add else: self.scale = self.scale[1] * 1.05 add = (height * self.scale - bb[3] + bb[1]) * 0.5 bb[1] -= add bb[3] += add add = (bb[2] - bb[0]) * 0.025 bb[0] -= add bb[2] += add self._update_data() self.canvas.configure(scrollregion=tuple([x / self.scale for x in bb])) self.canvas.zoom_rect = None if self.canvas_margins is None: self.update() self.canvas_margins = ( int(self.canvas.winfo_width()) - width, int(self.canvas.winfo_height()) - height, ) def _update_data(self): self.canvas.delete(tkinter.ALL) self.l_canvas.delete(tkinter.ALL) self.canvas.ruler = None self.canvas.x_rl = 0 self.canvas.y_rl = 0 pol_dict = self.cells[self.current_cell.get()].get_polygons( by_spec=True, depth=self.shown_depth ) lbl_dict = {} for label in self.cells[self.current_cell.get()].get_labels( depth=self.shown_depth ): key = (label.layer, label.texttype) if key in lbl_dict: lbl_dict[key].append(label) else: lbl_dict[key] = [label] layers = list(set(list(pol_dict.keys()) + list(lbl_dict.keys()))) layers.sort( reverse=True, key=lambda i: (-1, -1) if not isinstance(i, tuple) else i ) self.l_canvas_info = [] pos = 0 wid = None hei = None bg = self.canvas["bg"] self.l_canvas.configure(bg=bg) for i in layers: if i in self.hidden_layers: state = "hidden" fg = self.default_grey else: state = "normal" fg = self.default_outline if isinstance(i, tuple): lbl = ( tkinter.Label( self, bitmap=_icon_outline if self.pattern[i] == _invisible else _stipple[self.pattern[i]], bd=0, fg=self.color[i], bg=bg, anchor="c", ), tkinter.Label( self, text="{0[0]}/{0[1]}".format(i), bd=0, fg=fg, bg=bg, height=1, anchor="c", padx=8, ), tkinter.Label( self, bitmap=_icon_up, bd=0, fg=self.default_outline, bg=bg, anchor="c", ), tkinter.Label( self, bitmap=_icon_down, bd=0, fg=self.default_outline, bg=bg, anchor="c", ), ) lbl[0].bind("<Button-1>", self._change_color(lbl[0], i)) lbl[0].bind("<Button-2>", self._change_pattern(lbl[0], i)) lbl[0].bind("<Button-3>", self._change_pattern(lbl[0], i)) lbl[1].bind("<Button-1>", self._change_visibility(lbl[1], i)) lbl[1].bind("<Button-2>", self._change_other_visibility(i)) lbl[1].bind("<Button-3>", self._change_other_visibility(i)) lbl[2].bind("<Button-1>", self._raise(i)) lbl[3].bind("<Button-1>", self._lower(i)) for l in lbl: l.bind( "<MouseWheel>", lambda evt: self.l_canvas.yview( tkinter.SCROLL, 1 if evt.delta < 0 else -1, tkinter.UNITS ), ) l.bind( "<Button-4>", lambda evt: self.l_canvas.yview( tkinter.SCROLL, -1, tkinter.UNITS ), ) l.bind( "<Button-5>", lambda evt: self.l_canvas.yview( tkinter.SCROLL, 1, tkinter.UNITS ), ) if wid is None: lbl[1].configure(text="255/255") hei = max(lbl[0].winfo_reqheight(), lbl[1].winfo_reqheight()) wid = lbl[1].winfo_reqwidth() lbl[1].configure(text="{0[0]}/{0[1]}".format(i)) ids = ( self.l_canvas.create_window(0, pos, window=lbl[0], anchor="sw"), self.l_canvas.create_window(hei, pos, window=lbl[1], anchor="sw"), self.l_canvas.create_window( hei + wid, pos, window=lbl[2], anchor="sw" ), self.l_canvas.create_window( 2 * hei + wid, pos, window=lbl[3], anchor="sw" ), ) self.l_canvas_info.append((i, ids, lbl)) pos -= hei if i in pol_dict: if not isinstance(i, tuple): for pol in pol_dict[i]: self.canvas.create_polygon( *list((numpy.array((1, -1)) * pol / self.scale).flatten()), fill="", outline=self.default_outline, activeoutline=self.default_outline, activewidth=2, tag=("L" + str(i), "V" + str(pol.shape[0])), state=state, dash=(8, 8) ) self.canvas.create_text( pol[:, 0].mean() / self.scale, pol[:, 1].mean() / -self.scale, text=i, anchor=tkinter.CENTER, fill=self.default_outline, tag=("L" + str(i), "TEXT"), ) else: for pol in pol_dict[i]: self.canvas.create_polygon( *list((numpy.array((1, -1)) * pol / self.scale).flatten()), fill=self.color[i], stipple=_stipple[self.pattern[i]], offset="{},{}".format(*numpy.random.randint(16, size=2)), outline=self.color[i], activeoutline=self.default_outline, activewidth=2, tag=("L" + str(i), "V" + str(pol.shape[0])), state=state ) if i in lbl_dict: for label in lbl_dict[i]: self.canvas.create_text( label.position[0] / self.scale, label.position[1] / -self.scale, text=label.text, anchor=_tkinteranchors[label.anchor], fill=self.color[i], activefill=self.default_outline, tag=("L" + str(i), "TEXT"), state=state, ) if (wid is None) or (hei is None) or (pos is None): pos = -12 hei = 12 wid = 12 self.l_canvas.configure( width=3 * hei + wid, scrollregion=(0, pos, 3 * hei + wid, 0), yscrollincrement=hei, ) def _change_color(self, lbl, layer): def func(*args): rgb, color = tkinter.colorchooser.askcolor( self.color[layer], title="Select color" ) if color is not None: self.color[layer] = color lbl.configure(fg=color) for i in self.canvas.find_withtag("L" + str(layer)): self.canvas.itemconfigure(i, fill=color) if layer[0] >= 0 and "TEXT" not in self.canvas.gettags(i): self.canvas.itemconfigure(i, outline=color) return func def _change_pattern(self, lbl, layer): def func(*args): pattern = [] dlg = tkinter.Toplevel() dlg.title("Select pattern") dlg.resizable(False, False) for i in range(10): choice = tkinter.Button( dlg, bitmap=_stipple[i], command=(lambda x: (lambda: pattern.append(x) or dlg.destroy()))(i), ) choice.grid(row=0, column=i, padx=3, pady=3) choice = tkinter.Button(dlg, text="Cancel", command=dlg.destroy) choice.grid(row=1, column=0, columnspan=10, padx=3, pady=3, sticky="e") dlg.focus_set() dlg.wait_visibility() dlg.grab_set() dlg.wait_window(dlg) if len(pattern) > 0: self.pattern[layer] = pattern[0] lbl.configure( bitmap=_icon_outline if pattern[0] == _invisible else _stipple[pattern[0]] ) for i in self.canvas.find_withtag("L" + str(layer)): if "TEXT" not in self.canvas.gettags(i): self.canvas.itemconfigure(i, stipple=_stipple[pattern[0]]) return func def _change_visibility(self, lbl, layer): def func(*args): if layer in self.hidden_layers: self.hidden_layers.remove(layer) lbl.configure(fg=self.default_outline) for j in self.canvas.find_withtag("L" + str(layer)): self.canvas.itemconfigure(j, state="normal") else: self.hidden_layers.append(layer) lbl.configure(fg=self.default_grey) for j in self.canvas.find_withtag("L" + str(layer)): self.canvas.itemconfigure(j, state="hidden") return func def _change_other_visibility(self, layer): def func(*args): for other, ids, lbl in self.l_canvas_info: if layer != other: unhide = other in self.hidden_layers break for other, ids, lbl in self.l_canvas_info: if layer != other: if unhide and (other in self.hidden_layers): self.hidden_layers.remove(other) lbl[1].configure(fg=self.default_outline) for j in self.canvas.find_withtag("L" + str(other)): self.canvas.itemconfigure(j, state="normal") elif not (unhide or (other in self.hidden_layers)): self.hidden_layers.append(other) lbl[1].configure(fg=self.default_grey) for j in self.canvas.find_withtag("L" + str(other)): self.canvas.itemconfigure(j, state="hidden") return func def _raise(self, layer): def func(*args): idx = 0 while self.l_canvas_info[idx][0] != layer: idx += 1 if idx < len(self.l_canvas_info) - 1: hei = int(self.l_canvas["yscrollincrement"]) under, idu, _ = self.l_canvas_info[idx] above, ida, _ = self.l_canvas_info[idx + 1] self.canvas.tag_raise("L" + str(under), "L" + str(above)) for i in idu: self.l_canvas.move(i, 0, -hei) for i in ida: self.l_canvas.move(i, 0, hei) self.l_canvas.yview_scroll(-1, tkinter.UNITS) self.l_canvas_info[idx], self.l_canvas_info[idx + 1] = ( self.l_canvas_info[idx + 1], self.l_canvas_info[idx], ) return func def _lower(self, layer): def func(*args): idx = 0 while self.l_canvas_info[idx][0] != layer: idx += 1 if idx > 0: hei = int(self.l_canvas["yscrollincrement"]) under, idu, _ = self.l_canvas_info[idx - 1] above, ida, _ = self.l_canvas_info[idx] self.canvas.tag_raise("L" + str(under), "L" + str(above)) for i in idu: self.l_canvas.move(i, 0, -hei) for i in ida: self.l_canvas.move(i, 0, hei) self.l_canvas.yview_scroll(1, tkinter.UNITS) self.l_canvas_info[idx], self.l_canvas_info[idx - 1] = ( self.l_canvas_info[idx - 1], self.l_canvas_info[idx], ) return func def _mouse_move(self, evt): x = self.canvas.canvasx(evt.x) y = self.canvas.canvasy(evt.y) if self.canvas.ruler is None: self.coords.configure( text="{0:g}, {1:g}".format(x * self.scale, -y * self.scale) ) else: self.canvas.coords( self.canvas.ruler, self.canvas.x_rl, self.canvas.y_rl, x, y ) dx = (x - self.canvas.x_rl) * self.scale dy = (self.canvas.y_rl - y) * self.scale self.coords.configure( text="Distance: {0:g} | dx = {1:g} | dy = {2:g}".format( (dx ** 2 + dy ** 2) ** 0.5, dx, dy ) ) if int(evt.state) & 0x0200: if int(evt.state) & 0x0004: self.canvas.scan_dragto(evt.x, evt.y, 10) else: self.canvas.scan_dragto(evt.x, evt.y, 1) elif int(evt.state) & 0x0100: if self.canvas.zoom_rect is None: self.canvas.zoom_rect = self.canvas.create_rectangle( self.canvas.x_zr, self.canvas.y_zr, self.canvas.x_zr, self.canvas.y_zr, outline="#DDD", ) self.canvas.coords( self.canvas.zoom_rect, self.canvas.x_zr, self.canvas.y_zr, x, y ) def _zoom(self, evt): if evt.num == 4: evt.delta = 1 elif evt.num == 5: evt.delta = -1 s = 1.5 if evt.delta > 0 else 1 / 1.5 self.scale /= s x0 = s * self.canvas.canvasx(evt.x) - evt.x y0 = s * self.canvas.canvasy(evt.y) - evt.y self.canvas.scale(tkinter.ALL, 0, 0, s, s) self.canvas.x_rl *= s self.canvas.y_rl *= s bb = self.canvas.bbox(tkinter.ALL) if bb is not None: w = (bb[2] - bb[0]) * 1.2 h = (bb[3] - bb[1]) * 1.2 bb = (bb[0] - w, bb[1] - h, bb[2] + w, bb[3] + h) self.canvas["scrollregion"] = bb self.canvas.xview(tkinter.MOVETO, (x0 - bb[0]) / (bb[2] - bb[0])) self.canvas.yview(tkinter.MOVETO, (y0 - bb[1]) / (bb[3] - bb[1])) def _zoom_in(self): s = 1.5 self.scale /= s self.canvas.scale(tkinter.ALL, 0, 0, s, s) self.canvas.x_rl *= s self.canvas.y_rl *= s bb = self.canvas.bbox(tkinter.ALL) w = (bb[2] - bb[0]) * 1.2 h = (bb[3] - bb[1]) * 1.2 bb = (bb[0] - w, bb[1] - h, bb[2] + w, bb[3] + h) self.canvas["scrollregion"] = bb x0 = self.xscroll.get() x0 = 0.5 * (x0[1] + x0[0]) - 0.5 * ( (float(self.canvas.winfo_width()) - self.canvas_margins[0]) / (bb[2] - bb[0]) ) y0 = self.yscroll.get() y0 = 0.5 * (y0[1] + y0[0]) - 0.5 * ( (float(self.canvas.winfo_height()) - self.canvas_margins[1]) / (bb[3] - bb[1]) ) self.canvas.xview(tkinter.MOVETO, x0) self.canvas.yview(tkinter.MOVETO, y0) def _zoom_out(self): s = 1 / 1.5 self.scale /= s self.canvas.scale(tkinter.ALL, 0, 0, s, s) self.canvas.x_rl *= s self.canvas.y_rl *= s bb = self.canvas.bbox(tkinter.ALL) w = (bb[2] - bb[0]) * 1.2 h = (bb[3] - bb[1]) * 1.2 bb = (bb[0] - w, bb[1] - h, bb[2] + w, bb[3] + h) self.canvas["scrollregion"] = bb x0 = self.xscroll.get() x0 = 0.5 * (x0[1] + x0[0]) - 0.5 * ( (float(self.canvas.winfo_width()) - self.canvas_margins[0]) / (bb[2] - bb[0]) ) y0 = self.yscroll.get() y0 = 0.5 * (y0[1] + y0[0]) - 0.5 * ( (float(self.canvas.winfo_height()) - self.canvas_margins[1]) / (bb[3] - bb[1]) ) self.canvas.xview(tkinter.MOVETO, x0) self.canvas.yview(tkinter.MOVETO, y0) def _zoom_rect_mark(self, evt): self.canvas.x_zr = float(self.canvas.canvasx(evt.x)) self.canvas.y_zr = float(self.canvas.canvasy(evt.y)) def _mouse_btn_1(self, evt): if self.canvas.zoom_rect is None: if self.canvas.ruler is None: x0 = self.canvas.canvasx(evt.x) y0 = self.canvas.canvasy(evt.y) self.canvas.ruler = self.canvas.create_line( x0, y0, x0, y0, arrow=tkinter.BOTH, fill=self.default_outline, width=2, ) self.canvas.x_rl = x0 self.canvas.y_rl = y0 else: self.canvas.delete(self.canvas.ruler) self.canvas.ruler = None else: x1 = float(self.canvas.winfo_width()) - self.canvas_margins[0] sx = float(self.canvas.canvasx(evt.x)) dx = abs(self.canvas.x_zr - sx) sx += self.canvas.x_zr y1 = float(self.canvas.winfo_height()) - self.canvas_margins[1] sy = float(self.canvas.canvasy(evt.y)) dy = abs(self.canvas.y_zr - sy) sy += self.canvas.y_zr self.canvas.delete(self.canvas.zoom_rect) self.canvas.zoom_rect = None if abs(dx * dy) > 1.0e-12: s = (x1 / dx, y1 / dy) if s[0] < s[1]: s = s[0] y0 = 0.5 * (s * sy - y1) x0 = 0.5 * s * (sx - dx) else: s = s[1] x0 = 0.5 * (s * sx - x1) y0 = 0.5 * s * (sy - dy) self.scale /= s self.canvas.scale(tkinter.ALL, 0, 0, s, s) self.canvas.x_rl *= s self.canvas.y_rl *= s bb = self.canvas.bbox(tkinter.ALL) if bb is not None: w = (bb[2] - bb[0]) * 1.5 h = (bb[3] - bb[1]) * 1.5 bb = (bb[0] - w, bb[1] - h, bb[2] + w, bb[3] + h) self.canvas["scrollregion"] = bb self.canvas.xview(tkinter.MOVETO, (x0 - bb[0]) / (bb[2] - bb[0])) self.canvas.yview(tkinter.MOVETO, (y0 - bb[1]) / (bb[3] - bb[1])) def _properties(self, evt): if self.canvas.ruler is not None: self.canvas.delete(self.canvas.ruler) self.canvas.ruler = -1 i = self.canvas.find_closest( self.canvas.canvasx(evt.x), self.canvas.canvasy(evt.y) ) bb = self.canvas.bbox(i) if bb is not None: bb = ( bb[0] * self.scale, -bb[3] * self.scale, bb[2] * self.scale, -bb[1] * self.scale, ) tags = self.canvas.gettags(i) if "TEXT" not in tags: tkinter.messagebox.showinfo( "Element information", "Layer/datatpe: {0}\nVertices: {1}\nApproximate bounding box:\n({2[0]:g}, {2[1]:g}) - ({2[2]:g}, {2[3]:g})".format( tags[0][1:], tags[1][1:], bb ), parent=self.canvas, )