scitex 2.8.1__py3-none-any.whl → 2.10.2__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 (415) hide show
  1. scitex/__init__.py +15 -7
  2. scitex/__version__.py +1 -2
  3. scitex/_install_guide.py +250 -0
  4. scitex/_optional_deps.py +206 -39
  5. scitex/ai/_gen_ai/_Groq.py +2 -4
  6. scitex/ai/_gen_ai/_OpenAI.py +5 -2
  7. scitex/ai/_gen_ai/_Perplexity.py +20 -6
  8. scitex/audio/__init__.py +24 -15
  9. scitex/audio/_cross_process_lock.py +139 -0
  10. scitex/audio/_mcp_handlers.py +256 -0
  11. scitex/audio/_mcp_tool_schemas.py +203 -0
  12. scitex/audio/engines/elevenlabs_engine.py +5 -2
  13. scitex/audio/mcp_server.py +98 -457
  14. scitex/bridge/__init__.py +30 -19
  15. scitex/bridge/_figrecipe.py +245 -0
  16. scitex/bridge/_helpers.py +2 -1
  17. scitex/bridge/_plt_vis.py +23 -10
  18. scitex/bridge/_stats_plt.py +18 -5
  19. scitex/bridge/_stats_vis.py +16 -2
  20. scitex/browser/__init__.py +84 -44
  21. scitex/browser/automation/__init__.py +5 -1
  22. scitex/browser/core/BrowserMixin.py +17 -4
  23. scitex/browser/core/__init__.py +11 -2
  24. scitex/browser/remote/CaptchaHandler.py +1 -1
  25. scitex/browser/remote/ZenRowsAPIClient.py +1 -1
  26. scitex/capture/grid.py +487 -0
  27. scitex/capture/mcp_handlers.py +401 -0
  28. scitex/capture/mcp_tool_defs.py +192 -0
  29. scitex/capture/mcp_tools.py +241 -0
  30. scitex/capture/mcp_utils.py +30 -0
  31. scitex/cli/convert.py +421 -0
  32. scitex/cli/main.py +6 -4
  33. scitex/datetime/__init__.py +46 -0
  34. scitex/datetime/_linspace.py +100 -0
  35. scitex/datetime/_normalize_timestamp.py +306 -0
  36. scitex/db/_delete_duplicates.py +4 -4
  37. scitex/db/_sqlite3/_delete_duplicates.py +11 -2
  38. scitex/dev/plt/__init__.py +61 -62
  39. scitex/dev/plt/demo_plotters/__init__.py +0 -0
  40. scitex/dev/plt/demo_plotters/plot_mpl_axhline.py +28 -0
  41. scitex/dev/plt/demo_plotters/plot_mpl_axhspan.py +28 -0
  42. scitex/dev/plt/demo_plotters/plot_mpl_axvline.py +28 -0
  43. scitex/dev/plt/demo_plotters/plot_mpl_axvspan.py +28 -0
  44. scitex/dev/plt/demo_plotters/plot_mpl_bar.py +29 -0
  45. scitex/dev/plt/demo_plotters/plot_mpl_barh.py +29 -0
  46. scitex/dev/plt/demo_plotters/plot_mpl_boxplot.py +28 -0
  47. scitex/dev/plt/demo_plotters/plot_mpl_contour.py +31 -0
  48. scitex/dev/plt/demo_plotters/plot_mpl_contourf.py +31 -0
  49. scitex/dev/plt/demo_plotters/plot_mpl_errorbar.py +30 -0
  50. scitex/dev/plt/demo_plotters/plot_mpl_eventplot.py +28 -0
  51. scitex/dev/plt/demo_plotters/plot_mpl_fill.py +30 -0
  52. scitex/dev/plt/demo_plotters/plot_mpl_fill_between.py +31 -0
  53. scitex/dev/plt/demo_plotters/plot_mpl_hexbin.py +28 -0
  54. scitex/dev/plt/demo_plotters/plot_mpl_hist.py +28 -0
  55. scitex/dev/plt/demo_plotters/plot_mpl_hist2d.py +28 -0
  56. scitex/dev/plt/demo_plotters/plot_mpl_imshow.py +29 -0
  57. scitex/dev/plt/demo_plotters/plot_mpl_pcolormesh.py +31 -0
  58. scitex/dev/plt/demo_plotters/plot_mpl_pie.py +29 -0
  59. scitex/dev/plt/demo_plotters/plot_mpl_plot.py +29 -0
  60. scitex/dev/plt/demo_plotters/plot_mpl_quiver.py +31 -0
  61. scitex/dev/plt/demo_plotters/plot_mpl_scatter.py +28 -0
  62. scitex/dev/plt/demo_plotters/plot_mpl_stackplot.py +31 -0
  63. scitex/dev/plt/demo_plotters/plot_mpl_stem.py +29 -0
  64. scitex/dev/plt/demo_plotters/plot_mpl_step.py +29 -0
  65. scitex/dev/plt/demo_plotters/plot_mpl_violinplot.py +28 -0
  66. scitex/dev/plt/demo_plotters/plot_sns_barplot.py +29 -0
  67. scitex/dev/plt/demo_plotters/plot_sns_boxplot.py +29 -0
  68. scitex/dev/plt/demo_plotters/plot_sns_heatmap.py +28 -0
  69. scitex/dev/plt/demo_plotters/plot_sns_histplot.py +29 -0
  70. scitex/dev/plt/demo_plotters/plot_sns_kdeplot.py +29 -0
  71. scitex/dev/plt/demo_plotters/plot_sns_lineplot.py +31 -0
  72. scitex/dev/plt/demo_plotters/plot_sns_scatterplot.py +29 -0
  73. scitex/dev/plt/demo_plotters/plot_sns_stripplot.py +29 -0
  74. scitex/dev/plt/demo_plotters/plot_sns_swarmplot.py +29 -0
  75. scitex/dev/plt/demo_plotters/plot_sns_violinplot.py +29 -0
  76. scitex/dev/plt/demo_plotters/plot_stx_bar.py +29 -0
  77. scitex/dev/plt/demo_plotters/plot_stx_barh.py +29 -0
  78. scitex/dev/plt/demo_plotters/plot_stx_box.py +28 -0
  79. scitex/dev/plt/demo_plotters/plot_stx_boxplot.py +28 -0
  80. scitex/dev/plt/demo_plotters/plot_stx_conf_mat.py +28 -0
  81. scitex/dev/plt/demo_plotters/plot_stx_contour.py +31 -0
  82. scitex/dev/plt/demo_plotters/plot_stx_ecdf.py +28 -0
  83. scitex/dev/plt/demo_plotters/plot_stx_errorbar.py +30 -0
  84. scitex/dev/plt/demo_plotters/plot_stx_fill_between.py +31 -0
  85. scitex/dev/plt/demo_plotters/plot_stx_fillv.py +28 -0
  86. scitex/dev/plt/demo_plotters/plot_stx_heatmap.py +28 -0
  87. scitex/dev/plt/demo_plotters/plot_stx_image.py +28 -0
  88. scitex/dev/plt/demo_plotters/plot_stx_imshow.py +28 -0
  89. scitex/dev/plt/demo_plotters/plot_stx_joyplot.py +28 -0
  90. scitex/dev/plt/demo_plotters/plot_stx_kde.py +28 -0
  91. scitex/dev/plt/demo_plotters/plot_stx_line.py +28 -0
  92. scitex/dev/plt/demo_plotters/plot_stx_mean_ci.py +28 -0
  93. scitex/dev/plt/demo_plotters/plot_stx_mean_std.py +28 -0
  94. scitex/dev/plt/demo_plotters/plot_stx_median_iqr.py +28 -0
  95. scitex/dev/plt/demo_plotters/plot_stx_raster.py +28 -0
  96. scitex/dev/plt/demo_plotters/plot_stx_rectangle.py +28 -0
  97. scitex/dev/plt/demo_plotters/plot_stx_scatter.py +29 -0
  98. scitex/dev/plt/demo_plotters/plot_stx_shaded_line.py +29 -0
  99. scitex/dev/plt/demo_plotters/plot_stx_violin.py +28 -0
  100. scitex/dev/plt/demo_plotters/plot_stx_violinplot.py +28 -0
  101. scitex/dev/plt/mpl/get_dir_ax.py +46 -0
  102. scitex/dev/plt/mpl/get_signatures.py +176 -0
  103. scitex/dev/plt/mpl/get_signatures_details.py +522 -0
  104. scitex/dict/_pop_keys.py +1 -7
  105. scitex/dsp/__init__.py +15 -10
  106. scitex/dsp/add_noise.py +5 -2
  107. scitex/dsp/example.py +35 -22
  108. scitex/dsp/filt.py +8 -3
  109. scitex/dsp/reference.py +3 -2
  110. scitex/dsp/utils/__init__.py +2 -1
  111. scitex/dsp/utils/_differential_bandpass_filters.py +14 -4
  112. scitex/dt/__init__.py +39 -2
  113. scitex/errors.py +82 -521
  114. scitex/fig/__init__.py +4 -4
  115. scitex/fig/editor/edit/panel_loader.py +1 -1
  116. scitex/fig/io/_bundle.py +7 -7
  117. scitex/fts/README.md +262 -0
  118. scitex/fts/TODO.md +66 -0
  119. scitex/fts/__init__.py +90 -0
  120. scitex/fts/_bundle/README_IN_BUNDLE.md +102 -0
  121. scitex/fts/_bundle/_FTS.py +657 -0
  122. scitex/fts/_bundle/__init__.py +38 -0
  123. scitex/fts/_bundle/_children.py +216 -0
  124. scitex/fts/_bundle/_conversion/__init__.py +15 -0
  125. scitex/fts/_bundle/_conversion/_bundle2dict.py +44 -0
  126. scitex/fts/_bundle/_conversion/_dict2bundle.py +50 -0
  127. scitex/fts/_bundle/_dataclasses/_Axes.py +57 -0
  128. scitex/fts/_bundle/_dataclasses/_BBox.py +54 -0
  129. scitex/fts/_bundle/_dataclasses/_ColumnDef.py +72 -0
  130. scitex/fts/_bundle/_dataclasses/_DataFormat.py +40 -0
  131. scitex/fts/_bundle/_dataclasses/_DataInfo.py +135 -0
  132. scitex/fts/_bundle/_dataclasses/_DataSource.py +44 -0
  133. scitex/fts/_bundle/_dataclasses/_Node.py +319 -0
  134. scitex/fts/_bundle/_dataclasses/_NodeRefs.py +45 -0
  135. scitex/fts/_bundle/_dataclasses/_SizeMM.py +38 -0
  136. scitex/fts/_bundle/_dataclasses/__init__.py +35 -0
  137. scitex/fts/_bundle/_extractors/__init__.py +32 -0
  138. scitex/fts/_bundle/_extractors/_extract_bar.py +131 -0
  139. scitex/fts/_bundle/_extractors/_extract_line.py +71 -0
  140. scitex/fts/_bundle/_extractors/_extract_scatter.py +79 -0
  141. scitex/fts/_bundle/_loader.py +134 -0
  142. scitex/fts/_bundle/_mpl_helpers.py +389 -0
  143. scitex/fts/_bundle/_saver.py +269 -0
  144. scitex/fts/_bundle/_storage.py +200 -0
  145. scitex/fts/_bundle/_utils/__init__.py +55 -0
  146. scitex/fts/_bundle/_utils/_const.py +26 -0
  147. scitex/fts/_bundle/_utils/_errors.py +73 -0
  148. scitex/fts/_bundle/_utils/_generate.py +21 -0
  149. scitex/fts/_bundle/_utils/_types.py +76 -0
  150. scitex/fts/_bundle/_validation.py +434 -0
  151. scitex/fts/_bundle/_zipbundle.py +165 -0
  152. scitex/fts/_fig/__init__.py +22 -0
  153. scitex/fts/_fig/_backend/__init__.py +53 -0
  154. scitex/fts/_fig/_backend/_export.py +165 -0
  155. scitex/fts/_fig/_backend/_parser.py +188 -0
  156. scitex/fts/_fig/_backend/_render.py +538 -0
  157. scitex/fts/_fig/_composite.py +345 -0
  158. scitex/fts/_fig/_dataclasses/_ChannelEncoding.py +46 -0
  159. scitex/fts/_fig/_dataclasses/_Encoding.py +82 -0
  160. scitex/fts/_fig/_dataclasses/_Theme.py +441 -0
  161. scitex/fts/_fig/_dataclasses/_TraceEncoding.py +52 -0
  162. scitex/fts/_fig/_dataclasses/__init__.py +47 -0
  163. scitex/fts/_fig/_editor/__init__.py +14 -0
  164. scitex/fts/_fig/_editor/_cui/__init__.py +33 -0
  165. scitex/fts/_fig/_editor/_cui/_backend_detector.py +39 -0
  166. scitex/fts/_fig/_editor/_cui/_bundle_resolver.py +366 -0
  167. scitex/fts/_fig/_editor/_cui/_editor_launcher.py +175 -0
  168. scitex/fts/_fig/_editor/_cui/_manual_handler.py +52 -0
  169. scitex/fts/_fig/_editor/_cui/_panel_loader.py +246 -0
  170. scitex/fts/_fig/_editor/_cui/_path_resolver.py +66 -0
  171. scitex/fts/_fig/_editor/_defaults.py +300 -0
  172. scitex/fts/_fig/_editor/_gui/__init__.py +11 -0
  173. scitex/fts/_fig/_editor/_gui/_flask_editor/__init__.py +20 -0
  174. scitex/fts/_fig/_editor/_gui/_flask_editor/_bbox.py +1339 -0
  175. scitex/fts/_fig/_editor/_gui/_flask_editor/_core.py +1688 -0
  176. scitex/fts/_fig/_editor/_gui/_flask_editor/_plotter.py +664 -0
  177. scitex/fts/_fig/_editor/_gui/_flask_editor/_renderer.py +853 -0
  178. scitex/fts/_fig/_editor/_gui/_flask_editor/_utils.py +79 -0
  179. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/reset.css +41 -0
  180. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/typography.css +16 -0
  181. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/variables.css +85 -0
  182. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/buttons.css +217 -0
  183. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/context-menu.css +93 -0
  184. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/dropdown.css +57 -0
  185. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/forms.css +112 -0
  186. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/modal.css +59 -0
  187. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/sections.css +212 -0
  188. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/canvas.css +176 -0
  189. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/element-inspector.css +190 -0
  190. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/loading.css +59 -0
  191. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/overlay.css +45 -0
  192. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/panel-grid.css +95 -0
  193. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/selection.css +101 -0
  194. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/statistics.css +138 -0
  195. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/index.css +31 -0
  196. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/container.css +7 -0
  197. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/controls.css +56 -0
  198. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/preview.css +78 -0
  199. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/axis.js +314 -0
  200. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/basic.js +107 -0
  201. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/distribute.js +54 -0
  202. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/canvas.js +172 -0
  203. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/dragging.js +258 -0
  204. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/resize.js +48 -0
  205. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/selection.js +71 -0
  206. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/api.js +288 -0
  207. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/state.js +143 -0
  208. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/utils.js +245 -0
  209. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/dev/element-inspector.js +992 -0
  210. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/bbox.js +339 -0
  211. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/element-drag.js +286 -0
  212. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/overlay.js +371 -0
  213. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/preview.js +293 -0
  214. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/main.js +426 -0
  215. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/context-menu.js +152 -0
  216. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/keyboard.js +265 -0
  217. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/controls.js +184 -0
  218. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/download.js +57 -0
  219. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/help.js +100 -0
  220. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/theme.js +34 -0
  221. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/__init__.py +124 -0
  222. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_html.py +851 -0
  223. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_scripts.py +4932 -0
  224. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_styles.py +1657 -0
  225. scitex/fts/_fig/_editor/_gui/_flask_editor.py +36 -0
  226. scitex/fts/_fig/_models/_Annotations.py +115 -0
  227. scitex/fts/_fig/_models/_Axes.py +152 -0
  228. scitex/fts/_fig/_models/_Figure.py +138 -0
  229. scitex/fts/_fig/_models/_Guides.py +104 -0
  230. scitex/fts/_fig/_models/_Plot.py +123 -0
  231. scitex/fts/_fig/_models/_Styles.py +245 -0
  232. scitex/fts/_fig/_models/__init__.py +80 -0
  233. scitex/fts/_fig/_models/_plot_types/__init__.py +156 -0
  234. scitex/fts/_fig/_models/_plot_types/_bar.py +43 -0
  235. scitex/fts/_fig/_models/_plot_types/_box.py +38 -0
  236. scitex/fts/_fig/_models/_plot_types/_distribution.py +36 -0
  237. scitex/fts/_fig/_models/_plot_types/_errorbar.py +60 -0
  238. scitex/fts/_fig/_models/_plot_types/_histogram.py +30 -0
  239. scitex/fts/_fig/_models/_plot_types/_image.py +61 -0
  240. scitex/fts/_fig/_models/_plot_types/_line.py +57 -0
  241. scitex/fts/_fig/_models/_plot_types/_scatter.py +30 -0
  242. scitex/fts/_fig/_models/_plot_types/_seaborn.py +121 -0
  243. scitex/fts/_fig/_models/_plot_types/_violin.py +36 -0
  244. scitex/fts/_fig/_utils/__init__.py +129 -0
  245. scitex/fts/_fig/_utils/_auto_layout.py +127 -0
  246. scitex/fts/_fig/_utils/_calc_bounds.py +111 -0
  247. scitex/fts/_fig/_utils/_const_sizes.py +48 -0
  248. scitex/fts/_fig/_utils/_convert_coords.py +77 -0
  249. scitex/fts/_fig/_utils/_get_template.py +178 -0
  250. scitex/fts/_fig/_utils/_normalize.py +73 -0
  251. scitex/fts/_fig/_utils/_plot_layout.py +397 -0
  252. scitex/fts/_fig/_utils/_validate.py +197 -0
  253. scitex/fts/_kinds/__init__.py +45 -0
  254. scitex/fts/_kinds/_figure/__init__.py +19 -0
  255. scitex/fts/_kinds/_figure/_composite.py +345 -0
  256. scitex/fts/_kinds/_plot/__init__.py +25 -0
  257. scitex/fts/_kinds/_plot/_backend/__init__.py +53 -0
  258. scitex/fts/_kinds/_plot/_backend/_export.py +165 -0
  259. scitex/fts/_kinds/_plot/_backend/_parser.py +188 -0
  260. scitex/fts/_kinds/_plot/_backend/_render.py +538 -0
  261. scitex/fts/_kinds/_plot/_dataclasses/_ChannelEncoding.py +46 -0
  262. scitex/fts/_kinds/_plot/_dataclasses/_Encoding.py +82 -0
  263. scitex/fts/_kinds/_plot/_dataclasses/_Theme.py +441 -0
  264. scitex/fts/_kinds/_plot/_dataclasses/_TraceEncoding.py +52 -0
  265. scitex/fts/_kinds/_plot/_dataclasses/__init__.py +47 -0
  266. scitex/fts/_kinds/_plot/_models/_Annotations.py +115 -0
  267. scitex/fts/_kinds/_plot/_models/_Axes.py +152 -0
  268. scitex/fts/_kinds/_plot/_models/_Figure.py +138 -0
  269. scitex/fts/_kinds/_plot/_models/_Guides.py +104 -0
  270. scitex/fts/_kinds/_plot/_models/_Plot.py +123 -0
  271. scitex/fts/_kinds/_plot/_models/_Styles.py +245 -0
  272. scitex/fts/_kinds/_plot/_models/__init__.py +80 -0
  273. scitex/fts/_kinds/_plot/_models/_plot_types/__init__.py +156 -0
  274. scitex/fts/_kinds/_plot/_models/_plot_types/_bar.py +43 -0
  275. scitex/fts/_kinds/_plot/_models/_plot_types/_box.py +38 -0
  276. scitex/fts/_kinds/_plot/_models/_plot_types/_distribution.py +36 -0
  277. scitex/fts/_kinds/_plot/_models/_plot_types/_errorbar.py +60 -0
  278. scitex/fts/_kinds/_plot/_models/_plot_types/_histogram.py +30 -0
  279. scitex/fts/_kinds/_plot/_models/_plot_types/_image.py +61 -0
  280. scitex/fts/_kinds/_plot/_models/_plot_types/_line.py +57 -0
  281. scitex/fts/_kinds/_plot/_models/_plot_types/_scatter.py +30 -0
  282. scitex/fts/_kinds/_plot/_models/_plot_types/_seaborn.py +121 -0
  283. scitex/fts/_kinds/_plot/_models/_plot_types/_violin.py +36 -0
  284. scitex/fts/_kinds/_plot/_utils/__init__.py +129 -0
  285. scitex/fts/_kinds/_plot/_utils/_auto_layout.py +127 -0
  286. scitex/fts/_kinds/_plot/_utils/_calc_bounds.py +111 -0
  287. scitex/fts/_kinds/_plot/_utils/_const_sizes.py +48 -0
  288. scitex/fts/_kinds/_plot/_utils/_convert_coords.py +77 -0
  289. scitex/fts/_kinds/_plot/_utils/_get_template.py +178 -0
  290. scitex/fts/_kinds/_plot/_utils/_normalize.py +73 -0
  291. scitex/fts/_kinds/_plot/_utils/_plot_layout.py +397 -0
  292. scitex/fts/_kinds/_plot/_utils/_validate.py +197 -0
  293. scitex/fts/_kinds/_shape/__init__.py +141 -0
  294. scitex/fts/_kinds/_stats/__init__.py +56 -0
  295. scitex/fts/_kinds/_stats/_dataclasses/_Stats.py +423 -0
  296. scitex/fts/_kinds/_stats/_dataclasses/__init__.py +48 -0
  297. scitex/fts/_kinds/_table/__init__.py +72 -0
  298. scitex/fts/_kinds/_table/_latex/__init__.py +93 -0
  299. scitex/fts/_kinds/_table/_latex/_editor/__init__.py +11 -0
  300. scitex/fts/_kinds/_table/_latex/_editor/_app.py +725 -0
  301. scitex/fts/_kinds/_table/_latex/_export.py +279 -0
  302. scitex/fts/_kinds/_table/_latex/_figure_exporter.py +153 -0
  303. scitex/fts/_kinds/_table/_latex/_stats_formatter.py +274 -0
  304. scitex/fts/_kinds/_table/_latex/_table_exporter.py +362 -0
  305. scitex/fts/_kinds/_table/_latex/_utils.py +369 -0
  306. scitex/fts/_kinds/_table/_latex/_validator.py +445 -0
  307. scitex/fts/_kinds/_text/__init__.py +77 -0
  308. scitex/fts/_schemas/data_info.schema.json +75 -0
  309. scitex/fts/_schemas/encoding.schema.json +90 -0
  310. scitex/fts/_schemas/node.schema.json +145 -0
  311. scitex/fts/_schemas/render_manifest.schema.json +62 -0
  312. scitex/fts/_schemas/stats.schema.json +132 -0
  313. scitex/fts/_schemas/theme.schema.json +141 -0
  314. scitex/fts/_stats/__init__.py +48 -0
  315. scitex/fts/_stats/_dataclasses/_Stats.py +423 -0
  316. scitex/fts/_stats/_dataclasses/__init__.py +48 -0
  317. scitex/fts/_tables/__init__.py +65 -0
  318. scitex/fts/_tables/_latex/__init__.py +93 -0
  319. scitex/fts/_tables/_latex/_editor/__init__.py +11 -0
  320. scitex/fts/_tables/_latex/_editor/_app.py +725 -0
  321. scitex/fts/_tables/_latex/_export.py +279 -0
  322. scitex/fts/_tables/_latex/_figure_exporter.py +153 -0
  323. scitex/fts/_tables/_latex/_stats_formatter.py +274 -0
  324. scitex/fts/_tables/_latex/_table_exporter.py +362 -0
  325. scitex/fts/_tables/_latex/_utils.py +369 -0
  326. scitex/fts/_tables/_latex/_validator.py +445 -0
  327. scitex/gen/__init__.py +66 -25
  328. scitex/gen/misc.py +28 -0
  329. scitex/io/__init__.py +47 -32
  330. scitex/io/_load.py +87 -36
  331. scitex/io/_load_modules/__init__.py +10 -7
  332. scitex/io/_load_modules/_pandas.py +6 -1
  333. scitex/io/_save.py +299 -1556
  334. scitex/io/_save_modules/__init__.py +76 -19
  335. scitex/io/_save_modules/_figure_utils.py +90 -0
  336. scitex/io/_save_modules/_image_csv.py +497 -0
  337. scitex/io/_save_modules/_legends.py +91 -0
  338. scitex/io/_save_modules/_pltz_bundle.py +356 -0
  339. scitex/io/_save_modules/_pltz_stx.py +536 -0
  340. scitex/io/_save_modules/_stx_bundle.py +104 -0
  341. scitex/io/_save_modules/_symlink.py +96 -0
  342. scitex/io/_save_modules/_yaml.py +1 -1
  343. scitex/io/_save_modules/_zarr.py +64 -18
  344. scitex/io/bundle/README.md +212 -0
  345. scitex/io/bundle/__init__.py +110 -0
  346. scitex/io/{_bundle.py → bundle/_core.py} +168 -97
  347. scitex/io/bundle/_nested.py +713 -0
  348. scitex/io/bundle/_types.py +74 -0
  349. scitex/io/{_zip_bundle.py → bundle/_zip.py} +93 -45
  350. scitex/io/utils/h5_to_zarr.py +1 -1
  351. scitex/logging/__init__.py +108 -13
  352. scitex/logging/_errors.py +508 -0
  353. scitex/logging/_formatters.py +30 -6
  354. scitex/logging/_warnings.py +261 -0
  355. scitex/plt/__init__.py +4 -1
  356. scitex/plt/_figrecipe.py +236 -0
  357. scitex/plt/_subplots/_AxisWrapper.py +6 -0
  358. scitex/plt/_subplots/_AxisWrapperMixins/_UnitAwareMixin.py +112 -1
  359. scitex/plt/_subplots/_FigWrapper.py +15 -0
  360. scitex/plt/_subplots/_SubplotsWrapper.py +125 -489
  361. scitex/plt/_subplots/_export_as_csv.py +11 -0
  362. scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +2 -0
  363. scitex/plt/_subplots/_export_as_csv_formatters/_format_pcolormesh.py +66 -0
  364. scitex/plt/_subplots/_export_as_csv_formatters/_format_stackplot.py +62 -0
  365. scitex/plt/_subplots/_export_as_csv_formatters/test_formatters.py +208 -0
  366. scitex/plt/_subplots/_fonts.py +71 -0
  367. scitex/plt/_subplots/_mm_layout.py +282 -0
  368. scitex/plt/gallery/__init__.py +99 -2
  369. scitex/plt/styles/_plot_postprocess.py +3 -1
  370. scitex/plt/utils/_configure_mpl.py +16 -19
  371. scitex/repro/_RandomStateManager.py +13 -8
  372. scitex/resource/__init__.py +19 -1
  373. scitex/resource/_utils/_get_env_info.py +13 -25
  374. scitex/schema/__init__.py +149 -160
  375. scitex/schema/_encoding.py +273 -0
  376. scitex/schema/_figure_elements.py +406 -0
  377. scitex/schema/_theme.py +360 -0
  378. scitex/schema/_validation.py +0 -98
  379. scitex/scholar/__init__.py +56 -14
  380. scitex/scholar/auth/ScholarAuthManager.py +1 -1
  381. scitex/scholar/auth/__init__.py +11 -2
  382. scitex/scholar/auth/providers/BaseAuthenticator.py +1 -1
  383. scitex/scholar/auth/providers/EZProxyAuthenticator.py +1 -1
  384. scitex/scholar/auth/providers/OpenAthensAuthenticator.py +1 -1
  385. scitex/scholar/auth/providers/ShibbolethAuthenticator.py +1 -1
  386. scitex/scholar/config/ScholarConfig.py +1 -1
  387. scitex/scholar/core/Scholar.py +1 -1
  388. scitex/session/_decorator.py +18 -16
  389. scitex/session/_lifecycle.py +9 -11
  390. scitex/session/template.py +9 -8
  391. scitex/sh/test_sh.py +72 -0
  392. scitex/sh/test_sh_simple.py +61 -0
  393. scitex/stats/__init__.py +221 -97
  394. scitex/stats/_schema.py +21 -22
  395. scitex/stats/descriptive/_circular.py +212 -351
  396. scitex/stats/descriptive/_describe.py +81 -132
  397. scitex/stats/descriptive/_nan.py +205 -433
  398. scitex/stats/descriptive/_real.py +127 -141
  399. scitex/str/_format_plot_text.py +5 -5
  400. scitex/str/_latex.py +26 -84
  401. scitex/str/_latex_fallback.py +53 -47
  402. scitex/web/_search_pubmed.py +5 -4
  403. scitex/writer/tests/test_diff_between.py +451 -0
  404. scitex/writer/tests/test_document_section.py +311 -0
  405. scitex/writer/tests/test_document_workflow.py +393 -0
  406. scitex/writer/tests/test_writer.py +361 -0
  407. scitex/writer/tests/test_writer_integration.py +303 -0
  408. {scitex-2.8.1.dist-info → scitex-2.10.2.dist-info}/METADATA +368 -183
  409. {scitex-2.8.1.dist-info → scitex-2.10.2.dist-info}/RECORD +412 -97
  410. scitex/scholar/docs/to_claude/guidelines/examples/mgmt/ARCHITECTURE_EXAMPLE.md +0 -905
  411. scitex/scholar/docs/to_claude/guidelines/examples/mgmt/BULLETIN_BOARD_EXAMPLE.md +0 -99
  412. scitex/scholar/docs/to_claude/guidelines/examples/mgmt/PROJECT_DESCRIPTION_EXAMPLE.md +0 -96
  413. {scitex-2.8.1.dist-info → scitex-2.10.2.dist-info}/WHEEL +0 -0
  414. {scitex-2.8.1.dist-info → scitex-2.10.2.dist-info}/entry_points.txt +0 -0
  415. {scitex-2.8.1.dist-info → scitex-2.10.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Panel Dragging
3
+ * Handles drag-and-drop for panel repositioning in canvas view
4
+ */
5
+
6
+ // ============================================================================
7
+ // Panel Drag Initialization
8
+ // ============================================================================
9
+ function initPanelDrag(item, panelName) {
10
+ const handle = item.querySelector('.panel-drag-handle');
11
+ const label = item.querySelector('.panel-canvas-label');
12
+
13
+ // Drag from handle (always works)
14
+ if (handle) {
15
+ handle.addEventListener('mousedown', (e) => {
16
+ e.preventDefault();
17
+ e.stopPropagation();
18
+ startPanelDrag(e, item, panelName);
19
+ });
20
+ }
21
+
22
+ // Also allow dragging from panel label
23
+ if (label) {
24
+ label.addEventListener('mousedown', (e) => {
25
+ e.preventDefault();
26
+ e.stopPropagation();
27
+ startPanelDrag(e, item, panelName);
28
+ });
29
+ }
30
+
31
+ // Allow dragging from anywhere on the panel (except interactive elements)
32
+ // This enables intuitive drag behavior while preserving element selection
33
+ item.addEventListener('mousedown', (e) => {
34
+ // Skip if clicking on interactive elements (legends, text paths, etc.)
35
+ if (isInteractiveElement(e.target)) return;
36
+
37
+ // Skip if clicking on drag handle or label (already handled above)
38
+ if (e.target === handle || e.target === label) return;
39
+
40
+ // Start drag from anywhere else on the panel
41
+ startPanelDrag(e, item, panelName);
42
+ });
43
+
44
+ // Set cursor to indicate draggability
45
+ item.style.cursor = 'move';
46
+ }
47
+
48
+ // ============================================================================
49
+ // Panel Drag Start
50
+ // ============================================================================
51
+ function startPanelDrag(e, item, name) {
52
+ e.preventDefault();
53
+
54
+ // Handle selection based on Ctrl key
55
+ const isCtrlPressed = e.ctrlKey || e.metaKey;
56
+ const wasAlreadySelected = item.classList.contains('selected');
57
+
58
+ if (isCtrlPressed) {
59
+ // Ctrl+Click: toggle this panel's selection (add/remove from multi-select)
60
+ item.classList.toggle('selected');
61
+ } else if (!wasAlreadySelected) {
62
+ // Regular click on unselected panel: select only this one
63
+ deselectAllPanels();
64
+ item.classList.add('selected');
65
+ }
66
+ // If clicking on already-selected panel without Ctrl:
67
+ // Don't change selection yet - could be start of multi-panel drag
68
+ // Selection will be finalized in stopPanelDrag based on hasMoved
69
+
70
+ // Collect all selected panels for group dragging
71
+ const selectedItems = Array.from(document.querySelectorAll('.panel-canvas-item.selected'));
72
+ if (selectedItems.length === 0) {
73
+ // If somehow nothing selected, select the clicked item
74
+ item.classList.add('selected');
75
+ selectedItems.push(item);
76
+ }
77
+
78
+ // Store drag state for all selected panels
79
+ draggedPanel = {
80
+ primaryItem: item,
81
+ primaryName: name,
82
+ selectedItems: selectedItems,
83
+ startX: e.clientX,
84
+ startY: e.clientY,
85
+ hasMoved: false, // Track if actual drag occurred
86
+ wasAlreadySelected, // Track initial selection state for click handling
87
+ isCtrlPressed, // Track if Ctrl was pressed
88
+ startPositions: selectedItems.map(el => ({
89
+ item: el,
90
+ name: el.dataset.panelName,
91
+ x: parseFloat(el.style.left) || 0,
92
+ y: parseFloat(el.style.top) || 0
93
+ }))
94
+ };
95
+
96
+ document.addEventListener('mousemove', onPanelDrag);
97
+ document.addEventListener('mouseup', stopPanelDrag);
98
+
99
+ // Show position indicator for primary panel
100
+ updatePositionIndicator(name,
101
+ panelPositions[name]?.x || 0,
102
+ panelPositions[name]?.y || 0
103
+ );
104
+ }
105
+
106
+ // ============================================================================
107
+ // Panel Drag Move
108
+ // ============================================================================
109
+ function onPanelDrag(e) {
110
+ if (!draggedPanel) return;
111
+
112
+ // Calculate delta from drag start
113
+ const dx = e.clientX - draggedPanel.startX;
114
+ const dy = e.clientY - draggedPanel.startY;
115
+
116
+ // Mark as moved if we've actually dragged (threshold: 3px)
117
+ if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
118
+ draggedPanel.hasMoved = true;
119
+ }
120
+
121
+ // Snap to grid (optional: 5mm grid)
122
+ const gridSize = 5 * canvasScale; // 5mm grid
123
+ const snappedDx = Math.round(dx / gridSize) * gridSize;
124
+ const snappedDy = Math.round(dy / gridSize) * gridSize;
125
+
126
+ // Move all selected panels by the same delta
127
+ draggedPanel.startPositions.forEach(({item, name, x, y}) => {
128
+ const newX = x + snappedDx;
129
+ const newY = y + snappedDy;
130
+
131
+ // Constrain to canvas bounds (allow slight negative for edge alignment)
132
+ const constrainedX = Math.max(-10, newX);
133
+ const constrainedY = Math.max(-10, newY);
134
+
135
+ // Update pixel positions
136
+ item.style.left = constrainedX + 'px';
137
+ item.style.top = constrainedY + 'px';
138
+
139
+ panelPositions[name] = {
140
+ ...panelPositions[name],
141
+ x: constrainedX,
142
+ y: constrainedY
143
+ };
144
+
145
+ // Update mm positions
146
+ panelLayoutMm[name] = {
147
+ ...panelLayoutMm[name],
148
+ x_mm: constrainedX / canvasScale,
149
+ y_mm: constrainedY / canvasScale
150
+ };
151
+ });
152
+
153
+ // Show position indicator for primary panel
154
+ const primaryPos = panelPositions[draggedPanel.primaryName];
155
+ if (primaryPos) {
156
+ updatePositionIndicator(draggedPanel.primaryName, primaryPos.x, primaryPos.y);
157
+ }
158
+
159
+ // Mark layout as modified
160
+ layoutModified = true;
161
+ }
162
+
163
+ // ============================================================================
164
+ // Panel Drag Stop
165
+ // ============================================================================
166
+ function stopPanelDrag() {
167
+ if (draggedPanel) {
168
+ // Handle click (no movement) on already-selected panel without Ctrl:
169
+ // Finalize selection to only the clicked panel
170
+ if (!draggedPanel.hasMoved && draggedPanel.wasAlreadySelected && !draggedPanel.isCtrlPressed) {
171
+ // This was a simple click on an already-selected panel
172
+ // Deselect all others, keep only the clicked panel selected
173
+ deselectAllPanels();
174
+ draggedPanel.primaryItem.classList.add('selected');
175
+ }
176
+
177
+ // Reset cursor for all selected panels
178
+ draggedPanel.selectedItems.forEach(item => {
179
+ item.style.cursor = 'move';
180
+ });
181
+
182
+ draggedPanel = null;
183
+ document.removeEventListener('mousemove', onPanelDrag);
184
+ document.removeEventListener('mouseup', stopPanelDrag);
185
+
186
+ // Update canvas size if panel moved outside
187
+ updateCanvasSize();
188
+
189
+ // Hide position indicator after a delay
190
+ setTimeout(() => {
191
+ const indicator = document.getElementById('position-indicator');
192
+ if (indicator) {
193
+ indicator.style.display = 'none';
194
+ }
195
+ }, 1000);
196
+
197
+ // Auto-save layout
198
+ if (layoutModified) {
199
+ saveLayout();
200
+ }
201
+ }
202
+ }
203
+
204
+ // ============================================================================
205
+ // Position Indicator
206
+ // ============================================================================
207
+ function updatePositionIndicator(panelName, x, y) {
208
+ let indicator = document.getElementById('position-indicator');
209
+ if (!indicator) {
210
+ indicator = document.createElement('div');
211
+ indicator.id = 'position-indicator';
212
+ indicator.style.cssText = `
213
+ position: fixed;
214
+ bottom: 20px;
215
+ right: 20px;
216
+ background: rgba(0, 0, 0, 0.8);
217
+ color: white;
218
+ padding: 8px 12px;
219
+ border-radius: 4px;
220
+ font-family: monospace;
221
+ font-size: 12px;
222
+ z-index: 10000;
223
+ pointer-events: none;
224
+ `;
225
+ document.body.appendChild(indicator);
226
+ }
227
+
228
+ const xMm = (x / canvasScale).toFixed(1);
229
+ const yMm = (y / canvasScale).toFixed(1);
230
+ indicator.textContent = `${panelName}: x=${xMm}mm, y=${yMm}mm`;
231
+ indicator.style.display = 'block';
232
+ }
233
+
234
+ // ============================================================================
235
+ // Manual Layout Save
236
+ // ============================================================================
237
+ function saveLayoutManually() {
238
+ if (layoutModified) {
239
+ saveLayout();
240
+ } else {
241
+ setStatus('No changes to save');
242
+ }
243
+ }
244
+
245
+ // ============================================================================
246
+ // Legacy Drag Functions (for backward compatibility)
247
+ // ============================================================================
248
+ function startDrag(e, item, name) {
249
+ startPanelDrag(e, item, name);
250
+ }
251
+
252
+ function onDrag(e) {
253
+ onPanelDrag(e);
254
+ }
255
+
256
+ function stopDrag() {
257
+ stopPanelDrag();
258
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Panel Resizing
3
+ * Handles panel resize operations in canvas view
4
+ */
5
+
6
+ // ============================================================================
7
+ // Resize Start
8
+ // ============================================================================
9
+ function startResize(e, item, name) {
10
+ e.preventDefault();
11
+ e.stopPropagation();
12
+
13
+ resizingPanel = {item, name, startX: e.clientX, startY: e.clientY};
14
+ document.addEventListener('mousemove', onResize);
15
+ document.addEventListener('mouseup', stopResize);
16
+ }
17
+
18
+ // ============================================================================
19
+ // Resize Move
20
+ // ============================================================================
21
+ function onResize(e) {
22
+ if (!resizingPanel) return;
23
+
24
+ const dx = e.clientX - resizingPanel.startX;
25
+ const dy = e.clientY - resizingPanel.startY;
26
+
27
+ const pos = panelPositions[resizingPanel.name];
28
+ pos.width = Math.max(100, pos.width + dx);
29
+ pos.height = Math.max(100, pos.height + dy);
30
+
31
+ resizingPanel.item.style.width = pos.width + 'px';
32
+ resizingPanel.item.style.height = pos.height + 'px';
33
+
34
+ resizingPanel.startX = e.clientX;
35
+ resizingPanel.startY = e.clientY;
36
+ }
37
+
38
+ // ============================================================================
39
+ // Resize Stop
40
+ // ============================================================================
41
+ function stopResize() {
42
+ if (resizingPanel) {
43
+ resizingPanel = null;
44
+ document.removeEventListener('mousemove', onResize);
45
+ document.removeEventListener('mouseup', stopResize);
46
+ layoutModified = true;
47
+ }
48
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Panel Selection
3
+ * Handles panel selection and multi-selection in canvas view
4
+ */
5
+
6
+ // ============================================================================
7
+ // Select All Panels
8
+ // ============================================================================
9
+ function selectAllPanels() {
10
+ document.querySelectorAll('.panel-canvas-item').forEach(item => {
11
+ item.classList.add('selected');
12
+ });
13
+ }
14
+
15
+ // ============================================================================
16
+ // Deselect All Panels
17
+ // ============================================================================
18
+ function deselectAllPanels() {
19
+ document.querySelectorAll('.panel-canvas-item').forEach(item => {
20
+ item.classList.remove('selected');
21
+ });
22
+
23
+ // Also clear element selection in single-panel view
24
+ selectedElement = null;
25
+ hoveredElement = null;
26
+ updateOverlay();
27
+ }
28
+
29
+ // ============================================================================
30
+ // Get Selected Panels
31
+ // ============================================================================
32
+ function getSelectedPanels() {
33
+ const selectedItems = document.querySelectorAll('.panel-canvas-item.selected');
34
+ return Array.from(selectedItems).map(item => ({
35
+ item: item,
36
+ name: item.dataset.panelName,
37
+ pos: panelPositions[item.dataset.panelName]
38
+ }));
39
+ }
40
+
41
+ // ============================================================================
42
+ // Toggle Panel Selection
43
+ // ============================================================================
44
+ function togglePanelSelection(panelName) {
45
+ const item = document.querySelector(`.panel-canvas-item[data-panel-name="${panelName}"]`);
46
+ if (item) {
47
+ item.classList.toggle('selected');
48
+ }
49
+ }
50
+
51
+ // ============================================================================
52
+ // Select Single Panel
53
+ // ============================================================================
54
+ function selectPanel(panelName, clearOthers = true) {
55
+ if (clearOthers) {
56
+ deselectAllPanels();
57
+ }
58
+
59
+ const item = document.querySelector(`.panel-canvas-item[data-panel-name="${panelName}"]`);
60
+ if (item) {
61
+ item.classList.add('selected');
62
+ }
63
+ }
64
+
65
+ // ============================================================================
66
+ // Check if Panel is Selected
67
+ // ============================================================================
68
+ function isPanelSelected(panelName) {
69
+ const item = document.querySelector(`.panel-canvas-item[data-panel-name="${panelName}"]`);
70
+ return item ? item.classList.contains('selected') : false;
71
+ }
@@ -0,0 +1,288 @@
1
+ /**
2
+ * API Communication
3
+ * Handles all server requests for preview, update, save, etc.
4
+ */
5
+
6
+ // ============================================================================
7
+ // Preview and Update API
8
+ // ============================================================================
9
+ function updatePreview() {
10
+ const data = collectOverrides();
11
+
12
+ // Skip auto-update if showing original preview (user hasn't explicitly requested update)
13
+ if (isShowingOriginalPreview) {
14
+ return;
15
+ }
16
+
17
+ setStatus('Updating preview...');
18
+
19
+ // Preserve current selection to restore after update
20
+ const prevSelectedElement = selectedElement;
21
+
22
+ fetch('/update', {
23
+ method: 'POST',
24
+ headers: {'Content-Type': 'application/json'},
25
+ body: JSON.stringify(data)
26
+ })
27
+ .then(r => r.json())
28
+ .then(result => {
29
+ // Remove SVG wrapper if exists, show img element for re-rendered preview
30
+ const svgWrapper = document.getElementById('preview-svg-wrapper');
31
+ if (svgWrapper) {
32
+ svgWrapper.style.display = 'none';
33
+ }
34
+ document.getElementById('preview').style.display = 'block';
35
+
36
+ // Mark that we're no longer showing original preview
37
+ isShowingOriginalPreview = false;
38
+
39
+ if (result.image) {
40
+ document.getElementById('preview').src = 'data:image/png;base64,' + result.image;
41
+
42
+ // Store schema v0.3 metadata if available
43
+ if (result.schema_meta) {
44
+ schemaMeta = result.schema_meta;
45
+ }
46
+ }
47
+ elementBboxes = result.bboxes || {};
48
+ imgSize = result.img_size || {width: 0, height: 0};
49
+ setStatus('Preview updated');
50
+
51
+ // Restore selection if the element still exists in the new bboxes
52
+ if (prevSelectedElement && elementBboxes[prevSelectedElement]) {
53
+ selectedElement = prevSelectedElement;
54
+ updateOverlay();
55
+ } else {
56
+ selectedElement = null;
57
+ updateOverlay();
58
+ }
59
+ })
60
+ .catch(err => {
61
+ setStatus('Error updating preview: ' + err, true);
62
+ });
63
+ }
64
+
65
+ // ============================================================================
66
+ // Save API
67
+ // ============================================================================
68
+ function saveToBundle() {
69
+ const data = collectOverrides();
70
+ setStatus('Saving...');
71
+ return fetch('/save', {
72
+ method: 'POST',
73
+ headers: {'Content-Type': 'application/json'},
74
+ body: JSON.stringify(data)
75
+ })
76
+ .then(r => r.json())
77
+ .then(result => {
78
+ setStatus(result.status || 'Saved successfully');
79
+ })
80
+ .catch(err => {
81
+ setStatus('Error saving: ' + err, true);
82
+ throw err;
83
+ });
84
+ }
85
+
86
+ // ============================================================================
87
+ // Export API
88
+ // ============================================================================
89
+ function exportToFormat(format) {
90
+ const data = collectOverrides();
91
+ setStatus('Exporting to ' + format + '...');
92
+
93
+ // First export to bundle
94
+ fetch('/export', {
95
+ method: 'POST',
96
+ headers: {'Content-Type': 'application/json'},
97
+ body: JSON.stringify({...data, format: format})
98
+ })
99
+ .then(r => r.json())
100
+ .then(result => {
101
+ setStatus(result.status || 'Exported successfully');
102
+ // Then trigger download
103
+ downloadFile(result.path, format);
104
+ })
105
+ .catch(err => {
106
+ setStatus('Error exporting: ' + err, true);
107
+ });
108
+ }
109
+
110
+ function downloadFile(path, format) {
111
+ const filename = path.split('/').pop();
112
+ fetch('/download/' + filename)
113
+ .then(r => r.blob())
114
+ .then(blob => {
115
+ const url = URL.createObjectURL(blob);
116
+ const a = document.createElement('a');
117
+ a.href = url;
118
+ a.download = filename;
119
+ a.click();
120
+ URL.revokeObjectURL(url);
121
+ });
122
+ }
123
+
124
+ // ============================================================================
125
+ // Panel API (Multi-panel figures)
126
+ // ============================================================================
127
+
128
+ // Helper to normalize panel names for comparison (strip .pltz extensions)
129
+ function normalizePanelName(name) {
130
+ if (typeof name !== 'string') return '';
131
+ return name.replace('.pltz.d', '').replace('.pltz', '');
132
+ }
133
+
134
+ function loadPanelForEditing(panelName) {
135
+ setStatus('Loading panel ' + panelName + '...');
136
+
137
+ // Find panel index from panel name
138
+ // Handle both formats: array of strings ["A", "B"] or array of objects [{name: "A"}, ...]
139
+ // Also handle extension mismatches (e.g., "A" vs "A.pltz")
140
+ let panelIndex = -1;
141
+ const normalizedSearch = normalizePanelName(panelName);
142
+
143
+ if (panelData && panelData.panels) {
144
+ panelIndex = panelData.panels.findIndex(p => {
145
+ // Check if it's a string or an object with name property
146
+ if (typeof p === 'string') {
147
+ return normalizePanelName(p) === normalizedSearch;
148
+ } else if (p && p.name) {
149
+ return normalizePanelName(p.name) === normalizedSearch;
150
+ }
151
+ return false;
152
+ });
153
+ }
154
+
155
+ if (panelIndex === -1) {
156
+ setStatus('Panel not found: ' + panelName, true);
157
+ return Promise.reject(new Error('Panel not found: ' + panelName));
158
+ }
159
+
160
+ return fetch('/switch_panel/' + panelIndex)
161
+ .then(r => r.json())
162
+ .then(result => {
163
+ if (result.error) {
164
+ throw new Error(result.error);
165
+ }
166
+
167
+ // Update panel state
168
+ currentPanelIndex = panelIndex;
169
+
170
+ // Update preview image
171
+ if (result.image) {
172
+ document.getElementById('preview').src = 'data:image/png;base64,' + result.image;
173
+ }
174
+
175
+ // Update bboxes and overlays
176
+ elementBboxes = result.bboxes || {};
177
+ imgSize = result.img_size || {width: 0, height: 0};
178
+
179
+ // Sync bboxes cache
180
+ panelBboxesCache[panelName] = {bboxes: elementBboxes, imgSize: imgSize};
181
+
182
+ // Update overrides
183
+ if (result.overrides) {
184
+ overrides = result.overrides;
185
+ }
186
+
187
+ // Select the element that was clicked
188
+ if (result.selected_element) {
189
+ selectedElement = result.selected_element;
190
+ }
191
+
192
+ // Scroll to section and show properties
193
+ scrollToSection(selectedElement);
194
+
195
+ // Keep unified canvas view only - don't show single-panel preview
196
+ showingPanelGrid = true;
197
+ // Update panel path display in right panel header
198
+ const panelPath = document.getElementById('current-panel-path');
199
+ if (panelPath) {
200
+ panelPath.textContent = panelName || 'Single Panel';
201
+ }
202
+
203
+ setStatus('Panel loaded');
204
+ return result;
205
+ })
206
+ .catch(err => {
207
+ setStatus('Error loading panel: ' + err, true);
208
+ throw err;
209
+ });
210
+ }
211
+
212
+ // ============================================================================
213
+ // Layout Save API
214
+ // ============================================================================
215
+ function saveLayout() {
216
+ const data = {
217
+ layout: panelLayoutMm,
218
+ overrides: overrides
219
+ };
220
+
221
+ setStatus('Saving layout...');
222
+
223
+ return fetch('/save_layout', {
224
+ method: 'POST',
225
+ headers: {'Content-Type': 'application/json'},
226
+ body: JSON.stringify(data)
227
+ })
228
+ .then(r => r.json())
229
+ .then(result => {
230
+ setStatus('Layout saved');
231
+ layoutModified = false;
232
+ })
233
+ .catch(err => {
234
+ setStatus('Error saving layout: ' + err, true);
235
+ throw err;
236
+ });
237
+ }
238
+
239
+ // ============================================================================
240
+ // Element Position Update API
241
+ // ============================================================================
242
+ function updateElementPosition(panelName, elementName, position) {
243
+ const data = {
244
+ panel_name: panelName,
245
+ element_name: elementName,
246
+ position: position,
247
+ overrides: overrides
248
+ };
249
+
250
+ return fetch('/save_element_position', {
251
+ method: 'POST',
252
+ headers: {'Content-Type': 'application/json'},
253
+ body: JSON.stringify(data)
254
+ })
255
+ .then(r => r.json())
256
+ .then(result => {
257
+ setStatus('Element position updated');
258
+ // Reload panel to show updated position
259
+ return loadPanelForEditing(panelName);
260
+ })
261
+ .catch(err => {
262
+ setStatus('Error updating element position: ' + err, true);
263
+ throw err;
264
+ });
265
+ }
266
+
267
+ // ============================================================================
268
+ // Auto-Update Scheduling
269
+ // ============================================================================
270
+ function scheduleUpdate() {
271
+ clearTimeout(updateTimer);
272
+ updateTimer = setTimeout(updatePreview, DEBOUNCE_DELAY);
273
+ }
274
+
275
+ function setAutoUpdateInterval() {
276
+ if (autoUpdateIntervalId) {
277
+ clearInterval(autoUpdateIntervalId);
278
+ autoUpdateIntervalId = null;
279
+ }
280
+
281
+ const intervalMs = parseInt(document.getElementById('auto_update_interval').value);
282
+ if (intervalMs > 0) {
283
+ autoUpdateIntervalId = setInterval(() => {
284
+ updatePreview();
285
+ }, intervalMs);
286
+ }
287
+ }
288
+