@graphty/graphty-element 2.0.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +3 -3
  2. package/dist/ai.js +4 -4
  3. package/dist/catalog.js +5 -5
  4. package/dist/chunks/{AiManager-awS0MNr1.js → AiManager-Dp1mOkPm.js} +5 -5
  5. package/dist/chunks/{Algorithm-BdcqHKps.js → Algorithm-RQ629NLb.js} +210 -92
  6. package/dist/chunks/{DataSource-DK4GZBKg.js → DataSource-DEg3igzS.js} +3 -3
  7. package/dist/chunks/{GraphSession-D87eymV8.js → GraphSession-D3AV8tpF.js} +2527 -2248
  8. package/dist/chunks/{GraphtyError-B3eKs4yg.js → GraphtyError-BwcnblTH.js} +10 -8
  9. package/dist/chunks/{GraphtyLogger-CK03SnJi.js → GraphtyLogger-5KEttFUo.js} +1 -1
  10. package/dist/chunks/{NodeStyle-CS7dj20m.js → NodeStyle-Cup9O1lu.js} +2 -1
  11. package/dist/chunks/{VoiceInputAdapter-CcnCmxo5.js → VoiceInputAdapter-Bbze1r8k.js} +1 -1
  12. package/dist/chunks/{XRPivotCameraController-UjMsbran.js → XRPivotCameraController-8Il3KxFm.js} +2 -2
  13. package/dist/chunks/{cameras-BeIAlMWR.js → cameras-CeJYi-Na.js} +17 -18
  14. package/dist/chunks/{capability-check-DcKFwS6q.js → capability-check-EUCOfQLP.js} +1 -1
  15. package/dist/chunks/{detect-B-YbbP7i.js → detect-CqN0mC6u.js} +3 -3
  16. package/dist/chunks/{format-detection-DohSEZnb.js → format-detection-C8vbCSlS.js} +1 -1
  17. package/dist/chunks/{index-uohvGegX.js → index-Cz47b_vU.js} +1957 -1318
  18. package/dist/chunks/{paletteRegistry-B-oS4YxP.js → paletteRegistry-NrWOKT-A.js} +17 -17
  19. package/dist/chunks/{registry-DU-e49Y2.js → registry-CSba5QGJ.js} +1 -1
  20. package/dist/chunks/{scales-DvGid5Ln.js → scales-ulKMZPfG.js} +2206 -1357
  21. package/dist/custom-elements.json +1 -1
  22. package/dist/extend.js +26 -27
  23. package/dist/graphty-catalog.json +7 -4
  24. package/dist/graphty.bundle.js +36572 -34379
  25. package/dist/graphty.js +66 -64
  26. package/dist/index.d.ts +1 -1
  27. package/dist/logging.js +2 -2
  28. package/dist/schema.js +1 -1
  29. package/dist/session.d.ts +2 -1
  30. package/dist/session.js +37 -33
  31. package/dist/src/Edge.d.ts +1 -1
  32. package/dist/src/Graph.d.ts +42 -8
  33. package/dist/src/Node.d.ts +3 -4
  34. package/dist/src/acceleration/AccelerationController.d.ts +27 -0
  35. package/dist/src/acceleration/index.d.ts +1 -1
  36. package/dist/src/acceleration/narrow.d.ts +56 -0
  37. package/dist/src/acceleration/types.d.ts +88 -7
  38. package/dist/src/algorithms/Algorithm.d.ts +96 -2
  39. package/dist/src/algorithms/BFSAlgorithm.d.ts +9 -0
  40. package/dist/src/algorithms/DijkstraAlgorithm.d.ts +2 -8
  41. package/dist/src/algorithms/PageRankAlgorithm.d.ts +8 -0
  42. package/dist/src/algorithms/utils/graphUtils.d.ts +13 -0
  43. package/dist/src/cameras/OrbitCameraController.d.ts +1 -0
  44. package/dist/src/catalog/layouts.d.ts +13 -1
  45. package/dist/src/config/GraphBehavior.d.ts +16 -0
  46. package/dist/src/config/GraphStyle.d.ts +1 -1
  47. package/dist/src/config/StyleTemplate.d.ts +10 -0
  48. package/dist/src/data/CSVDataSource.d.ts +1 -0
  49. package/dist/src/data/csv-variant-detection.d.ts +11 -0
  50. package/dist/src/errors/codes.d.ts +14 -1
  51. package/dist/src/events.d.ts +17 -4
  52. package/dist/src/graphty-element.d.ts +52 -4
  53. package/dist/src/layout/ForceAtlas2LayoutEngine.d.ts +28 -45
  54. package/dist/src/layout/LayoutEngine.d.ts +7 -7
  55. package/dist/src/layout/SimulationLayoutEngine.d.ts +458 -0
  56. package/dist/src/layout/SpringElectricalLayoutEngine.d.ts +32 -0
  57. package/dist/src/layout/SpringLayoutEngine.d.ts +22 -32
  58. package/dist/src/managers/DataManager.d.ts +2 -0
  59. package/dist/src/managers/EventManager.d.ts +7 -0
  60. package/dist/src/managers/GraphContext.d.ts +6 -0
  61. package/dist/src/managers/LabelDeclutter.d.ts +97 -0
  62. package/dist/src/managers/LayoutManager.d.ts +98 -4
  63. package/dist/src/managers/RenderManager.d.ts +7 -0
  64. package/dist/src/managers/UpdateManager.d.ts +1 -1
  65. package/dist/src/meshes/NodeEffects.d.ts +29 -5
  66. package/dist/src/meshes/RichTextLabel.d.ts +17 -0
  67. package/dist/src/session/catalog.d.ts +2 -2
  68. package/dist/src/session/query.d.ts +83 -0
  69. package/dist/src/session/runs/types.d.ts +10 -4
  70. package/dist/src/session/selection/targets.d.ts +20 -3
  71. package/dist/src/session/styles/StylesApi.d.ts +18 -0
  72. package/dist/src/session/styles/channels.d.ts +0 -2
  73. package/dist/src/session/styles/legend.d.ts +9 -0
  74. package/dist/src/session/styles/predicate.d.ts +11 -0
  75. package/dist/src/session/styles/sources.d.ts +16 -0
  76. package/dist/src/session/types.d.ts +38 -2
  77. package/dist/src/testing/fakeAccelerator.d.ts +323 -0
  78. package/dist/webgpu.d.ts +23 -1
  79. package/dist/webgpu.js +70 -32
  80. package/package.json +10 -8
  81. package/dist/chunks/types-Dwm9waL2.js +0 -7
