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,270 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Composition-related Flask routes for the figure editor.
|
|
5
|
+
|
|
6
|
+
Provides API endpoints for:
|
|
7
|
+
- Panel visibility (hide/show)
|
|
8
|
+
- Panel alignment
|
|
9
|
+
- Panel distribution
|
|
10
|
+
- Smart alignment
|
|
11
|
+
- Importing axes from external recipes
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from flask import jsonify, request
|
|
17
|
+
|
|
18
|
+
from ._helpers import render_with_overrides
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def register_composition_routes(app, editor):
|
|
22
|
+
"""Register composition routes with the Flask app."""
|
|
23
|
+
|
|
24
|
+
@app.route("/api/panel-visibility", methods=["POST"])
|
|
25
|
+
def set_panel_visibility():
|
|
26
|
+
"""Toggle panel visibility.
|
|
27
|
+
|
|
28
|
+
Request JSON:
|
|
29
|
+
position: [row, col] - Panel position
|
|
30
|
+
visible: bool - Target visibility state
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
success: bool
|
|
34
|
+
image: str - Updated preview as base64
|
|
35
|
+
bboxes: dict - Updated bounding boxes
|
|
36
|
+
"""
|
|
37
|
+
from .._composition import hide_panel, show_panel
|
|
38
|
+
|
|
39
|
+
data = request.get_json() or {}
|
|
40
|
+
position = tuple(data.get("position", [0, 0]))
|
|
41
|
+
visible = data.get("visible", True)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
if visible:
|
|
45
|
+
show_panel(editor.fig, position)
|
|
46
|
+
else:
|
|
47
|
+
hide_panel(editor.fig, position)
|
|
48
|
+
|
|
49
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
50
|
+
editor.fig,
|
|
51
|
+
editor.get_effective_style(),
|
|
52
|
+
editor.dark_mode,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return jsonify(
|
|
56
|
+
{
|
|
57
|
+
"success": True,
|
|
58
|
+
"image": base64_img,
|
|
59
|
+
"bboxes": bboxes,
|
|
60
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return jsonify({"success": False, "error": str(e)}), 500
|
|
66
|
+
|
|
67
|
+
@app.route("/api/align-panels", methods=["POST"])
|
|
68
|
+
def align_panels_route():
|
|
69
|
+
"""Align selected panels.
|
|
70
|
+
|
|
71
|
+
Request JSON:
|
|
72
|
+
panels: [[row, col], ...] - List of panel positions
|
|
73
|
+
mode: str - Alignment mode (left, right, top, bottom, etc.)
|
|
74
|
+
reference: [row, col] - Optional reference panel
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
success: bool
|
|
78
|
+
image: str - Updated preview as base64
|
|
79
|
+
bboxes: dict - Updated bounding boxes
|
|
80
|
+
"""
|
|
81
|
+
from .._composition import align_panels
|
|
82
|
+
|
|
83
|
+
data = request.get_json() or {}
|
|
84
|
+
panels = [tuple(p) for p in data.get("panels", [])]
|
|
85
|
+
mode = data.get("mode", "left")
|
|
86
|
+
reference = tuple(data.get("reference")) if data.get("reference") else None
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
align_panels(editor.fig, panels, mode, reference)
|
|
90
|
+
|
|
91
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
92
|
+
editor.fig,
|
|
93
|
+
editor.get_effective_style(),
|
|
94
|
+
editor.dark_mode,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return jsonify(
|
|
98
|
+
{
|
|
99
|
+
"success": True,
|
|
100
|
+
"image": base64_img,
|
|
101
|
+
"bboxes": bboxes,
|
|
102
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
return jsonify({"success": False, "error": str(e)}), 500
|
|
108
|
+
|
|
109
|
+
@app.route("/api/distribute-panels", methods=["POST"])
|
|
110
|
+
def distribute_panels_route():
|
|
111
|
+
"""Distribute panels evenly.
|
|
112
|
+
|
|
113
|
+
Request JSON:
|
|
114
|
+
panels: [[row, col], ...] - List of panel positions
|
|
115
|
+
direction: str - 'horizontal' or 'vertical'
|
|
116
|
+
spacing_mm: float - Optional fixed spacing in mm
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
success: bool
|
|
120
|
+
image: str - Updated preview as base64
|
|
121
|
+
bboxes: dict - Updated bounding boxes
|
|
122
|
+
"""
|
|
123
|
+
from .._composition import distribute_panels
|
|
124
|
+
|
|
125
|
+
data = request.get_json() or {}
|
|
126
|
+
panels = [tuple(p) for p in data.get("panels", [])]
|
|
127
|
+
direction = data.get("direction", "horizontal")
|
|
128
|
+
spacing_mm = data.get("spacing_mm")
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
distribute_panels(editor.fig, panels, direction, spacing_mm)
|
|
132
|
+
|
|
133
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
134
|
+
editor.fig,
|
|
135
|
+
editor.get_effective_style(),
|
|
136
|
+
editor.dark_mode,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return jsonify(
|
|
140
|
+
{
|
|
141
|
+
"success": True,
|
|
142
|
+
"image": base64_img,
|
|
143
|
+
"bboxes": bboxes,
|
|
144
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
return jsonify({"success": False, "error": str(e)}), 500
|
|
150
|
+
|
|
151
|
+
@app.route("/api/smart-align", methods=["POST"])
|
|
152
|
+
def smart_align_route():
|
|
153
|
+
"""Auto-align all panels based on grid structure.
|
|
154
|
+
|
|
155
|
+
Request JSON:
|
|
156
|
+
panels: [[row, col], ...] - Optional specific panels to align
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
success: bool
|
|
160
|
+
image: str - Updated preview as base64
|
|
161
|
+
bboxes: dict - Updated bounding boxes
|
|
162
|
+
"""
|
|
163
|
+
from .._composition import smart_align
|
|
164
|
+
|
|
165
|
+
data = request.get_json() or {}
|
|
166
|
+
panels = None
|
|
167
|
+
if data.get("panels"):
|
|
168
|
+
panels = [tuple(p) for p in data["panels"]]
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
smart_align(editor.fig, panels)
|
|
172
|
+
|
|
173
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
174
|
+
editor.fig,
|
|
175
|
+
editor.get_effective_style(),
|
|
176
|
+
editor.dark_mode,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return jsonify(
|
|
180
|
+
{
|
|
181
|
+
"success": True,
|
|
182
|
+
"image": base64_img,
|
|
183
|
+
"bboxes": bboxes,
|
|
184
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
return jsonify({"success": False, "error": str(e)}), 500
|
|
190
|
+
|
|
191
|
+
@app.route("/api/import-panel", methods=["POST"])
|
|
192
|
+
def import_panel():
|
|
193
|
+
"""Import axes from another recipe into current figure.
|
|
194
|
+
|
|
195
|
+
Request JSON:
|
|
196
|
+
source: str - Path to source recipe file
|
|
197
|
+
source_axes: str - Axes key in source (default: 'ax_0_0')
|
|
198
|
+
target_position: [row, col] - Target panel position
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
success: bool
|
|
202
|
+
image: str - Updated preview as base64
|
|
203
|
+
bboxes: dict - Updated bounding boxes
|
|
204
|
+
"""
|
|
205
|
+
from .._composition import import_axes
|
|
206
|
+
|
|
207
|
+
data = request.get_json() or {}
|
|
208
|
+
source_path = data.get("source")
|
|
209
|
+
source_axes = data.get("source_axes", "ax_0_0")
|
|
210
|
+
target_position = tuple(data.get("target_position", [0, 0]))
|
|
211
|
+
|
|
212
|
+
if not source_path:
|
|
213
|
+
return jsonify({"success": False, "error": "No source path provided"}), 400
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
# Resolve relative paths against working directory
|
|
217
|
+
working_dir = getattr(editor, "working_dir", Path.cwd())
|
|
218
|
+
source_path = Path(source_path)
|
|
219
|
+
if not source_path.is_absolute():
|
|
220
|
+
source_path = working_dir / source_path
|
|
221
|
+
|
|
222
|
+
import_axes(editor.fig, target_position, source_path, source_axes)
|
|
223
|
+
|
|
224
|
+
base64_img, bboxes, img_size = render_with_overrides(
|
|
225
|
+
editor.fig,
|
|
226
|
+
editor.get_effective_style(),
|
|
227
|
+
editor.dark_mode,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return jsonify(
|
|
231
|
+
{
|
|
232
|
+
"success": True,
|
|
233
|
+
"image": base64_img,
|
|
234
|
+
"bboxes": bboxes,
|
|
235
|
+
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
return jsonify({"success": False, "error": str(e)}), 500
|
|
241
|
+
|
|
242
|
+
@app.route("/api/panel-info")
|
|
243
|
+
def get_panel_info():
|
|
244
|
+
"""Get information about all panels in the figure.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
panels: list of {position, visible, has_content}
|
|
248
|
+
"""
|
|
249
|
+
panels = []
|
|
250
|
+
for ax_key, ax_record in editor.fig.record.axes.items():
|
|
251
|
+
parts = ax_key.split("_")
|
|
252
|
+
if len(parts) >= 3:
|
|
253
|
+
row, col = int(parts[1]), int(parts[2])
|
|
254
|
+
else:
|
|
255
|
+
row, col = 0, 0
|
|
256
|
+
|
|
257
|
+
panels.append(
|
|
258
|
+
{
|
|
259
|
+
"key": ax_key,
|
|
260
|
+
"position": [row, col],
|
|
261
|
+
"visible": getattr(ax_record, "visible", True),
|
|
262
|
+
"has_content": len(ax_record.calls) > 0,
|
|
263
|
+
"call_count": len(ax_record.calls),
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return jsonify({"panels": panels})
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
__all__ = ["register_composition_routes"]
|
|
@@ -2,14 +2,23 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
4
|
Core Flask route handlers for the figure editor.
|
|
5
|
-
Handles main page, preview, update,
|
|
5
|
+
Handles main page, preview, update, and hitmap routes.
|
|
6
|
+
File operations moved to _routes_files.py.
|
|
6
7
|
"""
|
|
7
8
|
|
|
9
|
+
import os
|
|
10
|
+
|
|
8
11
|
from flask import jsonify, render_template_string, request
|
|
9
12
|
|
|
13
|
+
from . import _check_figure_has_content
|
|
10
14
|
from ._helpers import render_with_overrides
|
|
11
15
|
|
|
12
16
|
|
|
17
|
+
def _is_debug_mode() -> bool:
|
|
18
|
+
"""Check if debug mode is enabled via FIGRECIPE_DEBUG_MODE env var."""
|
|
19
|
+
return os.environ.get("FIGRECIPE_DEBUG_MODE", "").lower() in ("1", "true", "yes")
|
|
20
|
+
|
|
21
|
+
|
|
13
22
|
def register_core_routes(app, editor):
|
|
14
23
|
"""Register core routes with the Flask app."""
|
|
15
24
|
from ._hitmap import generate_hitmap, hitmap_to_base64
|
|
@@ -26,6 +35,9 @@ def register_core_routes(app, editor):
|
|
|
26
35
|
|
|
27
36
|
style_name = getattr(editor, "_style_name", "SCITEX")
|
|
28
37
|
|
|
38
|
+
# Check if figure has plot content
|
|
39
|
+
figure_has_content = _check_figure_has_content(editor.fig)
|
|
40
|
+
|
|
29
41
|
html = build_html_template(
|
|
30
42
|
image_base64=base64_img,
|
|
31
43
|
bboxes=bboxes,
|
|
@@ -36,6 +48,8 @@ def register_core_routes(app, editor):
|
|
|
36
48
|
style_name=style_name,
|
|
37
49
|
hot_reload=editor.hot_reload,
|
|
38
50
|
dark_mode=editor.dark_mode,
|
|
51
|
+
figure_has_content=figure_has_content,
|
|
52
|
+
debug_mode=_is_debug_mode(),
|
|
39
53
|
)
|
|
40
54
|
|
|
41
55
|
return render_template_string(html)
|
|
@@ -108,177 +122,5 @@ def register_core_routes(app, editor):
|
|
|
108
122
|
}
|
|
109
123
|
)
|
|
110
124
|
|
|
111
|
-
@app.route("/api/files")
|
|
112
|
-
def list_files():
|
|
113
|
-
"""List available recipe files in working directory."""
|
|
114
|
-
from pathlib import Path
|
|
115
|
-
|
|
116
|
-
working_dir = getattr(editor, "working_dir", Path.cwd())
|
|
117
|
-
files = []
|
|
118
|
-
|
|
119
|
-
# Find all YAML recipe files
|
|
120
|
-
for pattern in ["*.yaml", "*.yml"]:
|
|
121
|
-
for f in working_dir.glob(pattern):
|
|
122
|
-
# Skip hidden files and overrides files
|
|
123
|
-
if f.name.startswith(".") or f.name.endswith(".overrides.yaml"):
|
|
124
|
-
continue
|
|
125
|
-
|
|
126
|
-
# Check for associated PNG
|
|
127
|
-
png_path = f.with_suffix(".png")
|
|
128
|
-
has_png = png_path.exists()
|
|
129
|
-
|
|
130
|
-
files.append(
|
|
131
|
-
{
|
|
132
|
-
"path": str(f.relative_to(working_dir)),
|
|
133
|
-
"name": f.stem,
|
|
134
|
-
"has_image": has_png,
|
|
135
|
-
"is_current": (
|
|
136
|
-
editor.recipe_path
|
|
137
|
-
and f.resolve() == editor.recipe_path.resolve()
|
|
138
|
-
),
|
|
139
|
-
}
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
# Sort by name
|
|
143
|
-
files.sort(key=lambda x: x["name"].lower())
|
|
144
|
-
|
|
145
|
-
return jsonify(
|
|
146
|
-
{
|
|
147
|
-
"files": files,
|
|
148
|
-
"working_dir": str(working_dir),
|
|
149
|
-
"current_file": (
|
|
150
|
-
str(editor.recipe_path.name) if editor.recipe_path else None
|
|
151
|
-
),
|
|
152
|
-
}
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
@app.route("/api/switch", methods=["POST"])
|
|
156
|
-
def switch_file():
|
|
157
|
-
"""Switch to a different recipe file."""
|
|
158
|
-
from pathlib import Path
|
|
159
|
-
|
|
160
|
-
from .._reproducer import reproduce
|
|
161
|
-
|
|
162
|
-
data = request.get_json() or {}
|
|
163
|
-
file_path = data.get("path")
|
|
164
|
-
|
|
165
|
-
if not file_path:
|
|
166
|
-
return jsonify({"error": "No file path provided"}), 400
|
|
167
|
-
|
|
168
|
-
working_dir = getattr(editor, "working_dir", Path.cwd())
|
|
169
|
-
full_path = working_dir / file_path
|
|
170
|
-
|
|
171
|
-
if not full_path.exists():
|
|
172
|
-
return jsonify({"error": f"File not found: {file_path}"}), 404
|
|
173
|
-
|
|
174
|
-
try:
|
|
175
|
-
# Reproduce the figure from the new recipe
|
|
176
|
-
fig, axes = reproduce(full_path)
|
|
177
|
-
|
|
178
|
-
# Wrap in RecordingFigure if needed
|
|
179
|
-
from .._wrappers._figure import RecordingFigure
|
|
180
|
-
|
|
181
|
-
if not isinstance(fig, RecordingFigure):
|
|
182
|
-
from .._recorder import FigureRecord, Recorder
|
|
183
|
-
|
|
184
|
-
wrapped_fig = RecordingFigure.__new__(RecordingFigure)
|
|
185
|
-
wrapped_fig._fig = fig
|
|
186
|
-
wrapped_fig._axes = [[ax] for ax in fig.axes]
|
|
187
|
-
wrapped_fig._recorder = Recorder()
|
|
188
|
-
wrapped_fig._recorder._figure_record = FigureRecord(
|
|
189
|
-
figsize=tuple(fig.get_size_inches()),
|
|
190
|
-
dpi=int(fig.dpi),
|
|
191
|
-
)
|
|
192
|
-
fig = wrapped_fig
|
|
193
|
-
|
|
194
|
-
# Update editor state
|
|
195
|
-
editor.fig = fig
|
|
196
|
-
editor.recipe_path = full_path
|
|
197
|
-
editor._hitmap_generated = False
|
|
198
|
-
editor._color_map = {}
|
|
199
|
-
|
|
200
|
-
# Re-init style overrides
|
|
201
|
-
editor._init_style_overrides(None)
|
|
202
|
-
|
|
203
|
-
# Regenerate hitmap
|
|
204
|
-
hitmap_img, editor._color_map = generate_hitmap(editor.fig)
|
|
205
|
-
editor._hitmap_base64 = hitmap_to_base64(hitmap_img)
|
|
206
|
-
editor._hitmap_generated = True
|
|
207
|
-
|
|
208
|
-
# Render new preview
|
|
209
|
-
base64_img, bboxes, img_size = render_with_overrides(
|
|
210
|
-
editor.fig,
|
|
211
|
-
editor.get_effective_style(),
|
|
212
|
-
editor.dark_mode,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
return jsonify(
|
|
216
|
-
{
|
|
217
|
-
"success": True,
|
|
218
|
-
"image": base64_img,
|
|
219
|
-
"bboxes": bboxes,
|
|
220
|
-
"color_map": editor._color_map,
|
|
221
|
-
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
222
|
-
"file": file_path,
|
|
223
|
-
}
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
except Exception as e:
|
|
227
|
-
return jsonify({"error": str(e)}), 500
|
|
228
|
-
|
|
229
|
-
@app.route("/api/new", methods=["POST"])
|
|
230
|
-
def new_figure():
|
|
231
|
-
"""Create a new blank figure."""
|
|
232
|
-
from .. import subplots
|
|
233
|
-
|
|
234
|
-
try:
|
|
235
|
-
# Create new blank figure
|
|
236
|
-
fig, ax = subplots()
|
|
237
|
-
ax.set_title("New Figure")
|
|
238
|
-
ax.text(
|
|
239
|
-
0.5,
|
|
240
|
-
0.5,
|
|
241
|
-
"Add plots using fr.edit(fig)",
|
|
242
|
-
ha="center",
|
|
243
|
-
va="center",
|
|
244
|
-
transform=ax.transAxes,
|
|
245
|
-
fontsize=12,
|
|
246
|
-
color="gray",
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
# Update editor state
|
|
250
|
-
editor.fig = fig
|
|
251
|
-
editor.recipe_path = None
|
|
252
|
-
editor._hitmap_generated = False
|
|
253
|
-
editor._color_map = {}
|
|
254
|
-
|
|
255
|
-
# Re-init style overrides
|
|
256
|
-
editor._init_style_overrides(None)
|
|
257
|
-
|
|
258
|
-
# Regenerate hitmap
|
|
259
|
-
hitmap_img, editor._color_map = generate_hitmap(editor.fig)
|
|
260
|
-
editor._hitmap_base64 = hitmap_to_base64(hitmap_img)
|
|
261
|
-
editor._hitmap_generated = True
|
|
262
|
-
|
|
263
|
-
# Render new preview
|
|
264
|
-
base64_img, bboxes, img_size = render_with_overrides(
|
|
265
|
-
editor.fig,
|
|
266
|
-
editor.get_effective_style(),
|
|
267
|
-
editor.dark_mode,
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
return jsonify(
|
|
271
|
-
{
|
|
272
|
-
"success": True,
|
|
273
|
-
"image": base64_img,
|
|
274
|
-
"bboxes": bboxes,
|
|
275
|
-
"color_map": editor._color_map,
|
|
276
|
-
"img_size": {"width": img_size[0], "height": img_size[1]},
|
|
277
|
-
}
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
except Exception as e:
|
|
281
|
-
return jsonify({"error": str(e)}), 500
|
|
282
|
-
|
|
283
125
|
|
|
284
126
|
__all__ = ["register_core_routes"]
|