plexora 0.0.1__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 (248) hide show
  1. plexora/__init__.py +129 -0
  2. plexora/__main__.py +15 -0
  3. plexora/_url.py +105 -0
  4. plexora/api/__init__.py +55 -0
  5. plexora/api/dataset.py +398 -0
  6. plexora/api/http.py +19 -0
  7. plexora/api/plugin.py +541 -0
  8. plexora/api/store.py +221 -0
  9. plexora/cli.py +777 -0
  10. plexora/client/dist/354_bundle.js +2 -0
  11. plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
  12. plexora/client/dist/418_bundle.js +2 -0
  13. plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
  14. plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
  15. plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
  16. plexora/client/dist/770_bundle.js +1 -0
  17. plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
  18. plexora/client/dist/vendor_bundle.js +2 -0
  19. plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
  20. plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
  21. plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
  22. plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
  23. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
  24. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
  25. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
  26. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
  27. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
  28. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
  29. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
  30. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
  31. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
  32. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
  33. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
  34. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
  35. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
  36. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
  37. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
  38. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
  39. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
  40. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
  41. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
  42. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
  43. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
  44. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
  45. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
  46. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
  47. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
  48. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
  49. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
  50. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
  51. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
  52. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
  53. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
  54. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
  55. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
  56. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
  57. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
  58. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
  59. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
  60. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
  61. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
  62. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
  63. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
  64. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
  65. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
  66. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
  67. plexora/client/src/css/import.css +474 -0
  68. plexora/client/src/css/main.css +903 -0
  69. plexora/client/src/css/openProject.css +363 -0
  70. plexora/client/src/css/quickView.css +166 -0
  71. plexora/client/src/css/tokens.css +51 -0
  72. plexora/client/src/css/viewer.css +1655 -0
  73. plexora/client/src/img/apple-touch-icon.png +0 -0
  74. plexora/client/src/img/favicon.ico +0 -0
  75. plexora/client/src/img/logo.ai +8892 -33
  76. plexora/client/src/img/logo.svg +1 -0
  77. plexora/client/src/img/logo_with_text.ai +9011 -34
  78. plexora/client/src/img/logo_with_text.svg +1 -0
  79. plexora/client/src/js/main.js +704 -0
  80. plexora/client/src/js/pluginRegistry.js +108 -0
  81. plexora/client/src/js/services/appStatus.js +286 -0
  82. plexora/client/src/js/services/browsePicker.js +73 -0
  83. plexora/client/src/js/services/dataLayer.js +400 -0
  84. plexora/client/src/js/services/datasetContext.js +143 -0
  85. plexora/client/src/js/services/glRenderer.js +143 -0
  86. plexora/client/src/js/services/importFormValidation.js +257 -0
  87. plexora/client/src/js/services/numericData.js +100 -0
  88. plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
  89. plexora/client/src/js/services/simpleEventHandler.js +25 -0
  90. plexora/client/src/js/vendor.js +29 -0
  91. plexora/client/src/js/views/channelList.js +663 -0
  92. plexora/client/src/js/views/colorSwatchPicker.js +202 -0
  93. plexora/client/src/js/views/columnClassifier.js +146 -0
  94. plexora/client/src/js/views/coordinateField.js +133 -0
  95. plexora/client/src/js/views/dataSourceField.js +276 -0
  96. plexora/client/src/js/views/imageViewer.js +3306 -0
  97. plexora/client/src/js/views/miniMap.js +787 -0
  98. plexora/client/src/js/views/navbarControls.js +178 -0
  99. plexora/client/src/js/views/openProjectPage.js +239 -0
  100. plexora/client/src/js/views/projectEdit.js +335 -0
  101. plexora/client/src/js/views/quickViewLanding.js +136 -0
  102. plexora/client/src/js/views/rainbow.js +326 -0
  103. plexora/client/src/js/views/requirementsModal.js +572 -0
  104. plexora/client/src/js/views/rgbImageViewer.js +115 -0
  105. plexora/client/src/js/views/searchableSelect.js +411 -0
  106. plexora/client/src/js/views/segmentationProgress.js +122 -0
  107. plexora/client/src/js/views/toolLoader.js +747 -0
  108. plexora/client/src/js/views/viewerControls.js +689 -0
  109. plexora/client/src/js/views/viewerManager.js +434 -0
  110. plexora/client/src/js/views/viewerSidebar.js +1117 -0
  111. plexora/client/src/js/workers/tileDecoder.js +130 -0
  112. plexora/client/src/shaders/frag.glsl +528 -0
  113. plexora/client/src/shaders/vert.glsl +12 -0
  114. plexora/client/templates/base.html +212 -0
  115. plexora/client/templates/index.html +238 -0
  116. plexora/client/templates/open_project.html +96 -0
  117. plexora/client/templates/project_columns.html +86 -0
  118. plexora/client/templates/project_edit.html +196 -0
  119. plexora/client/templates/upload.html +118 -0
  120. plexora/connect.py +528 -0
  121. plexora/datasource.py +782 -0
  122. plexora/jupyter.py +406 -0
  123. plexora/notebook_env.py +232 -0
  124. plexora/paths.py +486 -0
  125. plexora/plugins/__init__.py +12 -0
  126. plexora/plugins/cell_explorer/__init__.py +100 -0
  127. plexora/plugins/cell_explorer/server/__init__.py +11 -0
  128. plexora/plugins/cell_explorer/server/routes.py +167 -0
  129. plexora/plugins/cell_explorer/server/state.py +281 -0
  130. plexora/plugins/cell_explorer/server/values.py +145 -0
  131. plexora/plugins/cell_explorer/server/variables.py +367 -0
  132. plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
  133. plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
  134. plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
  135. plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
  136. plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
  137. plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
  138. plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
  139. plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
  140. plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
  141. plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
  142. plexora/plugins/figure_builder/__init__.py +125 -0
  143. plexora/plugins/figure_builder/server/__init__.py +7 -0
  144. plexora/plugins/figure_builder/server/compose.py +365 -0
  145. plexora/plugins/figure_builder/server/export.py +784 -0
  146. plexora/plugins/figure_builder/server/export_jobs.py +148 -0
  147. plexora/plugins/figure_builder/server/operations.py +556 -0
  148. plexora/plugins/figure_builder/server/pixels.py +142 -0
  149. plexora/plugins/figure_builder/server/provenance.py +174 -0
  150. plexora/plugins/figure_builder/server/render.py +278 -0
  151. plexora/plugins/figure_builder/server/repository.py +701 -0
  152. plexora/plugins/figure_builder/server/routes.py +594 -0
  153. plexora/plugins/figure_builder/server/schema.py +1023 -0
  154. plexora/plugins/figure_builder/server/sources.py +120 -0
  155. plexora/plugins/figure_builder/server/textmetrics.py +155 -0
  156. plexora/plugins/figure_builder/static/figureActions.js +363 -0
  157. plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
  158. plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
  159. plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
  160. plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
  161. plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
  162. plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
  163. plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
  164. plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
  165. plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
  166. plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
  167. plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
  168. plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
  169. plexora/plugins/figure_builder/static/figureRichText.js +634 -0
  170. plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
  171. plexora/plugins/figure_builder/static/figureSchema.js +218 -0
  172. plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
  173. plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
  174. plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
  175. plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
  176. plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
  177. plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
  178. plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
  179. plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
  180. plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
  181. plexora/plugins/gating/__init__.py +76 -0
  182. plexora/plugins/gating/server/__init__.py +0 -0
  183. plexora/plugins/gating/server/anndata_gates.py +367 -0
  184. plexora/plugins/gating/server/database.py +16 -0
  185. plexora/plugins/gating/server/model.py +320 -0
  186. plexora/plugins/gating/server/routes.py +235 -0
  187. plexora/plugins/gating/static/csvGatingList.js +947 -0
  188. plexora/plugins/gating/static/gating.css +289 -0
  189. plexora/plugins/gating/static/gatingApi.js +221 -0
  190. plexora/plugins/gating/static/gatingSidebarController.js +513 -0
  191. plexora/plugins/gating/templates/gating/legacy.html +26 -0
  192. plexora/plugins/gating/templates/gating/panel.html +69 -0
  193. plexora/plugins/roi/__init__.py +95 -0
  194. plexora/plugins/roi/server/__init__.py +8 -0
  195. plexora/plugins/roi/server/adapters.py +655 -0
  196. plexora/plugins/roi/server/geojson.py +267 -0
  197. plexora/plugins/roi/server/geometry.py +181 -0
  198. plexora/plugins/roi/server/mapping.py +169 -0
  199. plexora/plugins/roi/server/operations.py +293 -0
  200. plexora/plugins/roi/server/repository.py +215 -0
  201. plexora/plugins/roi/server/routes.py +409 -0
  202. plexora/plugins/roi/server/schema.py +323 -0
  203. plexora/plugins/roi/static/roi.css +583 -0
  204. plexora/plugins/roi/static/roiApi.js +177 -0
  205. plexora/plugins/roi/static/roiFigureBridge.js +107 -0
  206. plexora/plugins/roi/static/roiGeometry.js +350 -0
  207. plexora/plugins/roi/static/roiRenderer.js +278 -0
  208. plexora/plugins/roi/static/roiSidebarController.js +1038 -0
  209. plexora/plugins/roi/static/roiState.js +518 -0
  210. plexora/plugins/roi/static/roiTools.js +993 -0
  211. plexora/plugins/roi/templates/roi/panel.html +240 -0
  212. plexora/proxy.py +57 -0
  213. plexora/server/models/adapters/__init__.py +94 -0
  214. plexora/server/models/adapters/anndata_adapter.py +388 -0
  215. plexora/server/models/adapters/base.py +93 -0
  216. plexora/server/models/adapters/classify.py +192 -0
  217. plexora/server/models/adapters/csv_adapter.py +105 -0
  218. plexora/server/models/adapters/inspection.py +265 -0
  219. plexora/server/models/adapters/spatialdata_adapter.py +248 -0
  220. plexora/server/models/centroid_tiles.py +305 -0
  221. plexora/server/models/data_model.py +1659 -0
  222. plexora/server/models/database_model.py +169 -0
  223. plexora/server/models/project.py +1270 -0
  224. plexora/server/plugins.py +199 -0
  225. plexora/server/routes/browse_routes.py +41 -0
  226. plexora/server/routes/data_routes.py +294 -0
  227. plexora/server/routes/import_routes.py +603 -0
  228. plexora/server/routes/page_routes.py +139 -0
  229. plexora/server/routes/project_routes.py +438 -0
  230. plexora/server/routes/quick_view_routes.py +62 -0
  231. plexora/server/routes/system_routes.py +39 -0
  232. plexora/server/routes/tool_routes.py +418 -0
  233. plexora/server/utils/addHEColumns.py +11 -0
  234. plexora/server/utils/fast_png.py +97 -0
  235. plexora/server/utils/fullConversion.py +50 -0
  236. plexora/server/utils/native_dialog.py +168 -0
  237. plexora/server/utils/pre_normalization.py +47 -0
  238. plexora/server/utils/segmentation_pyramid.py +598 -0
  239. plexora/server/utils/smallestenclosingcircle.py +126 -0
  240. plexora/server/utils/tiffsurgeon.py +372 -0
  241. plexora/server_cli.py +48 -0
  242. plexora/test.py +0 -0
  243. plexora-0.0.1.dist-info/METADATA +341 -0
  244. plexora-0.0.1.dist-info/RECORD +248 -0
  245. plexora-0.0.1.dist-info/WHEEL +5 -0
  246. plexora-0.0.1.dist-info/entry_points.txt +12 -0
  247. plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
  248. plexora-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,689 @@
