@graphty/graphty-element 2.0.1 → 2.2.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.
Files changed (76) 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-DWwQiBQu.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-CtqyVNsw.js} +2518 -2239
  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-mD0x3z7t.js} +1 -1
  12. package/dist/chunks/{XRPivotCameraController-UjMsbran.js → XRPivotCameraController-Clil9NhV.js} +2 -2
  13. package/dist/chunks/{cameras-BeIAlMWR.js → cameras-DFXNYBze.js} +2 -2
  14. package/dist/chunks/{capability-check-DcKFwS6q.js → capability-check-dXwQ_z5B.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-2VIpMJZP.js} +1576 -1211
  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-BUS7NWy2.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 +36288 -34368
  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 +41 -7
  33. package/dist/src/Node.d.ts +1 -1
  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 +13 -0
  46. package/dist/src/config/GraphStyle.d.ts +1 -1
  47. package/dist/src/config/StyleTemplate.d.ts +4 -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 +48 -3
  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/EventManager.d.ts +7 -0
  59. package/dist/src/managers/GraphContext.d.ts +6 -0
  60. package/dist/src/managers/LayoutManager.d.ts +98 -4
  61. package/dist/src/managers/RenderManager.d.ts +7 -0
  62. package/dist/src/managers/UpdateManager.d.ts +1 -1
  63. package/dist/src/session/catalog.d.ts +2 -2
  64. package/dist/src/session/query.d.ts +83 -0
  65. package/dist/src/session/runs/types.d.ts +10 -4
  66. package/dist/src/session/selection/targets.d.ts +20 -3
  67. package/dist/src/session/styles/StylesApi.d.ts +18 -0
  68. package/dist/src/session/styles/legend.d.ts +9 -0
  69. package/dist/src/session/styles/predicate.d.ts +11 -0
  70. package/dist/src/session/styles/sources.d.ts +16 -0
  71. package/dist/src/session/types.d.ts +38 -2
  72. package/dist/src/testing/fakeAccelerator.d.ts +323 -0
  73. package/dist/webgpu.d.ts +23 -1
  74. package/dist/webgpu.js +70 -32
  75. package/package.json +10 -8
  76. package/dist/chunks/types-Dwm9waL2.js +0 -7
@@ -1,56 +1,39 @@
1
- import { z } from "zod/v4";
1
+ /**
2
+ * @file The ForceAtlas2 layout: the Gephi arrangement, computed on the CPU or on an accelerator.
3
+ *
4
+ * It used to be a one-shot pass -- run `forceatlas2Layout` over a node and edge list, publish the
5
+ * answer, stop -- which is why the catalogue called it a batch layout. It is now a steppable
6
+ * simulation from `@graphty/layout`, so it keeps running until the arrangement settles and reheats
7
+ * on a drag or a pin, and the element decides at every load whether the simulation is the CPU's or
8
+ * the accelerator's.
9
+ */
10
+ import type { SimulationType } from "@graphty/layout";
2
11
  import { type OptionsSchema } from "../config";
3
- import { SimpleLayoutEngine } from "./LayoutEngine";
4
- declare const ForceAtlas2LayoutConfig: z.ZodObject<{
5
- pos: z.ZodDefault<z.ZodUnion<[z.ZodRecord<z.ZodNumber, z.ZodArray<z.ZodNumber>>, z.ZodNull]>>;
6
- maxIter: z.ZodDefault<z.ZodNumber>;
7
- jitterTolerance: z.ZodDefault<z.ZodNumber>;
8
- scalingRatio: z.ZodDefault<z.ZodNumber>;
9
- gravity: z.ZodDefault<z.ZodNumber>;
10
- distributedAction: z.ZodDefault<z.ZodBoolean>;
11
- strongGravity: z.ZodDefault<z.ZodBoolean>;
12
- nodeMass: z.ZodDefault<z.ZodUnion<[z.ZodRecord<z.ZodNumber, z.ZodNumber>, z.ZodNull]>>;
13
- nodeSize: z.ZodDefault<z.ZodUnion<[z.ZodRecord<z.ZodNumber, z.ZodNumber>, z.ZodNull]>>;
14
- weighted: z.ZodDefault<z.ZodBoolean>;
15
- dissuadeHubs: z.ZodDefault<z.ZodBoolean>;
16
- linlog: z.ZodDefault<z.ZodBoolean>;
17
- seed: z.ZodDefault<z.ZodUnion<[z.ZodNumber, z.ZodNull]>>;
18
- dim: z.ZodDefault<z.ZodNumber>;
19
- scalingFactor: z.ZodDefault<z.ZodNumber>;
20
- }, z.core.$strict>;
21
- type ForceAtlas2LayoutConfigType = z.infer<typeof ForceAtlas2LayoutConfig>;
22
- type ForceAtlas2LayoutOpts = Partial<ForceAtlas2LayoutConfigType>;
12
+ import { SimulationLayoutEngine } from "./SimulationLayoutEngine";
23
13
  /**
24
- * ForceAtlas2 layout engine for graph visualization with scaling and gravity options
14
+ * The ForceAtlas2 engine, as the element declares it.
15
+ *
16
+ * Every member is a static the element reads: `LayoutManager` builds the bridge itself, with the
17
+ * graph's acceleration controller, so this class never runs a layout of its own. It declares no
18
+ * `static descriptor` because its arrangement is authored in the layout catalogue, where it sits
19
+ * under `force`.
25
20
  */
