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,270 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Legend drag-to-move JavaScript for the figure editor.
|
|
4
|
+
|
|
5
|
+
This module contains the JavaScript code for:
|
|
6
|
+
- Detecting mousedown on legend elements
|
|
7
|
+
- Handling drag movement with visual feedback
|
|
8
|
+
- Updating legend position on drop
|
|
9
|
+
|
|
10
|
+
Legend coordinates are in axes-relative units (0-1 range).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
SCRIPTS_LEGEND_DRAG = """
|
|
14
|
+
// ===== LEGEND DRAG-TO-MOVE =====
|
|
15
|
+
|
|
16
|
+
let isDraggingLegend = false;
|
|
17
|
+
let legendDragStartPos = null;
|
|
18
|
+
let legendDragStartBbox = null;
|
|
19
|
+
let legendDragOverlay = null;
|
|
20
|
+
let legendAxIndex = 0;
|
|
21
|
+
|
|
22
|
+
// Initialize legend drag functionality
|
|
23
|
+
function initLegendDrag() {
|
|
24
|
+
console.log('[LegendDrag] initLegendDrag called');
|
|
25
|
+
const zoomContainer = document.getElementById('zoom-container');
|
|
26
|
+
if (!zoomContainer) {
|
|
27
|
+
console.error('[LegendDrag] zoom-container not found!');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create legend drag overlay element
|
|
32
|
+
legendDragOverlay = document.createElement('div');
|
|
33
|
+
legendDragOverlay.id = 'legend-drag-overlay';
|
|
34
|
+
legendDragOverlay.style.cssText = `
|
|
35
|
+
position: absolute;
|
|
36
|
+
border: 2px dashed #10b981;
|
|
37
|
+
background: rgba(16, 185, 129, 0.1);
|
|
38
|
+
pointer-events: none;
|
|
39
|
+
display: none;
|
|
40
|
+
z-index: 1001;
|
|
41
|
+
`;
|
|
42
|
+
zoomContainer.appendChild(legendDragOverlay);
|
|
43
|
+
console.log('[LegendDrag] Overlay created');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle legend drag start (called from hitmap click handler)
|
|
47
|
+
function startLegendDrag(event, legendKey) {
|
|
48
|
+
console.log('[LegendDrag] startLegendDrag called for:', legendKey);
|
|
49
|
+
|
|
50
|
+
const img = document.getElementById('preview-image');
|
|
51
|
+
if (!img) return false;
|
|
52
|
+
|
|
53
|
+
const bbox = currentBboxes[legendKey];
|
|
54
|
+
if (!bbox) return false;
|
|
55
|
+
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
event.stopPropagation();
|
|
58
|
+
|
|
59
|
+
// Capture state before drag for undo
|
|
60
|
+
if (typeof pushToHistory === 'function') {
|
|
61
|
+
pushToHistory();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
isDraggingLegend = true;
|
|
65
|
+
legendDragStartPos = { x: event.clientX, y: event.clientY };
|
|
66
|
+
legendDragStartBbox = { ...bbox };
|
|
67
|
+
|
|
68
|
+
// Extract axis index from key (e.g., "legend_ax0" -> 0)
|
|
69
|
+
const match = legendKey.match(/ax(\\d+)/);
|
|
70
|
+
legendAxIndex = match ? parseInt(match[1], 10) : 0;
|
|
71
|
+
|
|
72
|
+
// Show drag overlay
|
|
73
|
+
if (legendDragOverlay) {
|
|
74
|
+
updateLegendDragOverlay(bbox);
|
|
75
|
+
legendDragOverlay.style.display = 'block';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add temporary event listeners
|
|
79
|
+
document.addEventListener('mousemove', handleLegendDragMove);
|
|
80
|
+
document.addEventListener('mouseup', handleLegendDragEnd);
|
|
81
|
+
|
|
82
|
+
document.body.style.cursor = 'move';
|
|
83
|
+
console.log('[LegendDrag] Started dragging legend');
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle mouse move during legend drag
|
|
88
|
+
function handleLegendDragMove(event) {
|
|
89
|
+
if (!isDraggingLegend) return;
|
|
90
|
+
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
|
|
93
|
+
const img = document.getElementById('preview-image');
|
|
94
|
+
if (!img) return;
|
|
95
|
+
|
|
96
|
+
const rect = img.getBoundingClientRect();
|
|
97
|
+
|
|
98
|
+
// Calculate delta in pixels
|
|
99
|
+
const deltaX = event.clientX - legendDragStartPos.x;
|
|
100
|
+
const deltaY = event.clientY - legendDragStartPos.y;
|
|
101
|
+
|
|
102
|
+
// Calculate new position in image pixels
|
|
103
|
+
const scaleX = img.naturalWidth / rect.width;
|
|
104
|
+
const scaleY = img.naturalHeight / rect.height;
|
|
105
|
+
|
|
106
|
+
const newBbox = {
|
|
107
|
+
x: legendDragStartBbox.x + deltaX * scaleX,
|
|
108
|
+
y: legendDragStartBbox.y + deltaY * scaleY,
|
|
109
|
+
width: legendDragStartBbox.width,
|
|
110
|
+
height: legendDragStartBbox.height
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Update visual overlay
|
|
114
|
+
updateLegendDragOverlay(newBbox);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Update the legend drag overlay position
|
|
118
|
+
function updateLegendDragOverlay(bbox) {
|
|
119
|
+
if (!legendDragOverlay) return;
|
|
120
|
+
|
|
121
|
+
const img = document.getElementById('preview-image');
|
|
122
|
+
if (!img) return;
|
|
123
|
+
|
|
124
|
+
const rect = img.getBoundingClientRect();
|
|
125
|
+
const scaleX = rect.width / img.naturalWidth;
|
|
126
|
+
const scaleY = rect.height / img.naturalHeight;
|
|
127
|
+
|
|
128
|
+
const left = bbox.x * scaleX;
|
|
129
|
+
const top = bbox.y * scaleY;
|
|
130
|
+
const width = bbox.width * scaleX;
|
|
131
|
+
const height = bbox.height * scaleY;
|
|
132
|
+
|
|
133
|
+
legendDragOverlay.style.left = `${left}px`;
|
|
134
|
+
legendDragOverlay.style.top = `${top}px`;
|
|
135
|
+
legendDragOverlay.style.width = `${width}px`;
|
|
136
|
+
legendDragOverlay.style.height = `${height}px`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Handle mouse up - complete the legend drag
|
|
140
|
+
async function handleLegendDragEnd(event) {
|
|
141
|
+
console.log('[LegendDrag] handleLegendDragEnd called');
|
|
142
|
+
if (!isDraggingLegend) return;
|
|
143
|
+
|
|
144
|
+
// Remove temporary event listeners
|
|
145
|
+
document.removeEventListener('mousemove', handleLegendDragMove);
|
|
146
|
+
document.removeEventListener('mouseup', handleLegendDragEnd);
|
|
147
|
+
|
|
148
|
+
// Hide overlay
|
|
149
|
+
if (legendDragOverlay) {
|
|
150
|
+
legendDragOverlay.style.display = 'none';
|
|
151
|
+
}
|
|
152
|
+
document.body.style.cursor = '';
|
|
153
|
+
|
|
154
|
+
const img = document.getElementById('preview-image');
|
|
155
|
+
if (!img) {
|
|
156
|
+
isDraggingLegend = false;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const rect = img.getBoundingClientRect();
|
|
161
|
+
|
|
162
|
+
// Calculate delta in pixels
|
|
163
|
+
const deltaX = event.clientX - legendDragStartPos.x;
|
|
164
|
+
const deltaY = event.clientY - legendDragStartPos.y;
|
|
165
|
+
|
|
166
|
+
// Only update if moved significantly (5px threshold)
|
|
167
|
+
if (Math.abs(deltaX) < 5 && Math.abs(deltaY) < 5) {
|
|
168
|
+
console.log('[LegendDrag] Movement below threshold, not updating');
|
|
169
|
+
isDraggingLegend = false;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Convert to axes-relative coordinates (0-1 range)
|
|
174
|
+
// We need to get the axes position to calculate relative coords
|
|
175
|
+
const axKey = Object.keys(panelPositions).sort()[legendAxIndex];
|
|
176
|
+
const axPos = panelPositions[axKey];
|
|
177
|
+
|
|
178
|
+
if (!axPos || !figSize.width_mm || !figSize.height_mm) {
|
|
179
|
+
console.error('[LegendDrag] Cannot calculate axes-relative position');
|
|
180
|
+
isDraggingLegend = false;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Calculate scale factors: screen pixels to image pixels
|
|
185
|
+
const screenToImgX = img.naturalWidth / rect.width;
|
|
186
|
+
const screenToImgY = img.naturalHeight / rect.height;
|
|
187
|
+
|
|
188
|
+
// New legend upper-left corner in image pixels
|
|
189
|
+
const newImgX = legendDragStartBbox.x + deltaX * screenToImgX;
|
|
190
|
+
const newImgY = legendDragStartBbox.y + deltaY * screenToImgY;
|
|
191
|
+
|
|
192
|
+
// Convert image pixels to mm (upper-left origin)
|
|
193
|
+
const newMmX = newImgX / img.naturalWidth * figSize.width_mm;
|
|
194
|
+
const newMmY = newImgY / img.naturalHeight * figSize.height_mm;
|
|
195
|
+
|
|
196
|
+
// Convert to axes-relative (0-1) coordinates
|
|
197
|
+
// Use upper-left corner since we set _loc=2 (upper left) in backend
|
|
198
|
+
const relX = (newMmX - axPos.left) / axPos.width;
|
|
199
|
+
const relY = 1 - (newMmY - axPos.top) / axPos.height; // Flip Y (matplotlib uses bottom-left origin)
|
|
200
|
+
|
|
201
|
+
console.log('[LegendDrag] New legend position (rel):', relX.toFixed(3), relY.toFixed(3));
|
|
202
|
+
|
|
203
|
+
// Apply the new position
|
|
204
|
+
await applyLegendPosition(legendAxIndex, relX, relY);
|
|
205
|
+
|
|
206
|
+
// Reset state
|
|
207
|
+
isDraggingLegend = false;
|
|
208
|
+
legendDragStartPos = null;
|
|
209
|
+
legendDragStartBbox = null;
|
|
210
|
+
console.log('[LegendDrag] Drag state reset');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Apply the dragged legend position to the server
|
|
214
|
+
async function applyLegendPosition(axIndex, x, y) {
|
|
215
|
+
document.body.classList.add('loading');
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const response = await fetch('/update_legend_position', {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: { 'Content-Type': 'application/json' },
|
|
221
|
+
body: JSON.stringify({
|
|
222
|
+
ax_index: axIndex,
|
|
223
|
+
loc: 'custom',
|
|
224
|
+
x: x,
|
|
225
|
+
y: y
|
|
226
|
+
})
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const data = await response.json();
|
|
230
|
+
|
|
231
|
+
if (data.success) {
|
|
232
|
+
// Update preview image
|
|
233
|
+
const img = document.getElementById('preview-image');
|
|
234
|
+
if (img) {
|
|
235
|
+
await new Promise((resolve) => {
|
|
236
|
+
img.onload = resolve;
|
|
237
|
+
img.src = 'data:image/png;base64,' + data.image;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Update bboxes and hitmap
|
|
242
|
+
if (data.bboxes) {
|
|
243
|
+
currentBboxes = data.bboxes;
|
|
244
|
+
loadHitmap();
|
|
245
|
+
updateHitRegions();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log('[LegendDrag] Legend position updated successfully');
|
|
249
|
+
} else {
|
|
250
|
+
console.error('[LegendDrag] Failed to update legend:', data.error);
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error('[LegendDrag] Failed to update legend:', error);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
document.body.classList.remove('loading');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check if a key refers to a legend element
|
|
260
|
+
function isLegendElement(key) {
|
|
261
|
+
return key && key.startsWith('legend_');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Initialize on DOMContentLoaded
|
|
265
|
+
document.addEventListener('DOMContentLoaded', initLegendDrag);
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
__all__ = ["SCRIPTS_LEGEND_DRAG"]
|
|
269
|
+
|
|
270
|
+
# EOF
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Modal dialogs JavaScript for the figure editor.
|
|
4
|
+
|
|
5
|
+
This module contains the JavaScript code for:
|
|
6
|
+
- Theme modal (view, download, copy theme)
|
|
7
|
+
- Shortcuts modal
|
|
8
|
+
- Theme switching
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
SCRIPTS_MODALS = """
|
|
12
|
+
// ===== MODAL DIALOGS =====
|
|
13
|
+
|
|
14
|
+
// Initialize theme modal handlers
|
|
15
|
+
function initializeThemeModal() {
|
|
16
|
+
const modal = document.getElementById('theme-modal');
|
|
17
|
+
const themeSelector = document.getElementById('theme-selector');
|
|
18
|
+
const btnView = document.getElementById('btn-view-theme');
|
|
19
|
+
const btnDownload = document.getElementById('btn-download-theme');
|
|
20
|
+
const btnCopy = document.getElementById('btn-copy-theme');
|
|
21
|
+
const modalClose = document.getElementById('theme-modal-close');
|
|
22
|
+
const modalDownload = document.getElementById('theme-modal-download');
|
|
23
|
+
const modalCopy = document.getElementById('theme-modal-copy');
|
|
24
|
+
|
|
25
|
+
// Theme selector change handler
|
|
26
|
+
if (themeSelector) {
|
|
27
|
+
loadCurrentTheme();
|
|
28
|
+
themeSelector.addEventListener('change', function() {
|
|
29
|
+
switchTheme(this.value);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// View button opens modal
|
|
34
|
+
if (btnView) btnView.addEventListener('click', showThemeModal);
|
|
35
|
+
|
|
36
|
+
// Download and copy buttons
|
|
37
|
+
if (btnDownload) btnDownload.addEventListener('click', downloadTheme);
|
|
38
|
+
if (btnCopy) btnCopy.addEventListener('click', copyTheme);
|
|
39
|
+
|
|
40
|
+
// Modal close
|
|
41
|
+
if (modalClose) modalClose.addEventListener('click', hideThemeModal);
|
|
42
|
+
|
|
43
|
+
// Modal buttons
|
|
44
|
+
if (modalDownload) modalDownload.addEventListener('click', downloadTheme);
|
|
45
|
+
if (modalCopy) modalCopy.addEventListener('click', copyTheme);
|
|
46
|
+
|
|
47
|
+
// Close modal on outside click
|
|
48
|
+
if (modal) {
|
|
49
|
+
modal.addEventListener('click', function(e) {
|
|
50
|
+
if (e.target === modal) hideThemeModal();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Show theme modal
|
|
56
|
+
async function showThemeModal() {
|
|
57
|
+
const modal = document.getElementById('theme-modal');
|
|
58
|
+
const themeContent = document.getElementById('theme-content');
|
|
59
|
+
const themeModalName = document.getElementById('theme-modal-name');
|
|
60
|
+
const themeSelector = document.getElementById('theme-selector');
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch('/theme');
|
|
64
|
+
const data = await response.json();
|
|
65
|
+
|
|
66
|
+
const themeName = themeSelector ? themeSelector.value : data.name;
|
|
67
|
+
if (themeModalName) themeModalName.textContent = themeName;
|
|
68
|
+
if (themeContent) themeContent.textContent = data.content;
|
|
69
|
+
if (modal) modal.style.display = 'flex';
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Failed to load theme:', error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Hide theme modal
|
|
76
|
+
function hideThemeModal() {
|
|
77
|
+
const modal = document.getElementById('theme-modal');
|
|
78
|
+
if (modal) modal.style.display = 'none';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Initialize shortcuts modal handlers
|
|
82
|
+
function initializeShortcutsModal() {
|
|
83
|
+
const modal = document.getElementById('shortcuts-modal');
|
|
84
|
+
const btnShortcuts = document.getElementById('btn-shortcuts');
|
|
85
|
+
const modalClose = document.getElementById('shortcuts-modal-close');
|
|
86
|
+
|
|
87
|
+
if (btnShortcuts) btnShortcuts.addEventListener('click', showShortcutsModal);
|
|
88
|
+
if (modalClose) modalClose.addEventListener('click', hideShortcutsModal);
|
|
89
|
+
|
|
90
|
+
if (modal) {
|
|
91
|
+
modal.addEventListener('click', function(e) {
|
|
92
|
+
if (e.target === modal) hideShortcutsModal();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Show/hide shortcuts modal
|
|
98
|
+
function showShortcutsModal() {
|
|
99
|
+
const modal = document.getElementById('shortcuts-modal');
|
|
100
|
+
if (modal) modal.style.display = 'flex';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function hideShortcutsModal() {
|
|
104
|
+
const modal = document.getElementById('shortcuts-modal');
|
|
105
|
+
if (modal) modal.style.display = 'none';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Download theme as YAML
|
|
109
|
+
async function downloadTheme() {
|
|
110
|
+
try {
|
|
111
|
+
const response = await fetch('/theme');
|
|
112
|
+
const data = await response.json();
|
|
113
|
+
|
|
114
|
+
const blob = new Blob([data.content], { type: 'text/yaml' });
|
|
115
|
+
const url = URL.createObjectURL(blob);
|
|
116
|
+
const a = document.createElement('a');
|
|
117
|
+
a.href = url;
|
|
118
|
+
a.download = data.name + '.yaml';
|
|
119
|
+
document.body.appendChild(a);
|
|
120
|
+
a.click();
|
|
121
|
+
document.body.removeChild(a);
|
|
122
|
+
URL.revokeObjectURL(url);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('Failed to download theme:', error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Copy theme to clipboard
|
|
129
|
+
async function copyTheme() {
|
|
130
|
+
try {
|
|
131
|
+
const response = await fetch('/theme');
|
|
132
|
+
const data = await response.json();
|
|
133
|
+
|
|
134
|
+
await navigator.clipboard.writeText(data.content);
|
|
135
|
+
|
|
136
|
+
const btn = document.getElementById('btn-copy-theme');
|
|
137
|
+
const originalText = btn.textContent;
|
|
138
|
+
btn.textContent = 'Copied!';
|
|
139
|
+
setTimeout(() => { btn.textContent = originalText; }, 1500);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('Failed to copy theme:', error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Load current theme and set selector
|
|
146
|
+
async function loadCurrentTheme() {
|
|
147
|
+
try {
|
|
148
|
+
const response = await fetch('/list_themes');
|
|
149
|
+
const data = await response.json();
|
|
150
|
+
|
|
151
|
+
const selector = document.getElementById('theme-selector');
|
|
152
|
+
if (selector && data.current) {
|
|
153
|
+
selector.value = data.current;
|
|
154
|
+
}
|
|
155
|
+
console.log('Current theme:', data.current);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error('Failed to load current theme:', error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Switch to a different theme preset
|
|
162
|
+
async function switchTheme(themeName) {
|
|
163
|
+
console.log('Switching theme to:', themeName);
|
|
164
|
+
document.body.classList.add('loading');
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const response = await fetch('/switch_theme', {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: { 'Content-Type': 'application/json' },
|
|
170
|
+
body: JSON.stringify({ theme: themeName })
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const result = await response.json();
|
|
174
|
+
|
|
175
|
+
if (result.success) {
|
|
176
|
+
const previewImg = document.getElementById('preview-image');
|
|
177
|
+
previewImg.src = 'data:image/png;base64,' + result.image;
|
|
178
|
+
|
|
179
|
+
if (result.img_size) {
|
|
180
|
+
currentImgWidth = result.img_size.width;
|
|
181
|
+
currentImgHeight = result.img_size.height;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Update form values from new theme
|
|
185
|
+
if (result.values) {
|
|
186
|
+
for (const [key, value] of Object.entries(result.values)) {
|
|
187
|
+
const element = document.getElementById(key);
|
|
188
|
+
if (element) {
|
|
189
|
+
if (element.type === 'checkbox') {
|
|
190
|
+
element.checked = Boolean(value);
|
|
191
|
+
} else {
|
|
192
|
+
element.value = value;
|
|
193
|
+
}
|
|
194
|
+
if (element.placeholder !== undefined) {
|
|
195
|
+
element.placeholder = value;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
Object.assign(themeDefaults, result.values);
|
|
200
|
+
updateAllModifiedStates();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (result.bboxes) {
|
|
204
|
+
currentBboxes = result.bboxes;
|
|
205
|
+
previewImg.onload = () => {
|
|
206
|
+
updateHitRegions();
|
|
207
|
+
loadHitmap();
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
console.log('Theme switched to:', themeName);
|
|
211
|
+
} else {
|
|
212
|
+
console.error('Theme switch failed:', result.error);
|
|
213
|
+
loadCurrentTheme();
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error('Failed to switch theme:', error);
|
|
217
|
+
loadCurrentTheme();
|
|
218
|
+
} finally {
|
|
219
|
+
document.body.classList.remove('loading');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
__all__ = ["SCRIPTS_MODALS"]
|
|
225
|
+
|
|
226
|
+
# EOF
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Multi-selection JavaScript for the figure editor.
|
|
4
|
+
|
|
5
|
+
This module contains the JavaScript code for:
|
|
6
|
+
- Ctrl+Click to add/remove elements from selection
|
|
7
|
+
- Managing multiple selected elements
|
|
8
|
+
- Drawing multi-selection highlights
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
SCRIPTS_MULTI_SELECT = """
|
|
12
|
+
// ===== MULTI-SELECTION (Ctrl+Click) =====
|
|
13
|
+
|
|
14
|
+
// Array of selected elements (each element is {key, type, x, y, width, height, ...})
|
|
15
|
+
let selectedElements = [];
|
|
16
|
+
|
|
17
|
+
// Check if multi-select mode is active (Ctrl or Cmd key held)
|
|
18
|
+
function isMultiSelectMode(event) {
|
|
19
|
+
return event && (event.ctrlKey || event.metaKey);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Check if an element is currently selected
|
|
23
|
+
function isElementSelected(key) {
|
|
24
|
+
return selectedElements.some(el => el.key === key);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Add element to selection (if not already selected)
|
|
28
|
+
function addToSelection(element) {
|
|
29
|
+
if (!element || !element.key) return;
|
|
30
|
+
if (isElementSelected(element.key)) return;
|
|
31
|
+
|
|
32
|
+
selectedElements.push(element);
|
|
33
|
+
console.log('[MultiSelect] Added to selection:', element.key, '- total:', selectedElements.length);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Remove element from selection
|
|
37
|
+
function removeFromSelection(key) {
|
|
38
|
+
const idx = selectedElements.findIndex(el => el.key === key);
|
|
39
|
+
if (idx >= 0) {
|
|
40
|
+
selectedElements.splice(idx, 1);
|
|
41
|
+
console.log('[MultiSelect] Removed from selection:', key, '- total:', selectedElements.length);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Toggle element in selection
|
|
46
|
+
function toggleInSelection(element) {
|
|
47
|
+
if (!element || !element.key) return;
|
|
48
|
+
|
|
49
|
+
if (isElementSelected(element.key)) {
|
|
50
|
+
removeFromSelection(element.key);
|
|
51
|
+
} else {
|
|
52
|
+
addToSelection(element);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Clear all selections
|
|
57
|
+
function clearMultiSelection() {
|
|
58
|
+
selectedElements = [];
|
|
59
|
+
console.log('[MultiSelect] Selection cleared');
|
|
60
|
+
|
|
61
|
+
// Clear visual selection
|
|
62
|
+
const selOverlay = document.getElementById('selection-overlay');
|
|
63
|
+
if (selOverlay) {
|
|
64
|
+
const svg = selOverlay.querySelector('svg');
|
|
65
|
+
if (svg) {
|
|
66
|
+
// Remove multi-selection highlights
|
|
67
|
+
svg.querySelectorAll('.multi-select-highlight').forEach(el => el.remove());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Draw multi-selection highlights on the selection overlay
|
|
73
|
+
function drawMultiSelection() {
|
|
74
|
+
const selOverlay = document.getElementById('selection-overlay');
|
|
75
|
+
if (!selOverlay) return;
|
|
76
|
+
|
|
77
|
+
let svg = selOverlay.querySelector('svg');
|
|
78
|
+
if (!svg) {
|
|
79
|
+
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
80
|
+
svg.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;';
|
|
81
|
+
selOverlay.appendChild(svg);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Remove existing multi-selection highlights
|
|
85
|
+
svg.querySelectorAll('.multi-select-highlight').forEach(el => el.remove());
|
|
86
|
+
|
|
87
|
+
const img = document.getElementById('preview-image');
|
|
88
|
+
if (!img || !figSize.width_mm || !figSize.height_mm) return;
|
|
89
|
+
|
|
90
|
+
const imgRect = img.getBoundingClientRect();
|
|
91
|
+
const containerRect = selOverlay.getBoundingClientRect();
|
|
92
|
+
|
|
93
|
+
// Update SVG viewBox to match image
|
|
94
|
+
svg.setAttribute('viewBox', `0 0 ${imgRect.width} ${imgRect.height}`);
|
|
95
|
+
svg.style.width = imgRect.width + 'px';
|
|
96
|
+
svg.style.height = imgRect.height + 'px';
|
|
97
|
+
svg.style.left = (imgRect.left - containerRect.left) + 'px';
|
|
98
|
+
svg.style.top = (imgRect.top - containerRect.top) + 'px';
|
|
99
|
+
|
|
100
|
+
// Draw highlight for each selected element
|
|
101
|
+
selectedElements.forEach(element => {
|
|
102
|
+
if (!element.x || !element.width) return;
|
|
103
|
+
|
|
104
|
+
// Convert from image pixels to display pixels
|
|
105
|
+
const scaleX = imgRect.width / img.naturalWidth;
|
|
106
|
+
const scaleY = imgRect.height / img.naturalHeight;
|
|
107
|
+
|
|
108
|
+
const x = element.x * scaleX;
|
|
109
|
+
const y = element.y * scaleY;
|
|
110
|
+
const w = element.width * scaleX;
|
|
111
|
+
const h = element.height * scaleY;
|
|
112
|
+
|
|
113
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
114
|
+
rect.setAttribute('class', 'multi-select-highlight');
|
|
115
|
+
rect.setAttribute('x', x);
|
|
116
|
+
rect.setAttribute('y', y);
|
|
117
|
+
rect.setAttribute('width', w);
|
|
118
|
+
rect.setAttribute('height', h);
|
|
119
|
+
rect.setAttribute('fill', 'rgba(37, 99, 235, 0.15)');
|
|
120
|
+
rect.setAttribute('stroke', '#2563eb');
|
|
121
|
+
rect.setAttribute('stroke-width', '2');
|
|
122
|
+
rect.setAttribute('stroke-dasharray', '4,2');
|
|
123
|
+
svg.appendChild(rect);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
console.log('[MultiSelect] Drew selection for', selectedElements.length, 'elements');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Select all elements of a specific type
|
|
130
|
+
function selectAllOfType(type) {
|
|
131
|
+
clearMultiSelection();
|
|
132
|
+
|
|
133
|
+
for (const [key, bbox] of Object.entries(currentBboxes)) {
|
|
134
|
+
if (key === '_meta') continue;
|
|
135
|
+
if (!bbox || typeof bbox.x === 'undefined') continue;
|
|
136
|
+
|
|
137
|
+
const info = (colorMap && colorMap[key]) || {};
|
|
138
|
+
if (info.type === type || bbox.type === type) {
|
|
139
|
+
addToSelection({ key, ...bbox, ...info });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
drawMultiSelection();
|
|
144
|
+
console.log('[MultiSelect] Selected all', type, 'elements:', selectedElements.length);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Get indices of selected panels (axes)
|
|
148
|
+
function getSelectedPanelIndices() {
|
|
149
|
+
return selectedElements
|
|
150
|
+
.filter(el => el.key && el.key.includes('_axes'))
|
|
151
|
+
.map(el => {
|
|
152
|
+
const match = el.key.match(/ax(\\d+)_axes/);
|
|
153
|
+
return match ? parseInt(match[1], 10) : -1;
|
|
154
|
+
})
|
|
155
|
+
.filter(idx => idx >= 0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Update UI to show multi-selection state
|
|
159
|
+
function updateMultiSelectionUI() {
|
|
160
|
+
const countEl = document.getElementById('multi-select-count');
|
|
161
|
+
if (countEl) {
|
|
162
|
+
countEl.textContent = selectedElements.length > 1
|
|
163
|
+
? `${selectedElements.length} selected`
|
|
164
|
+
: '';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Handle keyboard shortcuts for multi-selection
|
|
169
|
+
function handleMultiSelectKeyboard(event) {
|
|
170
|
+
// Ctrl+A: Select all panels
|
|
171
|
+
if ((event.ctrlKey || event.metaKey) && event.key === 'a') {
|
|
172
|
+
// Only if not in an input field
|
|
173
|
+
if (event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
selectAllOfType('axes');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Escape: Clear selection
|
|
180
|
+
if (event.key === 'Escape') {
|
|
181
|
+
clearMultiSelection();
|
|
182
|
+
drawMultiSelection();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Initialize multi-selection support
|
|
187
|
+
function initMultiSelect() {
|
|
188
|
+
console.log('[MultiSelect] Initializing multi-selection support');
|
|
189
|
+
document.addEventListener('keydown', handleMultiSelectKeyboard);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Initialize on DOMContentLoaded
|
|
193
|
+
document.addEventListener('DOMContentLoaded', initMultiSelect);
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
__all__ = ["SCRIPTS_MULTI_SELECT"]
|
|
197
|
+
|
|
198
|
+
# EOF
|