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,273 @@
1
+ /**
2
+ * FigureViewOptions - rulers, grid, margins, snapping and the unit.
3
+ *
4
+ * Everything under the View menu, and nothing else. It is one file because it
5
+ * is one idea: these are all statements about how the page is DRAWN for this
6
+ * person on this machine, none of them touches the figure, and none of them
7
+ * belongs in the document -- two people opening the same figure should not
8
+ * inherit each other's rulers.
9
+ *
10
+ * So they live in localStorage, which also settles what happens on a new
11
+ * machine: the defaults below, which are the ones that get out of the way.
12
+ * Rulers and the grid are off, margins and smart guides are on, because the
13
+ * first two are reference and the second two are what stops a figure being
14
+ * subtly misaligned.
15
+ *
16
+ * ## Why the grid is a page child and the rulers are not
17
+ *
18
+ * The grid and the margins are drawn inside `.fb-page`, so they scale with it
19
+ * and a 5 mm square stays 5 mm at every zoom. The rulers cannot be: they are
20
+ * fixed to the viewport and have to redraw on scroll as well as on zoom, which
21
+ * is why they are `<canvas>` -- an A4 page at 1 mm resolution is 210 ticks, and
22
+ * 210 positioned elements rebuilt on every scroll event is a page that stutters.
23
+ */
24
+ class FigureViewOptions {
25
+
26
+ static get STORAGE_KEY() { return "plexora.figure_builder.view"; }
27
+
28
+ /** Height of the horizontal ruler and width of the vertical one, in CSS px. */
29
+ static get RULER_SIZE() { return 20; }
30
+
31
+ static get DEFAULTS() {
32
+ return {
33
+ rulers: false,
34
+ grid: false,
35
+ snapGrid: false,
36
+ smartGuides: true,
37
+ margins: true,
38
+ units: "mm",
39
+ gridMm: 5,
40
+ };
41
+ }
42
+
43
+ /** How many of the figure's millimetres one unit is, and how it is written. */
44
+ static get UNITS() {
45
+ return {
46
+ mm: { per: 1, label: "mm", step: 10, minor: 5 },
47
+ in: { per: 25.4, label: "in", step: 1, minor: 0.25 },
48
+ pt: { per: 25.4 / 72, label: "pt", step: 72, minor: 18 },
49
+ };
50
+ }
51
+
52
+ constructor(options) {
53
+ this.workspace = options.workspace;
54
+ this.canvas = options.canvas;
55
+ this.prefs = { ...FigureViewOptions.DEFAULTS, ...this.read() };
56
+ }
57
+
58
+ read() {
59
+ try {
60
+ return JSON.parse(window.localStorage.getItem(FigureViewOptions.STORAGE_KEY)) || {};
61
+ } catch (error) {
62
+ // Private-browsing modes throw rather than returning null. Losing a
63
+ // preference is a small inconvenience; throwing here would take the
64
+ // whole workspace down with it.
65
+ return {};
66
+ }
67
+ }
68
+
69
+ write() {
70
+ try {
71
+ window.localStorage.setItem(
72
+ FigureViewOptions.STORAGE_KEY, JSON.stringify(this.prefs));
73
+ } catch (error) {
74
+ /* see read() */
75
+ }
76
+ }
77
+
78
+ setup() {
79
+ this._onScroll = () => this.drawRulers();
80
+ this.workspace.el("fb_canvas_scroll")
81
+ ?.addEventListener("scroll", this._onScroll, { passive: true });
82
+ this._onResize = () => this.apply();
83
+ window.addEventListener("resize", this._onResize);
84
+ this.apply();
85
+ }
86
+
87
+ destroy() {
88
+ this.workspace.el("fb_canvas_scroll")
89
+ ?.removeEventListener("scroll", this._onScroll);
90
+ window.removeEventListener("resize", this._onResize);
91
+ }
92
+
93
+ // -- the menu ------------------------------------------------------------
94
+
95
+ menuEntries() {
96
+ const unit = FigureViewOptions.UNITS[this.prefs.units];
97
+ return [
98
+ { act: "rulers", label: "Rulers", checked: this.prefs.rulers },
99
+ { act: "grid", label: "Grid", checked: this.prefs.grid },
100
+ { act: "snapGrid", label: "Snap to grid", checked: this.prefs.snapGrid },
101
+ { act: "smartGuides", label: "Smart guides", checked: this.prefs.smartGuides },
102
+ { act: "margins", label: "Show margins", checked: this.prefs.margins },
103
+ { separator: true },
104
+ { act: "units", label: `Units: ${unit.label}` },
105
+ { separator: true },
106
+ { act: "background", label: "Page background…" },
107
+ ];
108
+ }
109
+
110
+ pick(act) {
111
+ if (act === "units") {
112
+ // Cycled rather than given a submenu: there are three, and a menu
113
+ // whose only job is to hold three items is a second click for no
114
+ // information.
115
+ const order = Object.keys(FigureViewOptions.UNITS);
116
+ this.prefs.units = order[(order.indexOf(this.prefs.units) + 1) % order.length];
117
+ } else if (act === "background") {
118
+ this.workspace.openPageBackground();
119
+ return;
120
+ } else if (act in this.prefs) {
121
+ this.prefs[act] = !this.prefs[act];
122
+ } else {
123
+ return;
124
+ }
125
+ this.write();
126
+ this.apply();
127
+ }
128
+
129
+ /**
130
+ * Set the unit outright.
131
+ *
132
+ * For a control that NAMES the three rather than cycling them -- the
133
+ * Transform popover has room for a select, and a field whose unit changes
134
+ * only by repeated clicking somewhere else is a field you cannot set.
135
+ * Same preference, same storage: there is one answer to "what is this page
136
+ * measured in", and the rulers and this popover both read it.
137
+ */
138
+ setUnit(name) {
139
+ if (!(name in FigureViewOptions.UNITS) || name === this.prefs.units) return;
140
+ this.prefs.units = name;
141
+ this.write();
142
+ this.apply();
143
+ }
144
+
145
+ // -- drawing ---------------------------------------------------------------
146
+
147
+ /** Push the preferences at everything that renders from them. */
148
+ apply() {
149
+ const main = this.workspace.el("fb_main");
150
+ main?.classList.toggle("has-rulers", this.prefs.rulers);
151
+
152
+ this.canvas.snapping = {
153
+ guides: this.prefs.smartGuides,
154
+ grid: this.prefs.snapGrid,
155
+ gridMm: this.prefs.gridMm,
156
+ };
157
+
158
+ this.drawGrid();
159
+ this.drawMargins();
160
+ this.drawRulers();
161
+ }
162
+
163
+ /**
164
+ * The grid, as a repeating gradient rather than as lines.
165
+ *
166
+ * One element and one paint, whatever the page size. Drawing it as elements
167
+ * would be four hundred divs on an A4 page at 5 mm, all of them rebuilt on
168
+ * every zoom.
169
+ */
170
+ drawGrid() {
171
+ const grid = this.workspace.el("fb_page_grid");
172
+ if (!grid) return;
173
+ grid.hidden = !this.prefs.grid;
174
+ if (!this.prefs.grid) return;
175
+ const step = this.canvas.toPx(this.prefs.gridMm);
176
+ grid.style.backgroundSize = `${step}px ${step}px`;
177
+ }
178
+
179
+ drawMargins() {
180
+ const element = this.workspace.el("fb_page_margins");
181
+ const page = this.canvas.page;
182
+ if (!element) return;
183
+ element.hidden = !this.prefs.margins || !page;
184
+ if (element.hidden) return;
185
+ const margins = page.margins_mm;
186
+ element.style.left = this.canvas.toPx(margins.left) + "px";
187
+ element.style.top = this.canvas.toPx(margins.top) + "px";
188
+ element.style.right = this.canvas.toPx(margins.right) + "px";
189
+ element.style.bottom = this.canvas.toPx(margins.bottom) + "px";
190
+ }
191
+
192
+ /**
193
+ * Both rulers, from where the page actually is on screen.
194
+ *
195
+ * Measured rather than computed from the scroll offset: the page is centred
196
+ * with `margin: auto` when it is narrower than the viewport, so its left
197
+ * edge is not a function of scrollLeft alone. Measuring is exact at every
198
+ * zoom and costs one layout read.
199
+ */
200
+ drawRulers() {
201
+ if (!this.prefs.rulers) return;
202
+ const scroll = this.workspace.el("fb_canvas_scroll");
203
+ const pageEl = this.workspace.el("fb_page");
204
+ const page = this.canvas.page;
205
+ if (!scroll || !pageEl || !page) return;
206
+
207
+ const view = scroll.getBoundingClientRect();
208
+ const paper = pageEl.getBoundingClientRect();
209
+ this.paintRuler(this.workspace.el("fb_ruler_h"), "h",
210
+ paper.left - view.left, view.width, page.size_mm.w);
211
+ this.paintRuler(this.workspace.el("fb_ruler_v"), "v",
212
+ paper.top - view.top, view.height, page.size_mm.h);
213
+ }
214
+
215
+ paintRuler(element, axis, originPx, lengthPx, spanMm) {
216
+ if (!element) return;
217
+ const dpr = window.devicePixelRatio || 1;
218
+ const thickness = FigureViewOptions.RULER_SIZE;
219
+ const width = axis === "h" ? lengthPx : thickness;
220
+ const height = axis === "h" ? thickness : lengthPx;
221
+ element.width = Math.max(1, Math.round(width * dpr));
222
+ element.height = Math.max(1, Math.round(height * dpr));
223
+ element.style.width = width + "px";
224
+ element.style.height = height + "px";
225
+
226
+ const context = element.getContext("2d");
227
+ if (!context) return;
228
+ context.setTransform(dpr, 0, 0, dpr, 0, 0);
229
+ context.clearRect(0, 0, width, height);
230
+ // Ink on a white strip. These were white when the workspace was dark;
231
+ // a canvas draws with literal colours and knows nothing about the
232
+ // stylesheet, so the theme change has to be made here by hand.
233
+ context.fillStyle = "rgba(22, 32, 46, 0.62)";
234
+ context.strokeStyle = "rgba(22, 32, 46, 0.38)";
235
+ context.font = "9px system-ui, sans-serif";
236
+ context.lineWidth = 1;
237
+
238
+ const unit = FigureViewOptions.UNITS[this.prefs.units];
239
+ const perUnitPx = this.canvas.toPx(unit.per);
240
+ // Skip labels that would overlap: at 25% zoom every 10 mm is four
241
+ // pixels apart, and a ruler of overlapping numbers is worse than none.
242
+ const labelEvery = Math.max(unit.step, unit.step * Math.ceil(28 / (perUnitPx * unit.step)));
243
+ const minorEvery = perUnitPx * unit.minor >= 4 ? unit.minor : labelEvery;
244
+
245
+ const spanUnits = spanMm / unit.per;
246
+ for (let value = 0; value <= spanUnits + 1e-6; value += minorEvery) {
247
+ const at = originPx + value * perUnitPx;
248
+ if (at < -20 || at > lengthPx + 20) continue;
249
+ const major = Math.abs(value % labelEvery) < 1e-6;
250
+ const tick = major ? thickness * 0.6 : thickness * 0.3;
251
+ context.beginPath();
252
+ if (axis === "h") {
253
+ context.moveTo(Math.round(at) + 0.5, thickness - tick);
254
+ context.lineTo(Math.round(at) + 0.5, thickness);
255
+ } else {
256
+ context.moveTo(thickness - tick, Math.round(at) + 0.5);
257
+ context.lineTo(thickness, Math.round(at) + 0.5);
258
+ }
259
+ context.stroke();
260
+ if (!major) continue;
261
+ const text = String(Math.round(value * 100) / 100);
262
+ if (axis === "h") {
263
+ context.fillText(text, at + 2, 9);
264
+ } else {
265
+ context.save();
266
+ context.translate(9, at - 2);
267
+ context.rotate(-Math.PI / 2);
268
+ context.fillText(text, 0, 0);
269
+ context.restore();
270
+ }
271
+ }
272
+ }
273
+ }