figrecipe 0.6.0__py3-none-any.whl → 0.9.0__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 +161 -1030
- figrecipe/__main__.py +12 -0
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +113 -0
- figrecipe/_api/_save.py +287 -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/_cli/__init__.py +7 -0
- figrecipe/_cli/_compose.py +87 -0
- figrecipe/_cli/_convert.py +117 -0
- figrecipe/_cli/_crop.py +82 -0
- figrecipe/_cli/_edit.py +70 -0
- figrecipe/_cli/_extract.py +128 -0
- figrecipe/_cli/_fonts.py +47 -0
- figrecipe/_cli/_info.py +67 -0
- figrecipe/_cli/_main.py +58 -0
- figrecipe/_cli/_reproduce.py +79 -0
- figrecipe/_cli/_style.py +77 -0
- figrecipe/_cli/_validate.py +66 -0
- figrecipe/_cli/_version.py +50 -0
- figrecipe/_composition/__init__.py +32 -0
- figrecipe/_composition/_alignment.py +452 -0
- figrecipe/_composition/_compose.py +179 -0
- figrecipe/_composition/_import_axes.py +127 -0
- figrecipe/_composition/_visibility.py +125 -0
- figrecipe/_dev/__init__.py +4 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/browser/__init__.py +69 -0
- figrecipe/_dev/browser/_audio.py +240 -0
- figrecipe/_dev/browser/_caption.py +356 -0
- figrecipe/_dev/browser/_click_effect.py +146 -0
- figrecipe/_dev/browser/_cursor.py +196 -0
- figrecipe/_dev/browser/_highlight.py +105 -0
- figrecipe/_dev/browser/_narration.py +237 -0
- figrecipe/_dev/browser/_recorder.py +446 -0
- figrecipe/_dev/browser/_utils.py +178 -0
- figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
- figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
- figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- 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/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +61 -13
- 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 +402 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +466 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_call_overrides.py +183 -0
- figrecipe/_editor/_datatable_plot_handlers.py +249 -0
- figrecipe/_editor/_figure_layout.py +211 -0
- figrecipe/_editor/_flask_app.py +200 -1030
- figrecipe/_editor/_helpers.py +251 -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 +194 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +507 -0
- figrecipe/_editor/_renderer.py +81 -186
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +482 -0
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +126 -0
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +335 -0
- figrecipe/_editor/_routes_files.py +443 -0
- figrecipe/_editor/_routes_image.py +200 -0
- figrecipe/_editor/_routes_snapshot.py +94 -0
- figrecipe/_editor/_routes_style.py +243 -0
- figrecipe/_editor/_templates/__init__.py +116 -1
- figrecipe/_editor/_templates/_html.py +154 -64
- figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
- figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
- figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
- figrecipe/_editor/_templates/_html_datatable.py +92 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +493 -0
- figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
- figrecipe/_editor/_templates/_scripts/_files.py +429 -0
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
- figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
- figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
- figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
- figrecipe/_editor/_templates/_styles/__init__.py +78 -0
- figrecipe/_editor/_templates/_styles/_base.py +111 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +430 -0
- figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
- figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
- figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
- figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
- figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
- figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +224 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +127 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +430 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
- figrecipe/_editor/static/audio/click.mp3 +0 -0
- figrecipe/_editor/static/click.mp3 +0 -0
- figrecipe/_editor/static/icons/favicon.ico +0 -0
- figrecipe/_integrations/__init__.py +17 -0
- figrecipe/_integrations/_scitex_stats.py +298 -0
- figrecipe/_params/_DECORATION_METHODS.py +8 -0
- figrecipe/_recorder.py +63 -109
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +509 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +252 -895
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +188 -1
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- 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 +42 -480
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +40 -28
- figrecipe-0.9.0.dist-info/METADATA +427 -0
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/METADATA +0 -394
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Undo/Redo functionality for the figure editor.
|
|
4
|
+
|
|
5
|
+
This module provides a history stack for tracking changes and
|
|
6
|
+
enabling undo/redo operations with Ctrl+Z and Ctrl+Shift+Z.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
SCRIPTS_UNDO_REDO = """
|
|
10
|
+
// ==================== UNDO/REDO HISTORY ====================
|
|
11
|
+
|
|
12
|
+
// History state
|
|
13
|
+
const historyStack = [];
|
|
14
|
+
const redoStack = [];
|
|
15
|
+
const MAX_HISTORY = 50; // Maximum number of undo steps
|
|
16
|
+
let isUndoRedoInProgress = false; // Prevent recursive history recording
|
|
17
|
+
|
|
18
|
+
// Capture current state as a snapshot
|
|
19
|
+
function captureState() {
|
|
20
|
+
const state = {
|
|
21
|
+
overrides: collectOverrides(),
|
|
22
|
+
panelPositions: typeof panelPositions !== 'undefined' ? JSON.parse(JSON.stringify(panelPositions)) : {},
|
|
23
|
+
annotationPositions: typeof annotationPositions !== 'undefined' ? JSON.parse(JSON.stringify(annotationPositions)) : {},
|
|
24
|
+
timestamp: Date.now()
|
|
25
|
+
};
|
|
26
|
+
return state;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Compare two states for equality
|
|
30
|
+
function statesEqual(a, b) {
|
|
31
|
+
return JSON.stringify(a.overrides) === JSON.stringify(b.overrides) &&
|
|
32
|
+
JSON.stringify(a.panelPositions) === JSON.stringify(b.panelPositions) &&
|
|
33
|
+
JSON.stringify(a.annotationPositions) === JSON.stringify(b.annotationPositions);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Push current state to history (call before making changes)
|
|
37
|
+
function pushToHistory() {
|
|
38
|
+
if (isUndoRedoInProgress) return;
|
|
39
|
+
|
|
40
|
+
const state = captureState();
|
|
41
|
+
|
|
42
|
+
// Don't push if identical to last state
|
|
43
|
+
if (historyStack.length > 0) {
|
|
44
|
+
const lastState = historyStack[historyStack.length - 1];
|
|
45
|
+
if (statesEqual(lastState, state)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
historyStack.push(state);
|
|
51
|
+
|
|
52
|
+
// Clear redo stack when new action is performed
|
|
53
|
+
redoStack.length = 0;
|
|
54
|
+
|
|
55
|
+
// Trim history if too long
|
|
56
|
+
while (historyStack.length > MAX_HISTORY) {
|
|
57
|
+
historyStack.shift();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
updateUndoRedoButtons();
|
|
61
|
+
console.log('[History] Pushed state, stack size:', historyStack.length);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Apply a state snapshot to the form
|
|
65
|
+
async function applyState(state) {
|
|
66
|
+
isUndoRedoInProgress = true;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const overrides = state.overrides;
|
|
70
|
+
|
|
71
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
72
|
+
const element = document.getElementById(key);
|
|
73
|
+
if (!element) continue;
|
|
74
|
+
|
|
75
|
+
if (element.type === 'checkbox') {
|
|
76
|
+
element.checked = Boolean(value);
|
|
77
|
+
} else if (element.type === 'range') {
|
|
78
|
+
element.value = value;
|
|
79
|
+
const valueSpan = document.getElementById(key + '_value');
|
|
80
|
+
if (valueSpan) valueSpan.textContent = value;
|
|
81
|
+
} else if (element.type === 'color') {
|
|
82
|
+
element.value = value;
|
|
83
|
+
} else if (element.tagName === 'SELECT') {
|
|
84
|
+
element.value = value;
|
|
85
|
+
} else {
|
|
86
|
+
element.value = value;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Restore panel positions if they differ
|
|
91
|
+
if (state.panelPositions && typeof panelPositions !== 'undefined') {
|
|
92
|
+
const axKeys = Object.keys(state.panelPositions).sort();
|
|
93
|
+
for (let i = 0; i < axKeys.length; i++) {
|
|
94
|
+
const axKey = axKeys[i];
|
|
95
|
+
const savedPos = state.panelPositions[axKey];
|
|
96
|
+
const currentPos = panelPositions[axKey];
|
|
97
|
+
|
|
98
|
+
// Check if position changed
|
|
99
|
+
if (currentPos && savedPos &&
|
|
100
|
+
(Math.abs(savedPos.left - currentPos.left) > 0.1 ||
|
|
101
|
+
Math.abs(savedPos.top - currentPos.top) > 0.1)) {
|
|
102
|
+
// Restore panel position via API
|
|
103
|
+
try {
|
|
104
|
+
await fetch('/update_axes_position', {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
ax_index: i,
|
|
109
|
+
left: savedPos.left,
|
|
110
|
+
top: savedPos.top,
|
|
111
|
+
width: savedPos.width,
|
|
112
|
+
height: savedPos.height
|
|
113
|
+
})
|
|
114
|
+
});
|
|
115
|
+
// Update local panelPositions to match restored state
|
|
116
|
+
panelPositions[axKey] = { ...savedPos };
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.error('[History] Failed to restore panel position:', e);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Restore annotation positions if they differ
|
|
125
|
+
if (state.annotationPositions && typeof annotationPositions !== 'undefined') {
|
|
126
|
+
let needsRefresh = false;
|
|
127
|
+
for (const [key, savedPos] of Object.entries(state.annotationPositions)) {
|
|
128
|
+
const currentPos = annotationPositions[key];
|
|
129
|
+
|
|
130
|
+
// Check if position changed
|
|
131
|
+
if (!currentPos ||
|
|
132
|
+
Math.abs(savedPos.x - (currentPos?.x || 0)) > 0.001 ||
|
|
133
|
+
Math.abs(savedPos.y - (currentPos?.y || 0)) > 0.001) {
|
|
134
|
+
|
|
135
|
+
// Parse key formats:
|
|
136
|
+
// "ax0_panel_label" -> axIndex=0, type=panel_label, textIndex=0
|
|
137
|
+
// "ax0_text_0" -> axIndex=0, type=text, textIndex=0
|
|
138
|
+
let axIndex, annotationType, textIndex;
|
|
139
|
+
|
|
140
|
+
if (key.includes('_panel_label')) {
|
|
141
|
+
const match = key.match(/ax(\\d+)_panel_label/);
|
|
142
|
+
if (match) {
|
|
143
|
+
axIndex = parseInt(match[1], 10);
|
|
144
|
+
annotationType = 'panel_label';
|
|
145
|
+
textIndex = 0;
|
|
146
|
+
}
|
|
147
|
+
} else if (key.includes('_text_')) {
|
|
148
|
+
const match = key.match(/ax(\\d+)_text_(\\d+)/);
|
|
149
|
+
if (match) {
|
|
150
|
+
axIndex = parseInt(match[1], 10);
|
|
151
|
+
annotationType = 'text';
|
|
152
|
+
textIndex = parseInt(match[2], 10);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (axIndex !== undefined) {
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetch('/update_annotation_position', {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
ax_index: axIndex,
|
|
163
|
+
annotation_type: annotationType,
|
|
164
|
+
text_index: textIndex,
|
|
165
|
+
x: savedPos.x,
|
|
166
|
+
y: savedPos.y
|
|
167
|
+
})
|
|
168
|
+
});
|
|
169
|
+
const data = await response.json();
|
|
170
|
+
|
|
171
|
+
if (data.success && data.image) {
|
|
172
|
+
// Update preview image
|
|
173
|
+
const img = document.getElementById('preview-image');
|
|
174
|
+
if (img) {
|
|
175
|
+
img.src = 'data:image/png;base64,' + data.image;
|
|
176
|
+
}
|
|
177
|
+
// Update bboxes
|
|
178
|
+
if (data.bboxes && typeof currentBboxes !== 'undefined') {
|
|
179
|
+
currentBboxes = data.bboxes;
|
|
180
|
+
}
|
|
181
|
+
needsRefresh = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Update local annotationPositions to match restored state
|
|
185
|
+
annotationPositions[key] = { ...savedPos };
|
|
186
|
+
console.log('[History] Restored annotation position:', key);
|
|
187
|
+
} catch (e) {
|
|
188
|
+
console.error('[History] Failed to restore annotation position:', e);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Refresh hitmap if positions were restored
|
|
195
|
+
if (needsRefresh && typeof loadHitmap === 'function') {
|
|
196
|
+
loadHitmap();
|
|
197
|
+
if (typeof updateHitRegions === 'function') {
|
|
198
|
+
updateHitRegions();
|
|
199
|
+
}
|
|
200
|
+
if (typeof initAnnotationPositions === 'function') {
|
|
201
|
+
initAnnotationPositions();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Update preview with the restored state
|
|
207
|
+
updatePreview();
|
|
208
|
+
|
|
209
|
+
} finally {
|
|
210
|
+
isUndoRedoInProgress = false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Undo last action
|
|
215
|
+
async function undo() {
|
|
216
|
+
if (historyStack.length === 0) {
|
|
217
|
+
showToast('Nothing to undo', 'info');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Save current state to redo stack
|
|
222
|
+
const currentState = captureState();
|
|
223
|
+
redoStack.push(currentState);
|
|
224
|
+
|
|
225
|
+
// Pop and apply previous state
|
|
226
|
+
const previousState = historyStack.pop();
|
|
227
|
+
await applyState(previousState);
|
|
228
|
+
|
|
229
|
+
updateUndoRedoButtons();
|
|
230
|
+
showToast('Undo', 'info');
|
|
231
|
+
console.log('[History] Undo, remaining:', historyStack.length);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Redo last undone action
|
|
235
|
+
async function redo() {
|
|
236
|
+
if (redoStack.length === 0) {
|
|
237
|
+
showToast('Nothing to redo', 'info');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Save current state to history
|
|
242
|
+
const currentState = captureState();
|
|
243
|
+
historyStack.push(currentState);
|
|
244
|
+
|
|
245
|
+
// Pop and apply redo state
|
|
246
|
+
const redoState = redoStack.pop();
|
|
247
|
+
await applyState(redoState);
|
|
248
|
+
|
|
249
|
+
updateUndoRedoButtons();
|
|
250
|
+
showToast('Redo', 'info');
|
|
251
|
+
console.log('[History] Redo, remaining redo:', redoStack.length);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Update undo/redo button states
|
|
255
|
+
function updateUndoRedoButtons() {
|
|
256
|
+
const undoBtn = document.getElementById('btn-undo');
|
|
257
|
+
const redoBtn = document.getElementById('btn-redo');
|
|
258
|
+
|
|
259
|
+
if (undoBtn) {
|
|
260
|
+
undoBtn.disabled = historyStack.length === 0;
|
|
261
|
+
undoBtn.title = historyStack.length > 0
|
|
262
|
+
? `Undo (${historyStack.length} steps available)`
|
|
263
|
+
: 'Nothing to undo';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (redoBtn) {
|
|
267
|
+
redoBtn.disabled = redoStack.length === 0;
|
|
268
|
+
redoBtn.title = redoStack.length > 0
|
|
269
|
+
? `Redo (${redoStack.length} steps available)`
|
|
270
|
+
: 'Nothing to redo';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Clear all history (e.g., when switching files)
|
|
275
|
+
function clearHistory() {
|
|
276
|
+
historyStack.length = 0;
|
|
277
|
+
redoStack.length = 0;
|
|
278
|
+
updateUndoRedoButtons();
|
|
279
|
+
console.log('[History] Cleared');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Hook into form changes to record history
|
|
283
|
+
function initUndoRedo() {
|
|
284
|
+
// Capture initial state
|
|
285
|
+
pushToHistory();
|
|
286
|
+
|
|
287
|
+
// Add change listeners to all form inputs
|
|
288
|
+
const inputs = document.querySelectorAll('input, select');
|
|
289
|
+
inputs.forEach(input => {
|
|
290
|
+
if (input.id === 'dark-mode-toggle') return;
|
|
291
|
+
if (!input.id) return;
|
|
292
|
+
|
|
293
|
+
// Capture state before change
|
|
294
|
+
input.addEventListener('focus', () => {
|
|
295
|
+
pushToHistory();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// For inputs without focus events (like range sliders)
|
|
299
|
+
if (input.type === 'range') {
|
|
300
|
+
let rangeStartValue = null;
|
|
301
|
+
input.addEventListener('mousedown', () => {
|
|
302
|
+
rangeStartValue = input.value;
|
|
303
|
+
pushToHistory();
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// For select elements
|
|
308
|
+
if (input.tagName === 'SELECT') {
|
|
309
|
+
input.addEventListener('mousedown', () => {
|
|
310
|
+
pushToHistory();
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Initialize button states
|
|
316
|
+
updateUndoRedoButtons();
|
|
317
|
+
|
|
318
|
+
console.log('[History] Undo/Redo initialized');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Initialize when DOM is ready
|
|
322
|
+
if (document.readyState === 'loading') {
|
|
323
|
+
document.addEventListener('DOMContentLoaded', initUndoRedo);
|
|
324
|
+
} else {
|
|
325
|
+
// Small delay to ensure other scripts have initialized
|
|
326
|
+
setTimeout(initUndoRedo, 100);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Add button click handlers after DOM is ready
|
|
330
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
331
|
+
const undoBtn = document.getElementById('btn-undo');
|
|
332
|
+
const redoBtn = document.getElementById('btn-redo');
|
|
333
|
+
|
|
334
|
+
if (undoBtn) {
|
|
335
|
+
undoBtn.addEventListener('click', undo);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (redoBtn) {
|
|
339
|
+
redoBtn.addEventListener('click', redo);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
console.log('[UndoRedo] Module loaded - Ctrl+Z to undo, Ctrl+Shift+Z to redo');
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
__all__ = ["SCRIPTS_UNDO_REDO"]
|
|
347
|
+
|
|
348
|
+
# EOF
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""View mode JavaScript for the figure editor.
|
|
4
|
+
|
|
5
|
+
This module contains the JavaScript code for:
|
|
6
|
+
- View mode management (all/selected)
|
|
7
|
+
- Property filtering by element type
|
|
8
|
+
- Section visibility control
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
SCRIPTS_VIEW_MODE = """
|
|
12
|
+
// ===== VIEW MODE MANAGEMENT =====
|
|
13
|
+
// Note: viewMode variable is declared in _core.py
|
|
14
|
+
|
|
15
|
+
// Set view mode (all or selected)
|
|
16
|
+
function setViewMode(mode) {
|
|
17
|
+
viewMode = mode;
|
|
18
|
+
|
|
19
|
+
// Update toggle buttons (legacy)
|
|
20
|
+
const btnAll = document.getElementById('btn-show-all');
|
|
21
|
+
const btnSelected = document.getElementById('btn-show-selected');
|
|
22
|
+
if (btnAll) btnAll.classList.toggle('active', mode === 'all');
|
|
23
|
+
if (btnSelected) btnSelected.classList.toggle('active', mode === 'selected');
|
|
24
|
+
|
|
25
|
+
// Update controls sections class
|
|
26
|
+
const controlsSections = document.querySelector('.controls-sections');
|
|
27
|
+
controlsSections.classList.toggle('filter-mode', mode === 'selected');
|
|
28
|
+
|
|
29
|
+
// Update selection hint
|
|
30
|
+
const hint = document.getElementById('selection-hint');
|
|
31
|
+
if (mode === 'selected') {
|
|
32
|
+
if (selectedElement) {
|
|
33
|
+
hint.textContent = `Showing: ${selectedElement.type}`;
|
|
34
|
+
hint.style.color = 'var(--accent-color)';
|
|
35
|
+
// Hide all style sections - only show call properties
|
|
36
|
+
hideAllStyleSections();
|
|
37
|
+
} else {
|
|
38
|
+
hint.textContent = '';
|
|
39
|
+
hint.style.color = '';
|
|
40
|
+
// Show all when no selection in filter mode
|
|
41
|
+
showAllProperties();
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
hint.textContent = '';
|
|
45
|
+
showAllProperties();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Hide all style sections (for Selected mode - only show call properties)
|
|
50
|
+
function hideAllStyleSections() {
|
|
51
|
+
const sections = document.querySelectorAll('.section[data-element-types]');
|
|
52
|
+
sections.forEach(section => {
|
|
53
|
+
section.classList.add('section-hidden');
|
|
54
|
+
section.classList.remove('section-visible');
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Filter properties by element type
|
|
59
|
+
function filterPropertiesByElementType(elementType) {
|
|
60
|
+
const sections = document.querySelectorAll('.section[data-element-types]');
|
|
61
|
+
|
|
62
|
+
sections.forEach(section => {
|
|
63
|
+
const types = section.getAttribute('data-element-types').split(',');
|
|
64
|
+
const isGlobal = types.includes('global');
|
|
65
|
+
const matches = isGlobal || types.includes(elementType);
|
|
66
|
+
|
|
67
|
+
section.classList.toggle('section-hidden', !matches);
|
|
68
|
+
section.classList.toggle('section-visible', matches);
|
|
69
|
+
|
|
70
|
+
// If section matches, filter individual form-rows within it
|
|
71
|
+
if (matches && !isGlobal) {
|
|
72
|
+
const formRows = section.querySelectorAll('.form-row[data-element-types]');
|
|
73
|
+
formRows.forEach(row => {
|
|
74
|
+
const rowTypes = row.getAttribute('data-element-types').split(',');
|
|
75
|
+
const rowMatches = rowTypes.includes(elementType);
|
|
76
|
+
row.classList.toggle('field-hidden', !rowMatches);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Open matching sections
|
|
80
|
+
section.setAttribute('open', '');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Update hint
|
|
85
|
+
const hint = document.getElementById('selection-hint');
|
|
86
|
+
hint.textContent = `Showing: ${elementType}`;
|
|
87
|
+
hint.style.color = 'var(--accent-color)';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Show all properties (remove filtering)
|
|
91
|
+
function showAllProperties() {
|
|
92
|
+
const sections = document.querySelectorAll('.section[data-element-types]');
|
|
93
|
+
|
|
94
|
+
sections.forEach(section => {
|
|
95
|
+
section.classList.remove('section-hidden', 'section-visible');
|
|
96
|
+
|
|
97
|
+
const formRows = section.querySelectorAll('.form-row[data-element-types]');
|
|
98
|
+
formRows.forEach(row => {
|
|
99
|
+
row.classList.remove('field-hidden');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
__all__ = ["SCRIPTS_VIEW_MODE"]
|
|
106
|
+
|
|
107
|
+
# EOF
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Zoom and pan JavaScript for the figure editor."""
|
|
4
|
+
|
|
5
|
+
SCRIPTS_ZOOM = """
|
|
6
|
+
// ==================== ZOOM/PAN FUNCTIONS ====================
|
|
7
|
+
|
|
8
|
+
function initializeZoomPan() {
|
|
9
|
+
const wrapper = document.getElementById('preview-wrapper');
|
|
10
|
+
const container = document.getElementById('zoom-container');
|
|
11
|
+
|
|
12
|
+
if (!wrapper || !container) return;
|
|
13
|
+
|
|
14
|
+
// Zoom dropdown
|
|
15
|
+
const zoomSelect = document.getElementById('zoom-select');
|
|
16
|
+
zoomSelect?.addEventListener('change', (e) => {
|
|
17
|
+
setZoom(parseInt(e.target.value) / 100);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Fit button
|
|
21
|
+
document.getElementById('btn-zoom-fit')?.addEventListener('click', zoomToFit);
|
|
22
|
+
|
|
23
|
+
// Mouse wheel zoom
|
|
24
|
+
wrapper.addEventListener('wheel', (e) => {
|
|
25
|
+
if (e.ctrlKey || e.metaKey) {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
|
|
28
|
+
setZoom(zoomLevel + delta);
|
|
29
|
+
}
|
|
30
|
+
}, { passive: false });
|
|
31
|
+
|
|
32
|
+
// Pan with middle mouse, alt+drag, or left-click on empty area when zoomed
|
|
33
|
+
wrapper.addEventListener('mousedown', (e) => {
|
|
34
|
+
// Middle mouse or Alt+drag always pans
|
|
35
|
+
if (e.button === 1 || (e.button === 0 && e.altKey)) {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
startPan(e);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Left-click when zoomed > 100% and clicking on background (not on elements)
|
|
41
|
+
if (e.button === 0 && zoomLevel > 1.0) {
|
|
42
|
+
const target = e.target;
|
|
43
|
+
// Only pan if clicking on wrapper/container background, not on canvas elements
|
|
44
|
+
if (target.id === 'preview-wrapper' || target.classList.contains('zoom-container') ||
|
|
45
|
+
target.tagName === 'svg' || target.id === 'preview-image') {
|
|
46
|
+
// Don't pan if clicking on hitmap regions (they have data attributes)
|
|
47
|
+
const hitRegion = document.elementFromPoint(e.clientX, e.clientY);
|
|
48
|
+
if (!hitRegion || !hitRegion.closest('.hit-region')) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
startPan(e);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
wrapper.addEventListener('mousemove', (e) => {
|
|
57
|
+
if (isPanning) {
|
|
58
|
+
doPan(e);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
wrapper.addEventListener('mouseup', endPan);
|
|
63
|
+
wrapper.addEventListener('mouseleave', endPan);
|
|
64
|
+
|
|
65
|
+
// Keyboard shortcuts for zoom
|
|
66
|
+
document.addEventListener('keydown', (e) => {
|
|
67
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
68
|
+
|
|
69
|
+
if (e.key === '+' || e.key === '=') {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
setZoom(zoomLevel + ZOOM_STEP);
|
|
72
|
+
} else if (e.key === '-' || e.key === '_') {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
setZoom(zoomLevel - ZOOM_STEP);
|
|
75
|
+
} else if (e.key === '0') {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
setZoom(1.0);
|
|
78
|
+
} else if (e.key === 'f' || e.key === 'F') {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
zoomToFit();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Initialize fit to view
|
|
85
|
+
setTimeout(zoomToFit, 200);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function setZoom(newLevel) {
|
|
89
|
+
zoomLevel = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, newLevel));
|
|
90
|
+
|
|
91
|
+
const container = document.getElementById('zoom-container');
|
|
92
|
+
const wrapper = document.getElementById('preview-wrapper');
|
|
93
|
+
const img = document.getElementById('preview-image');
|
|
94
|
+
|
|
95
|
+
if (container && wrapper) {
|
|
96
|
+
container.style.transform = `scale(${zoomLevel})`;
|
|
97
|
+
|
|
98
|
+
// Update container size to enable proper scrolling
|
|
99
|
+
// Transform scale doesn't change layout size, so we set explicit dimensions
|
|
100
|
+
if (img) {
|
|
101
|
+
// Use rendered dimensions if naturalWidth not available
|
|
102
|
+
const imgWidth = img.naturalWidth || img.width || img.clientWidth;
|
|
103
|
+
const imgHeight = img.naturalHeight || img.height || img.clientHeight;
|
|
104
|
+
|
|
105
|
+
if (imgWidth && imgHeight) {
|
|
106
|
+
const scaledWidth = imgWidth * zoomLevel;
|
|
107
|
+
const scaledHeight = imgHeight * zoomLevel;
|
|
108
|
+
|
|
109
|
+
// Set container dimensions for scroll area calculation
|
|
110
|
+
container.style.width = `${imgWidth}px`;
|
|
111
|
+
container.style.height = `${imgHeight}px`;
|
|
112
|
+
container.style.minWidth = `${scaledWidth}px`;
|
|
113
|
+
container.style.minHeight = `${scaledHeight}px`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Update wrapper class for cursor hint
|
|
118
|
+
if (zoomLevel > 1.0) {
|
|
119
|
+
wrapper.classList.add('zoomed-in');
|
|
120
|
+
} else {
|
|
121
|
+
wrapper.classList.remove('zoomed-in');
|
|
122
|
+
// Reset scroll position when not zoomed
|
|
123
|
+
wrapper.scrollLeft = 0;
|
|
124
|
+
wrapper.scrollTop = 0;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Update zoom dropdown to nearest value
|
|
129
|
+
const zoomSelect = document.getElementById('zoom-select');
|
|
130
|
+
if (zoomSelect) {
|
|
131
|
+
const percent = Math.round(zoomLevel * 100);
|
|
132
|
+
// Find closest option
|
|
133
|
+
const options = Array.from(zoomSelect.options).map(o => parseInt(o.value));
|
|
134
|
+
const closest = options.reduce((prev, curr) =>
|
|
135
|
+
Math.abs(curr - percent) < Math.abs(prev - percent) ? curr : prev
|
|
136
|
+
);
|
|
137
|
+
zoomSelect.value = closest;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function zoomToFit() {
|
|
142
|
+
const wrapper = document.getElementById('preview-wrapper');
|
|
143
|
+
const img = document.getElementById('preview-image');
|
|
144
|
+
|
|
145
|
+
if (!wrapper || !img || !img.naturalWidth) return;
|
|
146
|
+
|
|
147
|
+
const wrapperRect = wrapper.getBoundingClientRect();
|
|
148
|
+
const padding = 40;
|
|
149
|
+
|
|
150
|
+
const scaleX = (wrapperRect.width - padding) / img.naturalWidth;
|
|
151
|
+
const scaleY = (wrapperRect.height - padding) / img.naturalHeight;
|
|
152
|
+
|
|
153
|
+
setZoom(Math.min(scaleX, scaleY, 1.0));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Find nearest scrollable parent element
|
|
157
|
+
function findScrollableParent(element) {
|
|
158
|
+
while (element && element !== document.body) {
|
|
159
|
+
const style = window.getComputedStyle(element);
|
|
160
|
+
const overflowY = style.overflowY;
|
|
161
|
+
const overflowX = style.overflowX;
|
|
162
|
+
const isScrollable = (overflowY === 'auto' || overflowY === 'scroll' ||
|
|
163
|
+
overflowX === 'auto' || overflowX === 'scroll');
|
|
164
|
+
const canScroll = element.scrollHeight > element.clientHeight ||
|
|
165
|
+
element.scrollWidth > element.clientWidth;
|
|
166
|
+
if (isScrollable && canScroll) {
|
|
167
|
+
return element;
|
|
168
|
+
}
|
|
169
|
+
element = element.parentElement;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function startPan(e) {
|
|
175
|
+
// Find scrollable container under mouse
|
|
176
|
+
panTarget = findScrollableParent(e.target);
|
|
177
|
+
if (!panTarget) {
|
|
178
|
+
// Fallback to preview-wrapper for canvas
|
|
179
|
+
panTarget = document.getElementById('preview-wrapper');
|
|
180
|
+
}
|
|
181
|
+
if (!panTarget) return;
|
|
182
|
+
|
|
183
|
+
isPanning = true;
|
|
184
|
+
panStartX = e.clientX;
|
|
185
|
+
panStartY = e.clientY;
|
|
186
|
+
scrollStartX = panTarget.scrollLeft;
|
|
187
|
+
scrollStartY = panTarget.scrollTop;
|
|
188
|
+
panTarget.classList.add('panning');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function doPan(e) {
|
|
192
|
+
if (!isPanning || !panTarget) return;
|
|
193
|
+
|
|
194
|
+
const dx = e.clientX - panStartX;
|
|
195
|
+
const dy = e.clientY - panStartY;
|
|
196
|
+
|
|
197
|
+
panTarget.scrollLeft = scrollStartX - dx;
|
|
198
|
+
panTarget.scrollTop = scrollStartY - dy;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function endPan() {
|
|
202
|
+
if (isPanning && panTarget) {
|
|
203
|
+
panTarget.classList.remove('panning');
|
|
204
|
+
isPanning = false;
|
|
205
|
+
panTarget = null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ==================== END ZOOM/PAN ====================
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
__all__ = ["SCRIPTS_ZOOM"]
|