@@ -0,0 +1,97 @@
1
+ import { type Camera, type Scene } from "@babylonjs/core";
2
+ import type { Node } from "../Node";
3
+ import type { GraphContext } from "./GraphContext";
4
+ /**
5
+ * Hides node labels that would be drawn on top of each other, when the graph's behaviour
6
+ * configuration asks for it (`labels.declutter`, off by default).
7
+ *
8
+ * Every node label is its own billboarded plane, so two labelled nodes that land near each other
9
+ * on screen draw their words over one another. The pass projects each drawn label's WORDS -- not
10
+ * its padded plane -- to a screen rectangle, orders the labels by priority -- a selected node
11
+ * first, then the node with more edges, then the node id so the answer is stable -- and keeps a
12
+ * label only if its rectangle meets no label already kept. Kept rectangles are bucketed in a
13
+ * screen grid, so each label is tested against its neighbours rather than against every label.
14
+ *
15
+ * THE PASS RUNS ONLY WHEN SOMETHING THAT DECIDES PLACEMENT CHANGED: the camera's view or
16
+ * projection, the viewport size, a label added, rebuilt or removed, a node moved, shown, hidden
17
+ * or (de)selected, an edge added or removed, or the setting itself. A still scene costs one cheap
18
+ * walk over the labelled nodes per frame and no projection at all.
19
+ *
20
+ * A label that loses is hidden with `isVisible`, never with `setEnabled` and never through a
21
+ * style. `setEnabled` is the visibility mask's switch (`Node.applyRenderState`), and a style is
22
+ * the reader's answer to what a node looks like; this is a placement decision, so it has its own
23
+ * switch. Turning the setting off shows every label again.
24
+ *
25
+ * ONE PER SCENE, created by the first node that draws a label, and kept on `scene.metadata`
26
+ * the way `NodeEffects` keeps the glow layer.
27
+ */
28
+ export declare class LabelDeclutter {
29
+ private readonly scene;
30
+ private readonly context;
31
+ /** How many full passes have run. Read by tests to prove a still scene is not re-measured. */
32
+ passes: number;
33
+ private readonly observer;
34
+ private readonly entries;
35
+ /** Reused every pass: the candidates in priority order. */
36
+ private readonly order;
37
+ /** Reused every pass: kept labels by grid cell. Emptied, not rebuilt. */
38
+ private readonly grid;
39
+ private columns;
40
+ private rows;
41
+ private dirty;
42
+ private wasOn;
43
+ private camera;
44
+ private readonly view;
45
+ private readonly projection;
46
+ private width;
47
+ private height;
48
+ private edgeVersion;
49
+ private degrees;
50
+ private readonly transform;
51
+ private readonly toPixels;
52
+ private readonly worldToPixels;
53
+ private readonly viewport;
54
+ private readonly right;
55
+ private readonly up;
56
+ private readonly centre;
57
+ private readonly corner;
58
+ private readonly centreOnScreen;
59
+ private readonly cornerOnScreen;
60
+ private constructor();
61
+ /**
62
+ * Make sure the scene has a declutter pass, and add a node that is drawing a label to it.
63
+ * @param scene - The scene the labels are drawn in.
64
+ * @param context - The graph: its configuration says whether to declutter, its edges give
65
+ * the degree order.
66
+ * @param node - The node drawing a label.
67
+ */
68
+ static track(scene: Scene, context: GraphContext, node: Node): void;
69
+ /** Stop running before frames. */
70
+ dispose(): void;
71
+ /** Before every frame: decide whether placement could have changed, and if so, place. */
72
+ run(): void;
73
+ /**
74
+ * One full pass: decide which labels are drawn.
75
+ * @param camera - The camera the frame is drawn from.
76
+ */
77
+ place(camera: Camera): void;
78
+ /**
79
+ * Whether anything that decides placement differs from what the last pass saw.
80
+ * @param camera - The camera the frame is drawn from.
81
+ * @returns True when the labels have to be placed again.
82
+ */
83
+ private changed;
84
+ /**
85
+ * Record what this pass sees of one node, and drop it if its label is gone.
86
+ * @param entry - The node.
87
+ * @returns Its label plane, or null when it has none.
88
+ */
89
+ private observe;
90
+ private remember;
91
+ private isClear;
92
+ private keep;
93
+ /** The setting was turned off: every label is drawn again. */
94
+ private showAll;
95
+ /** How many edges each node has, counted again only when an edge is added or removed. */
96
+ private refreshDegrees;
97
+ }
@@ -1,11 +1,37 @@
1
+ import type { SimulationType } from "@graphty/layout";
2
+ import type { GraphLayoutBehavior } from "../config/GraphBehavior";
1
3
  import type { Edge } from "../Edge";
