figrecipe 0.7.4__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.
Files changed (143) hide show
  1. figrecipe/__init__.py +74 -76
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/_panel.py +67 -0
  4. figrecipe/_api/_save.py +100 -4
  5. figrecipe/_cli/__init__.py +7 -0
  6. figrecipe/_cli/_compose.py +87 -0
  7. figrecipe/_cli/_convert.py +117 -0
  8. figrecipe/_cli/_crop.py +82 -0
  9. figrecipe/_cli/_edit.py +70 -0
  10. figrecipe/_cli/_extract.py +128 -0
  11. figrecipe/_cli/_fonts.py +47 -0
  12. figrecipe/_cli/_info.py +67 -0
  13. figrecipe/_cli/_main.py +58 -0
  14. figrecipe/_cli/_reproduce.py +79 -0
  15. figrecipe/_cli/_style.py +77 -0
  16. figrecipe/_cli/_validate.py +66 -0
  17. figrecipe/_cli/_version.py +50 -0
  18. figrecipe/_composition/__init__.py +32 -0
  19. figrecipe/_composition/_alignment.py +452 -0
  20. figrecipe/_composition/_compose.py +179 -0
  21. figrecipe/_composition/_import_axes.py +127 -0
  22. figrecipe/_composition/_visibility.py +125 -0
  23. figrecipe/_dev/__init__.py +2 -0
  24. figrecipe/_dev/browser/__init__.py +69 -0
  25. figrecipe/_dev/browser/_audio.py +240 -0
  26. figrecipe/_dev/browser/_caption.py +356 -0
  27. figrecipe/_dev/browser/_click_effect.py +146 -0
  28. figrecipe/_dev/browser/_cursor.py +196 -0
  29. figrecipe/_dev/browser/_highlight.py +105 -0
  30. figrecipe/_dev/browser/_narration.py +237 -0
  31. figrecipe/_dev/browser/_recorder.py +446 -0
  32. figrecipe/_dev/browser/_utils.py +178 -0
  33. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  34. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  35. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  36. figrecipe/_editor/__init__.py +36 -36
  37. figrecipe/_editor/_bbox/_extract.py +155 -9
  38. figrecipe/_editor/_bbox/_extract_text.py +124 -0
  39. figrecipe/_editor/_call_overrides.py +183 -0
  40. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  41. figrecipe/_editor/_figure_layout.py +211 -0
  42. figrecipe/_editor/_flask_app.py +157 -16
  43. figrecipe/_editor/_helpers.py +17 -8
  44. figrecipe/_editor/_hitmap/_detect.py +89 -32
  45. figrecipe/_editor/_hitmap_main.py +4 -4
  46. figrecipe/_editor/_overrides.py +4 -1
  47. figrecipe/_editor/_plot_types_registry.py +190 -0
  48. figrecipe/_editor/_render_overrides.py +38 -11
  49. figrecipe/_editor/_renderer.py +46 -1
  50. figrecipe/_editor/_routes_annotation.py +114 -0
  51. figrecipe/_editor/_routes_axis.py +35 -6
  52. figrecipe/_editor/_routes_captions.py +130 -0
  53. figrecipe/_editor/_routes_composition.py +270 -0
  54. figrecipe/_editor/_routes_core.py +15 -173
  55. figrecipe/_editor/_routes_datatable.py +364 -0
  56. figrecipe/_editor/_routes_element.py +37 -19
  57. figrecipe/_editor/_routes_files.py +443 -0
  58. figrecipe/_editor/_routes_image.py +200 -0
  59. figrecipe/_editor/_routes_snapshot.py +94 -0
  60. figrecipe/_editor/_routes_style.py +28 -8
  61. figrecipe/_editor/_templates/__init__.py +40 -2
  62. figrecipe/_editor/_templates/_html.py +97 -103
  63. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  64. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  65. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  66. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  67. figrecipe/_editor/_templates/_scripts/__init__.py +58 -0
  68. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  69. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  70. figrecipe/_editor/_templates/_scripts/_api.py +1 -1
  71. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  72. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  73. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  74. figrecipe/_editor/_templates/_scripts/_core.py +94 -37
  75. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  76. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  77. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  78. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  79. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  80. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  81. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  82. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  83. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  84. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  85. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  86. figrecipe/_editor/_templates/_scripts/_element_editor.py +17 -2
  87. figrecipe/_editor/_templates/_scripts/_files.py +274 -40
  88. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  89. figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
  90. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  91. figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
  92. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  93. figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
  94. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  95. figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
  96. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  97. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  98. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  99. figrecipe/_editor/_templates/_scripts/_selection.py +8 -1
  100. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  101. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  102. figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
  103. figrecipe/_editor/_templates/_styles/__init__.py +9 -0
  104. figrecipe/_editor/_templates/_styles/_base.py +47 -0
  105. figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
  106. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  107. figrecipe/_editor/_templates/_styles/_controls.py +168 -3
  108. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  109. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  110. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  111. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  112. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  113. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  114. figrecipe/_editor/_templates/_styles/_dynamic_props.py +5 -5
  115. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  116. figrecipe/_editor/_templates/_styles/_forms.py +98 -0
  117. figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
  118. figrecipe/_editor/_templates/_styles/_modals.py +29 -0
  119. figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
  120. figrecipe/_editor/_templates/_styles/_preview.py +213 -8
  121. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  122. figrecipe/_editor/static/audio/click.mp3 +0 -0
  123. figrecipe/_editor/static/click.mp3 +0 -0
  124. figrecipe/_editor/static/icons/favicon.ico +0 -0
  125. figrecipe/_integrations/__init__.py +17 -0
  126. figrecipe/_integrations/_scitex_stats.py +298 -0
  127. figrecipe/_params/_DECORATION_METHODS.py +2 -0
  128. figrecipe/_recorder.py +28 -3
  129. figrecipe/_reproducer/_core.py +60 -49
  130. figrecipe/_utils/__init__.py +3 -0
  131. figrecipe/_utils/_bundle.py +205 -0
  132. figrecipe/_wrappers/_axes.py +150 -2
  133. figrecipe/_wrappers/_caption_generator.py +218 -0
  134. figrecipe/_wrappers/_figure.py +26 -1
  135. figrecipe/_wrappers/_stat_annotation.py +274 -0
  136. figrecipe/styles/_style_applier.py +10 -2
  137. figrecipe/styles/presets/SCITEX.yaml +11 -4
  138. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
  139. figrecipe-0.9.0.dist-info/RECORD +277 -0
  140. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  141. figrecipe-0.7.4.dist-info/RECORD +0 -188
  142. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  143. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -18,6 +18,11 @@ let draggedPanelIndex = null;
