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
@@ -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, hitmap, and file switching routes.
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"]