26
- export declare class ForceAtlas2Layout extends SimpleLayoutEngine {
21
+ export declare class ForceAtlas2Layout extends SimulationLayoutEngine {
27
22
  static type: string;
23
+ static simulationType: SimulationType;
28
24
  static maxDimensions: number;
29
- static honoursWeights: boolean;
30
- static zodOptionsSchema: OptionsSchema;
31
- scalingFactor: number;
32
- config: ForceAtlas2LayoutConfigType;
33
25
  /**
34
- * Create a ForceAtlas2 layout engine
35
- * @param opts - Configuration options for the ForceAtlas2 algorithm
26
+ * A WEIGHT IS AN ATTRACTION STRENGTH HERE, and is passed through as it is stored: a heavier
27
+ * edge pulls its two nodes closer, which is what a weight means everywhere else in the
28
+ * element. Kamada-Kawai reads the very same number as a distance and therefore inverts it; the
29
+ * two engines disagree about the arithmetic so that they agree about the meaning.
36
30
  */
37
- constructor(opts: ForceAtlas2LayoutOpts);
31
+ static honoursWeights: boolean;
32
+ static zodOptionsSchema: OptionsSchema;
38
33
  /**
39
- * Get dimension-specific options for ForceAtlas2 layout
40
- * @param dimension - The desired dimension (2 or 3)
41
- * @returns Options object with dim parameter
34
+ * Get dimension-specific options for ForceAtlas2 layout.
35
+ * @param dimension - The desired dimension (2 or 3).
36
+ * @returns Options object with dim parameter.
42
37
  */
43
38
  static getOptionsForDimension(dimension: 2 | 3): object;
44
- /**
45
- * Compute node positions using the ForceAtlas2 algorithm
46
- *
47
- * A WEIGHT IS PASSED THROUGH AS IT IS STORED. ForceAtlas2 reads a weight as an attraction
48
- * STRENGTH -- it becomes the adjacency matrix entry the attraction force is scaled by -- which
49
- * is already what a weight means everywhere else in the element, so a heavier edge pulls its
50
- * two nodes closer and nothing has to be inverted. Kamada-Kawai reads the very same number as
51
- * a distance and therefore does invert it; the two engines disagree about the arithmetic so
52
- * that they agree about the meaning.
53
- */
54
- doLayout(): void;
55
39
  }
56
- export {};
@@ -48,15 +48,15 @@ export interface LayoutEngineStatics {
48
48
  /**
49
49
  * Whether this engine arranges a graph differently when its edges carry weights.
50
50
  *
51
- * Optional, and false for all but two of the element's own sixteen. It exists so that a
51
+ * Optional, and false for all but two of the element's own seventeen. It exists so that a
52
52
  * picker can tell a reader which arrangements the `weighted` option actually does something
53
- * for, instead of offering it on fourteen layouts that ignore it.
53
+ * for, instead of offering it on fifteen layouts that ignore it.
54
54
  */
55
55
  honoursWeights?: boolean;
56
56
  /**
57
57
  * What the catalogue publishes about this layout, so a picker can offer it.
58
58
  *
59
- * REQUIRED OF A THIRD PARTY'S ENGINE and absent from the element's own sixteen, whose
59
+ * REQUIRED OF A THIRD PARTY'S ENGINE and absent from the element's own seventeen, whose
60
60
  * arrangements are authored in `src/catalog/layouts.ts` instead. `descriptor.id` must equal
61
61
  * {@link LayoutEngineStatics.type}: one key, so nothing is named twice and `layoutIdForEngine`
62
62
  * can answer a plugin's own id.
@@ -94,7 +94,7 @@ export declare abstract class LayoutEngine {
94
94
  * Whether this engine reads edge weights. See {@link LayoutEngineStatics.honoursWeights}.
95
95
  *
96
96
  * False here because most layouts have no weight channel at all: of the element's own
97
- * sixteen, only Kamada-Kawai and ForceAtlas2 can read one, and the other fourteen would be
97
+ * seventeen, only Kamada-Kawai and ForceAtlas2 can read one, and the other fifteen would be
98
98
  * advertising a control that changes nothing.
99
99
  */
100
100
  static honoursWeights: boolean;
@@ -162,7 +162,7 @@ export declare abstract class LayoutEngine {
162
162
  * Take a node out of the layout, before the element disposes the mesh that drew it.
163
163
  *
164
164
  * Declared here, with a default that does nothing, because it used to be duck-typed by the
165
- * element's data manager and implemented by none of the sixteen engines that ship here: an
165
+ * element's data manager and implemented by none of the seventeen engines that ship here: an
166
166
  * author learned it existed by reading the element's source, and got no worked example. An
167
167
  * engine that keeps its own node list must override this, or it holds every removed node --
168
168
  * and everything that node references -- for as long as the engine lives.
@@ -260,7 +260,7 @@ export declare abstract class LayoutEngine {
260
260
  * freeze counts as placed, which is what makes a file's own coordinates yield to it.
261
261
  *
262
262
  * A PINNED ROW REFUSES A LAYOUT STEP. This is the whole of "a pin is meaningful under every
263
- * arrangement": fourteen of the element's sixteen engines implement `pin()` as a no-op and
263
+ * arrangement": twelve of the element's seventeen engines implement `pin()` as a no-op and
264
264
  * `setNodePosition` as a no-op too, so before this guard a reader who dragged a node under a
265
265
  * static layout watched it snap back the next time the layout recomputed. One refusal here
266
266
  * covers every engine, including one written by a third party that has never heard of pinning,
@@ -351,7 +351,7 @@ export declare abstract class LayoutEngine {
351
351
  * offered by a picker, described in a reader's language, or found by `layoutIdForEngine`.
352
352
  *
353
353
  * A third party's class must declare a `static descriptor` whose `id` equals its
354
- * `static type`. The element's own sixteen are the one exemption, because their arrangements
354
+ * `static type`. The element's own seventeen are the one exemption, because their arrangements
355
355
  * are authored centrally in the layout catalogue where several engines may sit behind one
356
356
  * public name.
357
357
  * @param cls - The layout engine class.
@@ -0,0 +1,458 @@
1
+ /**
2
+ * @file The layout bridge: one `LayoutEngine` driving `@graphty/layout`'s `LayoutSimulation`,
3
+ * on the CPU or on whatever the element's acceleration controller has attached.
4
+ *
5
+ * WHERE THE DECISION IS TAKEN. "GPU or CPU" is asked of the controller ONCE per simulation, by
6
+ * `plan({ capability, nodeCount })`, before any work starts -- at the first load, at every
7
+ * reload (a freeze can cross the `acceleration.minNodes` threshold in either direction) and at
8
+ * every swap. Nothing inside a running batch may change the answer: a rejected batch is
9
+ * reported with its own code and stops the stepping, and only the controller's NEXT transition
10
+ * -- a recovery, a detach, an injection -- rebuilds the simulation, through
11
+ * {@link SimulationLayoutEngine.replaceSimulation}. That is the difference between capability
12
+ * detection, which is required, and a silent CPU fallback after a GPU failure, which is not.
13
+ *
14
+ * TWO ARRAYS, TWO UNIT SYSTEMS. The simulations are the only layouts in this element that never
15
+ * rescale what they compute: a ForceAtlas2 equilibrium grows with the graph, so a settled
16
+ * hundred-node arrangement is a few hundred units across and a settled fifty-thousand-node one is
17
+ * tens of thousands, while every visual size in the element -- node size, edge width, label size,
18
+ * arrow size -- is an absolute scene unit tuned for the plus-or-minus-100 box the one-shot engines
19
+ * publish into. So the simulation is given an array of ITS OWN, and `publishPositions()` maps it
20
+ * into the element's: the widest radius about its own centre, over the rows that publish actually
21
+ * writes, becomes `scalingFactor`, recomputed at every publish so a settling graph holds its
22
+ * apparent size instead of flying out of frame. Everything written the other way -- a drag, a pin replay, a
23
+ * `setNodePosition`, the seed -- arrives in scene units and is divided back out, so a dragged node
24
+ * lands under the pointer. The simulation's own settle rule, trace and statistics never see any of
25
+ * this: they are computed in its units, and its units do not move.
26
+ *
27
+ * WHAT THE SWAP PRESERVES. The bridge owns the simulation's array, so a swap keeps the arrangement
28
+ * by handing the new simulation the same one. Pins are the STORE's: a byte per row in the position
29
+ * lane, which a freeze remaps with the coordinates. The bridge packs that lane into the
30
+ * simulation's `NodeMask` after every load, because the simulation would otherwise keep spending
31
+ * force on a body the element refuses to move and its own copy of that row would wander away from
32
+ * where the reader put it. A drag holds the same bit temporarily and is not a pin.
33
+ *
34
+ * A FIXED ROW IS THE ONE PLACE THE ELEMENT'S ARRAY IS THE AUTHORITY rather than the simulation's,
35
+ * so it is not published over at all; see {@link SimulationLayoutEngine.publishPositions} for what
36
+ * that costs and why the repair that suggests itself is worse.
37
+ */
38
+ import { type F32, type GraphSnapshot, type NodeMask } from "@graphty/graph-format";
39
+ import { type CommonLayoutOptions, type ForceAtlas2Options, type FruchtermanReingoldOptions, type LayoutSimulation, type SimulationOptions, type SimulationType, type SpringElectricalOptions } from "@graphty/layout";
40
+ import type { AccelerationController } from "../acceleration/AccelerationController";
41
+ import { type AccelerationPrecision, type GraphAccelerator } from "../acceleration/types";
42
+ import type { Edge } from "../Edge";
43
+ import { GraphtyError } from "../errors";
44
+ import type { DataManager } from "../managers/DataManager";
45
+ import type { Node } from "../Node";
46
+ import { type EdgePosition, LayoutEngine, type Position } from "./LayoutEngine";
47
+ /**
48
+ * How many iterations one `step()` submits.
49
+ *
50
+ * The explicit knob decides whenever a host set one. Otherwise the count is the frame loop's own
51
+ * `stepMultiplier`, raised fourfold once the graph is large enough that reading a million
52
+ * positions back costs more than a frame: above that size the element computes more per batch and
53
+ * accepts a lower position refresh rate, which is the trade design 13 row P12 asks for.
54
+ *
55
+ * It is re-applied at EVERY load, because a freeze can carry a graph across the line in either
56
+ * direction.
57
+ * @param explicit - What a host asked for, or null to let the size decide.
58
+ * @param stepMultiplier - `behavior.layout.stepMultiplier`, the automatic rule's starting point.
59
+ * @param nodeCount - The graph this load is over.
60
+ * @returns The iteration count.
61
+ */
62
+ export declare function iterationsPerStepFor(explicit: number | null, stepMultiplier: number, nodeCount: number): number;
63
+ /**
64
+ * Turns a reader's `nodeMass` record into one value per dense row, and leaves every other form to
65
+ * the simulation.
66
+ *
67
+ * A reader may give `nodeMass` as a number per node ID, as the name of a numeric node column, or
68
+ * not at all. Only the FIRST is resolved here, because it is the only one an accelerator refuses:
69
+ * a GPU never sees a node ID. A node the record says nothing about gets one more than its degree,
70
+ * which is what the simulations themselves apply, so a partial record leaves the rest of the graph
71
+ * exactly as it would have been.
72
+ *
73
+ * Exported beside {@link iterationsPerStepFor} so both rules can be read off without a device, a
74
+ * renderer or a running simulation.
75
+ * @param spec - What the reader set, in any of its three forms.
76
+ * @param undirected - The graph the simulation will be loaded with.
77
+ * @returns The masses, or null when the simulation resolves them itself.
78
+ */
79
+ export declare function resolveNodeMass(spec: unknown, undirected: GraphSnapshot): F32 | null;
80
+ /**
81
+ * How a simulation's arrangement maps into the element's scene units.
82
+ *
83
+ * See {@link measureEnvelope}.
84
+ *
85
+ * Exported only because {@link measureEnvelope} returns it and TypeScript's declaration emit
86
+ * needs every named type in a published signature to be exported. Nothing outside this module
87
+ * names the type -- the test destructures the value -- so it is no entry point's surface.
88
+ * @internal
89
+ */
90
+ export interface LayoutEnvelope {
91
+ /** What a simulation-unit offset from {@link LayoutEnvelope.centre} is multiplied by. */
92
+ readonly scale: number;
93
+ /** The arrangement's centre, in the simulation's own units. */
94
+ readonly centre: readonly [number, number, number];
95
+ /**
96
+ * How many rows the measurement actually saw.
97
+ *
98
+ * Zero is the one answer a caller must not act on: it means the arrangement has nothing in
99
+ * it to fit, and the scale and centre beside it are the identity rather than a measurement.
100
+ */
101
+ readonly placed: number;
102
+ }
103
+ /**
104
+ * Measures the arrangement a simulation currently holds, and says how to publish it.
105
+ *
106
+ * The centre is the mean of the placed rows and the scale is `scalingFactor` divided by the
107
+ * furthest row's distance from that centre, so a published arrangement is exactly
108
+ * `scalingFactor` units from its centre at its widest whatever the simulation's own equilibrium
109
+ * turned out to be. A row that is not finite -- the element's "not laid out yet" -- counts for
110
+ * neither, and an arrangement that measures as a point (see {@link POINT_RADIUS}) publishes at
111
+ * its own size rather than being divided by nothing.
112
+ *
113
+ * A HELD ROW COUNTS FOR NEITHER EITHER, and that is what `held` is for. The publish does not
114
+ * write a row a pin or a drag is holding -- its scene coordinate is the reader's -- so measuring
115
+ * one would fit every OTHER row inside a radius set by a row that is not drawn there: a node
116
+ * dragged past the envelope would shrink the whole graph under the pointer. The caller decides
117
+ * which rows those are, because "held" is the element's business rather than this rule's.
118
+ *
119
+ * Exported beside {@link iterationsPerStepFor} and {@link resolveNodeMass} so the rule can be read
120
+ * off without a device, a renderer or a running simulation.
121
+ * @param positions - A stride-3 array in the simulation's own units, at least `3 * nodeCount` long.
122
+ * @param nodeCount - How many rows of it to measure.
123
+ * @param scalingFactor - The scene-unit radius the arrangement is to be published at.
124
+ * @param held - Answers true for a row the caller will not publish, or null to measure them all.
125
+ * @returns The centre to publish about, the multiplier to publish with, and how many rows were
126
+ * measured.
127
+ */
128
+ export declare function measureEnvelope(positions: ArrayLike<number>, nodeCount: number, scalingFactor: number, held?: ((row: number) => boolean) | null): LayoutEnvelope;
129
+ /**
130
+ * The resolved options a bridge runs on: the simulation's own knobs, plus the two the frame loop
131
+ * needs.
132
+ *
133
+ * `model` carries whatever the chosen simulation type accepts and WINS over the common members
134
+ * beside it, so one option is never spelled in two places with two answers.
135
+ */
136
+ export interface SimulationEngineOptions extends CommonLayoutOptions, Omit<SimulationOptions, "iterationsPerStep"> {
137
+ /** The type's own options, already mapped from what the consumer asked for. */
138
+ readonly model: ForceAtlas2Options | FruchtermanReingoldOptions | SpringElectricalOptions;
139
+ /**
140
+ * The radius, in scene units, the published arrangement is fitted to.
141
+ *
142
+ * The element's own envelope, not the simulation's: see the file header. It is every size
143
+ * control the layout publishes rolled into one number -- ForceAtlas2 and Spring publish
144
+ * "Scaling Factor", Spring and spring-electrical publish "Scale" -- because the refit fits
145
+ * the arrangement to exactly this radius, so a multiplier left out of it changes nothing a
146
+ * reader can see. `LayoutManager` is what rolls them up.
147
+ */
148
+ readonly scalingFactor: number;
149
+ /** Iterations each `step()` submits, or null for the automatic rule re-applied at every load. */
150
+ readonly iterationsPerStep: number | null;
151
+ /** `behavior.layout.stepMultiplier`, which the automatic rule starts from. */
152
+ readonly stepMultiplier: number;
153
+ }
154
+ /**
155
+ * Everything a bridge is built with: ONE constructor argument, because `LayoutEngine.register`
156
+ * accepts only a class whose constructor takes one object and `LayoutEngine.get` calls it with
157
+ * one.
158
+ */
159
+ export interface SimulationEngineInit {
160
+ /** Which simulation to drive. */
161
+ readonly type: SimulationType;
162
+ /**
163
+ * The registered layout name this bridge is standing in for.
164
+ *
165
+ * Every other engine answers `type` from its class's `static type`, which cannot work here:
166
+ * `LayoutManager` builds THIS class for each of the layouts that declare a
167
+ * `static simulationType`, so the class is the same one for all of them and its own static
168
+ * would name none of them.
169
+ */
170
+ readonly layoutType: string;
171
+ /** The resolved options. */
172
+ readonly options: SimulationEngineOptions;
173
+ /** The controller that decides where each simulation runs and announces every transition. */
174
+ readonly controller: AccelerationController;
175
+ /** Where a failure goes: the element's error channel, with `source: "layout"`. */
176
+ readonly report: (error: GraphtyError) => void;
177
+ /** The data manager the snapshot, its undirected copy and the position array come from. */
178
+ readonly dataManager: DataManager;
179
+ }
180
+ /**
181
+ * A `LayoutEngine` that drives one `LayoutSimulation` from `@graphty/layout`.
182
+ *
183
+ * It is registered under no name by itself: `LayoutManager` constructs it for the layout types
184
+ * that declare a `static simulationType`, which is how it receives the controller a registered
185
+ * class could not be handed.
186
+ */
187
+ export declare class SimulationLayoutEngine extends LayoutEngine {
188
+ #private;
189
+ /** Which simulation this bridge drives. */
190
+ readonly simulationType: SimulationType;
191
+ /** The accelerator member this simulation needs, asked of the controller before every build. */
192
+ readonly capability: string;
193
+ /**
194
+ * Builds a bridge. Nothing is planned and no simulation exists until {@link init} loads one.
195
+ *
196
+ * The parameter is declared as a bare `object` because `LayoutEngine.register` files a class
197
+ * whose constructor takes the options a consumer passed to `setLayout`, and a bridge takes
198
+ * more than that. A caller that means to build one declares its argument as a
199
+ * {@link SimulationEngineInit}, which is what `LayoutManager` does; a caller that arrived
200
+ * through `LayoutEngine.get` is refused on the next line.
201
+ * @param init - A {@link SimulationEngineInit}: the type, the resolved options, the
202
+ * controller, the error channel and the data.
203
+ * @throws A `GraphtyError` with `E_INTERNAL` when no controller was passed.
204
+ */
205
+ constructor(init: object);
206
+ /**
207
+ * The registered layout name, which every other engine reads off its class's `static type`.
208
+ *
209
+ * Without this the manager's `layoutType`, `getStats().layoutType` and the details of every
210
+ * error this bridge reports would all be undefined, and a 2D/3D switch would ask the element
211
+ * to set a layout called "undefined".
212
+ * @returns The name the consumer set.
213
+ */
214
+ get type(): string;
215
+ /**
216
+ * The running simulation, or null before the first load and while the bridge is stopped.
217
+ *
218
+ * Exposed so a caller that has to wait for the device -- a screenshot after a pause -- can
219
+ * reach an accelerated simulation's own `flush()`.
220
+ * @returns The simulation.
221
+ */
222
+ get simulation(): LayoutSimulation | null;
223
+ /**
224
+ * Whether the running simulation is the accelerator's rather than the CPU's.
225
+ * @returns True when the accelerator built it.
226
+ */
227
+ get isAccelerated(): boolean;
228
+ /**
229
+ * The arithmetic the running simulation computes in, for a caller that labels its results.
230
+ * @returns The precision, which is the CPU's whenever the CPU is running the layout.
231
+ */
232
+ get precision(): AccelerationPrecision;
233
+ /**
234
+ * How many iterations this bridge has submitted since the simulation was built. Submitted,
235
+ * not retired: an accelerated batch is counted when it goes out, not when it lands.
236
+ * @returns The count.
237
+ */
238
+ get iterationsDone(): number;
239
+ /**
240
+ * The node masses the running simulation was handed, when the element resolved them itself.
241
+ *
242
+ * A reader may give `nodeMass` as a number per node ID, as the name of a numeric node column,
243
+ * or not at all. Only the FIRST of those three is resolved here, into one value per dense row,
244
+ * because it is the only one an accelerator refuses: a GPU never sees a node ID. The other two
245
+ * are handed to the simulation as they stand and resolved by it at every load, on either path,
246
+ * which is also what keeps them right when a freeze renumbers the graph.
247
+ * @returns The masses, or null when the simulation resolves them itself.
248
+ */
249
+ get resolvedNodeMass(): F32 | null;
250
+ /**
251
+ * The fixed-node mask the simulation is running with, packed from the store's pin lane.
252
+ * @returns The mask, or an empty one before the first load.
253
+ */
254
+ get pinnedMask(): NodeMask;
255
+ /**
256
+ * Whether a different accelerator is attached now than when the simulation was built.
257
+ *
258
+ * What `LayoutManager` asks on every controller transition before it swaps: `null` is nothing
259
+ * attached, so a detach, an injection, a device loss and a recovery all answer true, while a
260
+ * transition that leaves the same accelerator attached answers false -- including for a
261
+ * simulation the plan put on the CPU because the graph is below `acceleration.minNodes`.
262
+ * Rebuilding that one on every status change would re-upload the graph and reheat a layout
263
+ * that had settled, and it is a RELOAD that re-plans the threshold, which it does on its own.
264
+ * @param accelerator - What the controller holds now.
265
+ * @returns True when the simulation has to be rebuilt.
266
+ */
267
+ builtWithChanged(accelerator: GraphAccelerator | null): boolean;
268
+ /**
269
+ * The FIRST load, which is what makes this engine's `init()` different from every other's.
270
+ *
271
+ * `LayoutManager._setLayoutInternal` replays the pinned nodes between `init()` and
272
+ * `setLayoutEngine`, through `setNodePosition` and `pin`, so a load deferred to any later
273
+ * point would meet those calls with no simulation to tell.
274
+ * @returns A promise that resolves once the simulation exists and holds the graph.
275
+ */
276
+ init(): Promise<void>;
277
+ /**
278
+ * Adopts a snapshot and builds the simulation the controller's plan chose for it.
279
+ * @param snapshot - The graph, as `getSnapshot()` returns it; the bridge derives the
280
+ * undirected copy the simulations require itself.
281
+ * @param positions - The element's stride-3 array, written in place by every batch.
282
+ * @throws A `GraphtyError` with `E_NO_ACCELERATOR` under the `required` policy with nothing
283
+ * attached, or whatever `createSimulation` throws.
284
+ */
285
+ load(snapshot: GraphSnapshot, positions: F32): void;
286
+ /**
287
+ * Hands the running simulation a new snapshot, rebuilding it first when the plan flipped.
288
+ *
289
+ * The re-plan is the point: a freeze can carry the graph across `acceleration.minNodes` in
290
+ * either direction, and design 9.4 item 4 re-evaluates the threshold at every load.
291
+ * @param snapshot - The snapshot that has just replaced the old one.
292
+ * @param positions - The element's array, re-viewed at the new node count.
293
+ * @throws A `GraphtyError` with `E_NO_ACCELERATOR` under the `required` policy with nothing
294
+ * attached.
295
+ */
296
+ reload(snapshot: GraphSnapshot, positions: F32): void;
297
+ /**
298
+ * Rebuilds the simulation on whatever the controller holds now, keeping the arrangement.
299
+ *
300
+ * The old simulation is disposed and the new one is loaded with the SAME positions view and
301
+ * the same pins, so an accelerator arriving mid-run continues the layout rather than
302
+ * restarting it, and one leaving hands it to the CPU where the policy allows a CPU.
303
+ * @throws A `GraphtyError` with `E_NO_ACCELERATOR` when the policy is `"required"` and
304
+ * nothing is attached. The bridge is then STOPPED -- no simulation, nothing submitted -- and
305
+ * the next transition that attaches an accelerator brings it back through this same method.
306
+ */
307
+ replaceSimulation(): void;
308
+ /**
309
+ * Submits one batch, fire and forget.
310
+ *
311
+ * The abstract signature returns nothing, and a GPU simulation's `step()` returns a promise,
312
+ * so the rejection is caught here instead of by a caller. A saturated simulation returns the
313
+ * OLDEST promise already in flight rather than submitting another, which is how it keeps the
314
+ * frame loop from queueing work faster than the device retires it -- so the handlers are
315
+ * attached only when the promise is one this bridge has not seen. They are what closes the
316
+ * controller's work span: a batch landing on a settled simulation is where a GPU layout
317
+ * stops being work on the device.
318
+ */
319
+ step(): void;
320
+ /**
321
+ * Submits one batch and waits for it, which the pre-step loop and the tests need.
322
+ * @param iterations - Iterations this batch computes.
323
+ * @returns A promise that resolves when the batch has landed, or rejects with its failure.
324
+ */
325
+ stepAsync(iterations: number): Promise<void>;
326
+ /**
327
+ * Starts the settle count again, on a simulation that has one.
328
+ *
329
+ * `reheat()` is not on the `LayoutSimulation` interface -- both CPU simulations and the GPU
330
+ * ones have it, and a third party's may not -- so it is feature-tested rather than assumed.
331
+ */
332
+ reheat(): void;
333
+ /**
334
+ * Holds a node still for the duration of a drag, without pinning it.
335
+ *
336
+ * The fixed bit is the same one a pin uses, because it is the only thing that stops a
337
+ * simulation writing the row; what makes this temporary is that {@link endDrag} clears it
338
+ * again unless the drag is about to become a pin.
339
+ * @param n - The node the pointer has taken.
340
+ */
341
+ beginDrag(n: Node): void;
342
+ /**
343
+ * Gives the node back to the simulation, or leaves it fixed because it is being pinned.
344
+ * A node that was ALREADY PINNED before the pointer took it keeps its bit whatever `pin`
345
+ * says: the store's pin stops the row being published over, but only the fixed bit stops the
346
+ * simulation's own copy of it drifting away from where the reader put it, which the next
347
+ * refit would then have to drag back. Clearing it would leave a node the store still calls
348
+ * pinned being pushed around inside the simulation every frame.
349
+ * @param n - The node the pointer has let go of.
350
+ * @param pin - True when the drag ends in a pin, so the bit stays set.
351
+ */
352
+ endDrag(n: Node, pin: boolean): void;
353
+ /**
354
+ * Fixes a pinned node in the simulation. The store already holds the pin.
355
+ * @param n - The node that was pinned.
356
+ */
357
+ pin(n: Node): void;
358
+ /**
359
+ * Releases a node the simulation was holding fixed.
360
+ * @param n - The node that was unpinned.
361
+ */
362
+ unpin(n: Node): void;
363
+ /**
364
+ * Places one node now. A drag reaches this per pointer move, and `replayPins` once per pin.
365
+ * @param n - The node that moved.
366
+ * @param p - Where it moved to.
367
+ */
368
+ setNodePosition(n: Node, p: Position): void;
369
+ /**
370
+ * Reads a node's current coordinates, in scene units, out of the element's own position array.
371
+ * @param n - The node to read.
372
+ * @returns Its scene-unit position; the origin for a row nothing has placed.
373
+ */
374
+ getNodePosition(n: Node): Position;
375
+ /**
376
+ * Reads both endpoints of an edge into the one pair this engine reuses.
377
+ * @param e - The edge to read.
378
+ * @returns The pair, valid until the next call; the origin for an endpoint nothing has placed.
379
+ */
380
+ getEdgePosition(e: Edge): EdgePosition;
381
+ /**
382
+ * Nothing: the snapshot IS the graph, and the simulation reads it rather than a node list.
383
+ */
384
+ addNode(): void;
385
+ /**
386
+ * Nothing, for the same reason as {@link addNode}.
387
+ */
388
+ addEdge(): void;
389
+ /**
390
+ * Every node of the graph, which is the list the FRAME LOOP walks.
391
+ *
392
+ * The bridge keeps no list of its own -- the snapshot is the graph -- but these two getters
393
+ * are not bookkeeping: `UpdateManager` moves a mesh only for a node this one yields, draws an
394
+ * edge only for an edge the next one yields, and frames the camera on their count. An empty
395
+ * iterable would let the simulation rewrite the position array every frame while not one mesh
396
+ * moved. So they answer with the data manager's own collections, which hold exactly what
397
+ * `LayoutManager` hands every other engine through `addNodes` / `addEdges`.
398
+ * @returns The graph's live node collection.
399
+ */
400
+ get nodes(): Iterable<Node>;
401
+ /**
402
+ * Every edge of the graph; see {@link SimulationLayoutEngine.nodes}.
403
+ * @returns The graph's live edge collection.
404
+ */
405
+ get edges(): Iterable<Edge>;
406
+ /**
407
+ * Whether the simulation has stopped moving.
408
+ *
409
+ * A bridge with no simulation -- before the first load, or stopped after a swap the policy
410
+ * refused -- reads settled, because nothing is going to move.
411
+ * @returns True when the last completed batch settled.
412
+ */
413
+ get isSettled(): boolean;
414
+ /**
415
+ * Maps the simulation's arrangement into the element's scene units, and writes it.
416
+ *
417
+ * This is the seam the file header describes. The mapping is remeasured here rather than
418
+ * fixed once, because a ForceAtlas2 graph expands for hundreds of iterations before it
419
+ * settles: a scale taken at the first step would let the arrangement grow out of the frame,
420
+ * and one taken at the last would not exist until the reader had already watched it happen.
421
+ * Remeasured, a settling graph holds its apparent size and only its shape changes.
422
+ *
423
+ * A FIXED ROW IS NEITHER PUBLISHED NOR MEASURED. Its scene-unit coordinate is the reader's --
424
+ * the pointer's, or where a pin left it -- and `LayoutEngine.writeNodePosition` refuses a
425
+ * layout-intent write to a pinned row for every engine in this element, so the skip is that
426
+ * same rule reaching a row a drag is holding as well. It is left out of the measurement for
427
+ * the same reason it is left out of the write: a radius taken over a row that is not drawn
428
+ * there would resize every row that is. The two sides start in step, because
429
+ * every path that fixes a row leaves them in step: a drag writes both through
430
+ * {@link SimulationLayoutEngine.setNodePosition}, and a pin, a freeze and a rebuild all fix a
431
+ * row whose simulation copy this mapping had just published from.
432
+ *
433
+ * THEY DRIFT APART AFTERWARDS, and deliberately. A refit rescales the arrangement around a
434
+ * held row without moving the row, so the longer the arrangement keeps growing the further
435
+ * the simulation's copy of that row sits from where it is drawn. The obvious repair -- write
436
+ * the row back into the simulation whenever the mapping moves -- was tried and is worse than
437
+ * the problem: a simulation treats a written position as a disturbance and reheats, so an
438
+ * accelerated ForceAtlas2 with one pinned node cleared its settle window on every frame and
439
+ * ran the full sixty-second test budget without once reporting itself at rest. Worse, the
440
+ * write changes the arrangement, which changes the refit, which asks for another write. A pin
441
+ * is a scene-unit promise and it is kept; how far the arrangement moves under it is not part
442
+ * of that promise.
443
+ *
444
+ * WHAT A GRAPH OF NOTHING BUT HELD ROWS COSTS. Fitting only the published rows means that the
445
+ * fewer of them there are the less there is to fit, and two free rows a hair apart are spread
446
+ * to the full radius exactly as a two-node graph would be. That is this rule meeting a reader
447
+ * who has pinned almost everything and taken the size out of the layout's hands; it is bounded
448
+ * -- an arrangement that measures as a point publishes at its own size -- and a graph with no
449
+ * free row left keeps the last map rather than falling back to the identity.
450
+ */
451
+ publishPositions(): void;
452
+ /**
453
+ * Releases the simulation. The accelerator's residency for the snapshot is NOT released here:
454
+ * it is per snapshot and shared with the algorithm runs, and `Graph` frees it at the next
455
+ * freeze and at shutdown.
456
+ */
457
+ dispose(): void;
458
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @file The spring-electrical layout: ngraph's force model, computed on an accelerator.
3
+ *
4
+ * It is registered whatever hardware is present, and `setLayout("spring-electrical")` without an
5
+ * accelerator that implements it fails loudly with `E_NO_ACCELERATOR` rather than quietly
6
+ * arranging the graph some other way. A picker does not have to try it to find out: the layout
7
+ * catalogue's entry for this engine declares `requires: { accelerator: true }`, which a consumer
8
+ * reads beside `capabilities.acceleration.state` and greys the entry out.
9
+ */
10
+ import type { SimulationType } from "@graphty/layout";
11
+ import { type OptionsSchema } from "../config";
12
+ import { SimulationLayoutEngine } from "./SimulationLayoutEngine";
13
+ /**
14
+ * The spring-electrical engine, as the element declares it.
15
+ *
16
+ * Every member is a static the element reads: `LayoutManager` builds the bridge itself, with the
17
+ * graph's acceleration controller, so this class never runs a layout of its own. It declares no
18
+ * `static descriptor` because its arrangement is authored in the layout catalogue, where it sits
19
+ * under `force` beside ngraph, d3, ForceAtlas2, Spring and Kamada-Kawai.
20
+ */
21
+ export declare class SpringElectricalLayout extends SimulationLayoutEngine {
22
+ static type: string;
23
+ static simulationType: SimulationType;
24
+ static maxDimensions: number;
25
+ static zodOptionsSchema: OptionsSchema;
26
+ /**
27
+ * Get dimension-specific options for the spring-electrical layout.
28
+ * @param dimension - The desired dimension (2 or 3).
29
+ * @returns Options object with dim parameter.
30
+ */
31
+ static getOptionsForDimension(dimension: 2 | 3): object;
32
+ }