2
4
  import { LayoutEngine } from "../layout/LayoutEngine";
5
+ import { type SimulationEngineOptions } from "../layout/SimulationLayoutEngine";
3
6
  import type { Node } from "../Node";
4
7
  import type { Styles } from "../Styles";
5
8
  import type { DataManager } from "./DataManager";
6
9
  import type { EventManager } from "./EventManager";
7
10
  import type { GraphContext } from "./GraphContext";
8
11
  import type { Manager } from "./interfaces";
12
+ /**
13
+ * Turn what the consumer asked a simulation layout for into what the bridge runs on.
14
+ *
15
+ * `scalingFactor` travels twice over, and deliberately. The simulation gets it as `scale`, where
16
+ * it sizes the seed and, in ForceAtlas2, the units the arrangement is computed in; the BRIDGE gets
17
+ * it as the radius it publishes that arrangement at, which is the only one of the two a reader
18
+ * sees in the picture. A layout that publishes no such control takes
19
+ * {@link DEFAULT_SCALING_FACTOR}. What the bridge is given is {@link publishedRadius}, every
20
+ * size multiplier the layout publishes rolled into one, because the refit fits the arrangement to
21
+ * that radius and leaves no other way for a reader to change how big the graph comes out.
22
+ *
23
+ * The four knobs the frame loop needs -- `iterationsPerStep`, `maxInFlight`, `settleThreshold` and
24
+ * `settleWindow` -- are NOT in any layout's published schema, because they are the same four for
25
+ * every simulation and belong to how the element drives one rather than to the arrangement it
26
+ * draws. They are read from the options a caller passed, where a per-layout value overrides the
27
+ * behaviour configuration, and `behavior.layout` is where a host sets them once for every layout.
28
+ * @param type - Which simulation is being configured.
29
+ * @param options - The merged layout options, as the consumer set them.
30
+ * @param behavior - `behavior.layout`, which the frame-loop knobs default from.
31
+ * @returns The resolved options.
32
+ * @throws A `ZodError` when an option is outside the range its published schema declares.
33
+ */
34
+ export declare function resolveSimulationOptions(type: SimulationType, options: Record<string, unknown>, behavior: GraphLayoutBehavior): SimulationEngineOptions;
9
35
  /**
10
36
  * Manages layout engines and their lifecycle
11
37
  * Coordinates layout updates and transitions
@@ -34,10 +60,27 @@ export declare class LayoutManager implements Manager {
34
60
  */
35
61
  get running(): boolean;
36
62
  /**
37
- * Sets the running state of the layout
63
+ * Sets the running state of the layout.
64
+ *
65
+ * Going false -> true is "play", and a simulation that had SETTLED does nothing when it is
66
+ * stepped, so a reader who pressed play -- or let go of a node they dragged -- would watch a
67
+ * still graph. Resuming therefore reheats a settled simulation: the settle count starts
68
+ * again and the next frame moves nodes. Going true -> false only stops `step()` being
69
+ * called; batches already in flight land in the position array by themselves, nothing is
70
+ * disposed and nothing is released.
38
71
  */
39
72
  set running(value: boolean);
40
73
  private graphContext;
