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.
- figrecipe/__init__.py +74 -76
- figrecipe/__main__.py +12 -0
- figrecipe/_api/_panel.py +67 -0
- figrecipe/_api/_save.py +100 -4
- 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 +2 -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/_editor/__init__.py +36 -36
- figrecipe/_editor/_bbox/_extract.py +155 -9
- figrecipe/_editor/_bbox/_extract_text.py +124 -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 +157 -16
- figrecipe/_editor/_helpers.py +17 -8
- figrecipe/_editor/_hitmap/_detect.py +89 -32
- figrecipe/_editor/_hitmap_main.py +4 -4
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_render_overrides.py +38 -11
- figrecipe/_editor/_renderer.py +46 -1
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +35 -6
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +15 -173
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +37 -19
- 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 +28 -8
- figrecipe/_editor/_templates/__init__.py +40 -2
- figrecipe/_editor/_templates/_html.py +97 -103
- 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 +58 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +1 -1
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +94 -37
- 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/_element_editor.py +17 -2
- figrecipe/_editor/_templates/_scripts/_files.py +274 -40
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
- 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 +8 -1
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
- figrecipe/_editor/_templates/_styles/__init__.py +9 -0
- figrecipe/_editor/_templates/_styles/_base.py +47 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +168 -3
- 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 +5 -5
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +98 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
- figrecipe/_editor/_templates/_styles/_modals.py +29 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
- figrecipe/_editor/_templates/_styles/_preview.py +213 -8
- 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 +2 -0
- figrecipe/_recorder.py +28 -3
- figrecipe/_reproducer/_core.py +60 -49
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +150 -2
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +26 -1
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/styles/_style_applier.py +10 -2
- figrecipe/styles/presets/SCITEX.yaml +11 -4
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe-0.7.4.dist-info/RECORD +0 -188
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Panel snapping JavaScript for the figure editor.
|
|
4
|
+
|
|
5
|
+
This module contains the JavaScript code for:
|
|
6
|
+
- Grid snapping (snap to mm grid)
|
|
7
|
+
- Edge alignment (snap to other panel edges)
|
|
8
|
+
- Center alignment (snap to other panel centers)
|
|
9
|
+
- Visual alignment guides
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
SCRIPTS_PANEL_SNAP = """
|
|
13
|
+
// ===== PANEL SNAPPING =====
|
|
14
|
+
|
|
15
|
+
// Snapping configuration
|
|
16
|
+
const SNAP_CONFIG = {
|
|
17
|
+
enabled: true,
|
|
18
|
+
gridSize: 5, // mm - snap to 5mm grid
|
|
19
|
+
snapThreshold: 3, // mm - distance to trigger hard snap
|
|
20
|
+
magneticZone: 8, // mm - distance where magnetic attraction starts
|
|
21
|
+
magneticStrength: 0.7, // 0-1, how strongly to pull toward target
|
|
22
|
+
showGuides: true // Show visual alignment guides
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Alignment guide elements
|
|
26
|
+
let snapGuides = [];
|
|
27
|
+
|
|
28
|
+
// Initialize snapping UI
|
|
29
|
+
function initPanelSnap() {
|
|
30
|
+
console.log('[PanelSnap] initPanelSnap called');
|
|
31
|
+
const zoomContainer = document.getElementById('zoom-container');
|
|
32
|
+
if (!zoomContainer) return;
|
|
33
|
+
|
|
34
|
+
// Create guide line elements (2 horizontal, 2 vertical for edges/centers)
|
|
35
|
+
for (let i = 0; i < 4; i++) {
|
|
36
|
+
const guide = document.createElement('div');
|
|
37
|
+
guide.className = 'snap-guide';
|
|
38
|
+
guide.style.cssText = `
|
|
39
|
+
position: absolute;
|
|
40
|
+
background: #f59e0b;
|
|
41
|
+
pointer-events: none;
|
|
42
|
+
display: none;
|
|
43
|
+
z-index: 999;
|
|
44
|
+
`;
|
|
45
|
+
zoomContainer.appendChild(guide);
|
|
46
|
+
snapGuides.push(guide);
|
|
47
|
+
}
|
|
48
|
+
console.log('[PanelSnap] Created', snapGuides.length, 'guide elements');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Find snap targets from other panels (excluding the panel being dragged)
|
|
52
|
+
function getSnapTargets(excludeIndex) {
|
|
53
|
+
const targets = { h: [], v: [] }; // horizontal and vertical snap lines
|
|
54
|
+
const axKeys = Object.keys(panelPositions).sort();
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < axKeys.length; i++) {
|
|
57
|
+
if (i === excludeIndex) continue;
|
|
58
|
+
|
|
59
|
+
const pos = panelPositions[axKeys[i]];
|
|
60
|
+
const left = pos.left;
|
|
61
|
+
const right = pos.left + pos.width;
|
|
62
|
+
const top = pos.top;
|
|
63
|
+
const bottom = pos.top + pos.height;
|
|
64
|
+
const centerX = pos.left + pos.width / 2;
|
|
65
|
+
const centerY = pos.top + pos.height / 2;
|
|
66
|
+
|
|
67
|
+
// Vertical lines (for left/right/centerX alignment)
|
|
68
|
+
targets.v.push({ pos: left, type: 'edge-left', panel: i });
|
|
69
|
+
targets.v.push({ pos: right, type: 'edge-right', panel: i });
|
|
70
|
+
targets.v.push({ pos: centerX, type: 'center', panel: i });
|
|
71
|
+
|
|
72
|
+
// Horizontal lines (for top/bottom/centerY alignment)
|
|
73
|
+
targets.h.push({ pos: top, type: 'edge-top', panel: i });
|
|
74
|
+
targets.h.push({ pos: bottom, type: 'edge-bottom', panel: i });
|
|
75
|
+
targets.h.push({ pos: centerY, type: 'center', panel: i });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add figure edges and center
|
|
79
|
+
targets.v.push({ pos: 0, type: 'figure-edge', panel: -1 });
|
|
80
|
+
targets.v.push({ pos: figSize.width_mm, type: 'figure-edge', panel: -1 });
|
|
81
|
+
targets.v.push({ pos: figSize.width_mm / 2, type: 'figure-center', panel: -1 });
|
|
82
|
+
targets.h.push({ pos: 0, type: 'figure-edge', panel: -1 });
|
|
83
|
+
targets.h.push({ pos: figSize.height_mm, type: 'figure-edge', panel: -1 });
|
|
84
|
+
targets.h.push({ pos: figSize.height_mm / 2, type: 'figure-center', panel: -1 });
|
|
85
|
+
|
|
86
|
+
return targets;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Apply magnetic attraction - eases movement toward target
|
|
90
|
+
function applyMagnetic(value, targetPos, dist) {
|
|
91
|
+
const zone = SNAP_CONFIG.magneticZone;
|
|
92
|
+
const strength = SNAP_CONFIG.magneticStrength;
|
|
93
|
+
const threshold = SNAP_CONFIG.snapThreshold;
|
|
94
|
+
|
|
95
|
+
if (dist <= threshold) {
|
|
96
|
+
// Hard snap - lock to target
|
|
97
|
+
return targetPos;
|
|
98
|
+
} else if (dist <= zone) {
|
|
99
|
+
// Magnetic zone - gradual attraction
|
|
100
|
+
// Strength increases as we get closer (quadratic easing)
|
|
101
|
+
const progress = 1 - (dist - threshold) / (zone - threshold);
|
|
102
|
+
const eased = progress * progress * strength;
|
|
103
|
+
return value + (targetPos - value) * eased;
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Apply snapping to a position
|
|
109
|
+
// Returns { pos: {left, top}, snapped: {h: bool, v: bool}, guides: [...], magnetic: {h: bool, v: bool} }
|
|
110
|
+
function applySnapping(left, top, width, height, excludeIndex) {
|
|
111
|
+
if (!SNAP_CONFIG.enabled) {
|
|
112
|
+
return { pos: { left, top }, snapped: { h: false, v: false }, guides: [], magnetic: { h: false, v: false } };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = {
|
|
116
|
+
pos: { left, top },
|
|
117
|
+
snapped: { h: false, v: false },
|
|
118
|
+
magnetic: { h: false, v: false },
|
|
119
|
+
guides: []
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Panel edges and center
|
|
123
|
+
const panelLeft = left;
|
|
124
|
+
const panelRight = left + width;
|
|
125
|
+
const panelCenterX = left + width / 2;
|
|
126
|
+
const panelTop = top;
|
|
127
|
+
const panelBottom = top + height;
|
|
128
|
+
const panelCenterY = top + height / 2;
|
|
129
|
+
|
|
130
|
+
const targets = getSnapTargets(excludeIndex);
|
|
131
|
+
const threshold = SNAP_CONFIG.snapThreshold;
|
|
132
|
+
const zone = SNAP_CONFIG.magneticZone;
|
|
133
|
+
|
|
134
|
+
// Find best vertical snap (for X position)
|
|
135
|
+
let bestVSnap = null;
|
|
136
|
+
let bestVDist = zone + 1;
|
|
137
|
+
|
|
138
|
+
for (const target of targets.v) {
|
|
139
|
+
// Check left edge
|
|
140
|
+
const distLeft = Math.abs(panelLeft - target.pos);
|
|
141
|
+
if (distLeft < bestVDist) {
|
|
142
|
+
bestVDist = distLeft;
|
|
143
|
+
bestVSnap = { offset: target.pos - panelLeft, target, edge: 'left' };
|
|
144
|
+
}
|
|
145
|
+
// Check right edge
|
|
146
|
+
const distRight = Math.abs(panelRight - target.pos);
|
|
147
|
+
if (distRight < bestVDist) {
|
|
148
|
+
bestVDist = distRight;
|
|
149
|
+
bestVSnap = { offset: target.pos - panelRight, target, edge: 'right' };
|
|
150
|
+
}
|
|
151
|
+
// Check center
|
|
152
|
+
const distCenter = Math.abs(panelCenterX - target.pos);
|
|
153
|
+
if (distCenter < bestVDist) {
|
|
154
|
+
bestVDist = distCenter;
|
|
155
|
+
bestVSnap = { offset: target.pos - panelCenterX, target, edge: 'center' };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (bestVSnap && bestVDist <= zone) {
|
|
160
|
+
const targetLeft = left + bestVSnap.offset;
|
|
161
|
+
result.pos.left = applyMagnetic(left, targetLeft, bestVDist);
|
|
162
|
+
result.snapped.v = bestVDist <= threshold;
|
|
163
|
+
result.magnetic.v = bestVDist > threshold && bestVDist <= zone;
|
|
164
|
+
result.guides.push({
|
|
165
|
+
type: 'vertical',
|
|
166
|
+
pos: bestVSnap.target.pos,
|
|
167
|
+
targetType: bestVSnap.target.type,
|
|
168
|
+
strength: result.snapped.v ? 1 : 1 - (bestVDist - threshold) / (zone - threshold)
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Find best horizontal snap (for Y position)
|
|
173
|
+
let bestHSnap = null;
|
|
174
|
+
let bestHDist = zone + 1;
|
|
175
|
+
|
|
176
|
+
for (const target of targets.h) {
|
|
177
|
+
// Check top edge
|
|
178
|
+
const distTop = Math.abs(panelTop - target.pos);
|
|
179
|
+
if (distTop < bestHDist) {
|
|
180
|
+
bestHDist = distTop;
|
|
181
|
+
bestHSnap = { offset: target.pos - panelTop, target, edge: 'top' };
|
|
182
|
+
}
|
|
183
|
+
// Check bottom edge
|
|
184
|
+
const distBottom = Math.abs(panelBottom - target.pos);
|
|
185
|
+
if (distBottom < bestHDist) {
|
|
186
|
+
bestHDist = distBottom;
|
|
187
|
+
bestHSnap = { offset: target.pos - panelBottom, target, edge: 'bottom' };
|
|
188
|
+
}
|
|
189
|
+
// Check center
|
|
190
|
+
const distCenter = Math.abs(panelCenterY - target.pos);
|
|
191
|
+
if (distCenter < bestHDist) {
|
|
192
|
+
bestHDist = distCenter;
|
|
193
|
+
bestHSnap = { offset: target.pos - panelCenterY, target, edge: 'center' };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (bestHSnap && bestHDist <= zone) {
|
|
198
|
+
const targetTop = top + bestHSnap.offset;
|
|
199
|
+
result.pos.top = applyMagnetic(top, targetTop, bestHDist);
|
|
200
|
+
result.snapped.h = bestHDist <= threshold;
|
|
201
|
+
result.magnetic.h = bestHDist > threshold && bestHDist <= zone;
|
|
202
|
+
result.guides.push({
|
|
203
|
+
type: 'horizontal',
|
|
204
|
+
pos: bestHSnap.target.pos,
|
|
205
|
+
targetType: bestHSnap.target.type,
|
|
206
|
+
strength: result.snapped.h ? 1 : 1 - (bestHDist - threshold) / (zone - threshold)
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Apply grid snapping if no edge/center snap or magnetic attraction
|
|
211
|
+
if (!result.snapped.v && !result.magnetic.v) {
|
|
212
|
+
result.pos.left = snapToGrid(result.pos.left);
|
|
213
|
+
}
|
|
214
|
+
if (!result.snapped.h && !result.magnetic.h) {
|
|
215
|
+
result.pos.top = snapToGrid(result.pos.top);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Snap value to grid
|
|
222
|
+
function snapToGrid(value) {
|
|
223
|
+
const grid = SNAP_CONFIG.gridSize;
|
|
224
|
+
return Math.round(value / grid) * grid;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Show alignment guides with opacity based on magnetic strength
|
|
228
|
+
function showSnapGuides(guides, imgRect) {
|
|
229
|
+
if (!SNAP_CONFIG.showGuides) {
|
|
230
|
+
hideSnapGuides();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const img = document.getElementById('preview-image');
|
|
235
|
+
if (!img) return;
|
|
236
|
+
|
|
237
|
+
// IMPORTANT: Use offsetWidth/offsetHeight (CSS dimensions) NOT imgRect (transformed by zoom)
|
|
238
|
+
// The guides are positioned in the zoom-container's local coordinate space,
|
|
239
|
+
// which is NOT affected by the CSS transform scale.
|
|
240
|
+
// imgRect.width/height = naturalWidth * zoomLevel (visual size, WRONG for positioning)
|
|
241
|
+
// img.offsetWidth/Height = naturalWidth (CSS size, CORRECT for positioning)
|
|
242
|
+
const scaleX = img.offsetWidth / figSize.width_mm;
|
|
243
|
+
const scaleY = img.offsetHeight / figSize.height_mm;
|
|
244
|
+
|
|
245
|
+
// Get image offset relative to zoom-container (its offset parent)
|
|
246
|
+
const imgOffsetX = img.offsetLeft;
|
|
247
|
+
const imgOffsetY = img.offsetTop;
|
|
248
|
+
|
|
249
|
+
// Debug logging for guide positioning
|
|
250
|
+
console.log('[SnapGuide] imgOffset:', imgOffsetX, imgOffsetY, '| offsetSize:', img.offsetWidth, img.offsetHeight, '| scale:', scaleX.toFixed(3), scaleY.toFixed(3));
|
|
251
|
+
|
|
252
|
+
// Hide all guides first
|
|
253
|
+
snapGuides.forEach(g => g.style.display = 'none');
|
|
254
|
+
|
|
255
|
+
// Show active guides
|
|
256
|
+
let guideIndex = 0;
|
|
257
|
+
for (const guide of guides) {
|
|
258
|
+
if (guideIndex >= snapGuides.length) break;
|
|
259
|
+
|
|
260
|
+
const el = snapGuides[guideIndex];
|
|
261
|
+
const isCenter = guide.targetType.includes('center');
|
|
262
|
+
const baseColor = isCenter ? '139, 92, 246' : '245, 158, 11'; // RGB values
|
|
263
|
+
const strength = guide.strength || 1;
|
|
264
|
+
const opacity = 0.3 + strength * 0.7; // 0.3-1.0 opacity range
|
|
265
|
+
|
|
266
|
+
if (guide.type === 'vertical') {
|
|
267
|
+
el.style.left = `${imgOffsetX + guide.pos * scaleX}px`;
|
|
268
|
+
el.style.top = `${imgOffsetY}px`;
|
|
269
|
+
el.style.width = strength >= 1 ? '3px' : '2px'; // Thicker when snapped
|
|
270
|
+
el.style.height = `${img.offsetHeight}px`; // Use CSS size, not transformed
|
|
271
|
+
} else {
|
|
272
|
+
el.style.left = `${imgOffsetX}px`;
|
|
273
|
+
el.style.top = `${imgOffsetY + guide.pos * scaleY}px`;
|
|
274
|
+
el.style.width = `${img.offsetWidth}px`; // Use CSS size, not transformed
|
|
275
|
+
el.style.height = strength >= 1 ? '3px' : '2px'; // Thicker when snapped
|
|
276
|
+
}
|
|
277
|
+
el.style.background = `rgba(${baseColor}, ${opacity})`;
|
|
278
|
+
el.style.display = 'block';
|
|
279
|
+
guideIndex++;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Hide all alignment guides
|
|
284
|
+
function hideSnapGuides() {
|
|
285
|
+
snapGuides.forEach(g => g.style.display = 'none');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Toggle snapping on/off
|
|
289
|
+
function toggleSnapping(enabled) {
|
|
290
|
+
SNAP_CONFIG.enabled = enabled;
|
|
291
|
+
console.log('[PanelSnap] Snapping', enabled ? 'enabled' : 'disabled');
|
|
292
|
+
if (!enabled) hideSnapGuides();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Set grid size
|
|
296
|
+
function setSnapGridSize(size) {
|
|
297
|
+
SNAP_CONFIG.gridSize = size;
|
|
298
|
+
console.log('[PanelSnap] Grid size set to', size, 'mm');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Initialize on DOMContentLoaded
|
|
302
|
+
document.addEventListener('DOMContentLoaded', initPanelSnap);
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
__all__ = ["SCRIPTS_PANEL_SNAP"]
|
|
306
|
+
|
|
307
|
+
# EOF
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Region selection (marquee) JavaScript for the figure editor.
|
|
4
|
+
|
|
5
|
+
This module contains the JavaScript code for:
|
|
6
|
+
- Drawing selection rectangle by dragging on the canvas
|
|
7
|
+
- Selecting all elements within the rectangle
|
|
8
|
+
- Combining with multi-select (Ctrl+drag to add to selection)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
SCRIPTS_REGION_SELECT = """
|
|
12
|
+
// ===== REGION SELECTION (Marquee/Rectangle Selection) =====
|
|
13
|
+
|
|
14
|
+
// Region selection state
|
|
15
|
+
let isRegionSelecting = false;
|
|
16
|
+
let regionSelectStart = null; // { x, y } in mm
|
|
17
|
+
let regionSelectRect = null; // { x, y, width, height } in mm
|
|
18
|
+
|
|
19
|
+
// Region selection overlay element
|
|
20
|
+
let regionSelectOverlay = null;
|
|
21
|
+
|
|
22
|
+
// Initialize region selection
|
|
23
|
+
function initRegionSelect() {
|
|
24
|
+
console.log('[RegionSelect] Initializing region selection');
|
|
25
|
+
|
|
26
|
+
const zoomContainer = document.getElementById('zoom-container');
|
|
27
|
+
if (!zoomContainer) return;
|
|
28
|
+
|
|
29
|
+
// Create selection rectangle overlay
|
|
30
|
+
regionSelectOverlay = document.createElement('div');
|
|
31
|
+
regionSelectOverlay.id = 'region-select-overlay';
|
|
32
|
+
regionSelectOverlay.style.cssText = `
|
|
33
|
+
position: absolute;
|
|
34
|
+
border: 2px dashed #2563eb;
|
|
35
|
+
background: rgba(37, 99, 235, 0.1);
|
|
36
|
+
pointer-events: none;
|
|
37
|
+
display: none;
|
|
38
|
+
z-index: 100;
|
|
39
|
+
`;
|
|
40
|
+
zoomContainer.appendChild(regionSelectOverlay);
|
|
41
|
+
|
|
42
|
+
// Add event listeners to zoom container
|
|
43
|
+
zoomContainer.addEventListener('mousedown', handleRegionSelectStart);
|
|
44
|
+
document.addEventListener('mousemove', handleRegionSelectMove);
|
|
45
|
+
document.addEventListener('mouseup', handleRegionSelectEnd);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Handle mousedown to start region selection
|
|
49
|
+
function handleRegionSelectStart(event) {
|
|
50
|
+
// Only start region select on left-click
|
|
51
|
+
if (event.button !== 0) return;
|
|
52
|
+
|
|
53
|
+
// Skip if clicking on a hit region, label, or other interactive element
|
|
54
|
+
const target = event.target;
|
|
55
|
+
if (target.closest('.hitregion-group') ||
|
|
56
|
+
target.closest('.panel-label-group') ||
|
|
57
|
+
target.closest('.hitregion-polyline') ||
|
|
58
|
+
target.closest('.hitregion-rect') ||
|
|
59
|
+
target.closest('.hitregion-circle')) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Skip if modifier keys suggest other operations (Alt for cycling)
|
|
64
|
+
if (event.altKey) return;
|
|
65
|
+
|
|
66
|
+
// Skip if clicking directly on the preview image (handled by hitmap)
|
|
67
|
+
if (target.id === 'preview-image') return;
|
|
68
|
+
|
|
69
|
+
// Check if click is on empty area of zoom container or overlays
|
|
70
|
+
const zoomContainer = document.getElementById('zoom-container');
|
|
71
|
+
const hitOverlay = document.getElementById('hitregion-overlay');
|
|
72
|
+
const selOverlay = document.getElementById('selection-overlay');
|
|
73
|
+
|
|
74
|
+
if (target !== zoomContainer && target !== hitOverlay && target !== selOverlay) {
|
|
75
|
+
// Not on container/overlay background - might be clicking on shape
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const img = document.getElementById('preview-image');
|
|
80
|
+
if (!img || !figSize.width_mm || !figSize.height_mm) return;
|
|
81
|
+
|
|
82
|
+
const imgRect = img.getBoundingClientRect();
|
|
83
|
+
|
|
84
|
+
// Check if click is within image bounds
|
|
85
|
+
const x = event.clientX - imgRect.left;
|
|
86
|
+
const y = event.clientY - imgRect.top;
|
|
87
|
+
if (x < 0 || x > imgRect.width || y < 0 || y > imgRect.height) return;
|
|
88
|
+
|
|
89
|
+
// Start region selection
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
event.stopPropagation();
|
|
92
|
+
|
|
93
|
+
isRegionSelecting = true;
|
|
94
|
+
|
|
95
|
+
// Convert to mm coordinates
|
|
96
|
+
const mmX = (x / imgRect.width) * figSize.width_mm;
|
|
97
|
+
const mmY = (y / imgRect.height) * figSize.height_mm;
|
|
98
|
+
|
|
99
|
+
regionSelectStart = { x: mmX, y: mmY };
|
|
100
|
+
regionSelectRect = { x: mmX, y: mmY, width: 0, height: 0 };
|
|
101
|
+
|
|
102
|
+
// Clear selection unless Ctrl is held (add mode)
|
|
103
|
+
if (!isMultiSelectMode(event)) {
|
|
104
|
+
clearMultiSelection();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Show overlay
|
|
108
|
+
updateRegionSelectOverlay(imgRect);
|
|
109
|
+
regionSelectOverlay.style.display = 'block';
|
|
110
|
+
|
|
111
|
+
console.log('[RegionSelect] Started at', mmX.toFixed(1), mmY.toFixed(1));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Handle mousemove during region selection
|
|
115
|
+
function handleRegionSelectMove(event) {
|
|
116
|
+
if (!isRegionSelecting) return;
|
|
117
|
+
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
|
|
120
|
+
const img = document.getElementById('preview-image');
|
|
121
|
+
if (!img) return;
|
|
122
|
+
|
|
123
|
+
const imgRect = img.getBoundingClientRect();
|
|
124
|
+
|
|
125
|
+
// Convert current position to mm
|
|
126
|
+
const x = event.clientX - imgRect.left;
|
|
127
|
+
const y = event.clientY - imgRect.top;
|
|
128
|
+
const mmX = Math.max(0, Math.min(figSize.width_mm, (x / imgRect.width) * figSize.width_mm));
|
|
129
|
+
const mmY = Math.max(0, Math.min(figSize.height_mm, (y / imgRect.height) * figSize.height_mm));
|
|
130
|
+
|
|
131
|
+
// Update rect (handle negative width/height by using min/max)
|
|
132
|
+
regionSelectRect = {
|
|
133
|
+
x: Math.min(regionSelectStart.x, mmX),
|
|
134
|
+
y: Math.min(regionSelectStart.y, mmY),
|
|
135
|
+
width: Math.abs(mmX - regionSelectStart.x),
|
|
136
|
+
height: Math.abs(mmY - regionSelectStart.y)
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Update visual overlay
|
|
140
|
+
updateRegionSelectOverlay(imgRect);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Handle mouseup to end region selection
|
|
144
|
+
function handleRegionSelectEnd(event) {
|
|
145
|
+
if (!isRegionSelecting) return;
|
|
146
|
+
|
|
147
|
+
isRegionSelecting = false;
|
|
148
|
+
regionSelectOverlay.style.display = 'none';
|
|
149
|
+
|
|
150
|
+
// Only select if rectangle has meaningful size (> 2mm)
|
|
151
|
+
if (regionSelectRect.width < 2 || regionSelectRect.height < 2) {
|
|
152
|
+
console.log('[RegionSelect] Rectangle too small, ignored');
|
|
153
|
+
regionSelectStart = null;
|
|
154
|
+
regionSelectRect = null;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Select elements within the rectangle
|
|
159
|
+
selectElementsInRegion(regionSelectRect, isMultiSelectMode(event));
|
|
160
|
+
|
|
161
|
+
regionSelectStart = null;
|
|
162
|
+
regionSelectRect = null;
|
|
163
|
+
|
|
164
|
+
console.log('[RegionSelect] Ended, selected', selectedElements.length, 'elements');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Update the visual selection rectangle overlay
|
|
168
|
+
function updateRegionSelectOverlay(imgRect) {
|
|
169
|
+
if (!regionSelectOverlay || !regionSelectRect) return;
|
|
170
|
+
|
|
171
|
+
// Convert mm to pixels
|
|
172
|
+
const scaleX = imgRect.width / figSize.width_mm;
|
|
173
|
+
const scaleY = imgRect.height / figSize.height_mm;
|
|
174
|
+
|
|
175
|
+
const left = regionSelectRect.x * scaleX;
|
|
176
|
+
const top = regionSelectRect.y * scaleY;
|
|
177
|
+
const width = regionSelectRect.width * scaleX;
|
|
178
|
+
const height = regionSelectRect.height * scaleY;
|
|
179
|
+
|
|
180
|
+
regionSelectOverlay.style.left = `${left}px`;
|
|
181
|
+
regionSelectOverlay.style.top = `${top}px`;
|
|
182
|
+
regionSelectOverlay.style.width = `${width}px`;
|
|
183
|
+
regionSelectOverlay.style.height = `${height}px`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Select all elements whose bounding boxes intersect with the selection rectangle
|
|
187
|
+
function selectElementsInRegion(rectMm, addToExisting) {
|
|
188
|
+
if (!addToExisting) {
|
|
189
|
+
clearMultiSelection();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const img = document.getElementById('preview-image');
|
|
193
|
+
if (!img || !figSize.width_mm || !figSize.height_mm) return;
|
|
194
|
+
|
|
195
|
+
// Convert selection rect from mm to image pixels for bbox comparison
|
|
196
|
+
const scaleX = img.naturalWidth / figSize.width_mm;
|
|
197
|
+
const scaleY = img.naturalHeight / figSize.height_mm;
|
|
198
|
+
|
|
199
|
+
const selRect = {
|
|
200
|
+
x: rectMm.x * scaleX,
|
|
201
|
+
y: rectMm.y * scaleY,
|
|
202
|
+
width: rectMm.width * scaleX,
|
|
203
|
+
height: rectMm.height * scaleY
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Check each element's bbox for intersection
|
|
207
|
+
for (const [key, bbox] of Object.entries(currentBboxes)) {
|
|
208
|
+
if (key === '_meta') continue;
|
|
209
|
+
if (!bbox || typeof bbox.x === 'undefined') continue;
|
|
210
|
+
|
|
211
|
+
// Check intersection between selection rect and element bbox
|
|
212
|
+
if (rectsIntersect(selRect, bbox)) {
|
|
213
|
+
const info = (colorMap && colorMap[key]) || {};
|
|
214
|
+
addToSelection({ key, ...bbox, ...info });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Also check line/scatter points
|
|
218
|
+
if ((bbox.type === 'line' || bbox.type === 'scatter') && bbox.points) {
|
|
219
|
+
const hasPointInRegion = bbox.points.some(pt =>
|
|
220
|
+
pt[0] >= selRect.x && pt[0] <= selRect.x + selRect.width &&
|
|
221
|
+
pt[1] >= selRect.y && pt[1] <= selRect.y + selRect.height
|
|
222
|
+
);
|
|
223
|
+
if (hasPointInRegion && !isElementSelected(key)) {
|
|
224
|
+
const info = (colorMap && colorMap[key]) || {};
|
|
225
|
+
addToSelection({ key, ...bbox, ...info });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Draw selection and update UI
|
|
231
|
+
drawMultiSelection();
|
|
232
|
+
updateMultiSelectionUI();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check if two rectangles intersect
|
|
236
|
+
function rectsIntersect(rect1, rect2) {
|
|
237
|
+
return !(rect1.x + rect1.width < rect2.x ||
|
|
238
|
+
rect2.x + rect2.width < rect1.x ||
|
|
239
|
+
rect1.y + rect1.height < rect2.y ||
|
|
240
|
+
rect2.y + rect2.height < rect1.y);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check if a point is inside a rectangle
|
|
244
|
+
function pointInRect(px, py, rect) {
|
|
245
|
+
return px >= rect.x && px <= rect.x + rect.width &&
|
|
246
|
+
py >= rect.y && py <= rect.y + rect.height;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Initialize on DOMContentLoaded
|
|
250
|
+
document.addEventListener('DOMContentLoaded', initRegionSelect);
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
__all__ = ["SCRIPTS_REGION_SELECT"]
|
|
254
|
+
|
|
255
|
+
# EOF
|
|
@@ -21,6 +21,11 @@ function clearSelection() {
|
|
|
21
21
|
document.querySelectorAll('.section-highlighted').forEach(s => s.classList.remove('section-highlighted'));
|
|
22
22
|
document.querySelectorAll('.field-highlighted').forEach(f => f.classList.remove('field-highlighted'));
|
|
23
23
|
|
|
24
|
+
// Clear panel selection
|
|
25
|
+
if (typeof clearPanelSelection === 'function') {
|
|
26
|
+
clearPanelSelection();
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
// Switch back to Figure tab when nothing selected
|
|
25
30
|
switchTab('figure');
|
|
26
31
|
|
|
@@ -62,7 +67,9 @@ function drawSelection(key) {
|
|
|
62
67
|
const bbox = currentBboxes[elemKey];
|
|
63
68
|
if (!bbox) continue;
|
|
64
69
|
|
|
65
|
-
|
|
70
|
+
// Get element color from colorMap (primary source) or bbox (fallback)
|
|
71
|
+
const colorMapInfo = (colorMap && colorMap[elemKey]) || {};
|
|
72
|
+
const elementColor = colorMapInfo.original_color || bbox.original_color || '#2563eb';
|
|
66
73
|
const isPrimary = elemKey === key;
|
|
67
74
|
|
|
68
75
|
if (bbox.type === 'line' && bbox.points && bbox.points.length > 1) {
|