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.
- plexora/__init__.py +129 -0
- plexora/__main__.py +15 -0
- plexora/_url.py +105 -0
- plexora/api/__init__.py +55 -0
- plexora/api/dataset.py +398 -0
- plexora/api/http.py +19 -0
- plexora/api/plugin.py +541 -0
- plexora/api/store.py +221 -0
- plexora/cli.py +777 -0
- plexora/client/dist/354_bundle.js +2 -0
- plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
- plexora/client/dist/418_bundle.js +2 -0
- plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
- plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
- plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
- plexora/client/dist/770_bundle.js +1 -0
- plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
- plexora/client/dist/vendor_bundle.js +2 -0
- plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
- plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
- plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
- plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
- plexora/client/src/css/import.css +474 -0
- plexora/client/src/css/main.css +903 -0
- plexora/client/src/css/openProject.css +363 -0
- plexora/client/src/css/quickView.css +166 -0
- plexora/client/src/css/tokens.css +51 -0
- plexora/client/src/css/viewer.css +1655 -0
- plexora/client/src/img/apple-touch-icon.png +0 -0
- plexora/client/src/img/favicon.ico +0 -0
- plexora/client/src/img/logo.ai +8892 -33
- plexora/client/src/img/logo.svg +1 -0
- plexora/client/src/img/logo_with_text.ai +9011 -34
- plexora/client/src/img/logo_with_text.svg +1 -0
- plexora/client/src/js/main.js +704 -0
- plexora/client/src/js/pluginRegistry.js +108 -0
- plexora/client/src/js/services/appStatus.js +286 -0
- plexora/client/src/js/services/browsePicker.js +73 -0
- plexora/client/src/js/services/dataLayer.js +400 -0
- plexora/client/src/js/services/datasetContext.js +143 -0
- plexora/client/src/js/services/glRenderer.js +143 -0
- plexora/client/src/js/services/importFormValidation.js +257 -0
- plexora/client/src/js/services/numericData.js +100 -0
- plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
- plexora/client/src/js/services/simpleEventHandler.js +25 -0
- plexora/client/src/js/vendor.js +29 -0
- plexora/client/src/js/views/channelList.js +663 -0
- plexora/client/src/js/views/colorSwatchPicker.js +202 -0
- plexora/client/src/js/views/columnClassifier.js +146 -0
- plexora/client/src/js/views/coordinateField.js +133 -0
- plexora/client/src/js/views/dataSourceField.js +276 -0
- plexora/client/src/js/views/imageViewer.js +3306 -0
- plexora/client/src/js/views/miniMap.js +787 -0
- plexora/client/src/js/views/navbarControls.js +178 -0
- plexora/client/src/js/views/openProjectPage.js +239 -0
- plexora/client/src/js/views/projectEdit.js +335 -0
- plexora/client/src/js/views/quickViewLanding.js +136 -0
- plexora/client/src/js/views/rainbow.js +326 -0
- plexora/client/src/js/views/requirementsModal.js +572 -0
- plexora/client/src/js/views/rgbImageViewer.js +115 -0
- plexora/client/src/js/views/searchableSelect.js +411 -0
- plexora/client/src/js/views/segmentationProgress.js +122 -0
- plexora/client/src/js/views/toolLoader.js +747 -0
- plexora/client/src/js/views/viewerControls.js +689 -0
- plexora/client/src/js/views/viewerManager.js +434 -0
- plexora/client/src/js/views/viewerSidebar.js +1117 -0
- plexora/client/src/js/workers/tileDecoder.js +130 -0
- plexora/client/src/shaders/frag.glsl +528 -0
- plexora/client/src/shaders/vert.glsl +12 -0
- plexora/client/templates/base.html +212 -0
- plexora/client/templates/index.html +238 -0
- plexora/client/templates/open_project.html +96 -0
- plexora/client/templates/project_columns.html +86 -0
- plexora/client/templates/project_edit.html +196 -0
- plexora/client/templates/upload.html +118 -0
- plexora/connect.py +528 -0
- plexora/datasource.py +782 -0
- plexora/jupyter.py +406 -0
- plexora/notebook_env.py +232 -0
- plexora/paths.py +486 -0
- plexora/plugins/__init__.py +12 -0
- plexora/plugins/cell_explorer/__init__.py +100 -0
- plexora/plugins/cell_explorer/server/__init__.py +11 -0
- plexora/plugins/cell_explorer/server/routes.py +167 -0
- plexora/plugins/cell_explorer/server/state.py +281 -0
- plexora/plugins/cell_explorer/server/values.py +145 -0
- plexora/plugins/cell_explorer/server/variables.py +367 -0
- plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
- plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
- plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
- plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
- plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
- plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
- plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
- plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
- plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
- plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
- plexora/plugins/figure_builder/__init__.py +125 -0
- plexora/plugins/figure_builder/server/__init__.py +7 -0
- plexora/plugins/figure_builder/server/compose.py +365 -0
- plexora/plugins/figure_builder/server/export.py +784 -0
- plexora/plugins/figure_builder/server/export_jobs.py +148 -0
- plexora/plugins/figure_builder/server/operations.py +556 -0
- plexora/plugins/figure_builder/server/pixels.py +142 -0
- plexora/plugins/figure_builder/server/provenance.py +174 -0
- plexora/plugins/figure_builder/server/render.py +278 -0
- plexora/plugins/figure_builder/server/repository.py +701 -0
- plexora/plugins/figure_builder/server/routes.py +594 -0
- plexora/plugins/figure_builder/server/schema.py +1023 -0
- plexora/plugins/figure_builder/server/sources.py +120 -0
- plexora/plugins/figure_builder/server/textmetrics.py +155 -0
- plexora/plugins/figure_builder/static/figureActions.js +363 -0
- plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
- plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
- plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
- plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
- plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
- plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
- plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
- plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
- plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
- plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
- plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
- plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
- plexora/plugins/figure_builder/static/figureRichText.js +634 -0
- plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
- plexora/plugins/figure_builder/static/figureSchema.js +218 -0
- plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
- plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
- plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
- plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
- plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
- plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
- plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
- plexora/plugins/gating/__init__.py +76 -0
- plexora/plugins/gating/server/__init__.py +0 -0
- plexora/plugins/gating/server/anndata_gates.py +367 -0
- plexora/plugins/gating/server/database.py +16 -0
- plexora/plugins/gating/server/model.py +320 -0
- plexora/plugins/gating/server/routes.py +235 -0
- plexora/plugins/gating/static/csvGatingList.js +947 -0
- plexora/plugins/gating/static/gating.css +289 -0
- plexora/plugins/gating/static/gatingApi.js +221 -0
- plexora/plugins/gating/static/gatingSidebarController.js +513 -0
- plexora/plugins/gating/templates/gating/legacy.html +26 -0
- plexora/plugins/gating/templates/gating/panel.html +69 -0
- plexora/plugins/roi/__init__.py +95 -0
- plexora/plugins/roi/server/__init__.py +8 -0
- plexora/plugins/roi/server/adapters.py +655 -0
- plexora/plugins/roi/server/geojson.py +267 -0
- plexora/plugins/roi/server/geometry.py +181 -0
- plexora/plugins/roi/server/mapping.py +169 -0
- plexora/plugins/roi/server/operations.py +293 -0
- plexora/plugins/roi/server/repository.py +215 -0
- plexora/plugins/roi/server/routes.py +409 -0
- plexora/plugins/roi/server/schema.py +323 -0
- plexora/plugins/roi/static/roi.css +583 -0
- plexora/plugins/roi/static/roiApi.js +177 -0
- plexora/plugins/roi/static/roiFigureBridge.js +107 -0
- plexora/plugins/roi/static/roiGeometry.js +350 -0
- plexora/plugins/roi/static/roiRenderer.js +278 -0
- plexora/plugins/roi/static/roiSidebarController.js +1038 -0
- plexora/plugins/roi/static/roiState.js +518 -0
- plexora/plugins/roi/static/roiTools.js +993 -0
- plexora/plugins/roi/templates/roi/panel.html +240 -0
- plexora/proxy.py +57 -0
- plexora/server/models/adapters/__init__.py +94 -0
- plexora/server/models/adapters/anndata_adapter.py +388 -0
- plexora/server/models/adapters/base.py +93 -0
- plexora/server/models/adapters/classify.py +192 -0
- plexora/server/models/adapters/csv_adapter.py +105 -0
- plexora/server/models/adapters/inspection.py +265 -0
- plexora/server/models/adapters/spatialdata_adapter.py +248 -0
- plexora/server/models/centroid_tiles.py +305 -0
- plexora/server/models/data_model.py +1659 -0
- plexora/server/models/database_model.py +169 -0
- plexora/server/models/project.py +1270 -0
- plexora/server/plugins.py +199 -0
- plexora/server/routes/browse_routes.py +41 -0
- plexora/server/routes/data_routes.py +294 -0
- plexora/server/routes/import_routes.py +603 -0
- plexora/server/routes/page_routes.py +139 -0
- plexora/server/routes/project_routes.py +438 -0
- plexora/server/routes/quick_view_routes.py +62 -0
- plexora/server/routes/system_routes.py +39 -0
- plexora/server/routes/tool_routes.py +418 -0
- plexora/server/utils/addHEColumns.py +11 -0
- plexora/server/utils/fast_png.py +97 -0
- plexora/server/utils/fullConversion.py +50 -0
- plexora/server/utils/native_dialog.py +168 -0
- plexora/server/utils/pre_normalization.py +47 -0
- plexora/server/utils/segmentation_pyramid.py +598 -0
- plexora/server/utils/smallestenclosingcircle.py +126 -0
- plexora/server/utils/tiffsurgeon.py +372 -0
- plexora/server_cli.py +48 -0
- plexora/test.py +0 -0
- plexora-0.0.1.dist-info/METADATA +341 -0
- plexora-0.0.1.dist-info/RECORD +248 -0
- plexora-0.0.1.dist-info/WHEEL +5 -0
- plexora-0.0.1.dist-info/entry_points.txt +12 -0
- plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
- plexora-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Identifying a source image, and noticing when it has changed underneath.
|
|
2
|
+
|
|
3
|
+
A figure keeps a reference, not a copy. That is what makes it small and what
|
|
4
|
+
makes it fragile in one specific way: the project it points at can be re-imported
|
|
5
|
+
with a different slide, have its channels renamed, or be deleted. A panel drawn
|
|
6
|
+
on the old image renders perfectly plausibly on the new one, in the wrong place,
|
|
7
|
+
and nothing about the pixels says otherwise.
|
|
8
|
+
|
|
9
|
+
So every source carries a fingerprint taken at capture time, and it is compared
|
|
10
|
+
on open. The comparison never rerenders anything: it produces `ok`, `changed`
|
|
11
|
+
or `missing`, and a `changed` source keeps its cached preview until the user
|
|
12
|
+
says what should happen. Silently re-rendering a scientifically established
|
|
13
|
+
panel from materially different data is the one outcome this whole module
|
|
14
|
+
exists to prevent.
|
|
15
|
+
|
|
16
|
+
**Nothing here loads a datasource.** Everything is read from the project record
|
|
17
|
+
(config.json), which is why this can be asked about eight sources in a row
|
|
18
|
+
without evicting the one the user is actually looking at -- `data_model` holds a
|
|
19
|
+
single loaded datasource behind a lock, and touching it here would make opening
|
|
20
|
+
a figure library unload the viewer.
|
|
21
|
+
|
|
22
|
+
The one fact that is NOT in the record is the physical pixel size, which lives
|
|
23
|
+
in the OME metadata and only comes back through a loader. It is therefore
|
|
24
|
+
captured client-side, at the moment of capture, for the datasource already on
|
|
25
|
+
screen -- and stored on the source. See `figureSceneSnapshot.js`.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from plexora import api
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def channel_key(channel) -> str:
|
|
34
|
+
"""The stable id of one image channel.
|
|
35
|
+
|
|
36
|
+
The last segment of the tile URL (`/generated/data/<ds>/<key>/`), which is
|
|
37
|
+
generated at import from the file name and the channel's position. It
|
|
38
|
+
survives renaming, which `fullname` is precisely what does not: a user who
|
|
39
|
+
relabels "LSP20209_2" as "CD8a" has not changed which channel it is, and a
|
|
40
|
+
figure that identified it by name would lose it.
|
|
41
|
+
"""
|
|
42
|
+
src = (channel or {}).get("src") or ""
|
|
43
|
+
return src.strip("/").rsplit("/", 1)[-1]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def describe(datasource) -> dict:
|
|
47
|
+
"""What a figure records about a project image, ready to store as a source.
|
|
48
|
+
|
|
49
|
+
Raises KeyError if there is no such project.
|
|
50
|
+
"""
|
|
51
|
+
handle = api.dataset(datasource)
|
|
52
|
+
width, height = handle.image.size
|
|
53
|
+
channels = [
|
|
54
|
+
{"key": channel_key(channel),
|
|
55
|
+
"fullname_at_capture": channel.get("fullname") or channel.get("name") or ""}
|
|
56
|
+
for channel in handle.image.channels
|
|
57
|
+
if channel_key(channel)
|
|
58
|
+
]
|
|
59
|
+
return {
|
|
60
|
+
"kind": "plexora_project",
|
|
61
|
+
"datasource": datasource,
|
|
62
|
+
"display_name": datasource,
|
|
63
|
+
"image": {"width": int(width or 0), "height": int(height or 0)},
|
|
64
|
+
"channels": channels,
|
|
65
|
+
"fingerprint": fingerprint(datasource),
|
|
66
|
+
"status": "ok",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def fingerprint(datasource) -> dict:
|
|
71
|
+
"""The cheap identity check: dimensions, channel keys, mask present.
|
|
72
|
+
|
|
73
|
+
Deliberately not a hash of the pixels. Hashing a 40 GB pyramid to answer
|
|
74
|
+
"is this still the same slide" is a check nobody runs twice, and these four
|
|
75
|
+
facts catch every re-import that would put a panel in the wrong place.
|
|
76
|
+
"""
|
|
77
|
+
handle = api.dataset(datasource)
|
|
78
|
+
width, height = handle.image.size
|
|
79
|
+
return {
|
|
80
|
+
"image_width": int(width or 0),
|
|
81
|
+
"image_height": int(height or 0),
|
|
82
|
+
"channel_keys": [channel_key(c) for c in handle.image.channels if channel_key(c)],
|
|
83
|
+
"has_segmentation": bool(handle.segmentation.available),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def status_of(source) -> dict:
|
|
88
|
+
"""Compare a stored source against the project it names.
|
|
89
|
+
|
|
90
|
+
Returns `{status, reasons, fingerprint}`. `reasons` is a list of short codes
|
|
91
|
+
-- never prose -- because the client branches on them and wording is not an
|
|
92
|
+
API.
|
|
93
|
+
"""
|
|
94
|
+
if source.get("kind") != "plexora_project":
|
|
95
|
+
# An imported asset lives inside the figure's own directory; nothing
|
|
96
|
+
# outside can change it.
|
|
97
|
+
return {"status": "ok", "reasons": [], "fingerprint": source.get("fingerprint") or {}}
|
|
98
|
+
|
|
99
|
+
datasource = source.get("datasource") or ""
|
|
100
|
+
try:
|
|
101
|
+
current = fingerprint(datasource)
|
|
102
|
+
except KeyError:
|
|
103
|
+
return {"status": "missing", "reasons": ["no_such_project"], "fingerprint": {}}
|
|
104
|
+
|
|
105
|
+
stored = source.get("fingerprint") or {}
|
|
106
|
+
reasons = []
|
|
107
|
+
if stored.get("image_width") and (stored.get("image_width"), stored.get("image_height")) != (
|
|
108
|
+
current["image_width"], current["image_height"]):
|
|
109
|
+
reasons.append("dimensions_changed")
|
|
110
|
+
stored_keys = set(stored.get("channel_keys") or [])
|
|
111
|
+
if stored_keys and not stored_keys.issubset(set(current["channel_keys"])):
|
|
112
|
+
reasons.append("channels_removed")
|
|
113
|
+
if stored.get("has_segmentation") and not current["has_segmentation"]:
|
|
114
|
+
reasons.append("segmentation_removed")
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"status": "changed" if reasons else "ok",
|
|
118
|
+
"reasons": reasons,
|
|
119
|
+
"fingerprint": current,
|
|
120
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Typographic constants, in one table, for three renderers.
|
|
2
|
+
|
|
3
|
+
The canvas, the PDF writer and the raster writer all have to put a baseline in
|
|
4
|
+
the same place. They cannot share code -- one of them is JavaScript -- so they
|
|
5
|
+
share a TABLE instead, and `figureRichText.js` carries a copy that
|
|
6
|
+
`test_the_two_sides_agree_on_the_typographic_constants` holds equal to this one.
|
|
7
|
+
That is the same arrangement as `export._arrow_head` and
|
|
8
|
+
`FigureCanvas.arrowHeadPoints`: one rule, two languages, pinned by a test.
|
|
9
|
+
|
|
10
|
+
Only the twelve Adobe core fonts are offered, and that is a deliberate ceiling
|
|
11
|
+
rather than a first step. reportlab knows their metrics without a font file, so
|
|
12
|
+
nothing has to ship, nothing has to be licensed, and no export can fail because
|
|
13
|
+
a font did not load. The three families also have metric-compatible substitutes
|
|
14
|
+
on every desktop platform -- Arial for Helvetica, Liberation Serif for Times,
|
|
15
|
+
Courier New for Courier -- so a browser measuring a string gets the same answer
|
|
16
|
+
the PDF writer will, which is what makes "wrap as you type" safe to store.
|
|
17
|
+
|
|
18
|
+
Sizes here are in POINTS and positions in MILLIMETRES, because that is the split
|
|
19
|
+
the rest of the plugin already uses: type is specified in points and the page is
|
|
20
|
+
measured in millimetres.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
#: Millimetres in one typographic point. Computed rather than written out:
|
|
26
|
+
#: `FigureCanvas.PT_PER_MM` is the rounded literal 2.8346 and the two must not
|
|
27
|
+
#: be confused. Rounding here would put a floor under how exactly the canvas and
|
|
28
|
+
#: the exporter can be asserted to agree.
|
|
29
|
+
MM_PER_PT = 25.4 / 72.0
|
|
30
|
+
|
|
31
|
+
#: Line box height as a multiple of the type size. 1.2 is the usual default and
|
|
32
|
+
#: replaces the 1.25 that `.fb-annotation-text` used to set in CSS -- the canvas
|
|
33
|
+
#: no longer leaves leading to the browser, so the number has to live somewhere
|
|
34
|
+
#: both languages can read it.
|
|
35
|
+
LINE_HEIGHT = 1.2
|
|
36
|
+
|
|
37
|
+
#: The type size a text box starts at, in points.
|
|
38
|
+
#:
|
|
39
|
+
#: NOT `settings.style.font_size_pt`, which is 8 and has to stay 8: that one is
|
|
40
|
+
#: the size of the furniture drawn ON a panel -- legend rows and the scale bar's
|
|
41
|
+
#: caption, both of which sit inside the image and have to be small enough to
|
|
42
|
+
#: stay out of it (see compose._panel_furniture). A caption BESIDE a figure is
|
|
43
|
+
#: read at a normal reading size, and it was inheriting the panel-label size
|
|
44
|
+
#: instead -- `FigureCanvas.drawStyle` asked for `label_size_pt`, which is the
|
|
45
|
+
#: size of the letter "A" in the corner of a panel and has nothing to do with a
|
|
46
|
+
#: sentence.
|
|
47
|
+
#:
|
|
48
|
+
#: Overridable on every box, so this is only where a new one starts.
|
|
49
|
+
DEFAULT_TEXT_SIZE_PT = 14.0
|
|
50
|
+
|
|
51
|
+
#: Ascent and descent as a fraction of the em, from the Adobe core AFM files.
|
|
52
|
+
#: Used to centre a line inside its line box, which is what makes vertical
|
|
53
|
+
#: alignment and multi-size lines land in the same place on all three renderers.
|
|
54
|
+
ASCENT = {
|
|
55
|
+
"Helvetica": 0.718,
|
|
56
|
+
"Times-Roman": 0.683,
|
|
57
|
+
"Courier": 0.629,
|
|
58
|
+
}
|
|
59
|
+
DESCENT = {
|
|
60
|
+
"Helvetica": 0.207,
|
|
61
|
+
"Times-Roman": 0.217,
|
|
62
|
+
"Courier": 0.157,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#: Underline and strike-through, as a fraction of the em from the baseline.
|
|
66
|
+
#: Drawn explicitly by every renderer rather than delegated to `text-decoration`
|
|
67
|
+
#: or whatever the PDF viewer would do: SVG takes its underline position from
|
|
68
|
+
#: the font file actually in use, so a browser showing Arial and a PDF holding
|
|
69
|
+
#: Helvetica would put the rule in two different places.
|
|
70
|
+
UNDERLINE_OFFSET_EM = 0.12
|
|
71
|
+
UNDERLINE_THICKNESS_EM = 0.06
|
|
72
|
+
STRIKE_OFFSET_EM = 0.26
|
|
73
|
+
|
|
74
|
+
#: The families a document may name. These are PostScript base names rather than
|
|
75
|
+
#: abstract keys like "sans", because `settings.style.font_family` already holds
|
|
76
|
+
#: "Helvetica" and hands it straight to reportlab's `setFont`. Keeping that
|
|
77
|
+
#: vocabulary means a figure written by this build still renders in the right
|
|
78
|
+
#: typeface in a build that predates this module.
|
|
79
|
+
FAMILIES = ("Helvetica", "Times-Roman", "Courier")
|
|
80
|
+
DEFAULT_FAMILY = "Helvetica"
|
|
81
|
+
|
|
82
|
+
_POSTSCRIPT = {
|
|
83
|
+
("Helvetica", False, False): "Helvetica",
|
|
84
|
+
("Helvetica", True, False): "Helvetica-Bold",
|
|
85
|
+
("Helvetica", False, True): "Helvetica-Oblique",
|
|
86
|
+
("Helvetica", True, True): "Helvetica-BoldOblique",
|
|
87
|
+
("Times-Roman", False, False): "Times-Roman",
|
|
88
|
+
("Times-Roman", True, False): "Times-Bold",
|
|
89
|
+
("Times-Roman", False, True): "Times-Italic",
|
|
90
|
+
("Times-Roman", True, True): "Times-BoldItalic",
|
|
91
|
+
("Courier", False, False): "Courier",
|
|
92
|
+
("Courier", True, False): "Courier-Bold",
|
|
93
|
+
("Courier", False, True): "Courier-Oblique",
|
|
94
|
+
("Courier", True, True): "Courier-BoldOblique",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#: What the browser should ask for, per family. Every stack names substitutes
|
|
98
|
+
#: that are metric-compatible with the core font, in the order they are likely
|
|
99
|
+
#: to exist: Arial (Windows/macOS) and Liberation Sans (Linux) were both drawn
|
|
100
|
+
#: to Helvetica's widths, so a line that fits on screen fits in the PDF.
|
|
101
|
+
#:
|
|
102
|
+
#: This lives here and not in the stylesheet so that the family list is one
|
|
103
|
+
#: table. `figureRichText.js` carries the same three strings.
|
|
104
|
+
CSS_STACK = {
|
|
105
|
+
"Helvetica": 'Helvetica, Arial, "Liberation Sans", sans-serif',
|
|
106
|
+
"Times-Roman": '"Times New Roman", Times, "Liberation Serif", serif',
|
|
107
|
+
"Courier": '"Courier New", Courier, "Liberation Mono", monospace',
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def family(value):
|
|
112
|
+
"""A family this build can draw, or the default.
|
|
113
|
+
|
|
114
|
+
Never raises. An unknown family is a style problem, and the document it
|
|
115
|
+
arrived in still has words in it worth showing.
|
|
116
|
+
"""
|
|
117
|
+
return value if value in FAMILIES else DEFAULT_FAMILY
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def postscript_name(name, bold=False, italic=False):
|
|
121
|
+
"""The PostScript font for one family and weight.
|
|
122
|
+
|
|
123
|
+
Raises `KeyError` on a family this module does not know, deliberately: by
|
|
124
|
+
the time a run reaches here it has been through `schema.normalize_annotation`
|
|
125
|
+
and its family is whitelisted, so a miss is a typo in `_POSTSCRIPT` or a
|
|
126
|
+
family added to the UI and not to this table. The export path catches it,
|
|
127
|
+
substitutes the default and says so on the provenance page rather than
|
|
128
|
+
silently shipping the wrong typeface.
|
|
129
|
+
"""
|
|
130
|
+
return _POSTSCRIPT[(name, bool(bold), bool(italic))]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def em_mm(size_pt):
|
|
134
|
+
"""One em of this type size, in millimetres."""
|
|
135
|
+
return size_pt * MM_PER_PT
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def line_metrics(runs, line_height=LINE_HEIGHT):
|
|
139
|
+
"""Height and baseline offset of the line box holding these runs.
|
|
140
|
+
|
|
141
|
+
The tallest run sets the line box, and the line is centred inside it -- half
|
|
142
|
+
the leading above, half below -- so that a line mixing an 8 pt caption with
|
|
143
|
+
a 6 pt superscript sits where a reader expects rather than riding on the
|
|
144
|
+
bottom of its box.
|
|
145
|
+
|
|
146
|
+
Returns `(lead_mm, ascent_mm, descent_mm)`, where the baseline of the line
|
|
147
|
+
is `lead_mm - ascent_mm - descent_mm` halved, plus `ascent_mm`, below the top
|
|
148
|
+
of the box. `text_layout` does that sum; this function only measures.
|
|
149
|
+
"""
|
|
150
|
+
if not runs:
|
|
151
|
+
return (0.0, 0.0, 0.0)
|
|
152
|
+
tallest = max(runs, key=lambda run: run["size_pt"])
|
|
153
|
+
em = em_mm(tallest["size_pt"])
|
|
154
|
+
name = family(tallest.get("family"))
|
|
155
|
+
return (em * line_height, em * ASCENT[name], em * DESCENT[name])
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What can be done to a selection, declared once.
|
|
3
|
+
*
|
|
4
|
+
* The bar that floats over the selection and the right-click menu are two
|
|
5
|
+
* surfaces onto ONE list. They were two hand-maintained lists, and they had
|
|
6
|
+
* already drifted: the menu marked "Bring to front" disabled when nothing in
|
|
7
|
+
* the selection could be reordered, while the bar offered it live on a text box
|
|
8
|
+
* where `reorderZ` filtered annotations out and did nothing at all. Two lists
|
|
9
|
+
* meant two answers to "can this be done", and only one of them was right.
|
|
10
|
+
*
|
|
11
|
+
* The rule the reference designs encode, and the one used here:
|
|
12
|
+
*
|
|
13
|
+
* * an action that only means something for one kind appears only for that
|
|
14
|
+
* kind -- "Split into channels" on a caption is not disabled, it is noise;
|
|
15
|
+
* * an action that applies to ANY object is always REACHABLE, but a generic
|
|
16
|
+
* one that cannot run right now is reachable through "More" rather than as
|
|
17
|
+
* a dead icon on the bar.
|
|
18
|
+
*
|
|
19
|
+
* That second half used to read "always present, and greyed when it cannot
|
|
20
|
+
* run", and on a panel it was fine. On a text box it was not: Align, Distribute,
|
|
21
|
+
* Match size, Layout and Group all need two objects, so selecting one caption
|
|
22
|
+
* gave a bar of five grey icons and one live one, with everything actually
|
|
23
|
+
* worth pressing hidden behind the overflow. A greyed icon with no label is not
|
|
24
|
+
* an affordance; a greyed ROW that says "Align" is, because it names itself and
|
|
25
|
+
* the menu has room to. So the action stays in the vocabulary either way and
|
|
26
|
+
* only its surface moves -- which is still one declaration per action, with
|
|
27
|
+
* nothing branching on the type of the selection.
|
|
28
|
+
*
|
|
29
|
+
* `applies` decides whether an action exists for this selection at all, and
|
|
30
|
+
* `enabled` decides both whether it is clickable and, for a generic action,
|
|
31
|
+
* which of the two surfaces it lands on.
|
|
32
|
+
*
|
|
33
|
+
* Text FORMATTING is deliberately not here. Font, size, colour and the rest
|
|
34
|
+
* live in the text sidebar, because they are properties of one kind of object
|
|
35
|
+
* rather than actions on any object -- and putting them here is what would turn
|
|
36
|
+
* this back into a per-type list.
|
|
37
|
+
*/
|
|
38
|
+
class FigureSelection {
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Everything the predicates below need, computed once per selection change.
|
|
42
|
+
*
|
|
43
|
+
* Pure: it reads the document and the canvas but changes nothing, so a
|
|
44
|
+
* probe can build one from a plain object.
|
|
45
|
+
*/
|
|
46
|
+
static describe(ids, state, canvas) {
|
|
47
|
+
const annotations = ids
|
|
48
|
+
.map((id) => state.document.annotations[id]).filter(Boolean);
|
|
49
|
+
const panels = ids.map((id) => state.panel(id)).filter(Boolean);
|
|
50
|
+
const single = ids.length === 1
|
|
51
|
+
? (panels[0] || annotations[0] || null) : null;
|
|
52
|
+
const kinds = new Set(panels.map(() => "panel")
|
|
53
|
+
.concat(annotations.map((annotation) => annotation.type)));
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
ids: ids,
|
|
57
|
+
count: ids.length,
|
|
58
|
+
panels: panels,
|
|
59
|
+
annotations: annotations,
|
|
60
|
+
single: single,
|
|
61
|
+
kinds: kinds,
|
|
62
|
+
singlePanel: ids.length === 1 && panels.length === 1 ? panels[0] : null,
|
|
63
|
+
singleAnnotation:
|
|
64
|
+
ids.length === 1 && annotations.length === 1 ? annotations[0] : null,
|
|
65
|
+
isText: annotations.length === ids.length && ids.length > 0
|
|
66
|
+
&& annotations.every((a) => a.type === "text"),
|
|
67
|
+
// A box, as opposed to a line or an arrow, whose geometry is a
|
|
68
|
+
// vector and which therefore has no corners to rotate about.
|
|
69
|
+
allBoxes: ids.length > 0 && annotations.length === ids.length
|
|
70
|
+
&& annotations.every((a) => !["line", "arrow"].includes(a.type)),
|
|
71
|
+
grouped: ids.some((id) => canvas.groupFor(id)),
|
|
72
|
+
placed: panels.filter((panel) => panel.placement),
|
|
73
|
+
// How many of the selected objects have a RECTANGLE that Align,
|
|
74
|
+
// Distribute, Match size and Layout can act on, which is not the
|
|
75
|
+
// same as how many objects there are. `count > 1` was the test, and
|
|
76
|
+
// it was wrong twice over: it counted a panel still in the tray,
|
|
77
|
+
// which has no placement to move, and it counted a line, whose
|
|
78
|
+
// `w_mm`/`h_mm` are the two components of a vector rather than a
|
|
79
|
+
// size -- "same width" on one would flip its direction. Two
|
|
80
|
+
// captions now enable these; two arrows do not, and say so by being
|
|
81
|
+
// greyed in "More" rather than by doing nothing when pressed.
|
|
82
|
+
arrangeable: panels.filter((panel) => panel.placement).length
|
|
83
|
+
+ annotations.filter(
|
|
84
|
+
(a) => !["line", "arrow"].includes(a.type)).length,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class FigureActions {
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The vocabulary. `surface` says where an action can appear:
|
|
94
|
+
*
|
|
95
|
+
* bar a button on the floating bar
|
|
96
|
+
* overflow a row in the bar's "More" menu
|
|
97
|
+
* menu a row in the right-click menu
|
|
98
|
+
*
|
|
99
|
+
* An action may name several. `popover: true` means the bar button opens a
|
|
100
|
+
* popover whose body `FigureContextBar.popoverMarkup` builds -- the detail
|
|
101
|
+
* of a scale bar or a legend is panel-specific and belongs there; what
|
|
102
|
+
* belongs HERE is only whether the button exists at all.
|
|
103
|
+
*
|
|
104
|
+
* `label` is the full sentence a menu row can afford. `short` is what the
|
|
105
|
+
* bar prints under the icon, and it is a separate field rather than a
|
|
106
|
+
* truncation because "Title, label and numbering" cut to fit says nothing
|
|
107
|
+
* and "Titles" says the whole thing. An action with no `short` prints its
|
|
108
|
+
* `label`, which is right for the ones that are already one word.
|
|
109
|
+
*
|
|
110
|
+
* `shortcut` is a FIELD and not two spaces and a glyph on the end of the
|
|
111
|
+
* label, which is how it was written first. A menu row sets the label and
|
|
112
|
+
* the key in two columns -- the labels ranged left, the keys ranged right
|
|
113
|
+
* and greyed -- and that is only possible if the row is given them
|
|
114
|
+
* separately. Concatenated, the keys landed wherever each label happened to
|
|
115
|
+
* end, which is a ragged column of the least important text on screen; and
|
|
116
|
+
* the bar's tooltip read "Group ⌘G" with the double space in it.
|
|
117
|
+
*/
|
|
118
|
+
static get ALL() {
|
|
119
|
+
const always = () => true;
|
|
120
|
+
return [
|
|
121
|
+
// -- type-specific, present only for what they mean something to --
|
|
122
|
+
{ id: "edit_text", icon: "i-cursor", label: "Edit the text",
|
|
123
|
+
short: "Edit", surface: ["bar", "menu"],
|
|
124
|
+
applies: (sel) => Boolean(sel.singleAnnotation)
|
|
125
|
+
&& sel.singleAnnotation.type === "text",
|
|
126
|
+
enabled: always,
|
|
127
|
+
run: (ctx) => ctx.handlers.onEditText?.(ctx.ids[0]) },
|
|
128
|
+
|
|
129
|
+
// A character palette rather than a search box: the ones a figure
|
|
130
|
+
// caption actually needs are two dozen, and a grid of two dozen is
|
|
131
|
+
// faster to read than any field is to type into. It is on the BAR
|
|
132
|
+
// and not only in the sidebar because the caret it inserts at is on
|
|
133
|
+
// the canvas, and walking away to a panel to reach it is what loses
|
|
134
|
+
// the caret.
|
|
135
|
+
{ id: "symbol", icon: "icons", label: "Insert a symbol",
|
|
136
|
+
short: "Symbol", surface: ["bar"], popover: true,
|
|
137
|
+
applies: (sel) => Boolean(sel.singleAnnotation)
|
|
138
|
+
&& sel.singleAnnotation.type === "text",
|
|
139
|
+
enabled: always },
|
|
140
|
+
|
|
141
|
+
{ id: "quick_edit", icon: "sliders", label: "Quick Edit…",
|
|
142
|
+
short: "Quick Edit", surface: ["bar", "menu"],
|
|
143
|
+
applies: (sel) => Boolean(sel.singlePanel),
|
|
144
|
+
enabled: (sel, ctx) => FigureActions.reopenable(sel.singlePanel, ctx),
|
|
145
|
+
run: (ctx) => ctx.handlers.onQuickEdit?.(ctx.ids[0]) },
|
|
146
|
+
|
|
147
|
+
{ id: "edit", icon: "arrow-up-right-from-square",
|
|
148
|
+
label: "Open in Main Viewer", short: "Viewer",
|
|
149
|
+
surface: ["bar", "menu"],
|
|
150
|
+
applies: (sel) => Boolean(sel.singlePanel),
|
|
151
|
+
enabled: (sel, ctx) => FigureActions.reopenable(sel.singlePanel, ctx),
|
|
152
|
+
run: (ctx) => ctx.handlers.onEditPanel?.(ctx.ids[0]) },
|
|
153
|
+
|
|
154
|
+
{ id: "titles", icon: "font", label: "Title, label and numbering",
|
|
155
|
+
short: "Titles", surface: ["bar"], popover: true,
|
|
156
|
+
applies: (sel) => Boolean(sel.singlePanel), enabled: always },
|
|
157
|
+
|
|
158
|
+
{ id: "split", icon: "table-columns", label: "Split into channels",
|
|
159
|
+
short: "Split", surface: ["bar"], popover: true,
|
|
160
|
+
applies: (sel) => Boolean(sel.singlePanel)
|
|
161
|
+
&& (sel.singlePanel.scene.channels || []).length > 1,
|
|
162
|
+
enabled: always },
|
|
163
|
+
|
|
164
|
+
{ id: "scalebar", icon: "ruler-horizontal", label: "Scale bar",
|
|
165
|
+
surface: ["bar"], popover: true,
|
|
166
|
+
applies: (sel) => sel.panels.length > 0, enabled: always,
|
|
167
|
+
pressed: (sel) => sel.panels.some((panel) => panel.scalebar.visible) },
|
|
168
|
+
|
|
169
|
+
{ id: "legend", icon: "list", label: "Legend",
|
|
170
|
+
surface: ["bar"], popover: true,
|
|
171
|
+
applies: (sel) => sel.panels.length > 0, enabled: always,
|
|
172
|
+
pressed: (sel) => sel.panels.some(
|
|
173
|
+
(panel) => panel.legend.channels || panel.legend.plugins) },
|
|
174
|
+
|
|
175
|
+
{ id: "stroke", icon: "pen", label: "Line and fill", short: "Line",
|
|
176
|
+
surface: ["bar"], popover: true,
|
|
177
|
+
applies: (sel) => Boolean(sel.singleAnnotation)
|
|
178
|
+
&& sel.singleAnnotation.type !== "text",
|
|
179
|
+
enabled: always },
|
|
180
|
+
|
|
181
|
+
{ id: "colour", icon: "palette", label: "Color",
|
|
182
|
+
surface: ["bar"], popover: true,
|
|
183
|
+
// Not for text: a caption's colour belongs with its font and its
|
|
184
|
+
// size, in the sidebar, and offering it in two places is how the
|
|
185
|
+
// two end up disagreeing about what "the colour" means when only
|
|
186
|
+
// half the sentence is red.
|
|
187
|
+
applies: (sel) => Boolean(sel.singleAnnotation)
|
|
188
|
+
&& sel.singleAnnotation.type !== "text",
|
|
189
|
+
enabled: always },
|
|
190
|
+
|
|
191
|
+
// -- generic: on the bar when they can run, in "More" when not ----
|
|
192
|
+
{ id: "align", icon: "align-center", label: "Align",
|
|
193
|
+
surface: ["bar"], popover: true, generic: true,
|
|
194
|
+
applies: always, enabled: (sel) => sel.arrangeable > 1 },
|
|
195
|
+
|
|
196
|
+
{ id: "distribute", icon: "arrows-left-right", label: "Distribute",
|
|
197
|
+
surface: ["bar"], popover: true, generic: true,
|
|
198
|
+
applies: always, enabled: (sel) => sel.arrangeable > 2 },
|
|
199
|
+
|
|
200
|
+
// `expand`, not `vector-square`. The second is a Font Awesome 5/6
|
|
201
|
+
// name that FA 7 no longer ships, and an icon name the set does not
|
|
202
|
+
// have renders as nothing at all -- so this was a button captioned
|
|
203
|
+
// "Match" with a blank square above it. tests/test_tool_assets.py
|
|
204
|
+
// now checks every name in this tree against the set.
|
|
205
|
+
{ id: "resize", icon: "expand", label: "Match size",
|
|
206
|
+
short: "Match", surface: ["bar"], popover: true, generic: true,
|
|
207
|
+
applies: always, enabled: (sel) => sel.arrangeable > 1 },
|
|
208
|
+
|
|
209
|
+
// "Layout", not "Arrange". Arrange is z-order in every design tool
|
|
210
|
+
// and in the reference this bar follows; the row/column/grid one
|
|
211
|
+
// needed the other name. `data-arrange` stays as it is -- it is the
|
|
212
|
+
// contract FigureCanvas answers to, and this is a label change.
|
|
213
|
+
{ id: "layout", icon: "table-cells", label: "Layout",
|
|
214
|
+
surface: ["bar"], popover: true, generic: true,
|
|
215
|
+
applies: always, enabled: (sel) => sel.arrangeable > 1 },
|
|
216
|
+
|
|
217
|
+
// Z-order as one popover rather than four overflow rows. "Arrange"
|
|
218
|
+
// means z-order in every design tool, which is why the row/column
|
|
219
|
+
// /grid action above had to take the name "Layout" -- and it is the
|
|
220
|
+
// one generic action that is live on a selection of ONE, which is
|
|
221
|
+
// what makes it worth a button rather than a menu row.
|
|
222
|
+
{ id: "arrange", icon: "layer-group", label: "Arrange",
|
|
223
|
+
surface: ["bar"], popover: true, generic: true,
|
|
224
|
+
applies: always,
|
|
225
|
+
enabled: (sel) => sel.placed.length > 0 || sel.annotations.length > 0 },
|
|
226
|
+
|
|
227
|
+
// Position and size as numbers, for the times the pointer cannot
|
|
228
|
+
// get there: a 0.5 mm nudge at 40% zoom, or two captions that have
|
|
229
|
+
// to be exactly the same width as each other.
|
|
230
|
+
{ id: "transform", icon: "up-down-left-right", label: "Transform",
|
|
231
|
+
surface: ["bar"], popover: true, generic: true,
|
|
232
|
+
applies: always,
|
|
233
|
+
// One object, and one that HAS a width and a height. A field
|
|
234
|
+
// showing a single number for a selection of three is either
|
|
235
|
+
// lying or blank, and a line's `w_mm`/`h_mm` are the two
|
|
236
|
+
// components of a vector rather than a size -- a box to type a
|
|
237
|
+
// width into would be a box that means something else.
|
|
238
|
+
enabled: (sel) => sel.count === 1
|
|
239
|
+
&& (Boolean(sel.singlePanel) || sel.allBoxes) },
|
|
240
|
+
|
|
241
|
+
{ id: "group", icon: "object-group", label: "Group", shortcut: "⌘G",
|
|
242
|
+
short: "Group", surface: ["bar", "menu"], generic: true,
|
|
243
|
+
applies: (sel) => !sel.grouped, enabled: (sel) => sel.count > 1,
|
|
244
|
+
run: (ctx) => ctx.canvas.groupSelection() },
|
|
245
|
+
|
|
246
|
+
{ id: "ungroup", icon: "object-ungroup", label: "Ungroup", shortcut: "⇧⌘G",
|
|
247
|
+
short: "Ungroup", surface: ["bar", "menu"], generic: true,
|
|
248
|
+
applies: (sel) => sel.grouped, enabled: always,
|
|
249
|
+
run: (ctx) => ctx.canvas.ungroupSelection() },
|
|
250
|
+
|
|
251
|
+
// Duplicate and Delete earn a button for the same reason Arrange
|
|
252
|
+
// does: they are the two things anyone does to a single object of
|
|
253
|
+
// any kind, and they were the reason the overflow had to be opened
|
|
254
|
+
// at all on a text selection.
|
|
255
|
+
{ id: "duplicate", icon: "clone", label: "Duplicate", shortcut: "⌘D",
|
|
256
|
+
short: "Duplicate", surface: ["bar", "menu"],
|
|
257
|
+
generic: true, applies: always, enabled: (sel) => sel.count > 0,
|
|
258
|
+
run: (ctx) => ctx.canvas.duplicateSelection() },
|
|
259
|
+
|
|
260
|
+
// -- generic, in the overflow and the right-click menu ------------
|
|
261
|
+
{ id: "copy", icon: "copy", label: "Copy", shortcut: "⌘C",
|
|
262
|
+
surface: ["overflow", "menu"],
|
|
263
|
+
generic: true, applies: always, enabled: (sel) => sel.count > 0,
|
|
264
|
+
run: (ctx) => ctx.canvas.copySelection() },
|
|
265
|
+
|
|
266
|
+
// The four z-order commands, reached on the bar through the Arrange
|
|
267
|
+
// popover -- which is why none of them is on it directly. One
|
|
268
|
+
// command in two places on the same toolbar is how the bar and the
|
|
269
|
+
// right-click menu ended up disagreeing before.
|
|
270
|
+
//
|
|
271
|
+
// Four rather than two: "to the front" and "one place forward" are
|
|
272
|
+
// different intents, and getting a panel BETWEEN two others is only
|
|
273
|
+
// possible with the relative pair.
|
|
274
|
+
{ id: "front", icon: "angles-up", label: "Bring to front", shortcut: "⌘⇧]",
|
|
275
|
+
surface: ["menu"], generic: true, applies: always,
|
|
276
|
+
// Enabled for annotations too, now that `reorderZ` reorders them.
|
|
277
|
+
// This was the drift: the menu said no and the bar said yes, and
|
|
278
|
+
// the bar was wrong.
|
|
279
|
+
enabled: (sel) => sel.placed.length > 0 || sel.annotations.length > 0,
|
|
280
|
+
run: (ctx) => ctx.canvas.reorderZ("front") },
|
|
281
|
+
|
|
282
|
+
{ id: "forward", icon: "angle-up", label: "Bring forward", shortcut: "⌘]",
|
|
283
|
+
surface: ["menu"], generic: true, applies: always,
|
|
284
|
+
enabled: (sel) => sel.placed.length > 0 || sel.annotations.length > 0,
|
|
285
|
+
run: (ctx) => ctx.canvas.reorderZ("forward") },
|
|
286
|
+
|
|
287
|
+
{ id: "backward", icon: "angle-down", label: "Send backward", shortcut: "⌘[",
|
|
288
|
+
surface: ["menu"], generic: true, applies: always,
|
|
289
|
+
enabled: (sel) => sel.placed.length > 0 || sel.annotations.length > 0,
|
|
290
|
+
run: (ctx) => ctx.canvas.reorderZ("backward") },
|
|
291
|
+
|
|
292
|
+
{ id: "back", icon: "angles-down", label: "Send to back", shortcut: "⌘⇧[",
|
|
293
|
+
surface: ["menu"], generic: true, applies: always,
|
|
294
|
+
enabled: (sel) => sel.placed.length > 0 || sel.annotations.length > 0,
|
|
295
|
+
run: (ctx) => ctx.canvas.reorderZ("back") },
|
|
296
|
+
|
|
297
|
+
// The tray, because that is where it goes: off the page and back
|
|
298
|
+
// into the figure's holding area, which is the whole difference
|
|
299
|
+
// between this row and Delete two rows down.
|
|
300
|
+
{ id: "remove_page", icon: "inbox", label: "Remove from page",
|
|
301
|
+
surface: ["overflow", "menu"], generic: true,
|
|
302
|
+
applies: (sel) => sel.panels.length > 0, enabled: always,
|
|
303
|
+
run: (ctx) => ctx.handlers.onRemoveFromPage?.() },
|
|
304
|
+
|
|
305
|
+
{ id: "delete", icon: "trash", label: "Delete from figure",
|
|
306
|
+
short: "Delete", shortcut: "⌫", danger: true,
|
|
307
|
+
surface: ["bar", "menu"],
|
|
308
|
+
generic: true, applies: always, enabled: (sel) => sel.count > 0,
|
|
309
|
+
run: (ctx) => ctx.handlers.onDeleteFromFigure?.(ctx.ids) },
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** The four z-order commands, in the order they belong in a menu: nearest
|
|
314
|
+
* the viewer at the top. Named here rather than in the bar, so the popover
|
|
315
|
+
* and the right-click menu cannot end up listing different ones. */
|
|
316
|
+
static get ARRANGE() {
|
|
317
|
+
return ["front", "forward", "backward", "back"];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** A panel can be reopened only if its source is a project that is still
|
|
321
|
+
* there -- an imported PNG has no viewer to go back to. */
|
|
322
|
+
static reopenable(panel, ctx) {
|
|
323
|
+
if (!panel) return false;
|
|
324
|
+
const source = ctx.state.source(panel.source_id);
|
|
325
|
+
return Boolean(source && source.kind === "plexora_project" && source.datasource);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The actions one surface should show for this selection, in order.
|
|
330
|
+
*
|
|
331
|
+
* The bar and the overflow are one list split in two by whether a generic
|
|
332
|
+
* action can currently run, so an action never falls out of reach: what the
|
|
333
|
+
* bar drops, "More" picks up, named and greyed. Type-specific actions are
|
|
334
|
+
* not moved -- they are on the bar because they are the point of having
|
|
335
|
+
* selected that kind of object, and `applies` has already decided they
|
|
336
|
+
* belong. The right-click menu takes everything its entries declare,
|
|
337
|
+
* enabled or not, because there every row carries its own label.
|
|
338
|
+
*/
|
|
339
|
+
static forSurface(surface, sel, ctx) {
|
|
340
|
+
const available = FigureActions.ALL
|
|
341
|
+
.filter((action) => action.applies(sel, ctx))
|
|
342
|
+
.map((action) => ({
|
|
343
|
+
...action,
|
|
344
|
+
isEnabled: Boolean(action.enabled(sel, ctx)),
|
|
345
|
+
isPressed: Boolean(action.pressed && action.pressed(sel, ctx)),
|
|
346
|
+
}));
|
|
347
|
+
|
|
348
|
+
if (surface === "bar") {
|
|
349
|
+
return available.filter((action) => action.surface.includes("bar")
|
|
350
|
+
&& (action.isEnabled || !action.generic));
|
|
351
|
+
}
|
|
352
|
+
if (surface === "overflow") {
|
|
353
|
+
return available.filter((action) => action.surface.includes("overflow")
|
|
354
|
+
|| (action.surface.includes("bar") && action.generic
|
|
355
|
+
&& !action.isEnabled));
|
|
356
|
+
}
|
|
357
|
+
return available.filter((action) => action.surface.includes(surface));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
static byId(id) {
|
|
361
|
+
return FigureActions.ALL.find((action) => action.id === id) || null;
|
|
362
|
+
}
|
|
363
|
+
}
|