host.controller_config.editor_utils
Shared utilities for the dialog-based curve editors and widgets.
Contains constants, coordinate helpers, grid drawing, undo stack, and math utilities shared across spline_editor, segment_editor, curve_editor_widget, and preview_widget.
1"""Shared utilities for the dialog-based curve editors and widgets. 2 3Contains constants, coordinate helpers, grid drawing, undo stack, 4and math utilities shared across spline_editor, segment_editor, 5curve_editor_widget, and preview_widget. 6""" 7 8import math 9from copy import deepcopy 10 11from host.controller_config.colors import ( 12 GRID_AXIS, 13 GRID_MAJOR, 14 GRID_MINOR, 15 LABEL_COLOR, 16) 17 18 19# --------------------------------------------------------------------------- 20# Dialog canvas constants (spline_editor + segment_editor) 21# --------------------------------------------------------------------------- 22 23DIALOG_W = 600 24DIALOG_H = 600 25DIALOG_MARGIN = 50 26DIALOG_PLOT_W = DIALOG_W - 2 * DIALOG_MARGIN 27DIALOG_PLOT_H = DIALOG_H - 2 * DIALOG_MARGIN 28 29# Border outline for editor canvases 30_BORDER_OUTLINE = "#808080" 31 32 33# --------------------------------------------------------------------------- 34# Grid drawing 35# --------------------------------------------------------------------------- 36 37def draw_editor_grid(canvas, d2c_fn, margin, plot_w, plot_h, 38 canvas_w, canvas_h, 39 x_min=-1.0, x_max=1.0, 40 y_min=-1.0, y_max=1.0): 41 """Draw the grid with axis labels for a dialog editor. 42 43 Args: 44 canvas: tk.Canvas to draw on. 45 d2c_fn: callable(x, y) -> (cx, cy) mapping data to canvas coords. 46 margin: pixel margin around the plot area. 47 plot_w: plot area width in pixels. 48 plot_h: plot area height in pixels. 49 canvas_w: total canvas width in pixels. 50 canvas_h: total canvas height in pixels. 51 x_min: minimum X data value (default -1.0). 52 x_max: maximum X data value (default 1.0). 53 y_min: minimum Y data value (default -1.0). 54 y_max: maximum Y data value (default 1.0). 55 """ 56 x_step = nice_grid_step(x_max - x_min) 57 y_step = nice_grid_step(y_max - y_min) 58 59 # Vertical grid lines (X axis) 60 v = math.ceil(x_min / (x_step / 2)) * (x_step / 2) 61 while v <= x_max + 1e-9: 62 cx, _ = d2c_fn(v, 0) 63 is_axis = abs(v) < 1e-9 64 is_major = abs(round(v / x_step) * x_step - v) < 1e-9 65 color = GRID_AXIS if is_axis else (GRID_MAJOR if is_major else GRID_MINOR) 66 w = 2 if is_axis else 1 67 canvas.create_line(cx, margin, cx, margin + plot_h, 68 fill=color, width=w) 69 v += x_step / 2 70 71 # Horizontal grid lines (Y axis) 72 v = math.ceil(y_min / (y_step / 2)) * (y_step / 2) 73 while v <= y_max + 1e-9: 74 _, cy = d2c_fn(0, v) 75 is_axis = abs(v) < 1e-9 76 is_major = abs(round(v / y_step) * y_step - v) < 1e-9 77 color = GRID_AXIS if is_axis else (GRID_MAJOR if is_major else GRID_MINOR) 78 w = 2 if is_axis else 1 79 canvas.create_line(margin, cy, margin + plot_w, cy, 80 fill=color, width=w) 81 v += y_step / 2 82 83 # X-axis labels (at major steps) 84 v = math.ceil(x_min / x_step) * x_step 85 while v <= x_max + 1e-9: 86 cx, _ = d2c_fn(v, 0) 87 canvas.create_text(cx, margin + plot_h + 15, 88 text=f"{v:g}", fill=LABEL_COLOR, 89 font=("TkDefaultFont", 8)) 90 v += x_step 91 92 # Y-axis labels (at major steps) 93 v = math.ceil(y_min / y_step) * y_step 94 while v <= y_max + 1e-9: 95 _, cy = d2c_fn(0, v) 96 canvas.create_text(margin - 22, cy, 97 text=f"{v:g}", fill=LABEL_COLOR, 98 font=("TkDefaultFont", 8)) 99 v += y_step 100 101 canvas.create_text(canvas_w / 2, canvas_h - 5, 102 text="Input", fill=LABEL_COLOR, 103 font=("TkDefaultFont", 9)) 104 canvas.create_text(12, canvas_h / 2, text="Output", 105 fill=LABEL_COLOR, font=("TkDefaultFont", 9), angle=90) 106 canvas.create_rectangle(margin, margin, 107 margin + plot_w, margin + plot_h, 108 outline=_BORDER_OUTLINE) 109 110 111# --------------------------------------------------------------------------- 112# Undo stack 113# --------------------------------------------------------------------------- 114 115class UndoStack: 116 """Simple deepcopy-based undo stack with a fixed capacity.""" 117 118 def __init__(self, max_size=30): 119 self._stack: list = [] 120 self._max = max_size 121 122 def push(self, state): 123 """Save a deepcopy of state onto the stack.""" 124 self._stack.append(deepcopy(state)) 125 if len(self._stack) > self._max: 126 self._stack.pop(0) 127 128 def pop(self): 129 """Restore and return the most recent state, or None if empty.""" 130 if not self._stack: 131 return None 132 return self._stack.pop() 133 134 def clear(self): 135 """Remove all entries from the stack.""" 136 self._stack.clear() 137 138 def __len__(self): 139 return len(self._stack) 140 141 142# --------------------------------------------------------------------------- 143# Grid step calculation 144# --------------------------------------------------------------------------- 145 146def nice_grid_step(span: float) -> float: 147 """Choose a nice gridline step for the given data span. 148 149 Aims for approximately 4 gridlines across the span. 150 """ 151 if span <= 0: 152 return 0.5 153 raw = span / 4 # aim for ~4 gridlines 154 mag = 10 ** math.floor(math.log10(raw)) 155 norm = raw / mag 156 if norm < 1.5: 157 return mag 158 elif norm < 3.5: 159 return 2 * mag 160 elif norm < 7.5: 161 return 5 * mag 162 else: 163 return 10 * mag
38def draw_editor_grid(canvas, d2c_fn, margin, plot_w, plot_h, 39 canvas_w, canvas_h, 40 x_min=-1.0, x_max=1.0, 41 y_min=-1.0, y_max=1.0): 42 """Draw the grid with axis labels for a dialog editor. 43 44 Args: 45 canvas: tk.Canvas to draw on. 46 d2c_fn: callable(x, y) -> (cx, cy) mapping data to canvas coords. 47 margin: pixel margin around the plot area. 48 plot_w: plot area width in pixels. 49 plot_h: plot area height in pixels. 50 canvas_w: total canvas width in pixels. 51 canvas_h: total canvas height in pixels. 52 x_min: minimum X data value (default -1.0). 53 x_max: maximum X data value (default 1.0). 54 y_min: minimum Y data value (default -1.0). 55 y_max: maximum Y data value (default 1.0). 56 """ 57 x_step = nice_grid_step(x_max - x_min) 58 y_step = nice_grid_step(y_max - y_min) 59 60 # Vertical grid lines (X axis) 61 v = math.ceil(x_min / (x_step / 2)) * (x_step / 2) 62 while v <= x_max + 1e-9: 63 cx, _ = d2c_fn(v, 0) 64 is_axis = abs(v) < 1e-9 65 is_major = abs(round(v / x_step) * x_step - v) < 1e-9 66 color = GRID_AXIS if is_axis else (GRID_MAJOR if is_major else GRID_MINOR) 67 w = 2 if is_axis else 1 68 canvas.create_line(cx, margin, cx, margin + plot_h, 69 fill=color, width=w) 70 v += x_step / 2 71 72 # Horizontal grid lines (Y axis) 73 v = math.ceil(y_min / (y_step / 2)) * (y_step / 2) 74 while v <= y_max + 1e-9: 75 _, cy = d2c_fn(0, v) 76 is_axis = abs(v) < 1e-9 77 is_major = abs(round(v / y_step) * y_step - v) < 1e-9 78 color = GRID_AXIS if is_axis else (GRID_MAJOR if is_major else GRID_MINOR) 79 w = 2 if is_axis else 1 80 canvas.create_line(margin, cy, margin + plot_w, cy, 81 fill=color, width=w) 82 v += y_step / 2 83 84 # X-axis labels (at major steps) 85 v = math.ceil(x_min / x_step) * x_step 86 while v <= x_max + 1e-9: 87 cx, _ = d2c_fn(v, 0) 88 canvas.create_text(cx, margin + plot_h + 15, 89 text=f"{v:g}", fill=LABEL_COLOR, 90 font=("TkDefaultFont", 8)) 91 v += x_step 92 93 # Y-axis labels (at major steps) 94 v = math.ceil(y_min / y_step) * y_step 95 while v <= y_max + 1e-9: 96 _, cy = d2c_fn(0, v) 97 canvas.create_text(margin - 22, cy, 98 text=f"{v:g}", fill=LABEL_COLOR, 99 font=("TkDefaultFont", 8)) 100 v += y_step 101 102 canvas.create_text(canvas_w / 2, canvas_h - 5, 103 text="Input", fill=LABEL_COLOR, 104 font=("TkDefaultFont", 9)) 105 canvas.create_text(12, canvas_h / 2, text="Output", 106 fill=LABEL_COLOR, font=("TkDefaultFont", 9), angle=90) 107 canvas.create_rectangle(margin, margin, 108 margin + plot_w, margin + plot_h, 109 outline=_BORDER_OUTLINE)
Draw the grid with axis labels for a dialog editor.
Args: canvas: tk.Canvas to draw on. d2c_fn: callable(x, y) -> (cx, cy) mapping data to canvas coords. margin: pixel margin around the plot area. plot_w: plot area width in pixels. plot_h: plot area height in pixels. canvas_w: total canvas width in pixels. canvas_h: total canvas height in pixels. x_min: minimum X data value (default -1.0). x_max: maximum X data value (default 1.0). y_min: minimum Y data value (default -1.0). y_max: maximum Y data value (default 1.0).
116class UndoStack: 117 """Simple deepcopy-based undo stack with a fixed capacity.""" 118 119 def __init__(self, max_size=30): 120 self._stack: list = [] 121 self._max = max_size 122 123 def push(self, state): 124 """Save a deepcopy of state onto the stack.""" 125 self._stack.append(deepcopy(state)) 126 if len(self._stack) > self._max: 127 self._stack.pop(0) 128 129 def pop(self): 130 """Restore and return the most recent state, or None if empty.""" 131 if not self._stack: 132 return None 133 return self._stack.pop() 134 135 def clear(self): 136 """Remove all entries from the stack.""" 137 self._stack.clear() 138 139 def __len__(self): 140 return len(self._stack)
Simple deepcopy-based undo stack with a fixed capacity.
123 def push(self, state): 124 """Save a deepcopy of state onto the stack.""" 125 self._stack.append(deepcopy(state)) 126 if len(self._stack) > self._max: 127 self._stack.pop(0)
Save a deepcopy of state onto the stack.
147def nice_grid_step(span: float) -> float: 148 """Choose a nice gridline step for the given data span. 149 150 Aims for approximately 4 gridlines across the span. 151 """ 152 if span <= 0: 153 return 0.5 154 raw = span / 4 # aim for ~4 gridlines 155 mag = 10 ** math.floor(math.log10(raw)) 156 norm = raw / mag 157 if norm < 1.5: 158 return mag 159 elif norm < 3.5: 160 return 2 * mag 161 elif norm < 7.5: 162 return 5 * mag 163 else: 164 return 10 * mag
Choose a nice gridline step for the given data span.
Aims for approximately 4 gridlines across the span.