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.
- figrecipe/__init__.py +220 -819
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +46 -0
- figrecipe/_api/_save.py +191 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_dev/__init__.py +29 -0
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +64 -0
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_bar.py +25 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
- figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
- figrecipe/_editor/__init__.py +278 -0
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +256 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +342 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_flask_app.py +258 -0
- figrecipe/_editor/_helpers.py +242 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +137 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +318 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +199 -0
- figrecipe/_editor/_routes_axis.py +453 -0
- figrecipe/_editor/_routes_core.py +284 -0
- figrecipe/_editor/_routes_element.py +317 -0
- figrecipe/_editor/_routes_style.py +223 -0
- figrecipe/_editor/_templates/__init__.py +152 -0
- figrecipe/_editor/_templates/_html.py +502 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_core.py +436 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
- figrecipe/_editor/_templates/_scripts/_files.py +195 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
- figrecipe/_editor/_templates/_styles/__init__.py +69 -0
- figrecipe/_editor/_templates/_styles/_base.py +64 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_controls.py +265 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_forms.py +126 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +98 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +225 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_params/_DECORATION_METHODS.py +33 -0
- figrecipe/_params/_PLOTTING_METHODS.py +58 -0
- figrecipe/_params/__init__.py +9 -0
- figrecipe/_recorder.py +92 -110
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +498 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_seaborn.py +14 -9
- figrecipe/_serializer.py +2 -2
- figrecipe/_signatures/README.md +68 -0
- figrecipe/_signatures/__init__.py +12 -2
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +114 -57
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_utils/__init__.py +6 -4
- figrecipe/_utils/_crop.py +10 -4
- figrecipe/_utils/_image_diff.py +37 -33
- figrecipe/_utils/_numpy_io.py +0 -1
- figrecipe/_utils/_units.py +11 -3
- figrecipe/_validator.py +12 -3
- figrecipe/_wrappers/_axes.py +193 -170
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_figure.py +277 -18
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/plt.py +0 -1
- figrecipe/pyplot.py +2 -1
- figrecipe/styles/__init__.py +12 -11
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +60 -202
- figrecipe/styles/_style_loader.py +73 -121
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
- figrecipe/styles/presets/SCITEX.yaml +181 -0
- figrecipe-0.7.4.dist-info/METADATA +429 -0
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_reproducer.py +0 -358
- figrecipe-0.5.0.dist-info/METADATA +0 -336
- figrecipe-0.5.0.dist-info/RECORD +0 -26
- {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
- {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"]
|