Wiresheet zoom, from the View menu, the toolbar and Ctrl+wheel
A ladder of scales from 50% to 300%, stepped by Zoom In / Zoom Out / Zoom 100%, by the two buttons beside the navigation arrows, and by Ctrl and the wheel, which holds the point under the pointer still. A menu or keyboard zoom holds the middle of the view instead, so it closes in on what is already being looked at. Painting and routing stay at 1:1, because that is where every metric is tuned: box header and row heights, wire stubs, lane gaps, standoffs, knob radii. Zoom scales the finished picture, so a zoomed sheet keeps the proportions of a 1:1 one and the router reaches the same decisions at every scale. Text comes along because the three box fonts are resized once the boxes have been measured with them. redraw_links replaces the direct draw_links calls a drag or a drop makes: the router reads box positions off the canvas, so the boxes go back to 1:1 for the routing pass and the result is scaled again. Canvas coordinates carry the zoom, which is why the drag, release and drop handlers now snap against meta_scale * zoom -- a box dragged the same distance across the screen lands on the same meta step at 50%, 100% and 200%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,9 @@ Undoing back to the depth of the last save clears the dirty tint rather than lea
|
|||||||
| `Ctrl`/`Cmd` + `B` | Show or hide the tree/palette column |
|
| `Ctrl`/`Cmd` + `B` | Show or hide the tree/palette column |
|
||||||
| `Ctrl` + `W` | Tidy Layout: re-flow the open sheet |
|
| `Ctrl` + `W` | Tidy Layout: re-flow the open sheet |
|
||||||
| `Alt` + `Left` / `Alt` + `Right` | Back and forward through the sheets opened this session |
|
| `Alt` + `Left` / `Alt` + `Right` | Back and forward through the sheets opened this session |
|
||||||
|
| `Ctrl`/`Cmd` + `+` / `-` | Zoom the sheet in and out |
|
||||||
|
| `Ctrl`/`Cmd` + `0` | Zoom back to 100% |
|
||||||
|
| `Ctrl` + wheel | Zoom around the pointer |
|
||||||
|
|
||||||
Both `Command-` and `Control-` are bound for each, so the same build works on macOS and
|
Both `Command-` and `Control-` are bound for each, so the same build works on macOS and
|
||||||
Windows; menu accelerators render as `Cmd` or `Ctrl` per platform. Tidy Layout is the
|
Windows; menu accelerators render as `Cmd` or `Ctrl` per platform. Tidy Layout is the
|
||||||
@@ -96,6 +99,17 @@ exception, on `Control-` alone: `Cmd` + `W` closes the window on macOS.
|
|||||||
it back at its configured width. ttk has no hide flag for a pane, so the column is dropped
|
it back at its configured width. ttk has no hide flag for a pane, so the column is dropped
|
||||||
from the paned window and re-inserted at the front.
|
from the paned window and re-inserted at the front.
|
||||||
|
|
||||||
|
**Zoom In**, **Zoom Out** and **Zoom 100%** step a ladder of scales, with the two zoom
|
||||||
|
buttons and the percentage sitting beside the navigation arrows in the toolbar. The sheet
|
||||||
|
is painted and routed at 1:1 -- box metrics, wire stubs, lane gaps, clearances and knob
|
||||||
|
radii are all tuned in those pixels -- and `apply_sheet_zoom` scales the finished picture,
|
||||||
|
so a zoomed sheet keeps exactly the proportions of a 1:1 one and the router makes the same
|
||||||
|
decisions at every scale. Text scales because the three box fonts are resized after the
|
||||||
|
boxes were measured with them. `redraw_links()` is what a drag or a drop calls: it puts
|
||||||
|
the boxes back to 1:1 for the routing pass and scales the result again, since the router
|
||||||
|
reads box positions straight off the canvas. Canvas coordinates therefore carry the zoom,
|
||||||
|
which is why the drag, release and drop handlers snap against `meta_scale * zoom`.
|
||||||
|
|
||||||
**Back** and **Forward** walk the sheets opened this session, mirrored by the two arrow
|
**Back** and **Forward** walk the sheets opened this session, mirrored by the two arrow
|
||||||
buttons in the toolbar. The history records the element whose children are painted -- the
|
buttons in the toolbar. The history records the element whose children are painted -- the
|
||||||
same thing `app_state.current_sheet_parent` holds -- rather than a tree item id, because
|
same thing `app_state.current_sheet_parent` holds -- rather than a tree item id, because
|
||||||
|
|||||||
+2
-1
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
|||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
|
||||||
VERSION = "0.0.0.032"
|
VERSION = "0.0.0.033"
|
||||||
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||||
DEFAULT_GRID_SIZE = 10
|
DEFAULT_GRID_SIZE = 10
|
||||||
DEFAULT_ADD_TIMESTAMP = True
|
DEFAULT_ADD_TIMESTAMP = True
|
||||||
@@ -53,6 +53,7 @@ link_mark_element = None # Component marked as a link endpoint via "Link Ma
|
|||||||
selected_link_tag = None # Canvas tag of the currently selected (red-highlighted) link wire
|
selected_link_tag = None # Canvas tag of the currently selected (red-highlighted) link wire
|
||||||
unroutable_link_count = 0 # Links drawn as knobs because no wire could avoid a component
|
unroutable_link_count = 0 # Links drawn as knobs because no wire could avoid a component
|
||||||
meta_scale = DEFAULT_GRID_SIZE # Pixels per meta coordinate step on the painted sheet
|
meta_scale = DEFAULT_GRID_SIZE # Pixels per meta coordinate step on the painted sheet
|
||||||
|
zoom = 1.0 # Scale the open sheet is painted at; 1.0 is one pixel per pixel
|
||||||
knob_link_ends = {} # wire tag -> (x1, y1, x2, y2) for a link shown as two knobs
|
knob_link_ends = {} # wire tag -> (x1, y1, x2, y2) for a link shown as two knobs
|
||||||
canvas_link_map = {} # Canvas link tag -> ET.Element (<link>)
|
canvas_link_map = {} # Canvas link tag -> ET.Element (<link>)
|
||||||
xml_root_element = None # Keep a persistent root reference to redraw tree layouts cleanly
|
xml_root_element = None # Keep a persistent root reference to redraw tree layouts cleanly
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ _history_file_path = None # the file sheet_history was recorded against
|
|||||||
# function that touches them checks first.
|
# function that touches them checks first.
|
||||||
btn_back = None
|
btn_back = None
|
||||||
btn_forward = None
|
btn_forward = None
|
||||||
|
zoom_label = None
|
||||||
menu_view = None
|
menu_view = None
|
||||||
show_left_column_var = None
|
show_left_column_var = None
|
||||||
|
|
||||||
@@ -119,6 +120,71 @@ def do_tidy_layout(event=None):
|
|||||||
return "break"
|
return "break"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Zoom ---
|
||||||
|
|
||||||
|
def update_zoom_label():
|
||||||
|
"""Puts the painted scale on the toolbar as a percentage."""
|
||||||
|
if zoom_label is not None:
|
||||||
|
zoom_label.config(text=f"{round(app_state.zoom * 100)}%")
|
||||||
|
|
||||||
|
|
||||||
|
def zoom_to(level, anchor=None):
|
||||||
|
"""Repaints the sheet at this zoom, holding one point of it still on screen.
|
||||||
|
|
||||||
|
``anchor`` is a canvas-relative (x, y) to keep under the same pixel -- the pointer,
|
||||||
|
for a wheel zoom -- and defaults to the middle of the visible area, so a menu or
|
||||||
|
keyboard zoom closes in on what is already being looked at rather than on the
|
||||||
|
top-left corner.
|
||||||
|
"""
|
||||||
|
canvas = app_state.canvas
|
||||||
|
before = app_state.zoom
|
||||||
|
|
||||||
|
if anchor is None:
|
||||||
|
anchor = (canvas.winfo_width() / 2, canvas.winfo_height() / 2)
|
||||||
|
|
||||||
|
# Where the held point sits on the unscaled sheet.
|
||||||
|
sheet_x = canvas.canvasx(anchor[0]) / before
|
||||||
|
sheet_y = canvas.canvasy(anchor[1]) / before
|
||||||
|
|
||||||
|
after = wiresheet.set_zoom(level)
|
||||||
|
update_zoom_label()
|
||||||
|
if after == before:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Scroll the repainted sheet back under the held point. The scrollregion starts at
|
||||||
|
# the origin, so its far corner is the span the view fractions are measured against.
|
||||||
|
region = str(canvas.cget("scrollregion")).split()
|
||||||
|
if len(region) == 4:
|
||||||
|
span_x = float(region[2]) or 1.0
|
||||||
|
span_y = float(region[3]) or 1.0
|
||||||
|
canvas.xview_moveto(max(0.0, (sheet_x * after - anchor[0]) / span_x))
|
||||||
|
canvas.yview_moveto(max(0.0, (sheet_y * after - anchor[1]) / span_y))
|
||||||
|
|
||||||
|
|
||||||
|
def do_zoom_in(event=None):
|
||||||
|
zoom_to(wiresheet.zoom_step(1))
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
|
||||||
|
def do_zoom_out(event=None):
|
||||||
|
zoom_to(wiresheet.zoom_step(-1))
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
|
||||||
|
def do_zoom_reset(event=None):
|
||||||
|
zoom_to(1.0)
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
|
||||||
|
def on_canvas_wheel_zoom(event):
|
||||||
|
"""Ctrl + wheel over the sheet, the gesture every other editor uses for zoom."""
|
||||||
|
if not event.delta:
|
||||||
|
return "break"
|
||||||
|
step = 1 if event.delta > 0 else -1
|
||||||
|
zoom_to(wiresheet.zoom_step(step), anchor=(event.x, event.y))
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
|
||||||
# --- Event handlers ---
|
# --- Event handlers ---
|
||||||
|
|
||||||
def on_tree_select(event):
|
def on_tree_select(event):
|
||||||
@@ -251,7 +317,7 @@ def on_canvas_click(event):
|
|||||||
|
|
||||||
# Bring the clicked box clear of any neighbour it happens to sit under.
|
# Bring the clicked box clear of any neighbour it happens to sit under.
|
||||||
canvas.tag_raise(group_tag)
|
canvas.tag_raise(group_tag)
|
||||||
wiresheet.draw_links()
|
wiresheet.redraw_links()
|
||||||
|
|
||||||
|
|
||||||
def do_link_mark():
|
def do_link_mark():
|
||||||
@@ -520,13 +586,14 @@ def on_canvas_drag(event):
|
|||||||
canvas.move(group_tag, cx - app_state.drag_data["x"], cy - app_state.drag_data["y"])
|
canvas.move(group_tag, cx - app_state.drag_data["x"], cy - app_state.drag_data["y"])
|
||||||
app_state.drag_data["x"] = cx
|
app_state.drag_data["x"] = cx
|
||||||
app_state.drag_data["y"] = cy
|
app_state.drag_data["y"] = cy
|
||||||
wiresheet.draw_links()
|
wiresheet.redraw_links()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Snap to whole meta steps, which is the grid on an ordinary sheet and a multiple of
|
# Snap to whole meta steps, which is the grid on an ordinary sheet and a multiple of
|
||||||
# it on one too wide for meta's range -- a position between steps cannot be stored.
|
# it on one too wide for meta's range -- a position between steps cannot be stored.
|
||||||
step = app_state.meta_scale
|
# Canvas coordinates carry the sheet's zoom, so the step and the margin do too.
|
||||||
margin = wiresheet.layout_margin(step)
|
step = app_state.meta_scale * app_state.zoom
|
||||||
|
margin = wiresheet.layout_margin(app_state.meta_scale) * app_state.zoom
|
||||||
|
|
||||||
# Follow the pointer, but land the box on a step every step of the way, and keep it
|
# Follow the pointer, but land the box on a step every step of the way, and keep it
|
||||||
# out of the margin strip along the top and left canvas edges.
|
# out of the margin strip along the top and left canvas edges.
|
||||||
@@ -542,7 +609,7 @@ def on_canvas_drag(event):
|
|||||||
canvas.move(group_tag, shift_x, shift_y)
|
canvas.move(group_tag, shift_x, shift_y)
|
||||||
# Wires are not part of the dragged group, so they are re-routed against the
|
# Wires are not part of the dragged group, so they are re-routed against the
|
||||||
# new positions.
|
# new positions.
|
||||||
wiresheet.draw_links()
|
wiresheet.redraw_links()
|
||||||
|
|
||||||
|
|
||||||
def on_canvas_release(event):
|
def on_canvas_release(event):
|
||||||
@@ -550,14 +617,14 @@ def on_canvas_release(event):
|
|||||||
return
|
return
|
||||||
|
|
||||||
canvas = app_state.canvas
|
canvas = app_state.canvas
|
||||||
step = app_state.meta_scale
|
step = app_state.meta_scale * app_state.zoom
|
||||||
group_tag = app_state.drag_data["tag"]
|
group_tag = app_state.drag_data["tag"]
|
||||||
origin = wiresheet.get_box_origin(group_tag)
|
origin = wiresheet.get_box_origin(group_tag)
|
||||||
|
|
||||||
if origin is not None:
|
if origin is not None:
|
||||||
current_x, current_y = origin
|
current_x, current_y = origin
|
||||||
|
|
||||||
margin = wiresheet.layout_margin(step)
|
margin = wiresheet.layout_margin(app_state.meta_scale) * app_state.zoom
|
||||||
snapped_x = max(margin, round(current_x / step) * step)
|
snapped_x = max(margin, round(current_x / step) * step)
|
||||||
snapped_y = max(margin, round(current_y / step) * step)
|
snapped_y = max(margin, round(current_y / step) * step)
|
||||||
|
|
||||||
@@ -589,7 +656,7 @@ def on_canvas_release(event):
|
|||||||
app_state.mark_dirty()
|
app_state.mark_dirty()
|
||||||
|
|
||||||
app_state.drag_data["tag"] = None
|
app_state.drag_data["tag"] = None
|
||||||
wiresheet.draw_links()
|
wiresheet.redraw_links()
|
||||||
|
|
||||||
|
|
||||||
def fit_tree_column(tree):
|
def fit_tree_column(tree):
|
||||||
@@ -1174,6 +1241,16 @@ SHORTCUTS = [
|
|||||||
("z", do_undo),
|
("z", do_undo),
|
||||||
("y", do_redo),
|
("y", do_redo),
|
||||||
("b", toggle_left_column),
|
("b", toggle_left_column),
|
||||||
|
# Zoom, in every spelling the key can arrive as: with and without Shift, and on the
|
||||||
|
# numeric keypad.
|
||||||
|
("plus", do_zoom_in),
|
||||||
|
("equal", do_zoom_in),
|
||||||
|
("KP_Add", do_zoom_in),
|
||||||
|
("minus", do_zoom_out),
|
||||||
|
("underscore", do_zoom_out),
|
||||||
|
("KP_Subtract", do_zoom_out),
|
||||||
|
("0", do_zoom_reset),
|
||||||
|
("KP_0", do_zoom_reset),
|
||||||
]
|
]
|
||||||
for key, handler in SHORTCUTS:
|
for key, handler in SHORTCUTS:
|
||||||
window.bind_all(f"<Command-{key}>", handler)
|
window.bind_all(f"<Command-{key}>", handler)
|
||||||
@@ -1233,6 +1310,10 @@ menu_view.add_command(
|
|||||||
label=MENU_FORWARD_LABEL, command=go_forward, accelerator="Alt+Right", state="disabled"
|
label=MENU_FORWARD_LABEL, command=go_forward, accelerator="Alt+Right", state="disabled"
|
||||||
)
|
)
|
||||||
menu_view.add_separator()
|
menu_view.add_separator()
|
||||||
|
menu_view.add_command(label="Zoom In", command=do_zoom_in, accelerator=f"{accel_mod}++")
|
||||||
|
menu_view.add_command(label="Zoom Out", command=do_zoom_out, accelerator=f"{accel_mod}+-")
|
||||||
|
menu_view.add_command(label="Zoom 100%", command=do_zoom_reset, accelerator=f"{accel_mod}+0")
|
||||||
|
menu_view.add_separator()
|
||||||
menu_view.add_command(label="Tidy Layout", command=do_tidy_layout, accelerator="Ctrl+W")
|
menu_view.add_command(label="Tidy Layout", command=do_tidy_layout, accelerator="Ctrl+W")
|
||||||
menu_bar.add_cascade(label="View", menu=menu_view)
|
menu_bar.add_cascade(label="View", menu=menu_view)
|
||||||
|
|
||||||
@@ -1269,6 +1350,13 @@ btn_back.pack(side=tk.LEFT)
|
|||||||
btn_forward = ttk.Button(toolbar, text="▶", width=3, command=go_forward, state="disabled")
|
btn_forward = ttk.Button(toolbar, text="▶", width=3, command=go_forward, state="disabled")
|
||||||
btn_forward.pack(side=tk.LEFT, padx=(3, 0))
|
btn_forward.pack(side=tk.LEFT, padx=(3, 0))
|
||||||
|
|
||||||
|
ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=8, pady=2)
|
||||||
|
|
||||||
|
ttk.Button(toolbar, text="−", width=3, command=do_zoom_out).pack(side=tk.LEFT)
|
||||||
|
zoom_label = ttk.Label(toolbar, text="100%", width=6, anchor="center")
|
||||||
|
zoom_label.pack(side=tk.LEFT, padx=1)
|
||||||
|
ttk.Button(toolbar, text="+", width=3, command=do_zoom_in).pack(side=tk.LEFT)
|
||||||
|
|
||||||
paned_window = ttk.PanedWindow(window, orient=tk.HORIZONTAL)
|
paned_window = ttk.PanedWindow(window, orient=tk.HORIZONTAL)
|
||||||
paned_window.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
paned_window.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
@@ -1461,8 +1549,8 @@ def on_palette_release(event):
|
|||||||
drop_x = canvas.canvasx(event.x_root - canvas.winfo_rootx())
|
drop_x = canvas.canvasx(event.x_root - canvas.winfo_rootx())
|
||||||
drop_y = canvas.canvasy(event.y_root - canvas.winfo_rooty())
|
drop_y = canvas.canvasy(event.y_root - canvas.winfo_rooty())
|
||||||
|
|
||||||
step = app_state.meta_scale
|
step = app_state.meta_scale * app_state.zoom
|
||||||
min_step = wiresheet.layout_margin(step) // step
|
min_step = wiresheet.layout_margin(app_state.meta_scale) // app_state.meta_scale
|
||||||
grid_x = max(min_step, min(wiresheet.META_COORD_MAX, round(drop_x / step)))
|
grid_x = max(min_step, min(wiresheet.META_COORD_MAX, round(drop_x / step)))
|
||||||
grid_y = max(min_step, min(wiresheet.META_COORD_MAX, round(drop_y / step)))
|
grid_y = max(min_step, min(wiresheet.META_COORD_MAX, round(drop_y / step)))
|
||||||
|
|
||||||
@@ -1511,6 +1599,11 @@ canvas.bind("<Configure>", lambda e: wiresheet.draw_grid())
|
|||||||
canvas.bind("<ButtonPress-3>", on_canvas_right_click)
|
canvas.bind("<ButtonPress-3>", on_canvas_right_click)
|
||||||
canvas.bind("<ButtonPress-2>", on_canvas_right_click)
|
canvas.bind("<ButtonPress-2>", on_canvas_right_click)
|
||||||
|
|
||||||
|
# Wheel zoom. Windows and macOS report a delta; X11 sends button 4 and 5 instead.
|
||||||
|
canvas.bind("<Control-MouseWheel>", on_canvas_wheel_zoom)
|
||||||
|
canvas.bind("<Control-Button-4>", do_zoom_in)
|
||||||
|
canvas.bind("<Control-Button-5>", do_zoom_out)
|
||||||
|
|
||||||
paned_window.add(frame_editor, weight=3)
|
paned_window.add(frame_editor, weight=3)
|
||||||
|
|
||||||
undo.set_refresh_callback(refresh_all_views)
|
undo.set_refresh_callback(refresh_all_views)
|
||||||
|
|||||||
+110
-11
@@ -58,6 +58,15 @@ META_SCALE_MAX = 12
|
|||||||
TIDY_GAP = 30 # minimum clear space kept between two boxes
|
TIDY_GAP = 30 # minimum clear space kept between two boxes
|
||||||
TIDY_MARGIN = 30 # minimum clear space kept from the top and left canvas edges
|
TIDY_MARGIN = 30 # minimum clear space kept from the top and left canvas edges
|
||||||
|
|
||||||
|
# The scales the sheet can be painted at. A ladder rather than a free multiplier, so
|
||||||
|
# stepping out and back in lands on 1.0 exactly instead of on 0.9999, and so the labels
|
||||||
|
# are the round percentages people expect.
|
||||||
|
ZOOM_LEVELS = (0.5, 0.67, 0.8, 1.0, 1.25, 1.5, 2.0, 3.0)
|
||||||
|
|
||||||
|
# Below this a grid line is closer to its neighbour than the line is wide, which reads
|
||||||
|
# as a grey wash rather than a grid, so the grid is left off instead.
|
||||||
|
MIN_GRID_STEP = 3
|
||||||
|
|
||||||
|
|
||||||
def snap_up(value, grid_size):
|
def snap_up(value, grid_size):
|
||||||
"""Smallest multiple of grid_size that is >= value; grid lines sit on multiples.
|
"""Smallest multiple of grid_size that is >= value; grid lines sit on multiples.
|
||||||
@@ -115,12 +124,18 @@ def fit_text(text, font, max_px):
|
|||||||
return (trimmed + ellipsis) if trimmed else ellipsis
|
return (trimmed + ellipsis) if trimmed else ellipsis
|
||||||
|
|
||||||
|
|
||||||
|
# Name, type and slot text, at 1:1. Zoom scales the fonts from these, which is why the
|
||||||
|
# sizes are named here rather than written into sheet_fonts alone.
|
||||||
|
FONT_SIZES = (9, 7, 8)
|
||||||
|
|
||||||
|
|
||||||
def sheet_fonts():
|
def sheet_fonts():
|
||||||
"""The three fonts a component box is drawn with. Box sizes are measured from these."""
|
"""The three fonts a component box is drawn with. Box sizes are measured from these."""
|
||||||
|
name_size, type_size, slot_size = FONT_SIZES
|
||||||
return (
|
return (
|
||||||
tkfont.Font(family="Segoe UI", size=9, weight="bold"),
|
tkfont.Font(family="Segoe UI", size=name_size, weight="bold"),
|
||||||
tkfont.Font(family="Segoe UI", size=7),
|
tkfont.Font(family="Segoe UI", size=type_size),
|
||||||
tkfont.Font(family="Segoe UI", size=8),
|
tkfont.Font(family="Segoe UI", size=slot_size),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -167,17 +182,24 @@ def measure_box(comp, fonts, linked_slot_index):
|
|||||||
|
|
||||||
|
|
||||||
def draw_grid():
|
def draw_grid():
|
||||||
"""Draws a grid onto the Canvas based on the scrollable region."""
|
"""Draws a grid onto the Canvas based on the scrollable region.
|
||||||
|
|
||||||
|
Drawn at the sheet's zoom, so a box still lands on a line at every scale. The area
|
||||||
|
covered is the same patch of sheet whatever the zoom, which is why the span is
|
||||||
|
scaled along with the step.
|
||||||
|
"""
|
||||||
canvas = app_state.canvas
|
canvas = app_state.canvas
|
||||||
canvas.delete("grid")
|
canvas.delete("grid")
|
||||||
|
|
||||||
width = 3000
|
step = app_state.GRID_SIZE * app_state.zoom
|
||||||
height = 3000
|
if step < MIN_GRID_STEP:
|
||||||
|
return
|
||||||
|
|
||||||
for x in range(0, width, app_state.GRID_SIZE):
|
span = 3000 * app_state.zoom
|
||||||
canvas.create_line(x, 0, x, height, fill="#e0e0e0", tags="grid")
|
for index in range(int(span // step) + 1):
|
||||||
for y in range(0, height, app_state.GRID_SIZE):
|
offset = index * step
|
||||||
canvas.create_line(0, y, width, y, fill="#e0e0e0", tags="grid")
|
canvas.create_line(offset, 0, offset, span, fill="#e0e0e0", tags="grid")
|
||||||
|
canvas.create_line(0, offset, span, offset, fill="#e0e0e0", tags="grid")
|
||||||
|
|
||||||
canvas.tag_lower("grid")
|
canvas.tag_lower("grid")
|
||||||
|
|
||||||
@@ -926,8 +948,85 @@ def render_components(components_to_render):
|
|||||||
fill="#b8b8b8", tags=("component", comp_tag)
|
fill="#b8b8b8", tags=("component", comp_tag)
|
||||||
)
|
)
|
||||||
|
|
||||||
canvas.config(scrollregion=(0, 0, max_reached_x + 200, max_reached_y + 200))
|
|
||||||
draw_links()
|
draw_links()
|
||||||
|
apply_sheet_zoom(fonts, max_reached_x + 200, max_reached_y + 200)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_sheet_zoom(fonts, extent_x, extent_y):
|
||||||
|
"""Scales the finished sheet to app_state.zoom and sizes the canvas to match.
|
||||||
|
|
||||||
|
Painting and routing both work at 1:1, because that is where every metric here is
|
||||||
|
tuned -- box header and row heights, wire stubs, lane gaps, knob radii -- so zoom is
|
||||||
|
applied to the finished picture rather than threaded through each of them. Text
|
||||||
|
scales with it because the three fonts the boxes were drawn with are resized after
|
||||||
|
they were measured with, which is what keeps a zoomed box in exactly the proportions
|
||||||
|
of a 1:1 one.
|
||||||
|
"""
|
||||||
|
canvas = app_state.canvas
|
||||||
|
zoom = app_state.zoom
|
||||||
|
|
||||||
|
if zoom != 1.0:
|
||||||
|
canvas.scale("component", 0, 0, zoom, zoom)
|
||||||
|
canvas.scale("link", 0, 0, zoom, zoom)
|
||||||
|
|
||||||
|
for font, base_size in zip(fonts, FONT_SIZES):
|
||||||
|
font.configure(size=max(1, round(base_size * zoom)))
|
||||||
|
|
||||||
|
scale_knob_ends(zoom)
|
||||||
|
|
||||||
|
canvas.config(scrollregion=(0, 0, extent_x * zoom, extent_y * zoom))
|
||||||
|
draw_grid()
|
||||||
|
|
||||||
|
|
||||||
|
def scale_knob_ends(zoom):
|
||||||
|
"""Brings the cached knob-to-knob ends onto the painted scale.
|
||||||
|
|
||||||
|
They are recorded while the wires are routed, which happens at 1:1, and are read
|
||||||
|
back at click time to draw the dashed line across a selected pair of knobs.
|
||||||
|
"""
|
||||||
|
for wire_tag, ends in app_state.knob_link_ends.items():
|
||||||
|
app_state.knob_link_ends[wire_tag] = tuple(value * zoom for value in ends)
|
||||||
|
|
||||||
|
|
||||||
|
def redraw_links():
|
||||||
|
"""Re-routes the wires of a sheet that is already painted, at whatever zoom it is at.
|
||||||
|
|
||||||
|
The router reads box positions straight off the canvas and measures its clearances
|
||||||
|
in 1:1 pixels, so the boxes are put back to that scale for the duration and the
|
||||||
|
whole picture, new wires included, is scaled again afterwards. Callers that have
|
||||||
|
just moved a box -- a drag, a drop -- want this rather than draw_links.
|
||||||
|
"""
|
||||||
|
zoom = app_state.zoom
|
||||||
|
if zoom == 1.0:
|
||||||
|
draw_links()
|
||||||
|
return
|
||||||
|
|
||||||
|
canvas = app_state.canvas
|
||||||
|
canvas.scale("component", 0, 0, 1.0 / zoom, 1.0 / zoom)
|
||||||
|
draw_links()
|
||||||
|
canvas.scale("component", 0, 0, zoom, zoom)
|
||||||
|
canvas.scale("link", 0, 0, zoom, zoom)
|
||||||
|
scale_knob_ends(zoom)
|
||||||
|
|
||||||
|
|
||||||
|
def zoom_step(direction):
|
||||||
|
"""The next zoom level in (+1) or out (-1); the current one at either end of the ladder."""
|
||||||
|
current = app_state.zoom
|
||||||
|
nearest = min(range(len(ZOOM_LEVELS)), key=lambda i: abs(ZOOM_LEVELS[i] - current))
|
||||||
|
return ZOOM_LEVELS[min(max(nearest + direction, 0), len(ZOOM_LEVELS) - 1)]
|
||||||
|
|
||||||
|
|
||||||
|
def set_zoom(level):
|
||||||
|
"""Repaints the open sheet at this scale. Returns the level actually applied."""
|
||||||
|
level = min(max(level, ZOOM_LEVELS[0]), ZOOM_LEVELS[-1])
|
||||||
|
if level != app_state.zoom:
|
||||||
|
app_state.zoom = level
|
||||||
|
if app_state.xml_root_element is None:
|
||||||
|
# Nothing painted yet, but the grid is already on screen.
|
||||||
|
draw_grid()
|
||||||
|
else:
|
||||||
|
render_current_sheet()
|
||||||
|
return app_state.zoom
|
||||||
|
|
||||||
|
|
||||||
def render_current_sheet():
|
def render_current_sheet():
|
||||||
|
|||||||
Reference in New Issue
Block a user