74
+ /**
75
+ * The graph's acceleration controller, once a context has arrived. A manager built without a
76
+ * graph -- which is what the manager's own unit tests build -- never gets one, and a
77
+ * simulation layout refuses to start on one.
78
+ */
79
+ private acceleration;
80
+ /** How to stop listening to the controller. */
81
+ private unsubscribeAcceleration;
82
+ /** How to stop listening for a new snapshot. */
83
+ private unsubscribeSnapshot;
41
84
  /**
42
85
  * Creates an instance of LayoutManager
43
86
  * @param eventManager - Event manager for emitting layout events
@@ -47,9 +90,33 @@ export declare class LayoutManager implements Manager {
47
90
  constructor(eventManager: EventManager, dataManager: DataManager, styles: Styles);
48
91
  /**
49
92
  * Set the GraphContext for error reporting
93
+ *
94
+ * This is also where the acceleration controller first becomes reachable: the manager is
95
+ * built before the graph finishes constructing itself, so it cannot be handed one.
50
96
  * @param context - GraphContext instance
51
97
  */
52
98
  setGraphContext(context: GraphContext): void;
99
+ /**
100
+ * Rebuild a running simulation on whatever the controller holds now.
101
+ *
102
+ * Every transition reaches here: an accelerator attached, detached, lost or recovered. The
103
+ * try is load-bearing. Under the `required` policy a detach cannot reach a CPU simulation --
104
+ * the controller's `plan()` throws `E_NO_ACCELERATOR` instead, which is the point of the
105
+ * policy -- and the throw has to be REPORTED here rather than escaping into the controller's
106
+ * listener loop, whose own catch would log it and continue. The layout then stops, and the
107
+ * next transition that attaches an accelerator brings it back.
108
+ */
109
+ private onAccelerationChange;
110
+ /**
111
+ * Hand a running simulation the snapshot that has just replaced the one it was laying out.
112
+ * @param event - The freeze, carrying the snapshot every consumer must switch to.
113
+ */
114
+ private onSnapshotReplaced;
115
+ /**
116
+ * Report a failure that happened to a layout already running, on the element's error channel.
117
+ * @param error - Whatever was thrown.
118
+ */
119
+ private reportSimulationFailure;
53
120
  /**
54
121
  * Update the styles reference when a new style template is loaded
55
122
  * @param styles - New styles instance
@@ -103,6 +170,9 @@ export declare class LayoutManager implements Manager {
103
170
  * @param type - The layout that failed.
104
171
  * @param error - Whatever was thrown.
105
172
  * @param phase - What the element was doing, in a word that fits "the layout could not be ...".
173
+ * `"stepped"` is the one that happens to a layout already running: a simulation's batch
174
+ * rejected minutes after set-up finished, and calling that "initialised" reads as a failure
175
+ * to start.
106
176
  * @returns The error to throw.
107
177
  */
108
178
  private reportLayoutFailure;
