react-cheminfo 0.10.0 → 0.11.0
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.
- package/lib/chrome/ui/NavLink.d.ts.map +1 -1
- package/lib/chrome/ui/NavLink.js +4 -3
- package/lib/chrome/ui/NavLink.js.map +1 -1
- package/lib/chrome/ui/SiteFooter.d.ts +8 -0
- package/lib/chrome/ui/SiteFooter.d.ts.map +1 -1
- package/lib/chrome/ui/SiteFooter.js +7 -2
- package/lib/chrome/ui/SiteFooter.js.map +1 -1
- package/lib/chrome/ui/SiteHeader.d.ts +11 -0
- package/lib/chrome/ui/SiteHeader.d.ts.map +1 -1
- package/lib/chrome/ui/SiteHeader.js +7 -2
- package/lib/chrome/ui/SiteHeader.js.map +1 -1
- package/lib/chrome/ui/navItem.d.ts +12 -0
- package/lib/chrome/ui/navItem.d.ts.map +1 -1
- package/lib/chrome/ui/navItem.js.map +1 -1
- package/lib/core.d.ts +1 -0
- package/lib/core.d.ts.map +1 -1
- package/lib/core.js +1 -0
- package/lib/core.js.map +1 -1
- package/lib/download/core/figureLegend.d.ts +90 -0
- package/lib/download/core/figureLegend.d.ts.map +1 -0
- package/lib/download/core/figureLegend.js +77 -0
- package/lib/download/core/figureLegend.js.map +1 -0
- package/lib/download/core/figureSvg.d.ts.map +1 -1
- package/lib/download/core/figureSvg.js +71 -1
- package/lib/download/core/figureSvg.js.map +1 -1
- package/lib/download/core/figureSvgDocument.d.ts +9 -2
- package/lib/download/core/figureSvgDocument.d.ts.map +1 -1
- package/lib/download/core/figureSvgDocument.js +4 -1
- package/lib/download/core/figureSvgDocument.js.map +1 -1
- package/lib/download/core/figureTarget.d.ts +11 -0
- package/lib/download/core/figureTarget.d.ts.map +1 -1
- package/lib/download/core/figureTarget.js +19 -0
- package/lib/download/core/figureTarget.js.map +1 -1
- package/lib/download/core/index.d.ts +3 -1
- package/lib/download/core/index.d.ts.map +1 -1
- package/lib/download/core/index.js +2 -1
- package/lib/download/core/index.js.map +1 -1
- package/lib/ecosystem/core/sites.d.ts +1 -1
- package/lib/ecosystem/core/sites.d.ts.map +1 -1
- package/lib/ecosystem/core/sites.js +13 -0
- package/lib/ecosystem/core/sites.js.map +1 -1
- package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
- package/lib/ecosystem/ui/glyphs.js +5 -0
- package/lib/ecosystem/ui/glyphs.js.map +1 -1
- package/lib/overlay/ui/OverlayLegend.d.ts.map +1 -1
- package/lib/overlay/ui/OverlayLegend.js +1 -1
- package/lib/overlay/ui/OverlayLegend.js.map +1 -1
- package/lib/projection/core/projectionCopy.d.ts +4 -0
- package/lib/projection/core/projectionCopy.d.ts.map +1 -1
- package/lib/projection/core/projectionCopy.js +3 -0
- package/lib/projection/core/projectionCopy.js.map +1 -1
- package/lib/projection/core/projectionOptions.d.ts +12 -0
- package/lib/projection/core/projectionOptions.d.ts.map +1 -1
- package/lib/projection/core/projectionOptions.js +27 -1
- package/lib/projection/core/projectionOptions.js.map +1 -1
- package/lib/projection/core/projectionStrings.d.ts +4 -0
- package/lib/projection/core/projectionStrings.d.ts.map +1 -1
- package/lib/projection/core/projectionStrings.js +14 -0
- package/lib/projection/core/projectionStrings.js.map +1 -1
- package/lib/projection/core/projectionTabDefaults.d.ts.map +1 -1
- package/lib/projection/core/projectionTabDefaults.js +12 -0
- package/lib/projection/core/projectionTabDefaults.js.map +1 -1
- package/lib/projection/core/projectionTabs.d.ts +1 -1
- package/lib/projection/core/projectionTabs.d.ts.map +1 -1
- package/lib/projection/core/projectionTabs.js +9 -0
- package/lib/projection/core/projectionTabs.js.map +1 -1
- package/lib/projection/ui/ProjectionMapTab.d.ts +6 -0
- package/lib/projection/ui/ProjectionMapTab.d.ts.map +1 -1
- package/lib/projection/ui/ProjectionMapTab.js +2 -1
- package/lib/projection/ui/ProjectionMapTab.js.map +1 -1
- package/lib/projection/ui/ProjectionPanel.d.ts +6 -0
- package/lib/projection/ui/ProjectionPanel.d.ts.map +1 -1
- package/lib/projection/ui/ProjectionPanel.js +15 -1
- package/lib/projection/ui/ProjectionPanel.js.map +1 -1
- package/lib/projection/ui/ProjectionReadout.d.ts +7 -0
- package/lib/projection/ui/ProjectionReadout.d.ts.map +1 -1
- package/lib/projection/ui/ProjectionReadout.js +5 -4
- package/lib/projection/ui/ProjectionReadout.js.map +1 -1
- package/lib/projection/ui/ProjectionSpaceControls.d.ts +51 -0
- package/lib/projection/ui/ProjectionSpaceControls.d.ts.map +1 -0
- package/lib/projection/ui/ProjectionSpaceControls.js +71 -0
- package/lib/projection/ui/ProjectionSpaceControls.js.map +1 -0
- package/lib/projection/ui/ProjectionSpaceMore.d.ts +32 -0
- package/lib/projection/ui/ProjectionSpaceMore.d.ts.map +1 -0
- package/lib/projection/ui/ProjectionSpaceMore.js +57 -0
- package/lib/projection/ui/ProjectionSpaceMore.js.map +1 -0
- package/lib/projection/ui/ProjectionSpaceTab.d.ts +98 -0
- package/lib/projection/ui/ProjectionSpaceTab.d.ts.map +1 -0
- package/lib/projection/ui/ProjectionSpaceTab.js +66 -0
- package/lib/projection/ui/ProjectionSpaceTab.js.map +1 -0
- package/lib/projection/ui/ProjectionViewer.d.ts +1 -0
- package/lib/projection/ui/ProjectionViewer.d.ts.map +1 -1
- package/lib/projection/ui/ProjectionViewer.js +2 -1
- package/lib/projection/ui/ProjectionViewer.js.map +1 -1
- package/lib/projection/ui/index.d.ts +1 -0
- package/lib/projection/ui/index.d.ts.map +1 -1
- package/lib/projection/ui/index.js.map +1 -1
- package/lib/projection/ui/projectionBarModel.d.ts.map +1 -1
- package/lib/projection/ui/projectionBarModel.js +3 -2
- package/lib/projection/ui/projectionBarModel.js.map +1 -1
- package/lib/projection/ui/projectionBarReadings.d.ts.map +1 -1
- package/lib/projection/ui/projectionBarReadings.js +17 -0
- package/lib/projection/ui/projectionBarReadings.js.map +1 -1
- package/lib/projection/ui/projectionBarSlots.d.ts.map +1 -1
- package/lib/projection/ui/projectionBarSlots.js +17 -0
- package/lib/projection/ui/projectionBarSlots.js.map +1 -1
- package/lib/projection/ui/projectionCloudChoices.d.ts +18 -0
- package/lib/projection/ui/projectionCloudChoices.d.ts.map +1 -0
- package/lib/projection/ui/projectionCloudChoices.js +19 -0
- package/lib/projection/ui/projectionCloudChoices.js.map +1 -0
- package/lib/projection/ui/projectionEllipsoid.d.ts +38 -0
- package/lib/projection/ui/projectionEllipsoid.d.ts.map +1 -0
- package/lib/projection/ui/projectionEllipsoid.js +71 -0
- package/lib/projection/ui/projectionEllipsoid.js.map +1 -0
- package/lib/projection/ui/projectionMapChrome.d.ts +8 -0
- package/lib/projection/ui/projectionMapChrome.d.ts.map +1 -1
- package/lib/projection/ui/projectionMapChrome.js +10 -6
- package/lib/projection/ui/projectionMapChrome.js.map +1 -1
- package/lib/projection/ui/projectionMapView.d.ts +13 -0
- package/lib/projection/ui/projectionMapView.d.ts.map +1 -1
- package/lib/projection/ui/projectionMapView.js +26 -0
- package/lib/projection/ui/projectionMapView.js.map +1 -1
- package/lib/projection/ui/projectionSpaceModel.d.ts +14 -0
- package/lib/projection/ui/projectionSpaceModel.d.ts.map +1 -0
- package/lib/projection/ui/projectionSpaceModel.js +14 -0
- package/lib/projection/ui/projectionSpaceModel.js.map +1 -0
- package/lib/projection/ui/projectionViewerProps.d.ts +34 -0
- package/lib/projection/ui/projectionViewerProps.d.ts.map +1 -1
- package/lib/scatter/ui/ScatterPlot.d.ts +1 -1
- package/lib/scatter/ui/ScatterPlot.d.ts.map +1 -1
- package/lib/scatter/ui/ScatterPlot.js +18 -2
- package/lib/scatter/ui/ScatterPlot.js.map +1 -1
- package/lib/scatter/ui/lassoGesture.d.ts +14 -2
- package/lib/scatter/ui/lassoGesture.d.ts.map +1 -1
- package/lib/scatter/ui/lassoGesture.js +4 -1
- package/lib/scatter/ui/lassoGesture.js.map +1 -1
- package/lib/scatter/ui/scatterPlotProps.d.ts +34 -0
- package/lib/scatter/ui/scatterPlotProps.d.ts.map +1 -1
- package/lib/scatter/ui/useScatterInteraction.d.ts +9 -0
- package/lib/scatter/ui/useScatterInteraction.d.ts.map +1 -1
- package/lib/scatter/ui/useScatterInteraction.js +9 -1
- package/lib/scatter/ui/useScatterInteraction.js.map +1 -1
- package/lib/scatter3d/core/cloudGesture.d.ts +17 -0
- package/lib/scatter3d/core/cloudGesture.d.ts.map +1 -0
- package/lib/scatter3d/core/cloudGesture.js +2 -0
- package/lib/scatter3d/core/cloudGesture.js.map +1 -0
- package/lib/scatter3d/core/confidenceEllipsoid.d.ts +63 -0
- package/lib/scatter3d/core/confidenceEllipsoid.d.ts.map +1 -0
- package/lib/scatter3d/core/confidenceEllipsoid.js +128 -0
- package/lib/scatter3d/core/confidenceEllipsoid.js.map +1 -0
- package/lib/scatter3d/core/cubeFrame.d.ts +40 -0
- package/lib/scatter3d/core/cubeFrame.d.ts.map +1 -0
- package/lib/scatter3d/core/cubeFrame.js +58 -0
- package/lib/scatter3d/core/cubeFrame.js.map +1 -0
- package/lib/scatter3d/core/cubePoints.d.ts +35 -0
- package/lib/scatter3d/core/cubePoints.d.ts.map +1 -0
- package/lib/scatter3d/core/cubePoints.js +50 -0
- package/lib/scatter3d/core/cubePoints.js.map +1 -0
- package/lib/scatter3d/core/ellipsoidCoverage.d.ts +32 -0
- package/lib/scatter3d/core/ellipsoidCoverage.d.ts.map +1 -0
- package/lib/scatter3d/core/ellipsoidCoverage.js +83 -0
- package/lib/scatter3d/core/ellipsoidCoverage.js.map +1 -0
- package/lib/scatter3d/core/ellipsoidSilhouette.d.ts +42 -0
- package/lib/scatter3d/core/ellipsoidSilhouette.d.ts.map +1 -0
- package/lib/scatter3d/core/ellipsoidSilhouette.js +52 -0
- package/lib/scatter3d/core/ellipsoidSilhouette.js.map +1 -0
- package/lib/scatter3d/core/index.d.ts +29 -0
- package/lib/scatter3d/core/index.d.ts.map +1 -0
- package/lib/scatter3d/core/index.js +22 -0
- package/lib/scatter3d/core/index.js.map +1 -0
- package/lib/scatter3d/core/orbitCamera.d.ts +113 -0
- package/lib/scatter3d/core/orbitCamera.d.ts.map +1 -0
- package/lib/scatter3d/core/orbitCamera.js +104 -0
- package/lib/scatter3d/core/orbitCamera.js.map +1 -0
- package/lib/scatter3d/core/symmetricEigen.d.ts +47 -0
- package/lib/scatter3d/core/symmetricEigen.d.ts.map +1 -0
- package/lib/scatter3d/core/symmetricEigen.js +219 -0
- package/lib/scatter3d/core/symmetricEigen.js.map +1 -0
- package/lib/scatter3d/ui/CloudFrameLayer.d.ts +33 -0
- package/lib/scatter3d/ui/CloudFrameLayer.d.ts.map +1 -0
- package/lib/scatter3d/ui/CloudFrameLayer.js +37 -0
- package/lib/scatter3d/ui/CloudFrameLayer.js.map +1 -0
- package/lib/scatter3d/ui/CloudPointLayer.d.ts +62 -0
- package/lib/scatter3d/ui/CloudPointLayer.d.ts.map +1 -0
- package/lib/scatter3d/ui/CloudPointLayer.js +67 -0
- package/lib/scatter3d/ui/CloudPointLayer.js.map +1 -0
- package/lib/scatter3d/ui/CloudShellLayer.d.ts +52 -0
- package/lib/scatter3d/ui/CloudShellLayer.d.ts.map +1 -0
- package/lib/scatter3d/ui/CloudShellLayer.js +40 -0
- package/lib/scatter3d/ui/CloudShellLayer.js.map +1 -0
- package/lib/scatter3d/ui/ScatterCloud.d.ts +24 -0
- package/lib/scatter3d/ui/ScatterCloud.d.ts.map +1 -0
- package/lib/scatter3d/ui/ScatterCloud.js +189 -0
- package/lib/scatter3d/ui/ScatterCloud.js.map +1 -0
- package/lib/scatter3d/ui/cloudGeometry.d.ts +108 -0
- package/lib/scatter3d/ui/cloudGeometry.d.ts.map +1 -0
- package/lib/scatter3d/ui/cloudGeometry.js +122 -0
- package/lib/scatter3d/ui/cloudGeometry.js.map +1 -0
- package/lib/scatter3d/ui/cloudShellStyles.d.ts +36 -0
- package/lib/scatter3d/ui/cloudShellStyles.d.ts.map +1 -0
- package/lib/scatter3d/ui/cloudShellStyles.js +62 -0
- package/lib/scatter3d/ui/cloudShellStyles.js.map +1 -0
- package/lib/scatter3d/ui/cloudZoom.d.ts +21 -0
- package/lib/scatter3d/ui/cloudZoom.d.ts.map +1 -0
- package/lib/scatter3d/ui/cloudZoom.js +22 -0
- package/lib/scatter3d/ui/cloudZoom.js.map +1 -0
- package/lib/scatter3d/ui/index.d.ts +29 -0
- package/lib/scatter3d/ui/index.d.ts.map +1 -0
- package/lib/scatter3d/ui/index.js +21 -0
- package/lib/scatter3d/ui/index.js.map +1 -0
- package/lib/scatter3d/ui/scatterCloudModel.d.ts +60 -0
- package/lib/scatter3d/ui/scatterCloudModel.d.ts.map +1 -0
- package/lib/scatter3d/ui/scatterCloudModel.js +66 -0
- package/lib/scatter3d/ui/scatterCloudModel.js.map +1 -0
- package/lib/scatter3d/ui/scatterCloudProps.d.ts +185 -0
- package/lib/scatter3d/ui/scatterCloudProps.d.ts.map +1 -0
- package/lib/scatter3d/ui/scatterCloudProps.js +9 -0
- package/lib/scatter3d/ui/scatterCloudProps.js.map +1 -0
- package/lib/scatter3d/ui/useOrbitDrag.d.ts +60 -0
- package/lib/scatter3d/ui/useOrbitDrag.d.ts.map +1 -0
- package/lib/scatter3d/ui/useOrbitDrag.js +129 -0
- package/lib/scatter3d/ui/useOrbitDrag.js.map +1 -0
- package/lib/ui.d.ts +1 -0
- package/lib/ui.d.ts.map +1 -1
- package/lib/ui.js +1 -0
- package/lib/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/chrome/ui/NavLink.tsx +5 -3
- package/src/chrome/ui/SiteFooter.tsx +16 -1
- package/src/chrome/ui/SiteHeader.tsx +19 -1
- package/src/chrome/ui/navItem.ts +12 -0
- package/src/core.ts +1 -0
- package/src/download/core/figureLegend.ts +167 -0
- package/src/download/core/figureSvg.ts +81 -1
- package/src/download/core/figureSvgDocument.ts +13 -3
- package/src/download/core/figureTarget.ts +21 -0
- package/src/download/core/index.ts +8 -0
- package/src/ecosystem/core/sites.ts +14 -0
- package/src/ecosystem/ui/glyphs.tsx +26 -0
- package/src/overlay/ui/OverlayLegend.tsx +7 -1
- package/src/projection/core/projectionCopy.ts +9 -0
- package/src/projection/core/projectionOptions.ts +48 -3
- package/src/projection/core/projectionStrings.ts +16 -0
- package/src/projection/core/projectionTabDefaults.ts +12 -0
- package/src/projection/core/projectionTabs.ts +10 -1
- package/src/projection/ui/ProjectionMapTab.tsx +8 -0
- package/src/projection/ui/ProjectionPanel.tsx +63 -0
- package/src/projection/ui/ProjectionReadout.tsx +16 -6
- package/src/projection/ui/ProjectionSpaceControls.tsx +219 -0
- package/src/projection/ui/ProjectionSpaceMore.tsx +169 -0
- package/src/projection/ui/ProjectionSpaceTab.tsx +217 -0
- package/src/projection/ui/ProjectionViewer.tsx +3 -0
- package/src/projection/ui/index.ts +1 -0
- package/src/projection/ui/projectionBarModel.ts +3 -2
- package/src/projection/ui/projectionBarReadings.ts +18 -0
- package/src/projection/ui/projectionBarSlots.tsx +19 -0
- package/src/projection/ui/projectionCloudChoices.ts +23 -0
- package/src/projection/ui/projectionEllipsoid.ts +78 -0
- package/src/projection/ui/projectionMapChrome.ts +26 -5
- package/src/projection/ui/projectionMapView.ts +45 -0
- package/src/projection/ui/projectionSpaceModel.ts +14 -0
- package/src/projection/ui/projectionViewerProps.ts +35 -0
- package/src/scatter/ui/ScatterPlot.tsx +21 -2
- package/src/scatter/ui/lassoGesture.ts +15 -2
- package/src/scatter/ui/scatterPlotProps.ts +35 -0
- package/src/scatter/ui/useScatterInteraction.ts +22 -1
- package/src/scatter3d/core/cloudGesture.ts +16 -0
- package/src/scatter3d/core/confidenceEllipsoid.ts +177 -0
- package/src/scatter3d/core/cubeFrame.ts +72 -0
- package/src/scatter3d/core/cubePoints.ts +75 -0
- package/src/scatter3d/core/ellipsoidCoverage.ts +86 -0
- package/src/scatter3d/core/ellipsoidSilhouette.ts +76 -0
- package/src/scatter3d/core/index.ts +52 -0
- package/src/scatter3d/core/orbitCamera.ts +159 -0
- package/src/scatter3d/core/symmetricEigen.ts +273 -0
- package/src/scatter3d/ui/CloudFrameLayer.tsx +97 -0
- package/src/scatter3d/ui/CloudPointLayer.tsx +132 -0
- package/src/scatter3d/ui/CloudShellLayer.tsx +107 -0
- package/src/scatter3d/ui/ScatterCloud.tsx +312 -0
- package/src/scatter3d/ui/cloudGeometry.ts +200 -0
- package/src/scatter3d/ui/cloudShellStyles.ts +74 -0
- package/src/scatter3d/ui/cloudZoom.ts +22 -0
- package/src/scatter3d/ui/index.ts +41 -0
- package/src/scatter3d/ui/scatterCloudModel.ts +98 -0
- package/src/scatter3d/ui/scatterCloudProps.ts +191 -0
- package/src/scatter3d/ui/useOrbitDrag.ts +220 -0
- package/src/ui.ts +1 -0
- package/styles/chrome.css +29 -4
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import { memo, useId } from 'react';
|
|
3
|
+
|
|
4
|
+
import type { ConfidenceEllipsoid } from '../core/confidenceEllipsoid.ts';
|
|
5
|
+
import type { OrbitCamera, OrbitViewport } from '../core/orbitCamera.ts';
|
|
6
|
+
|
|
7
|
+
import { projectShellOutline } from './cloudGeometry.ts';
|
|
8
|
+
import { SHELL_FILL_OPACITY, shellGlassStops } from './cloudShellStyles.ts';
|
|
9
|
+
|
|
10
|
+
/** One group's shell, fitted once and ready to be turned. */
|
|
11
|
+
export interface CloudShell {
|
|
12
|
+
/** A stable key, so a re-render does not rebuild every shell. */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Its colour, which is its group's. */
|
|
15
|
+
color: string;
|
|
16
|
+
/** The shape it summarises, in cube units. */
|
|
17
|
+
ellipsoid: ConfidenceEllipsoid;
|
|
18
|
+
/**
|
|
19
|
+
* How strongly it is drawn, from a legend entry switching its group off.
|
|
20
|
+
* @default 1
|
|
21
|
+
*/
|
|
22
|
+
opacity?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** What {@link CloudShellLayer} draws. */
|
|
26
|
+
export interface CloudShellLayerProps {
|
|
27
|
+
/** The shells, one per group that has one. */
|
|
28
|
+
shells: readonly CloudShell[];
|
|
29
|
+
/** Where the reader is standing. */
|
|
30
|
+
camera: OrbitCamera;
|
|
31
|
+
/** Where the middle of the cube is, and how large it is drawn. */
|
|
32
|
+
viewport: OrbitViewport;
|
|
33
|
+
/**
|
|
34
|
+
* How solid one wall of the glass is. Turn it down when many groups overlap,
|
|
35
|
+
* since two shells crossing compound to twice this.
|
|
36
|
+
* @default 0.1
|
|
37
|
+
*/
|
|
38
|
+
fillOpacity?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The group shells, as translucent glass.
|
|
43
|
+
*
|
|
44
|
+
* Each one is drawn as a single ellipse, because that is exactly what a shell
|
|
45
|
+
* looks like: an orthographic camera sends a sphere carried by three axes to
|
|
46
|
+
* an ellipse, whatever it is turned to. So the outline is a curve rather than
|
|
47
|
+
* a ring of facets, it stays a curve at any zoom, and there are no seams
|
|
48
|
+
* between neighbouring faces to read as a wireframe.
|
|
49
|
+
*
|
|
50
|
+
* What the faces were for is kept in the fill. A ray through a shell crosses
|
|
51
|
+
* it twice — the near wall and the far one — so the glass is darkest where the
|
|
52
|
+
* reader looks through both walls at a glancing angle, which is the rim, and
|
|
53
|
+
* lightest through the middle where the far wall is turned away. That is one
|
|
54
|
+
* gradient, and it says the shell is a solid the same way a hundred and sixty
|
|
55
|
+
* stacked polygons did.
|
|
56
|
+
* @param props - See {@link CloudShellLayerProps}.
|
|
57
|
+
* @returns The shells.
|
|
58
|
+
*/
|
|
59
|
+
export const CloudShellLayer = memo(function CloudShellLayer(
|
|
60
|
+
props: CloudShellLayerProps,
|
|
61
|
+
): ReactElement {
|
|
62
|
+
const { shells, camera, viewport, fillOpacity = SHELL_FILL_OPACITY } = props;
|
|
63
|
+
const prefix = useId();
|
|
64
|
+
|
|
65
|
+
const drawn = [];
|
|
66
|
+
for (const shell of shells) {
|
|
67
|
+
const outline = projectShellOutline(shell.ellipsoid, camera, viewport);
|
|
68
|
+
if (outline === null) continue;
|
|
69
|
+
drawn.push({ shell, outline });
|
|
70
|
+
}
|
|
71
|
+
// Furthest first, so a shell in front of another is drawn over it.
|
|
72
|
+
drawn.sort((a, b) => a.outline.depth - b.outline.depth);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<g data-layer="shells" pointerEvents="none">
|
|
76
|
+
{drawn.map(({ shell, outline }) => {
|
|
77
|
+
const paint = `${prefix}${shell.id}`;
|
|
78
|
+
return (
|
|
79
|
+
<g key={shell.id} data-shell={shell.id}>
|
|
80
|
+
<defs>
|
|
81
|
+
<radialGradient id={paint}>
|
|
82
|
+
{shellGlassStops((shell.opacity ?? 1) * fillOpacity).map(
|
|
83
|
+
(stop) => (
|
|
84
|
+
<stop
|
|
85
|
+
key={stop.offset}
|
|
86
|
+
offset={stop.offset}
|
|
87
|
+
stopColor={shell.color}
|
|
88
|
+
stopOpacity={stop.opacity}
|
|
89
|
+
/>
|
|
90
|
+
),
|
|
91
|
+
)}
|
|
92
|
+
</radialGradient>
|
|
93
|
+
</defs>
|
|
94
|
+
<ellipse
|
|
95
|
+
cx={outline.cx}
|
|
96
|
+
cy={outline.cy}
|
|
97
|
+
rx={outline.rx}
|
|
98
|
+
ry={outline.ry}
|
|
99
|
+
transform={`rotate(${outline.angle} ${outline.cx} ${outline.cy})`}
|
|
100
|
+
fill={`url(#${paint})`}
|
|
101
|
+
/>
|
|
102
|
+
</g>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</g>
|
|
106
|
+
);
|
|
107
|
+
});
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import type { MouseEvent as ReactMouseEvent, ReactElement } from 'react';
|
|
2
|
+
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CHART_WHEEL_DWELL,
|
|
6
|
+
useWheelZoom,
|
|
7
|
+
} from '../../chart/ui/useWheelZoom.ts';
|
|
8
|
+
import type { ScatterSelectionMode } from '../../scatter/core/scatterSelection.ts';
|
|
9
|
+
import { nearestPointIndex } from '../../scatter/core/screenPoints.ts';
|
|
10
|
+
import { ScatterLabelLayer } from '../../scatter/ui/ScatterLabelLayer.tsx';
|
|
11
|
+
import { ScatterMarkLayer } from '../../scatter/ui/ScatterMarkLayer.tsx';
|
|
12
|
+
import { surfacePosition } from '../../scatter/ui/lassoGesture.ts';
|
|
13
|
+
import {
|
|
14
|
+
SCATTER_GROUP_LABEL_SIZE,
|
|
15
|
+
SCATTER_LABEL_SIZE,
|
|
16
|
+
} from '../../scatter/ui/scatterLabelBoxes.ts';
|
|
17
|
+
import { placeScatterLabels } from '../../scatter/ui/scatterLabelPlacement.ts';
|
|
18
|
+
import {
|
|
19
|
+
scatterGroupLabels,
|
|
20
|
+
scatterPointLabels,
|
|
21
|
+
} from '../../scatter/ui/scatterLabels.ts';
|
|
22
|
+
import {
|
|
23
|
+
LASSO_STYLE,
|
|
24
|
+
scatterGroupInk,
|
|
25
|
+
} from '../../scatter/ui/scatterPlotModel.ts';
|
|
26
|
+
import { useScatterInteraction } from '../../scatter/ui/useScatterInteraction.ts';
|
|
27
|
+
import { cubePoints } from '../core/cubePoints.ts';
|
|
28
|
+
import type { OrbitCamera } from '../core/orbitCamera.ts';
|
|
29
|
+
import { DEFAULT_ORBIT_CAMERA } from '../core/orbitCamera.ts';
|
|
30
|
+
|
|
31
|
+
import { CloudFrameLayer } from './CloudFrameLayer.tsx';
|
|
32
|
+
import { CloudPointLayer } from './CloudPointLayer.tsx';
|
|
33
|
+
import type { CloudShell } from './CloudShellLayer.tsx';
|
|
34
|
+
import { CloudShellLayer } from './CloudShellLayer.tsx';
|
|
35
|
+
import { cloudView, projectCloud } from './cloudGeometry.ts';
|
|
36
|
+
import { clampCloudZoom } from './cloudZoom.ts';
|
|
37
|
+
import { cloudLabel, cloudShells } from './scatterCloudModel.ts';
|
|
38
|
+
import type { ScatterCloudProps } from './scatterCloudProps.ts';
|
|
39
|
+
import { useOrbitDrag } from './useOrbitDrag.ts';
|
|
40
|
+
|
|
41
|
+
export type { CloudGesture, ScatterCloudProps } from './scatterCloudProps.ts';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A cloud of samples in a box the reader can turn, with a glass shell around
|
|
45
|
+
* each group.
|
|
46
|
+
*
|
|
47
|
+
* It is the flat scatter's twin and shares with it everything it can — the
|
|
48
|
+
* selection, the lasso, the hover card, the labels and the group colours are
|
|
49
|
+
* the same hooks and the same helpers — because a reader who has learned the
|
|
50
|
+
* map must not have to learn this separately. What it cannot share is the
|
|
51
|
+
* frame: three axes seen from an angle have no room for three sets of tick
|
|
52
|
+
* labels, so the axes are all stretched to one cube, the frame names them, and
|
|
53
|
+
* the numbers stay in the card the pointer raises.
|
|
54
|
+
*
|
|
55
|
+
* It draws in SVG rather than in WebGL, and that is a decision about saving
|
|
56
|
+
* rather than about drawing. Every other figure in this package is saved from
|
|
57
|
+
* the bar above it as an SVG, and a canvas laid over the same picture would
|
|
58
|
+
* leave a hole in that file exactly where the shells are.
|
|
59
|
+
* @param props - See {@link ScatterCloudProps}.
|
|
60
|
+
* @returns The cloud.
|
|
61
|
+
*/
|
|
62
|
+
export function ScatterCloud(props: ScatterCloudProps): ReactElement {
|
|
63
|
+
const { x, y, z, width, height, xLabel, yLabel, zLabel } = props;
|
|
64
|
+
const { groupOf, groups, mutedGroups, ellipsoid = null } = props;
|
|
65
|
+
const { ellipsoidMinimumPoints, ellipsoidFillOpacity } = props;
|
|
66
|
+
const { showGroupLabels = false, pointLabels, pointRadius = 3.5 } = props;
|
|
67
|
+
const { outlinedFrom, selected, defaultSelected, onSelectionChange } = props;
|
|
68
|
+
const { selectMode = 'replace', onHoverChange, overlay } = props;
|
|
69
|
+
const { onPointDoubleClick } = props;
|
|
70
|
+
const { gesture = 'turn', wheelZoom = true, label, testId } = props;
|
|
71
|
+
|
|
72
|
+
const [ownCamera, setOwnCamera] = useState<OrbitCamera>(DEFAULT_ORBIT_CAMERA);
|
|
73
|
+
const [ownZoom, setOwnZoom] = useState(1);
|
|
74
|
+
const camera = props.camera ?? ownCamera;
|
|
75
|
+
const zoom = props.zoom ?? ownZoom;
|
|
76
|
+
const selecting = gesture === 'select';
|
|
77
|
+
|
|
78
|
+
const view = useMemo(
|
|
79
|
+
() => cloudView(width, height, zoom),
|
|
80
|
+
[height, width, zoom],
|
|
81
|
+
);
|
|
82
|
+
const cube = useMemo(() => cubePoints(x, y, z), [x, y, z]);
|
|
83
|
+
const cloud = useMemo(
|
|
84
|
+
() => projectCloud(cube, camera, view.viewport),
|
|
85
|
+
[camera, cube, view.viewport],
|
|
86
|
+
);
|
|
87
|
+
const ink = useMemo(
|
|
88
|
+
() => scatterGroupInk(groups, mutedGroups),
|
|
89
|
+
[groups, mutedGroups],
|
|
90
|
+
);
|
|
91
|
+
const shells = useMemo<readonly CloudShell[]>(
|
|
92
|
+
() =>
|
|
93
|
+
ellipsoid === null || groupOf === undefined || groups === undefined
|
|
94
|
+
? NO_SHELLS
|
|
95
|
+
: cloudShells({
|
|
96
|
+
cube,
|
|
97
|
+
groupOf,
|
|
98
|
+
groups,
|
|
99
|
+
ink,
|
|
100
|
+
size: ellipsoid,
|
|
101
|
+
minimumPoints: ellipsoidMinimumPoints,
|
|
102
|
+
}),
|
|
103
|
+
[cube, ellipsoid, ellipsoidMinimumPoints, groupOf, groups, ink],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const interaction = useScatterInteraction({
|
|
107
|
+
points: cloud.points,
|
|
108
|
+
selected,
|
|
109
|
+
defaultSelected,
|
|
110
|
+
onSelectionChange,
|
|
111
|
+
selectMode,
|
|
112
|
+
enabled: selecting,
|
|
113
|
+
touchLasso: selecting,
|
|
114
|
+
hoverRadius: pointRadius + HOVER_SLACK,
|
|
115
|
+
onHoverChange,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Everything a gesture callback reads is taken from here rather than from
|
|
119
|
+
// the closure, so a caller that owns the camera and answers with an inline
|
|
120
|
+
// arrow — which is how every caller writes it — does not rebuild the drag
|
|
121
|
+
// sixty times during its own turn.
|
|
122
|
+
const live = useRef({ camera, zoom, points: cloud.points, props });
|
|
123
|
+
useLayoutEffect(() => {
|
|
124
|
+
live.current = { camera, zoom, points: cloud.points, props };
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const { select, mask } = interaction.selection;
|
|
128
|
+
const { moveTo, leave, pin, unpin, hovered } = interaction.hover;
|
|
129
|
+
|
|
130
|
+
const onOrbit = useCallback((turn: (was: OrbitCamera) => OrbitCamera) => {
|
|
131
|
+
const next = turn(live.current.camera);
|
|
132
|
+
// Written back so the frames of one drag compose, rather than each of them
|
|
133
|
+
// turning the camera React has not re-rendered with yet.
|
|
134
|
+
live.current.camera = next;
|
|
135
|
+
if (live.current.props.camera === undefined) setOwnCamera(next);
|
|
136
|
+
live.current.props.onCameraChange?.(next);
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
const onTap = useCallback(
|
|
140
|
+
(tapX: number, tapY: number, mode: ScatterSelectionMode) => {
|
|
141
|
+
const radius = (live.current.props.pointRadius ?? 3.5) + HOVER_SLACK;
|
|
142
|
+
const index = nearestPointIndex(live.current.points, tapX, tapY, radius);
|
|
143
|
+
if (index === -1) {
|
|
144
|
+
unpin();
|
|
145
|
+
if (mode === 'replace') interaction.selection.clear();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
select([index], mode, 'point');
|
|
149
|
+
pin(index);
|
|
150
|
+
},
|
|
151
|
+
[interaction.selection, pin, select, unpin],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const onWheel = useCallback((_x: number, _y: number, factor: number) => {
|
|
155
|
+
// A factor above one widens a frame, which on a box means standing back.
|
|
156
|
+
const next = clampCloudZoom(live.current.zoom / factor);
|
|
157
|
+
live.current.zoom = next;
|
|
158
|
+
if (live.current.props.zoom === undefined) setOwnZoom(next);
|
|
159
|
+
live.current.props.onZoomChange?.(next);
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
function handleDoubleClick(event: ReactMouseEvent<SVGRectElement>): void {
|
|
163
|
+
if (onPointDoubleClick === undefined) return;
|
|
164
|
+
const at = surfacePosition(event, 0, 0);
|
|
165
|
+
const radius = (live.current.props.pointRadius ?? 3.5) + HOVER_SLACK;
|
|
166
|
+
const index = nearestPointIndex(live.current.points, at.x, at.y, radius);
|
|
167
|
+
if (index === -1) return;
|
|
168
|
+
onPointDoubleClick({
|
|
169
|
+
index,
|
|
170
|
+
x: at.x,
|
|
171
|
+
y: at.y,
|
|
172
|
+
clientX: event.clientX,
|
|
173
|
+
clientY: event.clientY,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const orbit = useOrbitDrag({
|
|
178
|
+
enabled: !selecting,
|
|
179
|
+
onOrbit,
|
|
180
|
+
onTap,
|
|
181
|
+
onMove: moveTo,
|
|
182
|
+
onLeave: leave,
|
|
183
|
+
mode: selectMode,
|
|
184
|
+
});
|
|
185
|
+
const { ref: wheelSurface } = useWheelZoom<SVGRectElement>({
|
|
186
|
+
enabled: wheelZoom,
|
|
187
|
+
delay: CHART_WHEEL_DWELL,
|
|
188
|
+
onZoom: onWheel,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const named = useMemo(
|
|
192
|
+
() =>
|
|
193
|
+
pointLabels === undefined
|
|
194
|
+
? undefined
|
|
195
|
+
: placeScatterLabels(
|
|
196
|
+
scatterPointLabels(cloud.points, pointLabels, {
|
|
197
|
+
groupOf,
|
|
198
|
+
colors: ink.colors,
|
|
199
|
+
}),
|
|
200
|
+
{
|
|
201
|
+
fontSize: SCATTER_LABEL_SIZE,
|
|
202
|
+
radius: pointRadius,
|
|
203
|
+
bounds: view.rect,
|
|
204
|
+
},
|
|
205
|
+
),
|
|
206
|
+
[cloud.points, groupOf, ink.colors, pointLabels, pointRadius, view.rect],
|
|
207
|
+
);
|
|
208
|
+
const crowds = useMemo(
|
|
209
|
+
() =>
|
|
210
|
+
showGroupLabels && groups !== undefined
|
|
211
|
+
? placeScatterLabels(
|
|
212
|
+
scatterGroupLabels(
|
|
213
|
+
cloud.points,
|
|
214
|
+
groups.map((group) => group.label),
|
|
215
|
+
{ groupOf, colors: ink.colors },
|
|
216
|
+
),
|
|
217
|
+
{
|
|
218
|
+
fontSize: SCATTER_GROUP_LABEL_SIZE,
|
|
219
|
+
bold: true,
|
|
220
|
+
centered: true,
|
|
221
|
+
radius: pointRadius,
|
|
222
|
+
bounds: view.rect,
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
: undefined,
|
|
226
|
+
[
|
|
227
|
+
cloud.points,
|
|
228
|
+
groupOf,
|
|
229
|
+
groups,
|
|
230
|
+
ink.colors,
|
|
231
|
+
pointRadius,
|
|
232
|
+
showGroupLabels,
|
|
233
|
+
view.rect,
|
|
234
|
+
],
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div
|
|
239
|
+
data-testid={testId}
|
|
240
|
+
style={{ position: 'relative', width, height }}
|
|
241
|
+
role="img"
|
|
242
|
+
aria-label={label ?? cloudLabel(xLabel, yLabel, zLabel, cube.length)}
|
|
243
|
+
>
|
|
244
|
+
<svg width={width} height={height} style={SVG_STYLE}>
|
|
245
|
+
<CloudFrameLayer
|
|
246
|
+
camera={camera}
|
|
247
|
+
viewport={view.viewport}
|
|
248
|
+
xLabel={xLabel}
|
|
249
|
+
yLabel={yLabel}
|
|
250
|
+
zLabel={zLabel}
|
|
251
|
+
/>
|
|
252
|
+
<CloudShellLayer
|
|
253
|
+
shells={shells}
|
|
254
|
+
camera={camera}
|
|
255
|
+
viewport={view.viewport}
|
|
256
|
+
fillOpacity={ellipsoidFillOpacity}
|
|
257
|
+
/>
|
|
258
|
+
<CloudPointLayer
|
|
259
|
+
points={cloud.points}
|
|
260
|
+
depths={cloud.depths}
|
|
261
|
+
order={cloud.order}
|
|
262
|
+
groupOf={groupOf}
|
|
263
|
+
colors={ink.colors}
|
|
264
|
+
opacities={ink.opacities}
|
|
265
|
+
radius={pointRadius}
|
|
266
|
+
outlinedFrom={outlinedFrom}
|
|
267
|
+
/>
|
|
268
|
+
<ScatterMarkLayer
|
|
269
|
+
points={cloud.points}
|
|
270
|
+
selected={mask}
|
|
271
|
+
hovered={hovered}
|
|
272
|
+
radius={pointRadius}
|
|
273
|
+
/>
|
|
274
|
+
{named === undefined ? null : (
|
|
275
|
+
<ScatterLabelLayer labels={named} radius={pointRadius} />
|
|
276
|
+
)}
|
|
277
|
+
{crowds === undefined ? null : (
|
|
278
|
+
<ScatterLabelLayer labels={crowds} radius={pointRadius} strong />
|
|
279
|
+
)}
|
|
280
|
+
{interaction.lasso.pathData === '' ? null : (
|
|
281
|
+
<path d={interaction.lasso.pathData} {...LASSO_STYLE} />
|
|
282
|
+
)}
|
|
283
|
+
<rect
|
|
284
|
+
x={0}
|
|
285
|
+
y={0}
|
|
286
|
+
width={Math.max(0, width)}
|
|
287
|
+
height={Math.max(0, height)}
|
|
288
|
+
ref={wheelSurface}
|
|
289
|
+
fill="transparent"
|
|
290
|
+
onDoubleClick={handleDoubleClick}
|
|
291
|
+
{...(selecting ? interaction.surface : orbit.surface)}
|
|
292
|
+
/>
|
|
293
|
+
</svg>
|
|
294
|
+
{overlay}
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* How much further than a dot's own radius the pointer may be and still catch
|
|
301
|
+
* it.
|
|
302
|
+
*
|
|
303
|
+
* More slack than the map allows, because a dot in a cloud is a smaller target
|
|
304
|
+
* than the same dot on a map: the box is turned, so the dots are packed into a
|
|
305
|
+
* hexagon rather than spread over a rectangle, and the ones at the back are
|
|
306
|
+
* drawn smaller still.
|
|
307
|
+
*/
|
|
308
|
+
const HOVER_SLACK = 8.5;
|
|
309
|
+
|
|
310
|
+
const NO_SHELLS: readonly CloudShell[] = [];
|
|
311
|
+
|
|
312
|
+
const SVG_STYLE = { display: 'block', touchAction: 'none' } as const;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the cube, the cloud and the shells land on screen for one camera.
|
|
3
|
+
*
|
|
4
|
+
* Everything a cloud draws is cut once, in cube units, and re-projected on
|
|
5
|
+
* every turn — so an orbit costs one multiply-add per vertex and nothing else.
|
|
6
|
+
* That is what keeps a drag smooth on a figure holding a dozen shells of a
|
|
7
|
+
* hundred and sixty faces each, and it is why the cutting and the projecting
|
|
8
|
+
* are two steps rather than one.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ScreenPoints } from '../../scatter/core/screenPoints.ts';
|
|
12
|
+
import type { ConfidenceEllipsoid } from '../core/confidenceEllipsoid.ts';
|
|
13
|
+
import { ellipsoidSilhouette } from '../core/ellipsoidSilhouette.ts';
|
|
14
|
+
import type {
|
|
15
|
+
OrbitCamera,
|
|
16
|
+
OrbitViewport,
|
|
17
|
+
Vector3,
|
|
18
|
+
} from '../core/orbitCamera.ts';
|
|
19
|
+
import { projectPoint } from '../core/orbitCamera.ts';
|
|
20
|
+
|
|
21
|
+
/** Room kept between the figure's edge and the cube, in pixels. */
|
|
22
|
+
export const CLOUD_MARGIN = 16;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* How much of the shorter side the cube spans before the reader zooms.
|
|
26
|
+
*
|
|
27
|
+
* Well short of the whole: the cube is drawn turned, so its silhouette is a
|
|
28
|
+
* hexagon as wide as its body diagonal, and a cube sized to the box edge-on
|
|
29
|
+
* would push two of its corners past the frame the moment it was turned.
|
|
30
|
+
*/
|
|
31
|
+
export const CLOUD_FILL = 0.62;
|
|
32
|
+
|
|
33
|
+
/** The rectangle the cloud is drawn in, and how the cube maps into it. */
|
|
34
|
+
export interface CloudView {
|
|
35
|
+
/** The drawing area, ready to spread onto a `<rect>`. */
|
|
36
|
+
rect: { x: number; y: number; width: number; height: number };
|
|
37
|
+
/** Where the middle of the cube sits, and how large it is drawn. */
|
|
38
|
+
viewport: OrbitViewport;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The drawing area and the cube's place in it.
|
|
43
|
+
* @param width - Total width of the figure, in pixels.
|
|
44
|
+
* @param height - Total height.
|
|
45
|
+
* @param zoom - What the reader has zoomed to; `1` is the resting size.
|
|
46
|
+
* @returns See {@link CloudView}.
|
|
47
|
+
*/
|
|
48
|
+
export function cloudView(
|
|
49
|
+
width: number,
|
|
50
|
+
height: number,
|
|
51
|
+
zoom: number,
|
|
52
|
+
): CloudView {
|
|
53
|
+
const inner = Math.max(0, width - CLOUD_MARGIN * 2);
|
|
54
|
+
const tall = Math.max(0, height - CLOUD_MARGIN * 2);
|
|
55
|
+
const factor = Number.isFinite(zoom) && zoom > 0 ? zoom : 1;
|
|
56
|
+
return {
|
|
57
|
+
rect: { x: CLOUD_MARGIN, y: CLOUD_MARGIN, width: inner, height: tall },
|
|
58
|
+
viewport: {
|
|
59
|
+
scale: (Math.min(inner, tall) / 2) * CLOUD_FILL * factor,
|
|
60
|
+
centerX: CLOUD_MARGIN + inner / 2,
|
|
61
|
+
centerY: CLOUD_MARGIN + tall / 2,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Where every sample landed, and in what order they are painted. */
|
|
67
|
+
export interface CloudCloud {
|
|
68
|
+
/** The positions, in the same space a gesture arrives in. */
|
|
69
|
+
points: ScreenPoints;
|
|
70
|
+
/** How far towards the reader each one is, in cube units. */
|
|
71
|
+
depths: Float64Array;
|
|
72
|
+
/** The rows, furthest first, which is the order they are drawn in. */
|
|
73
|
+
order: Uint32Array;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Every sample, projected and ordered back to front.
|
|
78
|
+
*
|
|
79
|
+
* The order is what makes the cloud read as a solid: a dot at the front drawn
|
|
80
|
+
* over a dot at the back says which is which, where the same two dots in row
|
|
81
|
+
* order say nothing at all. It is a separate array rather than a sorted copy
|
|
82
|
+
* of the points because the hit test, the labels and the selection all index
|
|
83
|
+
* by row and must not be reordered with the painting.
|
|
84
|
+
* @param cube - Every sample, in cube units.
|
|
85
|
+
* @param camera - Where the reader is standing.
|
|
86
|
+
* @param viewport - Where the middle of the cube is, and how large it is drawn.
|
|
87
|
+
* @returns See {@link CloudCloud}.
|
|
88
|
+
*/
|
|
89
|
+
export function projectCloud(
|
|
90
|
+
cube: readonly Vector3[],
|
|
91
|
+
camera: OrbitCamera,
|
|
92
|
+
viewport: OrbitViewport,
|
|
93
|
+
): CloudCloud {
|
|
94
|
+
const count = cube.length;
|
|
95
|
+
const x = new Float64Array(count);
|
|
96
|
+
const y = new Float64Array(count);
|
|
97
|
+
const depths = new Float64Array(count);
|
|
98
|
+
for (let index = 0; index < count; index++) {
|
|
99
|
+
const at = projectPoint(cube[index] as Vector3, camera, viewport);
|
|
100
|
+
x[index] = at.x;
|
|
101
|
+
y[index] = at.y;
|
|
102
|
+
depths[index] = at.depth;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const rows = new Uint32Array(count);
|
|
106
|
+
for (let index = 0; index < count; index++) rows[index] = index;
|
|
107
|
+
const order = rows.toSorted(
|
|
108
|
+
(a, b) => (depths[a] as number) - (depths[b] as number),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return { points: { x, y }, depths, order };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** One shell's outline, ready to be filled. */
|
|
115
|
+
export interface CloudOutline {
|
|
116
|
+
/** Where its middle landed, in pixels from the left. */
|
|
117
|
+
cx: number;
|
|
118
|
+
/** And from the top. */
|
|
119
|
+
cy: number;
|
|
120
|
+
/** Half its longest width, in pixels. */
|
|
121
|
+
rx: number;
|
|
122
|
+
/** Half its width across that, in pixels. */
|
|
123
|
+
ry: number;
|
|
124
|
+
/** How far the long axis is turned from the horizontal, in degrees. */
|
|
125
|
+
angle: number;
|
|
126
|
+
/** How far towards the reader its middle is, in cube units. */
|
|
127
|
+
depth: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Where one shell's outline lands on screen.
|
|
132
|
+
* @param ellipsoid - The shell, in cube units.
|
|
133
|
+
* @param camera - Where the reader is standing.
|
|
134
|
+
* @param viewport - Where the middle of the cube is, and how large it is drawn.
|
|
135
|
+
* @returns See {@link CloudOutline}, or `null` when the camera sends the shell nowhere a browser can draw.
|
|
136
|
+
*/
|
|
137
|
+
export function projectShellOutline(
|
|
138
|
+
ellipsoid: ConfidenceEllipsoid,
|
|
139
|
+
camera: OrbitCamera,
|
|
140
|
+
viewport: OrbitViewport,
|
|
141
|
+
): CloudOutline | null {
|
|
142
|
+
const at = projectPoint(ellipsoid.center, camera, viewport);
|
|
143
|
+
const { rx, ry, angle } = ellipsoidSilhouette(ellipsoid, camera);
|
|
144
|
+
const outline = {
|
|
145
|
+
cx: round(at.x),
|
|
146
|
+
cy: round(at.y),
|
|
147
|
+
rx: round(rx * viewport.scale),
|
|
148
|
+
ry: round(ry * viewport.scale),
|
|
149
|
+
angle: round((angle * 180) / Math.PI),
|
|
150
|
+
depth: at.depth,
|
|
151
|
+
};
|
|
152
|
+
for (const value of [outline.cx, outline.cy, outline.rx, outline.ry]) {
|
|
153
|
+
if (!Number.isFinite(value)) return null;
|
|
154
|
+
}
|
|
155
|
+
return outline;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A segment of the frame, in pixels. */
|
|
159
|
+
export interface CloudSegment {
|
|
160
|
+
/** Where it starts. */
|
|
161
|
+
x1: number;
|
|
162
|
+
/** Where it starts. */
|
|
163
|
+
y1: number;
|
|
164
|
+
/** Where it ends. */
|
|
165
|
+
x2: number;
|
|
166
|
+
/** Where it ends. */
|
|
167
|
+
y2: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A run of cube-space segments, projected.
|
|
172
|
+
* @param segments - The segments, as pairs of corners in cube units.
|
|
173
|
+
* @param camera - Where the reader is standing.
|
|
174
|
+
* @param viewport - Where the middle of the cube is, and how large it is drawn.
|
|
175
|
+
* @returns The segments, in pixels, in the order they were given.
|
|
176
|
+
*/
|
|
177
|
+
export function projectSegments(
|
|
178
|
+
segments: ReadonlyArray<readonly [Vector3, Vector3]>,
|
|
179
|
+
camera: OrbitCamera,
|
|
180
|
+
viewport: OrbitViewport,
|
|
181
|
+
): CloudSegment[] {
|
|
182
|
+
const lines: CloudSegment[] = [];
|
|
183
|
+
for (const [from, to] of segments) {
|
|
184
|
+
const start = projectPoint(from, camera, viewport);
|
|
185
|
+
const end = projectPoint(to, camera, viewport);
|
|
186
|
+
lines.push({
|
|
187
|
+
x1: round(start.x),
|
|
188
|
+
y1: round(start.y),
|
|
189
|
+
x2: round(end.x),
|
|
190
|
+
y2: round(end.y),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return lines;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
* Two decimals, which is under a tenth of a device pixel at any zoom a browser
|
|
198
|
+
* offers and keeps the markup short enough to read in a failing test.
|
|
199
|
+
*/
|
|
200
|
+
const round = (value: number): number => Math.round(value * 100) / 100;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How solid the glass is, and how it thickens from the middle to the rim.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* How solid one wall of the glass is, seen square on, by default.
|
|
7
|
+
*
|
|
8
|
+
* Low, because a ray straight through a shell already meets two walls, and two
|
|
9
|
+
* shells that overlap put four in the reader's way.
|
|
10
|
+
*/
|
|
11
|
+
export const SHELL_FILL_OPACITY = 0.1;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* How many walls' worth of glass the rim is allowed to reach.
|
|
15
|
+
*
|
|
16
|
+
* The path through a shell grows as the secant of the angle it is seen at, so
|
|
17
|
+
* at the silhouette it is unbounded and the rim would go opaque. Held to a few
|
|
18
|
+
* walls it reads as the swell of a ball; unheld it reads as a ring drawn round
|
|
19
|
+
* a disc, which is the wireframe this fill replaced.
|
|
20
|
+
*/
|
|
21
|
+
const RIM_WALLS = 6;
|
|
22
|
+
|
|
23
|
+
/** One stop of the glass gradient. */
|
|
24
|
+
export interface ShellGlassStop {
|
|
25
|
+
/** How far out it sits, from `0` at the middle to `1` at the rim. */
|
|
26
|
+
offset: number;
|
|
27
|
+
/** How solid the glass is there. */
|
|
28
|
+
opacity: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The gradient one shell is filled with, which is what gives it its relief.
|
|
33
|
+
*
|
|
34
|
+
* A shell is a surface, not a filled solid, so what the reader looks through
|
|
35
|
+
* is two walls of it. Straight through the middle they are met square on and
|
|
36
|
+
* the glass is at its thinnest; towards the rim the same two walls are met at
|
|
37
|
+
* a closing angle and the path through them lengthens as the secant of it, so
|
|
38
|
+
* the shell swells towards its edge and reads as round. That is the whole of
|
|
39
|
+
* the modelling the tessellated version did, minus the seams between the faces
|
|
40
|
+
* it needed to do it.
|
|
41
|
+
*
|
|
42
|
+
* The walls are composited rather than added, so two of them at a tenth are
|
|
43
|
+
* not a fifth but a nineteenth-something — the same arithmetic the browser was
|
|
44
|
+
* doing when they were stacked polygons.
|
|
45
|
+
* @param strength - How solid one wall is, after the group's own opacity.
|
|
46
|
+
* @returns The stops, from the middle outwards.
|
|
47
|
+
*/
|
|
48
|
+
export function shellGlassStops(strength: number): ShellGlassStop[] {
|
|
49
|
+
const wall = Math.min(1, Math.max(0, strength));
|
|
50
|
+
const clear = 1 - wall;
|
|
51
|
+
return GLASS_OFFSETS.map((offset) => ({
|
|
52
|
+
offset,
|
|
53
|
+
opacity: round(1 - clear ** wallsAt(offset)),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* How much glass a ray meets at a given distance out from the middle.
|
|
59
|
+
* @param offset - How far out, from `0` at the middle to `1` at the rim.
|
|
60
|
+
* @returns The number of walls' worth, held to {@link RIM_WALLS}.
|
|
61
|
+
*/
|
|
62
|
+
function wallsAt(offset: number): number {
|
|
63
|
+
const facing = Math.sqrt(Math.max(0, 1 - offset * offset));
|
|
64
|
+
if (facing <= 0) return RIM_WALLS;
|
|
65
|
+
return Math.min(RIM_WALLS, 2 / facing);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* Close together near the rim, where the glass thickens quickest, and sparse
|
|
70
|
+
* through the middle, where it barely changes.
|
|
71
|
+
*/
|
|
72
|
+
const GLASS_OFFSETS = [0, 0.3, 0.5, 0.66, 0.78, 0.87, 0.93, 0.97, 1];
|
|
73
|
+
|
|
74
|
+
const round = (value: number): number => Math.round(value * 10000) / 10000;
|