1
+ /**
2
+ * @class ViewerControls - wires the core, module-independent sidebar controls:
3
+ * how cells are drawn, and HD tiles. These render whenever the viewer has
4
+ * channels (see index.html's image_kind != 'rgb' gate) regardless of which tool,
5
+ * if any, is currently open -- so their listeners live here instead of inside a
6
+ * specific tool module (previously csvGatingList.js, which meant they silently
7
+ * did nothing unless Thresholding happened to be the active tool on this page
8
+ * view).
9
+ *
10
+ * **Cells is one choice, not several switches.** None / Centroids / Outlines /
11
+ * Filled, exactly one active. This replaced two independent checkboxes, which
12
+ * could express states the renderer had to arbitrate ("Outlines" and
13
+ * "Centroids" both on) and could not express one that matters (Filled). The
14
+ * boundary it draws is the important part: geometry is core's, so a plugin that
15
+ * colours cells supplies a colour per cell id and gets all three
16
+ * representations, rather than shipping a centroid renderer, an outline
17
+ * renderer and a mask renderer of its own.
18
+ *
19
+ * Nothing is drawn over the image on load. A cell layer costs a manifest fetch,
20
+ * a mask pyramid read and a full repaint, and a user who opened a project to
21
+ * look at the image wanted the image. It is turned on by `enableCellLayer()`
22
+ * when a plugin that colours cells activates, using the layer the project
23
+ * recorded (server/models/project.py's CELL_LAYERS).
24
+ *
25
+ * This used to default Outlines on whenever a mask existed, and fall back to
26
+ * centroids when it did not. That produced the worst version of both: a project
27
+ * whose mask pyramid was still being built in the background reported no
28
+ * segmentation yet, so it silently opened on centroids -- for a slide the user
29
+ * had just supplied a mask for -- and the mask only appeared minutes later, if
30
+ * they thought to toggle it.
31
+ */
32
+ class ViewerControls {
33
+
34
+ //: Every representation the control offers, in the order it shows them.
35
+ static MODES = ["none", "centroids", "outlines", "filled"];
36
+
37
+ /**
38
+ * @constructor
39
+ * @param seaDragonViewer - the ImageViewer instance
40
+ * @param config - the configuration file (json)
41
+ * @param eventHandler - the event handler for distributing interface and data updates
42
+ */
43
+ constructor(seaDragonViewer, config, eventHandler) {
44
+ this.seaDragonViewer = seaDragonViewer;
45
+ this.config = config;
46
+ this.eventHandler = eventHandler;
47
+ this.mode = "none";
48
+ this.control = null;
49
+ this.buttons = new Map();
50
+ //: The point-size row, shown only while centroids are the drawing. It
51
+ //: is meaningless for the other three, and a control that is present
52
+ //: but inert reads as broken rather than as not applicable.
53
+ this.pointSizeRow = null;
54
+ //: The per-layer opacity row, shown only while a plugin layer is active
55
+ //: -- there is nothing to fade a plain white cell layer against.
56
+ this.opacityRow = null;
57
+ this.opacitySlider = null;
58
+ }
59
+
60
+ /**
61
+ * @function currentFilter - ranges the active layer's plugin wants drawn, or
62
+ * {} when there is no active layer (a plain viewer, or one whose tools are
63
+ * all closed). Core asks the viewer who is active rather than reading a
64
+ * named plugin off window, so this works for any plugin.
65
+ */
66
+ currentFilter() {
67
+ return window.__plexora?.seaDragonViewer?.cellLayer?.getColorCodedRanges?.() || {};
68
+ }
69
+
70
+ /**
71
+ * @function activeLayer - the cell layer these controls are editing, or null
72
+ * for a viewer with no plugin layers.
73
+ *
74
+ * "Active" and "visible" are different questions: other layers may well be
75
+ * on screen. This is the one whose mode this control shows and whose opacity
76
+ * the slider moves -- the tool the user selected in the sidebar.
77
+ */
78
+ activeLayer() {
79
+ const viewer = this.seaDragonViewer;
80
+ return viewer?.getCellLayer?.(viewer.cellLayerOwner) || null;
81
+ }
82
+
83
+ /** Every registered layer, bottom of the stack first. */
84
+ layers() {
85
+ return this.seaDragonViewer?.cellLayers?.() || [];
86
+ }
87
+
88
+ /**
89
+ * @function init - binds the Cells control and the HD checkbox. No-ops on
90
+ * datasources that don't render them at all (e.g. RGB quick-view, where
91
+ * index.html never emits the controls).
92
+ */
93
+ init() {
94
+ const control = document.querySelector('#cell_display_control');
95
+ const hdEl = document.querySelector('#viewer_controls_hd');
96
+ if (!control || !hdEl) return;
97
+
98
+ this.control = control;
99
+ control.querySelectorAll('[data-cell-mode]').forEach((button) => {
100
+ this.buttons.set(button.dataset.cellMode, button);
101
+ });
102
+
103
+ // config.segmentation is the authoritative signal -- it's set only when
104
+ // a segmentation file was actually registered. imageData[0].src is NOT
105
+ // a reliable proxy: it's the label/"Area" channel only when segmentation
106
+ // exists, otherwise it's just the first real image channel (always has
107
+ // a real src), which used to make this evaluate true even with no
108
+ // segmentation at all.
109
+ //
110
+ // Recorded here rather than acted on: the viewer needs to know there is
111
+ // no mask to fetch, whether or not anything asks to draw one.
112
+ if (!this.hasSegmentation()) {
113
+ this.seaDragonViewer.noLabel = true;
114
+ }
115
+
116
+ this.refreshAvailability();
117
+
118
+ // Delegated, so the disabled check happens in one place and a button
119
+ // added later needs no wiring of its own.
120
+ control.addEventListener('click', (event) => {
121
+ const button = event.target.closest?.('[data-cell-mode]');
122
+ if (!button || button.disabled || !control.contains(button)) return;
123
+ // A click is a decision, so it outranks whatever the automatic
124
+ // fallback chose -- main.js's adoptSegmentation reads this to know
125
+ // whether a mask arriving later may take the drawing over.
126
+ this.seaDragonViewer.centroidsFromFallback = false;
127
+ this.selectMode(button.dataset.cellMode);
128
+ });
129
+
130
+ // Left/right arrows across the group, which is what a radiogroup is
131
+ // expected to do and the only way to reach it without a pointer.
132
+ control.addEventListener('keydown', (event) => {
133
+ const step = event.key === "ArrowRight" || event.key === "ArrowDown" ? 1
134
+ : event.key === "ArrowLeft" || event.key === "ArrowUp" ? -1
135
+ : 0;
136
+ if (!step) return;
137
+ const enabled = ViewerControls.MODES
138
+ .filter((mode) => !this.buttons.get(mode)?.disabled);
139
+ const index = enabled.indexOf(this.mode);
140
+ const next = enabled[(index + step + enabled.length) % enabled.length];
141
+ if (!next) return;
142
+ event.preventDefault();
143
+ this.seaDragonViewer.centroidsFromFallback = false;
144
+ this.buttons.get(next)?.focus();
145
+ this.selectMode(next);
146
+ });
147
+
148
+ // Toggle HD (full-precision 16-bit) tile quality
149
+ hdEl.addEventListener('change', e => {
150
+ this.seaDragonViewer.viewerManagerVMain.setHdMode(e.target.checked);
151
+ });
152
+
153
+ this.bindPointSize();
154
+ this.bindLayerOpacity();
155
+ }
156
+
157
+ /**
158
+ * @function bindPointSize - the centroid dot size slider.
159
+ *
160
+ * Geometry is core's, so this is core's: every plugin that colours cells
161
+ * gets it, and none of them ships a centroid renderer to put it on. Fires
162
+ * on `input` rather than `change` because it is a redraw of what is already
163
+ * in view -- there is nothing to defer to the end of the drag.
164
+ */
165
+ bindPointSize() {
166
+ const slider = document.querySelector('#cell_point_size');
167
+ this.pointSizeRow = document.querySelector('#cell_point_size_row');
168
+ if (!slider) return;
169
+ slider.value = String(this.seaDragonViewer.centroidPointScale ?? 1);
170
+ slider.addEventListener('input', (event) => {
171
+ this.seaDragonViewer.setCentroidPointScale?.(Number(event.target.value));
172
+ });
173
+ this.paintPointSize();
174
+ }
175
+
176
+ /** Show the size slider exactly when there are points to size. */
177
+ paintPointSize() {
178
+ if (this.pointSizeRow) this.pointSizeRow.hidden = this.mode !== "centroids";
179
+ }
180
+
181
+ /**
182
+ * @function bindLayerOpacity - how strongly the ACTIVE layer sits over what
183
+ * is under it.
184
+ *
185
+ * Core's, and shared, for the same reason the mode buttons are: geometry and
186
+ * compositing belong to the viewer, so every plugin that colours cells gets
187
+ * this without shipping a slider of its own. It used to live inside Cell
188
+ * Explorer's panel, where a second such plugin would have had to grow a
189
+ * duplicate -- and where it silently moved whichever layer happened to be
190
+ * active rather than the one the panel was about.
191
+ *
192
+ * Two events, two costs. `input` fires per pixel of drag and only changes
193
+ * the alpha the tile canvases composite at, which is a redraw. `change`
194
+ * fires once on release, and is the only one a plugin needs to hear in order
195
+ * to persist the value.
196
+ */
197
+ bindLayerOpacity() {
198
+ const slider = document.querySelector('#cell_layer_opacity');
199
+ this.opacityRow = document.querySelector('#cell_layer_opacity_row');
200
+ if (!slider) return;
201
+ this.opacitySlider = slider;
202
+ slider.addEventListener('input', (event) => {
203
+ const layer = this.activeLayer();
204
+ if (!layer) return;
205
+ const value = Number(event.target.value) / 100;
206
+ this.seaDragonViewer.setLayerOpacity(layer.name, value);
207
+ this.paintOpacityReadout(value);
208
+ });
209
+ slider.addEventListener('change', (event) => {
210
+ const layer = this.activeLayer();
211
+ if (!layer) return;
212
+ window.dispatchEvent(new CustomEvent("plexora:cell-layer-opacity-changed", {
213
+ detail: { layer: layer.name, value: Number(event.target.value) / 100 },
214
+ }));
215
+ });
216
+ this.paintLayerOpacity();
217
+ }
218
+
219
+ /** Put the slider where the active layer actually is, and show it only when
220
+ * there is a layer for it to act on. */
221
+ paintLayerOpacity() {
222
+ const layer = this.activeLayer();
223
+ if (this.opacityRow) this.opacityRow.hidden = !layer;
224
+ if (!this.opacitySlider || !layer) return;
225
+ this.opacitySlider.value = String(Math.round(layer.opacity * 100));
226
+ this.paintOpacityReadout(layer.opacity);
227
+ }
228
+
229
+ paintOpacityReadout(value) {
230
+ const readout = document.querySelector('#cell_layer_opacity_value');
231
+ if (readout) readout.textContent = `${Math.round(value * 100)}%`;
232
+ }
233
+
234
+ /**
235
+ * @function selectMode - draw cells the given way.
236
+ *
237
+ * The one path that changes what the cell layer shows. Both halves of the
238
+ * old pair of handlers live here because they are not independent: turning
239
+ * outlines on means turning centroids off, and the two checkboxes could
240
+ * only express that by each undoing the other after the fact.
241
+ *
242
+ * @param mode - "none" | "centroids" | "outlines" | "filled"
243
+ */
244
+ async selectMode(mode) {
245
+ const next = ViewerControls.MODES.includes(mode) ? mode : "none";
246
+ if (next === this.mode) return;
247
+ const button = this.buttons.get(next);
248
+ if (button?.disabled) return;
249
+
250
+ const previous = this.mode;
251
+ this.paint(next);
252
+ // Told before the work below, so the label tiles that ensureSegmentationReady
253
+ // renders on their way in are already rendered the right way -- otherwise
254
+ // switching to Filled would draw outlines once and re-render them.
255
+ this.applyMode(next);
256
+
257
+ // Not "what did the user just pick" but "what is on screen now": the
258
+ // mask item and the point overlay are one each, shared by every layer,
259
+ // so a second layer drawing outlines has to keep them on when the active
260
+ // layer moves to centroids.
261
+ const wantsMask = this.maskWanted();
262
+ const wantsPoints = this.pointsWanted();
263
+
264
+ try {
265
+ this.seaDragonViewer.viewerManagerVMain.sel_outlines = wantsMask;
266
+ await this.seaDragonViewer.updateCentroidVisibility(wantsPoints);
267
+ if (wantsMask) {
268
+ await this.seaDragonViewer.ensureSegmentationReady(true);
269
+ await this.seaDragonViewer.updateSegmentationFilter(this.currentFilter(), true);
270
+ }
271
+ if (wantsPoints) {
272
+ this.seaDragonViewer.setLoading(true);
273
+ try {
274
+ this.seaDragonViewer.updateCentroidFilter(this.currentFilter(), true);
275
+ } finally {
276
+ this.seaDragonViewer.setLoading(false);
277
+ }
278
+ }
279
+ if (!wantsMask) {
280
+ this.seaDragonViewer.viewer.forceRedraw();
281
+ }
282
+ } catch (error) {
283
+ console.warn(`Unable to draw cells as "${next}".`, error);
284
+ // Centroids are the fallback only when there are positions to draw:
285
+ // a project with no table, or one whose coordinate roles nobody has
286
+ // answered, would spend the fetch on a manifest with no points in it.
287
+ if (wantsMask && this.canDrawCentroids()) {
288
+ this.seaDragonViewer.viewerManagerVMain.sel_outlines = false;
289
+ await this.seaDragonViewer.updateCentroidFallback(true);
290
+ } else {
291
+ this.paint(previous === next ? "none" : previous);
292
+ this.applyMode(this.mode);
293
+ }
294
+ }
295
+
296
+ this.eventHandler.trigger("SELECTION_CHANGED", this.currentFilter());
297
+ this.announce();
298
+ }
299
+
300
+ /**
301
+ * Push a mode onto whatever this control is editing: the active layer, or
302
+ * core itself when there is none.
303
+ *
304
+ * Choosing a mode for a layer the user had switched off is also a request to
305
+ * see it -- otherwise the click would visibly do nothing.
306
+ */
307
+ applyMode(mode) {
308
+ const layer = this.activeLayer();
309
+ if (!layer) {
310
+ this.seaDragonViewer.setCellDisplayMode(mode);
311
+ return;
312
+ }
313
+ if (mode !== "none") {
314
+ this.seaDragonViewer.setCellLayerVisible(layer.name, true);
315
+ }
316
+ this.seaDragonViewer.setCellLayerMode(layer.name, mode);
317
+ }
318
+
319
+ /** Whether anything on screen is drawn from the label tiles right now. */
320
+ maskWanted() {
321
+ const layers = this.layers();
322
+ if (!layers.length) return this.mode === "outlines" || this.mode === "filled";
323
+ return layers.some((layer) => layer.visible
324
+ && (layer.mode === "outlines" || layer.mode === "filled"));
325
+ }
326
+
327
+ /** Whether anything on screen is drawn as points right now. */
328
+ pointsWanted() {
329
+ const layers = this.layers();
330
+ if (!layers.length) return this.mode === "centroids";
331
+ return layers.some((layer) => layer.visible && layer.mode === "centroids");
332
+ }
333
+
334
+ /**
335
+ * @function refreshLayerSurfaces - bring the shared surfaces into line with
336
+ * whatever the layers now want.
337
+ *
338
+ * The label item and the point overlay are one each and every layer draws
339
+ * onto them, so neither can be switched by the layer that happened to
340
+ * change. Called after a card's eye is toggled or a tool is removed --
341
+ * selectMode does this work inline for the click that caused it.
342
+ *
343
+ * Without this, turning a layer back on while nothing else was drawing a
344
+ * mask left the mask item unloaded: the layer was visible, its canvases were
345
+ * built, and the eye did nothing anyone could see.
346
+ */
347
+ async refreshLayerSurfaces() {
348
+ const manager = this.seaDragonViewer?.viewerManagerVMain;
349
+ if (!this.control || !manager) return;
350
+ const wantsMask = this.maskWanted();
351
+ const wantsPoints = this.pointsWanted();
352
+ // Loading the mask is the expensive half, so it is done only on the edge
353
+ // -- turning it off and on again for a layer that was already drawing
354
+ // one would re-read the pyramid for nothing.
355
+ const maskArriving = wantsMask && !manager.sel_outlines;
356
+ manager.sel_outlines = wantsMask;
357
+ try {
358
+ await this.seaDragonViewer.updateCentroidVisibility(wantsPoints);
359
+ if (maskArriving) {
360
+ await this.seaDragonViewer.ensureSegmentationReady(true);
361
+ await this.seaDragonViewer.updateSegmentationFilter(this.currentFilter(), true);
362
+ }
363
+ if (wantsPoints) {
364
+ this.seaDragonViewer.updateCentroidFilter(this.currentFilter(), true);
365
+ }
366
+ } catch (error) {
367
+ console.warn("Unable to update what the cell layers draw.", error);
368
+ }
369
+ this.seaDragonViewer.viewer?.forceRedraw?.();
370
+ this.announce();
371
+ }
372
+
373
+ /**
374
+ * @function syncToActiveLayer - point the shared controls at whichever layer
375
+ * is now active.
376
+ *
377
+ * Called when a plugin registers, when the user selects a different tool's
378
+ * card, and when one is removed. Purely a repaint: nothing about what is
379
+ * drawn changes here, only what the control says and offers.
380
+ */
381
+ syncToActiveLayer() {
382
+ if (!this.control) return;
383
+ const layer = this.activeLayer();
384
+ this.refreshAvailability();
385
+ this.paint(layer && ViewerControls.MODES.includes(layer.mode)
386
+ ? layer.mode : this.mode);
387
+ this.announce();
388
+ }
389
+
390
+ /**
391
+ * @function adoptMode - reflect a mode the viewer switched to on its own.
392
+ *
393
+ * For the automatic paths -- a mask that failed to load, a pyramid that
394
+ * arrived late -- where the drawing has already changed and the control has
395
+ * to agree with it. Deliberately does none of the loading work: the caller
396
+ * has done it, and re-entering selectMode from inside one of those paths
397
+ * would recurse.
398
+ */
399
+ adoptMode(mode) {
400
+ if (!ViewerControls.MODES.includes(mode) || mode === this.mode) return;
401
+ this.paint(mode);
402
+ this.applyMode(mode);
403
+ this.announce();
404
+ }
405
+
406
+ /** Move the selection. Visual only -- see selectMode for the work. */
407
+ paint(mode) {
408
+ this.mode = mode;
409
+ this.buttons.forEach((button, name) => {
410
+ const active = name === mode;
411
+ button.classList.toggle("is-active", active);
412
+ button.setAttribute("aria-checked", active ? "true" : "false");
413
+ });
414
+ this.paintPointSize();
415
+ this.paintLayerOpacity();
416
+ }
417
+
418
+ /**
419
+ * Tell the rest of the page. The two legacy events are still fired because
420
+ * they are the cross-file, non-module notification other views already
421
+ * listen for (navbarControls.js, and any plugin that hooked them); the mode
422
+ * event is what a listener that understands all four should use.
423
+ *
424
+ * `layer` is the important addition: with several plugins loaded, a mode
425
+ * change belongs to ONE of them, and a plugin storing the user's choice must
426
+ * ignore the others' -- otherwise opening a second tool and clicking
427
+ * Outlines silently rewrites the first tool's saved preference.
428
+ */
429
+ announce() {
430
+ const layer = this.activeLayer();
431
+ const detail = {
432
+ mode: this.mode,
433
+ available: this.availability(),
434
+ offered: this.offeredModes(),
435
+ layer: layer?.name || null,
436
+ opacity: layer ? layer.opacity : null,
437
+ };
438
+ window.dispatchEvent(new CustomEvent("plexora:cell-mode-changed", { detail }));
439
+ window.dispatchEvent(new CustomEvent("plexora:outlines-changed", {
440
+ detail: { enabled: this.maskWanted() },
441
+ }));
442
+ window.dispatchEvent(new CustomEvent("plexora:centroids-changed", {
443
+ detail: { enabled: this.pointsWanted() },
444
+ }));
445
+ }
446
+
447
+ /**
448
+ * @function hasSegmentation - whether a mask pyramid exists and can be
449
+ * drawn right now. False while the background conversion job is still
450
+ * running, which is a state the viewer sees often: import starts the job
451
+ * and opens the project without waiting for it.
452
+ */
453
+ hasSegmentation() {
454
+ return Boolean(this.config?.segmentation);
455
+ }
456
+
457
+ /**
458
+ * @function maskPending - whether a mask exists but is still being
459
+ * converted. The other half of hasSegmentation: between them, "no mask" and
460
+ * "not yet" are two different projects and want two different answers.
461
+ */
462
+ maskPending() {
463
+ return this.config?.segmentation_status === "pending";
464
+ }
465
+
466
+ canDrawOutlines() {
467
+ return this.hasSegmentation() && !this.seaDragonViewer.noLabel;
468
+ }
469
+
470
+ /**
471
+ * Filled needs the labels stored whole. A pyramid written already reduced
472
+ * to boundaries (`segmentationMode` "outlines") has no interior pixels, so
473
+ * there is literally nothing to fill -- offering it would be a button that
474
+ * changes nothing.
475
+ */
476
+ canDrawFilled() {
477
+ return this.canDrawOutlines() && this.config?.segmentationMode === "filled";
478
+ }
479
+
480
+ canDrawCentroids() {
481
+ return PlexoraDataset.hasCentroids(this.config);
482
+ }
483
+
484
+ availability() {
485
+ return {
486
+ none: true,
487
+ centroids: this.canDrawCentroids(),
488
+ outlines: this.canDrawOutlines(),
489
+ filled: this.canDrawFilled(),
490
+ };
491
+ }
492
+
493
+ /**
494
+ * @function offeredModes - what the control actually shows right now.
495
+ *
496
+ * Two filters, in order. The PROJECT decides what can be drawn at all
497
+ * (availability). An active layer then narrows that to what its plugin can
498
+ * draw -- a tool that only marks a handful of cells has no use for Filled,
499
+ * and offering it is offering a button whose result the tool did not design
500
+ * for. A plugin that declares nothing gets everything the project can do.
501
+ *
502
+ * "None" is offered only while there is NO active layer. With one, the
503
+ * card's own eye toggle is what turns a layer off, and a second control
504
+ * meaning almost-but-not-quite the same thing is the kind of ambiguity that
505
+ * makes both of them feel broken.
506
+ */
507
+ offeredModes() {
508
+ const available = this.availability();
509
+ const layer = this.activeLayer();
510
+ const supported = layer?.supportedModes || null;
511
+ const offered = {};
512
+ ViewerControls.MODES.forEach((mode) => {
513
+ offered[mode] = Boolean(available[mode])
514
+ && (!supported || supported.includes(mode));
515
+ });
516
+ if (layer) offered.none = false;
517
+ return offered;
518
+ }
519
+
520
+ /**
521
+ * @function refreshAvailability - show and enable what can be drawn now.
522
+ *
523
+ * Called at init, whenever a background mask conversion lands (main.js's
524
+ * adoptSegmentation), and whenever the active layer changes. Every option
525
+ * ships disabled from the template, so a Filled button is never briefly
526
+ * clickable on a project that cannot fill.
527
+ *
528
+ * Disabled and hidden mean different things here. A mode the PROJECT cannot
529
+ * draw stays visible and disabled, with the reason on its tooltip -- that is
530
+ * a fact about this dataset the user should be able to see. A mode the
531
+ * active PLUGIN does not use is hidden outright, because "not applicable to
532
+ * the tool you have open" has no explanation worth a tooltip and would
533
+ * otherwise leave a row of permanently greyed buttons. The buttons stay in
534
+ * the DOM either way: they belong to the control, not to whichever tool
535
+ * happens to be open.
536
+ */
537
+ refreshAvailability() {
538
+ if (!this.control) return;
539
+ const available = this.availability();
540
+ const offered = this.offeredModes();
541
+ const reasons = {
542
+ centroids: "Needs cell coordinates",
543
+ outlines: "Needs a segmentation mask",
544
+ filled: this.canDrawOutlines()
545
+ ? "This mask is stored as outlines, so there is nothing to fill"
546
+ : "Needs a segmentation mask",
547
+ };
548
+ this.buttons.forEach((button, name) => {
549
+ const usable = Boolean(offered[name]);
550
+ button.style.display = !usable && Boolean(available[name]) ? "none" : "";
551
+ button.disabled = !usable;
552
+ if (usable) {
553
+ button.removeAttribute("title");
554
+ } else if (reasons[name]) {
555
+ button.title = reasons[name];
556
+ }
557
+ });
558
+ }
559
+
560
+ /**
561
+ * @function enableCellLayer - turn on the layer this project draws cells
562
+ * with, for a plugin that colours them.
563
+ *
564
+ * Two separate questions, deliberately kept apart:
565
+ *
566
+ * WHICH LAYER -- mask or points -- is the project's, recorded on it
567
+ * (server/models/project.py's CELL_LAYERS). A plugin does not get to
568
+ * overrule that, or the answer would change depending on which tool was
569
+ * opened first.
570
+ *
571
+ * HOW TO DRAW THE MASK -- filled or outlines -- is the plugin's, because it
572
+ * depends on what the plugin is showing. A tool that colours every cell by
573
+ * a phenotype wants filled; one that marks a few cells wants outlines over
574
+ * visible tissue.
575
+ *
576
+ * @param preference - "filled" | "outlines" | "centroids" | null, from the
577
+ * plugin's `preferredCellMode`. "segmentation" is accepted as an older
578
+ * spelling of "outlines". Anything the project or the mask cannot
579
+ * actually do falls back rather than failing.
580
+ *
581
+ * The recorded layer is a preference, not a promise: a mask whose pyramid
582
+ * is still being built cannot be drawn yet, so this falls back to centroids
583
+ * for now and leaves the stored choice alone. main.js polls the conversion
584
+ * job and swaps the drawing over when it lands.
585
+ *
586
+ * @param name - the layer this is being decided for, or null for core's own
587
+ * control on a viewer with no layers.
588
+ *
589
+ * Does nothing if that layer is already drawing something -- the user's own
590
+ * choice outranks a tool's opinion, and re-opening a tool must not undo it.
591
+ * Asked per layer rather than of the control as a whole: with one shared
592
+ * control, "something is already showing" was true as soon as ANY tool had
593
+ * turned the mask on, so the second plugin to open never got the mode it
594
+ * asked for.
595
+ */
596
+ async enableCellLayer(preference, name = null) {
597
+ if (!this.control) return;
598
+ const layer = name ? this.seaDragonViewer.getCellLayer?.(name) : null;
599
+ if (layer ? layer.mode !== "none" : this.mode !== "none") return;
600
+
601
+ const wanted = preference === 'centroids' ? 'centroids'
602
+ : this.config?.cellLayer || null;
603
+ const canOutline = this.canDrawOutlines();
604
+ const canCentroid = this.canDrawCentroids();
605
+ const onMask = this.maskMode(preference);
606
+
607
+ // A mask that is still converting is not a project without a mask.
608
+ // Standing centroids in for it draws a different representation of the
609
+ // same cells, silently, for however long the job runs -- and a tool that
610
+ // asked for the mask asked because cell SHAPE is what it is showing, so
611
+ // the substitute is the wrong picture rather than a rougher one. Nothing
612
+ // is turned on here: whoever asked shows the wait (see Cell Explorer's
613
+ // panel), and adoptSegmentation turns the layer on when the pyramid
614
+ // lands. A user who would rather not wait can say so, which is what
615
+ // fallBackToCentroids is for.
616
+ if (wanted !== 'centroids' && !canOutline && this.maskPending()) {
617
+ this.seaDragonViewer.cellLayerAwaitingMask = true;
618
+ return;
619
+ }
620
+
621
+ // Segmentation first whenever it is wanted and ready: it shows the real
622
+ // cell shape, and a user who supplied a mask supplied it to be used.
623
+ const target = (wanted !== 'centroids' && canOutline) ? onMask
624
+ : canCentroid ? "centroids"
625
+ : canOutline ? onMask
626
+ : null;
627
+ if (!target) return;
628
+
629
+ await this.selectMode(target);
630
+
631
+ // Set after the switch, which clears it: selectMode treats a click as a
632
+ // user decision, and this one was not. Centroids reached here despite a
633
+ // mask being wanted means there is no mask coming at all -- a pending
634
+ // one returned above -- but the project can still gain one from the edit
635
+ // page mid-session, and main.js swaps the drawing over when it does.
636
+ if (this.mode === "centroids" && wanted !== 'centroids' && !canOutline) {
637
+ this.seaDragonViewer.centroidsFromFallback = true;
638
+ }
639
+ }
640
+
641
+ /**
642
+ * Draw centroids for now, and take the mask when it arrives.
643
+ *
644
+ * The way out of the wait above, for somebody who would rather see roughly
645
+ * where the cells are than nothing at all while a long conversion finishes.
646
+ * Deliberately marked as a fallback rather than as a choice: it is standing
647
+ * in for a mask that is still coming, so adoptSegmentation swaps the drawing
648
+ * over when the pyramid lands, exactly as it would have without the wait.
649
+ *
650
+ * @returns whether there were centroids to fall back to.
651
+ */
652
+ async fallBackToCentroids() {
653
+ if (!this.canDrawCentroids()) return false;
654
+ this.seaDragonViewer.cellLayerAwaitingMask = false;
655
+ await this.selectMode("centroids");
656
+ this.seaDragonViewer.centroidsFromFallback = true;
657
+ return true;
658
+ }
659
+
660
+ /**
661
+ * How to draw the mask for a plugin that asked for `preference`.
662
+ *
663
+ * Filled only when the plugin wants it AND this mask can do it -- a pyramid
664
+ * written already reduced to boundaries has no interior to fill, so asking
665
+ * for filled there has to land on outlines rather than on a mode the
666
+ * control itself has disabled.
667
+ */
668
+ maskMode(preference) {
669
+ return preference === 'filled' && this.canDrawFilled() ? "filled" : "outlines";
670
+ }
671
+
672
+ /**
673
+ * @function ownerMaskPreference - how the plugin currently holding the cell
674
+ * layer would like the mask drawn, or null when nothing holds it.
675
+ *
676
+ * Asked of the viewer rather than of a named plugin, the same way
677
+ * currentFilter() is -- core never learns which plugins exist. main.js puts
678
+ * the preference on the provider when the layer is claimed.
679
+ *
680
+ * This exists for the paths that turn the mask on WITHOUT a plugin
681
+ * activating: a pyramid that finishes converting minutes into a session is
682
+ * the one that matters. That used to hardcode outlines, so a project whose
683
+ * mask was attached from the edit page came up as outlines for the rest of
684
+ * the session however the active tool wanted to draw it.
685
+ */
686
+ ownerMaskPreference() {
687
+ return this.seaDragonViewer?.cellLayer?.preferredCellMode || null;
688
+ }
689
+ }