18
18
  let dragStartPos = null;
19
19
  let dragStartPanelPos = null;
20
20
  let panelDragOverlay = null;
21
+ let panelBboxDragOverlay = null; // Outer panel bbox overlay
22
+ let panelHoverOverlay = null;
23
+ let hoveredPanelIndex = null;
24
+ let dragStartPanelBbox = null; // Initial panel bbox in pixels
25
+ let panelBboxOffset = null; // Offset from axis to panel bbox (in mm)
21
26
 
22
27
  // Initialize panel drag functionality
23
28
  function initPanelDrag() {
@@ -34,48 +39,68 @@ function initPanelDrag() {
34
39
  document.addEventListener('mouseup', handlePanelDragEnd);
35
40
  console.log('[PanelDrag] Event listeners attached');
36
41
 
37
- // Create drag overlay element
42
+ // Create drag overlay for axis bbox (inner, subtle orange)
38
43
  panelDragOverlay = document.createElement('div');
39
44
  panelDragOverlay.id = 'panel-drag-overlay';
40
45
  panelDragOverlay.style.cssText = `
46
+ position: absolute;
47
+ border: 2px dashed #f59e0b;
48
+ background: rgba(245, 158, 11, 0.08);
49
+ pointer-events: none;
50
+ display: none;
51
+ z-index: 999;
52
+ `;
53
+ zoomContainer.appendChild(panelDragOverlay);
54
+
55
+ // Create outer panel bbox overlay (prominent blue)
56
+ panelBboxDragOverlay = document.createElement('div');
57
+ panelBboxDragOverlay.id = 'panel-bbox-drag-overlay';
58
+ panelBboxDragOverlay.style.cssText = `
41
59
  position: absolute;
42
60
  border: 2px dashed #2563eb;
43
- background: rgba(37, 99, 235, 0.1);
61
+ background: rgba(37, 99, 235, 0.05);
44
62
  pointer-events: none;
45
63
  display: none;
46
64
  z-index: 1000;
47
65
  `;
48
- zoomContainer.appendChild(panelDragOverlay);
49
- console.log('[PanelDrag] Overlay created:', panelDragOverlay ? 'success' : 'failed');
66
+ zoomContainer.appendChild(panelBboxDragOverlay);
67
+
68
+ // Create hover overlay element for visual feedback
69
+ panelHoverOverlay = document.createElement('div');
70
+ panelHoverOverlay.id = 'panel-hover-overlay';
71
+ panelHoverOverlay.style.cssText = `
72
+ position: absolute;
73
+ border: 2px solid rgba(37, 99, 235, 0.5);
74
+ background: rgba(37, 99, 235, 0.05);
75
+ pointer-events: none;
76
+ display: none;
77
+ z-index: 999;
78
+ transition: opacity 0.15s ease-in-out;
79
+ `;
80
+ zoomContainer.appendChild(panelHoverOverlay);
81
+
82
+ // Add hover detection on zoom container
83
+ zoomContainer.addEventListener('mousemove', handlePanelHover);
84
+ zoomContainer.addEventListener('mouseleave', hidePanelHover);
85
+
86
+ console.log('[PanelDrag] Overlays created');
50
87
  }
51
88
 
52
89
  // Handle mouse down - check if on a panel/axes (only drag from empty panel area)
53
90
  function handlePanelDragStart(event) {
54
- console.log('[PanelDrag] handlePanelDragStart called, button:', event.button);
55
- // Skip if using modifier keys for other actions
56
- if (event.ctrlKey || event.metaKey || event.altKey) {
57
- console.log('[PanelDrag] Skipped - modifier key pressed');
58
- return;
59
- }
91
+ if (event.ctrlKey || event.metaKey || event.altKey) return; // Skip modifier keys
60
92
 
61
- // Only start drag if clicking on axes element or empty panel area
62
- // Skip if clicking on specific elements (they should be selected instead)
93
+ // Only allow drag from axes/imshow/contour/quadmesh/quiver (fills panel area)
63
94
  const target = event.target;
64
95
  const targetKey = target.getAttribute ? target.getAttribute('data-key') : null;
65
96
  if (targetKey && typeof currentBboxes !== 'undefined' && currentBboxes[targetKey]) {
66
97
  const elemType = currentBboxes[targetKey].type;
67
- // Only allow drag from axes bbox or if no specific element type
68
- if (elemType && elemType !== 'axes') {
69
- console.log('[PanelDrag] Skipped - clicked on element:', elemType);
70
- return;
71
- }
98
+ const dragAllowedTypes = ['axes', 'image', 'contour', 'quadmesh', 'quiver'];
99
+ if (elemType && !dragAllowedTypes.includes(elemType)) return;
72
100
  }
73
101
 
74
102
  const img = document.getElementById('preview-image');
75
- if (!img || !figSize.width_mm || !figSize.height_mm) {
76
- console.log('[PanelDrag] Skipped - img or figSize not ready');
77
- return;
78
- }
103
+ if (!img || !figSize.width_mm || !figSize.height_mm) return;
79
104
 
80
105
  const rect = img.getBoundingClientRect();
81
106
  const x = event.clientX - rect.left;
@@ -93,15 +118,41 @@ function handlePanelDragStart(event) {
93
118
  event.preventDefault();
94
119
  event.stopPropagation();
95
120
 
121
+ // Capture state before drag for undo
122
+ if (typeof pushToHistory === 'function') {
123
+ pushToHistory();
124
+ }
125
+
96
126
  isDraggingPanel = true;
97
127
  draggedPanelIndex = panelIndex;
98
128
  dragStartPos = { x: event.clientX, y: event.clientY };
99
129
 
130
+ // Hide hover overlay when starting drag
131
+ hidePanelHover();
132
+
100
133
  // Get current panel position (in mm)
101
134
  const axKey = Object.keys(panelPositions).sort()[panelIndex];
102
135
  const pos = panelPositions[axKey];
103
136
  dragStartPanelPos = { ...pos };
104
137
 
138
+ // Get panel bbox (outer bounds including labels) and calculate offset from axis
139
+ const panelBboxes = currentBboxes?._meta?.panel_bboxes;
140
+ if (panelBboxes && panelBboxes[panelIndex] && img.naturalWidth) {
141
+ dragStartPanelBbox = { ...panelBboxes[panelIndex] };
142
+ // Convert panel bbox to mm and calculate offset from axis position
143
+ const pxToMmX = figSize.width_mm / img.naturalWidth;
144
+ const pxToMmY = figSize.height_mm / img.naturalHeight;
145
+ panelBboxOffset = {
146
+ left: dragStartPanelBbox.x * pxToMmX - pos.left,
147
+ top: dragStartPanelBbox.y * pxToMmY - pos.top,
148
+ width: dragStartPanelBbox.width * pxToMmX,
149
+ height: dragStartPanelBbox.height * pxToMmY
150
+ };
151
+ } else {
152
+ dragStartPanelBbox = null;
153
+ panelBboxOffset = null;
154
+ }
155
+
105
156
  // Create overlay if it doesn't exist
106
157
  if (!panelDragOverlay) {
107
158
  console.log('[PanelDrag] Creating overlay on-demand');
@@ -122,15 +173,27 @@ function handlePanelDragStart(event) {
122
173
  }
123
174
  }
124
175
 
125
- // Show drag overlay
176
+ // Show drag overlay (axis bbox)
126
177
  if (panelDragOverlay) {
127
178
  updateDragOverlayMm(pos, rect);
128
179
  panelDragOverlay.style.display = 'block';
129
- console.log('[PanelDrag] Overlay shown');
180
+ console.log('[PanelDrag] Axis overlay shown');
130
181
  } else {
131
182
  console.warn('[PanelDrag] Overlay still null after creation attempt');
132
183
  }
133
184
 
185
+ // Show panel bbox overlay (outer bounds) - follows snapped axis position
186
+ if (panelBboxDragOverlay && panelBboxOffset) {
187
+ updatePanelBboxDragOverlayMm(pos, rect);
188
+ panelBboxDragOverlay.style.display = 'block';
189
+ console.log('[PanelDrag] Panel bbox overlay shown');
190
+ }
191
+
192
+ // Create and show panel snapshot for visual feedback
193
+ if (typeof startSnapshotDrag === 'function') {
194
+ startSnapshotDrag(panelIndex, rect, pos);
195
+ }
196
+
134
197
  // Change cursor
135
198
  document.body.style.cursor = 'move';
136
199
 
@@ -167,6 +230,84 @@ function findPanelAtPositionMm(mmX, mmY) {
167
230
  return null;
168
231
  }
169
232
 
233
+ // Handle mouse hover over panels - show visual feedback
234
+ function handlePanelHover(event) {
235
+ // Skip if dragging
236
+ if (isDraggingPanel) {
237
+ hidePanelHover();
238
+ return;
239
+ }
240
+
241
+ const img = document.getElementById('preview-image');
242
+ if (!img || !figSize.width_mm || !figSize.height_mm) return;
243
+
244
+ const rect = img.getBoundingClientRect();
245
+ const x = event.clientX - rect.left;
246
+ const y = event.clientY - rect.top;
247
+
248
+ // Check if mouse is within image bounds
249
+ if (x < 0 || x > rect.width || y < 0 || y > rect.height) {
250
+ hidePanelHover();
251
+ return;
252
+ }
253
+
254
+ // Convert to mm coordinates
255
+ const mmX = (x / rect.width) * figSize.width_mm;
256
+ const mmY = (y / rect.height) * figSize.height_mm;
257
+
258
+ // Find panel at position
259
+ const panelIndex = findPanelAtPositionMm(mmX, mmY);
260
+
261
+ if (panelIndex !== null && panelIndex !== hoveredPanelIndex) {
262
+ showPanelHover(panelIndex, rect);
263
+ } else if (panelIndex === null) {
264
+ hidePanelHover();
265
+ }
266
+ }
267
+
268
+ // Show hover feedback for a panel
269
+ function showPanelHover(panelIndex, imgRect) {
270
+ if (!panelHoverOverlay) return;
271
+
272
+ hoveredPanelIndex = panelIndex;
273
+
274
+ // Get panel position
275
+ const axKey = Object.keys(panelPositions).sort()[panelIndex];
276
+ const pos = panelPositions[axKey];
277
+ if (!pos) return;
278
+
279
+ // Convert mm to screen pixels
280
+ const scaleX = imgRect.width / figSize.width_mm;
281
+ const scaleY = imgRect.height / figSize.height_mm;
282
+
283
+ const left = pos.left * scaleX;
284
+ const top = pos.top * scaleY;
285
+ const width = pos.width * scaleX;
286
+ const height = pos.height * scaleY;
287
+
288
+ panelHoverOverlay.style.left = `${left}px`;
289
+ panelHoverOverlay.style.top = `${top}px`;
290
+ panelHoverOverlay.style.width = `${width}px`;
291
+ panelHoverOverlay.style.height = `${height}px`;
292
+ panelHoverOverlay.style.display = 'block';
293
+
294
+ // Change cursor to indicate draggable
295
+ document.body.style.cursor = 'move';
296
+ }
297
+
298
+ // Hide hover feedback
299
+ function hidePanelHover() {
300
+ if (panelHoverOverlay) {
301
+ panelHoverOverlay.style.display = 'none';
302
+ }
303
+ hoveredPanelIndex = null;
304
+
305
+ // Reset cursor if not dragging
306
+ if (!isDraggingPanel) {
307
+ document.body.style.cursor = '';
308
+ }
309
+ }
310
+
170
311
  // Handle mouse move during drag
171
312
  function handlePanelDragMove(event) {
172
313
  if (!isDraggingPanel) return;
@@ -182,9 +323,17 @@ function handlePanelDragMove(event) {
182
323
  const deltaMmX = (event.clientX - dragStartPos.x) / rect.width * figSize.width_mm;
183
324
  const deltaMmY = (event.clientY - dragStartPos.y) / rect.height * figSize.height_mm;
184
325
 
185
- // Calculate new position (clamped to figure bounds)
186
- const newLeft = Math.max(0, Math.min(figSize.width_mm - dragStartPanelPos.width, dragStartPanelPos.left + deltaMmX));
187
- const newTop = Math.max(0, Math.min(figSize.height_mm - dragStartPanelPos.height, dragStartPanelPos.top + deltaMmY));
326
+ // Calculate raw new position (clamped to figure bounds)
327
+ let newLeft = Math.max(0, Math.min(figSize.width_mm - dragStartPanelPos.width, dragStartPanelPos.left + deltaMmX));
328
+ let newTop = Math.max(0, Math.min(figSize.height_mm - dragStartPanelPos.height, dragStartPanelPos.top + deltaMmY));
329
+
330
+ // Apply snapping (Alt key disables snapping for fine control)
331
+ let snapResult = { pos: { left: newLeft, top: newTop }, guides: [] };
332
+ if (typeof applySnapping === 'function' && !event.altKey) {
333
+ snapResult = applySnapping(newLeft, newTop, dragStartPanelPos.width, dragStartPanelPos.height, draggedPanelIndex);
334
+ newLeft = snapResult.pos.left;
335
+ newTop = snapResult.pos.top;
336
+ }
188
337
 
189
338
  const newPos = {
190
339
  left: newLeft,
@@ -193,27 +342,40 @@ function handlePanelDragMove(event) {
193
342
  height: dragStartPanelPos.height
194
343
  };
195
344
 
196
- // Update visual overlay
345
+ // Update visual overlays - both use snapped position in mm
197
346
  updateDragOverlayMm(newPos, rect);
347
+ updatePanelBboxDragOverlayMm(newPos, rect);
348
+
349
+ // Update snapshot position
350
+ if (typeof updateSnapshotPosition === 'function') {
351
+ updateSnapshotPosition(newPos, rect);
352
+ }
353
+
354
+ // Show/hide alignment guides
355
+ if (typeof showSnapGuides === 'function') {
356
+ showSnapGuides(snapResult.guides, rect);
357
+ }
198
358
  }
199
359
 
200
- // Update the drag overlay position (pos in mm, upper-left origin)
360
+ // Update axis drag overlay (mm to screen pixels)
201
361
  function updateDragOverlayMm(pos, imgRect) {
202
362
  if (!panelDragOverlay || !figSize.width_mm) return;
363
+ const scaleX = imgRect.width / figSize.width_mm, scaleY = imgRect.height / figSize.height_mm;
364
+ panelDragOverlay.style.left = `${pos.left * scaleX}px`;
365
+ panelDragOverlay.style.top = `${pos.top * scaleY}px`;
366
+ panelDragOverlay.style.width = `${pos.width * scaleX}px`;
367
+ panelDragOverlay.style.height = `${pos.height * scaleY}px`;
368
+ }
203
369
 
204
- // Convert mm to screen pixels
205
- const scaleX = imgRect.width / figSize.width_mm;
206
- const scaleY = imgRect.height / figSize.height_mm;
207
-
208
- const left = pos.left * scaleX;
209
- const top = pos.top * scaleY;
210
- const width = pos.width * scaleX;
211
- const height = pos.height * scaleY;
212
-
213
- panelDragOverlay.style.left = `${left}px`;
214
- panelDragOverlay.style.top = `${top}px`;
215
- panelDragOverlay.style.width = `${width}px`;
216
- panelDragOverlay.style.height = `${height}px`;
370
+ // Update panel bbox overlay based on snapped axis position (in mm)
371
+ function updatePanelBboxDragOverlayMm(axisPos, imgRect) {
372
+ if (!panelBboxDragOverlay || !panelBboxOffset || !figSize.width_mm) return;
373
+ const scaleX = imgRect.width / figSize.width_mm, scaleY = imgRect.height / figSize.height_mm;
374
+ // Panel bbox position = axis position + offset (both in mm, converted to screen pixels)
375
+ panelBboxDragOverlay.style.left = `${(axisPos.left + panelBboxOffset.left) * scaleX}px`;
376
+ panelBboxDragOverlay.style.top = `${(axisPos.top + panelBboxOffset.top) * scaleY}px`;
377
+ panelBboxDragOverlay.style.width = `${panelBboxOffset.width * scaleX}px`;
378
+ panelBboxDragOverlay.style.height = `${panelBboxOffset.height * scaleY}px`;
217
379
  }
218
380
 
219
381
  // Handle mouse up - complete the drag
@@ -221,12 +383,14 @@ async function handlePanelDragEnd(event) {
221
383
  console.log('[PanelDrag] handlePanelDragEnd called, isDraggingPanel:', isDraggingPanel);
222
384
  if (!isDraggingPanel) return;
223
385
 
224
- // Hide overlay (with null check)
225
- if (panelDragOverlay) {
226
- panelDragOverlay.style.display = 'none';
227
- console.log('[PanelDrag] Overlay hidden');
228
- }
386
+ // Hide overlays, snapshot, and snap guides
387
+ if (panelDragOverlay) panelDragOverlay.style.display = 'none';
388
+ if (panelBboxDragOverlay) panelBboxDragOverlay.style.display = 'none';
389
+ if (typeof endSnapshotDrag === 'function') endSnapshotDrag();
390
+ if (typeof hideSnapGuides === 'function') hideSnapGuides();
229
391
  document.body.style.cursor = '';
392
+ dragStartPanelBbox = null;
393
+ panelBboxOffset = null;
230
394
 
231
395
  const img = document.getElementById('preview-image');
232
396
  if (!img) {
@@ -240,8 +404,15 @@ async function handlePanelDragEnd(event) {
240
404
  const deltaMmX = (event.clientX - dragStartPos.x) / rect.width * figSize.width_mm;
241
405
  const deltaMmY = (event.clientY - dragStartPos.y) / rect.height * figSize.height_mm;
242
406
 
243
- const newLeft = Math.max(0, Math.min(figSize.width_mm - dragStartPanelPos.width, dragStartPanelPos.left + deltaMmX));
244
- const newTop = Math.max(0, Math.min(figSize.height_mm - dragStartPanelPos.height, dragStartPanelPos.top + deltaMmY));
407
+ let newLeft = Math.max(0, Math.min(figSize.width_mm - dragStartPanelPos.width, dragStartPanelPos.left + deltaMmX));
408
+ let newTop = Math.max(0, Math.min(figSize.height_mm - dragStartPanelPos.height, dragStartPanelPos.top + deltaMmY));
409
+
410
+ // Apply snapping to final position (unless Alt was held)
411
+ if (typeof applySnapping === 'function' && !event.altKey) {
412
+ const snapResult = applySnapping(newLeft, newTop, dragStartPanelPos.width, dragStartPanelPos.height, draggedPanelIndex);
413
+ newLeft = snapResult.pos.left;
414
+ newTop = snapResult.pos.top;
415
+ }
245
416
 
246
417
  // Only update if position actually changed (threshold in mm)
247
418
  const threshold = 1.0; // 1mm threshold
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Panel drag snapshot functionality with server-side isolated rendering.
4
+
5
+ This module provides clean panel snapshots rendered in isolation (no overlap)
6
+ by fetching from the server, with async caching for smooth UX.
7
+ """
8
+
9
+ SCRIPTS_PANEL_DRAG_SNAPSHOT = """
10
+ // ===== PANEL DRAG SNAPSHOT (DISABLED - corrupts figure state) =====
11
+ // Server-side snapshot rendering was disabled because matplotlib figures
12
+ // are not thread-safe. Modifying visibility to render isolated panels
13
+ // corrupts the shared figure state in Flask's threaded mode.
14
+
15
+ // No-op stubs to prevent errors from panel_drag.py calls
16
+ function startSnapshotDrag(panelIndex, imgRect, initialPos) {
17
+ // Disabled - no snapshot during drag
18
+ }
19
+
20
+ function updateSnapshotPosition(pos, imgRect) {
21
+ // Disabled - no snapshot during drag
22
+ }
23
+
24
+ function endSnapshotDrag() {
25
+ // Disabled - no snapshot during drag
26
+ }
27
+
28
+ // No initialization needed
29
+ """
30
+
31
+ __all__ = ["SCRIPTS_PANEL_DRAG_SNAPSHOT"]
32
+
33
+ # EOF