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,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FigureContextMenu - the right-click, on the page and in the tray.
|
|
3
|
+
*
|
|
4
|
+
* The third level of the interface. Level 1 is the tool rail and the topbar
|
|
5
|
+
* (always visible), level 2 is the floating bar (visible while something is
|
|
6
|
+
* selected), and this is everything else: the actions that are worth having and
|
|
7
|
+
* are not worth a permanent button, which is most of them.
|
|
8
|
+
*
|
|
9
|
+
* Having it is what lets the rest of the interface stay small. Bring to front,
|
|
10
|
+
* Replace image, Copy, Delete from figure and the rest are all one click away
|
|
11
|
+
* from the thing they act on, and none of them costs a pixel of chrome.
|
|
12
|
+
*
|
|
13
|
+
* ## Two deletes, and they are not the same delete
|
|
14
|
+
*
|
|
15
|
+
* "Remove from page" unplaces a panel: it goes back to the tray with its
|
|
16
|
+
* captured scene intact. "Delete from figure" destroys it. The distinction is
|
|
17
|
+
* load-bearing -- a captured scene can be the only record of a view somebody
|
|
18
|
+
* spent an hour finding -- so the destructive one is further away, worded as
|
|
19
|
+
* what it does, and asks first.
|
|
20
|
+
*/
|
|
21
|
+
class FigureContextMenu {
|
|
22
|
+
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.state = options.state;
|
|
25
|
+
this.canvas = options.canvas;
|
|
26
|
+
this.handlers = options.handlers || {};
|
|
27
|
+
this.el = null;
|
|
28
|
+
//: Where the menu was opened, in page millimetres. Paste puts things
|
|
29
|
+
//: where the user right-clicked, which is the only place they could
|
|
30
|
+
//: have meant.
|
|
31
|
+
this.point = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setup(surfaceEl, trayEl) {
|
|
35
|
+
this._onSurface = (event) => this.openForCanvas(event);
|
|
36
|
+
surfaceEl?.addEventListener("contextmenu", this._onSurface);
|
|
37
|
+
this._onTray = (event) => this.openForTray(event);
|
|
38
|
+
trayEl?.addEventListener("contextmenu", this._onTray);
|
|
39
|
+
|
|
40
|
+
this._onDismiss = (event) => {
|
|
41
|
+
if (this.el && !this.el.contains(event.target)) this.close();
|
|
42
|
+
};
|
|
43
|
+
document.addEventListener("pointerdown", this._onDismiss, true);
|
|
44
|
+
this._onScroll = () => this.close();
|
|
45
|
+
window.addEventListener("scroll", this._onScroll, true);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
destroy() {
|
|
49
|
+
document.removeEventListener("pointerdown", this._onDismiss, true);
|
|
50
|
+
window.removeEventListener("scroll", this._onScroll, true);
|
|
51
|
+
this.close();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// -- opening -----------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
openForCanvas(event) {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
this.point = this.canvas.surfacePoint(event);
|
|
59
|
+
|
|
60
|
+
const panelEl = event.target.closest?.(".fb-panel");
|
|
61
|
+
const annotationEl = event.target.closest?.(".fb-annotation");
|
|
62
|
+
const id = panelEl ? panelEl.dataset.panelId
|
|
63
|
+
: annotationEl ? annotationEl.dataset.annotationId : null;
|
|
64
|
+
|
|
65
|
+
// Right-clicking something that is not in the selection selects it
|
|
66
|
+
// first. A menu that acted on a selection somewhere else on the page is
|
|
67
|
+
// the classic way to delete the wrong thing.
|
|
68
|
+
if (id && !this.canvas.selection.has(id)) this.canvas.select([id], false);
|
|
69
|
+
if (!id) this.canvas.select([], false);
|
|
70
|
+
|
|
71
|
+
const ids = Array.from(this.canvas.selection);
|
|
72
|
+
this.open(event.clientX, event.clientY,
|
|
73
|
+
ids.length ? this.selectionEntries(ids) : this.pageEntries());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
openForTray(event) {
|
|
77
|
+
const item = event.target.closest?.(".fb-tray-item");
|
|
78
|
+
if (!item) return;
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
this.point = null;
|
|
81
|
+
this.handlers.onTrayContext?.(item.dataset.panelId);
|
|
82
|
+
this.open(event.clientX, event.clientY,
|
|
83
|
+
this.trayEntries(this.handlers.traySelection?.() || [item.dataset.panelId]));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// -- what each menu holds ------------------------------------------------
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The right-click rows, from the same registry the floating bar reads.
|
|
90
|
+
*
|
|
91
|
+
* It was a separate hand-built list, and the two had already drifted: this
|
|
92
|
+
* one honestly disabled "Bring to front" when nothing could be reordered
|
|
93
|
+
* while the bar offered it live on a text box, where it did nothing.
|
|
94
|
+
*/
|
|
95
|
+
selectionEntries(ids) {
|
|
96
|
+
const context = {
|
|
97
|
+
ids: ids,
|
|
98
|
+
sel: FigureSelection.describe(ids, this.state, this.canvas),
|
|
99
|
+
canvas: this.canvas,
|
|
100
|
+
state: this.state,
|
|
101
|
+
handlers: this.handlers,
|
|
102
|
+
};
|
|
103
|
+
const entries = [];
|
|
104
|
+
let generic = null;
|
|
105
|
+
for (const action of FigureActions.forSurface("menu", context.sel, context)) {
|
|
106
|
+
if (generic !== null && Boolean(action.generic) !== generic) {
|
|
107
|
+
entries.push({ separator: true });
|
|
108
|
+
}
|
|
109
|
+
generic = Boolean(action.generic);
|
|
110
|
+
entries.push({ act: action.id, label: action.label,
|
|
111
|
+
icon: action.icon, shortcut: action.shortcut,
|
|
112
|
+
disabled: !action.isEnabled, danger: action.danger });
|
|
113
|
+
}
|
|
114
|
+
return entries;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
pageEntries() {
|
|
118
|
+
return [
|
|
119
|
+
{ act: "paste", label: "Paste", disabled: !FigureClipboard.hasContent() },
|
|
120
|
+
{ separator: true },
|
|
121
|
+
{ act: "select_all", label: "Select everything on this page" },
|
|
122
|
+
{ separator: true },
|
|
123
|
+
{ act: "page_background", label: "Page background…" },
|
|
124
|
+
{ act: "page_duplicate", label: "Duplicate this page" },
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
trayEntries(ids) {
|
|
129
|
+
return [
|
|
130
|
+
{ act: "tray_place", label: ids.length > 1 ? "Place all on this page" : "Place on this page" },
|
|
131
|
+
{ act: "tray_duplicate", label: "Duplicate" },
|
|
132
|
+
{ separator: true },
|
|
133
|
+
{ act: "tray_delete", label: "Delete from figure", danger: true },
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// -- the menu itself -----------------------------------------------------
|
|
138
|
+
|
|
139
|
+
open(clientX, clientY, entries) {
|
|
140
|
+
this.close();
|
|
141
|
+
this.el = document.createElement("div");
|
|
142
|
+
this.el.className = "fb-menu";
|
|
143
|
+
// The icon gutter is drawn only when SOME row in this menu has an icon.
|
|
144
|
+
// A fixed gutter is what stops a half-iconed list having a ragged left
|
|
145
|
+
// edge; in a list where no row has one at all -- the page menu, the tray
|
|
146
|
+
// menu -- it is fourteen pixels of indent that says nothing.
|
|
147
|
+
const gutter = entries.some((entry) => entry.icon);
|
|
148
|
+
this.el.innerHTML = entries.map((entry) => entry.separator
|
|
149
|
+
? '<span class="fb-menu-separator"></span>'
|
|
150
|
+
: `<button type="button" class="fb-menu-item${entry.danger ? " is-danger" : ""}"
|
|
151
|
+
data-act="${entry.act}" ${entry.disabled ? "disabled" : ""}>${gutter
|
|
152
|
+
? `<span class="fb-menu-icon" aria-hidden="true">${entry.icon
|
|
153
|
+
? `<span class="fas fa-${entry.icon}"></span>` : ""}</span>` : ""}
|
|
154
|
+
<span class="fb-menu-text">${FigureSchema.escapeHtml(entry.label)}</span>${
|
|
155
|
+
entry.shortcut
|
|
156
|
+
? `<span class="fb-menu-key" aria-hidden="true">${
|
|
157
|
+
FigureSchema.escapeHtml(entry.shortcut)}</span>` : ""}</button>`
|
|
158
|
+
).join("");
|
|
159
|
+
document.body.appendChild(this.el);
|
|
160
|
+
|
|
161
|
+
// Flipped rather than clipped: a menu opened near the bottom right of
|
|
162
|
+
// the window has to open up and to the left, or its last item -- which
|
|
163
|
+
// is the destructive one -- is off screen.
|
|
164
|
+
// offsetWidth/Height and not getBoundingClientRect: the card animates
|
|
165
|
+
// in with a `scale(0.97)`, and a client rect is the TRANSFORMED box.
|
|
166
|
+
// Measuring it mid-animation would under-report the size by a few
|
|
167
|
+
// pixels and decide "does this fit below?" from a number that stops
|
|
168
|
+
// being true 120ms later. The offset pair is the layout box, which the
|
|
169
|
+
// animation never touches.
|
|
170
|
+
const size = { width: this.el.offsetWidth, height: this.el.offsetHeight };
|
|
171
|
+
const left = clientX + size.width > window.innerWidth - 8
|
|
172
|
+
? Math.max(8, clientX - size.width) : clientX;
|
|
173
|
+
const top = clientY + size.height > window.innerHeight - 8
|
|
174
|
+
? Math.max(8, clientY - size.height) : clientY;
|
|
175
|
+
this.el.style.left = Math.round(left) + "px";
|
|
176
|
+
this.el.style.top = Math.round(top) + "px";
|
|
177
|
+
|
|
178
|
+
this.el.addEventListener("click", (event) => {
|
|
179
|
+
const item = event.target.closest("[data-act]");
|
|
180
|
+
if (!item || item.disabled) return;
|
|
181
|
+
const act = item.dataset.act;
|
|
182
|
+
this.close();
|
|
183
|
+
this.run(act);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
close() {
|
|
188
|
+
this.el?.remove();
|
|
189
|
+
this.el = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
run(act) {
|
|
193
|
+
const ids = Array.from(this.canvas.selection);
|
|
194
|
+
const handlers = this.handlers;
|
|
195
|
+
|
|
196
|
+
// A selection action runs from the registry. Only the entries that are
|
|
197
|
+
// about the PAGE or the TRAY rather than about the selection are
|
|
198
|
+
// dispatched here, because those have no selection to describe.
|
|
199
|
+
const action = FigureActions.byId(act);
|
|
200
|
+
if (action && action.run) {
|
|
201
|
+
action.run({ ids: ids,
|
|
202
|
+
sel: FigureSelection.describe(ids, this.state, this.canvas),
|
|
203
|
+
canvas: this.canvas, state: this.state,
|
|
204
|
+
handlers: handlers });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
({
|
|
208
|
+
paste: () => this.canvas.paste(this.point),
|
|
209
|
+
select_all: () => this.canvas.selectAllOnPage(),
|
|
210
|
+
page_background: () => handlers.onPageBackground?.(),
|
|
211
|
+
page_duplicate: () => handlers.onDuplicatePage?.(),
|
|
212
|
+
tray_place: () => handlers.onPlaceFromTray?.(),
|
|
213
|
+
tray_duplicate: () => handlers.onDuplicateTray?.(),
|
|
214
|
+
tray_delete: () => handlers.onDeleteTray?.(),
|
|
215
|
+
}[act] || (() => {}))();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The clipboard, for this tab.
|
|
222
|
+
*
|
|
223
|
+
* A module singleton rather than the OS clipboard. What is copied here is a
|
|
224
|
+
* captured SCENE -- a source id, a region in image pixels, a set of channels
|
|
225
|
+
* and windows -- and none of that means anything in another application, or in
|
|
226
|
+
* another figure that does not reference the same image. Writing it to the
|
|
227
|
+
* system clipboard would offer a paste everywhere that can only fail almost
|
|
228
|
+
* everywhere.
|
|
229
|
+
*
|
|
230
|
+
* Cross-tab paste is deliberately not built either: two tabs on one figure is
|
|
231
|
+
* the case the whole revision system exists to keep honest, and a paste that
|
|
232
|
+
* crossed between them would arrive against a revision it was not copied from.
|
|
233
|
+
*/
|
|
234
|
+
const FigureClipboard = {
|
|
235
|
+
panels: [],
|
|
236
|
+
annotations: [],
|
|
237
|
+
|
|
238
|
+
hasContent() {
|
|
239
|
+
return this.panels.length > 0 || this.annotations.length > 0;
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
put(panels, annotations) {
|
|
243
|
+
this.panels = JSON.parse(JSON.stringify(panels || []));
|
|
244
|
+
this.annotations = JSON.parse(JSON.stringify(annotations || []));
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
take() {
|
|
248
|
+
return {
|
|
249
|
+
panels: JSON.parse(JSON.stringify(this.panels)),
|
|
250
|
+
annotations: JSON.parse(JSON.stringify(this.annotations)),
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FigureDocumentState - the open figure, and everything about saving it.
|
|
3
|
+
*
|
|
4
|
+
* One object owns the document, the revision, the save queue and the undo
|
|
5
|
+
* stack, because all four are the same question asked from different angles and
|
|
6
|
+
* splitting them is how a client ends up saving a revision it no longer holds.
|
|
7
|
+
*
|
|
8
|
+
* Three decisions worth stating:
|
|
9
|
+
*
|
|
10
|
+
* **The server is the only interpreter of operations.** This side edits its own
|
|
11
|
+
* copy directly and sends the equivalent operation list; it does not reimplement
|
|
12
|
+
* `operations.py`. Two implementations of "what does add_panel mean" is two
|
|
13
|
+
* implementations that can disagree, and the disagreement would be invisible
|
|
14
|
+
* until a figure was reopened. If the server rejects a batch, the local copy is
|
|
15
|
+
* rolled back to the last confirmed one -- so the two can never drift, because
|
|
16
|
+
* only agreement survives.
|
|
17
|
+
*
|
|
18
|
+
* **Saves are chained, never concurrent**, and each reads the revision at the
|
|
19
|
+
* moment it is SENT rather than the moment the edit was made. Those two moments
|
|
20
|
+
* are not the same one: a commit made while an earlier save is still in flight
|
|
21
|
+
* would otherwise carry a revision the server has already moved past, and be
|
|
22
|
+
* answered with a 409 that no other session had anything to do with. Reading it
|
|
23
|
+
* late is safe precisely because the chain means nothing else can have written
|
|
24
|
+
* in between. Chaining also makes the outcome deterministic: without it, a slow
|
|
25
|
+
* earlier save can land after a newer one and quietly reinstate what the newer
|
|
26
|
+
* one changed.
|
|
27
|
+
*
|
|
28
|
+
* **Undo is a snapshot, not an inverse.** A figure document is a few hundred
|
|
29
|
+
* kilobytes at worst -- it holds references and geometry, never pixels -- so
|
|
30
|
+
* keeping N of them costs nothing next to writing and maintaining an inverse
|
|
31
|
+
* for every operation, and an inverse that is subtly wrong corrupts a figure
|
|
32
|
+
* silently. Undo is sent as a whole-document replace, which is a new revision:
|
|
33
|
+
* revisions only ever go forwards, and the conflict check depends on that.
|
|
34
|
+
*/
|
|
35
|
+
class FigureDocumentState {
|
|
36
|
+
|
|
37
|
+
/** How many undo steps are kept. Past this, the oldest is dropped. */
|
|
38
|
+
static get HISTORY_LIMIT() { return 50; }
|
|
39
|
+
|
|
40
|
+
constructor(options) {
|
|
41
|
+
this.api = options.api;
|
|
42
|
+
this.figureId = options.figureId;
|
|
43
|
+
|
|
44
|
+
this.document = null;
|
|
45
|
+
this.sourceStatus = {};
|
|
46
|
+
/** "loading" | "saved" | "saving" | "unsaved" | "failed" | "conflict" | "unreadable" */
|
|
47
|
+
this.status = "loading";
|
|
48
|
+
this.statusDetail = "";
|
|
49
|
+
|
|
50
|
+
this._listeners = { change: [], status: [] };
|
|
51
|
+
this._chain = Promise.resolve();
|
|
52
|
+
this._undo = [];
|
|
53
|
+
this._redo = [];
|
|
54
|
+
|
|
55
|
+
["change", "status"].forEach((name) => {
|
|
56
|
+
if (typeof options["on" + name.charAt(0).toUpperCase() + name.slice(1)] === "function") {
|
|
57
|
+
this._listeners[name].push(options["on" + name.charAt(0).toUpperCase() + name.slice(1)]);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
on(event, handler) {
|
|
63
|
+
if (this._listeners[event]) this._listeners[event].push(handler);
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_emit(event, payload) {
|
|
68
|
+
(this._listeners[event] || []).forEach((handler) => {
|
|
69
|
+
try {
|
|
70
|
+
handler(payload, this);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// One broken listener must not stop the others, and must not
|
|
73
|
+
// abort a save that has already happened.
|
|
74
|
+
console.error("figure_builder: listener failed", error);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_setStatus(status, detail) {
|
|
80
|
+
this.status = status;
|
|
81
|
+
this.statusDetail = detail || "";
|
|
82
|
+
this._emit("status", { status: status, detail: this.statusDetail });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get revision() {
|
|
86
|
+
return this.document ? this.document.revision : 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get canUndo() { return this._undo.length > 0; }
|
|
90
|
+
get canRedo() { return this._redo.length > 0; }
|
|
91
|
+
|
|
92
|
+
async load() {
|
|
93
|
+
const result = await this.api.getFigure(this.figureId);
|
|
94
|
+
if (result.status === 422) {
|
|
95
|
+
this._setStatus("unreadable", result.data.detail || result.data.error || "");
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (!result.ok) {
|
|
99
|
+
this._setStatus("failed", result.status === 404
|
|
100
|
+
? "This figure no longer exists."
|
|
101
|
+
: "Could not open this figure.");
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
this.document = result.data.document;
|
|
105
|
+
this.sourceStatus = result.data.source_status || {};
|
|
106
|
+
this._undo = [];
|
|
107
|
+
this._redo = [];
|
|
108
|
+
this._setStatus("saved");
|
|
109
|
+
this._emit("change", this.document);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Change the figure and save the change.
|
|
115
|
+
*
|
|
116
|
+
* `mutate(draft)` applies the edit to a copy of the document for immediate
|
|
117
|
+
* display; `operations` is the same edit expressed for the server. They
|
|
118
|
+
* describe one action, which is why they are passed together -- and one
|
|
119
|
+
* call here is one undo step, whatever it contains, because that is what
|
|
120
|
+
* makes a five-panel split undo as a split rather than as five deletions.
|
|
121
|
+
*
|
|
122
|
+
* Returns a promise for `true` if it was stored.
|
|
123
|
+
*/
|
|
124
|
+
commit(operations, mutate) {
|
|
125
|
+
if (!this.document) return Promise.resolve(false);
|
|
126
|
+
|
|
127
|
+
const before = this._snapshot();
|
|
128
|
+
const draft = this._snapshot();
|
|
129
|
+
try {
|
|
130
|
+
if (typeof mutate === "function") mutate(draft);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error("figure_builder: local edit failed", error);
|
|
133
|
+
return Promise.resolve(false);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.document = draft;
|
|
137
|
+
this._undo.push(before);
|
|
138
|
+
if (this._undo.length > FigureDocumentState.HISTORY_LIMIT) this._undo.shift();
|
|
139
|
+
// A new edit is a new branch: whatever was undone is no longer
|
|
140
|
+
// reachable, and offering a redo that would reinstate a state the user
|
|
141
|
+
// has since edited past is offering an action nobody can predict.
|
|
142
|
+
this._redo = [];
|
|
143
|
+
this._setStatus("unsaved");
|
|
144
|
+
this._emit("change", this.document);
|
|
145
|
+
|
|
146
|
+
return this._enqueue(async () => {
|
|
147
|
+
// Read HERE, not where the commit was made. The queue serialises
|
|
148
|
+
// saves, so an earlier one may have moved the figure on between the
|
|
149
|
+
// two moments -- and `before.revision` is then a revision the server
|
|
150
|
+
// has already superseded, which it answers with a 409 that has
|
|
151
|
+
// nothing to do with another session. Placing a text box and pressing
|
|
152
|
+
// Escape was enough: the add and the remove are two commits in the
|
|
153
|
+
// same breath, and the second carried the revision from before the
|
|
154
|
+
// first.
|
|
155
|
+
//
|
|
156
|
+
// Basing the operations on the latest CONFIRMED revision is exactly
|
|
157
|
+
// what the chain exists to make safe: nothing else can have written
|
|
158
|
+
// in between, because nothing else is allowed to be in flight.
|
|
159
|
+
const baseRevision = this.revision;
|
|
160
|
+
this._setStatus("saving");
|
|
161
|
+
let result;
|
|
162
|
+
try {
|
|
163
|
+
result = await this.api.patchFigure(this.figureId, baseRevision, operations);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
// Never discard local state after a failed save: the network is
|
|
166
|
+
// the thing that failed, and the user's work is still here.
|
|
167
|
+
this._setStatus("failed", "Could not reach the server. Your changes are still here.");
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return this._settle(result, before);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Roll back one committed action. */
|
|
175
|
+
undo() {
|
|
176
|
+
if (!this._undo.length) return Promise.resolve(false);
|
|
177
|
+
const target = this._undo.pop();
|
|
178
|
+
this._redo.push(this._snapshot());
|
|
179
|
+
return this._replaceWith(target);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
redo() {
|
|
183
|
+
if (!this._redo.length) return Promise.resolve(false);
|
|
184
|
+
const target = this._redo.pop();
|
|
185
|
+
this._undo.push(this._snapshot());
|
|
186
|
+
return this._replaceWith(target);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_replaceWith(target) {
|
|
190
|
+
const current = this.document;
|
|
191
|
+
const draft = JSON.parse(JSON.stringify(target));
|
|
192
|
+
// The revision is the CURRENT one, not the one the snapshot was taken
|
|
193
|
+
// at: going back to an earlier state is a new edit, and rewinding the
|
|
194
|
+
// number would make every other tab's next save look valid when it is
|
|
195
|
+
// not.
|
|
196
|
+
draft.revision = current.revision;
|
|
197
|
+
this.document = draft;
|
|
198
|
+
this._setStatus("unsaved");
|
|
199
|
+
this._emit("change", this.document);
|
|
200
|
+
|
|
201
|
+
return this._enqueue(async () => {
|
|
202
|
+
// Same as `commit`: the revision is whatever is confirmed when the
|
|
203
|
+
// request goes out, not what was current when undo was pressed. Two
|
|
204
|
+
// undos in a row is the case that broke.
|
|
205
|
+
const baseRevision = this.revision;
|
|
206
|
+
draft.revision = baseRevision;
|
|
207
|
+
this._setStatus("saving");
|
|
208
|
+
let result;
|
|
209
|
+
try {
|
|
210
|
+
result = await this.api.replaceFigure(this.figureId, baseRevision, draft);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
this._setStatus("failed", "Could not reach the server. Your changes are still here.");
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
return this._settle(result, current);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
_settle(result, before) {
|
|
220
|
+
if (result.ok) {
|
|
221
|
+
this.document.revision = result.data.revision;
|
|
222
|
+
this._setStatus("saved");
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
if (result.status === 409) {
|
|
226
|
+
// Both sides have work. Nothing is discarded here -- the banner
|
|
227
|
+
// asks, and reloading is the user's decision.
|
|
228
|
+
this._setStatus("conflict", "This figure was modified in another session.");
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
if (result.status === 422) {
|
|
232
|
+
this._setStatus("unreadable", result.data.detail || "");
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
// The server refused the edit itself. The local copy is now describing
|
|
236
|
+
// something that was never stored, so it goes back to what was
|
|
237
|
+
// confirmed -- otherwise the next save would be built on a state the
|
|
238
|
+
// server has never seen.
|
|
239
|
+
this.document = before;
|
|
240
|
+
this._undo.pop();
|
|
241
|
+
this._setStatus("failed", result.data.error || "That change could not be saved.");
|
|
242
|
+
this._emit("change", this.document);
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
_enqueue(task) {
|
|
247
|
+
this._chain = this._chain.then(task, task);
|
|
248
|
+
return this._chain;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_snapshot() {
|
|
252
|
+
return JSON.parse(JSON.stringify(this.document));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// -- convenience readers, so callers do not reach into the shape ------
|
|
256
|
+
|
|
257
|
+
get title() { return this.document ? this.document.title : ""; }
|
|
258
|
+
|
|
259
|
+
get pages() { return this.document ? this.document.pages : []; }
|
|
260
|
+
|
|
261
|
+
panel(panelId) {
|
|
262
|
+
return (this.document && this.document.panels[panelId]) || null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
source(sourceId) {
|
|
266
|
+
return (this.document && this.document.sources[sourceId]) || null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The source registered for a datasource, or null if it is not one yet. */
|
|
270
|
+
sourceForDatasource(datasource) {
|
|
271
|
+
if (!this.document) return null;
|
|
272
|
+
return Object.values(this.document.sources).find(
|
|
273
|
+
(source) => source.kind === "plexora_project" && source.datasource === datasource) || null;
|
|
274
|
+
}
|
|
275
|
+
}
|