@graphty/cytoscape-extensions 0.0.10 → 0.0.12
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/package.json +7 -7
- package/src/algorithms.ts +19 -2
- package/src/elements.ts +5 -60
- package/src/gpu.ts +9 -1
- package/src/graph-data.ts +6 -0
- package/src/io.ts +0 -42
- package/src/layouts.ts +65 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphty/cytoscape-extensions",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Graphty layouts, algorithms, graph generators, sample datasets and file import/export as Cytoscape.js extensions",
|
|
5
5
|
"author": "Adam Powers <apowers@ato.ms>",
|
|
6
6
|
"type": "module",
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
},
|
|
72
72
|
"homepage": "https://github.com/graphty-org/graphty-monorepo/tree/master/cytoscape-extensions#readme",
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@graphty/graph-io": "^0.3.
|
|
75
|
-
"@graphty/
|
|
76
|
-
"@graphty/graph-
|
|
74
|
+
"@graphty/graph-io": "^0.3.36",
|
|
75
|
+
"@graphty/graph-samples": "^0.1.28",
|
|
76
|
+
"@graphty/webgpu-graph-algorithms": "^0.6.47"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
79
|
"@graphty/algorithms": "^3.0.0",
|
|
@@ -102,9 +102,9 @@
|
|
|
102
102
|
"typescript": "^5.9.3",
|
|
103
103
|
"vite": "^7.0.5",
|
|
104
104
|
"vitest": "4.1.11",
|
|
105
|
-
"@graphty/
|
|
106
|
-
"@graphty/
|
|
107
|
-
"@graphty/
|
|
105
|
+
"@graphty/algorithms": "^3.4.3",
|
|
106
|
+
"@graphty/graph-format": "^1.3.13",
|
|
107
|
+
"@graphty/layout": "^2.4.2"
|
|
108
108
|
},
|
|
109
109
|
"scripts": {
|
|
110
110
|
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && vite build --config vite.bundle.config.ts --mode cdn --logLevel warn && vite build --config vite.bundle.config.ts --logLevel warn",
|
package/src/algorithms.ts
CHANGED
|
@@ -136,7 +136,16 @@ import type {
|
|
|
136
136
|
} from "cytoscape";
|
|
137
137
|
|
|
138
138
|
import { OPTION_NAMES } from "./algorithm-options.js";
|
|
139
|
-
import {
|
|
139
|
+
import {
|
|
140
|
+
type Backend,
|
|
141
|
+
backendOf,
|
|
142
|
+
checkGpuMode,
|
|
143
|
+
coreDestroyedError,
|
|
144
|
+
gpuFor,
|
|
145
|
+
type GpuMode,
|
|
146
|
+
recording,
|
|
147
|
+
warnIfFixable,
|
|
148
|
+
} from "./gpu.js";
|
|
140
149
|
import {
|
|
141
150
|
coreOf,
|
|
142
151
|
type CytoscapeSnapshot,
|
|
@@ -1859,7 +1868,12 @@ function runSync(eles: Collection, key: keyof Impls, options: AlgorithmOptions =
|
|
|
1859
1868
|
*/
|
|
1860
1869
|
async function runAsync(eles: Collection, key: keyof Impls, options: AlgorithmOptions = {}): Promise<unknown> {
|
|
1861
1870
|
checkGpuMode(`${methodName(key)}Async`, options?.gpu);
|
|
1862
|
-
const
|
|
1871
|
+
const cy = coreOf(eles);
|
|
1872
|
+
const d = await gpuFor(cy, options.gpu);
|
|
1873
|
+
// destroyed while the device was acquired: its device is disposed and its elements are gone with it
|
|
1874
|
+
if (cy.destroyed()) {
|
|
1875
|
+
throw coreDestroyedError();
|
|
1876
|
+
}
|
|
1863
1877
|
const rec = d.gpu === null ? null : recording(d.gpu.accelerator);
|
|
1864
1878
|
warnIfFixable(d, eles.nodes().length);
|
|
1865
1879
|
const { result, snapshot } = run(eles, key, options, accelerated(rec?.accelerator ?? null));
|
|
@@ -1868,6 +1882,9 @@ async function runAsync(eles: Collection, key: keyof Impls, options: AlgorithmOp
|
|
|
1868
1882
|
let value: object;
|
|
1869
1883
|
try {
|
|
1870
1884
|
value = (await result) as object;
|
|
1885
|
+
} catch (e) {
|
|
1886
|
+
// destroyed mid-run, the device fails with "GpuContext is disposed"; the cause is the destroy
|
|
1887
|
+
throw cy.destroyed() ? coreDestroyedError() : e;
|
|
1871
1888
|
} finally {
|
|
1872
1889
|
unhold();
|
|
1873
1890
|
if (d.gpu !== null && typeof options.weight === "function") {
|
package/src/elements.ts
CHANGED
|
@@ -23,29 +23,6 @@ function plain(v: unknown): unknown {
|
|
|
23
23
|
return ArrayBuffer.isView(v) ? Array.from(v as unknown as ArrayLike<number>) : v;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
* RGB(A) channels in 0..1 as a CSS color: `#rrggbb` when opaque, `rgba(r, g, b, a)` otherwise.
|
|
28
|
-
* @param c - the channels
|
|
29
|
-
* @returns the color
|
|
30
|
-
*/
|
|
31
|
-
function cssColor(c: ArrayLike<number>): string {
|
|
32
|
-
const ch = (i: number): number => Math.max(0, Math.min(255, Math.round(c[i] * 255)));
|
|
33
|
-
const a = c.length > 3 ? Math.round(c[3] * 1000) / 1000 : 1;
|
|
34
|
-
if (a === 1) {
|
|
35
|
-
return `#${[0, 1, 2].map((i) => ch(i).toString(16).padStart(2, "0")).join("")}`;
|
|
36
|
-
}
|
|
37
|
-
return `rgba(${ch(0)}, ${ch(1)}, ${ch(2)}, ${a})`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Whether a column is a color: the role "color" with 3 or 4 numeric components (GEXF's viz:color).
|
|
42
|
-
* @param c - the column
|
|
43
|
-
* @returns true for a color column
|
|
44
|
-
*/
|
|
45
|
-
function isColor(c: Column): boolean {
|
|
46
|
-
return c.meta.role === "color" && (c.meta.components === 3 || c.meta.components === 4) && c.dtype !== "json";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
26
|
/**
|
|
50
27
|
* A y coordinate between Cytoscape (y grows downward) and graph-io (y grows upward, so a Cytoscape-family file's
|
|
51
28
|
* screen y is negated on import and negated back on export). Never yields -0.
|
|
@@ -56,9 +33,6 @@ export function flipY(y: number): number {
|
|
|
56
33
|
return y === 0 ? 0 : -y;
|
|
57
34
|
}
|
|
58
35
|
|
|
59
|
-
/** Reads a CSS color as RGBA channels in 0..1, or undefined when it cannot. */
|
|
60
|
-
type ColorParser = (text: string) => [number, number, number, number] | undefined;
|
|
61
|
-
|
|
62
36
|
/** A column that arrives under another data field name, because Cytoscape reserves its own. */
|
|
63
37
|
interface RenamedColumn {
|
|
64
38
|
readonly domain: "node" | "edge";
|
|
@@ -70,7 +44,6 @@ interface RenamedColumn {
|
|
|
70
44
|
* Cytoscape element definitions for a snapshot. Node ids are the snapshot's ids as strings. Every node and edge
|
|
71
45
|
* column becomes a data field of the same name, except that one named `id`, `source`, `target` or `parent` (which
|
|
72
46
|
* Cytoscape reserves) becomes `<name>#<the attribute's id in the file>`, or `<name>#2` when it has none, and:
|
|
73
|
-
* - a column with the role "color" (GEXF's viz:color) becomes a CSS color string, `#rrggbb` or `rgba(r, g, b, a)`;
|
|
74
47
|
* - a node column with the role "position" becomes the node's position (x and y; a z is dropped);
|
|
75
48
|
* - a node column with the role "parent" (a compound parent, as Cytoscape JSON, GraphML nested graphs and DOT
|
|
76
49
|
* clusters carry it) becomes `data.parent`;
|
|
@@ -153,8 +126,7 @@ function rowData(cols: readonly Column[], row: number, fieldOf: (c: Column) => s
|
|
|
153
126
|
const data: Record<string, unknown> = {};
|
|
154
127
|
for (const c of cols) {
|
|
155
128
|
if (c.isSet(row)) {
|
|
156
|
-
|
|
157
|
-
data[fieldOf(c)] = isColor(c) ? cssColor(v as ArrayLike<number>) : plain(v);
|
|
129
|
+
data[fieldOf(c)] = plain(c.value(row));
|
|
158
130
|
}
|
|
159
131
|
}
|
|
160
132
|
return data;
|
|
@@ -287,30 +259,12 @@ function column(values: unknown[], label: boolean): ColumnInput | undefined {
|
|
|
287
259
|
return { data: values.map((v) => v ?? undefined), decl: { dtype: "json" } };
|
|
288
260
|
}
|
|
289
261
|
|
|
290
|
-
/**
|
|
291
|
-
* A string column of CSS colors as a color column (role "color", RGBA in 0..1), which GEXF writes as viz:color.
|
|
292
|
-
* @param c - the column
|
|
293
|
-
* @param parse - reads one color
|
|
294
|
-
* @returns the color column, or undefined when it is not a string column or a value is not a color `parse` reads
|
|
295
|
-
*/
|
|
296
|
-
function colorColumn(c: ColumnInput, parse: ColorParser): ColumnInput | undefined {
|
|
297
|
-
if (c.decl.dtype !== "string") {
|
|
298
|
-
return undefined;
|
|
299
|
-
}
|
|
300
|
-
const rows = (c.data as (string | null)[]).map((v) => (v === null ? null : parse(v)));
|
|
301
|
-
if (rows.includes(undefined)) {
|
|
302
|
-
return undefined;
|
|
303
|
-
}
|
|
304
|
-
return { data: rows, decl: { dtype: "f32", components: 4, role: "color" } };
|
|
305
|
-
}
|
|
306
|
-
|
|
307
262
|
/**
|
|
308
263
|
* The columns of a set of elements' data, one per field, skipping the reserved ones.
|
|
309
264
|
* @param records - each element's data
|
|
310
|
-
* @param colors - when given, a `color` field whose every value it reads is written as the role "color"
|
|
311
265
|
* @returns the columns by name
|
|
312
266
|
*/
|
|
313
|
-
function columns(records: readonly Record<string, unknown>[]
|
|
267
|
+
function columns(records: readonly Record<string, unknown>[]): Record<string, ColumnInput> {
|
|
314
268
|
const names = new Set<string>();
|
|
315
269
|
for (const r of records) {
|
|
316
270
|
for (const k of Object.keys(r)) {
|
|
@@ -326,7 +280,7 @@ function columns(records: readonly Record<string, unknown>[], colors?: ColorPars
|
|
|
326
280
|
name === "label",
|
|
327
281
|
);
|
|
328
282
|
if (c !== undefined) {
|
|
329
|
-
out[name] =
|
|
283
|
+
out[name] = c;
|
|
330
284
|
}
|
|
331
285
|
}
|
|
332
286
|
return out;
|
|
@@ -342,8 +296,6 @@ function columns(records: readonly Record<string, unknown>[], colors?: ColorPars
|
|
|
342
296
|
* @param eles - the collection
|
|
343
297
|
* @param options - the direction and the id type
|
|
344
298
|
* @param options.directed - write a directed graph (default false)
|
|
345
|
-
* @param options.colors - reads a CSS color as RGBA in 0..1; when given, a `color` data field whose every value it
|
|
346
|
-
* reads is written as the role "color", for a format with a color slot of its own (GEXF's viz:color)
|
|
347
299
|
* @param options.integerIds - when every node id is a non-negative integer written plainly ("0", "17"), give the
|
|
348
300
|
* snapshot number ids, so a format that holds only integer ids (GML, CX2) writes them as they are (default false)
|
|
349
301
|
* @returns the snapshot
|
|
@@ -353,7 +305,6 @@ export function elementsToSnapshot(
|
|
|
353
305
|
options: {
|
|
354
306
|
readonly directed?: boolean | undefined;
|
|
355
307
|
readonly integerIds?: boolean | undefined;
|
|
356
|
-
readonly colors?: ColorParser | undefined;
|
|
357
308
|
} = {},
|
|
358
309
|
): GraphSnapshot {
|
|
359
310
|
const nodes = eles.nodes();
|
|
@@ -378,10 +329,7 @@ export function elementsToSnapshot(
|
|
|
378
329
|
xy[2 * i + 1] = flipY(p.y);
|
|
379
330
|
});
|
|
380
331
|
const parents = nodes.map((n) => index.get(n.parent().first().id()) ?? null);
|
|
381
|
-
const edgeColumns = columns(
|
|
382
|
-
edges.map((e) => e.data() as Record<string, unknown>),
|
|
383
|
-
options.colors,
|
|
384
|
-
);
|
|
332
|
+
const edgeColumns = columns(edges.map((e) => e.data() as Record<string, unknown>));
|
|
385
333
|
let weights: Float64Array<ArrayBuffer> | undefined;
|
|
386
334
|
const w = edgeColumns.weight;
|
|
387
335
|
if (w?.decl.dtype === "f64" && (w.data as unknown[]).every((v) => v !== null)) {
|
|
@@ -395,10 +343,7 @@ export function elementsToSnapshot(
|
|
|
395
343
|
dst,
|
|
396
344
|
weights,
|
|
397
345
|
nodeColumns: {
|
|
398
|
-
...columns(
|
|
399
|
-
nodes.map((n) => n.data() as Record<string, unknown>),
|
|
400
|
-
options.colors,
|
|
401
|
-
),
|
|
346
|
+
...columns(nodes.map((n) => n.data() as Record<string, unknown>)),
|
|
402
347
|
position: { data: xy, decl: { dtype: "f64", components: 2, role: "position" } },
|
|
403
348
|
...(parents.some((p) => p !== null)
|
|
404
349
|
? { parent: { data: parents, decl: { dtype: "u32", role: "parent", refersTo: "node" } } }
|
package/src/gpu.ts
CHANGED
|
@@ -57,6 +57,14 @@ export class GpuUnavailableError extends Error {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The error a call on a destroyed Cytoscape core rejects with, or a layout run started on one emits as layouterror.
|
|
62
|
+
* @returns the error, `code` "E_CORE_DESTROYED"
|
|
63
|
+
*/
|
|
64
|
+
export function coreDestroyedError(): Error & { readonly code: "E_CORE_DESTROYED" } {
|
|
65
|
+
return Object.assign(new Error("graphty: the Cytoscape core was destroyed"), { code: "E_CORE_DESTROYED" as const });
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
/** Probes, constructs and verifies a device, or throws GpuUnavailableError. */
|
|
61
69
|
export interface GpuProvider {
|
|
62
70
|
acquire(): Promise<AcquiredGpu>;
|
|
@@ -333,7 +341,7 @@ export async function gpuFor(cy: Core, mode: GpuMode = "auto"): Promise<Decision
|
|
|
333
341
|
d = { gpu: null, ...src };
|
|
334
342
|
} else {
|
|
335
343
|
if (g.destroyed) {
|
|
336
|
-
throw
|
|
344
|
+
throw coreDestroyedError();
|
|
337
345
|
}
|
|
338
346
|
if (g.decision === null || g.by !== src) {
|
|
339
347
|
disposeWhenDecided(g.decision);
|
package/src/graph-data.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ImportInput, ImportReport } from "@graphty/graph-io";
|
|
|
8
8
|
import type { FetchDatasetOptions } from "@graphty/graph-samples";
|
|
9
9
|
import type { Collection, CollectionReturnValue, Core, ElementDefinition } from "cytoscape";
|
|
10
10
|
|
|
11
|
+
import { coreDestroyedError } from "./gpu.js";
|
|
11
12
|
import type { ExportFormat, ExportOptions, ImportFormat, ImportOptions } from "./io.js";
|
|
12
13
|
import { loadPart } from "./lazy.js";
|
|
13
14
|
import type { GeneratorName, GeneratorOptions } from "./samples.js";
|
|
@@ -108,8 +109,13 @@ export class IdTakenError extends Error {
|
|
|
108
109
|
* @param elements - the definitions; edge data may be changed
|
|
109
110
|
* @returns the added elements
|
|
110
111
|
* @throws IdTakenError naming the first node id the core already holds; nothing is added
|
|
112
|
+
* @throws Error coded E_CORE_DESTROYED when the core was destroyed (before the call or while it loaded); nothing
|
|
113
|
+
* is added
|
|
111
114
|
*/
|
|
112
115
|
function addTo(cy: Core, elements: ElementDefinition[]): CollectionReturnValue {
|
|
116
|
+
if (cy.destroyed()) {
|
|
117
|
+
throw coreDestroyedError();
|
|
118
|
+
}
|
|
113
119
|
for (const el of elements) {
|
|
114
120
|
if (el.group === "nodes" && el.data.id !== undefined && cy.getElementById(el.data.id).nonempty()) {
|
|
115
121
|
throw new IdTakenError(el.data.id);
|
package/src/io.ts
CHANGED
|
@@ -163,46 +163,6 @@ function lostForCytoscape(n: LossNote): boolean {
|
|
|
163
163
|
return !(n.code === "W_ROLE_DROPPED" && n.column === "label");
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
/**
|
|
167
|
-
* A CSS color as RGBA channels in 0..1: `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb()`, `rgba()`, `hsl()` and
|
|
168
|
-
* `hsla()` with comma-separated arguments. A named color ("red") is not read.
|
|
169
|
-
* @param text - the color
|
|
170
|
-
* @returns the channels, or undefined when the text is not one of those forms
|
|
171
|
-
*/
|
|
172
|
-
function parseCssColor(text: string): [number, number, number, number] | undefined {
|
|
173
|
-
const s = text.trim().toLowerCase();
|
|
174
|
-
const hex = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(s)?.[1];
|
|
175
|
-
if (hex !== undefined) {
|
|
176
|
-
const pairs = hex.length <= 4 ? Array.from(hex, (h) => h + h) : (hex.match(/../g) ?? []);
|
|
177
|
-
const [r, g, b, a = 255] = pairs.map((h) => Number.parseInt(h, 16) / 255);
|
|
178
|
-
return [r, g, b, a === 255 ? 1 : a];
|
|
179
|
-
}
|
|
180
|
-
const fn = /^(rgba?|hsla?)\(([^)]*)\)$/.exec(s);
|
|
181
|
-
const args = fn?.[2].split(",").map((a) => a.trim());
|
|
182
|
-
if (fn === undefined || fn === null || args === undefined || (args.length !== 3 && args.length !== 4)) {
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
185
|
-
// each argument: a number, or a percentage of `full`
|
|
186
|
-
const num = (a: string, full: number): number =>
|
|
187
|
-
a.endsWith("%") ? (Number(a.slice(0, -1)) / 100) * full : Number(a);
|
|
188
|
-
const alpha = args.length === 4 ? num(args[3], 1) : 1;
|
|
189
|
-
let rgb: number[];
|
|
190
|
-
if (fn[1].startsWith("rgb")) {
|
|
191
|
-
rgb = args.slice(0, 3).map((a) => num(a, 255) / 255);
|
|
192
|
-
} else {
|
|
193
|
-
const h = Number(args[0].replace(/deg$/, ""));
|
|
194
|
-
const sat = num(args[1], 1);
|
|
195
|
-
const l = num(args[2], 1);
|
|
196
|
-
const k = (n: number): number => (n + h / 30) % 12;
|
|
197
|
-
const c = sat * Math.min(l, 1 - l);
|
|
198
|
-
rgb = [0, 8, 4].map((n) => l - c * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1)));
|
|
199
|
-
}
|
|
200
|
-
const out = [...rgb, alpha];
|
|
201
|
-
return out.every((v) => Number.isFinite(v) && v >= 0 && v <= 1)
|
|
202
|
-
? (out as [number, number, number, number])
|
|
203
|
-
: undefined;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
166
|
/** The formats that hold only integer node ids: a graph whose ids are all integers is written with them as they are. */
|
|
207
167
|
const INTEGER_ID_FORMATS: ReadonlySet<ExportFormat> = new Set(["gml", "cx2"]);
|
|
208
168
|
|
|
@@ -229,8 +189,6 @@ export async function exportElements(
|
|
|
229
189
|
const snapshot = elementsToSnapshot(eles, {
|
|
230
190
|
directed,
|
|
231
191
|
integerIds: INTEGER_ID_FORMATS.has(format),
|
|
232
|
-
// GEXF owns the name "color" for viz:color: a plain "color" field would read back renamed
|
|
233
|
-
colors: format === "gexf" ? parseCssColor : undefined,
|
|
234
192
|
});
|
|
235
193
|
const graphOptions = {
|
|
236
194
|
sanitizeIds: "mangle" as const,
|
package/src/layouts.ts
CHANGED
|
@@ -55,7 +55,16 @@ import type {
|
|
|
55
55
|
Position,
|
|
56
56
|
} from "cytoscape";
|
|
57
57
|
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
type Backend,
|
|
60
|
+
backendOf,
|
|
61
|
+
checkGpuMode,
|
|
62
|
+
coreDestroyedError,
|
|
63
|
+
cpuWithoutAsking,
|
|
64
|
+
gpuFor,
|
|
65
|
+
type GpuMode,
|
|
66
|
+
warnIfFixable,
|
|
67
|
+
} from "./gpu.js";
|
|
59
68
|
import { type CytoscapeSnapshot, hold, indexOf, indicesOf, type NodeSelection, toSnapshot } from "./snapshot.js";
|
|
60
69
|
|
|
61
70
|
/**
|
|
@@ -363,7 +372,10 @@ export const LAYOUT_NAMES: readonly string[] = [...Object.keys(STATIC), ...Objec
|
|
|
363
372
|
*/
|
|
364
373
|
function box(bb: BoundingBox12 | BoundingBoxWH | undefined, cy: Core): BoundingBoxWH {
|
|
365
374
|
if (bb === undefined) {
|
|
366
|
-
|
|
375
|
+
// a container detached from the page reads NaN (its computed padding is empty); use Cytoscape's own size
|
|
376
|
+
// for a core without a container, 1 x 1
|
|
377
|
+
const finite = (v: number): number => (Number.isFinite(v) ? v : 1);
|
|
378
|
+
return { x1: 0, y1: 0, w: finite(cy.width()), h: finite(cy.height()) };
|
|
367
379
|
}
|
|
368
380
|
return "w" in bb ? bb : { x1: bb.x1, y1: bb.y1, w: bb.x2 - bb.x1, h: bb.y2 - bb.y1 };
|
|
369
381
|
}
|
|
@@ -567,7 +579,7 @@ function runStatic(layout: LayoutThis, fn: StaticLayout): void {
|
|
|
567
579
|
* @returns a promise only when the simulation is asynchronous
|
|
568
580
|
*/
|
|
569
581
|
function stepToEnd(sim: LayoutSimulation, layout: LayoutThis): void | Promise<void> {
|
|
570
|
-
while (!sim.settled && !layout.stopped) {
|
|
582
|
+
while (!sim.settled && !layout.stopped && !layout.options.cy.destroyed()) {
|
|
571
583
|
const r = sim.step(50);
|
|
572
584
|
if (r instanceof Promise) {
|
|
573
585
|
return r.then(() => stepToEnd(sim, layout));
|
|
@@ -610,7 +622,7 @@ function runSimulation(layout: LayoutThis, type: SimulationType): void {
|
|
|
610
622
|
layout.looping = false;
|
|
611
623
|
layout.backend = backendOf(d, d.gpu !== null, "");
|
|
612
624
|
warnIfFixable(d, o.eles.nodes().length);
|
|
613
|
-
if (layout.stopped) {
|
|
625
|
+
if (layout.stopped || o.cy.destroyed()) {
|
|
614
626
|
layout.emit({ type: "layoutstop", layout });
|
|
615
627
|
return;
|
|
616
628
|
}
|
|
@@ -618,7 +630,10 @@ function runSimulation(layout: LayoutThis, type: SimulationType): void {
|
|
|
618
630
|
})
|
|
619
631
|
.catch((error: unknown) => {
|
|
620
632
|
layout.looping = false;
|
|
621
|
-
|
|
633
|
+
// the core destroyed meanwhile ends the run as stop() would
|
|
634
|
+
if (!o.cy.destroyed()) {
|
|
635
|
+
layout.emit("layouterror", [error]);
|
|
636
|
+
}
|
|
622
637
|
layout.emit({ type: "layoutstop", layout });
|
|
623
638
|
});
|
|
624
639
|
}
|
|
@@ -658,12 +673,7 @@ function simulate(layout: LayoutThis, type: SimulationType, accelerator: LayoutA
|
|
|
658
673
|
`graphty-spring-electrical has no CPU simulation and runs only on the GPU; no GPU ran because ${reason}`,
|
|
659
674
|
);
|
|
660
675
|
// reported as gpu: "require" reports it, after run() returns, whether or not the GPU was asked for
|
|
661
|
-
layout
|
|
662
|
-
void Promise.resolve().then(() => {
|
|
663
|
-
layout.looping = false;
|
|
664
|
-
layout.emit("layouterror", [error]);
|
|
665
|
-
layout.emit({ type: "layoutstop", layout });
|
|
666
|
-
});
|
|
676
|
+
failLater(layout, error);
|
|
667
677
|
return;
|
|
668
678
|
}
|
|
669
679
|
const cs = layoutSnapshot(o);
|
|
@@ -735,10 +745,14 @@ function simulate(layout: LayoutThis, type: SimulationType, accelerator: LayoutA
|
|
|
735
745
|
|
|
736
746
|
// Fitted to the box as a static result is; with a locked node, the free nodes are scaled about the locked ones
|
|
737
747
|
const xy = (): ArrayLike<number> => (anyLocked ? fitAroundLocked(pos, fixed, n, b) : fitRows(pos, 3, n, b));
|
|
748
|
+
// A core destroyed mid-run ends the run as stop() would: its device is gone with it ("GpuContext is disposed"),
|
|
749
|
+
// and a write to it fails in Cytoscape ("reading 'notify'" of its null renderer).
|
|
738
750
|
const fail = (error: unknown): void => {
|
|
739
751
|
layout.looping = false;
|
|
740
752
|
dispose();
|
|
741
|
-
|
|
753
|
+
if (!o.cy.destroyed()) {
|
|
754
|
+
layout.emit("layouterror", [error]);
|
|
755
|
+
}
|
|
742
756
|
layout.emit({ type: "layoutstop", layout });
|
|
743
757
|
};
|
|
744
758
|
|
|
@@ -746,7 +760,11 @@ function simulate(layout: LayoutThis, type: SimulationType, accelerator: LayoutA
|
|
|
746
760
|
const done = (): void => {
|
|
747
761
|
layout.looping = false;
|
|
748
762
|
dispose();
|
|
749
|
-
|
|
763
|
+
if (o.cy.destroyed()) {
|
|
764
|
+
layout.emit({ type: "layoutstop", layout });
|
|
765
|
+
} else {
|
|
766
|
+
finishDiscrete(layout, cs.nodes, xy());
|
|
767
|
+
}
|
|
750
768
|
};
|
|
751
769
|
const r = stepToEnd(sim, layout);
|
|
752
770
|
if (r === undefined) {
|
|
@@ -766,7 +784,19 @@ function simulate(layout: LayoutThis, type: SimulationType, accelerator: LayoutA
|
|
|
766
784
|
typeof requestAnimationFrame === "function"
|
|
767
785
|
? requestAnimationFrame
|
|
768
786
|
: (f: () => void): unknown => setTimeout(f, 16);
|
|
787
|
+
const end = (): void => {
|
|
788
|
+
layout.looping = false;
|
|
789
|
+
dispose();
|
|
790
|
+
if (o.stop !== undefined) {
|
|
791
|
+
layout.one("layoutstop", o.stop);
|
|
792
|
+
}
|
|
793
|
+
layout.emit({ type: "layoutstop", layout });
|
|
794
|
+
};
|
|
769
795
|
const draw = (): void => {
|
|
796
|
+
if (o.cy.destroyed()) {
|
|
797
|
+
end();
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
770
800
|
cs.nodes.positions(positionOf(xy()));
|
|
771
801
|
if (o.fit !== false) {
|
|
772
802
|
o.cy.fit(o.eles, o.padding);
|
|
@@ -779,18 +809,13 @@ function simulate(layout: LayoutThis, type: SimulationType, accelerator: LayoutA
|
|
|
779
809
|
layout.emit({ type: "layoutready", layout });
|
|
780
810
|
}
|
|
781
811
|
if (sim.settled || layout.stopped) {
|
|
782
|
-
|
|
783
|
-
dispose();
|
|
784
|
-
if (o.stop !== undefined) {
|
|
785
|
-
layout.one("layoutstop", o.stop);
|
|
786
|
-
}
|
|
787
|
-
layout.emit({ type: "layoutstop", layout });
|
|
812
|
+
end();
|
|
788
813
|
} else {
|
|
789
814
|
schedule(tick);
|
|
790
815
|
}
|
|
791
816
|
};
|
|
792
817
|
const tick = (): void => {
|
|
793
|
-
if (layout.stopped) {
|
|
818
|
+
if (layout.stopped || o.cy.destroyed()) {
|
|
794
819
|
draw();
|
|
795
820
|
return;
|
|
796
821
|
}
|
|
@@ -810,15 +835,33 @@ function simulate(layout: LayoutThis, type: SimulationType, accelerator: LayoutA
|
|
|
810
835
|
tick();
|
|
811
836
|
}
|
|
812
837
|
|
|
838
|
+
/**
|
|
839
|
+
* Ends a run that never started: layouterror with the error, then layoutstop, after run() returns.
|
|
840
|
+
* @param layout - the layout
|
|
841
|
+
* @param error - the error
|
|
842
|
+
*/
|
|
843
|
+
function failLater(layout: LayoutThis, error: Error): void {
|
|
844
|
+
layout.looping = true;
|
|
845
|
+
void Promise.resolve().then(() => {
|
|
846
|
+
layout.looping = false;
|
|
847
|
+
layout.emit("layouterror", [error]);
|
|
848
|
+
layout.emit({ type: "layoutstop", layout });
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
813
852
|
/** Each layout object's run body, set by its registrant's constructor. */
|
|
814
853
|
const RUN_BODIES = new WeakMap<LayoutThis, (layout: LayoutThis) => void>();
|
|
815
854
|
|
|
816
855
|
/**
|
|
817
|
-
* A registrant's run(): runs the layout's body.
|
|
856
|
+
* A registrant's run(): runs the layout's body, or refuses a destroyed core with E_CORE_DESTROYED.
|
|
818
857
|
* @returns the layout
|
|
819
858
|
*/
|
|
820
859
|
function runLayout(this: LayoutThis): LayoutThis {
|
|
821
|
-
|
|
860
|
+
if (this.options.cy.destroyed()) {
|
|
861
|
+
failLater(this, coreDestroyedError());
|
|
862
|
+
} else {
|
|
863
|
+
RUN_BODIES.get(this)?.(this);
|
|
864
|
+
}
|
|
822
865
|
return this;
|
|
823
866
|
}
|
|
824
867
|
|