figrecipe 0.6.0__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. figrecipe/__init__.py +161 -1030
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/__init__.py +48 -0
  4. figrecipe/_api/_extract.py +108 -0
  5. figrecipe/_api/_notebook.py +61 -0
  6. figrecipe/_api/_panel.py +113 -0
  7. figrecipe/_api/_save.py +287 -0
  8. figrecipe/_api/_seaborn_proxy.py +34 -0
  9. figrecipe/_api/_style_manager.py +153 -0
  10. figrecipe/_api/_subplots.py +333 -0
  11. figrecipe/_api/_validate.py +82 -0
  12. figrecipe/_cli/__init__.py +7 -0
  13. figrecipe/_cli/_compose.py +87 -0
  14. figrecipe/_cli/_convert.py +117 -0
  15. figrecipe/_cli/_crop.py +82 -0
  16. figrecipe/_cli/_edit.py +70 -0
  17. figrecipe/_cli/_extract.py +128 -0
  18. figrecipe/_cli/_fonts.py +47 -0
  19. figrecipe/_cli/_info.py +67 -0
  20. figrecipe/_cli/_main.py +58 -0
  21. figrecipe/_cli/_reproduce.py +79 -0
  22. figrecipe/_cli/_style.py +77 -0
  23. figrecipe/_cli/_validate.py +66 -0
  24. figrecipe/_cli/_version.py +50 -0
  25. figrecipe/_composition/__init__.py +32 -0
  26. figrecipe/_composition/_alignment.py +452 -0
  27. figrecipe/_composition/_compose.py +179 -0
  28. figrecipe/_composition/_import_axes.py +127 -0
  29. figrecipe/_composition/_visibility.py +125 -0
  30. figrecipe/_dev/__init__.py +4 -93
  31. figrecipe/_dev/_plotters.py +76 -0
  32. figrecipe/_dev/_run_demos.py +56 -0
  33. figrecipe/_dev/browser/__init__.py +69 -0
  34. figrecipe/_dev/browser/_audio.py +240 -0
  35. figrecipe/_dev/browser/_caption.py +356 -0
  36. figrecipe/_dev/browser/_click_effect.py +146 -0
  37. figrecipe/_dev/browser/_cursor.py +196 -0
  38. figrecipe/_dev/browser/_highlight.py +105 -0
  39. figrecipe/_dev/browser/_narration.py +237 -0
  40. figrecipe/_dev/browser/_recorder.py +446 -0
  41. figrecipe/_dev/browser/_utils.py +178 -0
  42. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  43. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  44. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  45. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  46. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  47. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  48. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  49. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  50. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  51. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  53. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  55. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  56. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  57. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  58. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  59. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  60. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  61. figrecipe/_editor/__init__.py +61 -13
  62. figrecipe/_editor/_bbox/__init__.py +43 -0
  63. figrecipe/_editor/_bbox/_collections.py +177 -0
  64. figrecipe/_editor/_bbox/_elements.py +159 -0
  65. figrecipe/_editor/_bbox/_extract.py +402 -0
  66. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  67. figrecipe/_editor/_bbox/_extract_text.py +466 -0
  68. figrecipe/_editor/_bbox/_lines.py +173 -0
  69. figrecipe/_editor/_bbox/_transforms.py +146 -0
  70. figrecipe/_editor/_call_overrides.py +183 -0
  71. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  72. figrecipe/_editor/_figure_layout.py +211 -0
  73. figrecipe/_editor/_flask_app.py +200 -1030
  74. figrecipe/_editor/_helpers.py +251 -0
  75. figrecipe/_editor/_hitmap/__init__.py +76 -0
  76. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  77. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  78. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  79. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  80. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  81. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  82. figrecipe/_editor/_hitmap/_colors.py +181 -0
  83. figrecipe/_editor/_hitmap/_detect.py +194 -0
  84. figrecipe/_editor/_hitmap/_restore.py +154 -0
  85. figrecipe/_editor/_hitmap_main.py +182 -0
  86. figrecipe/_editor/_overrides.py +4 -1
  87. figrecipe/_editor/_plot_types_registry.py +190 -0
  88. figrecipe/_editor/_preferences.py +135 -0
  89. figrecipe/_editor/_render_overrides.py +507 -0
  90. figrecipe/_editor/_renderer.py +81 -186
  91. figrecipe/_editor/_routes_annotation.py +114 -0
  92. figrecipe/_editor/_routes_axis.py +482 -0
  93. figrecipe/_editor/_routes_captions.py +130 -0
  94. figrecipe/_editor/_routes_composition.py +270 -0
  95. figrecipe/_editor/_routes_core.py +126 -0
  96. figrecipe/_editor/_routes_datatable.py +364 -0
  97. figrecipe/_editor/_routes_element.py +335 -0
  98. figrecipe/_editor/_routes_files.py +443 -0
  99. figrecipe/_editor/_routes_image.py +200 -0
  100. figrecipe/_editor/_routes_snapshot.py +94 -0
  101. figrecipe/_editor/_routes_style.py +243 -0
  102. figrecipe/_editor/_templates/__init__.py +116 -1
  103. figrecipe/_editor/_templates/_html.py +154 -64
  104. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  105. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  106. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  107. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  108. figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
  109. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  110. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  111. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  112. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  113. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  114. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  115. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  116. figrecipe/_editor/_templates/_scripts/_core.py +493 -0
  117. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  118. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  119. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  120. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  121. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  122. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  123. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  124. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  125. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  126. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  127. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  128. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  129. figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
  130. figrecipe/_editor/_templates/_scripts/_files.py +429 -0
  131. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  132. figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
  133. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  134. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  135. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  136. figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
  137. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  138. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  139. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  140. figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
  141. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  142. figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
  143. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  144. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  145. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  146. figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
  147. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  148. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  149. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  150. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  151. figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
  152. figrecipe/_editor/_templates/_styles/__init__.py +78 -0
  153. figrecipe/_editor/_templates/_styles/_base.py +111 -0
  154. figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
  155. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  156. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  157. figrecipe/_editor/_templates/_styles/_controls.py +430 -0
  158. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  159. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  160. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  161. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  162. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  163. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  164. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  165. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  166. figrecipe/_editor/_templates/_styles/_forms.py +224 -0
  167. figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
  168. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  169. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  170. figrecipe/_editor/_templates/_styles/_modals.py +127 -0
  171. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  172. figrecipe/_editor/_templates/_styles/_preview.py +430 -0
  173. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  174. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  175. figrecipe/_editor/static/audio/click.mp3 +0 -0
  176. figrecipe/_editor/static/click.mp3 +0 -0
  177. figrecipe/_editor/static/icons/favicon.ico +0 -0
  178. figrecipe/_integrations/__init__.py +17 -0
  179. figrecipe/_integrations/_scitex_stats.py +298 -0
  180. figrecipe/_params/_DECORATION_METHODS.py +8 -0
  181. figrecipe/_recorder.py +63 -109
  182. figrecipe/_recorder_utils.py +124 -0
  183. figrecipe/_reproducer/__init__.py +18 -0
  184. figrecipe/_reproducer/_core.py +509 -0
  185. figrecipe/_reproducer/_custom_plots.py +279 -0
  186. figrecipe/_reproducer/_seaborn.py +100 -0
  187. figrecipe/_reproducer/_violin.py +186 -0
  188. figrecipe/_signatures/_kwargs.py +273 -0
  189. figrecipe/_signatures/_loader.py +21 -423
  190. figrecipe/_signatures/_parsing.py +147 -0
  191. figrecipe/_utils/__init__.py +3 -0
  192. figrecipe/_utils/_bundle.py +205 -0
  193. figrecipe/_wrappers/_axes.py +252 -895
  194. figrecipe/_wrappers/_axes_helpers.py +136 -0
  195. figrecipe/_wrappers/_axes_plots.py +418 -0
  196. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  197. figrecipe/_wrappers/_caption_generator.py +218 -0
  198. figrecipe/_wrappers/_figure.py +188 -1
  199. figrecipe/_wrappers/_panel_labels.py +127 -0
  200. figrecipe/_wrappers/_plot_helpers.py +143 -0
  201. figrecipe/_wrappers/_stat_annotation.py +274 -0
  202. figrecipe/_wrappers/_violin_helpers.py +180 -0
  203. figrecipe/styles/__init__.py +8 -6
  204. figrecipe/styles/_dotdict.py +72 -0
  205. figrecipe/styles/_finalize.py +134 -0
  206. figrecipe/styles/_fonts.py +77 -0
  207. figrecipe/styles/_kwargs_converter.py +178 -0
  208. figrecipe/styles/_plot_styles.py +209 -0
  209. figrecipe/styles/_style_applier.py +42 -480
  210. figrecipe/styles/_style_loader.py +16 -192
  211. figrecipe/styles/_themes.py +151 -0
  212. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  213. figrecipe/styles/presets/SCITEX.yaml +40 -28
  214. figrecipe-0.9.0.dist-info/METADATA +427 -0
  215. figrecipe-0.9.0.dist-info/RECORD +277 -0
  216. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  217. figrecipe/_editor/_bbox.py +0 -978
  218. figrecipe/_editor/_hitmap.py +0 -937
  219. figrecipe/_editor/_templates/_scripts.py +0 -2778
  220. figrecipe/_editor/_templates/_styles.py +0 -1326
  221. figrecipe/_reproducer.py +0 -975
  222. figrecipe-0.6.0.dist-info/METADATA +0 -394
  223. figrecipe-0.6.0.dist-info/RECORD +0 -90
  224. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  225. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  226. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  227. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  228. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  229. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  230. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  231. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  232. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  233. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  234. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  235. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  236. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  237. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  238. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  239. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  240. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  241. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  242. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  243. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  244. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  245. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  246. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  247. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  248. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  249. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  250. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  251. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  252. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  253. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  254. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  255. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  256. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  257. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  258. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  259. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  260. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  261. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  262. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  263. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  264. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  265. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  266. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  267. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  268. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  269. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
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"]
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Core Flask route handlers for the figure editor.
5
+ Handles main page, preview, update, and hitmap routes.
6
+ File operations moved to _routes_files.py.
7
+ """
8
+
9
+ import os
10
+
11
+ from flask import jsonify, render_template_string, request
12
+
13
+ from . import _check_figure_has_content
14
+ from ._helpers import render_with_overrides
15
+
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
+
22
+ def register_core_routes(app, editor):
23
+ """Register core routes with the Flask app."""
24
+ from ._hitmap import generate_hitmap, hitmap_to_base64
25
+ from ._templates import build_html_template
26
+
27
+ @app.route("/")
28
+ def index():
29
+ """Main editor page."""
30
+ base64_img, bboxes, img_size = render_with_overrides(
31
+ editor.fig,
32
+ editor.get_effective_style(),
33
+ editor.dark_mode,
34
+ )
35
+
36
+ style_name = getattr(editor, "_style_name", "SCITEX")
37
+
38
+ # Check if figure has plot content
39
+ figure_has_content = _check_figure_has_content(editor.fig)
40
+
41
+ html = build_html_template(
42
+ image_base64=base64_img,
43
+ bboxes=bboxes,
44
+ color_map=editor._color_map,
45
+ style=editor.style,
46
+ overrides=editor.get_effective_style(),
47
+ img_size=img_size,
48
+ style_name=style_name,
49
+ hot_reload=editor.hot_reload,
50
+ dark_mode=editor.dark_mode,
51
+ figure_has_content=figure_has_content,
52
+ debug_mode=_is_debug_mode(),
53
+ )
54
+
55
+ return render_template_string(html)
56
+
57
+ @app.route("/preview")
58
+ def preview():
59
+ """Get current preview image."""
60
+ base64_img, bboxes, img_size = render_with_overrides(
61
+ editor.fig,
62
+ editor.get_effective_style(),
63
+ editor.dark_mode,
64
+ )
65
+
66
+ return jsonify(
67
+ {
68
+ "image": base64_img,
69
+ "bboxes": bboxes,
70
+ "img_size": {"width": img_size[0], "height": img_size[1]},
71
+ }
72
+ )
73
+
74
+ @app.route("/ping")
75
+ def ping():
76
+ """Health check endpoint for hot reload detection."""
77
+ return jsonify({"status": "ok"})
78
+
79
+ @app.route("/update", methods=["POST"])
80
+ def update():
81
+ """Update preview with new style overrides."""
82
+ from ._preferences import set_preference
83
+
84
+ data = request.get_json() or {}
85
+
86
+ editor.overrides.update(data.get("overrides", {}))
87
+
88
+ # Update and persist dark mode preference
89
+ new_dark_mode = data.get("dark_mode")
90
+ if new_dark_mode is not None and new_dark_mode != editor.dark_mode:
91
+ editor.dark_mode = new_dark_mode
92
+ set_preference("dark_mode", new_dark_mode)
93
+
94
+ base64_img, bboxes, img_size = render_with_overrides(
95
+ editor.fig,
96
+ editor.get_effective_style(),
97
+ editor.dark_mode,
98
+ )
99
+
100
+ return jsonify(
101
+ {
102
+ "image": base64_img,
103
+ "bboxes": bboxes,
104
+ "img_size": {"width": img_size[0], "height": img_size[1]},
105
+ }
106
+ )
107
+
108
+ @app.route("/hitmap")
109
+ def hitmap():
110
+ """Get hitmap image and color map (lazy generation on first request)."""
111
+ if not editor._hitmap_generated:
112
+ print("Generating hitmap (first request)...")
113
+ hitmap_img, editor._color_map = generate_hitmap(editor.fig)
114
+ editor._hitmap_base64 = hitmap_to_base64(hitmap_img)
115
+ editor._hitmap_generated = True
116
+ print("Hitmap ready.")
117
+
118
+ return jsonify(
119
+ {
120
+ "image": editor._hitmap_base64,
121
+ "color_map": editor._color_map,
122
+ }
123
+ )
124
+
125
+
126
+ __all__ = ["register_core_routes"]