@@ -120,7 +190,19 @@ export declare class LayoutManager implements Manager {
120
190
  * `preSteps` is what makes a screenshot of a physics layout the same picture twice: an
121
191
  * unstepped force layout is a graph in mid-flight, and how far it has flown depends on when
122
192
  * the picture was taken.
193
+ *
194
+ * A SIMULATION CANNOT BE STEPPED IN A TIGHT SYNCHRONOUS LOOP, which is why this is
195
+ * asynchronous and why both the path that spends the pre-steps when a layout is built and the
196
+ * path that pays owed ones later come through here rather than each writing their own loop. A
197
+ * simulation's step is a BATCH: an accelerated one is fire-and-forget, and a simulation that
198
+ * already has a few batches in flight returns the oldest of them instead of submitting
199
+ * another, so a loop that calls `step()` a thousand times without waiting has all but the
200
+ * first couple coalesced away and draws a graph the consumer asked to be far more arranged
201
+ * than it is -- silently, because a coalesced batch is not an error. So a simulation is
202
+ * awaited one chunk at a time, the chunk being the largest batch the GPU package takes, and
203
+ * every other engine keeps the plain loop, whose `step()` returns having done the work.
123
204
  * @param engine - the engine to settle.
205
+ * @returns A promise that resolves once every pre-step has been taken and published.
124
206
  */
125
207
  private spendPreSteps;
126
208
  /**
@@ -136,15 +218,27 @@ export declare class LayoutManager implements Manager {
136
218
  *
137
219
  * So the count is owed rather than spent, and paid here: on the first frame at which there is
138
220
  * a node to move. This runs inside the render loop's update, which is called before
139
- * `scene.render()`, so the steps really are taken before the frame is drawn -- and because it
140
- * waits for a node rather than for a particular call, it does not matter whether the data
141
- * arrived in one batch, in ten, or from a fetch that finished a second later.
221
+ * `scene.render()`, so an engine that steps on the spot really is stepped before the frame is
222
+ * drawn -- and because it waits for a node rather than for a particular call, it does not
223
+ * matter whether the data arrived in one batch, in ten, or from a fetch that finished a
224
+ * second later. A simulation's batches land over the next few frames instead, because no
225
+ * caller inside a render loop can wait for a device; what {@link LayoutManager.spendPreSteps}
226
+ * guarantees for one is that every owed iteration is computed rather than coalesced away.
142
227
  */
143
228
  private runPreStepsOnceThereIsSomethingToStep;
144
229
  /**
145
230
  * Step the layout engine forward
146
231
  */
147
232
  step(): void;
233
+ /**
234
+ * Step the layout ONCE, whatever the frame loop's multiplier is.
235
+ *
236
+ * The name is the difference a reader of `UpdateManager` needs: a simulation layout does its
237
+ * whole frame's work in one call -- the batch it submits computes `iterationsPerStep`
238
+ * iterations -- so stepping it `stepMultiplier` times a frame would queue work the device
239
+ * cannot retire.
240
+ */
241
+ stepBatch(): void;
148
242
  /**
149
243
  * Get node position from layout engine
150
244
  * @param node - Node to get position for
@@ -23,6 +23,13 @@ export declare class RenderManager implements Manager {
23
23
  private renderLoopActive;
24
24
  private updateCallback?;
25
25
  private resizeHandler;
26
+ /**
27
+ * Stands in for Babylon's own pointer handling, which calls preventDefault and then
28
+ * `canvas.focus()` on every pointer down and up. That focus call scrolls the host page to the
29
+ * canvas. This one does the same thing without scrolling.
30
+ * @param evt - The pointer down or up event on the canvas
31
+ */
32
+ private focusOnPointer;
26
33
  /**
27
34
  * Creates a new render manager for Babylon.js scene and rendering
28
35
  * @param canvas - HTML canvas element for rendering
@@ -330,7 +330,7 @@ export declare class UpdateManager implements Manager {
330
330
  /**
331
331
  * Update the graph for the current frame.
332
332
  *
333
- * The pass itself is {@link UpdateManager.runUpdatePass}; what is added here is the one
333
+ * The pass itself is the private `runUpdatePass()`; what is added here is the one
334
334
  * question a consumer cares about and the pass has several exits from -- whether the state it
335
335
  * leaves behind is a finished picture.
336
336
  */
@@ -114,14 +114,38 @@ export declare class NodeEffects {
114
114
  * material. Unfreezing to set `emissiveColor` would make every glowing style pay a material
115
115
  * re-bind per frame and would change the node's lit appearance as a side effect.
116
116
  *
117
- * KNOWN LIMIT, stated rather than hidden: `intensity` is a property of the LAYER, not of a
118
- * mesh, so with two glowing styles on screen the last one applied sets the strength for both.
119
- * Colour is per style (see {@link NodeEffects.resolveRenderedMesh}). Per-style strength needs
120
- * one layer per strength, which costs a full-screen pass each and was not worth it here.
117
+ * STRENGTH IS PER SOURCE MESH, like colour, through {@link NodeEffects.syncGlowStrengths}.
118
+ * It used to be written to `glowLayer.intensity`, which belongs to the one layer the scene
119
+ * shares, so the last glowing style applied set the strength of every glowing node.
121
120
  * @param mesh - The mesh to apply the effect to
122
121
  * @param effect - The effect configuration from the node style
123
122
  */
124
123
  static applyGlowEffect(mesh: AbstractMesh, effect: NodeStyleConfig["effect"] | undefined): void;
124
+ /**
125
+ * The glow strength each glowing source mesh asked for, kept on scene.metadata beside the
126
+ * glow colours.
127
+ * @param scene - The Babylon.js scene
128
+ * @returns The per-source-mesh strengths
129
+ */
130
+ private static glowStrengths;
131
+ /**
132
+ * Draw every glowing source mesh at its own strength, with one layer.
133
+ *
134
+ * Babylon multiplies a per-mesh `setEffectIntensity` into the glow colour as the glow map is
135
+ * drawn, and the layer's `intensity` scales the blurred result. The glow map is an 8-bit
136
+ * texture, so a per-mesh factor above 1 clamps and a strength of 3 would read the same as 1.
137
+ * So the layer carries the LARGEST strength on screen and each mesh carries its share of it,
138
+ * which is never above 1.
139
+ *
140
+ * Only source meshes that still draw a node count toward the largest strength. MeshCache
141
+ * never evicts a source mesh, so a strength that is no longer used (a slider dragged from 100
142
+ * back to 0.1) leaves a mesh with no instances behind; counted, it would hold the layer at 100
143
+ * and round the live glow away in the 8-bit map. It keeps its entry, because a node that goes
144
+ * back to that strength reuses the cached mesh. A disposed mesh is dropped.
145
+ * @param glowLayer - The scene's glow layer
146
+ * @param scene - The Babylon.js scene
147
+ */
148
+ private static syncGlowStrengths;
125
149
  /**
126
150
  * Remove a mesh from the highlight layer.
127
151
  *
@@ -149,7 +173,7 @@ export declare class NodeEffects {
149
173
  */
150
174
  static disposeHighlightLayer(scene: Scene): void;
151
175
  /**
152
- * Dispose the glow layer for a scene, and forget the per-style colours with it.
176
+ * Dispose the glow layer for a scene, and forget the per-style colours and strengths with it.
153
177
  *
154
178
  * The colour map is keyed by mesh uniqueId, so it MUST die with the layer: leaving it behind
155
179
  * would let a later mesh that happens to reuse a uniqueId inherit a dead style's glow colour.
@@ -42,6 +42,8 @@ export declare class RichTextLabel {
42
42
  private parsedContent;
43
43
  private actualDimensions;
44
44
  private contentArea;
45
+ /** The size of the words alone, in the same units as `actualDimensions`. */
46
+ private textSize;
45
47
  private totalBorderWidth;
46
48
  private pointerInfo;
47
49
  private _progressValue;
@@ -112,6 +114,21 @@ export declare class RichTextLabel {
112
114
  * @returns The Babylon.js mesh or null if not created
113
115
  */
114
116
  get labelMesh(): Mesh | null;
117
+ /**
118
+ * Where the words sit on the label's plane, without its margins, padding, borders or pointer.
119
+ *
120
+ * Each edge is a fraction of the plane: `left`/`right` across it from its left edge, and
121
+ * `top`/`bottom` down it from its top edge, so a label whose words fill it reads
122
+ * `{ left: 0, right: 1, top: 0, bottom: 1 }`. Two labels whose planes overlap only in their
123
+ * padding do not draw over each other's words; this is what tells them apart.
124
+ * @returns The words' rectangle as fractions of the plane.
125
+ */
126
+ get textBounds(): {
127
+ left: number;
128
+ right: number;
129
+ top: number;
130
+ bottom: number;
131
+ };
115
132
  /**
116
133
  * Gets the label's unique identifier
117
134
  * @returns The label ID
@@ -29,8 +29,8 @@
29
29
  * already been run here. So a session's catalogue is the shared tables with its own `metrics()`
30
30
  * closed over its own graph.
31
31
  *
32
- * The rest of the graph-dependent half -- resolved option bounds, expression validation -- still
33
- * waits on the query engine.
32
+ * The rest of the graph-dependent half -- resolved option bounds, expression validation -- is not
33
+ * wired to the session's query engine yet.
34
34
  */
35
35
  import type { AlgorithmDescriptor } from "../catalog/types";
36
36
  import { type MetricsSource } from "./metrics";
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @file The session's one query engine: `{ where }` predicates and text search.
3
+ *
4
+ * A scope, a selection, a visibility filter and an edge filter all accept an expression, and a
5
+ * selection also accepts typed text. Every one of them reaches THIS object, and this object
6
+ * evaluates an expression with the compiler the style layers use (`./styles/predicate`) over the
7
+ * selector source the style layers read (`./styles/sources`). That is the point of it: a layer
8
+ * selector and a scope with the same text match the same elements, because they are the same
9
+ * code reading the same values. A second evaluator would drift from the first in exactly the
10
+ * places nobody tests -- JMESPath truthiness, a column default, an edge's endpoints.
11
+ *
12
+ * A path nothing answers matches nothing and is REPORTED through `unresolvedPathsOf`, never
13
+ * swallowed: "0 matched" and "you misspelled the attribute" must not look the same.
14
+ *
15
+ * Nothing here reaches Babylon.js, Lit or the DOM.
16
+ */
17
+ import type { GraphSnapshot } from "@graphty/graph-format";
18
+ import type { EdgeId, NodeId, Path, Query } from "../catalog/types";
19
+ import type { SelectionMatch, SelectionSearchHit, SelectionTextMode } from "./selection";
20
+ import { type SelectorSource, type SelectorTarget } from "./styles/predicate";
21
+ /** Everything the engine reads. */
22
+ interface QueryEngineParts {
23
+ /**
24
+ * The snapshot the dense indices address, read on every query.
25
+ * @returns The snapshot as it stands now.
26
+ */
27
+ readonly snapshot: () => GraphSnapshot;
28
+ /** Where values are read: the same source the style layers read. */
29
+ readonly elements: Required<Pick<SelectorSource, "edgeIdOf" | "nodeIdOf">> & SelectorSource;
30
+ /**
31
+ * Whether this session answers a path for one kind of element. Absent, no path is reported.
32
+ * @param path - The path as the query spelled it.
33
+ * @param target - Which kind of element.
34
+ * @returns True when something in the session answers it.
35
+ */
36
+ readonly answers?: (path: Path, target: SelectorTarget) => boolean;
37
+ /**
38
+ * The node attribute paths a text search reads, beside the id.
39
+ * @returns The paths, such as `data.label`.
40
+ */
41
+ readonly searchPaths: () => readonly Path[];
42
+ }
43
+ /** The session's query engine. */
44
+ export interface QueryEngine {
45
+ /**
46
+ * The nodes an expression matches.
47
+ * @param where - The expression.
48
+ * @returns The node ids, in index order.
49
+ */
50
+ nodes(where: Query): NodeId[];
51
+ /**
52
+ * The edges an expression matches.
53
+ * @param where - The expression.
54
+ * @returns The edge ids, in index order.
55
+ */
56
+ edges(where: Query): EdgeId[];
57
+ /**
58
+ * Both halves an expression matches, and the paths it names that nothing answers.
59
+ * @param where - The expression.
60
+ * @returns The match.
61
+ */
62
+ select(where: Query): SelectionMatch;
63
+ /**
64
+ * The paths an expression names that no node and no edge in this session answers.
65
+ * @param where - The expression.
66
+ * @returns The unresolved paths.
67
+ */
68
+ unresolvedPathsOf(where: Query): readonly Path[];
69
+ /**
70
+ * The nodes a text search finds.
71
+ * @param text - What was typed.
72
+ * @param mode - How to match it.
73
+ * @returns The hits, in index order.
74
+ */
75
+ find(text: string, mode: SelectionTextMode): SelectionSearchHit[];
76
+ }
77
+ /**
78
+ * Build the query engine over a session's selector source.
79
+ * @param parts - The snapshot, the value source, the path directory and the searchable paths.
80
+ * @returns The engine.
81
+ */
82
+ export declare function createQueryEngine(parts: QueryEngineParts): QueryEngine;
83
+ export {};
@@ -465,10 +465,16 @@ export interface Run<T = RunResult> extends PromiseLike<T> {
465
465
  readonly shape: ResultShape;
466
466
  /** What qualifies the numbers. */
467
467
  readonly caveats: Caveats;
468
- /** The result, once there is one. Awaiting the run is the other way to get it. */
469
- readonly result?: T;
470
- /** Why it failed, when it failed. */
471
- readonly error?: GraphtyError;
468
+ /**
469
+ * The result, once there is one. Awaiting the run is the other way to get it.
470
+ *
471
+ * Spelled `?: T | undefined` rather than `?: T` because the implementation answers with a
472
+ * getter, and under a consumer's `exactOptionalPropertyTypes` a getter that can return
473
+ * undefined does not satisfy a property that can only be absent or present.
474
+ */
475
+ readonly result?: T | undefined;
476
+ /** Why it failed, when it failed. Optional-or-undefined for the reason {@link Run.result} gives. */
477
+ readonly error?: GraphtyError | undefined;
472
478
  /** The frozen, structured-cloneable snapshot of everything above. */
473
479
  readonly record: RunRecord;
474
480
  /** The journal entry this run's command wrote, or null until it lands. */
@@ -32,7 +32,15 @@ import type { EdgeId, NodeId, Path, Query, Scope } from "../../catalog/types";
32
32
  import type { ResultsApi, RunRef } from "../results/types";
33
33
  import { ElementMask, type MaskIdSpace } from "../scope/ElementMask";
34
34
  import type { ScopeResolver } from "../scope/ScopeApi";
35
- /** How a text target decides whether an element matched. */
35
+ /**
36
+ * How a text target decides whether an element matched.
37
+ *
38
+ * - `substring`: the id or an attribute value contains the text, ignoring case.
39
+ * - `exact`: the id or an attribute value is the text, exactly.
40
+ * - `regex`: the id or an attribute value matches the text as a regular expression.
41
+ * - `attribute`: the text is `key:value`, and the node's `key` (or its id, for `id`) is the value,
42
+ * ignoring case. A key no node carries is searched as plain substring text.
43
+ */
36
44
  export type SelectionTextMode = "substring" | "exact" | "regex" | "attribute";
37
45
  /** Which way a neighbourhood target follows an edge. */
38
46
  export type SelectionDirection = "in" | "out" | "all";
@@ -70,14 +78,23 @@ export interface NeighborhoodTarget {
70
78
  * second ranking that could disagree with the one on screen.
71
79
  */
72
80
  export type SelectionTarget = ElementIdTarget | NeighborhoodTarget
73
- /** Every element a predicate matches. */
81
+ /** Every element a predicate matches, narrowed to a scope when one is named. */
74
82
  | {
75
83
  readonly where: Query;
84
+ readonly scope?: Scope;
76
85
  }
77
- /** Every element a text search finds. */
86
+ /**
87
+ * Every node a text search finds, narrowed to a scope when one is named.
88
+ *
89
+ * With no `mode`, the text may carry one as a prefix: `exact:`, `regex:`, or `<attribute>:`
90
+ * (`id:a17`, `type:person`), and anything else is a case-insensitive substring search over
91
+ * the node's id and its attribute values. See {@link SelectionTextMode}. A leading `=` makes
92
+ * the rest an expression, exactly as `{ where }` reads it, which selects edges as well.
93
+ */
78
94
  | {
79
95
  readonly text: string;
80
96
  readonly mode?: SelectionTextMode;
97
+ readonly scope?: Scope;
81
98
  }
82
99
  /** A pasted list of ids, which may name nodes, edges, or nothing at all. */
83
100
  | {
@@ -494,6 +494,24 @@ export interface StylesSources {
494
494
  */
495
495
  readonly onChange?: (change: StyleChange) => void;
496
496
  }
497
+ /**
498
+ * What a highlight paints when its caller names no style.
499
+ *
500
+ * A highlight is drawn OVER the default indigo node, the default darkgrey edge and the whitesmoke
501
+ * background, so it is chosen against those three rather than on its own. Okabe-Ito vermilion is
502
+ * at least Delta E 16 from each of them at normal vision and under all three kinds of colour
503
+ * blindness, and 3.5:1 against the background. The blue this replaced was Delta E 13 from the
504
+ * default node and under 4 for tritanopia, so a route through default nodes disappeared into
505
+ * them. `test/catalog/default-palette-quality.test.ts` measures it.
506
+ *
507
+ * Colour alone is not enough for an edge: a thin line at the default width reads as a thin line
508
+ * whatever its colour, so a highlighted edge is also drawn three times as wide. A node gets no
509
+ * size, because a size here would flatten whatever size encoding the layers beneath it drew.
510
+ */
511
+ export declare const DEFAULT_HIGHLIGHT: {
512
+ readonly color: "#D55E00";
513
+ readonly edgeWidth: number;
514
+ };
497
515
  /**
498
516
  * Build the style stack one session holds.
499
517
  * @param sources - What a selector compiles against, the element's own layers, the scales, the
@@ -21,8 +21,6 @@
21
21
  * compile error rather than a silent no-op.
22
22
  * - `edge.curvature` is a switch, not an amount. The edge renderer offsets its control points by
23
23
  * a fixed fraction of the edge length, so "curve this edge" is the only thing it can be told.
24
- * - `node.glowStrength` is drawn, but Babylon keeps a glow's intensity on the LAYER rather than
25
- * on a mesh, so two glowing styles on screen share whichever strength was applied last.
26
24
  * - `node.outline` is a colour and no width, for the same reason in the same shape: the stroke
27
25
  * is drawn by a highlight LAYER whose blur size belongs to the layer, so every outline on
28
26
  * screen is one width. This is the sentence the element's natural-language layer reads out
@@ -184,6 +184,15 @@ export interface LegendSources {
184
184
  * @returns The words, or undefined when nothing in the session names that path.
185
185
  */
186
186
  readonly field?: (path: Path, target: SelectorTarget) => FieldWords | undefined;
187
+ /**
188
+ * Whether a layer above selects every element a layer below it selects.
189
+ *
190
+ * Absent, only a layer that selects everything is known to cover one below it.
191
+ * @param above - The higher layer.
192
+ * @param below - The lower layer.
193
+ * @returns True only when every element `below` selects is also selected by `above`.
194
+ */
195
+ readonly covers?: (above: Layer, below: Layer) => boolean;
187
196
  }
188
197
  /**
189
198
  * Read the legend out of the encoding model.
@@ -104,6 +104,17 @@ export interface SelectorSource {
104
104
  * @returns The id at that row.
105
105
  */
106
106
  readonly edgeIdOf?: (index: number) => EdgeId;
107
+ /**
108
+ * The dense indices that carry a value for one column, ascending, when the source can list
109
+ * them without a walk over every element. A session answers it for a run's column.
110
+ *
111
+ * Absent, or answering undefined, nothing asks the question another way: the legend then
112
+ * reports a layer as painted over only by a layer that selects everything.
113
+ * @param path - The column path.
114
+ * @param target - Whether the asking layer paints nodes or edges.
115
+ * @returns The indices, or undefined when the column cannot be enumerated.
116
+ */
117
+ readonly measured?: (path: Path, target: SelectorTarget) => ArrayLike<number> | undefined;
107
118
  }
108
119
  /**
109
120
  * One target's half of a {@link SelectorSource}, resolved once so the predicate never chooses.
@@ -132,6 +132,22 @@ export interface SessionSelectorSource extends SelectorSource {
132
132
  */
133
133
  readonly measured: (path: Path, target: SelectorTarget) => ArrayLike<number> | undefined;
134
134
  }
135
+ /**
136
+ * The id of the node at one end of an edge, for the attribute keys that name an endpoint.
137
+ *
138
+ * An edge's endpoints are graph structure, so the importer removes the keys they arrived under
139
+ * (`src`/`dst`, or whatever the id paths name) from the record a selector reads, to keep the data
140
+ * table from showing them twice. Without this, `data.source == 'A'` matched no edge at all and
141
+ * said nothing -- the empty answer looked exactly like a correct zero. So `source` and `target`
142
+ * are read from the snapshot for an edge whose record holds nothing under that key; an edge that
143
+ * really carries a `source` attribute (a provenance field, with its endpoints under `src`/`dst`)
144
+ * keeps it.
145
+ * @param graph - The snapshot the index addresses.
146
+ * @param index - The dense (logical) edge index, already bounded by the caller.
147
+ * @param key - The attribute key, without the `data.` prefix.
148
+ * @returns The endpoint's node id, or undefined when the key names no endpoint.
149
+ */
150
+ export declare function edgeEndpointOf(graph: GraphSnapshot, index: number, key: string): NodeId | undefined;
135
151
  /**
136
152
  * Build the selector source for one session.
137
153
  *