figrecipe 0.5.0__py3-none-any.whl → 0.7.4__py3-none-any.whl

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.
Files changed (189) hide show
  1. figrecipe/__init__.py +220 -819
  2. figrecipe/_api/__init__.py +48 -0
  3. figrecipe/_api/_extract.py +108 -0
  4. figrecipe/_api/_notebook.py +61 -0
  5. figrecipe/_api/_panel.py +46 -0
  6. figrecipe/_api/_save.py +191 -0
  7. figrecipe/_api/_seaborn_proxy.py +34 -0
  8. figrecipe/_api/_style_manager.py +153 -0
  9. figrecipe/_api/_subplots.py +333 -0
  10. figrecipe/_api/_validate.py +82 -0
  11. figrecipe/_dev/__init__.py +29 -0
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +64 -0
  15. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  16. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  17. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  18. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  19. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  20. figrecipe/_dev/demo_plotters/bar_categorical/plot_bar.py +25 -0
  21. figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
  22. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
  24. figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
  25. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
  26. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
  27. figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
  28. figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
  29. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  30. figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
  31. figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
  32. figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
  33. figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
  34. figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
  35. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  36. figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
  37. figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
  38. figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
  39. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
  40. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
  41. figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
  42. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  43. figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
  44. figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
  45. figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
  46. figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
  47. figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
  48. figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
  49. figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
  50. figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
  51. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
  53. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
  55. figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
  56. figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
  57. figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
  58. figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
  59. figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
  60. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  61. figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
  62. figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
  63. figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
  64. figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
  65. figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
  66. figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
  67. figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
  68. figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
  69. figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
  70. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  71. figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
  72. figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
  73. figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
  74. figrecipe/_editor/__init__.py +278 -0
  75. figrecipe/_editor/_bbox/__init__.py +43 -0
  76. figrecipe/_editor/_bbox/_collections.py +177 -0
  77. figrecipe/_editor/_bbox/_elements.py +159 -0
  78. figrecipe/_editor/_bbox/_extract.py +256 -0
  79. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  80. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  81. figrecipe/_editor/_bbox/_lines.py +173 -0
  82. figrecipe/_editor/_bbox/_transforms.py +146 -0
  83. figrecipe/_editor/_flask_app.py +258 -0
  84. figrecipe/_editor/_helpers.py +242 -0
  85. figrecipe/_editor/_hitmap/__init__.py +76 -0
  86. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  87. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  88. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  89. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  90. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  91. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  92. figrecipe/_editor/_hitmap/_colors.py +181 -0
  93. figrecipe/_editor/_hitmap/_detect.py +137 -0
  94. figrecipe/_editor/_hitmap/_restore.py +154 -0
  95. figrecipe/_editor/_hitmap_main.py +182 -0
  96. figrecipe/_editor/_overrides.py +318 -0
  97. figrecipe/_editor/_preferences.py +135 -0
  98. figrecipe/_editor/_render_overrides.py +480 -0
  99. figrecipe/_editor/_renderer.py +199 -0
  100. figrecipe/_editor/_routes_axis.py +453 -0
  101. figrecipe/_editor/_routes_core.py +284 -0
  102. figrecipe/_editor/_routes_element.py +317 -0
  103. figrecipe/_editor/_routes_style.py +223 -0
  104. figrecipe/_editor/_templates/__init__.py +152 -0
  105. figrecipe/_editor/_templates/_html.py +502 -0
  106. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  107. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  108. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  109. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  110. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  111. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  112. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  113. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  114. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  115. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  116. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  117. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  118. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  119. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  120. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  121. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  122. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  123. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  124. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  125. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  126. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  127. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  128. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  129. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  130. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  131. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  132. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  133. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  134. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  135. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  136. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  137. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  138. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  139. figrecipe/_params/_DECORATION_METHODS.py +33 -0
  140. figrecipe/_params/_PLOTTING_METHODS.py +58 -0
  141. figrecipe/_params/__init__.py +9 -0
  142. figrecipe/_recorder.py +92 -110
  143. figrecipe/_recorder_utils.py +124 -0
  144. figrecipe/_reproducer/__init__.py +18 -0
  145. figrecipe/_reproducer/_core.py +498 -0
  146. figrecipe/_reproducer/_custom_plots.py +279 -0
  147. figrecipe/_reproducer/_seaborn.py +100 -0
  148. figrecipe/_reproducer/_violin.py +186 -0
  149. figrecipe/_seaborn.py +14 -9
  150. figrecipe/_serializer.py +2 -2
  151. figrecipe/_signatures/README.md +68 -0
  152. figrecipe/_signatures/__init__.py +12 -2
  153. figrecipe/_signatures/_kwargs.py +273 -0
  154. figrecipe/_signatures/_loader.py +114 -57
  155. figrecipe/_signatures/_parsing.py +147 -0
  156. figrecipe/_utils/__init__.py +6 -4
  157. figrecipe/_utils/_crop.py +10 -4
  158. figrecipe/_utils/_image_diff.py +37 -33
  159. figrecipe/_utils/_numpy_io.py +0 -1
  160. figrecipe/_utils/_units.py +11 -3
  161. figrecipe/_validator.py +12 -3
  162. figrecipe/_wrappers/_axes.py +193 -170
  163. figrecipe/_wrappers/_axes_helpers.py +136 -0
  164. figrecipe/_wrappers/_axes_plots.py +418 -0
  165. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  166. figrecipe/_wrappers/_figure.py +277 -18
  167. figrecipe/_wrappers/_panel_labels.py +127 -0
  168. figrecipe/_wrappers/_plot_helpers.py +143 -0
  169. figrecipe/_wrappers/_violin_helpers.py +180 -0
  170. figrecipe/plt.py +0 -1
  171. figrecipe/pyplot.py +2 -1
  172. figrecipe/styles/__init__.py +12 -11
  173. figrecipe/styles/_dotdict.py +72 -0
  174. figrecipe/styles/_finalize.py +134 -0
  175. figrecipe/styles/_fonts.py +77 -0
  176. figrecipe/styles/_kwargs_converter.py +178 -0
  177. figrecipe/styles/_plot_styles.py +209 -0
  178. figrecipe/styles/_style_applier.py +60 -202
  179. figrecipe/styles/_style_loader.py +73 -121
  180. figrecipe/styles/_themes.py +151 -0
  181. figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
  182. figrecipe/styles/presets/SCITEX.yaml +181 -0
  183. figrecipe-0.7.4.dist-info/METADATA +429 -0
  184. figrecipe-0.7.4.dist-info/RECORD +188 -0
  185. figrecipe/_reproducer.py +0 -358
  186. figrecipe-0.5.0.dist-info/METADATA +0 -336
  187. figrecipe-0.5.0.dist-info/RECORD +0 -26
  188. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  189. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,436 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Core state, initialization, and utility JavaScript for the figure editor."""
4
+
5
+ SCRIPTS_CORE = """
6
+ // ==================== CORE STATE & INITIALIZATION ====================
7
+
8
+ // State
9
+ let currentBboxes = initialBboxes;
10
+ let colorMap = initialColorMap;
11
+ let callsData = {}; // Recorded calls with signatures
12
+ let selectedElement = null;
13
+ let hitmapLoaded = false;
14
+ let hitmapCtx = null;
15
+ let hitmapImg = null;
16
+ let updateTimeout = null;
17
+ let currentImgWidth = imgWidth; // Track current preview dimensions
18
+ let currentImgHeight = imgHeight;
19
+ let hitmapVisible = false; // Hitmap overlay visibility (hover-only by default)
20
+ const UPDATE_DEBOUNCE = 500; // ms
21
+
22
+ // Overlapping element cycling state
23
+ let lastClickPosition = null;
24
+ let overlappingElements = [];
25
+ let cycleIndex = 0;
26
+ let hoveredElement = null; // Track currently hovered element for click priority
27
+
28
+ // View mode: 'all' shows all properties, 'selected' shows only element-specific
29
+ let viewMode = 'all';
30
+
31
+ // Zoom/Pan state
32
+ let zoomLevel = 1.0;
33
+ const ZOOM_MIN = 0.1;
34
+ const ZOOM_MAX = 5.0;
35
+ const ZOOM_STEP = 0.25;
36
+ let isPanning = false;
37
+ let panStartX = 0;
38
+ let panStartY = 0;
39
+ let scrollStartX = 0;
40
+ let scrollStartY = 0;
41
+
42
+ // Initialize
43
+ document.addEventListener('DOMContentLoaded', function() {
44
+ initializeValues();
45
+ initializeEventListeners();
46
+ loadHitmap();
47
+ loadLabels(); // Load current axis labels
48
+
49
+ // Update hit regions on window resize
50
+ window.addEventListener('resize', updateHitRegions);
51
+
52
+ // Update hit regions and overlays when preview image loads
53
+ const previewImg = document.getElementById('preview-image');
54
+ previewImg.addEventListener('load', updateHitRegions);
55
+ previewImg.addEventListener('load', updateOverlays);
56
+
57
+ // Initialize hit regions visibility state
58
+ const overlay = document.getElementById('hitregion-overlay');
59
+ const btn = document.getElementById('btn-show-hitmap');
60
+
61
+ if (hitmapVisible) {
62
+ if (overlay) overlay.classList.add('visible');
63
+ if (btn) {
64
+ btn.classList.add('active');
65
+ btn.textContent = 'Hide Hit Regions';
66
+ }
67
+ } else {
68
+ // Hover-only mode when hidden
69
+ if (overlay) overlay.classList.add('hover-mode');
70
+ }
71
+
72
+ // Draw hit regions - handle both already-loaded and loading images
73
+ function initHitRegions() {
74
+ if (previewImg.complete && previewImg.naturalWidth > 0) {
75
+ console.log('Image already loaded, drawing hit regions');
76
+ drawHitRegions();
77
+ } else {
78
+ console.log('Image not loaded yet, waiting...');
79
+ setTimeout(initHitRegions, 100);
80
+ }
81
+ }
82
+ setTimeout(initHitRegions, 50);
83
+
84
+ // Initialize zoom/pan
85
+ initializeZoomPan();
86
+
87
+ // Initialize measurement overlay controls
88
+ initializeOverlayControls();
89
+ });
90
+
91
+ // Theme values are passed from server via initialValues
92
+ // These come from the applied theme (SCITEX, MATPLOTLIB, etc.)
93
+ // initialValues is populated by the server from the loaded style preset
94
+
95
+ // Store original theme defaults for comparison
96
+ const themeDefaults = {...initialValues};
97
+
98
+ // Initialize form values and placeholders from applied theme
99
+ function initializeValues() {
100
+ // initialValues contains the theme's default values from the server
101
+ // These are the actual values from the applied style preset (not hardcoded)
102
+
103
+ for (const [key, value] of Object.entries(initialValues)) {
104
+ const element = document.getElementById(key);
105
+ if (element) {
106
+ if (element.type === 'checkbox') {
107
+ element.checked = Boolean(value);
108
+ } else if (element.type === 'range') {
109
+ element.value = value;
110
+ const valueSpan = document.getElementById(key + '_value');
111
+ if (valueSpan) valueSpan.textContent = value;
112
+ } else {
113
+ // Set the value
114
+ element.value = value;
115
+ // Set placeholder to show theme default (visible when field is cleared)
116
+ if (element.type === 'number' || element.type === 'text') {
117
+ element.placeholder = value;
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ // Log applied theme info
124
+ const styleNameEl = document.getElementById('style-name');
125
+ if (styleNameEl) {
126
+ console.log('Applied theme:', styleNameEl.textContent);
127
+ }
128
+ }
129
+
130
+ // Check if a field value differs from the theme default
131
+ function updateModifiedState(element) {
132
+ const key = element.id;
133
+ const defaultValue = themeDefaults[key];
134
+ const formRow = element.closest('.form-row');
135
+ if (!formRow || defaultValue === undefined) return;
136
+
137
+ let currentValue;
138
+ if (element.type === 'checkbox') {
139
+ currentValue = element.checked;
140
+ } else if (element.type === 'number') {
141
+ currentValue = parseFloat(element.value);
142
+ } else {
143
+ currentValue = element.value;
144
+ }
145
+
146
+ // Compare values (handle type conversion)
147
+ const isModified = String(currentValue) !== String(defaultValue);
148
+ formRow.classList.toggle('value-modified', isModified);
149
+ }
150
+
151
+ // Update all modified states
152
+ function updateAllModifiedStates() {
153
+ const inputs = document.querySelectorAll('input, select');
154
+ inputs.forEach(input => {
155
+ if (input.id && input.id !== 'dark-mode-toggle') {
156
+ updateModifiedState(input);
157
+ }
158
+ });
159
+ }
160
+
161
+ // ==================== EVENT LISTENERS ====================
162
+
163
+ // Initialize event listeners
164
+ function initializeEventListeners() {
165
+ // Preview image click for element selection
166
+ const previewImg = document.getElementById('preview-image');
167
+ previewImg.addEventListener('click', handlePreviewClick);
168
+
169
+ // SVG overlay click - deselect when clicking on empty area (not on a shape)
170
+ const hitregionOverlay = document.getElementById('hitregion-overlay');
171
+ hitregionOverlay.addEventListener('click', function(event) {
172
+ // Only clear if clicking directly on the SVG (not on a shape inside it)
173
+ if (event.target === hitregionOverlay) {
174
+ clearSelection();
175
+ }
176
+ });
177
+
178
+ // Selection overlay click - same behavior
179
+ const selectionOverlay = document.getElementById('selection-overlay');
180
+ selectionOverlay.addEventListener('click', function(event) {
181
+ if (event.target === selectionOverlay) {
182
+ clearSelection();
183
+ }
184
+ });
185
+
186
+ // Dark mode toggle
187
+ const darkModeToggle = document.getElementById('dark-mode-toggle');
188
+ darkModeToggle.addEventListener('change', function() {
189
+ document.documentElement.setAttribute('data-theme', this.checked ? 'dark' : 'light');
190
+ scheduleUpdate();
191
+ });
192
+
193
+ // Form inputs - auto update on change
194
+ // Exclude panel position inputs - they have their own Apply button
195
+ const panelPositionInputIds = ['panel_left', 'panel_top', 'panel_width', 'panel_height', 'panel_selector'];
196
+ const inputs = document.querySelectorAll('input, select');
197
+ inputs.forEach(input => {
198
+ if (input.id === 'dark-mode-toggle') return;
199
+ if (panelPositionInputIds.includes(input.id)) return; // Skip panel position inputs
200
+
201
+ // Update modified state and trigger preview update
202
+ input.addEventListener('change', function() {
203
+ updateModifiedState(this);
204
+ scheduleUpdate();
205
+ });
206
+ if (input.type === 'number' || input.type === 'text') {
207
+ input.addEventListener('input', function() {
208
+ updateModifiedState(this);
209
+ scheduleUpdate();
210
+ });
211
+ }
212
+
213
+ // Range slider value display
214
+ if (input.type === 'range') {
215
+ input.addEventListener('input', function() {
216
+ const valueSpan = document.getElementById(this.id + '_value');
217
+ if (valueSpan) valueSpan.textContent = this.value;
218
+ updateModifiedState(this);
219
+ });
220
+ }
221
+ });
222
+
223
+ // Buttons
224
+ document.getElementById('btn-refresh').addEventListener('click', updatePreview);
225
+ document.getElementById('btn-reset').addEventListener('click', resetValues);
226
+ document.getElementById('btn-save').addEventListener('click', saveOverrides);
227
+ document.getElementById('btn-restore').addEventListener('click', restoreOriginal);
228
+ // Hit regions toggle (optional - button may be hidden in production)
229
+ const hitmapBtn = document.getElementById('btn-show-hitmap');
230
+ if (hitmapBtn) hitmapBtn.addEventListener('click', toggleHitmapOverlay);
231
+
232
+ // Download dropdown buttons
233
+ initializeDownloadDropdown();
234
+
235
+ // Label input handlers
236
+ initializeLabelInputs();
237
+
238
+ // View mode toggle buttons (legacy - replaced by tabs)
239
+ const btnAll = document.getElementById('btn-show-all');
240
+ const btnSelected = document.getElementById('btn-show-selected');
241
+ if (btnAll) btnAll.addEventListener('click', () => setViewMode('all'));
242
+ if (btnSelected) btnSelected.addEventListener('click', () => setViewMode('selected'));
243
+
244
+ // Tab navigation
245
+ document.getElementById('tab-figure').addEventListener('click', () => switchTab('figure'));
246
+ document.getElementById('tab-axis').addEventListener('click', () => switchTab('axis'));
247
+ document.getElementById('tab-element').addEventListener('click', () => switchTab('element'));
248
+
249
+ // Theme modal handlers
250
+ initializeThemeModal();
251
+ initializeShortcutsModal();
252
+
253
+ // Check initial override status
254
+ checkOverrideStatus();
255
+
256
+ // Check modified states after initial values are set
257
+ setTimeout(updateAllModifiedStates, 100);
258
+
259
+ // Keyboard shortcuts
260
+ document.addEventListener('keydown', handleKeyboardShortcuts);
261
+ }
262
+
263
+ // Handle keyboard shortcuts
264
+ function handleKeyboardShortcuts(event) {
265
+ // Ignore shortcuts when typing in input fields
266
+ const activeElement = document.activeElement;
267
+ const isInputField = activeElement.tagName === 'INPUT' ||
268
+ activeElement.tagName === 'TEXTAREA' ||
269
+ activeElement.tagName === 'SELECT';
270
+
271
+ // Ctrl+Alt+I: Debug snapshot (screenshot + console logs)
272
+ if (event.ctrlKey && event.altKey && (event.key === 'i' || event.key === 'I')) {
273
+ event.preventDefault();
274
+ event.stopPropagation();
275
+ console.log('[DEBUG] Ctrl+Alt+I pressed, calling captureDebugSnapshot');
276
+ if (typeof captureDebugSnapshot === 'function') {
277
+ captureDebugSnapshot();
278
+ } else {
279
+ console.error('[DEBUG] captureDebugSnapshot is not defined!');
280
+ showToast('Debug snapshot not available', 'error');
281
+ }
282
+ return;
283
+ }
284
+
285
+ // Ctrl+S: Save overrides
286
+ if (event.ctrlKey && event.key === 's') {
287
+ event.preventDefault();
288
+ saveOverrides();
289
+ showToast('Saved!', 'success');
290
+ return;
291
+ }
292
+
293
+ // Ctrl+N: New blank figure
294
+ if (event.ctrlKey && event.key === 'n') {
295
+ event.preventDefault();
296
+ if (typeof createNewFigure === 'function') {
297
+ createNewFigure();
298
+ }
299
+ return;
300
+ }
301
+
302
+ // Ctrl+Shift+S: Download PNG
303
+ if (event.ctrlKey && event.shiftKey && event.key === 'S') {
304
+ event.preventDefault();
305
+ downloadFigure('png');
306
+ return;
307
+ }
308
+
309
+ // F5 or Ctrl+R: Refresh preview
310
+ if (event.key === 'F5' || (event.ctrlKey && event.key === 'r')) {
311
+ event.preventDefault();
312
+ updatePreview();
313
+ showToast('Refreshed', 'info');
314
+ return;
315
+ }
316
+
317
+ // Only handle the following shortcuts if not in an input field
318
+ if (isInputField) return;
319
+
320
+ // Escape: Close modals or clear selection
321
+ if (event.key === 'Escape') {
322
+ const shortcutsModal = document.getElementById('shortcuts-modal');
323
+ if (shortcutsModal && shortcutsModal.style.display === 'flex') {
324
+ hideShortcutsModal();
325
+ return;
326
+ }
327
+ clearSelection();
328
+ return;
329
+ }
330
+
331
+ // Tab navigation: 1, 2, 3 keys
332
+ if (event.key === '1') {
333
+ switchTab('figure');
334
+ return;
335
+ }
336
+ if (event.key === '2') {
337
+ switchTab('axis');
338
+ return;
339
+ }
340
+ if (event.key === '3') {
341
+ switchTab('element');
342
+ return;
343
+ }
344
+
345
+ // R: Reset to theme defaults
346
+ if (event.key === 'r' || event.key === 'R') {
347
+ resetValues();
348
+ showToast('Reset to defaults', 'info');
349
+ return;
350
+ }
351
+
352
+ // G: Toggle rulers and grid
353
+ if (event.key === 'g' || event.key === 'G') {
354
+ toggleRulerGrid();
355
+ const state = rulerGridVisible ? 'ON' : 'OFF';
356
+ showToast(`Ruler & Grid: ${state}`, 'info');
357
+ return;
358
+ }
359
+
360
+ // ?: Show keyboard shortcuts
361
+ if (event.key === '?') {
362
+ showShortcutsModal();
363
+ return;
364
+ }
365
+ }
366
+
367
+ // ==================== UTILITY FUNCTIONS ====================
368
+
369
+ // Show toast notification
370
+ function showToast(message, type = 'info') {
371
+ // Remove existing toast if any
372
+ const existingToast = document.querySelector('.toast-notification');
373
+ if (existingToast) {
374
+ existingToast.remove();
375
+ }
376
+
377
+ // Create toast element
378
+ const toast = document.createElement('div');
379
+ toast.className = 'toast-notification toast-' + type;
380
+ toast.textContent = message;
381
+
382
+ // Style the toast
383
+ Object.assign(toast.style, {
384
+ position: 'fixed',
385
+ bottom: '20px',
386
+ left: '50%',
387
+ transform: 'translateX(-50%)',
388
+ padding: '10px 20px',
389
+ borderRadius: '4px',
390
+ color: 'white',
391
+ fontWeight: '500',
392
+ zIndex: '10000',
393
+ opacity: '0',
394
+ transition: 'opacity 0.3s ease',
395
+ boxShadow: '0 2px 8px rgba(0,0,0,0.3)'
396
+ });
397
+
398
+ // Set background color based on type
399
+ const colors = {
400
+ success: '#4CAF50',
401
+ info: '#2196F3',
402
+ warning: '#ff9800',
403
+ error: '#f44336'
404
+ };
405
+ toast.style.backgroundColor = colors[type] || colors.info;
406
+
407
+ document.body.appendChild(toast);
408
+
409
+ // Fade in
410
+ requestAnimationFrame(() => {
411
+ toast.style.opacity = '1';
412
+ });
413
+
414
+ // Remove after delay
415
+ setTimeout(() => {
416
+ toast.style.opacity = '0';
417
+ setTimeout(() => toast.remove(), 300);
418
+ }, 2000);
419
+ }
420
+
421
+ // Debounce utility
422
+ function debounce(func, wait) {
423
+ let timeout;
424
+ return function(...args) {
425
+ clearTimeout(timeout);
426
+ timeout = setTimeout(() => func.apply(this, args), wait);
427
+ };
428
+ }
429
+
430
+ // Note: scheduleUpdate() is defined in _api.py to avoid duplication
431
+ // It calls updatePreview() with debounce, which properly includes dark_mode
432
+
433
+ // ==================== END CORE ====================
434
+ """
435
+
436
+ __all__ = ["SCRIPTS_CORE"]
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Debug snapshot functionality for the figure editor.
4
+
5
+ Captures screenshots and console logs with Ctrl+Alt+I shortcut.
6
+ """
7
+
8
+ SCRIPTS_DEBUG_SNAPSHOT = """
9
+ // ==================== DEBUG SNAPSHOT ====================
10
+ console.log('[DEBUG] Debug snapshot module loaded');
11
+
12
+ // Console log collection for debug snapshots
13
+ const debugSnapshotLogs = [];
14
+ const maxDebugLogs = 500;
15
+ const _origConsole = {
16
+ log: console.log.bind(console),
17
+ warn: console.warn.bind(console),
18
+ error: console.error.bind(console),
19
+ info: console.info.bind(console),
20
+ debug: console.debug.bind(console)
21
+ };
22
+
23
+ // Wrap console methods to capture logs (non-destructive - chains with existing)
24
+ ['log', 'warn', 'error', 'info', 'debug'].forEach(method => {
25
+ const prev = console[method];
26
+ console[method] = function(...args) {
27
+ debugSnapshotLogs.push({
28
+ type: method,
29
+ timestamp: new Date().toISOString(),
30
+ args: args.map(arg => {
31
+ if (arg === null) return 'null';
32
+ if (arg === undefined) return 'undefined';
33
+ if (typeof arg === 'string') return arg;
34
+ if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
35
+ try { return JSON.stringify(arg); } catch (e) { return String(arg); }
36
+ })
37
+ });
38
+ if (debugSnapshotLogs.length > maxDebugLogs) debugSnapshotLogs.shift();
39
+ return prev.apply(console, args);
40
+ };
41
+ });
42
+
43
+ // Get formatted console logs
44
+ function getConsoleLogs() {
45
+ if (debugSnapshotLogs.length === 0) return 'No console logs captured.';
46
+ return debugSnapshotLogs.map(entry => {
47
+ const icon = { error: '❌', warn: '⚠️', info: 'ℹ️', debug: '🔍', log: '📝' }[entry.type] || '📝';
48
+ return `${icon} [${entry.type.toUpperCase()}] ${entry.args.join(' ')}`;
49
+ }).join('\\n');
50
+ }
51
+
52
+ // Show camera flash effect
53
+ function showCameraFlash() {
54
+ const flash = document.createElement('div');
55
+ flash.style.cssText = `
56
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
57
+ background: white; opacity: 0.8; z-index: 99999;
58
+ animation: flashFade 0.3s ease-out forwards;
59
+ `;
60
+ const style = document.createElement('style');
61
+ style.textContent = '@keyframes flashFade { to { opacity: 0; } }';
62
+ document.head.appendChild(style);
63
+ document.body.appendChild(flash);
64
+ setTimeout(() => { flash.remove(); style.remove(); }, 300);
65
+ }
66
+
67
+ // Capture FULL PAGE screenshot using html2canvas (no dialog required)
68
+ async function captureScreenshot() {
69
+ console.log('[DebugSnapshot] === Starting screenshot capture ===');
70
+
71
+ // Check html2canvas availability
72
+ if (typeof html2canvas === 'undefined') {
73
+ console.error('[DebugSnapshot] html2canvas NOT LOADED!');
74
+ return await captureFigureOnly();
75
+ }
76
+
77
+ console.log('[DebugSnapshot] html2canvas version:', html2canvas.toString().slice(0, 50));
78
+
79
+ try {
80
+ // Small delay to ensure DOM is stable
81
+ await new Promise(r => setTimeout(r, 100));
82
+
83
+ // Get the editor container
84
+ const container = document.querySelector('.editor-container');
85
+ if (!container) {
86
+ console.error('[DebugSnapshot] .editor-container not found!');
87
+ return await captureFigureOnly();
88
+ }
89
+
90
+ const rect = container.getBoundingClientRect();
91
+ console.log('[DebugSnapshot] Container size:', rect.width, 'x', rect.height);
92
+
93
+ // Capture with explicit dimensions
94
+ console.log('[DebugSnapshot] Starting html2canvas...');
95
+ const canvas = await html2canvas(container, {
96
+ backgroundColor: getComputedStyle(document.documentElement).getPropertyValue('--bg-color') || '#1e1e1e',
97
+ width: Math.ceil(rect.width),
98
+ height: Math.ceil(rect.height),
99
+ scale: 1,
100
+ useCORS: true,
101
+ allowTaint: true,
102
+ logging: true, // Enable for debugging
103
+ onclone: (clonedDoc) => {
104
+ console.log('[DebugSnapshot] DOM cloned for rendering');
105
+ }
106
+ });
107
+
108
+ console.log('[DebugSnapshot] Canvas result:', canvas.width, 'x', canvas.height);
109
+
110
+ // Validate result - full page should be wider than just the figure
111
+ if (canvas.width > 500 && canvas.height > 300) {
112
+ const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
113
+ console.log('[DebugSnapshot] FULL PAGE SUCCESS! Blob size:', blob?.size);
114
+ return blob;
115
+ }
116
+
117
+ console.warn('[DebugSnapshot] Canvas too small, using figure fallback');
118
+ } catch (err) {
119
+ console.error('[DebugSnapshot] html2canvas ERROR:', err.name, err.message);
120
+ console.error(err.stack);
121
+ }
122
+
123
+ return await captureFigureOnly();
124
+ }
125
+
126
+ // Fallback: capture just the figure image
127
+ async function captureFigureOnly() {
128
+ console.log('[DebugSnapshot] Using figure-only fallback...');
129
+ try {
130
+ const img = document.getElementById('preview-image');
131
+ if (img && img.src && img.src.startsWith('data:')) {
132
+ const response = await fetch(img.src);
133
+ const blob = await response.blob();
134
+ console.log('[DebugSnapshot] Figure captured, size:', blob.size);
135
+ return blob;
136
+ }
137
+ } catch (err) {
138
+ console.error('[DebugSnapshot] Figure fallback failed:', err);
139
+ }
140
+ return null;
141
+ }
142
+
143
+ // Capture debug snapshot (screenshot + console logs)
144
+ async function captureDebugSnapshot() {
145
+ showCameraFlash();
146
+ showToast('📷 Capturing...', 'info');
147
+
148
+ const screenshotBlob = await captureScreenshot();
149
+ const logsText = getConsoleLogs();
150
+
151
+ if (!screenshotBlob && logsText === 'No console logs captured.') {
152
+ showToast('✗ Capture failed', 'error');
153
+ return;
154
+ }
155
+
156
+ // Copy screenshot first
157
+ if (screenshotBlob) {
158
+ try {
159
+ await navigator.clipboard.write([
160
+ new ClipboardItem({ 'image/png': screenshotBlob })
161
+ ]);
162
+ showToast('📷 Screenshot copied! Paste now, then logs copy in 3s...', 'success');
163
+ } catch (e) {
164
+ console.error('[DebugSnapshot] Clipboard failed:', e);
165
+ showToast('✗ Clipboard failed', 'error');
166
+ return;
167
+ }
168
+ }
169
+
170
+ // Copy logs after delay
171
+ if (logsText !== 'No console logs captured.') {
172
+ const delay = screenshotBlob ? 3000 : 0;
173
+ await new Promise(r => setTimeout(r, delay));
174
+ try {
175
+ await navigator.clipboard.writeText(logsText);
176
+ showToast('📋 Console logs copied!', 'success');
177
+ } catch (e) {
178
+ console.error('[DebugSnapshot] Logs clipboard failed:', e);
179
+ }
180
+ }
181
+ }
182
+
183
+ // ==================== END DEBUG SNAPSHOT ====================
184
+ """
185
+
186
+ __all__ = ["SCRIPTS_DEBUG_SNAPSHOT"]