@bruno00o/graph 0.0.0-stage → 0.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.
- package/LICENSE +21 -0
- package/README.md +119 -2
- package/dist/db.d.ts +14 -0
- package/dist/db.js +85 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/mount-graph-B88WGOQu.d.ts +133 -0
- package/dist/mount-graph-DYOgD_ql.js +4343 -0
- package/dist/mount-graph-DYOgD_ql.js.map +1 -0
- package/dist/query.d.ts +30 -0
- package/dist/query.js +182 -0
- package/dist/query.js.map +1 -0
- package/dist/react.d.ts +35 -0
- package/dist/react.js +132 -0
- package/dist/react.js.map +1 -0
- package/dist/theme.css +90 -0
- package/dist/types-D6b86Rt1.d.ts +156 -0
- package/package.json +104 -4
|
@@ -0,0 +1,4343 @@
|
|
|
1
|
+
import { createStore } from "@tanstack/store";
|
|
2
|
+
import TinySDF from "@mapbox/tiny-sdf";
|
|
3
|
+
// src/core/state-names.ts
|
|
4
|
+
const builtInStates = [
|
|
5
|
+
"selected",
|
|
6
|
+
"hovered",
|
|
7
|
+
"focused"
|
|
8
|
+
];
|
|
9
|
+
const maxFreeStates = 32 - builtInStates.length;
|
|
10
|
+
function isBuiltInState(name) {
|
|
11
|
+
return builtInStates.includes(name);
|
|
12
|
+
}
|
|
13
|
+
function freeStateNames(states) {
|
|
14
|
+
return Object.keys(states).filter((name) => !isBuiltInState(name));
|
|
15
|
+
}
|
|
16
|
+
// src/core/accessor.ts
|
|
17
|
+
function toAccessor(channel) {
|
|
18
|
+
if (typeof channel === "function") return channel;
|
|
19
|
+
return (datum) => datum === null || datum === void 0 ? void 0 : datum[channel];
|
|
20
|
+
}
|
|
21
|
+
function isKey(value) {
|
|
22
|
+
return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
23
|
+
}
|
|
24
|
+
// src/core/index-pool.ts
|
|
25
|
+
var IndexPool = class {
|
|
26
|
+
#next = 0;
|
|
27
|
+
#free = [];
|
|
28
|
+
get capacity() {
|
|
29
|
+
return this.#next;
|
|
30
|
+
}
|
|
31
|
+
acquire() {
|
|
32
|
+
return this.#free.pop() ?? this.#next++;
|
|
33
|
+
}
|
|
34
|
+
release(index) {
|
|
35
|
+
this.#free.push(index);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
// src/core/live-source.ts
|
|
39
|
+
function isLiveSource(data) {
|
|
40
|
+
return typeof data === "object" && data !== null && typeof data.subscribeChanges === "function";
|
|
41
|
+
}
|
|
42
|
+
// src/core/shallow-equal.ts
|
|
43
|
+
function shallowEqual(a, b) {
|
|
44
|
+
if (a === b) return true;
|
|
45
|
+
const keys = Object.keys(a);
|
|
46
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
47
|
+
return keys.every((key) => Object.hasOwn(b, key) && Object.is(a[key], b[key]));
|
|
48
|
+
}
|
|
49
|
+
// src/core/source-table.ts
|
|
50
|
+
var SourceTable = class {
|
|
51
|
+
dirty = /* @__PURE__ */ new Map();
|
|
52
|
+
remarked = [];
|
|
53
|
+
#kind;
|
|
54
|
+
#issues;
|
|
55
|
+
#notify;
|
|
56
|
+
#sources = [];
|
|
57
|
+
constructor(kind, issues, notify) {
|
|
58
|
+
this.#kind = kind;
|
|
59
|
+
this.#issues = issues;
|
|
60
|
+
this.#notify = notify;
|
|
61
|
+
}
|
|
62
|
+
setMarks(marks) {
|
|
63
|
+
const previous = this.#sources;
|
|
64
|
+
this.#sources = marks.map((mark, slot) => this.#reconcile(slot, mark, previous[slot]));
|
|
65
|
+
for (const old of previous.slice(marks.length)) this.#retire(old);
|
|
66
|
+
}
|
|
67
|
+
drain() {
|
|
68
|
+
for (const source of this.#sources) {
|
|
69
|
+
for (const change of source.queue) {
|
|
70
|
+
if (change.type === "delete") source.items.delete(change.key);
|
|
71
|
+
else source.items.set(change.key, change.value);
|
|
72
|
+
this.#markDirty(change.key);
|
|
73
|
+
}
|
|
74
|
+
source.queue.length = 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
owner(key) {
|
|
78
|
+
let owner;
|
|
79
|
+
for (const source of this.#sources) {
|
|
80
|
+
if (!source.items.has(key)) continue;
|
|
81
|
+
if (owner) {
|
|
82
|
+
this.#issues.push({
|
|
83
|
+
type: "duplicate-key",
|
|
84
|
+
kind: this.#kind,
|
|
85
|
+
key
|
|
86
|
+
});
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
owner = {
|
|
90
|
+
source,
|
|
91
|
+
datum: source.items.get(key)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return owner;
|
|
95
|
+
}
|
|
96
|
+
destroy() {
|
|
97
|
+
for (const source of this.#sources) {
|
|
98
|
+
source.retired = true;
|
|
99
|
+
source.subscription?.unsubscribe();
|
|
100
|
+
source.subscription = null;
|
|
101
|
+
}
|
|
102
|
+
this.#sources = [];
|
|
103
|
+
this.dirty.clear();
|
|
104
|
+
this.remarked.length = 0;
|
|
105
|
+
}
|
|
106
|
+
#reconcile(slot, mark, old) {
|
|
107
|
+
if (old?.mark === mark) return old;
|
|
108
|
+
const optionsChanged = old !== void 0 && !shallowEqual(old.mark.options, mark.options);
|
|
109
|
+
const data = mark.data;
|
|
110
|
+
if (isLiveSource(data)) {
|
|
111
|
+
if (old?.live === data) {
|
|
112
|
+
old.mark = mark;
|
|
113
|
+
this.#refresh(old, optionsChanged);
|
|
114
|
+
return old;
|
|
115
|
+
}
|
|
116
|
+
if (old) this.#retire(old);
|
|
117
|
+
const source = this.#create(slot, mark, data);
|
|
118
|
+
source.subscription = data.subscribeChanges((changes) => {
|
|
119
|
+
if (source.retired || changes.length === 0) return;
|
|
120
|
+
for (const change of changes) source.queue.push(change);
|
|
121
|
+
this.#notify();
|
|
122
|
+
}, { includeInitialState: true });
|
|
123
|
+
return source;
|
|
124
|
+
}
|
|
125
|
+
const source = this.#create(slot, mark, null);
|
|
126
|
+
this.#collect(source, data);
|
|
127
|
+
if (!old || old.live) {
|
|
128
|
+
if (old) this.#retire(old);
|
|
129
|
+
for (const key of source.items.keys()) this.#markDirty(key);
|
|
130
|
+
return source;
|
|
131
|
+
}
|
|
132
|
+
old.retired = true;
|
|
133
|
+
for (const key of old.items.keys()) if (!source.items.has(key)) this.#markDirty(key);
|
|
134
|
+
for (const [key, datum] of source.items) if (optionsChanged || !old.items.has(key) || old.items.get(key) !== datum) this.#markDirty(key, optionsChanged);
|
|
135
|
+
if (!optionsChanged) this.remarked.push(source);
|
|
136
|
+
return source;
|
|
137
|
+
}
|
|
138
|
+
#refresh(source, optionsChanged) {
|
|
139
|
+
if (!optionsChanged) {
|
|
140
|
+
this.remarked.push(source);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
for (const key of source.items.keys()) this.#markDirty(key, true);
|
|
144
|
+
}
|
|
145
|
+
#collect(source, data) {
|
|
146
|
+
const keyOf = toAccessor(source.mark.options.key ?? "id");
|
|
147
|
+
for (const datum of data) {
|
|
148
|
+
const key = keyOf(datum);
|
|
149
|
+
if (!isKey(key)) {
|
|
150
|
+
this.#issues.push({
|
|
151
|
+
type: "missing-key",
|
|
152
|
+
kind: this.#kind,
|
|
153
|
+
datum
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (source.items.has(key)) {
|
|
158
|
+
this.#issues.push({
|
|
159
|
+
type: "duplicate-key",
|
|
160
|
+
kind: this.#kind,
|
|
161
|
+
key
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
source.items.set(key, datum);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
#create(slot, mark, live) {
|
|
169
|
+
return {
|
|
170
|
+
slot,
|
|
171
|
+
mark,
|
|
172
|
+
live,
|
|
173
|
+
items: /* @__PURE__ */ new Map(),
|
|
174
|
+
queue: [],
|
|
175
|
+
subscription: null,
|
|
176
|
+
retired: false
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
#retire(source) {
|
|
180
|
+
source.retired = true;
|
|
181
|
+
source.subscription?.unsubscribe();
|
|
182
|
+
source.subscription = null;
|
|
183
|
+
for (const key of source.items.keys()) this.#markDirty(key);
|
|
184
|
+
}
|
|
185
|
+
#markDirty(key, forced = false) {
|
|
186
|
+
this.dirty.set(key, forced || this.dirty.get(key) === true);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
// src/core/graph-model.ts
|
|
190
|
+
function isNodeMark(mark) {
|
|
191
|
+
return mark.type === "nodes";
|
|
192
|
+
}
|
|
193
|
+
function isEdgeMark(mark) {
|
|
194
|
+
return mark.type === "edges";
|
|
195
|
+
}
|
|
196
|
+
function isVisible(entry) {
|
|
197
|
+
return entry.index >= 0;
|
|
198
|
+
}
|
|
199
|
+
function endpoint(channel, datum) {
|
|
200
|
+
const value = toAccessor(channel)(datum);
|
|
201
|
+
return isKey(value) ? value : null;
|
|
202
|
+
}
|
|
203
|
+
function emptyChanges() {
|
|
204
|
+
return {
|
|
205
|
+
added: [],
|
|
206
|
+
updated: [],
|
|
207
|
+
removed: []
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function remark(sources, entries) {
|
|
211
|
+
for (const source of sources) {
|
|
212
|
+
if (source.retired) continue;
|
|
213
|
+
for (const key of source.items.keys()) {
|
|
214
|
+
const entry = entries.get(key);
|
|
215
|
+
if (entry?.slot === source.slot) entry.mark = source.mark;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
sources.length = 0;
|
|
219
|
+
}
|
|
220
|
+
function createGraphModel(options = {}) {
|
|
221
|
+
const schedule = options.schedule ?? (() => {});
|
|
222
|
+
const issues = [];
|
|
223
|
+
const nodeSources = new SourceTable("nodes", issues, schedule);
|
|
224
|
+
const edgeSources = new SourceTable("edges", issues, schedule);
|
|
225
|
+
const nodeEntries = /* @__PURE__ */ new Map();
|
|
226
|
+
const edgeEntries = /* @__PURE__ */ new Map();
|
|
227
|
+
const incident = /* @__PURE__ */ new Map();
|
|
228
|
+
const nodePool = new IndexPool();
|
|
229
|
+
const edgePool = new IndexPool();
|
|
230
|
+
function link(entry) {
|
|
231
|
+
for (const key of [entry.source, entry.target]) {
|
|
232
|
+
if (key === null) continue;
|
|
233
|
+
let edges = incident.get(key);
|
|
234
|
+
if (!edges) {
|
|
235
|
+
edges = /* @__PURE__ */ new Set();
|
|
236
|
+
incident.set(key, edges);
|
|
237
|
+
}
|
|
238
|
+
edges.add(entry.key);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function unlink(entry) {
|
|
242
|
+
for (const key of [entry.source, entry.target]) {
|
|
243
|
+
if (key === null) continue;
|
|
244
|
+
const edges = incident.get(key);
|
|
245
|
+
edges?.delete(entry.key);
|
|
246
|
+
if (edges?.size === 0) incident.delete(key);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function visibleIncident(key) {
|
|
250
|
+
const result = [];
|
|
251
|
+
for (const edgeKey of incident.get(key) ?? []) {
|
|
252
|
+
const entry = edgeEntries.get(edgeKey);
|
|
253
|
+
if (entry && isVisible(entry)) result.push(entry);
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
function flushNodes(changes, touched, freed) {
|
|
258
|
+
remark(nodeSources.remarked, nodeEntries);
|
|
259
|
+
const touch = (key) => {
|
|
260
|
+
for (const edgeKey of incident.get(key) ?? []) touched.add(edgeKey);
|
|
261
|
+
};
|
|
262
|
+
for (const [key, forced] of nodeSources.dirty) {
|
|
263
|
+
const owner = nodeSources.owner(key);
|
|
264
|
+
const entry = nodeEntries.get(key);
|
|
265
|
+
if (!owner) {
|
|
266
|
+
if (entry) {
|
|
267
|
+
nodeEntries.delete(key);
|
|
268
|
+
changes.removed.push({
|
|
269
|
+
key,
|
|
270
|
+
index: entry.index
|
|
271
|
+
});
|
|
272
|
+
freed.push(entry.index);
|
|
273
|
+
touch(key);
|
|
274
|
+
}
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const { mark, slot } = owner.source;
|
|
278
|
+
if (!entry) {
|
|
279
|
+
const index = nodePool.acquire();
|
|
280
|
+
nodeEntries.set(key, {
|
|
281
|
+
key,
|
|
282
|
+
index,
|
|
283
|
+
datum: owner.datum,
|
|
284
|
+
mark,
|
|
285
|
+
slot
|
|
286
|
+
});
|
|
287
|
+
changes.added.push({
|
|
288
|
+
key,
|
|
289
|
+
index
|
|
290
|
+
});
|
|
291
|
+
touch(key);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (forced || entry.datum !== owner.datum || entry.slot !== slot) changes.updated.push({
|
|
295
|
+
key,
|
|
296
|
+
index: entry.index
|
|
297
|
+
});
|
|
298
|
+
entry.datum = owner.datum;
|
|
299
|
+
entry.mark = mark;
|
|
300
|
+
entry.slot = slot;
|
|
301
|
+
}
|
|
302
|
+
nodeSources.dirty.clear();
|
|
303
|
+
}
|
|
304
|
+
function flushEdges(changes, touched, freed) {
|
|
305
|
+
remark(edgeSources.remarked, edgeEntries);
|
|
306
|
+
const dirty = edgeSources.dirty;
|
|
307
|
+
for (const key of /* @__PURE__ */ new Set([...dirty.keys(), ...touched])) {
|
|
308
|
+
let entry = edgeEntries.get(key);
|
|
309
|
+
const wasVisible = entry !== void 0 && isVisible(entry);
|
|
310
|
+
const wasPending = entry !== void 0 && !wasVisible;
|
|
311
|
+
let changed = false;
|
|
312
|
+
if (dirty.has(key)) {
|
|
313
|
+
const owner = edgeSources.owner(key);
|
|
314
|
+
if (!owner) {
|
|
315
|
+
if (entry) {
|
|
316
|
+
unlink(entry);
|
|
317
|
+
edgeEntries.delete(key);
|
|
318
|
+
if (wasVisible) {
|
|
319
|
+
changes.removed.push({
|
|
320
|
+
key,
|
|
321
|
+
index: entry.index
|
|
322
|
+
});
|
|
323
|
+
freed.push(entry.index);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const { mark, slot } = owner.source;
|
|
329
|
+
const source = endpoint(mark.options.source, owner.datum);
|
|
330
|
+
const target = endpoint(mark.options.target, owner.datum);
|
|
331
|
+
if (!entry) {
|
|
332
|
+
entry = {
|
|
333
|
+
key,
|
|
334
|
+
index: -1,
|
|
335
|
+
datum: owner.datum,
|
|
336
|
+
mark,
|
|
337
|
+
slot,
|
|
338
|
+
source,
|
|
339
|
+
target
|
|
340
|
+
};
|
|
341
|
+
edgeEntries.set(key, entry);
|
|
342
|
+
link(entry);
|
|
343
|
+
} else {
|
|
344
|
+
const moved = entry.source !== source || entry.target !== target;
|
|
345
|
+
changed = moved || dirty.get(key) === true || entry.datum !== owner.datum;
|
|
346
|
+
changed ||= entry.slot !== slot;
|
|
347
|
+
if (moved) {
|
|
348
|
+
unlink(entry);
|
|
349
|
+
entry.source = source;
|
|
350
|
+
entry.target = target;
|
|
351
|
+
link(entry);
|
|
352
|
+
}
|
|
353
|
+
entry.datum = owner.datum;
|
|
354
|
+
entry.mark = mark;
|
|
355
|
+
entry.slot = slot;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (!entry) continue;
|
|
359
|
+
const visible = entry.source !== null && entry.target !== null && nodeEntries.has(entry.source) && nodeEntries.has(entry.target);
|
|
360
|
+
if (visible && !wasVisible) {
|
|
361
|
+
entry.index = edgePool.acquire();
|
|
362
|
+
changes.added.push({
|
|
363
|
+
key,
|
|
364
|
+
index: entry.index
|
|
365
|
+
});
|
|
366
|
+
} else if (!visible && wasVisible) {
|
|
367
|
+
changes.removed.push({
|
|
368
|
+
key,
|
|
369
|
+
index: entry.index
|
|
370
|
+
});
|
|
371
|
+
freed.push(entry.index);
|
|
372
|
+
entry.index = -1;
|
|
373
|
+
changes.pending.push(key);
|
|
374
|
+
} else if (visible && changed) changes.updated.push({
|
|
375
|
+
key,
|
|
376
|
+
index: entry.index
|
|
377
|
+
});
|
|
378
|
+
else if (!visible && !wasPending) changes.pending.push(key);
|
|
379
|
+
}
|
|
380
|
+
dirty.clear();
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
setMarks(marks) {
|
|
384
|
+
nodeSources.setMarks(marks.filter(isNodeMark));
|
|
385
|
+
edgeSources.setMarks(marks.filter(isEdgeMark));
|
|
386
|
+
if (nodeSources.dirty.size > 0 || edgeSources.dirty.size > 0 || nodeSources.remarked.length > 0 || edgeSources.remarked.length > 0 || issues.length > 0) schedule();
|
|
387
|
+
},
|
|
388
|
+
flush() {
|
|
389
|
+
nodeSources.drain();
|
|
390
|
+
edgeSources.drain();
|
|
391
|
+
const nodes = emptyChanges();
|
|
392
|
+
const edges = {
|
|
393
|
+
...emptyChanges(),
|
|
394
|
+
pending: []
|
|
395
|
+
};
|
|
396
|
+
const touched = /* @__PURE__ */ new Set();
|
|
397
|
+
const freedNodes = [];
|
|
398
|
+
const freedEdges = [];
|
|
399
|
+
flushNodes(nodes, touched, freedNodes);
|
|
400
|
+
flushEdges(edges, touched, freedEdges);
|
|
401
|
+
for (const index of freedNodes) nodePool.release(index);
|
|
402
|
+
for (const index of freedEdges) edgePool.release(index);
|
|
403
|
+
return {
|
|
404
|
+
nodes,
|
|
405
|
+
edges,
|
|
406
|
+
issues: issues.splice(0)
|
|
407
|
+
};
|
|
408
|
+
},
|
|
409
|
+
node(key) {
|
|
410
|
+
return nodeEntries.get(key);
|
|
411
|
+
},
|
|
412
|
+
edge(key) {
|
|
413
|
+
const entry = edgeEntries.get(key);
|
|
414
|
+
return entry && isVisible(entry) ? entry : void 0;
|
|
415
|
+
},
|
|
416
|
+
nodes() {
|
|
417
|
+
return nodeEntries.values();
|
|
418
|
+
},
|
|
419
|
+
*edges() {
|
|
420
|
+
for (const entry of edgeEntries.values()) if (isVisible(entry)) yield entry;
|
|
421
|
+
},
|
|
422
|
+
neighbors(key) {
|
|
423
|
+
const result = /* @__PURE__ */ new Set();
|
|
424
|
+
for (const entry of visibleIncident(key)) result.add(entry.source === key ? entry.target : entry.source);
|
|
425
|
+
return [...result];
|
|
426
|
+
},
|
|
427
|
+
edgesOf(key) {
|
|
428
|
+
return visibleIncident(key).map((entry) => entry.key);
|
|
429
|
+
},
|
|
430
|
+
endpoints(key) {
|
|
431
|
+
const entry = edgeEntries.get(key);
|
|
432
|
+
return entry && isVisible(entry) ? {
|
|
433
|
+
source: entry.source,
|
|
434
|
+
target: entry.target
|
|
435
|
+
} : void 0;
|
|
436
|
+
},
|
|
437
|
+
pendingEdges() {
|
|
438
|
+
const result = [];
|
|
439
|
+
for (const entry of edgeEntries.values()) if (!isVisible(entry)) result.push(entry.key);
|
|
440
|
+
return result;
|
|
441
|
+
},
|
|
442
|
+
destroy() {
|
|
443
|
+
nodeSources.destroy();
|
|
444
|
+
edgeSources.destroy();
|
|
445
|
+
nodeEntries.clear();
|
|
446
|
+
edgeEntries.clear();
|
|
447
|
+
incident.clear();
|
|
448
|
+
issues.length = 0;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
// src/core/edge-curve.ts
|
|
453
|
+
const loopTurn = .6;
|
|
454
|
+
const loopGrowth = .5;
|
|
455
|
+
const maxBend = .12;
|
|
456
|
+
const spreadBend = .4;
|
|
457
|
+
function laneBend(position, count) {
|
|
458
|
+
if (count < 2) return 0;
|
|
459
|
+
const step = Math.min(maxBend, spreadBend / (count - 1));
|
|
460
|
+
return (position - (count - 1) / 2) * step;
|
|
461
|
+
}
|
|
462
|
+
function loopReach(radius, ordinal, minimum) {
|
|
463
|
+
return radius + Math.max(radius * 1.5, minimum) * (1 + loopGrowth * ordinal);
|
|
464
|
+
}
|
|
465
|
+
function parallelCurve(ax, ay, dx, dy, bend) {
|
|
466
|
+
const length = Math.hypot(dx - ax, dy - ay);
|
|
467
|
+
const nx = length > 0 ? -(dy - ay) / length : 0;
|
|
468
|
+
const ny = length > 0 ? (dx - ax) / length : 1;
|
|
469
|
+
const offset = 2 * bend * length;
|
|
470
|
+
const px = (ax + dx) / 2 + nx * offset;
|
|
471
|
+
const py = (ay + dy) / 2 + ny * offset;
|
|
472
|
+
return {
|
|
473
|
+
ax,
|
|
474
|
+
ay,
|
|
475
|
+
bx: ax + (px - ax) * 2 / 3,
|
|
476
|
+
by: ay + (py - ay) * 2 / 3,
|
|
477
|
+
cx: dx + (px - dx) * 2 / 3,
|
|
478
|
+
cy: dy + (py - dy) * 2 / 3,
|
|
479
|
+
dx,
|
|
480
|
+
dy
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function loopCurve(x, y, reach) {
|
|
484
|
+
const arm = reach / (.75 * Math.cos(loopTurn));
|
|
485
|
+
const ux = Math.SQRT1_2;
|
|
486
|
+
const uy = -Math.SQRT1_2;
|
|
487
|
+
const cos = Math.cos(loopTurn);
|
|
488
|
+
const sin = Math.sin(loopTurn);
|
|
489
|
+
return {
|
|
490
|
+
ax: x,
|
|
491
|
+
ay: y,
|
|
492
|
+
bx: x + arm * (ux * cos - uy * sin),
|
|
493
|
+
by: y + arm * (ux * sin + uy * cos),
|
|
494
|
+
cx: x + arm * (ux * cos + uy * sin),
|
|
495
|
+
cy: y + arm * (-ux * sin + uy * cos),
|
|
496
|
+
dx: x,
|
|
497
|
+
dy: y
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function pointOnCubic(curve, t) {
|
|
501
|
+
const u = 1 - t;
|
|
502
|
+
const a = u * u * u;
|
|
503
|
+
const b = 3 * u * u * t;
|
|
504
|
+
const c = 3 * u * t * t;
|
|
505
|
+
const d = t * t * t;
|
|
506
|
+
return {
|
|
507
|
+
x: a * curve.ax + b * curve.bx + c * curve.cx + d * curve.dx,
|
|
508
|
+
y: a * curve.ay + b * curve.by + c * curve.cy + d * curve.dy
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function sampleCubic(curve, segments) {
|
|
512
|
+
const points = new Float64Array((segments + 1) * 2);
|
|
513
|
+
for (let step = 0; step <= segments; step++) {
|
|
514
|
+
const point = pointOnCubic(curve, step / segments);
|
|
515
|
+
points[step * 2] = point.x;
|
|
516
|
+
points[step * 2 + 1] = point.y;
|
|
517
|
+
}
|
|
518
|
+
return points;
|
|
519
|
+
}
|
|
520
|
+
function distanceToCubic(curve, x, y, segments) {
|
|
521
|
+
const points = sampleCubic(curve, segments);
|
|
522
|
+
let best = Number.POSITIVE_INFINITY;
|
|
523
|
+
for (let step = 0; step < segments; step++) {
|
|
524
|
+
const ax = points[step * 2] ?? 0;
|
|
525
|
+
const ay = points[step * 2 + 1] ?? 0;
|
|
526
|
+
const vx = (points[step * 2 + 2] ?? 0) - ax;
|
|
527
|
+
const vy = (points[step * 2 + 3] ?? 0) - ay;
|
|
528
|
+
const squared = vx * vx + vy * vy;
|
|
529
|
+
const t = squared > 0 ? Math.min(1, Math.max(0, ((x - ax) * vx + (y - ay) * vy) / squared)) : 0;
|
|
530
|
+
best = Math.min(best, Math.hypot(x - (ax + vx * t), y - (ay + vy * t)));
|
|
531
|
+
}
|
|
532
|
+
return best;
|
|
533
|
+
}
|
|
534
|
+
// src/core/pick-index.ts
|
|
535
|
+
const minimumPickRadius = 2;
|
|
536
|
+
const pickSegments = 16;
|
|
537
|
+
const indexSegments = 8;
|
|
538
|
+
function createGrid(frame, count, visit) {
|
|
539
|
+
const cells = frame.cols * frame.rows;
|
|
540
|
+
const offsets = new Uint32Array(cells + 1);
|
|
541
|
+
for (let index = 0; index < count; index++) visit(index, (cell) => {
|
|
542
|
+
offsets[cell + 1] = (offsets[cell + 1] ?? 0) + 1;
|
|
543
|
+
});
|
|
544
|
+
for (let cell = 0; cell < cells; cell++) offsets[cell + 1] = (offsets[cell + 1] ?? 0) + (offsets[cell] ?? 0);
|
|
545
|
+
const cursor = offsets.slice(0, cells);
|
|
546
|
+
const items = new Uint32Array(offsets[cells] ?? 0);
|
|
547
|
+
for (let index = 0; index < count; index++) visit(index, (cell) => {
|
|
548
|
+
const at = cursor[cell] ?? 0;
|
|
549
|
+
items[at] = index;
|
|
550
|
+
cursor[cell] = at + 1;
|
|
551
|
+
});
|
|
552
|
+
return {
|
|
553
|
+
...frame,
|
|
554
|
+
offsets,
|
|
555
|
+
items
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function traverse(grid, ax, ay, bx, by, emit) {
|
|
559
|
+
const x0 = (ax - grid.minX) / grid.cell;
|
|
560
|
+
const y0 = (ay - grid.minY) / grid.cell;
|
|
561
|
+
const x1 = (bx - grid.minX) / grid.cell;
|
|
562
|
+
const y1 = (by - grid.minY) / grid.cell;
|
|
563
|
+
let col = Math.floor(x0);
|
|
564
|
+
let row = Math.floor(y0);
|
|
565
|
+
const lastCol = Math.floor(x1);
|
|
566
|
+
const lastRow = Math.floor(y1);
|
|
567
|
+
const stepX = Math.sign(x1 - x0);
|
|
568
|
+
const stepY = Math.sign(y1 - y0);
|
|
569
|
+
const deltaX = stepX === 0 ? Number.POSITIVE_INFINITY : Math.abs(1 / (x1 - x0));
|
|
570
|
+
const deltaY = stepY === 0 ? Number.POSITIVE_INFINITY : Math.abs(1 / (y1 - y0));
|
|
571
|
+
let maxX = stepX > 0 ? (col + 1 - x0) * deltaX : stepX < 0 ? (x0 - col) * deltaX : deltaX;
|
|
572
|
+
let maxY = stepY > 0 ? (row + 1 - y0) * deltaY : stepY < 0 ? (y0 - row) * deltaY : deltaY;
|
|
573
|
+
const limit = Math.abs(lastCol - col) + Math.abs(lastRow - row);
|
|
574
|
+
for (let step = 0; step <= limit; step++) {
|
|
575
|
+
if (col >= 0 && row >= 0 && col < grid.cols && row < grid.rows) emit(row * grid.cols + col);
|
|
576
|
+
if (col === lastCol && row === lastRow) return;
|
|
577
|
+
if (maxX < maxY) {
|
|
578
|
+
maxX += deltaX;
|
|
579
|
+
col += stepX;
|
|
580
|
+
} else {
|
|
581
|
+
maxY += deltaY;
|
|
582
|
+
row += stepY;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
var PickIndex = class {
|
|
587
|
+
#nodes = null;
|
|
588
|
+
#edges = null;
|
|
589
|
+
#maxWorldRadius = 0;
|
|
590
|
+
#maxScreenRadius = 0;
|
|
591
|
+
#maxEdgeHalfWidth = 0;
|
|
592
|
+
#loops = [];
|
|
593
|
+
build(source) {
|
|
594
|
+
const { nodes, edges, positions } = source;
|
|
595
|
+
let count = 0;
|
|
596
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
597
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
598
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
599
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
600
|
+
this.#maxWorldRadius = 0;
|
|
601
|
+
this.#maxScreenRadius = 0;
|
|
602
|
+
for (let index = 0; index < nodes.capacity; index++) {
|
|
603
|
+
if (nodes.alive[index] !== 1) continue;
|
|
604
|
+
const x = positions.x(index);
|
|
605
|
+
const y = positions.y(index);
|
|
606
|
+
const radius = (nodes.size[index] ?? 0) / 2;
|
|
607
|
+
if (nodes.screenUnits[index] === 1) this.#maxScreenRadius = Math.max(this.#maxScreenRadius, radius);
|
|
608
|
+
else this.#maxWorldRadius = Math.max(this.#maxWorldRadius, radius);
|
|
609
|
+
minX = Math.min(minX, x);
|
|
610
|
+
minY = Math.min(minY, y);
|
|
611
|
+
maxX = Math.max(maxX, x);
|
|
612
|
+
maxY = Math.max(maxY, y);
|
|
613
|
+
count++;
|
|
614
|
+
}
|
|
615
|
+
if (count === 0) {
|
|
616
|
+
this.#nodes = null;
|
|
617
|
+
this.#edges = null;
|
|
618
|
+
this.#loops = [];
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
this.#loops = [];
|
|
622
|
+
for (let index = 0; index < edges.capacity; index++) {
|
|
623
|
+
if (edges.alive[index] !== 1) continue;
|
|
624
|
+
if (edges.source[index] === edges.target[index]) {
|
|
625
|
+
this.#loops.push(index);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if ((edges.bend[index] ?? 0) === 0) continue;
|
|
629
|
+
const points = sampleCubic(this.#parallel(source, index), indexSegments);
|
|
630
|
+
for (let at = 0; at < points.length; at += 2) {
|
|
631
|
+
minX = Math.min(minX, points[at] ?? minX);
|
|
632
|
+
maxX = Math.max(maxX, points[at] ?? maxX);
|
|
633
|
+
minY = Math.min(minY, points[at + 1] ?? minY);
|
|
634
|
+
maxY = Math.max(maxY, points[at + 1] ?? maxY);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const spanX = Math.max(maxX - minX, 1);
|
|
638
|
+
const spanY = Math.max(maxY - minY, 1);
|
|
639
|
+
let cell = Math.max(this.#maxWorldRadius * 2, Math.sqrt(spanX * spanY / count) * 2, .001);
|
|
640
|
+
const budget = count * 4 + 1024;
|
|
641
|
+
const cells = (spanX / cell + 1) * (spanY / cell + 1);
|
|
642
|
+
if (cells > budget) cell *= Math.sqrt(cells / budget);
|
|
643
|
+
const frame = {
|
|
644
|
+
cell,
|
|
645
|
+
minX,
|
|
646
|
+
minY,
|
|
647
|
+
cols: Math.floor(spanX / cell) + 1,
|
|
648
|
+
rows: Math.floor(spanY / cell) + 1
|
|
649
|
+
};
|
|
650
|
+
this.#nodes = createGrid(frame, nodes.capacity, (index, emit) => {
|
|
651
|
+
if (nodes.alive[index] !== 1) return;
|
|
652
|
+
const col = Math.floor((positions.x(index) - minX) / cell);
|
|
653
|
+
emit(Math.floor((positions.y(index) - minY) / cell) * frame.cols + col);
|
|
654
|
+
});
|
|
655
|
+
this.#maxEdgeHalfWidth = 0;
|
|
656
|
+
this.#edges = createGrid(frame, edges.capacity, (index, emit) => {
|
|
657
|
+
if (edges.alive[index] !== 1) return;
|
|
658
|
+
this.#maxEdgeHalfWidth = Math.max(this.#maxEdgeHalfWidth, (edges.width[index] ?? 0) / 2);
|
|
659
|
+
const from = edges.source[index] ?? 0;
|
|
660
|
+
const to = edges.target[index] ?? 0;
|
|
661
|
+
if (from === to) return;
|
|
662
|
+
if ((edges.bend[index] ?? 0) === 0) {
|
|
663
|
+
traverse(frame, positions.x(from), positions.y(from), positions.x(to), positions.y(to), emit);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const points = sampleCubic(this.#parallel(source, index), indexSegments);
|
|
667
|
+
for (let at = 0; at + 3 < points.length; at += 2) traverse(frame, points[at] ?? 0, points[at + 1] ?? 0, points[at + 2] ?? 0, points[at + 3] ?? 0, emit);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
pick(source, x, y, zoom, tolerance) {
|
|
671
|
+
const node = this.#pickNode(source, x, y, zoom);
|
|
672
|
+
if (node >= 0) return {
|
|
673
|
+
kind: "node",
|
|
674
|
+
index: node
|
|
675
|
+
};
|
|
676
|
+
const edge = this.#pickEdge(source, x, y, zoom, tolerance);
|
|
677
|
+
return edge >= 0 ? {
|
|
678
|
+
kind: "edge",
|
|
679
|
+
index: edge
|
|
680
|
+
} : null;
|
|
681
|
+
}
|
|
682
|
+
#cells(grid, x, y, reach, visit) {
|
|
683
|
+
const c0 = Math.max(0, Math.floor((x - reach - grid.minX) / grid.cell));
|
|
684
|
+
const c1 = Math.min(grid.cols - 1, Math.floor((x + reach - grid.minX) / grid.cell));
|
|
685
|
+
const r0 = Math.max(0, Math.floor((y - reach - grid.minY) / grid.cell));
|
|
686
|
+
const r1 = Math.min(grid.rows - 1, Math.floor((y + reach - grid.minY) / grid.cell));
|
|
687
|
+
for (let row = r0; row <= r1; row++) for (let col = c0; col <= c1; col++) {
|
|
688
|
+
const cell = row * grid.cols + col;
|
|
689
|
+
const end = grid.offsets[cell + 1] ?? 0;
|
|
690
|
+
for (let at = grid.offsets[cell] ?? 0; at < end; at++) visit(grid.items[at] ?? 0);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
#parallel(source, index) {
|
|
694
|
+
const { edges, positions } = source;
|
|
695
|
+
const from = edges.source[index] ?? 0;
|
|
696
|
+
const to = edges.target[index] ?? 0;
|
|
697
|
+
return parallelCurve(positions.x(from), positions.y(from), positions.x(to), positions.y(to), edges.bend[index] ?? 0);
|
|
698
|
+
}
|
|
699
|
+
#loop(source, index, zoom) {
|
|
700
|
+
const node = source.edges.source[index] ?? 0;
|
|
701
|
+
const size = (source.nodes.size[node] ?? 0) / 2;
|
|
702
|
+
const reach = loopReach(Math.max(source.nodes.screenUnits[node] === 1 ? size : size * zoom, 1.25), source.edges.bend[index] ?? 0, 12) / zoom;
|
|
703
|
+
return loopCurve(source.positions.x(node), source.positions.y(node), reach);
|
|
704
|
+
}
|
|
705
|
+
#radius(source, index, zoom) {
|
|
706
|
+
const size = (source.nodes.size[index] ?? 0) / 2;
|
|
707
|
+
const radius = source.nodes.screenUnits[index] === 1 ? size / zoom : size;
|
|
708
|
+
return Math.max(radius, minimumPickRadius / zoom);
|
|
709
|
+
}
|
|
710
|
+
#pickNode(source, x, y, zoom) {
|
|
711
|
+
const grid = this.#nodes;
|
|
712
|
+
if (!grid) return -1;
|
|
713
|
+
const reach = Math.max(this.#maxWorldRadius, (this.#maxScreenRadius + minimumPickRadius) / zoom);
|
|
714
|
+
let best = -1;
|
|
715
|
+
let bestOrder = Number.NEGATIVE_INFINITY;
|
|
716
|
+
this.#cells(grid, x, y, reach, (index) => {
|
|
717
|
+
if (source.nodes.alive[index] !== 1) return;
|
|
718
|
+
if (Math.hypot(x - source.positions.x(index), y - source.positions.y(index)) > this.#radius(source, index, zoom)) return;
|
|
719
|
+
const order = source.order("nodes", index);
|
|
720
|
+
if (order > bestOrder) {
|
|
721
|
+
best = index;
|
|
722
|
+
bestOrder = order;
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
return best;
|
|
726
|
+
}
|
|
727
|
+
#pickEdge(source, x, y, zoom, tolerance) {
|
|
728
|
+
const grid = this.#edges;
|
|
729
|
+
if (!grid) return -1;
|
|
730
|
+
const { edges, positions } = source;
|
|
731
|
+
const reach = (this.#maxEdgeHalfWidth + tolerance) / zoom;
|
|
732
|
+
let best = -1;
|
|
733
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
734
|
+
let bestOrder = Number.NEGATIVE_INFINITY;
|
|
735
|
+
const consider = (index, distance) => {
|
|
736
|
+
if (distance > ((edges.width[index] ?? 0) / 2 + tolerance) / zoom) return;
|
|
737
|
+
const order = source.order("edges", index);
|
|
738
|
+
if (distance < bestDistance || distance === bestDistance && order > bestOrder) {
|
|
739
|
+
best = index;
|
|
740
|
+
bestDistance = distance;
|
|
741
|
+
bestOrder = order;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
const seen = /* @__PURE__ */ new Set();
|
|
745
|
+
this.#cells(grid, x, y, reach, (index) => {
|
|
746
|
+
if (edges.alive[index] !== 1) return;
|
|
747
|
+
const s = edges.source[index] ?? 0;
|
|
748
|
+
const t = edges.target[index] ?? 0;
|
|
749
|
+
if ((edges.bend[index] ?? 0) !== 0) {
|
|
750
|
+
if (seen.has(index)) return;
|
|
751
|
+
seen.add(index);
|
|
752
|
+
consider(index, distanceToCubic(this.#parallel(source, index), x, y, pickSegments));
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const ax = positions.x(s);
|
|
756
|
+
const ay = positions.y(s);
|
|
757
|
+
const dx = positions.x(t) - ax;
|
|
758
|
+
const dy = positions.y(t) - ay;
|
|
759
|
+
const length = Math.hypot(dx, dy);
|
|
760
|
+
if (length === 0) return;
|
|
761
|
+
const along = ((x - ax) * dx + (y - ay) * dy) / length;
|
|
762
|
+
if (along < this.#radius(source, s, zoom) || along > length - this.#radius(source, t, zoom)) return;
|
|
763
|
+
consider(index, Math.abs((x - ax) * dy - (y - ay) * dx) / length);
|
|
764
|
+
});
|
|
765
|
+
for (const index of this.#loops) {
|
|
766
|
+
if (edges.alive[index] !== 1) continue;
|
|
767
|
+
consider(index, distanceToCubic(this.#loop(source, index, zoom), x, y, pickSegments));
|
|
768
|
+
}
|
|
769
|
+
return best;
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
// src/core/compile-mark.ts
|
|
773
|
+
function channel(value) {
|
|
774
|
+
return value === void 0 ? null : toAccessor(value);
|
|
775
|
+
}
|
|
776
|
+
function style(value) {
|
|
777
|
+
if (value === void 0) return null;
|
|
778
|
+
return typeof value === "function" ? value : () => value;
|
|
779
|
+
}
|
|
780
|
+
const badgeCorners = [
|
|
781
|
+
"topLeft",
|
|
782
|
+
"topRight",
|
|
783
|
+
"bottomLeft",
|
|
784
|
+
"bottomRight"
|
|
785
|
+
];
|
|
786
|
+
const nodeCache = /* @__PURE__ */ new WeakMap();
|
|
787
|
+
const edgeCache = /* @__PURE__ */ new WeakMap();
|
|
788
|
+
function compileNodeMark(mark) {
|
|
789
|
+
const cached = nodeCache.get(mark);
|
|
790
|
+
if (cached) return cached;
|
|
791
|
+
const options = mark.options;
|
|
792
|
+
const label = options.label;
|
|
793
|
+
const badges = [];
|
|
794
|
+
for (const corner of badgeCorners) {
|
|
795
|
+
const read = options.badges?.[corner];
|
|
796
|
+
if (read) badges.push([corner, read]);
|
|
797
|
+
}
|
|
798
|
+
const compiled = {
|
|
799
|
+
color: channel(options.color),
|
|
800
|
+
size: channel(options.size),
|
|
801
|
+
x: channel(options.x),
|
|
802
|
+
y: channel(options.y),
|
|
803
|
+
label: channel(typeof label === "object" ? label.text : label),
|
|
804
|
+
secondary: channel(typeof label === "object" ? label.secondary : void 0),
|
|
805
|
+
shape: style(options.shape),
|
|
806
|
+
fill: style(options.fill),
|
|
807
|
+
stroke: style(options.stroke),
|
|
808
|
+
strokeWidth: style(options.strokeWidth),
|
|
809
|
+
opacity: style(options.opacity),
|
|
810
|
+
image: style(options.image),
|
|
811
|
+
badges,
|
|
812
|
+
screenUnits: options.units === "screen"
|
|
813
|
+
};
|
|
814
|
+
nodeCache.set(mark, compiled);
|
|
815
|
+
return compiled;
|
|
816
|
+
}
|
|
817
|
+
function compileEdgeMark(mark) {
|
|
818
|
+
const cached = edgeCache.get(mark);
|
|
819
|
+
if (cached) return cached;
|
|
820
|
+
const options = mark.options;
|
|
821
|
+
const compiled = {
|
|
822
|
+
color: channel(options.color),
|
|
823
|
+
width: channel(options.width),
|
|
824
|
+
label: channel(options.label),
|
|
825
|
+
dash: style(options.dash),
|
|
826
|
+
arrow: style(options.arrow),
|
|
827
|
+
stroke: style(options.stroke),
|
|
828
|
+
opacity: style(options.opacity)
|
|
829
|
+
};
|
|
830
|
+
edgeCache.set(mark, compiled);
|
|
831
|
+
return compiled;
|
|
832
|
+
}
|
|
833
|
+
// src/core/positions.ts
|
|
834
|
+
const goldenAngle = Math.PI * (3 - Math.sqrt(5));
|
|
835
|
+
var Positions = class {
|
|
836
|
+
capacity = 0;
|
|
837
|
+
version = 0;
|
|
838
|
+
xy = /* @__PURE__ */ new Float32Array(0);
|
|
839
|
+
placed = /* @__PURE__ */ new Uint8Array(0);
|
|
840
|
+
source = /* @__PURE__ */ new Float64Array(0);
|
|
841
|
+
#spiral = 0;
|
|
842
|
+
#rings = /* @__PURE__ */ new Map();
|
|
843
|
+
reserve(index) {
|
|
844
|
+
if (index < this.capacity) return;
|
|
845
|
+
const capacity = Math.max(64, this.capacity * 2, index + 1);
|
|
846
|
+
const xy = new Float32Array(capacity * 2);
|
|
847
|
+
xy.set(this.xy);
|
|
848
|
+
const placed = new Uint8Array(capacity);
|
|
849
|
+
placed.set(this.placed);
|
|
850
|
+
const source = new Float64Array(capacity * 2).fill(NaN);
|
|
851
|
+
source.set(this.source);
|
|
852
|
+
this.xy = xy;
|
|
853
|
+
this.placed = placed;
|
|
854
|
+
this.source = source;
|
|
855
|
+
this.capacity = capacity;
|
|
856
|
+
}
|
|
857
|
+
x(index) {
|
|
858
|
+
return this.xy[index * 2] ?? 0;
|
|
859
|
+
}
|
|
860
|
+
y(index) {
|
|
861
|
+
return this.xy[index * 2 + 1] ?? 0;
|
|
862
|
+
}
|
|
863
|
+
set(index, x, y) {
|
|
864
|
+
this.reserve(index);
|
|
865
|
+
this.xy[index * 2] = x;
|
|
866
|
+
this.xy[index * 2 + 1] = y;
|
|
867
|
+
this.placed[index] = 1;
|
|
868
|
+
this.version++;
|
|
869
|
+
}
|
|
870
|
+
release(index) {
|
|
871
|
+
if (index < this.capacity) this.placed[index] = 0;
|
|
872
|
+
this.follow(index, null);
|
|
873
|
+
this.#rings.delete(index);
|
|
874
|
+
}
|
|
875
|
+
follow(index, point) {
|
|
876
|
+
this.reserve(index);
|
|
877
|
+
const x = point ? point[0] : NaN;
|
|
878
|
+
const y = point ? point[1] : NaN;
|
|
879
|
+
const changed = point !== null && (this.source[index * 2] !== x || this.source[index * 2 + 1] !== y);
|
|
880
|
+
this.source[index * 2] = x;
|
|
881
|
+
this.source[index * 2 + 1] = y;
|
|
882
|
+
return changed;
|
|
883
|
+
}
|
|
884
|
+
nextSpiral() {
|
|
885
|
+
const n = this.#spiral++;
|
|
886
|
+
const radius = 24 * Math.sqrt(n);
|
|
887
|
+
return [radius * Math.cos(n * goldenAngle), radius * Math.sin(n * goldenAngle)];
|
|
888
|
+
}
|
|
889
|
+
nextRing(anchor) {
|
|
890
|
+
const count = this.#rings.get(anchor) ?? 0;
|
|
891
|
+
this.#rings.set(anchor, count + 1);
|
|
892
|
+
return count;
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
function finite(value) {
|
|
896
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
897
|
+
}
|
|
898
|
+
function channelPoint(mark, datum) {
|
|
899
|
+
const compiled = compileNodeMark(mark);
|
|
900
|
+
const x = finite(compiled.x?.(datum));
|
|
901
|
+
const y = finite(compiled.y?.(datum));
|
|
902
|
+
return x !== null && y !== null ? [x, y] : null;
|
|
903
|
+
}
|
|
904
|
+
function placeNodes(model, positions, changes) {
|
|
905
|
+
for (const { index } of changes.nodes.removed) positions.release(index);
|
|
906
|
+
const indices = [];
|
|
907
|
+
const to = [];
|
|
908
|
+
for (const { key, index } of changes.nodes.updated) {
|
|
909
|
+
const record = model.node(key);
|
|
910
|
+
if (!record) continue;
|
|
911
|
+
const point = channelPoint(record.mark, record.datum);
|
|
912
|
+
if (!point || !positions.follow(index, point)) continue;
|
|
913
|
+
if (positions.x(index) === Math.fround(point[0]) && positions.y(index) === Math.fround(point[1])) continue;
|
|
914
|
+
indices.push(index);
|
|
915
|
+
to.push(point[0], point[1]);
|
|
916
|
+
}
|
|
917
|
+
const batch = new Set(changes.nodes.added.map((ref) => ref.key));
|
|
918
|
+
for (const { key, index } of changes.nodes.added) {
|
|
919
|
+
const record = model.node(key);
|
|
920
|
+
if (!record) continue;
|
|
921
|
+
const point = channelPoint(record.mark, record.datum);
|
|
922
|
+
positions.follow(index, point);
|
|
923
|
+
if (point) {
|
|
924
|
+
positions.set(index, point[0], point[1]);
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
let sumX = 0;
|
|
928
|
+
let sumY = 0;
|
|
929
|
+
let count = 0;
|
|
930
|
+
let anchor = -1;
|
|
931
|
+
for (const neighbor of model.neighbors(key)) {
|
|
932
|
+
if (batch.has(neighbor)) continue;
|
|
933
|
+
const other = model.node(neighbor);
|
|
934
|
+
if (!other || positions.placed[other.index] !== 1) continue;
|
|
935
|
+
sumX += positions.x(other.index);
|
|
936
|
+
sumY += positions.y(other.index);
|
|
937
|
+
count++;
|
|
938
|
+
if (anchor < 0) anchor = other.index;
|
|
939
|
+
}
|
|
940
|
+
if (count === 0) {
|
|
941
|
+
const [sx, sy] = positions.nextSpiral();
|
|
942
|
+
positions.set(index, sx, sy);
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
const ring = positions.nextRing(anchor);
|
|
946
|
+
const angle = ring * goldenAngle;
|
|
947
|
+
const radius = 36 * Math.sqrt(ring + 1);
|
|
948
|
+
positions.set(index, sumX / count + radius * Math.cos(angle), sumY / count + radius * Math.sin(angle));
|
|
949
|
+
}
|
|
950
|
+
return {
|
|
951
|
+
indices,
|
|
952
|
+
to
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
// src/theme/tokens.ts
|
|
956
|
+
const lightTheme = {
|
|
957
|
+
paper: "#f7f7f4",
|
|
958
|
+
"paper-raised": "#ffffff",
|
|
959
|
+
grid: "#c4e0dd",
|
|
960
|
+
ink: "#191d22",
|
|
961
|
+
"ink-muted": "#5b6167",
|
|
962
|
+
node: "#474e55",
|
|
963
|
+
edge: "#7f858c",
|
|
964
|
+
"group-fill": "#e7efee",
|
|
965
|
+
"group-line": "#6e8d8b",
|
|
966
|
+
accent: "#c93700",
|
|
967
|
+
"on-accent": "#ffffff",
|
|
968
|
+
"focus-ring": "#c93700",
|
|
969
|
+
halo: "#c9370047",
|
|
970
|
+
"label-halo": "#f7f7f4",
|
|
971
|
+
"node-ring": "#f7f7f4",
|
|
972
|
+
"badge-fill": "#191d22",
|
|
973
|
+
"badge-ink": "#f7f7f4",
|
|
974
|
+
"cat-1": "#2769b7",
|
|
975
|
+
"cat-2": "#a47c13",
|
|
976
|
+
"cat-3": "#12867c",
|
|
977
|
+
"cat-4": "#9b357f",
|
|
978
|
+
"cat-5": "#4189ad",
|
|
979
|
+
"cat-6": "#448335",
|
|
980
|
+
"cat-7": "#5d4099",
|
|
981
|
+
"cat-8": "#745338"
|
|
982
|
+
};
|
|
983
|
+
const defaultFont = "system-ui, sans-serif";
|
|
984
|
+
function themeColor(name) {
|
|
985
|
+
return `var(--graph-${name})`;
|
|
986
|
+
}
|
|
987
|
+
const categoricalRange = Array.from({ length: 8 }, (_, index) => themeColor(`cat-${index + 1}`));
|
|
988
|
+
const sequentialRange = [themeColor("cat-5"), themeColor("cat-1")];
|
|
989
|
+
const defaults = {
|
|
990
|
+
nodeSize: 16,
|
|
991
|
+
nodeSizeRange: [8, 32],
|
|
992
|
+
nodeFill: themeColor("node"),
|
|
993
|
+
nodeStroke: themeColor("node-ring"),
|
|
994
|
+
nodeStrokeWidth: 1,
|
|
995
|
+
edgeWidth: 1.5,
|
|
996
|
+
edgeWidthRange: [1, 6],
|
|
997
|
+
edgeColor: themeColor("edge")
|
|
998
|
+
};
|
|
999
|
+
const builtInStyles = {
|
|
1000
|
+
nodes: {
|
|
1001
|
+
selected: {
|
|
1002
|
+
stroke: themeColor("accent"),
|
|
1003
|
+
strokeWidth: 2,
|
|
1004
|
+
halo: true,
|
|
1005
|
+
label: true
|
|
1006
|
+
},
|
|
1007
|
+
hovered: {
|
|
1008
|
+
stroke: themeColor("ink"),
|
|
1009
|
+
strokeWidth: 2,
|
|
1010
|
+
label: true
|
|
1011
|
+
},
|
|
1012
|
+
focused: {
|
|
1013
|
+
stroke: themeColor("focus-ring"),
|
|
1014
|
+
strokeWidth: 2,
|
|
1015
|
+
label: true
|
|
1016
|
+
}
|
|
1017
|
+
},
|
|
1018
|
+
edges: {
|
|
1019
|
+
selected: {
|
|
1020
|
+
stroke: themeColor("accent"),
|
|
1021
|
+
halo: true
|
|
1022
|
+
},
|
|
1023
|
+
hovered: { stroke: themeColor("ink") },
|
|
1024
|
+
focused: { stroke: themeColor("focus-ring") }
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
// src/scales/color-scale.ts
|
|
1028
|
+
function interpolate(stops, t) {
|
|
1029
|
+
const last = stops.length - 1;
|
|
1030
|
+
if (last < 1) return stops[0] ?? null;
|
|
1031
|
+
const position = t * last;
|
|
1032
|
+
const segment = Math.min(Math.floor(position), last - 1);
|
|
1033
|
+
const percent = Math.round((position - segment) * 100);
|
|
1034
|
+
const from = stops[segment] ?? null;
|
|
1035
|
+
const to = stops[segment + 1] ?? null;
|
|
1036
|
+
if (percent === 0 || to === null) return from;
|
|
1037
|
+
if (percent === 100) return to;
|
|
1038
|
+
return `color-mix(in oklab, ${from}, ${to} ${percent}%)`;
|
|
1039
|
+
}
|
|
1040
|
+
function categorical(domain, range) {
|
|
1041
|
+
const positions = /* @__PURE__ */ new Map();
|
|
1042
|
+
for (const value of domain) if (!positions.has(value)) positions.set(value, positions.size);
|
|
1043
|
+
return {
|
|
1044
|
+
sequential: false,
|
|
1045
|
+
needsExtent: false,
|
|
1046
|
+
map(value) {
|
|
1047
|
+
if (value === null || value === void 0 || range.length === 0) return null;
|
|
1048
|
+
let position = positions.get(value);
|
|
1049
|
+
if (position === void 0) {
|
|
1050
|
+
position = positions.size;
|
|
1051
|
+
positions.set(value, position);
|
|
1052
|
+
}
|
|
1053
|
+
return range[position % range.length] ?? null;
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
function sequential(domain, range) {
|
|
1058
|
+
return {
|
|
1059
|
+
sequential: true,
|
|
1060
|
+
needsExtent: domain === null,
|
|
1061
|
+
map(value, extent) {
|
|
1062
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
1063
|
+
const [d0, d1] = domain ?? extent ?? [value, value];
|
|
1064
|
+
return interpolate(range, d0 === d1 ? .5 : Math.min(1, Math.max(0, (value - d0) / (d1 - d0))));
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function resolveColorScale(options) {
|
|
1069
|
+
if (typeof options === "function") return {
|
|
1070
|
+
sequential: false,
|
|
1071
|
+
needsExtent: false,
|
|
1072
|
+
map: (value) => value === null || value === void 0 ? null : options(value)
|
|
1073
|
+
};
|
|
1074
|
+
if (options?.type === "sequential") return sequential(options.domain ?? null, options.range ?? sequentialRange);
|
|
1075
|
+
return categorical(options?.domain ?? [], options?.range ?? categoricalRange);
|
|
1076
|
+
}
|
|
1077
|
+
// src/scales/extent.ts
|
|
1078
|
+
function sameExtent(a, b) {
|
|
1079
|
+
return a === b || a !== null && b !== null && a[0] === b[0] && a[1] === b[1];
|
|
1080
|
+
}
|
|
1081
|
+
function combineExtents(a, b) {
|
|
1082
|
+
if (a === null) return b;
|
|
1083
|
+
if (b === null) return a;
|
|
1084
|
+
return [Math.min(a[0], b[0]), Math.max(a[1], b[1])];
|
|
1085
|
+
}
|
|
1086
|
+
var ExtentTracker = class {
|
|
1087
|
+
#values = /* @__PURE__ */ new Map();
|
|
1088
|
+
#extent = null;
|
|
1089
|
+
#stale = false;
|
|
1090
|
+
get extent() {
|
|
1091
|
+
if (this.#stale) {
|
|
1092
|
+
this.#stale = false;
|
|
1093
|
+
this.#extent = null;
|
|
1094
|
+
for (const value of this.#values.values()) this.#include(value);
|
|
1095
|
+
}
|
|
1096
|
+
return this.#extent;
|
|
1097
|
+
}
|
|
1098
|
+
ids() {
|
|
1099
|
+
return this.#values.keys();
|
|
1100
|
+
}
|
|
1101
|
+
set(id, value) {
|
|
1102
|
+
const previous = this.#values.get(id);
|
|
1103
|
+
this.#values.set(id, value);
|
|
1104
|
+
if (previous !== void 0 && this.#isBound(previous)) this.#stale = true;
|
|
1105
|
+
if (!this.#stale) this.#include(value);
|
|
1106
|
+
}
|
|
1107
|
+
delete(id) {
|
|
1108
|
+
const previous = this.#values.get(id);
|
|
1109
|
+
if (previous === void 0) return;
|
|
1110
|
+
this.#values.delete(id);
|
|
1111
|
+
if (this.#isBound(previous)) this.#stale = true;
|
|
1112
|
+
}
|
|
1113
|
+
clear() {
|
|
1114
|
+
this.#values.clear();
|
|
1115
|
+
this.#extent = null;
|
|
1116
|
+
this.#stale = false;
|
|
1117
|
+
}
|
|
1118
|
+
#isBound(value) {
|
|
1119
|
+
return this.#extent !== null && (value === this.#extent[0] || value === this.#extent[1]);
|
|
1120
|
+
}
|
|
1121
|
+
#include(value) {
|
|
1122
|
+
this.#extent = this.#extent === null ? [value, value] : [Math.min(this.#extent[0], value), Math.max(this.#extent[1], value)];
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
// src/scales/number-scale.ts
|
|
1126
|
+
const transforms = {
|
|
1127
|
+
linear: (value) => value,
|
|
1128
|
+
sqrt: (value) => Math.sign(value) * Math.sqrt(Math.abs(value))
|
|
1129
|
+
};
|
|
1130
|
+
function resolveNumberScale(options, fallback) {
|
|
1131
|
+
if (typeof options === "function") return {
|
|
1132
|
+
needsExtent: false,
|
|
1133
|
+
map: (value) => options(value)
|
|
1134
|
+
};
|
|
1135
|
+
const transform = transforms[options?.type ?? fallback.type];
|
|
1136
|
+
const [r0, r1] = options?.range ?? fallback.range;
|
|
1137
|
+
const domain = options?.domain ?? null;
|
|
1138
|
+
return {
|
|
1139
|
+
needsExtent: domain === null,
|
|
1140
|
+
map(value, extent) {
|
|
1141
|
+
const [d0, d1] = domain ?? extent ?? [value, value];
|
|
1142
|
+
const t0 = transform(d0);
|
|
1143
|
+
const t1 = transform(d1);
|
|
1144
|
+
if (t0 === t1) return (r0 + r1) / 2;
|
|
1145
|
+
const t = Math.min(1, Math.max(0, (transform(value) - t0) / (t1 - t0)));
|
|
1146
|
+
return r0 + t * (r1 - r0);
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
// src/core/columns.ts
|
|
1151
|
+
const shapeCodes = {
|
|
1152
|
+
circle: 0,
|
|
1153
|
+
square: 1,
|
|
1154
|
+
diamond: 2
|
|
1155
|
+
};
|
|
1156
|
+
const dashCodes = {
|
|
1157
|
+
solid: 0,
|
|
1158
|
+
dashed: 1,
|
|
1159
|
+
dotted: 2
|
|
1160
|
+
};
|
|
1161
|
+
const arrowCodes = {
|
|
1162
|
+
none: 0,
|
|
1163
|
+
source: 1,
|
|
1164
|
+
target: 2,
|
|
1165
|
+
both: 3
|
|
1166
|
+
};
|
|
1167
|
+
function grow$1(array, length) {
|
|
1168
|
+
const next = new array.constructor(length);
|
|
1169
|
+
next.set(array);
|
|
1170
|
+
return next;
|
|
1171
|
+
}
|
|
1172
|
+
function nextCapacity(capacity, index) {
|
|
1173
|
+
return Math.max(64, capacity * 2, index + 1);
|
|
1174
|
+
}
|
|
1175
|
+
var NodeColumns = class {
|
|
1176
|
+
capacity = 0;
|
|
1177
|
+
keys = [];
|
|
1178
|
+
alive = /* @__PURE__ */ new Uint8Array(0);
|
|
1179
|
+
slot = /* @__PURE__ */ new Uint16Array(0);
|
|
1180
|
+
size = /* @__PURE__ */ new Float32Array(0);
|
|
1181
|
+
shape = /* @__PURE__ */ new Uint8Array(0);
|
|
1182
|
+
fill = /* @__PURE__ */ new Uint32Array(0);
|
|
1183
|
+
stroke = /* @__PURE__ */ new Uint32Array(0);
|
|
1184
|
+
strokeWidth = /* @__PURE__ */ new Float32Array(0);
|
|
1185
|
+
opacity = /* @__PURE__ */ new Float32Array(0);
|
|
1186
|
+
screenUnits = /* @__PURE__ */ new Uint8Array(0);
|
|
1187
|
+
label = [];
|
|
1188
|
+
secondary = [];
|
|
1189
|
+
image = [];
|
|
1190
|
+
badges = [];
|
|
1191
|
+
reserve(index) {
|
|
1192
|
+
if (index < this.capacity) return;
|
|
1193
|
+
const capacity = nextCapacity(this.capacity, index);
|
|
1194
|
+
this.alive = grow$1(this.alive, capacity);
|
|
1195
|
+
this.slot = grow$1(this.slot, capacity);
|
|
1196
|
+
this.size = grow$1(this.size, capacity);
|
|
1197
|
+
this.shape = grow$1(this.shape, capacity);
|
|
1198
|
+
this.fill = grow$1(this.fill, capacity);
|
|
1199
|
+
this.stroke = grow$1(this.stroke, capacity);
|
|
1200
|
+
this.strokeWidth = grow$1(this.strokeWidth, capacity);
|
|
1201
|
+
this.opacity = grow$1(this.opacity, capacity);
|
|
1202
|
+
this.screenUnits = grow$1(this.screenUnits, capacity);
|
|
1203
|
+
this.capacity = capacity;
|
|
1204
|
+
}
|
|
1205
|
+
clear(index) {
|
|
1206
|
+
if (index >= this.capacity) return;
|
|
1207
|
+
this.alive[index] = 0;
|
|
1208
|
+
this.keys[index] = void 0;
|
|
1209
|
+
this.label[index] = null;
|
|
1210
|
+
this.secondary[index] = null;
|
|
1211
|
+
this.image[index] = null;
|
|
1212
|
+
this.badges[index] = null;
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
var EdgeColumns = class {
|
|
1216
|
+
capacity = 0;
|
|
1217
|
+
keys = [];
|
|
1218
|
+
alive = /* @__PURE__ */ new Uint8Array(0);
|
|
1219
|
+
slot = /* @__PURE__ */ new Uint16Array(0);
|
|
1220
|
+
source = /* @__PURE__ */ new Uint32Array(0);
|
|
1221
|
+
target = /* @__PURE__ */ new Uint32Array(0);
|
|
1222
|
+
color = /* @__PURE__ */ new Uint32Array(0);
|
|
1223
|
+
width = /* @__PURE__ */ new Float32Array(0);
|
|
1224
|
+
dash = /* @__PURE__ */ new Uint8Array(0);
|
|
1225
|
+
arrow = /* @__PURE__ */ new Uint8Array(0);
|
|
1226
|
+
opacity = /* @__PURE__ */ new Float32Array(0);
|
|
1227
|
+
bend = /* @__PURE__ */ new Float32Array(0);
|
|
1228
|
+
label = [];
|
|
1229
|
+
reserve(index) {
|
|
1230
|
+
if (index < this.capacity) return;
|
|
1231
|
+
const capacity = nextCapacity(this.capacity, index);
|
|
1232
|
+
this.alive = grow$1(this.alive, capacity);
|
|
1233
|
+
this.slot = grow$1(this.slot, capacity);
|
|
1234
|
+
this.source = grow$1(this.source, capacity);
|
|
1235
|
+
this.target = grow$1(this.target, capacity);
|
|
1236
|
+
this.color = grow$1(this.color, capacity);
|
|
1237
|
+
this.width = grow$1(this.width, capacity);
|
|
1238
|
+
this.dash = grow$1(this.dash, capacity);
|
|
1239
|
+
this.arrow = grow$1(this.arrow, capacity);
|
|
1240
|
+
this.opacity = grow$1(this.opacity, capacity);
|
|
1241
|
+
this.bend = grow$1(this.bend, capacity);
|
|
1242
|
+
this.capacity = capacity;
|
|
1243
|
+
}
|
|
1244
|
+
clear(index) {
|
|
1245
|
+
if (index >= this.capacity) return;
|
|
1246
|
+
this.alive[index] = 0;
|
|
1247
|
+
this.bend[index] = 0;
|
|
1248
|
+
this.keys[index] = void 0;
|
|
1249
|
+
this.label[index] = null;
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
// src/core/edge-lanes.ts
|
|
1253
|
+
const empty = -1;
|
|
1254
|
+
const removed = -2;
|
|
1255
|
+
function hash(low, high) {
|
|
1256
|
+
let h = Math.imul(low, 2654435761) ^ Math.imul(high + 2135587861, 2246822519);
|
|
1257
|
+
h ^= h >>> 15;
|
|
1258
|
+
return Math.imul(h, 739982445) ^ h >>> 12;
|
|
1259
|
+
}
|
|
1260
|
+
function grown(array, length) {
|
|
1261
|
+
const next = new Int32Array(length).fill(-1);
|
|
1262
|
+
next.set(array);
|
|
1263
|
+
return next;
|
|
1264
|
+
}
|
|
1265
|
+
function createEdgeLanes() {
|
|
1266
|
+
let mask = -1;
|
|
1267
|
+
let lows = /* @__PURE__ */ new Uint32Array(0);
|
|
1268
|
+
let highs = /* @__PURE__ */ new Uint32Array(0);
|
|
1269
|
+
let heads = /* @__PURE__ */ new Int32Array(0);
|
|
1270
|
+
let tails = /* @__PURE__ */ new Int32Array(0);
|
|
1271
|
+
let counts = /* @__PURE__ */ new Int32Array(0);
|
|
1272
|
+
let stamps = /* @__PURE__ */ new Uint32Array(0);
|
|
1273
|
+
let used = 0;
|
|
1274
|
+
let live = 0;
|
|
1275
|
+
let generation = 0;
|
|
1276
|
+
let slots = /* @__PURE__ */ new Int32Array(0);
|
|
1277
|
+
let nexts = /* @__PURE__ */ new Int32Array(0);
|
|
1278
|
+
let prevs = /* @__PURE__ */ new Int32Array(0);
|
|
1279
|
+
let touched = [];
|
|
1280
|
+
function reserve(index) {
|
|
1281
|
+
if (index < slots.length) return;
|
|
1282
|
+
const length = Math.max(64, slots.length * 2, index + 1);
|
|
1283
|
+
slots = grown(slots, length);
|
|
1284
|
+
nexts = grown(nexts, length);
|
|
1285
|
+
prevs = grown(prevs, length);
|
|
1286
|
+
}
|
|
1287
|
+
function locate(low, high) {
|
|
1288
|
+
let slot = hash(low, high) & mask;
|
|
1289
|
+
let free = -1;
|
|
1290
|
+
for (;;) {
|
|
1291
|
+
const head = heads[slot] ?? empty;
|
|
1292
|
+
if (head === empty) return -(free >= 0 ? free : slot) - 1;
|
|
1293
|
+
if (head === removed) {
|
|
1294
|
+
if (free < 0) free = slot;
|
|
1295
|
+
} else if (lows[slot] === low && highs[slot] === high) return slot;
|
|
1296
|
+
slot = slot + 1 & mask;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
function resize(minimum) {
|
|
1300
|
+
let capacity = 16;
|
|
1301
|
+
while (capacity < minimum) capacity *= 2;
|
|
1302
|
+
const oldLows = lows;
|
|
1303
|
+
const oldHighs = highs;
|
|
1304
|
+
const oldHeads = heads;
|
|
1305
|
+
const oldTails = tails;
|
|
1306
|
+
const oldCounts = counts;
|
|
1307
|
+
mask = capacity - 1;
|
|
1308
|
+
lows = new Uint32Array(capacity);
|
|
1309
|
+
highs = new Uint32Array(capacity);
|
|
1310
|
+
heads = new Int32Array(capacity).fill(empty);
|
|
1311
|
+
tails = new Int32Array(capacity).fill(-1);
|
|
1312
|
+
counts = new Int32Array(capacity);
|
|
1313
|
+
stamps = new Uint32Array(capacity);
|
|
1314
|
+
generation = 0;
|
|
1315
|
+
used = 0;
|
|
1316
|
+
live = 0;
|
|
1317
|
+
for (let old = 0; old < oldHeads.length; old++) {
|
|
1318
|
+
const first = oldHeads[old] ?? empty;
|
|
1319
|
+
if (first < 0) continue;
|
|
1320
|
+
const low = oldLows[old] ?? 0;
|
|
1321
|
+
const high = oldHighs[old] ?? 0;
|
|
1322
|
+
const slot = -locate(low, high) - 1;
|
|
1323
|
+
lows[slot] = low;
|
|
1324
|
+
highs[slot] = high;
|
|
1325
|
+
heads[slot] = first;
|
|
1326
|
+
tails[slot] = oldTails[old] ?? -1;
|
|
1327
|
+
counts[slot] = oldCounts[old] ?? 0;
|
|
1328
|
+
used++;
|
|
1329
|
+
live++;
|
|
1330
|
+
for (let at = first; at >= 0; at = nexts[at] ?? -1) slots[at] = slot;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function unlink(index) {
|
|
1334
|
+
const slot = slots[index] ?? -1;
|
|
1335
|
+
if (slot < 0) return;
|
|
1336
|
+
const prev = prevs[index] ?? -1;
|
|
1337
|
+
const next = nexts[index] ?? -1;
|
|
1338
|
+
if (prev >= 0) nexts[prev] = next;
|
|
1339
|
+
else heads[slot] = next;
|
|
1340
|
+
if (next >= 0) prevs[next] = prev;
|
|
1341
|
+
else tails[slot] = prev;
|
|
1342
|
+
slots[index] = -1;
|
|
1343
|
+
const count = (counts[slot] ?? 1) - 1;
|
|
1344
|
+
counts[slot] = count;
|
|
1345
|
+
if (count === 0) {
|
|
1346
|
+
heads[slot] = removed;
|
|
1347
|
+
live--;
|
|
1348
|
+
} else touched.push(heads[slot] ?? -1);
|
|
1349
|
+
}
|
|
1350
|
+
function link(index, source, target) {
|
|
1351
|
+
reserve(index);
|
|
1352
|
+
const low = source < target ? source : target;
|
|
1353
|
+
const high = source < target ? target : source;
|
|
1354
|
+
const current = slots[index] ?? -1;
|
|
1355
|
+
if (current >= 0) {
|
|
1356
|
+
if (lows[current] === low && highs[current] === high) return;
|
|
1357
|
+
unlink(index);
|
|
1358
|
+
}
|
|
1359
|
+
if ((used + 1) * 2 > mask + 1) resize(Math.max(16, (live + 1) * 4));
|
|
1360
|
+
const found = locate(low, high);
|
|
1361
|
+
prevs[index] = -1;
|
|
1362
|
+
nexts[index] = -1;
|
|
1363
|
+
if (found >= 0) {
|
|
1364
|
+
const tail = tails[found] ?? -1;
|
|
1365
|
+
nexts[tail] = index;
|
|
1366
|
+
prevs[index] = tail;
|
|
1367
|
+
tails[found] = index;
|
|
1368
|
+
counts[found] = (counts[found] ?? 0) + 1;
|
|
1369
|
+
slots[index] = found;
|
|
1370
|
+
} else {
|
|
1371
|
+
const slot = -found - 1;
|
|
1372
|
+
if (heads[slot] === empty) used++;
|
|
1373
|
+
live++;
|
|
1374
|
+
lows[slot] = low;
|
|
1375
|
+
highs[slot] = high;
|
|
1376
|
+
heads[slot] = index;
|
|
1377
|
+
tails[slot] = index;
|
|
1378
|
+
counts[slot] = 1;
|
|
1379
|
+
slots[index] = slot;
|
|
1380
|
+
}
|
|
1381
|
+
touched.push(index);
|
|
1382
|
+
}
|
|
1383
|
+
function write(edges, dirty) {
|
|
1384
|
+
if (touched.length === 0) return;
|
|
1385
|
+
generation = generation + 1 >>> 0;
|
|
1386
|
+
if (generation === 0) {
|
|
1387
|
+
stamps.fill(0);
|
|
1388
|
+
generation = 1;
|
|
1389
|
+
}
|
|
1390
|
+
const bends = edges.bend;
|
|
1391
|
+
for (const edge of touched) {
|
|
1392
|
+
const slot = edge >= 0 ? slots[edge] ?? -1 : -1;
|
|
1393
|
+
if (slot < 0 || stamps[slot] === generation) continue;
|
|
1394
|
+
stamps[slot] = generation;
|
|
1395
|
+
const count = counts[slot] ?? 0;
|
|
1396
|
+
const loop = lows[slot] === highs[slot];
|
|
1397
|
+
if (count === 1 && !loop) {
|
|
1398
|
+
const only = heads[slot] ?? 0;
|
|
1399
|
+
if (bends[only] !== 0) {
|
|
1400
|
+
bends[only] = 0;
|
|
1401
|
+
dirty.add(only);
|
|
1402
|
+
}
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
let position = 0;
|
|
1406
|
+
for (let at = heads[slot] ?? -1; at >= 0; at = nexts[at] ?? -1) {
|
|
1407
|
+
const source = edges.source[at] ?? 0;
|
|
1408
|
+
const target = edges.target[at] ?? 0;
|
|
1409
|
+
const bend = loop ? position : Math.fround(laneBend(position, count) * (source < target ? 1 : -1)) || 0;
|
|
1410
|
+
if (bends[at] !== bend) {
|
|
1411
|
+
bends[at] = bend;
|
|
1412
|
+
dirty.add(at);
|
|
1413
|
+
}
|
|
1414
|
+
position++;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
touched = [];
|
|
1418
|
+
}
|
|
1419
|
+
return {
|
|
1420
|
+
link,
|
|
1421
|
+
unlink,
|
|
1422
|
+
write
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
// src/core/palette.ts
|
|
1426
|
+
var Palette = class {
|
|
1427
|
+
#ids = /* @__PURE__ */ new Map();
|
|
1428
|
+
#colors = [];
|
|
1429
|
+
get colors() {
|
|
1430
|
+
return this.#colors;
|
|
1431
|
+
}
|
|
1432
|
+
get version() {
|
|
1433
|
+
return this.#colors.length;
|
|
1434
|
+
}
|
|
1435
|
+
id(color) {
|
|
1436
|
+
let id = this.#ids.get(color);
|
|
1437
|
+
if (id === void 0) {
|
|
1438
|
+
id = this.#colors.length;
|
|
1439
|
+
this.#ids.set(color, id);
|
|
1440
|
+
this.#colors.push(color);
|
|
1441
|
+
}
|
|
1442
|
+
return id;
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
// src/core/scene.ts
|
|
1446
|
+
function sameList$1(a, b) {
|
|
1447
|
+
if (a === b) return true;
|
|
1448
|
+
if (!a || !b || a.length !== b.length) return false;
|
|
1449
|
+
return a.every((value, index) => Object.is(value, b[index]));
|
|
1450
|
+
}
|
|
1451
|
+
function sameScale(a, b) {
|
|
1452
|
+
if (a === b) return true;
|
|
1453
|
+
if (!a || !b || typeof a === "function" || typeof b === "function") return false;
|
|
1454
|
+
return a.type === b.type && sameList$1(a.domain, b.domain) && sameList$1(a.range, b.range);
|
|
1455
|
+
}
|
|
1456
|
+
function toNumber(value) {
|
|
1457
|
+
return typeof value === "number" && Number.isFinite(value) ? value : NaN;
|
|
1458
|
+
}
|
|
1459
|
+
function toText(value) {
|
|
1460
|
+
return typeof value === "string" ? value : null;
|
|
1461
|
+
}
|
|
1462
|
+
function toColor(value) {
|
|
1463
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
1464
|
+
}
|
|
1465
|
+
function finiteOr(value, fallback) {
|
|
1466
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1467
|
+
}
|
|
1468
|
+
function codeOf(codes, value) {
|
|
1469
|
+
return typeof value === "string" && Object.hasOwn(codes, value) ? codes[value] : 0;
|
|
1470
|
+
}
|
|
1471
|
+
function readBadges(mark, datum) {
|
|
1472
|
+
if (mark.badges.length === 0) return null;
|
|
1473
|
+
const badges = {};
|
|
1474
|
+
let count = 0;
|
|
1475
|
+
for (const [corner, read] of mark.badges) {
|
|
1476
|
+
const badge = read(datum);
|
|
1477
|
+
if (typeof badge === "object" && badge !== null) {
|
|
1478
|
+
badges[corner] = badge;
|
|
1479
|
+
count++;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return count === 0 ? null : badges;
|
|
1483
|
+
}
|
|
1484
|
+
function layersOf(marks) {
|
|
1485
|
+
const slots = {
|
|
1486
|
+
nodes: 0,
|
|
1487
|
+
edges: 0
|
|
1488
|
+
};
|
|
1489
|
+
return marks.map((mark) => ({
|
|
1490
|
+
kind: mark.type,
|
|
1491
|
+
slot: slots[mark.type]++
|
|
1492
|
+
}));
|
|
1493
|
+
}
|
|
1494
|
+
function createScene() {
|
|
1495
|
+
const nodes = new NodeColumns();
|
|
1496
|
+
const edges = new EdgeColumns();
|
|
1497
|
+
const palette = new Palette();
|
|
1498
|
+
const lanes = createEdgeLanes();
|
|
1499
|
+
const slots = /* @__PURE__ */ new Map();
|
|
1500
|
+
let layers = [];
|
|
1501
|
+
let layersDirty = false;
|
|
1502
|
+
let paletteVersion = 0;
|
|
1503
|
+
let dirtyNodes = /* @__PURE__ */ new Set();
|
|
1504
|
+
let dirtyEdges = /* @__PURE__ */ new Set();
|
|
1505
|
+
let scales = {};
|
|
1506
|
+
let colorScale = resolveColorScale(void 0);
|
|
1507
|
+
let sizeScale = resolveNumberScale(void 0, {
|
|
1508
|
+
type: "sqrt",
|
|
1509
|
+
range: defaults.nodeSizeRange
|
|
1510
|
+
});
|
|
1511
|
+
let widthScale = resolveNumberScale(void 0, {
|
|
1512
|
+
type: "linear",
|
|
1513
|
+
range: defaults.edgeWidthRange
|
|
1514
|
+
});
|
|
1515
|
+
let full = false;
|
|
1516
|
+
const nodeColorValues = [];
|
|
1517
|
+
const edgeColorValues = [];
|
|
1518
|
+
const sizeValues = [];
|
|
1519
|
+
const widthValues = [];
|
|
1520
|
+
const nodeColorExtent = new ExtentTracker();
|
|
1521
|
+
const edgeColorExtent = new ExtentTracker();
|
|
1522
|
+
const sizeExtent = new ExtentTracker();
|
|
1523
|
+
const widthExtent = new ExtentTracker();
|
|
1524
|
+
let colorExtent = null;
|
|
1525
|
+
let sizeDomain = null;
|
|
1526
|
+
let widthDomain = null;
|
|
1527
|
+
function track(tracker, index, value) {
|
|
1528
|
+
if (typeof value === "number" && Number.isFinite(value)) tracker.set(index, value);
|
|
1529
|
+
else tracker.delete(index);
|
|
1530
|
+
}
|
|
1531
|
+
function readNode(record) {
|
|
1532
|
+
const index = record.index;
|
|
1533
|
+
const mark = compileNodeMark(record.mark);
|
|
1534
|
+
nodes.reserve(index);
|
|
1535
|
+
nodes.keys[index] = record.key;
|
|
1536
|
+
const size = mark.size ? toNumber(mark.size(record.datum)) : NaN;
|
|
1537
|
+
const color = mark.color?.(record.datum);
|
|
1538
|
+
sizeValues[index] = size;
|
|
1539
|
+
nodeColorValues[index] = color;
|
|
1540
|
+
track(sizeExtent, index, size);
|
|
1541
|
+
track(nodeColorExtent, index, color);
|
|
1542
|
+
}
|
|
1543
|
+
function readEdge(record) {
|
|
1544
|
+
const index = record.index;
|
|
1545
|
+
const mark = compileEdgeMark(record.mark);
|
|
1546
|
+
edges.reserve(index);
|
|
1547
|
+
edges.keys[index] = record.key;
|
|
1548
|
+
const width = mark.width ? toNumber(mark.width(record.datum)) : NaN;
|
|
1549
|
+
const color = mark.color?.(record.datum);
|
|
1550
|
+
widthValues[index] = width;
|
|
1551
|
+
edgeColorValues[index] = color;
|
|
1552
|
+
track(widthExtent, index, width);
|
|
1553
|
+
track(edgeColorExtent, index, color);
|
|
1554
|
+
}
|
|
1555
|
+
function writeNode(record) {
|
|
1556
|
+
const index = record.index;
|
|
1557
|
+
const mark = compileNodeMark(record.mark);
|
|
1558
|
+
const datum = record.datum;
|
|
1559
|
+
const size = sizeValues[index] ?? NaN;
|
|
1560
|
+
const fill = toColor(mark.fill?.(datum)) ?? colorScale.map(nodeColorValues[index], colorExtent) ?? defaults.nodeFill;
|
|
1561
|
+
nodes.alive[index] = 1;
|
|
1562
|
+
nodes.slot[index] = slots.get(record.mark) ?? 0;
|
|
1563
|
+
nodes.size[index] = Number.isNaN(size) ? defaults.nodeSize : sizeScale.map(size, sizeDomain);
|
|
1564
|
+
nodes.shape[index] = codeOf(shapeCodes, mark.shape?.(datum));
|
|
1565
|
+
nodes.fill[index] = palette.id(fill);
|
|
1566
|
+
nodes.stroke[index] = palette.id(toColor(mark.stroke?.(datum)) ?? defaults.nodeStroke);
|
|
1567
|
+
nodes.strokeWidth[index] = finiteOr(mark.strokeWidth?.(datum), defaults.nodeStrokeWidth);
|
|
1568
|
+
nodes.opacity[index] = Math.min(1, Math.max(0, finiteOr(mark.opacity?.(datum), 1)));
|
|
1569
|
+
nodes.screenUnits[index] = mark.screenUnits ? 1 : 0;
|
|
1570
|
+
nodes.label[index] = toText(mark.label?.(datum));
|
|
1571
|
+
nodes.secondary[index] = toText(mark.secondary?.(datum));
|
|
1572
|
+
nodes.image[index] = toText(mark.image?.(datum));
|
|
1573
|
+
nodes.badges[index] = readBadges(mark, datum);
|
|
1574
|
+
dirtyNodes.add(index);
|
|
1575
|
+
}
|
|
1576
|
+
function writeEdge(model, record) {
|
|
1577
|
+
const index = record.index;
|
|
1578
|
+
const mark = compileEdgeMark(record.mark);
|
|
1579
|
+
const datum = record.datum;
|
|
1580
|
+
const width = widthValues[index] ?? NaN;
|
|
1581
|
+
const color = toColor(mark.stroke?.(datum)) ?? colorScale.map(edgeColorValues[index], colorExtent) ?? defaults.edgeColor;
|
|
1582
|
+
edges.alive[index] = 1;
|
|
1583
|
+
edges.slot[index] = slots.get(record.mark) ?? 0;
|
|
1584
|
+
edges.source[index] = model.node(record.source)?.index ?? 0;
|
|
1585
|
+
edges.target[index] = model.node(record.target)?.index ?? 0;
|
|
1586
|
+
lanes.link(index, edges.source[index] ?? 0, edges.target[index] ?? 0);
|
|
1587
|
+
edges.color[index] = palette.id(color);
|
|
1588
|
+
edges.width[index] = Number.isNaN(width) ? defaults.edgeWidth : widthScale.map(width, widthDomain);
|
|
1589
|
+
edges.dash[index] = codeOf(dashCodes, mark.dash?.(datum));
|
|
1590
|
+
edges.arrow[index] = codeOf(arrowCodes, mark.arrow?.(datum));
|
|
1591
|
+
edges.opacity[index] = Math.min(1, Math.max(0, finiteOr(mark.opacity?.(datum), 1)));
|
|
1592
|
+
edges.label[index] = toText(mark.label?.(datum));
|
|
1593
|
+
dirtyEdges.add(index);
|
|
1594
|
+
}
|
|
1595
|
+
function removeNode(index) {
|
|
1596
|
+
nodes.clear(index);
|
|
1597
|
+
sizeValues[index] = NaN;
|
|
1598
|
+
nodeColorValues[index] = void 0;
|
|
1599
|
+
sizeExtent.delete(index);
|
|
1600
|
+
nodeColorExtent.delete(index);
|
|
1601
|
+
dirtyNodes.add(index);
|
|
1602
|
+
}
|
|
1603
|
+
function removeEdge(index) {
|
|
1604
|
+
lanes.unlink(index);
|
|
1605
|
+
edges.clear(index);
|
|
1606
|
+
widthValues[index] = NaN;
|
|
1607
|
+
edgeColorValues[index] = void 0;
|
|
1608
|
+
widthExtent.delete(index);
|
|
1609
|
+
edgeColorExtent.delete(index);
|
|
1610
|
+
dirtyEdges.add(index);
|
|
1611
|
+
}
|
|
1612
|
+
function include(target, ids, keys, lookup) {
|
|
1613
|
+
for (const index of ids) {
|
|
1614
|
+
const key = keys[index];
|
|
1615
|
+
if (target.has(index) || key === void 0) continue;
|
|
1616
|
+
const record = lookup(key);
|
|
1617
|
+
if (record) target.set(index, record);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return {
|
|
1621
|
+
nodes,
|
|
1622
|
+
edges,
|
|
1623
|
+
palette,
|
|
1624
|
+
get layers() {
|
|
1625
|
+
return layers;
|
|
1626
|
+
},
|
|
1627
|
+
setDefinition(definition) {
|
|
1628
|
+
const next = definition.scales;
|
|
1629
|
+
if (!sameScale(next.color, scales.color)) {
|
|
1630
|
+
colorScale = resolveColorScale(next.color);
|
|
1631
|
+
full = true;
|
|
1632
|
+
}
|
|
1633
|
+
if (!sameScale(next.size, scales.size)) {
|
|
1634
|
+
sizeScale = resolveNumberScale(next.size, {
|
|
1635
|
+
type: "sqrt",
|
|
1636
|
+
range: defaults.nodeSizeRange
|
|
1637
|
+
});
|
|
1638
|
+
full = true;
|
|
1639
|
+
}
|
|
1640
|
+
if (!sameScale(next.width, scales.width)) {
|
|
1641
|
+
widthScale = resolveNumberScale(next.width, {
|
|
1642
|
+
type: "linear",
|
|
1643
|
+
range: defaults.edgeWidthRange
|
|
1644
|
+
});
|
|
1645
|
+
full = true;
|
|
1646
|
+
}
|
|
1647
|
+
scales = next;
|
|
1648
|
+
const nextLayers = layersOf(definition.marks);
|
|
1649
|
+
slots.clear();
|
|
1650
|
+
definition.marks.forEach((mark, position) => {
|
|
1651
|
+
slots.set(mark, nextLayers[position]?.slot ?? 0);
|
|
1652
|
+
});
|
|
1653
|
+
if (nextLayers.length !== layers.length || nextLayers.some((layer, i) => layer.kind !== layers[i]?.kind || layer.slot !== layers[i]?.slot)) {
|
|
1654
|
+
layers = nextLayers;
|
|
1655
|
+
layersDirty = true;
|
|
1656
|
+
}
|
|
1657
|
+
},
|
|
1658
|
+
apply(model, changes) {
|
|
1659
|
+
for (const { index } of changes.nodes.removed) removeNode(index);
|
|
1660
|
+
for (const { index } of changes.edges.removed) removeEdge(index);
|
|
1661
|
+
const nodeRecords = /* @__PURE__ */ new Map();
|
|
1662
|
+
const edgeRecords = /* @__PURE__ */ new Map();
|
|
1663
|
+
if (full) {
|
|
1664
|
+
for (const record of model.nodes()) nodeRecords.set(record.index, record);
|
|
1665
|
+
for (const record of model.edges()) edgeRecords.set(record.index, record);
|
|
1666
|
+
full = false;
|
|
1667
|
+
}
|
|
1668
|
+
for (const ref of [...changes.nodes.added, ...changes.nodes.updated]) {
|
|
1669
|
+
const record = model.node(ref.key);
|
|
1670
|
+
if (record) nodeRecords.set(record.index, record);
|
|
1671
|
+
}
|
|
1672
|
+
for (const ref of [...changes.edges.added, ...changes.edges.updated]) {
|
|
1673
|
+
const record = model.edge(ref.key);
|
|
1674
|
+
if (record) edgeRecords.set(record.index, record);
|
|
1675
|
+
}
|
|
1676
|
+
for (const record of nodeRecords.values()) readNode(record);
|
|
1677
|
+
for (const record of edgeRecords.values()) readEdge(record);
|
|
1678
|
+
const nextColor = colorScale.needsExtent ? combineExtents(nodeColorExtent.extent, edgeColorExtent.extent) : null;
|
|
1679
|
+
if (!sameExtent(nextColor, colorExtent)) {
|
|
1680
|
+
colorExtent = nextColor;
|
|
1681
|
+
include(nodeRecords, nodeColorExtent.ids(), nodes.keys, (key) => model.node(key));
|
|
1682
|
+
include(edgeRecords, edgeColorExtent.ids(), edges.keys, (key) => model.edge(key));
|
|
1683
|
+
}
|
|
1684
|
+
const nextSize = sizeScale.needsExtent ? sizeExtent.extent : null;
|
|
1685
|
+
if (!sameExtent(nextSize, sizeDomain)) {
|
|
1686
|
+
sizeDomain = nextSize;
|
|
1687
|
+
include(nodeRecords, sizeExtent.ids(), nodes.keys, (key) => model.node(key));
|
|
1688
|
+
}
|
|
1689
|
+
const nextWidth = widthScale.needsExtent ? widthExtent.extent : null;
|
|
1690
|
+
if (!sameExtent(nextWidth, widthDomain)) {
|
|
1691
|
+
widthDomain = nextWidth;
|
|
1692
|
+
include(edgeRecords, widthExtent.ids(), edges.keys, (key) => model.edge(key));
|
|
1693
|
+
}
|
|
1694
|
+
for (const record of nodeRecords.values()) writeNode(record);
|
|
1695
|
+
for (const record of edgeRecords.values()) writeEdge(model, record);
|
|
1696
|
+
lanes.write(edges, dirtyEdges);
|
|
1697
|
+
},
|
|
1698
|
+
takeDirty() {
|
|
1699
|
+
const dirty = {
|
|
1700
|
+
nodes: dirtyNodes,
|
|
1701
|
+
edges: dirtyEdges,
|
|
1702
|
+
palette: palette.version !== paletteVersion,
|
|
1703
|
+
layers: layersDirty
|
|
1704
|
+
};
|
|
1705
|
+
dirtyNodes = /* @__PURE__ */ new Set();
|
|
1706
|
+
dirtyEdges = /* @__PURE__ */ new Set();
|
|
1707
|
+
paletteVersion = palette.version;
|
|
1708
|
+
layersDirty = false;
|
|
1709
|
+
return dirty;
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
const noPaint = 4294967295;
|
|
1714
|
+
const tableOffset = {
|
|
1715
|
+
nodes: 0,
|
|
1716
|
+
edges: 32
|
|
1717
|
+
};
|
|
1718
|
+
var DirtyRange = class {
|
|
1719
|
+
from = Number.POSITIVE_INFINITY;
|
|
1720
|
+
to = -1;
|
|
1721
|
+
add(index) {
|
|
1722
|
+
if (index < this.from) this.from = index;
|
|
1723
|
+
if (index > this.to) this.to = index;
|
|
1724
|
+
}
|
|
1725
|
+
take() {
|
|
1726
|
+
if (this.to < this.from) return null;
|
|
1727
|
+
const range = [this.from, this.to];
|
|
1728
|
+
this.from = Number.POSITIVE_INFINITY;
|
|
1729
|
+
this.to = -1;
|
|
1730
|
+
return range;
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
function grow(array, capacity) {
|
|
1734
|
+
if (capacity <= array.length) return array;
|
|
1735
|
+
const next = new Uint32Array(Math.max(capacity, array.length * 2));
|
|
1736
|
+
next.set(array);
|
|
1737
|
+
return next;
|
|
1738
|
+
}
|
|
1739
|
+
var StateBuffers = class {
|
|
1740
|
+
numbers = (/* @__PURE__ */ new Float32Array(256)).fill(-1);
|
|
1741
|
+
paint = (/* @__PURE__ */ new Uint32Array(256)).fill(noPaint);
|
|
1742
|
+
labelFlags = (/* @__PURE__ */ new Int8Array(32)).fill(-1);
|
|
1743
|
+
nodeDirty = new DirtyRange();
|
|
1744
|
+
edgeDirty = new DirtyRange();
|
|
1745
|
+
nodeMask = /* @__PURE__ */ new Uint32Array(0);
|
|
1746
|
+
edgeMask = /* @__PURE__ */ new Uint32Array(0);
|
|
1747
|
+
highlightedNodes = [];
|
|
1748
|
+
highlightedEdges = [];
|
|
1749
|
+
tableVersion = 0;
|
|
1750
|
+
highlightVersion = 0;
|
|
1751
|
+
reserve(nodeCapacity, edgeCapacity) {
|
|
1752
|
+
this.nodeMask = grow(this.nodeMask, nodeCapacity);
|
|
1753
|
+
this.edgeMask = grow(this.edgeMask, edgeCapacity);
|
|
1754
|
+
}
|
|
1755
|
+
setNode(index, mask) {
|
|
1756
|
+
this.nodeMask = grow(this.nodeMask, index + 1);
|
|
1757
|
+
if (this.nodeMask[index] === mask >>> 0) return;
|
|
1758
|
+
this.nodeMask[index] = mask >>> 0;
|
|
1759
|
+
this.nodeDirty.add(index);
|
|
1760
|
+
}
|
|
1761
|
+
setEdge(index, mask) {
|
|
1762
|
+
this.edgeMask = grow(this.edgeMask, index + 1);
|
|
1763
|
+
if (this.edgeMask[index] === mask >>> 0) return;
|
|
1764
|
+
this.edgeMask[index] = mask >>> 0;
|
|
1765
|
+
this.edgeDirty.add(index);
|
|
1766
|
+
}
|
|
1767
|
+
setStyle(kind, bit, style) {
|
|
1768
|
+
const at = (tableOffset[kind] + bit) * 4;
|
|
1769
|
+
this.numbers[at] = style?.opacity ?? -1;
|
|
1770
|
+
this.numbers[at + 1] = style?.strokeWidth ?? -1;
|
|
1771
|
+
this.numbers[at + 2] = style?.halo === void 0 ? -1 : style.halo ? 1 : 0;
|
|
1772
|
+
this.numbers[at + 3] = style?.label === void 0 ? -1 : style.label ? 1 : 0;
|
|
1773
|
+
this.paint[at] = style?.fill ?? 4294967295;
|
|
1774
|
+
this.paint[at + 1] = style?.stroke ?? 4294967295;
|
|
1775
|
+
if (kind === "nodes") this.labelFlags[bit] = this.numbers[at + 3] ?? -1;
|
|
1776
|
+
this.tableVersion++;
|
|
1777
|
+
}
|
|
1778
|
+
setHighlighted(nodes, edges) {
|
|
1779
|
+
this.highlightedNodes = nodes;
|
|
1780
|
+
this.highlightedEdges = edges;
|
|
1781
|
+
this.highlightVersion++;
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
// src/core/view.ts
|
|
1785
|
+
const defaultZoomLimits = {
|
|
1786
|
+
min: .001,
|
|
1787
|
+
max: 64
|
|
1788
|
+
};
|
|
1789
|
+
function clampZoom(zoom, limits = defaultZoomLimits) {
|
|
1790
|
+
return Math.min(limits.max, Math.max(limits.min, zoom));
|
|
1791
|
+
}
|
|
1792
|
+
function toScreen(view, viewport, point) {
|
|
1793
|
+
return {
|
|
1794
|
+
x: (point.x - view.x) * view.zoom + viewport.width / 2,
|
|
1795
|
+
y: (point.y - view.y) * view.zoom + viewport.height / 2
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
function toWorld(view, viewport, point) {
|
|
1799
|
+
return {
|
|
1800
|
+
x: (point.x - viewport.width / 2) / view.zoom + view.x,
|
|
1801
|
+
y: (point.y - viewport.height / 2) / view.zoom + view.y
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
function panBy(view, dx, dy) {
|
|
1805
|
+
return {
|
|
1806
|
+
x: view.x - dx / view.zoom,
|
|
1807
|
+
y: view.y - dy / view.zoom,
|
|
1808
|
+
zoom: view.zoom
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
function zoomAround(view, viewport, anchor, factor, limits = defaultZoomLimits) {
|
|
1812
|
+
const zoom = clampZoom(view.zoom * factor, limits);
|
|
1813
|
+
const before = toWorld(view, viewport, anchor);
|
|
1814
|
+
const after = toWorld({
|
|
1815
|
+
x: view.x,
|
|
1816
|
+
y: view.y,
|
|
1817
|
+
zoom
|
|
1818
|
+
}, viewport, anchor);
|
|
1819
|
+
return {
|
|
1820
|
+
x: view.x + before.x - after.x,
|
|
1821
|
+
y: view.y + before.y - after.y,
|
|
1822
|
+
zoom
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
function fitBounds(bounds, viewport, padding, limits = defaultZoomLimits) {
|
|
1826
|
+
const width = Math.max(1, viewport.width - padding * 2);
|
|
1827
|
+
const height = Math.max(1, viewport.height - padding * 2);
|
|
1828
|
+
const spanX = Math.max(1e-9, bounds.maxX - bounds.minX);
|
|
1829
|
+
const spanY = Math.max(1e-9, bounds.maxY - bounds.minY);
|
|
1830
|
+
return {
|
|
1831
|
+
x: (bounds.minX + bounds.maxX) / 2,
|
|
1832
|
+
y: (bounds.minY + bounds.maxY) / 2,
|
|
1833
|
+
zoom: clampZoom(Math.min(width / spanX, height / spanY), limits)
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
function wheelFactor(deltaY, deltaMode, viewportHeight, speed) {
|
|
1837
|
+
const delta = Math.max(-120, Math.min(120, deltaY * (deltaMode === 1 ? 16 : deltaMode === 2 ? viewportHeight : 1)));
|
|
1838
|
+
return Math.exp(-delta * .0025 * speed);
|
|
1839
|
+
}
|
|
1840
|
+
function easeInOut(t) {
|
|
1841
|
+
return t < .5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
|
|
1842
|
+
}
|
|
1843
|
+
function interpolateView(from, to, t) {
|
|
1844
|
+
return {
|
|
1845
|
+
x: from.x + (to.x - from.x) * t,
|
|
1846
|
+
y: from.y + (to.y - from.y) * t,
|
|
1847
|
+
zoom: Math.exp(Math.log(from.zoom) + (Math.log(to.zoom) - Math.log(from.zoom)) * t)
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
// src/dev.ts
|
|
1851
|
+
function detect() {
|
|
1852
|
+
try {
|
|
1853
|
+
return process.env.NODE_ENV !== "production";
|
|
1854
|
+
} catch {
|
|
1855
|
+
return false;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
const dev = detect();
|
|
1859
|
+
// src/dom/theme.ts
|
|
1860
|
+
const tokenPattern = /var\(\s*--graph-([a-z0-9-]+)\s*\)/g;
|
|
1861
|
+
function withFallbacks(color) {
|
|
1862
|
+
return color.replace(tokenPattern, (match, name) => {
|
|
1863
|
+
const fallback = lightTheme[name];
|
|
1864
|
+
return fallback === void 0 ? match : `var(--graph-${name}, ${fallback})`;
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
var ThemeResolver = class {
|
|
1868
|
+
#host;
|
|
1869
|
+
#probe;
|
|
1870
|
+
#context;
|
|
1871
|
+
constructor(host) {
|
|
1872
|
+
this.#host = host;
|
|
1873
|
+
this.#probe = document.createElement("span");
|
|
1874
|
+
this.#probe.setAttribute("aria-hidden", "true");
|
|
1875
|
+
this.#probe.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;visibility:hidden";
|
|
1876
|
+
host.append(this.#probe);
|
|
1877
|
+
const canvas = document.createElement("canvas");
|
|
1878
|
+
canvas.width = 1;
|
|
1879
|
+
canvas.height = 1;
|
|
1880
|
+
this.#context = canvas.getContext("2d", { willReadFrequently: true });
|
|
1881
|
+
}
|
|
1882
|
+
color(css) {
|
|
1883
|
+
const probe = this.#probe;
|
|
1884
|
+
probe.style.color = "";
|
|
1885
|
+
probe.style.color = withFallbacks(css);
|
|
1886
|
+
const computed = getComputedStyle(probe).color;
|
|
1887
|
+
const context = this.#context;
|
|
1888
|
+
if (!context) return [
|
|
1889
|
+
0,
|
|
1890
|
+
0,
|
|
1891
|
+
0,
|
|
1892
|
+
255
|
|
1893
|
+
];
|
|
1894
|
+
context.clearRect(0, 0, 1, 1);
|
|
1895
|
+
context.fillStyle = "#000";
|
|
1896
|
+
context.fillStyle = computed;
|
|
1897
|
+
context.fillRect(0, 0, 1, 1);
|
|
1898
|
+
const [r = 0, g = 0, b = 0, a = 0] = context.getImageData(0, 0, 1, 1).data;
|
|
1899
|
+
return [
|
|
1900
|
+
r,
|
|
1901
|
+
g,
|
|
1902
|
+
b,
|
|
1903
|
+
a
|
|
1904
|
+
];
|
|
1905
|
+
}
|
|
1906
|
+
palette(colors, target, from = 0) {
|
|
1907
|
+
for (let id = from; id < colors.length; id++) target.set(this.color(colors[id] ?? ""), id * 4);
|
|
1908
|
+
}
|
|
1909
|
+
theme() {
|
|
1910
|
+
const font = getComputedStyle(this.#host).getPropertyValue("--graph-font").trim();
|
|
1911
|
+
return {
|
|
1912
|
+
labelInk: this.color(themeColor("ink")),
|
|
1913
|
+
labelMuted: this.color(themeColor("ink-muted")),
|
|
1914
|
+
labelHalo: this.color(themeColor("label-halo")),
|
|
1915
|
+
halo: this.color(themeColor("halo")),
|
|
1916
|
+
font: font === "" ? defaultFont : font
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
destroy() {
|
|
1920
|
+
this.#probe.remove();
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
function observeTheme(host, onChange) {
|
|
1924
|
+
const observer = new MutationObserver(onChange);
|
|
1925
|
+
const attributes = [
|
|
1926
|
+
"class",
|
|
1927
|
+
"style",
|
|
1928
|
+
"data-theme"
|
|
1929
|
+
];
|
|
1930
|
+
for (const element of [
|
|
1931
|
+
document.documentElement,
|
|
1932
|
+
document.body,
|
|
1933
|
+
host
|
|
1934
|
+
]) if (element) observer.observe(element, {
|
|
1935
|
+
attributes: true,
|
|
1936
|
+
attributeFilter: attributes
|
|
1937
|
+
});
|
|
1938
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
1939
|
+
media.addEventListener("change", onChange);
|
|
1940
|
+
return () => {
|
|
1941
|
+
observer.disconnect();
|
|
1942
|
+
media.removeEventListener("change", onChange);
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
// src/webgl/gl.ts
|
|
1946
|
+
const textureWidth = 1024;
|
|
1947
|
+
function createProgram(gl, vertex, fragment, blocks) {
|
|
1948
|
+
const program = gl.createProgram();
|
|
1949
|
+
const shaders = [];
|
|
1950
|
+
for (const [type, source] of [[gl.VERTEX_SHADER, vertex], [gl.FRAGMENT_SHADER, fragment]]) {
|
|
1951
|
+
const shader = gl.createShader(type);
|
|
1952
|
+
if (!shader) throw new Error("graph: cannot create a WebGL shader");
|
|
1953
|
+
gl.shaderSource(shader, source);
|
|
1954
|
+
gl.compileShader(shader);
|
|
1955
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error(`graph: shader compilation failed\n${gl.getShaderInfoLog(shader) ?? ""}`);
|
|
1956
|
+
gl.attachShader(program, shader);
|
|
1957
|
+
shaders.push(shader);
|
|
1958
|
+
}
|
|
1959
|
+
gl.linkProgram(program);
|
|
1960
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(`graph: program link failed\n${gl.getProgramInfoLog(program) ?? ""}`);
|
|
1961
|
+
for (const shader of shaders) {
|
|
1962
|
+
gl.detachShader(program, shader);
|
|
1963
|
+
gl.deleteShader(shader);
|
|
1964
|
+
}
|
|
1965
|
+
for (const [name, binding] of Object.entries(blocks)) {
|
|
1966
|
+
const index = gl.getUniformBlockIndex(program, name);
|
|
1967
|
+
if (index !== gl.INVALID_INDEX) gl.uniformBlockBinding(program, index, binding);
|
|
1968
|
+
}
|
|
1969
|
+
return program;
|
|
1970
|
+
}
|
|
1971
|
+
function formats(gl) {
|
|
1972
|
+
return {
|
|
1973
|
+
rg32f: {
|
|
1974
|
+
internal: gl.RG32F,
|
|
1975
|
+
format: gl.RG,
|
|
1976
|
+
type: gl.FLOAT,
|
|
1977
|
+
components: 2
|
|
1978
|
+
},
|
|
1979
|
+
rgba32f: {
|
|
1980
|
+
internal: gl.RGBA32F,
|
|
1981
|
+
format: gl.RGBA,
|
|
1982
|
+
type: gl.FLOAT,
|
|
1983
|
+
components: 4
|
|
1984
|
+
},
|
|
1985
|
+
rgba32ui: {
|
|
1986
|
+
internal: gl.RGBA32UI,
|
|
1987
|
+
format: gl.RGBA_INTEGER,
|
|
1988
|
+
type: gl.UNSIGNED_INT,
|
|
1989
|
+
components: 4
|
|
1990
|
+
},
|
|
1991
|
+
r32ui: {
|
|
1992
|
+
internal: gl.R32UI,
|
|
1993
|
+
format: gl.RED_INTEGER,
|
|
1994
|
+
type: gl.UNSIGNED_INT,
|
|
1995
|
+
components: 1
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
var DataTexture = class {
|
|
2000
|
+
texture;
|
|
2001
|
+
#gl;
|
|
2002
|
+
#format;
|
|
2003
|
+
#create;
|
|
2004
|
+
#rows = 0;
|
|
2005
|
+
data;
|
|
2006
|
+
constructor(gl, format, create) {
|
|
2007
|
+
this.#gl = gl;
|
|
2008
|
+
this.#format = format;
|
|
2009
|
+
this.#create = create;
|
|
2010
|
+
this.data = create(0);
|
|
2011
|
+
this.texture = gl.createTexture();
|
|
2012
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
2013
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
2014
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
2015
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
2016
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
2017
|
+
}
|
|
2018
|
+
get rows() {
|
|
2019
|
+
return this.#rows;
|
|
2020
|
+
}
|
|
2021
|
+
reserve(capacity) {
|
|
2022
|
+
const rows = Math.max(1, Math.ceil(capacity / textureWidth));
|
|
2023
|
+
if (rows <= this.#rows) return false;
|
|
2024
|
+
const data = this.#create(rows * textureWidth * this.#format.components);
|
|
2025
|
+
data.set(this.data);
|
|
2026
|
+
this.data = data;
|
|
2027
|
+
this.#rows = rows;
|
|
2028
|
+
const gl = this.#gl;
|
|
2029
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
2030
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
2031
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, this.#format.internal, textureWidth, rows, 0, this.#format.format, this.#format.type, data);
|
|
2032
|
+
return true;
|
|
2033
|
+
}
|
|
2034
|
+
upload(first, last) {
|
|
2035
|
+
if (last < first || this.#rows === 0) return;
|
|
2036
|
+
const gl = this.#gl;
|
|
2037
|
+
const startRow = Math.floor(first / textureWidth);
|
|
2038
|
+
const endRow = Math.min(this.#rows - 1, Math.floor(last / textureWidth));
|
|
2039
|
+
const stride = textureWidth * this.#format.components;
|
|
2040
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
2041
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
2042
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, startRow, textureWidth, endRow - startRow + 1, this.#format.format, this.#format.type, this.data.subarray(startRow * stride, (endRow + 1) * stride));
|
|
2043
|
+
}
|
|
2044
|
+
destroy() {
|
|
2045
|
+
this.#gl.deleteTexture(this.texture);
|
|
2046
|
+
}
|
|
2047
|
+
};
|
|
2048
|
+
function createBuffer(gl, data, usage) {
|
|
2049
|
+
const buffer = gl.createBuffer();
|
|
2050
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
2051
|
+
gl.bufferData(gl.ARRAY_BUFFER, data, usage);
|
|
2052
|
+
return buffer;
|
|
2053
|
+
}
|
|
2054
|
+
function createVertexArray(gl, program, attributes) {
|
|
2055
|
+
const vao = gl.createVertexArray();
|
|
2056
|
+
gl.bindVertexArray(vao);
|
|
2057
|
+
for (const attribute of attributes) {
|
|
2058
|
+
const location = gl.getAttribLocation(program, attribute.name);
|
|
2059
|
+
if (location < 0) continue;
|
|
2060
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, attribute.buffer);
|
|
2061
|
+
gl.enableVertexAttribArray(location);
|
|
2062
|
+
const stride = attribute.stride ?? 0;
|
|
2063
|
+
const offset = attribute.offset ?? 0;
|
|
2064
|
+
if (attribute.integer) gl.vertexAttribIPointer(location, attribute.size, attribute.type, stride, offset);
|
|
2065
|
+
else gl.vertexAttribPointer(location, attribute.size, attribute.type, attribute.normalized ?? false, stride, offset);
|
|
2066
|
+
gl.vertexAttribDivisor(location, attribute.divisor);
|
|
2067
|
+
}
|
|
2068
|
+
gl.bindVertexArray(null);
|
|
2069
|
+
return vao;
|
|
2070
|
+
}
|
|
2071
|
+
const atlasWidth = 1024;
|
|
2072
|
+
const glyphBuffer = 3;
|
|
2073
|
+
const maxHeight = 4096;
|
|
2074
|
+
var GlyphAtlas = class {
|
|
2075
|
+
pixels = new Uint8Array(atlasWidth * 256);
|
|
2076
|
+
height = 256;
|
|
2077
|
+
generation = 0;
|
|
2078
|
+
dirtyFrom = Number.POSITIVE_INFINITY;
|
|
2079
|
+
dirtyTo = -1;
|
|
2080
|
+
#glyphs = /* @__PURE__ */ new Map();
|
|
2081
|
+
#lines = {
|
|
2082
|
+
"400": /* @__PURE__ */ new Map(),
|
|
2083
|
+
"500": /* @__PURE__ */ new Map()
|
|
2084
|
+
};
|
|
2085
|
+
#renderers = /* @__PURE__ */ new Map();
|
|
2086
|
+
#font;
|
|
2087
|
+
#penX = 0;
|
|
2088
|
+
#penY = 0;
|
|
2089
|
+
#rowHeight = 0;
|
|
2090
|
+
constructor(font) {
|
|
2091
|
+
this.#font = font;
|
|
2092
|
+
}
|
|
2093
|
+
setFont(font) {
|
|
2094
|
+
if (font === this.#font) return false;
|
|
2095
|
+
this.#font = font;
|
|
2096
|
+
this.#glyphs.clear();
|
|
2097
|
+
this.#lines["400"].clear();
|
|
2098
|
+
this.#lines["500"].clear();
|
|
2099
|
+
this.#renderers.clear();
|
|
2100
|
+
this.#penX = 0;
|
|
2101
|
+
this.#penY = 0;
|
|
2102
|
+
this.#rowHeight = 0;
|
|
2103
|
+
this.pixels.fill(0);
|
|
2104
|
+
this.generation++;
|
|
2105
|
+
return true;
|
|
2106
|
+
}
|
|
2107
|
+
line(text, weight) {
|
|
2108
|
+
const lines = this.#lines[weight];
|
|
2109
|
+
const cached = lines.get(text);
|
|
2110
|
+
if (cached) return cached;
|
|
2111
|
+
const glyphs = [];
|
|
2112
|
+
let pen = 0;
|
|
2113
|
+
for (const char of text) {
|
|
2114
|
+
const glyph = this.#glyph(char, weight);
|
|
2115
|
+
if (glyph.width > 0 && char.trim() !== "") glyphs.push({
|
|
2116
|
+
glyph,
|
|
2117
|
+
x: pen + glyph.left - glyphBuffer
|
|
2118
|
+
});
|
|
2119
|
+
pen += glyph.advance;
|
|
2120
|
+
}
|
|
2121
|
+
const layout = {
|
|
2122
|
+
width: pen,
|
|
2123
|
+
glyphs: glyphs.map((g) => ({
|
|
2124
|
+
glyph: g.glyph,
|
|
2125
|
+
x: g.x - pen / 2
|
|
2126
|
+
}))
|
|
2127
|
+
};
|
|
2128
|
+
lines.set(text, layout);
|
|
2129
|
+
return layout;
|
|
2130
|
+
}
|
|
2131
|
+
takeDirty() {
|
|
2132
|
+
if (this.dirtyTo < this.dirtyFrom) return null;
|
|
2133
|
+
const range = [this.dirtyFrom, this.dirtyTo];
|
|
2134
|
+
this.dirtyFrom = Number.POSITIVE_INFINITY;
|
|
2135
|
+
this.dirtyTo = -1;
|
|
2136
|
+
return range;
|
|
2137
|
+
}
|
|
2138
|
+
#renderer(weight) {
|
|
2139
|
+
let renderer = this.#renderers.get(weight);
|
|
2140
|
+
if (!renderer) {
|
|
2141
|
+
renderer = new TinySDF({
|
|
2142
|
+
fontSize: 24,
|
|
2143
|
+
buffer: glyphBuffer,
|
|
2144
|
+
radius: 8,
|
|
2145
|
+
cutoff: .25,
|
|
2146
|
+
fontFamily: this.#font,
|
|
2147
|
+
fontWeight: weight
|
|
2148
|
+
});
|
|
2149
|
+
this.#renderers.set(weight, renderer);
|
|
2150
|
+
}
|
|
2151
|
+
return renderer;
|
|
2152
|
+
}
|
|
2153
|
+
#glyph(char, weight) {
|
|
2154
|
+
const key = `${weight}${char}`;
|
|
2155
|
+
const cached = this.#glyphs.get(key);
|
|
2156
|
+
if (cached) return cached;
|
|
2157
|
+
const drawn = this.#renderer(weight).draw(char);
|
|
2158
|
+
if (this.#penX + drawn.width > 1024) {
|
|
2159
|
+
this.#penX = 0;
|
|
2160
|
+
this.#penY += this.#rowHeight + 1;
|
|
2161
|
+
this.#rowHeight = 0;
|
|
2162
|
+
}
|
|
2163
|
+
if (this.#penY + drawn.height > this.height) this.#grow(this.#penY + drawn.height);
|
|
2164
|
+
const glyph = {
|
|
2165
|
+
x: this.#penX,
|
|
2166
|
+
y: this.#penY,
|
|
2167
|
+
width: drawn.width,
|
|
2168
|
+
height: drawn.height,
|
|
2169
|
+
left: drawn.glyphLeft,
|
|
2170
|
+
top: 24 - drawn.glyphTop - glyphBuffer,
|
|
2171
|
+
advance: drawn.glyphAdvance
|
|
2172
|
+
};
|
|
2173
|
+
if (this.#penY + drawn.height <= this.height) {
|
|
2174
|
+
for (let row = 0; row < drawn.height; row++) this.pixels.set(drawn.data.subarray(row * drawn.width, (row + 1) * drawn.width), (glyph.y + row) * atlasWidth + glyph.x);
|
|
2175
|
+
this.dirtyFrom = Math.min(this.dirtyFrom, glyph.y);
|
|
2176
|
+
this.dirtyTo = Math.max(this.dirtyTo, glyph.y + drawn.height - 1);
|
|
2177
|
+
}
|
|
2178
|
+
this.#penX += drawn.width + 1;
|
|
2179
|
+
this.#rowHeight = Math.max(this.#rowHeight, drawn.height);
|
|
2180
|
+
this.#glyphs.set(key, glyph);
|
|
2181
|
+
return glyph;
|
|
2182
|
+
}
|
|
2183
|
+
#grow(required) {
|
|
2184
|
+
let height = this.height;
|
|
2185
|
+
while (height < required && height < maxHeight) height *= 2;
|
|
2186
|
+
if (height === this.height) return;
|
|
2187
|
+
const pixels = new Uint8Array(atlasWidth * height);
|
|
2188
|
+
pixels.set(this.pixels);
|
|
2189
|
+
this.pixels = pixels;
|
|
2190
|
+
this.height = height;
|
|
2191
|
+
this.generation++;
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
const primarySize = 12;
|
|
2195
|
+
const lineHeight = 1.25;
|
|
2196
|
+
const labelMin = 8;
|
|
2197
|
+
const cell = 8;
|
|
2198
|
+
const maxLabels = 4e3;
|
|
2199
|
+
const cullMargin = 400;
|
|
2200
|
+
var GlyphBuffer = class {
|
|
2201
|
+
data = /* @__PURE__ */ new ArrayBuffer(8192);
|
|
2202
|
+
count = 0;
|
|
2203
|
+
#u32 = new Uint32Array(this.data);
|
|
2204
|
+
#f32 = new Float32Array(this.data);
|
|
2205
|
+
#u16 = new Uint16Array(this.data);
|
|
2206
|
+
clear() {
|
|
2207
|
+
this.count = 0;
|
|
2208
|
+
}
|
|
2209
|
+
push(node, kind, line, atlasHeight) {
|
|
2210
|
+
for (const { glyph, x } of line.glyphs) {
|
|
2211
|
+
if ((this.count + 1) * 32 > this.data.byteLength) this.#grow();
|
|
2212
|
+
const at = this.count * 8;
|
|
2213
|
+
this.#u32[at] = node;
|
|
2214
|
+
this.#u32[at + 1] = kind;
|
|
2215
|
+
this.#f32[at + 2] = x;
|
|
2216
|
+
this.#f32[at + 3] = glyph.top;
|
|
2217
|
+
this.#f32[at + 4] = glyph.width;
|
|
2218
|
+
this.#f32[at + 5] = glyph.height;
|
|
2219
|
+
const uv = at * 2 + 12;
|
|
2220
|
+
this.#u16[uv] = Math.round(glyph.x / atlasWidth * 65535);
|
|
2221
|
+
this.#u16[uv + 1] = Math.round(glyph.y / atlasHeight * 65535);
|
|
2222
|
+
this.#u16[uv + 2] = Math.round((glyph.x + glyph.width) / atlasWidth * 65535);
|
|
2223
|
+
this.#u16[uv + 3] = Math.round((glyph.y + glyph.height) / atlasHeight * 65535);
|
|
2224
|
+
this.count++;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
#grow() {
|
|
2228
|
+
const data = /* @__PURE__ */ new ArrayBuffer(this.data.byteLength * 2);
|
|
2229
|
+
new Uint8Array(data).set(new Uint8Array(this.data));
|
|
2230
|
+
this.data = data;
|
|
2231
|
+
this.#u32 = new Uint32Array(data);
|
|
2232
|
+
this.#f32 = new Float32Array(data);
|
|
2233
|
+
this.#u16 = new Uint16Array(data);
|
|
2234
|
+
}
|
|
2235
|
+
};
|
|
2236
|
+
function labelVisible(mask, flags) {
|
|
2237
|
+
let bits = mask >>> 0;
|
|
2238
|
+
while (bits !== 0) {
|
|
2239
|
+
const bit = 31 - Math.clz32(bits & -bits);
|
|
2240
|
+
bits &= bits - 1;
|
|
2241
|
+
const flag = flags[bit] ?? -1;
|
|
2242
|
+
if (flag >= 0) return flag === 1;
|
|
2243
|
+
}
|
|
2244
|
+
return true;
|
|
2245
|
+
}
|
|
2246
|
+
var LabelLayer = class {
|
|
2247
|
+
main = new GlyphBuffer();
|
|
2248
|
+
highlight = new GlyphBuffer();
|
|
2249
|
+
#occupancy = /* @__PURE__ */ new Uint8Array(0);
|
|
2250
|
+
select(frame, atlas) {
|
|
2251
|
+
this.main.clear();
|
|
2252
|
+
this.highlight.clear();
|
|
2253
|
+
const { nodes, positions, view, width, height } = frame;
|
|
2254
|
+
const cols = Math.ceil(width / cell) + 1;
|
|
2255
|
+
const rows = Math.ceil(height / cell) + 1;
|
|
2256
|
+
if (this.#occupancy.length < cols * rows) this.#occupancy = new Uint8Array(cols * rows);
|
|
2257
|
+
else this.#occupancy.fill(0, 0, cols * rows);
|
|
2258
|
+
const occupancy = this.#occupancy;
|
|
2259
|
+
const highlighted = new Set(frame.highlighted);
|
|
2260
|
+
const worldPrimary = primarySize * Math.min(1, view.zoom);
|
|
2261
|
+
let placed = 0;
|
|
2262
|
+
const place = (index, force) => {
|
|
2263
|
+
const label = nodes.label[index];
|
|
2264
|
+
if (!label || nodes.alive[index] !== 1) return;
|
|
2265
|
+
if (!labelVisible(frame.nodeMask[index] ?? 0, frame.labelFlags)) return;
|
|
2266
|
+
const screenUnits = nodes.screenUnits[index] === 1;
|
|
2267
|
+
const primary = screenUnits ? primarySize : worldPrimary;
|
|
2268
|
+
if (primary < labelMin) return;
|
|
2269
|
+
const diameter = screenUnits ? nodes.size[index] ?? 0 : (nodes.size[index] ?? 0) * view.zoom;
|
|
2270
|
+
const radius = Math.max(diameter / 2, 1.25);
|
|
2271
|
+
const x = (positions.x(index) - view.x) * view.zoom + width / 2;
|
|
2272
|
+
const y = (positions.y(index) - view.y) * view.zoom + height / 2;
|
|
2273
|
+
if (x < -400 || x > width + cullMargin) return;
|
|
2274
|
+
if (y + radius > height || y + radius + primary * lineHeight * 2 < -3) return;
|
|
2275
|
+
const line = atlas.line(label, "500");
|
|
2276
|
+
const secondaryText = nodes.secondary[index];
|
|
2277
|
+
const secondary = secondaryText ? atlas.line(secondaryText, "400") : null;
|
|
2278
|
+
const scale = primary / 24;
|
|
2279
|
+
const textWidth = Math.max(line.width * scale, secondary ? secondary.width * scale * 11 / 12 : 0);
|
|
2280
|
+
const top = y + radius + 3;
|
|
2281
|
+
const bottom = top + primary * lineHeight * (secondary ? 2 : 1);
|
|
2282
|
+
const left = x - textWidth / 2;
|
|
2283
|
+
const right = x + textWidth / 2;
|
|
2284
|
+
if (right < 0 || left > width || bottom < 0 || top > height) return;
|
|
2285
|
+
const c0 = Math.max(0, Math.floor(left / cell));
|
|
2286
|
+
const c1 = Math.min(cols - 1, Math.floor(right / cell));
|
|
2287
|
+
const r0 = Math.max(0, Math.floor(top / cell));
|
|
2288
|
+
const r1 = Math.min(rows - 1, Math.floor(bottom / cell));
|
|
2289
|
+
if (!force) {
|
|
2290
|
+
for (let row = r0; row <= r1; row++) for (let col = c0; col <= c1; col++) if (occupancy[row * cols + col] === 1) return;
|
|
2291
|
+
}
|
|
2292
|
+
for (let row = r0; row <= r1; row++) occupancy.fill(1, row * cols + c0, row * cols + c1 + 1);
|
|
2293
|
+
const target = force ? this.highlight : this.main;
|
|
2294
|
+
target.push(index, 0, line, atlas.height);
|
|
2295
|
+
if (secondary) target.push(index, 1, secondary, atlas.height);
|
|
2296
|
+
placed++;
|
|
2297
|
+
};
|
|
2298
|
+
for (const index of highlighted) place(index, true);
|
|
2299
|
+
if (worldPrimary < labelMin && !frame.screenLabels) return;
|
|
2300
|
+
const xy = positions.xy;
|
|
2301
|
+
const order = frame.order;
|
|
2302
|
+
const left = view.x - (width / 2 + cullMargin) / view.zoom;
|
|
2303
|
+
const right = view.x + (width / 2 + cullMargin) / view.zoom;
|
|
2304
|
+
const largest = nodes.size[order[0] ?? 0] ?? 0;
|
|
2305
|
+
const reach = cullMargin + (frame.screenLabels ? largest : largest * view.zoom) / 2;
|
|
2306
|
+
const top = view.y - (height / 2 + reach) / view.zoom;
|
|
2307
|
+
const bottom = view.y + height / 2 / view.zoom;
|
|
2308
|
+
for (let at = 0; at < frame.orderLength && placed < maxLabels; at++) {
|
|
2309
|
+
const index = order[at] ?? 0;
|
|
2310
|
+
const x = xy[index * 2] ?? 0;
|
|
2311
|
+
const y = xy[index * 2 + 1] ?? 0;
|
|
2312
|
+
if (x < left || x > right || y < top || y > bottom) continue;
|
|
2313
|
+
if (!highlighted.has(index)) place(index, false);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
function sortBySize(nodes, order) {
|
|
2318
|
+
let length = 0;
|
|
2319
|
+
let screenLabels = false;
|
|
2320
|
+
for (let index = 0; index < nodes.capacity; index++) if (nodes.alive[index] === 1 && nodes.label[index]) {
|
|
2321
|
+
order[length++] = index;
|
|
2322
|
+
if (nodes.screenUnits[index] === 1) screenLabels = true;
|
|
2323
|
+
}
|
|
2324
|
+
const sizes = nodes.size;
|
|
2325
|
+
order.subarray(0, length).sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0));
|
|
2326
|
+
return {
|
|
2327
|
+
length,
|
|
2328
|
+
screenLabels
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
// src/webgl/shaders.ts
|
|
2332
|
+
const header = `#version 300 es
|
|
2333
|
+
precision highp float;
|
|
2334
|
+
precision highp int;
|
|
2335
|
+
precision highp sampler2D;
|
|
2336
|
+
precision highp usampler2D;
|
|
2337
|
+
layout(std140) uniform Frame {
|
|
2338
|
+
vec2 center;
|
|
2339
|
+
vec2 viewport;
|
|
2340
|
+
float zoom;
|
|
2341
|
+
float pixelRatio;
|
|
2342
|
+
float haloWidth;
|
|
2343
|
+
float labelMin;
|
|
2344
|
+
vec4 haloColor;
|
|
2345
|
+
vec4 labelInk;
|
|
2346
|
+
vec4 labelMuted;
|
|
2347
|
+
vec4 labelHalo;
|
|
2348
|
+
};
|
|
2349
|
+
layout(std140) uniform States {
|
|
2350
|
+
vec4 stateNumbers[64];
|
|
2351
|
+
uvec4 statePaint[64];
|
|
2352
|
+
};
|
|
2353
|
+
uniform int layerSlot;
|
|
2354
|
+
uniform int overlay;
|
|
2355
|
+
uniform sampler2D palette;
|
|
2356
|
+
const uint highlightMask = 3u;
|
|
2357
|
+
const uint none = 0xffffffffu;
|
|
2358
|
+
const vec4 hidden = vec4(2.0, 2.0, 2.0, 1.0);
|
|
2359
|
+
vec2 toScreen(vec2 world) { return (world - center) * zoom + viewport * 0.5; }
|
|
2360
|
+
vec4 toClip(vec2 screen) {
|
|
2361
|
+
return vec4(screen.x / viewport.x * 2.0 - 1.0, 1.0 - screen.y / viewport.y * 2.0, 0.0, 1.0);
|
|
2362
|
+
}
|
|
2363
|
+
ivec2 texel(uint index) { return ivec2(int(index & 1023u), int(index >> 10u)); }
|
|
2364
|
+
vec4 paletteColor(uint id) { return texelFetch(palette, ivec2(int(id & 255u), int(id >> 8u)), 0); }
|
|
2365
|
+
struct Look { float opacity; float strokeWidth; float halo; float label; uint fill; uint stroke; };
|
|
2366
|
+
Look resolve(uint mask, Look look, int table) {
|
|
2367
|
+
bool opacity = false;
|
|
2368
|
+
bool strokeWidth = false;
|
|
2369
|
+
bool halo = false;
|
|
2370
|
+
bool label = false;
|
|
2371
|
+
bool fill = false;
|
|
2372
|
+
bool stroke = false;
|
|
2373
|
+
float base = look.opacity;
|
|
2374
|
+
for (int bit = 0; bit < 32; bit++) {
|
|
2375
|
+
if (mask == 0u) break;
|
|
2376
|
+
bool present = (mask & 1u) != 0u;
|
|
2377
|
+
mask >>= 1u;
|
|
2378
|
+
if (!present) continue;
|
|
2379
|
+
vec4 numbers = stateNumbers[table + bit];
|
|
2380
|
+
uvec4 paint = statePaint[table + bit];
|
|
2381
|
+
if (!opacity && numbers.x >= 0.0) { look.opacity = base * numbers.x; opacity = true; }
|
|
2382
|
+
if (!strokeWidth && numbers.y >= 0.0) { look.strokeWidth = numbers.y; strokeWidth = true; }
|
|
2383
|
+
if (!halo && numbers.z >= 0.0) { look.halo = numbers.z; halo = true; }
|
|
2384
|
+
if (!label && numbers.w >= 0.0) { look.label = numbers.w; label = true; }
|
|
2385
|
+
if (!fill && paint.x != none) { look.fill = paint.x; fill = true; }
|
|
2386
|
+
if (!stroke && paint.y != none) { look.stroke = paint.y; stroke = true; }
|
|
2387
|
+
}
|
|
2388
|
+
return look;
|
|
2389
|
+
}
|
|
2390
|
+
`;
|
|
2391
|
+
const nodeData = `
|
|
2392
|
+
uniform sampler2D nodePosition;
|
|
2393
|
+
uniform sampler2D nodeStyle;
|
|
2394
|
+
uniform usampler2D nodePaint;
|
|
2395
|
+
uniform usampler2D nodeState;
|
|
2396
|
+
float nodeDiameter(vec4 style) {
|
|
2397
|
+
uint flags = uint(style.w + 0.5);
|
|
2398
|
+
return (flags & 4u) != 0u ? style.x : style.x * zoom;
|
|
2399
|
+
}
|
|
2400
|
+
float nodeRadius(vec4 style) { return max(nodeDiameter(style) * 0.5, 1.25); }
|
|
2401
|
+
`;
|
|
2402
|
+
const edgeData = `
|
|
2403
|
+
uniform usampler2D edgeTopology;
|
|
2404
|
+
uniform sampler2D edgeStyle;
|
|
2405
|
+
uniform usampler2D edgeState;
|
|
2406
|
+
`;
|
|
2407
|
+
const nodeVertex = `${header}${nodeData}
|
|
2408
|
+
in vec2 corner;
|
|
2409
|
+
in uint instance;
|
|
2410
|
+
out vec2 vLocal;
|
|
2411
|
+
out float vRadius;
|
|
2412
|
+
out float vStrokeWidth;
|
|
2413
|
+
out float vHaloWidth;
|
|
2414
|
+
flat out uint vShape;
|
|
2415
|
+
flat out vec4 vFill;
|
|
2416
|
+
flat out vec4 vStroke;
|
|
2417
|
+
flat out float vOpacity;
|
|
2418
|
+
void main() {
|
|
2419
|
+
ivec2 t = texel(instance);
|
|
2420
|
+
vec4 style = texelFetch(nodeStyle, t, 0);
|
|
2421
|
+
uvec4 paint = texelFetch(nodePaint, t, 0);
|
|
2422
|
+
uint flags = uint(style.w + 0.5);
|
|
2423
|
+
uint mask = texelFetch(nodeState, t, 0).r;
|
|
2424
|
+
bool highlighted = (mask & highlightMask) != 0u;
|
|
2425
|
+
if ((flags & 8u) == 0u || (overlay == 0 && (int(paint.z) != layerSlot || highlighted))) {
|
|
2426
|
+
gl_Position = hidden;
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
Look look = resolve(mask, Look(style.z, style.y, 0.0, 1.0, paint.x, paint.y), 0);
|
|
2430
|
+
vec2 xy = texelFetch(nodePosition, t, 0).xy;
|
|
2431
|
+
float radius = nodeRadius(style);
|
|
2432
|
+
float halo = look.halo > 0.5 ? haloWidth : 0.0;
|
|
2433
|
+
vLocal = corner * (radius + halo + 1.5);
|
|
2434
|
+
vRadius = radius;
|
|
2435
|
+
vStrokeWidth = min(look.strokeWidth, radius * 0.5);
|
|
2436
|
+
vHaloWidth = halo;
|
|
2437
|
+
vShape = flags & 3u;
|
|
2438
|
+
vFill = paletteColor(look.fill);
|
|
2439
|
+
vStroke = paletteColor(look.stroke);
|
|
2440
|
+
vOpacity = look.opacity;
|
|
2441
|
+
gl_Position = toClip(toScreen(xy) + vLocal);
|
|
2442
|
+
}
|
|
2443
|
+
`;
|
|
2444
|
+
const nodeFragment = `${header}
|
|
2445
|
+
in vec2 vLocal;
|
|
2446
|
+
in float vRadius;
|
|
2447
|
+
in float vStrokeWidth;
|
|
2448
|
+
in float vHaloWidth;
|
|
2449
|
+
flat in uint vShape;
|
|
2450
|
+
flat in vec4 vFill;
|
|
2451
|
+
flat in vec4 vStroke;
|
|
2452
|
+
flat in float vOpacity;
|
|
2453
|
+
out vec4 fragColor;
|
|
2454
|
+
float shapeDistance(vec2 p, float r) {
|
|
2455
|
+
if (vShape == 0u) return length(p) - r;
|
|
2456
|
+
if (vShape == 1u) {
|
|
2457
|
+
vec2 q = abs(p) - vec2(r * 0.86);
|
|
2458
|
+
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0);
|
|
2459
|
+
}
|
|
2460
|
+
return (abs(p.x) + abs(p.y) - r * 1.2) * 0.70710678;
|
|
2461
|
+
}
|
|
2462
|
+
void main() {
|
|
2463
|
+
float aa = 0.5 / pixelRatio + 0.25;
|
|
2464
|
+
float d = shapeDistance(vLocal, vRadius);
|
|
2465
|
+
float inside = 1.0 - smoothstep(-aa, aa, d);
|
|
2466
|
+
float ring = vStrokeWidth > 0.0 ? smoothstep(-vStrokeWidth - aa, -vStrokeWidth + aa, d) : 0.0;
|
|
2467
|
+
vec4 color = mix(vFill, vStroke, ring);
|
|
2468
|
+
float alpha = color.a * inside * vOpacity;
|
|
2469
|
+
vec4 result = vec4(color.rgb * alpha, alpha);
|
|
2470
|
+
if (vHaloWidth > 0.0) {
|
|
2471
|
+
float h = haloColor.a * (1.0 - smoothstep(0.0, vHaloWidth, d)) * vOpacity;
|
|
2472
|
+
result += vec4(haloColor.rgb * h, h) * (1.0 - result.a);
|
|
2473
|
+
}
|
|
2474
|
+
if (result.a < 0.002) discard;
|
|
2475
|
+
fragColor = result;
|
|
2476
|
+
}
|
|
2477
|
+
`;
|
|
2478
|
+
const edgeVertex = `${header}${nodeData}${edgeData}
|
|
2479
|
+
in vec3 corner;
|
|
2480
|
+
in uint instance;
|
|
2481
|
+
out vec2 vPos;
|
|
2482
|
+
out float vStart;
|
|
2483
|
+
out float vEnd;
|
|
2484
|
+
out float vHalfWidth;
|
|
2485
|
+
out float vArrowLength;
|
|
2486
|
+
out float vArrowHalf;
|
|
2487
|
+
out float vHaloWidth;
|
|
2488
|
+
flat out uint vDash;
|
|
2489
|
+
flat out uint vArrow;
|
|
2490
|
+
flat out vec4 vColor;
|
|
2491
|
+
flat out float vOpacity;
|
|
2492
|
+
void main() {
|
|
2493
|
+
ivec2 t = texel(instance);
|
|
2494
|
+
uvec4 topology = texelFetch(edgeTopology, t, 0);
|
|
2495
|
+
vec4 style = texelFetch(edgeStyle, t, 0);
|
|
2496
|
+
uint packed = topology.w;
|
|
2497
|
+
uint mask = texelFetch(edgeState, t, 0).r;
|
|
2498
|
+
bool highlighted = (mask & highlightMask) != 0u;
|
|
2499
|
+
if ((packed & 16u) == 0u || (overlay == 0 && (int(packed >> 5u) != layerSlot || highlighted))) {
|
|
2500
|
+
gl_Position = hidden;
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
if (topology.x == topology.y || style.z != 0.0) {
|
|
2504
|
+
gl_Position = hidden;
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
Look look = resolve(mask, Look(style.y, style.x, 0.0, 1.0, none, topology.z), 32);
|
|
2508
|
+
ivec2 s = texel(topology.x);
|
|
2509
|
+
ivec2 e = texel(topology.y);
|
|
2510
|
+
vec2 ps = toScreen(texelFetch(nodePosition, s, 0).xy);
|
|
2511
|
+
vec2 pt = toScreen(texelFetch(nodePosition, e, 0).xy);
|
|
2512
|
+
vec2 delta = pt - ps;
|
|
2513
|
+
float len = length(delta);
|
|
2514
|
+
vec2 dir = len > 0.0 ? delta / len : vec2(1.0, 0.0);
|
|
2515
|
+
vec2 normal = vec2(-dir.y, dir.x);
|
|
2516
|
+
float width = max(look.strokeWidth, 0.5);
|
|
2517
|
+
float start = nodeRadius(texelFetch(nodeStyle, s, 0));
|
|
2518
|
+
float end = len - nodeRadius(texelFetch(nodeStyle, e, 0));
|
|
2519
|
+
float visible = end - start;
|
|
2520
|
+
uint arrow = (packed >> 2u) & 3u;
|
|
2521
|
+
float arrowLength = arrow != 0u && visible > width * 8.0 ? max(width * 4.0, 6.0) : 0.0;
|
|
2522
|
+
float arrowHalf = max(width * 1.6, 3.0);
|
|
2523
|
+
float halo = look.halo > 0.5 ? haloWidth : 0.0;
|
|
2524
|
+
float margin = 0.5 / pixelRatio + 0.5;
|
|
2525
|
+
float startHead = (arrow & 1u) != 0u ? arrowLength : 0.0;
|
|
2526
|
+
float endHead = (arrow & 2u) != 0u ? arrowLength : 0.0;
|
|
2527
|
+
float body = width * 0.5 + halo + margin;
|
|
2528
|
+
float head = max(body, arrowHalf + halo + margin);
|
|
2529
|
+
float along = corner.x < 0.5 ? start - margin
|
|
2530
|
+
: corner.x < 1.5 ? start + startHead
|
|
2531
|
+
: corner.x < 2.5 ? end - endHead
|
|
2532
|
+
: end + margin;
|
|
2533
|
+
float extent = corner.z < 0.5 ? body
|
|
2534
|
+
: corner.z < 1.5 ? (startHead > 0.0 ? head : body)
|
|
2535
|
+
: (endHead > 0.0 ? head : body);
|
|
2536
|
+
float across = corner.y * extent;
|
|
2537
|
+
vPos = vec2(along, across);
|
|
2538
|
+
vStart = start;
|
|
2539
|
+
vEnd = end;
|
|
2540
|
+
vHalfWidth = width * 0.5;
|
|
2541
|
+
vArrowLength = arrowLength;
|
|
2542
|
+
vArrowHalf = arrowHalf;
|
|
2543
|
+
vHaloWidth = halo;
|
|
2544
|
+
vDash = packed & 3u;
|
|
2545
|
+
vArrow = arrow;
|
|
2546
|
+
vColor = paletteColor(look.stroke);
|
|
2547
|
+
vOpacity = look.opacity;
|
|
2548
|
+
gl_Position = visible > 0.5 ? toClip(ps + dir * along + normal * across) : hidden;
|
|
2549
|
+
}
|
|
2550
|
+
`;
|
|
2551
|
+
const segments = 24 .toFixed(1);
|
|
2552
|
+
const curveVertex = `${header}${nodeData}${edgeData}
|
|
2553
|
+
in vec3 corner;
|
|
2554
|
+
in uint instance;
|
|
2555
|
+
out vec2 vPos;
|
|
2556
|
+
out float vStart;
|
|
2557
|
+
out float vEnd;
|
|
2558
|
+
out float vHalfWidth;
|
|
2559
|
+
out float vArrowLength;
|
|
2560
|
+
out float vArrowHalf;
|
|
2561
|
+
out float vHaloWidth;
|
|
2562
|
+
flat out uint vDash;
|
|
2563
|
+
flat out uint vArrow;
|
|
2564
|
+
flat out vec4 vColor;
|
|
2565
|
+
flat out float vOpacity;
|
|
2566
|
+
vec2 cubicAt(vec2 a, vec2 b, vec2 c, vec2 d, float t) {
|
|
2567
|
+
float u = 1.0 - t;
|
|
2568
|
+
return u * u * u * a + 3.0 * u * u * t * b + 3.0 * u * t * t * c + t * t * t * d;
|
|
2569
|
+
}
|
|
2570
|
+
vec2 cubicTangent(vec2 a, vec2 b, vec2 c, vec2 d, float t) {
|
|
2571
|
+
float u = 1.0 - t;
|
|
2572
|
+
return 3.0 * u * u * (b - a) + 6.0 * u * t * (c - b) + 3.0 * t * t * (d - c);
|
|
2573
|
+
}
|
|
2574
|
+
float crossing(vec2 a, vec2 b, vec2 c, vec2 d, vec2 point, float radius, float lo, float hi, bool rising) {
|
|
2575
|
+
for (int i = 0; i < 14; i++) {
|
|
2576
|
+
float mid = (lo + hi) * 0.5;
|
|
2577
|
+
bool inside = distance(cubicAt(a, b, c, d, mid), point) < radius;
|
|
2578
|
+
if (inside == rising) lo = mid; else hi = mid;
|
|
2579
|
+
}
|
|
2580
|
+
return (lo + hi) * 0.5;
|
|
2581
|
+
}
|
|
2582
|
+
float arcLength(vec2 a, vec2 b, vec2 c, vec2 d, float from, float to) {
|
|
2583
|
+
float total = 0.0;
|
|
2584
|
+
vec2 previous = cubicAt(a, b, c, d, from);
|
|
2585
|
+
for (int i = 1; i <= 8; i++) {
|
|
2586
|
+
vec2 next = cubicAt(a, b, c, d, mix(from, to, float(i) / 8.0));
|
|
2587
|
+
total += distance(previous, next);
|
|
2588
|
+
previous = next;
|
|
2589
|
+
}
|
|
2590
|
+
return total;
|
|
2591
|
+
}
|
|
2592
|
+
vec2 turn(vec2 v, float angle) {
|
|
2593
|
+
float c = cos(angle);
|
|
2594
|
+
float s = sin(angle);
|
|
2595
|
+
return vec2(v.x * c - v.y * s, v.x * s + v.y * c);
|
|
2596
|
+
}
|
|
2597
|
+
void main() {
|
|
2598
|
+
ivec2 t = texel(instance);
|
|
2599
|
+
uvec4 topology = texelFetch(edgeTopology, t, 0);
|
|
2600
|
+
vec4 style = texelFetch(edgeStyle, t, 0);
|
|
2601
|
+
uint packed = topology.w;
|
|
2602
|
+
uint mask = texelFetch(edgeState, t, 0).r;
|
|
2603
|
+
bool highlighted = (mask & highlightMask) != 0u;
|
|
2604
|
+
bool loop = topology.x == topology.y;
|
|
2605
|
+
if ((packed & 16u) == 0u || (!loop && style.z == 0.0) || (overlay == 0 && (int(packed >> 5u) != layerSlot || highlighted))) {
|
|
2606
|
+
gl_Position = hidden;
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
Look look = resolve(mask, Look(style.y, style.x, 0.0, 1.0, none, topology.z), 32);
|
|
2610
|
+
ivec2 s = texel(topology.x);
|
|
2611
|
+
ivec2 e = texel(topology.y);
|
|
2612
|
+
vec2 a = toScreen(texelFetch(nodePosition, s, 0).xy);
|
|
2613
|
+
vec2 d = toScreen(texelFetch(nodePosition, e, 0).xy);
|
|
2614
|
+
float sourceRadius = nodeRadius(texelFetch(nodeStyle, s, 0));
|
|
2615
|
+
float targetRadius = nodeRadius(texelFetch(nodeStyle, e, 0));
|
|
2616
|
+
vec2 b;
|
|
2617
|
+
vec2 c;
|
|
2618
|
+
if (loop) {
|
|
2619
|
+
float reach = sourceRadius + max(sourceRadius * 1.5, ${12 .toFixed(1)}) * (1.0 + ${loopGrowth.toFixed(2)} * style.z);
|
|
2620
|
+
float arm = reach / (0.75 * cos(${loopTurn.toFixed(4)}));
|
|
2621
|
+
vec2 u = vec2(0.70710678, -0.70710678);
|
|
2622
|
+
b = a + arm * turn(u, ${loopTurn.toFixed(4)});
|
|
2623
|
+
c = a + arm * turn(u, -${loopTurn.toFixed(4)});
|
|
2624
|
+
} else {
|
|
2625
|
+
vec2 delta = d - a;
|
|
2626
|
+
float len = length(delta);
|
|
2627
|
+
vec2 side = len > 0.0 ? vec2(-delta.y, delta.x) / len : vec2(0.0, 1.0);
|
|
2628
|
+
vec2 control = (a + d) * 0.5 + side * (2.0 * style.z * len);
|
|
2629
|
+
b = a + (control - a) * (2.0 / 3.0);
|
|
2630
|
+
c = d + (control - d) * (2.0 / 3.0);
|
|
2631
|
+
}
|
|
2632
|
+
float t0 = crossing(a, b, c, d, a, sourceRadius, 0.0, 0.5, true);
|
|
2633
|
+
float t1 = crossing(a, b, c, d, d, targetRadius, 0.5, 1.0, false);
|
|
2634
|
+
float visible = arcLength(a, b, c, d, t0, t1);
|
|
2635
|
+
float width = max(look.strokeWidth, 0.5);
|
|
2636
|
+
uint arrow = (packed >> 2u) & 3u;
|
|
2637
|
+
float arrowLength = arrow != 0u && visible > width * 8.0 ? max(width * 4.0, 6.0) : 0.0;
|
|
2638
|
+
float arrowHalf = max(width * 1.6, 3.0);
|
|
2639
|
+
float halo = look.halo > 0.5 ? haloWidth : 0.0;
|
|
2640
|
+
float margin = 0.5 / pixelRatio + 0.5;
|
|
2641
|
+
float startHead = (arrow & 1u) != 0u ? arrowLength : 0.0;
|
|
2642
|
+
float endHead = (arrow & 2u) != 0u ? arrowLength : 0.0;
|
|
2643
|
+
vec2 first = cubicAt(a, b, c, d, t0);
|
|
2644
|
+
vec2 last = cubicAt(a, b, c, d, t1);
|
|
2645
|
+
float ta = startHead > 0.0 ? crossing(a, b, c, d, first, startHead, t0, t1, true) : t0;
|
|
2646
|
+
float tb = endHead > 0.0 ? crossing(a, b, c, d, last, endHead, t0, t1, false) : t1;
|
|
2647
|
+
float body = width * 0.5 + halo + margin;
|
|
2648
|
+
float head = max(body, arrowHalf + halo + margin);
|
|
2649
|
+
int station = int(corner.x + 0.5);
|
|
2650
|
+
float at;
|
|
2651
|
+
float along;
|
|
2652
|
+
float push = 0.0;
|
|
2653
|
+
if (station == 0) {
|
|
2654
|
+
at = t0;
|
|
2655
|
+
along = -margin;
|
|
2656
|
+
push = -margin;
|
|
2657
|
+
} else if (station > int(${segments}) + 1) {
|
|
2658
|
+
at = t1;
|
|
2659
|
+
along = visible + margin;
|
|
2660
|
+
push = margin;
|
|
2661
|
+
} else {
|
|
2662
|
+
float f = float(station - 1) / ${segments};
|
|
2663
|
+
at = mix(ta, tb, f);
|
|
2664
|
+
along = mix(startHead, visible - endHead, f);
|
|
2665
|
+
}
|
|
2666
|
+
vec2 tangent = cubicTangent(a, b, c, d, at);
|
|
2667
|
+
float tangentLength = length(tangent);
|
|
2668
|
+
vec2 dir = tangentLength > 0.0 ? tangent / tangentLength : vec2(1.0, 0.0);
|
|
2669
|
+
vec2 normal = vec2(-dir.y, dir.x);
|
|
2670
|
+
float extent = corner.z < 0.5 ? body
|
|
2671
|
+
: corner.z < 1.5 ? (startHead > 0.0 ? head : body)
|
|
2672
|
+
: (endHead > 0.0 ? head : body);
|
|
2673
|
+
float across = corner.y * extent;
|
|
2674
|
+
vPos = vec2(along, across);
|
|
2675
|
+
vStart = 0.0;
|
|
2676
|
+
vEnd = visible;
|
|
2677
|
+
vHalfWidth = width * 0.5;
|
|
2678
|
+
vArrowLength = arrowLength;
|
|
2679
|
+
vArrowHalf = arrowHalf;
|
|
2680
|
+
vHaloWidth = halo;
|
|
2681
|
+
vDash = packed & 3u;
|
|
2682
|
+
vArrow = arrow;
|
|
2683
|
+
vColor = paletteColor(look.stroke);
|
|
2684
|
+
vOpacity = look.opacity;
|
|
2685
|
+
gl_Position = visible > 0.5 ? toClip(cubicAt(a, b, c, d, at) + dir * push + normal * across) : hidden;
|
|
2686
|
+
}
|
|
2687
|
+
`;
|
|
2688
|
+
const edgeFragment = `${header}
|
|
2689
|
+
in vec2 vPos;
|
|
2690
|
+
in float vStart;
|
|
2691
|
+
in float vEnd;
|
|
2692
|
+
in float vHalfWidth;
|
|
2693
|
+
in float vArrowLength;
|
|
2694
|
+
in float vArrowHalf;
|
|
2695
|
+
in float vHaloWidth;
|
|
2696
|
+
flat in uint vDash;
|
|
2697
|
+
flat in uint vArrow;
|
|
2698
|
+
flat in vec4 vColor;
|
|
2699
|
+
flat in float vOpacity;
|
|
2700
|
+
out vec4 fragColor;
|
|
2701
|
+
void main() {
|
|
2702
|
+
float aa = 0.5 / pixelRatio + 0.25;
|
|
2703
|
+
float along = vPos.x;
|
|
2704
|
+
float halfWidth = vHalfWidth;
|
|
2705
|
+
bool head = false;
|
|
2706
|
+
if (vArrowLength > 0.0) {
|
|
2707
|
+
if ((vArrow & 2u) != 0u && along > vEnd - vArrowLength) {
|
|
2708
|
+
halfWidth = vArrowHalf * (vEnd - along) / vArrowLength;
|
|
2709
|
+
head = true;
|
|
2710
|
+
}
|
|
2711
|
+
if ((vArrow & 1u) != 0u && along < vStart + vArrowLength) {
|
|
2712
|
+
halfWidth = vArrowHalf * (along - vStart) / vArrowLength;
|
|
2713
|
+
head = true;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
float span = smoothstep(vStart - aa, vStart + aa, along) * (1.0 - smoothstep(vEnd - aa, vEnd + aa, along));
|
|
2717
|
+
float coverage = (1.0 - smoothstep(halfWidth - aa, halfWidth + aa, abs(vPos.y))) * span;
|
|
2718
|
+
if (!head && vDash != 0u) {
|
|
2719
|
+
float width = vHalfWidth * 2.0;
|
|
2720
|
+
float offset = along - vStart;
|
|
2721
|
+
if (vDash == 1u) {
|
|
2722
|
+
float period = max(width * 5.0, 8.0);
|
|
2723
|
+
float m = mod(offset, period);
|
|
2724
|
+
coverage *= smoothstep(0.0, aa, m) * (1.0 - smoothstep(period * 0.6 - aa, period * 0.6 + aa, m));
|
|
2725
|
+
} else {
|
|
2726
|
+
float period = max(width * 2.5, 4.0);
|
|
2727
|
+
float m = mod(offset, period) - period * 0.5;
|
|
2728
|
+
float radius = max(width * 0.6, 1.0);
|
|
2729
|
+
coverage = (1.0 - smoothstep(radius - aa, radius + aa, length(vec2(m, vPos.y)))) * span;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
float alpha = vColor.a * coverage * vOpacity;
|
|
2733
|
+
vec4 result = vec4(vColor.rgb * alpha, alpha);
|
|
2734
|
+
if (vHaloWidth > 0.0) {
|
|
2735
|
+
float distance = max(abs(vPos.y) - halfWidth, 0.0);
|
|
2736
|
+
float h = haloColor.a * (1.0 - smoothstep(0.0, vHaloWidth, distance)) * span * vOpacity;
|
|
2737
|
+
result += vec4(haloColor.rgb * h, h) * (1.0 - result.a);
|
|
2738
|
+
}
|
|
2739
|
+
if (result.a < 0.002) discard;
|
|
2740
|
+
fragColor = result;
|
|
2741
|
+
}
|
|
2742
|
+
`;
|
|
2743
|
+
const labelVertex = `${header}${nodeData}
|
|
2744
|
+
uniform float atlasFontSize;
|
|
2745
|
+
in vec2 corner;
|
|
2746
|
+
in uint glyphNode;
|
|
2747
|
+
in uint glyphKind;
|
|
2748
|
+
in vec4 glyphRect;
|
|
2749
|
+
in vec4 glyphUv;
|
|
2750
|
+
out vec2 vUv;
|
|
2751
|
+
out float vScale;
|
|
2752
|
+
flat out uint vKind;
|
|
2753
|
+
flat out float vOpacity;
|
|
2754
|
+
void main() {
|
|
2755
|
+
ivec2 t = texel(glyphNode);
|
|
2756
|
+
vec4 style = texelFetch(nodeStyle, t, 0);
|
|
2757
|
+
uvec4 paint = texelFetch(nodePaint, t, 0);
|
|
2758
|
+
uint flags = uint(style.w + 0.5);
|
|
2759
|
+
uint mask = texelFetch(nodeState, t, 0).r;
|
|
2760
|
+
Look look = resolve(mask, Look(style.z, style.y, 0.0, 1.0, paint.x, paint.y), 0);
|
|
2761
|
+
float primary = (flags & 4u) != 0u ? 12.0 : 12.0 * min(1.0, zoom);
|
|
2762
|
+
if ((flags & 8u) == 0u || look.label < 0.5 || primary < labelMin) {
|
|
2763
|
+
gl_Position = hidden;
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
float size = glyphKind == 0u ? primary : primary * 11.0 / 12.0;
|
|
2767
|
+
float scale = size / atlasFontSize;
|
|
2768
|
+
float below = nodeRadius(style) + 3.0 + (glyphKind == 1u ? primary * 1.25 : 0.0);
|
|
2769
|
+
vec2 anchor = toScreen(texelFetch(nodePosition, t, 0).xy) + vec2(0.0, below);
|
|
2770
|
+
vUv = mix(glyphUv.xy, glyphUv.zw, corner);
|
|
2771
|
+
vScale = scale;
|
|
2772
|
+
vKind = glyphKind;
|
|
2773
|
+
vOpacity = look.opacity;
|
|
2774
|
+
gl_Position = toClip(anchor + (glyphRect.xy + corner * glyphRect.zw) * scale);
|
|
2775
|
+
}
|
|
2776
|
+
`;
|
|
2777
|
+
const labelFragment = `${header}
|
|
2778
|
+
uniform sampler2D atlas;
|
|
2779
|
+
in vec2 vUv;
|
|
2780
|
+
in float vScale;
|
|
2781
|
+
flat in uint vKind;
|
|
2782
|
+
flat in float vOpacity;
|
|
2783
|
+
out vec4 fragColor;
|
|
2784
|
+
void main() {
|
|
2785
|
+
float d = texture(atlas, vUv).r;
|
|
2786
|
+
float gamma = 0.08 / max(vScale * pixelRatio, 0.05);
|
|
2787
|
+
float ink = smoothstep(0.75 - gamma, 0.75 + gamma, d);
|
|
2788
|
+
float halo = smoothstep(0.55 - gamma, 0.55 + gamma, d);
|
|
2789
|
+
vec4 inkColor = vKind == 0u ? labelInk : labelMuted;
|
|
2790
|
+
vec3 rgb = mix(labelHalo.rgb, inkColor.rgb, ink);
|
|
2791
|
+
float alpha = max(halo * labelHalo.a, ink * inkColor.a) * vOpacity;
|
|
2792
|
+
if (alpha < 0.002) discard;
|
|
2793
|
+
fragColor = vec4(rgb * alpha, alpha);
|
|
2794
|
+
}
|
|
2795
|
+
`;
|
|
2796
|
+
const cornerNode = new Float32Array([
|
|
2797
|
+
-1,
|
|
2798
|
+
-1,
|
|
2799
|
+
1,
|
|
2800
|
+
-1,
|
|
2801
|
+
-1,
|
|
2802
|
+
1,
|
|
2803
|
+
1,
|
|
2804
|
+
1
|
|
2805
|
+
]);
|
|
2806
|
+
const cornerEdge = new Float32Array([
|
|
2807
|
+
0,
|
|
2808
|
+
-1,
|
|
2809
|
+
1,
|
|
2810
|
+
0,
|
|
2811
|
+
1,
|
|
2812
|
+
1,
|
|
2813
|
+
1,
|
|
2814
|
+
-1,
|
|
2815
|
+
1,
|
|
2816
|
+
1,
|
|
2817
|
+
1,
|
|
2818
|
+
1,
|
|
2819
|
+
1,
|
|
2820
|
+
-1,
|
|
2821
|
+
0,
|
|
2822
|
+
1,
|
|
2823
|
+
1,
|
|
2824
|
+
0,
|
|
2825
|
+
2,
|
|
2826
|
+
-1,
|
|
2827
|
+
0,
|
|
2828
|
+
2,
|
|
2829
|
+
1,
|
|
2830
|
+
0,
|
|
2831
|
+
2,
|
|
2832
|
+
-1,
|
|
2833
|
+
2,
|
|
2834
|
+
2,
|
|
2835
|
+
1,
|
|
2836
|
+
2,
|
|
2837
|
+
3,
|
|
2838
|
+
-1,
|
|
2839
|
+
2,
|
|
2840
|
+
3,
|
|
2841
|
+
1,
|
|
2842
|
+
2
|
|
2843
|
+
]);
|
|
2844
|
+
function curveCorners() {
|
|
2845
|
+
const values = [
|
|
2846
|
+
0,
|
|
2847
|
+
-1,
|
|
2848
|
+
1,
|
|
2849
|
+
0,
|
|
2850
|
+
1,
|
|
2851
|
+
1,
|
|
2852
|
+
1,
|
|
2853
|
+
-1,
|
|
2854
|
+
1,
|
|
2855
|
+
1,
|
|
2856
|
+
1,
|
|
2857
|
+
1
|
|
2858
|
+
];
|
|
2859
|
+
for (let station = 1; station <= 25; station++) values.push(station, -1, 0, station, 1, 0);
|
|
2860
|
+
const end = 25;
|
|
2861
|
+
values.push(end, -1, 2, end, 1, 2, 26, -1, 2, 26, 1, 2);
|
|
2862
|
+
return new Float32Array(values);
|
|
2863
|
+
}
|
|
2864
|
+
const cornerCurve = curveCorners();
|
|
2865
|
+
const cornerLabel = new Float32Array([
|
|
2866
|
+
0,
|
|
2867
|
+
0,
|
|
2868
|
+
1,
|
|
2869
|
+
0,
|
|
2870
|
+
0,
|
|
2871
|
+
1,
|
|
2872
|
+
1,
|
|
2873
|
+
1
|
|
2874
|
+
]);
|
|
2875
|
+
// src/webgl/renderer.ts
|
|
2876
|
+
const blocks = {
|
|
2877
|
+
Frame: 0,
|
|
2878
|
+
States: 1
|
|
2879
|
+
};
|
|
2880
|
+
const paletteWidth = 256;
|
|
2881
|
+
const labelInterval = 120;
|
|
2882
|
+
const units = {
|
|
2883
|
+
palette: 0,
|
|
2884
|
+
nodePosition: 1,
|
|
2885
|
+
nodeStyle: 2,
|
|
2886
|
+
nodePaint: 3,
|
|
2887
|
+
nodeState: 4,
|
|
2888
|
+
edgeTopology: 5,
|
|
2889
|
+
edgeStyle: 6,
|
|
2890
|
+
edgeState: 7,
|
|
2891
|
+
atlas: 8
|
|
2892
|
+
};
|
|
2893
|
+
function iota(length) {
|
|
2894
|
+
const values = new Uint32Array(length);
|
|
2895
|
+
for (let index = 0; index < length; index++) values[index] = index;
|
|
2896
|
+
return values;
|
|
2897
|
+
}
|
|
2898
|
+
function rgba(color) {
|
|
2899
|
+
return color.map((channel) => channel / 255);
|
|
2900
|
+
}
|
|
2901
|
+
function createWebglRenderer(canvas) {
|
|
2902
|
+
const context = canvas.getContext("webgl2", {
|
|
2903
|
+
alpha: true,
|
|
2904
|
+
antialias: false,
|
|
2905
|
+
premultipliedAlpha: true,
|
|
2906
|
+
preserveDrawingBuffer: false,
|
|
2907
|
+
powerPreference: "high-performance"
|
|
2908
|
+
});
|
|
2909
|
+
if (!context) throw new Error("graph: WebGL2 is not available");
|
|
2910
|
+
const gl = context;
|
|
2911
|
+
const format = formats(gl);
|
|
2912
|
+
const f32 = (length) => new Float32Array(length);
|
|
2913
|
+
const u32 = (length) => new Uint32Array(length);
|
|
2914
|
+
const nodePosition = new DataTexture(gl, format.rg32f, f32);
|
|
2915
|
+
const nodeStyle = new DataTexture(gl, format.rgba32f, f32);
|
|
2916
|
+
const nodePaint = new DataTexture(gl, format.rgba32ui, u32);
|
|
2917
|
+
const nodeState = new DataTexture(gl, format.r32ui, u32);
|
|
2918
|
+
const edgeTopology = new DataTexture(gl, format.rgba32ui, u32);
|
|
2919
|
+
const edgeStyle = new DataTexture(gl, format.rgba32f, f32);
|
|
2920
|
+
const edgeState = new DataTexture(gl, format.r32ui, u32);
|
|
2921
|
+
const nodeTextures = [
|
|
2922
|
+
nodePosition,
|
|
2923
|
+
nodeStyle,
|
|
2924
|
+
nodePaint,
|
|
2925
|
+
nodeState
|
|
2926
|
+
];
|
|
2927
|
+
const edgeTextures = [
|
|
2928
|
+
edgeTopology,
|
|
2929
|
+
edgeStyle,
|
|
2930
|
+
edgeState
|
|
2931
|
+
];
|
|
2932
|
+
const palette = gl.createTexture();
|
|
2933
|
+
let paletteRows = 0;
|
|
2934
|
+
const atlasTexture = gl.createTexture();
|
|
2935
|
+
let atlasGeneration = -1;
|
|
2936
|
+
let atlasRows = 0;
|
|
2937
|
+
for (const texture of [palette, atlasTexture]) {
|
|
2938
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
2939
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
2940
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
2941
|
+
const filter = texture === atlasTexture ? gl.LINEAR : gl.NEAREST;
|
|
2942
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
|
|
2943
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
|
|
2944
|
+
}
|
|
2945
|
+
const frameBlock = /* @__PURE__ */ new Float32Array(24);
|
|
2946
|
+
const frameBuffer = gl.createBuffer();
|
|
2947
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, frameBuffer);
|
|
2948
|
+
gl.bufferData(gl.UNIFORM_BUFFER, frameBlock.byteLength, gl.DYNAMIC_DRAW);
|
|
2949
|
+
gl.bindBufferBase(gl.UNIFORM_BUFFER, blocks.Frame, frameBuffer);
|
|
2950
|
+
const statesBlock = /* @__PURE__ */ new ArrayBuffer(2048);
|
|
2951
|
+
const statesBuffer = gl.createBuffer();
|
|
2952
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, statesBuffer);
|
|
2953
|
+
gl.bufferData(gl.UNIFORM_BUFFER, statesBlock.byteLength, gl.DYNAMIC_DRAW);
|
|
2954
|
+
gl.bindBufferBase(gl.UNIFORM_BUFFER, blocks.States, statesBuffer);
|
|
2955
|
+
let tableVersion = -1;
|
|
2956
|
+
let highlightVersion = -1;
|
|
2957
|
+
const nodeProgram = createProgram(gl, nodeVertex, nodeFragment, blocks);
|
|
2958
|
+
const edgeProgram = createProgram(gl, edgeVertex, edgeFragment, blocks);
|
|
2959
|
+
const labelProgram = createProgram(gl, labelVertex, labelFragment, blocks);
|
|
2960
|
+
const programs = [
|
|
2961
|
+
nodeProgram,
|
|
2962
|
+
edgeProgram,
|
|
2963
|
+
labelProgram
|
|
2964
|
+
];
|
|
2965
|
+
const locations = /* @__PURE__ */ new Map();
|
|
2966
|
+
function prepare(program) {
|
|
2967
|
+
gl.useProgram(program);
|
|
2968
|
+
for (const [name, unit] of Object.entries(units)) {
|
|
2969
|
+
const location = gl.getUniformLocation(program, name);
|
|
2970
|
+
if (location) gl.uniform1i(location, unit);
|
|
2971
|
+
}
|
|
2972
|
+
locations.set(program, {
|
|
2973
|
+
layerSlot: gl.getUniformLocation(program, "layerSlot"),
|
|
2974
|
+
overlay: gl.getUniformLocation(program, "overlay")
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
for (const program of programs) prepare(program);
|
|
2978
|
+
gl.useProgram(labelProgram);
|
|
2979
|
+
gl.uniform1f(gl.getUniformLocation(labelProgram, "atlasFontSize"), 24);
|
|
2980
|
+
const nodeCorner = createBuffer(gl, cornerNode, gl.STATIC_DRAW);
|
|
2981
|
+
const edgeCorner = createBuffer(gl, cornerEdge, gl.STATIC_DRAW);
|
|
2982
|
+
const curveCorner = createBuffer(gl, cornerCurve, gl.STATIC_DRAW);
|
|
2983
|
+
const labelCorner = createBuffer(gl, cornerLabel, gl.STATIC_DRAW);
|
|
2984
|
+
const nodeIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.STATIC_DRAW);
|
|
2985
|
+
const edgeIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.STATIC_DRAW);
|
|
2986
|
+
const nodeOverlayIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.DYNAMIC_DRAW);
|
|
2987
|
+
const edgeOverlayIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.DYNAMIC_DRAW);
|
|
2988
|
+
const curveIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.DYNAMIC_DRAW);
|
|
2989
|
+
const labelMainBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
|
|
2990
|
+
const labelHighlightBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
|
|
2991
|
+
const warmIndex = createBuffer(gl, /* @__PURE__ */ new Uint32Array(1), gl.STATIC_DRAW);
|
|
2992
|
+
const warmGlyph = createBuffer(gl, /* @__PURE__ */ new Uint8Array(32), gl.STATIC_DRAW);
|
|
2993
|
+
const buffers = [
|
|
2994
|
+
nodeCorner,
|
|
2995
|
+
edgeCorner,
|
|
2996
|
+
curveCorner,
|
|
2997
|
+
labelCorner,
|
|
2998
|
+
nodeIndices,
|
|
2999
|
+
edgeIndices,
|
|
3000
|
+
nodeOverlayIndices,
|
|
3001
|
+
edgeOverlayIndices,
|
|
3002
|
+
curveIndices,
|
|
3003
|
+
labelMainBuffer,
|
|
3004
|
+
labelHighlightBuffer,
|
|
3005
|
+
warmIndex,
|
|
3006
|
+
warmGlyph
|
|
3007
|
+
];
|
|
3008
|
+
const corner = (buffer, size = 2) => ({
|
|
3009
|
+
name: "corner",
|
|
3010
|
+
buffer,
|
|
3011
|
+
size,
|
|
3012
|
+
type: gl.FLOAT,
|
|
3013
|
+
divisor: 0
|
|
3014
|
+
});
|
|
3015
|
+
const instance = (buffer) => ({
|
|
3016
|
+
name: "instance",
|
|
3017
|
+
buffer,
|
|
3018
|
+
size: 1,
|
|
3019
|
+
type: gl.UNSIGNED_INT,
|
|
3020
|
+
integer: true,
|
|
3021
|
+
divisor: 1
|
|
3022
|
+
});
|
|
3023
|
+
const glyphs = (buffer) => [
|
|
3024
|
+
corner(labelCorner),
|
|
3025
|
+
{
|
|
3026
|
+
name: "glyphNode",
|
|
3027
|
+
buffer,
|
|
3028
|
+
size: 1,
|
|
3029
|
+
type: gl.UNSIGNED_INT,
|
|
3030
|
+
integer: true,
|
|
3031
|
+
divisor: 1,
|
|
3032
|
+
stride: 32,
|
|
3033
|
+
offset: 0
|
|
3034
|
+
},
|
|
3035
|
+
{
|
|
3036
|
+
name: "glyphKind",
|
|
3037
|
+
buffer,
|
|
3038
|
+
size: 1,
|
|
3039
|
+
type: gl.UNSIGNED_INT,
|
|
3040
|
+
integer: true,
|
|
3041
|
+
divisor: 1,
|
|
3042
|
+
stride: 32,
|
|
3043
|
+
offset: 4
|
|
3044
|
+
},
|
|
3045
|
+
{
|
|
3046
|
+
name: "glyphRect",
|
|
3047
|
+
buffer,
|
|
3048
|
+
size: 4,
|
|
3049
|
+
type: gl.FLOAT,
|
|
3050
|
+
divisor: 1,
|
|
3051
|
+
stride: 32,
|
|
3052
|
+
offset: 8
|
|
3053
|
+
},
|
|
3054
|
+
{
|
|
3055
|
+
name: "glyphUv",
|
|
3056
|
+
buffer,
|
|
3057
|
+
size: 4,
|
|
3058
|
+
type: gl.UNSIGNED_SHORT,
|
|
3059
|
+
normalized: true,
|
|
3060
|
+
divisor: 1,
|
|
3061
|
+
stride: 32,
|
|
3062
|
+
offset: 24
|
|
3063
|
+
}
|
|
3064
|
+
];
|
|
3065
|
+
const vertexArrays = {
|
|
3066
|
+
nodes: createVertexArray(gl, nodeProgram, [corner(nodeCorner), instance(nodeIndices)]),
|
|
3067
|
+
nodeOverlay: createVertexArray(gl, nodeProgram, [corner(nodeCorner), instance(nodeOverlayIndices)]),
|
|
3068
|
+
edges: createVertexArray(gl, edgeProgram, [corner(edgeCorner, 3), instance(edgeIndices)]),
|
|
3069
|
+
edgeOverlay: createVertexArray(gl, edgeProgram, [corner(edgeCorner, 3), instance(edgeOverlayIndices)]),
|
|
3070
|
+
labels: createVertexArray(gl, labelProgram, glyphs(labelMainBuffer)),
|
|
3071
|
+
labelHighlight: createVertexArray(gl, labelProgram, glyphs(labelHighlightBuffer))
|
|
3072
|
+
};
|
|
3073
|
+
const vertexCounts = /* @__PURE__ */ new Map([
|
|
3074
|
+
[nodeProgram, cornerNode.length / 2],
|
|
3075
|
+
[edgeProgram, cornerEdge.length / 3],
|
|
3076
|
+
[labelProgram, cornerLabel.length / 2]
|
|
3077
|
+
]);
|
|
3078
|
+
let warmed = false;
|
|
3079
|
+
let curvePass = null;
|
|
3080
|
+
function curves() {
|
|
3081
|
+
if (curvePass) return curvePass;
|
|
3082
|
+
const program = createProgram(gl, curveVertex, edgeFragment, blocks);
|
|
3083
|
+
prepare(program);
|
|
3084
|
+
programs.push(program);
|
|
3085
|
+
vertexCounts.set(program, cornerCurve.length / 3);
|
|
3086
|
+
curvePass = {
|
|
3087
|
+
program,
|
|
3088
|
+
main: createVertexArray(gl, program, [corner(curveCorner, 3), instance(curveIndices)]),
|
|
3089
|
+
overlay: createVertexArray(gl, program, [corner(curveCorner, 3), instance(edgeOverlayIndices)])
|
|
3090
|
+
};
|
|
3091
|
+
return curvePass;
|
|
3092
|
+
}
|
|
3093
|
+
let nodeCapacity = 0;
|
|
3094
|
+
let edgeCapacity = 0;
|
|
3095
|
+
let nodeOverlayCount = 0;
|
|
3096
|
+
let edgeOverlayCount = 0;
|
|
3097
|
+
const curved = /* @__PURE__ */ new Set();
|
|
3098
|
+
let curvedChanged = false;
|
|
3099
|
+
let input = null;
|
|
3100
|
+
const atlas = new GlyphAtlas("system-ui, sans-serif");
|
|
3101
|
+
const labels = new LabelLayer();
|
|
3102
|
+
let labelOrder = /* @__PURE__ */ new Uint32Array(0);
|
|
3103
|
+
let labelOrderLength = 0;
|
|
3104
|
+
let screenLabels = false;
|
|
3105
|
+
let labelsStale = true;
|
|
3106
|
+
let labelsMoved = false;
|
|
3107
|
+
let orderStale = true;
|
|
3108
|
+
let lastLabelTime = Number.NEGATIVE_INFINITY;
|
|
3109
|
+
let lastLabelView = "";
|
|
3110
|
+
function packNode(nodes, index) {
|
|
3111
|
+
const at = index * 4;
|
|
3112
|
+
const flags = (nodes.shape[index] ?? 0) | (nodes.screenUnits[index] ?? 0) << 2 | (nodes.alive[index] ?? 0) << 3;
|
|
3113
|
+
nodeStyle.data[at] = nodes.size[index] ?? 0;
|
|
3114
|
+
nodeStyle.data[at + 1] = nodes.strokeWidth[index] ?? 0;
|
|
3115
|
+
nodeStyle.data[at + 2] = nodes.opacity[index] ?? 1;
|
|
3116
|
+
nodeStyle.data[at + 3] = flags;
|
|
3117
|
+
nodePaint.data[at] = nodes.fill[index] ?? 0;
|
|
3118
|
+
nodePaint.data[at + 1] = nodes.stroke[index] ?? 0;
|
|
3119
|
+
nodePaint.data[at + 2] = nodes.slot[index] ?? 0;
|
|
3120
|
+
}
|
|
3121
|
+
function packEdge(edges, index) {
|
|
3122
|
+
const at = index * 4;
|
|
3123
|
+
edgeTopology.data[at] = edges.source[index] ?? 0;
|
|
3124
|
+
edgeTopology.data[at + 1] = edges.target[index] ?? 0;
|
|
3125
|
+
edgeTopology.data[at + 2] = edges.color[index] ?? 0;
|
|
3126
|
+
edgeTopology.data[at + 3] = (edges.dash[index] ?? 0) | (edges.arrow[index] ?? 0) << 2 | (edges.alive[index] ?? 0) << 4 | (edges.slot[index] ?? 0) << 5;
|
|
3127
|
+
edgeStyle.data[at] = edges.width[index] ?? 0;
|
|
3128
|
+
edgeStyle.data[at + 1] = edges.opacity[index] ?? 1;
|
|
3129
|
+
const bend = edges.bend[index] ?? 0;
|
|
3130
|
+
edgeStyle.data[at + 2] = bend;
|
|
3131
|
+
const isCurved = edges.alive[index] === 1 && (bend !== 0 || edges.source[index] === edges.target[index]);
|
|
3132
|
+
if (isCurved !== curved.has(index)) {
|
|
3133
|
+
if (isCurved) curved.add(index);
|
|
3134
|
+
else curved.delete(index);
|
|
3135
|
+
curvedChanged = true;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
function syncNodes(next) {
|
|
3139
|
+
const nodes = next.scene.nodes;
|
|
3140
|
+
const grown = nodeTextures.map((texture) => texture.reserve(nodes.capacity)).some(Boolean);
|
|
3141
|
+
if (nodes.capacity > nodeCapacity) {
|
|
3142
|
+
nodeCapacity = nodes.capacity;
|
|
3143
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, nodeIndices);
|
|
3144
|
+
gl.bufferData(gl.ARRAY_BUFFER, iota(nodeCapacity), gl.STATIC_DRAW);
|
|
3145
|
+
}
|
|
3146
|
+
let first = Number.POSITIVE_INFINITY;
|
|
3147
|
+
let last = -1;
|
|
3148
|
+
if (next.full || grown) {
|
|
3149
|
+
for (let index = 0; index < nodes.capacity; index++) packNode(nodes, index);
|
|
3150
|
+
first = 0;
|
|
3151
|
+
last = nodes.capacity - 1;
|
|
3152
|
+
} else for (const index of next.dirty.nodes) {
|
|
3153
|
+
packNode(nodes, index);
|
|
3154
|
+
first = Math.min(first, index);
|
|
3155
|
+
last = Math.max(last, index);
|
|
3156
|
+
}
|
|
3157
|
+
nodeStyle.upload(first, last);
|
|
3158
|
+
nodePaint.upload(first, last);
|
|
3159
|
+
if (next.positionsChanged || next.full || grown) {
|
|
3160
|
+
const xy = next.positions.xy;
|
|
3161
|
+
nodePosition.data.set(xy.subarray(0, Math.min(xy.length, nodePosition.data.length)));
|
|
3162
|
+
nodePosition.upload(0, nodeCapacity - 1);
|
|
3163
|
+
}
|
|
3164
|
+
const states = next.states;
|
|
3165
|
+
const range = states.nodeDirty.take();
|
|
3166
|
+
if (next.full || grown) {
|
|
3167
|
+
nodeState.data.set(states.nodeMask.subarray(0, Math.min(states.nodeMask.length, nodeState.data.length)));
|
|
3168
|
+
nodeState.upload(0, nodeCapacity - 1);
|
|
3169
|
+
} else if (range) {
|
|
3170
|
+
nodeState.data.set(states.nodeMask.subarray(range[0], range[1] + 1), range[0]);
|
|
3171
|
+
nodeState.upload(range[0], range[1]);
|
|
3172
|
+
}
|
|
3173
|
+
if (next.dirty.nodes.size > 0 || next.full) orderStale = true;
|
|
3174
|
+
}
|
|
3175
|
+
function syncEdges(next) {
|
|
3176
|
+
const edges = next.scene.edges;
|
|
3177
|
+
const grown = edgeTextures.map((texture) => texture.reserve(edges.capacity)).some(Boolean);
|
|
3178
|
+
if (edges.capacity > edgeCapacity) {
|
|
3179
|
+
edgeCapacity = edges.capacity;
|
|
3180
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, edgeIndices);
|
|
3181
|
+
gl.bufferData(gl.ARRAY_BUFFER, iota(edgeCapacity), gl.STATIC_DRAW);
|
|
3182
|
+
}
|
|
3183
|
+
let first = Number.POSITIVE_INFINITY;
|
|
3184
|
+
let last = -1;
|
|
3185
|
+
if (next.full || grown) {
|
|
3186
|
+
for (let index = 0; index < edges.capacity; index++) packEdge(edges, index);
|
|
3187
|
+
first = 0;
|
|
3188
|
+
last = edges.capacity - 1;
|
|
3189
|
+
} else for (const index of next.dirty.edges) {
|
|
3190
|
+
packEdge(edges, index);
|
|
3191
|
+
first = Math.min(first, index);
|
|
3192
|
+
last = Math.max(last, index);
|
|
3193
|
+
}
|
|
3194
|
+
edgeTopology.upload(first, last);
|
|
3195
|
+
edgeStyle.upload(first, last);
|
|
3196
|
+
if (curvedChanged) {
|
|
3197
|
+
curvedChanged = false;
|
|
3198
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, curveIndices);
|
|
3199
|
+
gl.bufferData(gl.ARRAY_BUFFER, Uint32Array.from(curved), gl.DYNAMIC_DRAW);
|
|
3200
|
+
}
|
|
3201
|
+
const states = next.states;
|
|
3202
|
+
const range = states.edgeDirty.take();
|
|
3203
|
+
if (next.full || grown) {
|
|
3204
|
+
edgeState.data.set(states.edgeMask.subarray(0, Math.min(states.edgeMask.length, edgeState.data.length)));
|
|
3205
|
+
edgeState.upload(0, edgeCapacity - 1);
|
|
3206
|
+
} else if (range) {
|
|
3207
|
+
edgeState.data.set(states.edgeMask.subarray(range[0], range[1] + 1), range[0]);
|
|
3208
|
+
edgeState.upload(range[0], range[1]);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
function syncStates(next) {
|
|
3212
|
+
const states = next.states;
|
|
3213
|
+
if (states.tableVersion !== tableVersion || next.full) {
|
|
3214
|
+
tableVersion = states.tableVersion;
|
|
3215
|
+
new Float32Array(statesBlock, 0, 256).set(states.numbers);
|
|
3216
|
+
new Uint32Array(statesBlock, 1024, 256).set(states.paint);
|
|
3217
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, statesBuffer);
|
|
3218
|
+
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, statesBlock);
|
|
3219
|
+
labelsStale = true;
|
|
3220
|
+
}
|
|
3221
|
+
if (states.highlightVersion !== highlightVersion || next.full) {
|
|
3222
|
+
highlightVersion = states.highlightVersion;
|
|
3223
|
+
nodeOverlayCount = states.highlightedNodes.length;
|
|
3224
|
+
edgeOverlayCount = states.highlightedEdges.length;
|
|
3225
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, nodeOverlayIndices);
|
|
3226
|
+
gl.bufferData(gl.ARRAY_BUFFER, Uint32Array.from(states.highlightedNodes), gl.DYNAMIC_DRAW);
|
|
3227
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, edgeOverlayIndices);
|
|
3228
|
+
gl.bufferData(gl.ARRAY_BUFFER, Uint32Array.from(states.highlightedEdges), gl.DYNAMIC_DRAW);
|
|
3229
|
+
labelsStale = true;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
function syncAtlas() {
|
|
3233
|
+
if (atlas.generation !== atlasGeneration || atlas.height !== atlasRows) {
|
|
3234
|
+
atlasGeneration = atlas.generation;
|
|
3235
|
+
atlasRows = atlas.height;
|
|
3236
|
+
atlas.takeDirty();
|
|
3237
|
+
gl.bindTexture(gl.TEXTURE_2D, atlasTexture);
|
|
3238
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
3239
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, atlasWidth, atlas.height, 0, gl.RED, gl.UNSIGNED_BYTE, atlas.pixels);
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
const range = atlas.takeDirty();
|
|
3243
|
+
if (!range) return;
|
|
3244
|
+
gl.bindTexture(gl.TEXTURE_2D, atlasTexture);
|
|
3245
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
3246
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, range[0], atlasWidth, range[1] - range[0] + 1, gl.RED, gl.UNSIGNED_BYTE, atlas.pixels.subarray(range[0] * atlasWidth, (range[1] + 1) * atlasWidth));
|
|
3247
|
+
}
|
|
3248
|
+
function selectLabels(frame, now) {
|
|
3249
|
+
if (!input) return -1;
|
|
3250
|
+
const key = `${frame.view.x},${frame.view.y},${frame.view.zoom},${frame.width},${frame.height}`;
|
|
3251
|
+
if (!labelsStale && !labelsMoved && key === lastLabelView) return -1;
|
|
3252
|
+
const wait = labelInterval - (now - lastLabelTime);
|
|
3253
|
+
if (!labelsStale && wait > 0) return wait;
|
|
3254
|
+
const nodes = input.scene.nodes;
|
|
3255
|
+
if (orderStale) {
|
|
3256
|
+
if (labelOrder.length < nodes.capacity) labelOrder = new Uint32Array(nodes.capacity);
|
|
3257
|
+
const sorted = sortBySize(nodes, labelOrder);
|
|
3258
|
+
labelOrderLength = sorted.length;
|
|
3259
|
+
screenLabels = sorted.screenLabels;
|
|
3260
|
+
orderStale = false;
|
|
3261
|
+
}
|
|
3262
|
+
const generation = atlas.generation;
|
|
3263
|
+
labels.select({
|
|
3264
|
+
nodes,
|
|
3265
|
+
positions: input.positions,
|
|
3266
|
+
view: frame.view,
|
|
3267
|
+
width: frame.width,
|
|
3268
|
+
height: frame.height,
|
|
3269
|
+
nodeMask: input.states.nodeMask,
|
|
3270
|
+
labelFlags: input.states.labelFlags,
|
|
3271
|
+
highlighted: input.states.highlightedNodes,
|
|
3272
|
+
order: labelOrder,
|
|
3273
|
+
orderLength: labelOrderLength,
|
|
3274
|
+
screenLabels
|
|
3275
|
+
}, atlas);
|
|
3276
|
+
if (atlas.generation !== generation) return selectLabels(frame, now);
|
|
3277
|
+
syncAtlas();
|
|
3278
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, labelMainBuffer);
|
|
3279
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(labels.main.data, 0, labels.main.count * 32), gl.DYNAMIC_DRAW);
|
|
3280
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, labelHighlightBuffer);
|
|
3281
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(labels.highlight.data, 0, labels.highlight.count * 32), gl.DYNAMIC_DRAW);
|
|
3282
|
+
labelsStale = false;
|
|
3283
|
+
labelsMoved = false;
|
|
3284
|
+
lastLabelTime = now;
|
|
3285
|
+
lastLabelView = key;
|
|
3286
|
+
return -1;
|
|
3287
|
+
}
|
|
3288
|
+
function draw(program, vao, count, slot, overlay) {
|
|
3289
|
+
if (count === 0) return;
|
|
3290
|
+
const location = locations.get(program);
|
|
3291
|
+
gl.useProgram(program);
|
|
3292
|
+
gl.uniform1i(location?.layerSlot ?? null, slot);
|
|
3293
|
+
gl.uniform1i(location?.overlay ?? null, overlay);
|
|
3294
|
+
gl.bindVertexArray(vao);
|
|
3295
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, vertexCounts.get(program) ?? 4, count);
|
|
3296
|
+
}
|
|
3297
|
+
function warm() {
|
|
3298
|
+
const passes = [
|
|
3299
|
+
[nodeProgram, [corner(nodeCorner), instance(warmIndex)]],
|
|
3300
|
+
[edgeProgram, [corner(edgeCorner, 3), instance(warmIndex)]],
|
|
3301
|
+
[labelProgram, glyphs(warmGlyph)]
|
|
3302
|
+
];
|
|
3303
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
3304
|
+
gl.scissor(0, 0, 0, 0);
|
|
3305
|
+
for (const [program, attributes] of passes) {
|
|
3306
|
+
const vao = createVertexArray(gl, program, attributes);
|
|
3307
|
+
draw(program, vao, 1, 0, 0);
|
|
3308
|
+
gl.bindVertexArray(null);
|
|
3309
|
+
gl.deleteVertexArray(vao);
|
|
3310
|
+
}
|
|
3311
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
3312
|
+
warmed = true;
|
|
3313
|
+
}
|
|
3314
|
+
return {
|
|
3315
|
+
sync(next) {
|
|
3316
|
+
input = next;
|
|
3317
|
+
syncNodes(next);
|
|
3318
|
+
syncEdges(next);
|
|
3319
|
+
syncStates(next);
|
|
3320
|
+
if (next.dirty.nodes.size > 0 || next.full) labelsStale = true;
|
|
3321
|
+
if (next.positionsChanged) labelsMoved = true;
|
|
3322
|
+
},
|
|
3323
|
+
setPalette(colors, count) {
|
|
3324
|
+
const rows = Math.max(1, Math.ceil(count / paletteWidth));
|
|
3325
|
+
const data = new Uint8Array(rows * paletteWidth * 4);
|
|
3326
|
+
data.set(colors.subarray(0, Math.min(colors.length, data.length)));
|
|
3327
|
+
gl.bindTexture(gl.TEXTURE_2D, palette);
|
|
3328
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
3329
|
+
if (rows !== paletteRows) {
|
|
3330
|
+
paletteRows = rows;
|
|
3331
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, paletteWidth, rows, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
|
3332
|
+
} else gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, paletteWidth, rows, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
|
3333
|
+
},
|
|
3334
|
+
setTheme(theme) {
|
|
3335
|
+
frameBlock.set(rgba(theme.halo), 8);
|
|
3336
|
+
frameBlock.set(rgba(theme.labelInk), 12);
|
|
3337
|
+
frameBlock.set(rgba(theme.labelMuted), 16);
|
|
3338
|
+
frameBlock.set(rgba(theme.labelHalo), 20);
|
|
3339
|
+
if (atlas.setFont(theme.font)) labelsStale = true;
|
|
3340
|
+
},
|
|
3341
|
+
render(frame) {
|
|
3342
|
+
const width = gl.drawingBufferWidth;
|
|
3343
|
+
const height = gl.drawingBufferHeight;
|
|
3344
|
+
frameBlock[0] = frame.view.x;
|
|
3345
|
+
frameBlock[1] = frame.view.y;
|
|
3346
|
+
frameBlock[2] = frame.width;
|
|
3347
|
+
frameBlock[3] = frame.height;
|
|
3348
|
+
frameBlock[4] = frame.view.zoom;
|
|
3349
|
+
frameBlock[5] = frame.pixelRatio;
|
|
3350
|
+
frameBlock[6] = 6;
|
|
3351
|
+
frameBlock[7] = 8;
|
|
3352
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, frameBuffer);
|
|
3353
|
+
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, frameBlock);
|
|
3354
|
+
const wait = selectLabels(frame, performance.now());
|
|
3355
|
+
gl.viewport(0, 0, width, height);
|
|
3356
|
+
gl.clearColor(0, 0, 0, 0);
|
|
3357
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
3358
|
+
gl.enable(gl.BLEND);
|
|
3359
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
3360
|
+
bind();
|
|
3361
|
+
if (!warmed) warm();
|
|
3362
|
+
for (const layer of input?.scene.layers ?? []) if (layer.kind === "edges") {
|
|
3363
|
+
draw(edgeProgram, vertexArrays.edges, edgeCapacity, layer.slot, 0);
|
|
3364
|
+
if (curved.size > 0) {
|
|
3365
|
+
const pass = curves();
|
|
3366
|
+
draw(pass.program, pass.main, curved.size, layer.slot, 0);
|
|
3367
|
+
}
|
|
3368
|
+
} else draw(nodeProgram, vertexArrays.nodes, nodeCapacity, layer.slot, 0);
|
|
3369
|
+
draw(labelProgram, vertexArrays.labels, labels.main.count, 0, 0);
|
|
3370
|
+
draw(edgeProgram, vertexArrays.edgeOverlay, edgeOverlayCount, 0, 1);
|
|
3371
|
+
if (curvePass && curved.size > 0) draw(curvePass.program, curvePass.overlay, edgeOverlayCount, 0, 1);
|
|
3372
|
+
draw(nodeProgram, vertexArrays.nodeOverlay, nodeOverlayCount, 0, 1);
|
|
3373
|
+
draw(labelProgram, vertexArrays.labelHighlight, labels.highlight.count, 0, 1);
|
|
3374
|
+
gl.bindVertexArray(null);
|
|
3375
|
+
return wait;
|
|
3376
|
+
},
|
|
3377
|
+
destroy() {
|
|
3378
|
+
for (const texture of [...nodeTextures, ...edgeTextures]) texture.destroy();
|
|
3379
|
+
gl.deleteTexture(palette);
|
|
3380
|
+
gl.deleteTexture(atlasTexture);
|
|
3381
|
+
for (const buffer of [
|
|
3382
|
+
...buffers,
|
|
3383
|
+
frameBuffer,
|
|
3384
|
+
statesBuffer
|
|
3385
|
+
]) gl.deleteBuffer(buffer);
|
|
3386
|
+
for (const vao of Object.values(vertexArrays)) gl.deleteVertexArray(vao);
|
|
3387
|
+
if (curvePass) {
|
|
3388
|
+
gl.deleteVertexArray(curvePass.main);
|
|
3389
|
+
gl.deleteVertexArray(curvePass.overlay);
|
|
3390
|
+
}
|
|
3391
|
+
for (const program of programs) gl.deleteProgram(program);
|
|
3392
|
+
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
3393
|
+
}
|
|
3394
|
+
};
|
|
3395
|
+
function bind() {
|
|
3396
|
+
const textures = [
|
|
3397
|
+
[units.palette, palette],
|
|
3398
|
+
[units.nodePosition, nodePosition.texture],
|
|
3399
|
+
[units.nodeStyle, nodeStyle.texture],
|
|
3400
|
+
[units.nodePaint, nodePaint.texture],
|
|
3401
|
+
[units.nodeState, nodeState.texture],
|
|
3402
|
+
[units.edgeTopology, edgeTopology.texture],
|
|
3403
|
+
[units.edgeStyle, edgeStyle.texture],
|
|
3404
|
+
[units.edgeState, edgeState.texture],
|
|
3405
|
+
[units.atlas, atlasTexture]
|
|
3406
|
+
];
|
|
3407
|
+
for (const [unit, texture] of textures) {
|
|
3408
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
3409
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
// src/host/events.ts
|
|
3414
|
+
var Emitter = class {
|
|
3415
|
+
#handlers = /* @__PURE__ */ new Map();
|
|
3416
|
+
on(name, handler) {
|
|
3417
|
+
let set = this.#handlers.get(name);
|
|
3418
|
+
if (!set) {
|
|
3419
|
+
set = /* @__PURE__ */ new Set();
|
|
3420
|
+
this.#handlers.set(name, set);
|
|
3421
|
+
}
|
|
3422
|
+
const entry = handler;
|
|
3423
|
+
set.add(entry);
|
|
3424
|
+
return () => {
|
|
3425
|
+
set.delete(entry);
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
has(name) {
|
|
3429
|
+
return (this.#handlers.get(name)?.size ?? 0) > 0;
|
|
3430
|
+
}
|
|
3431
|
+
emit(name, event) {
|
|
3432
|
+
const set = this.#handlers.get(name);
|
|
3433
|
+
if (!set) return;
|
|
3434
|
+
for (const handler of [...set]) handler(event);
|
|
3435
|
+
}
|
|
3436
|
+
clear() {
|
|
3437
|
+
this.#handlers.clear();
|
|
3438
|
+
}
|
|
3439
|
+
};
|
|
3440
|
+
// src/host/state-registry.ts
|
|
3441
|
+
const highlightBits = 3;
|
|
3442
|
+
const kinds = ["nodes", "edges"];
|
|
3443
|
+
function members() {
|
|
3444
|
+
return {
|
|
3445
|
+
nodes: /* @__PURE__ */ new Set(),
|
|
3446
|
+
edges: /* @__PURE__ */ new Set()
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
var StateRegistry = class {
|
|
3450
|
+
#model;
|
|
3451
|
+
#buffers;
|
|
3452
|
+
#members = /* @__PURE__ */ new Map();
|
|
3453
|
+
#names = builtInStates;
|
|
3454
|
+
#highlightDirty = true;
|
|
3455
|
+
constructor(model, buffers) {
|
|
3456
|
+
this.#model = model;
|
|
3457
|
+
this.#buffers = buffers;
|
|
3458
|
+
for (const name of builtInStates) this.#members.set(name, members());
|
|
3459
|
+
}
|
|
3460
|
+
get names() {
|
|
3461
|
+
return this.#names;
|
|
3462
|
+
}
|
|
3463
|
+
define(free) {
|
|
3464
|
+
const names = [...builtInStates, ...free];
|
|
3465
|
+
if (names.length === this.#names.length && names.every((name, i) => name === this.#names[i])) return false;
|
|
3466
|
+
for (const name of this.#members.keys()) if (!names.includes(name)) this.#members.delete(name);
|
|
3467
|
+
for (const name of names) if (!this.#members.has(name)) this.#members.set(name, members());
|
|
3468
|
+
this.#names = names;
|
|
3469
|
+
this.#rebuild();
|
|
3470
|
+
return true;
|
|
3471
|
+
}
|
|
3472
|
+
bit(name) {
|
|
3473
|
+
return this.#names.indexOf(name);
|
|
3474
|
+
}
|
|
3475
|
+
members(name) {
|
|
3476
|
+
const entry = this.#entry(name);
|
|
3477
|
+
return {
|
|
3478
|
+
nodes: [...entry.nodes],
|
|
3479
|
+
edges: [...entry.edges]
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
has(name, kind, key) {
|
|
3483
|
+
return this.#members.get(name)?.[kind].has(key) ?? false;
|
|
3484
|
+
}
|
|
3485
|
+
set(name, input) {
|
|
3486
|
+
const entry = this.#entry(name);
|
|
3487
|
+
let changed = false;
|
|
3488
|
+
for (const kind of kinds) {
|
|
3489
|
+
const next = new Set(input[kind] ?? []);
|
|
3490
|
+
for (const key of [...entry[kind]]) if (!next.has(key)) changed = this.#toggle(name, kind, key, false) || changed;
|
|
3491
|
+
for (const key of next) changed = this.#toggle(name, kind, key, true) || changed;
|
|
3492
|
+
}
|
|
3493
|
+
return changed;
|
|
3494
|
+
}
|
|
3495
|
+
add(name, input) {
|
|
3496
|
+
this.#entry(name);
|
|
3497
|
+
let changed = false;
|
|
3498
|
+
for (const kind of kinds) for (const key of input[kind] ?? []) changed = this.#toggle(name, kind, key, true) || changed;
|
|
3499
|
+
return changed;
|
|
3500
|
+
}
|
|
3501
|
+
remove(name, input) {
|
|
3502
|
+
this.#entry(name);
|
|
3503
|
+
let changed = false;
|
|
3504
|
+
for (const kind of kinds) for (const key of input[kind] ?? []) changed = this.#toggle(name, kind, key, false) || changed;
|
|
3505
|
+
return changed;
|
|
3506
|
+
}
|
|
3507
|
+
clear(name) {
|
|
3508
|
+
return this.set(name, {});
|
|
3509
|
+
}
|
|
3510
|
+
apply(changes) {
|
|
3511
|
+
let selection = false;
|
|
3512
|
+
let hovered = false;
|
|
3513
|
+
const forget = (kind, removed) => {
|
|
3514
|
+
for (const { key, index } of removed) {
|
|
3515
|
+
this.#setMask(kind, index, 0);
|
|
3516
|
+
for (const name of builtInStates) {
|
|
3517
|
+
if (!this.#members.get(name)?.[kind].delete(key)) continue;
|
|
3518
|
+
if (name === "selected") selection = true;
|
|
3519
|
+
if (name === "hovered") hovered = true;
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
};
|
|
3523
|
+
const learn = (kind, added) => {
|
|
3524
|
+
for (const { key, index } of added) this.#setMask(kind, index, this.#maskOf(kind, key));
|
|
3525
|
+
};
|
|
3526
|
+
forget("nodes", changes.nodes.removed);
|
|
3527
|
+
forget("edges", changes.edges.removed);
|
|
3528
|
+
learn("nodes", changes.nodes.added);
|
|
3529
|
+
learn("edges", changes.edges.added);
|
|
3530
|
+
return {
|
|
3531
|
+
selection,
|
|
3532
|
+
hovered
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3535
|
+
takeHighlighted() {
|
|
3536
|
+
if (!this.#highlightDirty) return;
|
|
3537
|
+
this.#highlightDirty = false;
|
|
3538
|
+
const indices = (kind) => {
|
|
3539
|
+
const found = /* @__PURE__ */ new Set();
|
|
3540
|
+
for (const name of ["selected", "hovered"]) for (const key of this.#members.get(name)?.[kind] ?? []) {
|
|
3541
|
+
const index = this.#index(kind, key);
|
|
3542
|
+
if (index >= 0) found.add(index);
|
|
3543
|
+
}
|
|
3544
|
+
return [...found];
|
|
3545
|
+
};
|
|
3546
|
+
this.#buffers.setHighlighted(indices("nodes"), indices("edges"));
|
|
3547
|
+
}
|
|
3548
|
+
#entry(name) {
|
|
3549
|
+
const entry = this.#members.get(name);
|
|
3550
|
+
if (!entry) throw new Error(`graph: unknown state "${name}"`);
|
|
3551
|
+
return entry;
|
|
3552
|
+
}
|
|
3553
|
+
#toggle(name, kind, key, on) {
|
|
3554
|
+
const set = this.#entry(name)[kind];
|
|
3555
|
+
if (set.has(key) === on) return false;
|
|
3556
|
+
if (on) set.add(key);
|
|
3557
|
+
else set.delete(key);
|
|
3558
|
+
const index = this.#index(kind, key);
|
|
3559
|
+
if (index >= 0) this.#setMask(kind, index, this.#maskOf(kind, key));
|
|
3560
|
+
return true;
|
|
3561
|
+
}
|
|
3562
|
+
#index(kind, key) {
|
|
3563
|
+
return (kind === "nodes" ? this.#model.node(key) : this.#model.edge(key))?.index ?? -1;
|
|
3564
|
+
}
|
|
3565
|
+
#maskOf(kind, key) {
|
|
3566
|
+
let mask = 0;
|
|
3567
|
+
this.#names.forEach((name, bit) => {
|
|
3568
|
+
if (this.#members.get(name)?.[kind].has(key)) mask |= 1 << bit;
|
|
3569
|
+
});
|
|
3570
|
+
return mask >>> 0;
|
|
3571
|
+
}
|
|
3572
|
+
#setMask(kind, index, mask) {
|
|
3573
|
+
if (((((kind === "nodes" ? this.#buffers.nodeMask : this.#buffers.edgeMask)[index] ?? 0) ^ mask) & highlightBits) !== 0) this.#highlightDirty = true;
|
|
3574
|
+
if (kind === "nodes") this.#buffers.setNode(index, mask);
|
|
3575
|
+
else this.#buffers.setEdge(index, mask);
|
|
3576
|
+
}
|
|
3577
|
+
#rebuild() {
|
|
3578
|
+
for (const kind of kinds) {
|
|
3579
|
+
const records = kind === "nodes" ? this.#model.nodes() : this.#model.edges();
|
|
3580
|
+
for (const record of records) this.#setMask(kind, record.index, this.#maskOf(kind, record.key));
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
};
|
|
3584
|
+
// src/host/mount-graph.ts
|
|
3585
|
+
const defaultPadding = 32;
|
|
3586
|
+
const clickSlop = 4;
|
|
3587
|
+
const pickTolerance = 4;
|
|
3588
|
+
const highlightOrder = 2 ** 40;
|
|
3589
|
+
const layerOrder = 2 ** 26;
|
|
3590
|
+
const kindOf = {
|
|
3591
|
+
node: "nodes",
|
|
3592
|
+
edge: "edges"
|
|
3593
|
+
};
|
|
3594
|
+
const noKeys = Object.freeze({
|
|
3595
|
+
nodes: [],
|
|
3596
|
+
edges: []
|
|
3597
|
+
});
|
|
3598
|
+
let reducedMotionQuery;
|
|
3599
|
+
function reducedMotion() {
|
|
3600
|
+
reducedMotionQuery ??= window.matchMedia?.("(prefers-reduced-motion: reduce)");
|
|
3601
|
+
return reducedMotionQuery?.matches ?? false;
|
|
3602
|
+
}
|
|
3603
|
+
function toKeys(input) {
|
|
3604
|
+
return {
|
|
3605
|
+
nodes: [...input.nodes ?? []],
|
|
3606
|
+
edges: [...input.edges ?? []]
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
function sameList(a, b) {
|
|
3610
|
+
if (a.length !== b.length) return false;
|
|
3611
|
+
const set = new Set(a);
|
|
3612
|
+
return b.every((key) => set.has(key));
|
|
3613
|
+
}
|
|
3614
|
+
function sameKeys(a, b) {
|
|
3615
|
+
return sameList(a.nodes, b.nodes) && sameList(a.edges, b.edges);
|
|
3616
|
+
}
|
|
3617
|
+
function sameTarget(a, b) {
|
|
3618
|
+
return a === b || a !== null && b !== null && a.kind === b.kind && a.key === b.key;
|
|
3619
|
+
}
|
|
3620
|
+
function resolveStyle(style, id) {
|
|
3621
|
+
return {
|
|
3622
|
+
...style.opacity !== void 0 ? { opacity: style.opacity } : {},
|
|
3623
|
+
...style.strokeWidth !== void 0 ? { strokeWidth: style.strokeWidth } : {},
|
|
3624
|
+
...style.halo !== void 0 ? { halo: style.halo } : {},
|
|
3625
|
+
...style.label !== void 0 ? { label: style.label } : {},
|
|
3626
|
+
...style.fill !== void 0 ? { fill: id(style.fill) } : {},
|
|
3627
|
+
...style.stroke !== void 0 ? { stroke: id(style.stroke) } : {}
|
|
3628
|
+
};
|
|
3629
|
+
}
|
|
3630
|
+
function createGraphHost(container, options, createRenderer) {
|
|
3631
|
+
let definition = options.definition;
|
|
3632
|
+
const controlled = options.selection !== void 0;
|
|
3633
|
+
let onSelectionChange = options.onSelectionChange;
|
|
3634
|
+
let warnedEmpty = false;
|
|
3635
|
+
const element = document.createElement("div");
|
|
3636
|
+
element.setAttribute("data-graph", "");
|
|
3637
|
+
element.setAttribute("role", "img");
|
|
3638
|
+
element.setAttribute("aria-label", options.ariaLabel);
|
|
3639
|
+
element.style.cssText = "position:relative;width:100%;height:100%;overflow:hidden";
|
|
3640
|
+
const canvas = document.createElement("canvas");
|
|
3641
|
+
canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;outline:none";
|
|
3642
|
+
element.append(canvas);
|
|
3643
|
+
container.append(element);
|
|
3644
|
+
let frameRequest = 0;
|
|
3645
|
+
let laterRequest;
|
|
3646
|
+
let needsFlush = false;
|
|
3647
|
+
let full = true;
|
|
3648
|
+
let destroyed = false;
|
|
3649
|
+
let contextLost = false;
|
|
3650
|
+
let positionsVersion = -1;
|
|
3651
|
+
let paletteCount = 0;
|
|
3652
|
+
let paletteColors = /* @__PURE__ */ new Uint8Array(1024);
|
|
3653
|
+
let viewAnimation = null;
|
|
3654
|
+
let positionAnimations = [];
|
|
3655
|
+
let animationOwner = /* @__PURE__ */ new Uint32Array(0);
|
|
3656
|
+
let animationSerial = 0;
|
|
3657
|
+
let pendingFit = options.initialView ? null : { padding: defaultPadding };
|
|
3658
|
+
let pixelRatio = window.devicePixelRatio || 1;
|
|
3659
|
+
let sceneVersion = 0;
|
|
3660
|
+
let pickedScene = -1;
|
|
3661
|
+
let pickedPositions = -1;
|
|
3662
|
+
let gesture = null;
|
|
3663
|
+
const model = createGraphModel({ schedule: () => request(true) });
|
|
3664
|
+
const scene = createScene();
|
|
3665
|
+
const positions = new Positions();
|
|
3666
|
+
const buffers = new StateBuffers();
|
|
3667
|
+
const registry = new StateRegistry(model, buffers);
|
|
3668
|
+
const emitter = new Emitter();
|
|
3669
|
+
const pickIndex = new PickIndex();
|
|
3670
|
+
const themeResolver = new ThemeResolver(element);
|
|
3671
|
+
let renderer = createRenderer(canvas);
|
|
3672
|
+
registry.define(freeStateNames(definition.states));
|
|
3673
|
+
if (options.selection !== void 0) registry.set("selected", options.selection);
|
|
3674
|
+
const store = createStore({
|
|
3675
|
+
view: options.initialView ?? {
|
|
3676
|
+
x: 0,
|
|
3677
|
+
y: 0,
|
|
3678
|
+
zoom: 1
|
|
3679
|
+
},
|
|
3680
|
+
width: 0,
|
|
3681
|
+
height: 0,
|
|
3682
|
+
hovered: null,
|
|
3683
|
+
selection: controlled ? registry.members("selected") : noKeys,
|
|
3684
|
+
states: freeStates(),
|
|
3685
|
+
positionsVersion: 0
|
|
3686
|
+
});
|
|
3687
|
+
const readonlyStore = createStore(() => store.state);
|
|
3688
|
+
const layerPositions = {
|
|
3689
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
3690
|
+
edges: /* @__PURE__ */ new Map()
|
|
3691
|
+
};
|
|
3692
|
+
const pickSource = {
|
|
3693
|
+
nodes: scene.nodes,
|
|
3694
|
+
edges: scene.edges,
|
|
3695
|
+
positions,
|
|
3696
|
+
order: (kind, index) => {
|
|
3697
|
+
const slot = (kind === "nodes" ? scene.nodes.slot[index] : scene.edges.slot[index]) ?? 0;
|
|
3698
|
+
const mask = (kind === "nodes" ? buffers.nodeMask[index] : buffers.edgeMask[index]) ?? 0;
|
|
3699
|
+
const layer = layerPositions[kind].get(slot) ?? 0;
|
|
3700
|
+
return ((mask & 3) !== 0 ? highlightOrder : 0) + layer * layerOrder + index;
|
|
3701
|
+
}
|
|
3702
|
+
};
|
|
3703
|
+
function freeStates() {
|
|
3704
|
+
return Object.fromEntries(freeStateNames(definition.states).map((name) => [name, registry.members(name)]));
|
|
3705
|
+
}
|
|
3706
|
+
function patch(next) {
|
|
3707
|
+
store.setState((state) => ({
|
|
3708
|
+
...state,
|
|
3709
|
+
...next
|
|
3710
|
+
}));
|
|
3711
|
+
}
|
|
3712
|
+
function request(flush = false) {
|
|
3713
|
+
if (flush) needsFlush = true;
|
|
3714
|
+
if (frameRequest !== 0 || destroyed) return;
|
|
3715
|
+
frameRequest = requestAnimationFrame(frame);
|
|
3716
|
+
}
|
|
3717
|
+
function requestLater(delay) {
|
|
3718
|
+
if (laterRequest !== void 0 || destroyed) return;
|
|
3719
|
+
laterRequest = setTimeout(() => {
|
|
3720
|
+
laterRequest = void 0;
|
|
3721
|
+
request();
|
|
3722
|
+
}, delay);
|
|
3723
|
+
}
|
|
3724
|
+
function commitView(view) {
|
|
3725
|
+
patch({ view });
|
|
3726
|
+
emitter.emit("viewchange", { view });
|
|
3727
|
+
request();
|
|
3728
|
+
}
|
|
3729
|
+
function viewport() {
|
|
3730
|
+
const { width, height } = store.get();
|
|
3731
|
+
return {
|
|
3732
|
+
width,
|
|
3733
|
+
height
|
|
3734
|
+
};
|
|
3735
|
+
}
|
|
3736
|
+
function resolvePalette(from) {
|
|
3737
|
+
const colors = scene.palette.colors;
|
|
3738
|
+
if (colors.length * 4 > paletteColors.length) {
|
|
3739
|
+
const next = new Uint8Array(Math.max(colors.length * 4, paletteColors.length * 2));
|
|
3740
|
+
next.set(paletteColors);
|
|
3741
|
+
paletteColors = next;
|
|
3742
|
+
}
|
|
3743
|
+
themeResolver.palette(colors, paletteColors, from);
|
|
3744
|
+
paletteCount = colors.length;
|
|
3745
|
+
renderer.setPalette(paletteColors, paletteCount);
|
|
3746
|
+
}
|
|
3747
|
+
function refreshTheme() {
|
|
3748
|
+
renderer.setTheme(themeResolver.theme());
|
|
3749
|
+
resolvePalette(0);
|
|
3750
|
+
request();
|
|
3751
|
+
}
|
|
3752
|
+
function applyStyles() {
|
|
3753
|
+
const id = (color) => scene.palette.id(color);
|
|
3754
|
+
for (const kind of ["nodes", "edges"]) {
|
|
3755
|
+
registry.names.forEach((name, bit) => {
|
|
3756
|
+
const own = definition.states[name];
|
|
3757
|
+
const style = isBuiltInState(name) ? {
|
|
3758
|
+
...builtInStyles[kind][name],
|
|
3759
|
+
...own
|
|
3760
|
+
} : own;
|
|
3761
|
+
buffers.setStyle(kind, bit, style ? resolveStyle(style, id) : null);
|
|
3762
|
+
});
|
|
3763
|
+
for (let bit = registry.names.length; bit < 32; bit++) buffers.setStyle(kind, bit, null);
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
function applyLayers() {
|
|
3767
|
+
layerPositions.nodes.clear();
|
|
3768
|
+
layerPositions.edges.clear();
|
|
3769
|
+
scene.layers.forEach((layer, position) => {
|
|
3770
|
+
layerPositions[layer.kind].set(layer.slot, position);
|
|
3771
|
+
});
|
|
3772
|
+
}
|
|
3773
|
+
function bounds(keys) {
|
|
3774
|
+
const nodes = scene.nodes;
|
|
3775
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
3776
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
3777
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
3778
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
3779
|
+
const include = (index) => {
|
|
3780
|
+
if (nodes.alive[index] !== 1) return;
|
|
3781
|
+
const radius = nodes.screenUnits[index] === 1 ? 0 : (nodes.size[index] ?? 0) / 2;
|
|
3782
|
+
minX = Math.min(minX, positions.x(index) - radius);
|
|
3783
|
+
minY = Math.min(minY, positions.y(index) - radius);
|
|
3784
|
+
maxX = Math.max(maxX, positions.x(index) + radius);
|
|
3785
|
+
maxY = Math.max(maxY, positions.y(index) + radius);
|
|
3786
|
+
};
|
|
3787
|
+
if (keys) for (const key of keys) {
|
|
3788
|
+
const record = model.node(key);
|
|
3789
|
+
if (record) include(record.index);
|
|
3790
|
+
}
|
|
3791
|
+
else for (let index = 0; index < nodes.capacity; index++) include(index);
|
|
3792
|
+
return maxX < minX ? null : {
|
|
3793
|
+
minX,
|
|
3794
|
+
minY,
|
|
3795
|
+
maxX,
|
|
3796
|
+
maxY
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
function animateTo(target, duration = 0) {
|
|
3800
|
+
const ms = reducedMotion() ? 0 : duration;
|
|
3801
|
+
if (ms <= 0) {
|
|
3802
|
+
viewAnimation = null;
|
|
3803
|
+
commitView(target);
|
|
3804
|
+
return;
|
|
3805
|
+
}
|
|
3806
|
+
viewAnimation = {
|
|
3807
|
+
from: store.get().view,
|
|
3808
|
+
to: target,
|
|
3809
|
+
start: performance.now(),
|
|
3810
|
+
duration: ms
|
|
3811
|
+
};
|
|
3812
|
+
request();
|
|
3813
|
+
}
|
|
3814
|
+
function fitTo(keys, fitOptions) {
|
|
3815
|
+
const box = bounds(keys);
|
|
3816
|
+
const size = viewport();
|
|
3817
|
+
if (!box || size.width === 0 || size.height === 0) return false;
|
|
3818
|
+
animateTo(fitBounds(box, size, fitOptions.padding ?? defaultPadding), fitOptions.duration);
|
|
3819
|
+
return true;
|
|
3820
|
+
}
|
|
3821
|
+
function target(kind, key) {
|
|
3822
|
+
const record = kind === "node" ? model.node(key) : model.edge(key);
|
|
3823
|
+
return record ? {
|
|
3824
|
+
kind,
|
|
3825
|
+
key,
|
|
3826
|
+
datum: record.datum
|
|
3827
|
+
} : null;
|
|
3828
|
+
}
|
|
3829
|
+
function pickAt(point) {
|
|
3830
|
+
if (!gesture?.drag && (pickedScene !== sceneVersion || pickedPositions !== positions.version)) {
|
|
3831
|
+
pickIndex.build(pickSource);
|
|
3832
|
+
pickedScene = sceneVersion;
|
|
3833
|
+
pickedPositions = positions.version;
|
|
3834
|
+
}
|
|
3835
|
+
const world = toWorld(store.get().view, viewport(), point);
|
|
3836
|
+
const hit = pickIndex.pick(pickSource, world.x, world.y, store.get().view.zoom, pickTolerance);
|
|
3837
|
+
if (!hit) return null;
|
|
3838
|
+
const key = hit.kind === "node" ? scene.nodes.keys[hit.index] : scene.edges.keys[hit.index];
|
|
3839
|
+
return key === void 0 ? null : target(hit.kind, key);
|
|
3840
|
+
}
|
|
3841
|
+
function publishSelection() {
|
|
3842
|
+
const selection = registry.members("selected");
|
|
3843
|
+
patch({ selection });
|
|
3844
|
+
emitter.emit("selectionchange", selection);
|
|
3845
|
+
request();
|
|
3846
|
+
}
|
|
3847
|
+
function requestSelection(next) {
|
|
3848
|
+
if (controlled) {
|
|
3849
|
+
if (!sameKeys(registry.members("selected"), next)) onSelectionChange?.(next);
|
|
3850
|
+
} else if (registry.set("selected", next)) publishSelection();
|
|
3851
|
+
}
|
|
3852
|
+
function publishState(name) {
|
|
3853
|
+
patch({ states: {
|
|
3854
|
+
...store.get().states,
|
|
3855
|
+
[name]: registry.members(name)
|
|
3856
|
+
} });
|
|
3857
|
+
request();
|
|
3858
|
+
}
|
|
3859
|
+
function setHovered(next, event) {
|
|
3860
|
+
const current = store.get().hovered;
|
|
3861
|
+
if (sameTarget(current, next)) return;
|
|
3862
|
+
registry.set("hovered", next ? { [kindOf[next.kind]]: [next.key] } : {});
|
|
3863
|
+
patch({ hovered: next });
|
|
3864
|
+
if (current && event) emitter.emit("pointerleave", {
|
|
3865
|
+
target: current,
|
|
3866
|
+
originalEvent: event
|
|
3867
|
+
});
|
|
3868
|
+
if (next && event) emitter.emit("pointerenter", {
|
|
3869
|
+
target: next,
|
|
3870
|
+
originalEvent: event
|
|
3871
|
+
});
|
|
3872
|
+
updateCursor();
|
|
3873
|
+
request();
|
|
3874
|
+
}
|
|
3875
|
+
function updateCursor() {
|
|
3876
|
+
const interactive = definition.interaction.select !== false || definition.interaction.drag;
|
|
3877
|
+
if (gesture?.moved) canvas.style.cursor = "grabbing";
|
|
3878
|
+
else if (store.get().hovered && interactive) canvas.style.cursor = "pointer";
|
|
3879
|
+
else canvas.style.cursor = "";
|
|
3880
|
+
}
|
|
3881
|
+
function selectOnClick(clicked, event) {
|
|
3882
|
+
const mode = definition.interaction.select;
|
|
3883
|
+
if (mode === false) return;
|
|
3884
|
+
const additive = mode === "multiple" && (event.shiftKey || event.metaKey || event.ctrlKey);
|
|
3885
|
+
if (!clicked) {
|
|
3886
|
+
if (!additive) requestSelection(noKeys);
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
const kind = kindOf[clicked.kind];
|
|
3890
|
+
const current = registry.members("selected");
|
|
3891
|
+
if (!additive) requestSelection({
|
|
3892
|
+
...noKeys,
|
|
3893
|
+
[kind]: [clicked.key]
|
|
3894
|
+
});
|
|
3895
|
+
else if (registry.has("selected", kind, clicked.key)) requestSelection({
|
|
3896
|
+
...current,
|
|
3897
|
+
[kind]: current[kind].filter((key) => key !== clicked.key)
|
|
3898
|
+
});
|
|
3899
|
+
else requestSelection({
|
|
3900
|
+
...current,
|
|
3901
|
+
[kind]: [...current[kind], clicked.key]
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
function applyChanges(changes) {
|
|
3905
|
+
scene.apply(model, changes);
|
|
3906
|
+
followPositions(placeNodes(model, positions, changes));
|
|
3907
|
+
buffers.reserve(scene.nodes.capacity, scene.edges.capacity);
|
|
3908
|
+
const result = registry.apply(changes);
|
|
3909
|
+
sceneVersion++;
|
|
3910
|
+
applyLayers();
|
|
3911
|
+
if (result.selection) {
|
|
3912
|
+
publishSelection();
|
|
3913
|
+
if (controlled) onSelectionChange?.(registry.members("selected"));
|
|
3914
|
+
}
|
|
3915
|
+
const hovered = store.get().hovered;
|
|
3916
|
+
if (result.hovered) {
|
|
3917
|
+
patch({ hovered: null });
|
|
3918
|
+
updateCursor();
|
|
3919
|
+
} else if (hovered) {
|
|
3920
|
+
const fresh = target(hovered.kind, hovered.key);
|
|
3921
|
+
if (fresh && fresh.datum !== hovered.datum) patch({ hovered: fresh });
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
function movePositions(indices, to, duration) {
|
|
3925
|
+
if (indices.length === 0) return;
|
|
3926
|
+
const ms = reducedMotion() ? 0 : duration;
|
|
3927
|
+
if (ms <= 0) indices.forEach((index, i) => {
|
|
3928
|
+
if (index < animationOwner.length) animationOwner[index] = 0;
|
|
3929
|
+
positions.set(index, to[i * 2] ?? 0, to[i * 2 + 1] ?? 0);
|
|
3930
|
+
});
|
|
3931
|
+
else {
|
|
3932
|
+
if (positions.capacity > animationOwner.length) {
|
|
3933
|
+
const owner = new Uint32Array(positions.capacity);
|
|
3934
|
+
owner.set(animationOwner);
|
|
3935
|
+
animationOwner = owner;
|
|
3936
|
+
}
|
|
3937
|
+
const id = ++animationSerial;
|
|
3938
|
+
const from = new Float32Array(indices.length * 2);
|
|
3939
|
+
indices.forEach((index, i) => {
|
|
3940
|
+
animationOwner[index] = id;
|
|
3941
|
+
from[i * 2] = positions.x(index);
|
|
3942
|
+
from[i * 2 + 1] = positions.y(index);
|
|
3943
|
+
});
|
|
3944
|
+
positionAnimations.push({
|
|
3945
|
+
id,
|
|
3946
|
+
indices,
|
|
3947
|
+
from,
|
|
3948
|
+
to: Float32Array.from(to),
|
|
3949
|
+
start: performance.now(),
|
|
3950
|
+
duration: ms
|
|
3951
|
+
});
|
|
3952
|
+
}
|
|
3953
|
+
request();
|
|
3954
|
+
}
|
|
3955
|
+
function followPositions(moves) {
|
|
3956
|
+
if (moves.indices.length === 0) return;
|
|
3957
|
+
const dragged = new Set(gesture?.drag?.indices);
|
|
3958
|
+
const indices = [];
|
|
3959
|
+
const to = [];
|
|
3960
|
+
moves.indices.forEach((index, i) => {
|
|
3961
|
+
if (dragged.has(index)) return;
|
|
3962
|
+
indices.push(index);
|
|
3963
|
+
to.push(moves.to[i * 2] ?? 0, moves.to[i * 2 + 1] ?? 0);
|
|
3964
|
+
});
|
|
3965
|
+
movePositions(indices, to, definition.transitions.positions);
|
|
3966
|
+
}
|
|
3967
|
+
function stepPositions(now) {
|
|
3968
|
+
positionAnimations = positionAnimations.filter((animation) => {
|
|
3969
|
+
const t = Math.max(0, Math.min(1, (now - animation.start) / animation.duration));
|
|
3970
|
+
const k = easeInOut(t);
|
|
3971
|
+
animation.indices.forEach((index, i) => {
|
|
3972
|
+
if (animationOwner[index] !== animation.id) return;
|
|
3973
|
+
const fromX = animation.from[i * 2] ?? 0;
|
|
3974
|
+
const fromY = animation.from[i * 2 + 1] ?? 0;
|
|
3975
|
+
const toX = animation.to[i * 2] ?? 0;
|
|
3976
|
+
const toY = animation.to[i * 2 + 1] ?? 0;
|
|
3977
|
+
positions.set(index, fromX + (toX - fromX) * k, fromY + (toY - fromY) * k);
|
|
3978
|
+
});
|
|
3979
|
+
return t < 1;
|
|
3980
|
+
});
|
|
3981
|
+
}
|
|
3982
|
+
function frame(now) {
|
|
3983
|
+
frameRequest = 0;
|
|
3984
|
+
if (destroyed || contextLost) return;
|
|
3985
|
+
if (needsFlush) {
|
|
3986
|
+
needsFlush = false;
|
|
3987
|
+
applyChanges(model.flush());
|
|
3988
|
+
}
|
|
3989
|
+
if (pendingFit && fitTo(void 0, pendingFit)) pendingFit = null;
|
|
3990
|
+
if (viewAnimation) {
|
|
3991
|
+
const t = Math.min(1, (now - viewAnimation.start) / viewAnimation.duration);
|
|
3992
|
+
const view = interpolateView(viewAnimation.from, viewAnimation.to, easeInOut(t));
|
|
3993
|
+
if (t >= 1) viewAnimation = null;
|
|
3994
|
+
patch({ view });
|
|
3995
|
+
emitter.emit("viewchange", { view });
|
|
3996
|
+
}
|
|
3997
|
+
if (positionAnimations.length > 0) stepPositions(now);
|
|
3998
|
+
registry.takeHighlighted();
|
|
3999
|
+
const dirty = scene.takeDirty();
|
|
4000
|
+
if (dirty.palette || scene.palette.colors.length !== paletteCount) resolvePalette(paletteCount);
|
|
4001
|
+
buffers.reserve(scene.nodes.capacity, scene.edges.capacity);
|
|
4002
|
+
const positionsChanged = positions.version !== positionsVersion;
|
|
4003
|
+
renderer.sync({
|
|
4004
|
+
scene,
|
|
4005
|
+
dirty,
|
|
4006
|
+
positions,
|
|
4007
|
+
positionsChanged,
|
|
4008
|
+
states: buffers,
|
|
4009
|
+
full
|
|
4010
|
+
});
|
|
4011
|
+
full = false;
|
|
4012
|
+
if (positionsChanged) {
|
|
4013
|
+
positionsVersion = positions.version;
|
|
4014
|
+
patch({ positionsVersion });
|
|
4015
|
+
}
|
|
4016
|
+
const { view, width, height } = store.get();
|
|
4017
|
+
const wait = renderer.render({
|
|
4018
|
+
view,
|
|
4019
|
+
width,
|
|
4020
|
+
height,
|
|
4021
|
+
pixelRatio
|
|
4022
|
+
});
|
|
4023
|
+
if (viewAnimation || positionAnimations.length > 0) request();
|
|
4024
|
+
else if (wait >= 0) requestLater(wait);
|
|
4025
|
+
}
|
|
4026
|
+
function resize(width, height) {
|
|
4027
|
+
pixelRatio = window.devicePixelRatio || 1;
|
|
4028
|
+
canvas.width = Math.max(1, Math.round(width * pixelRatio));
|
|
4029
|
+
canvas.height = Math.max(1, Math.round(height * pixelRatio));
|
|
4030
|
+
if (dev && !warnedEmpty && (width === 0 || height === 0)) {
|
|
4031
|
+
warnedEmpty = true;
|
|
4032
|
+
console.warn("graph: the container measures 0 px; give it a width and a height");
|
|
4033
|
+
}
|
|
4034
|
+
patch({
|
|
4035
|
+
width,
|
|
4036
|
+
height
|
|
4037
|
+
});
|
|
4038
|
+
request();
|
|
4039
|
+
}
|
|
4040
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
4041
|
+
const entry = entries[entries.length - 1];
|
|
4042
|
+
if (entry) resize(entry.contentRect.width, entry.contentRect.height);
|
|
4043
|
+
});
|
|
4044
|
+
resizeObserver.observe(element);
|
|
4045
|
+
const stopTheme = observeTheme(element, refreshTheme);
|
|
4046
|
+
function local(event) {
|
|
4047
|
+
const rect = canvas.getBoundingClientRect();
|
|
4048
|
+
return {
|
|
4049
|
+
x: event.clientX - rect.left,
|
|
4050
|
+
y: event.clientY - rect.top
|
|
4051
|
+
};
|
|
4052
|
+
}
|
|
4053
|
+
function pointerEvent(event, hit) {
|
|
4054
|
+
const point = local(event);
|
|
4055
|
+
return {
|
|
4056
|
+
target: hit,
|
|
4057
|
+
point,
|
|
4058
|
+
world: toWorld(store.get().view, viewport(), point),
|
|
4059
|
+
originalEvent: event
|
|
4060
|
+
};
|
|
4061
|
+
}
|
|
4062
|
+
function startDrag(hit, point) {
|
|
4063
|
+
if (hit.kind !== "node" || !definition.interaction.drag) return null;
|
|
4064
|
+
const group = registry.has("selected", "nodes", hit.key) ? registry.members("selected").nodes : [hit.key];
|
|
4065
|
+
const world = toWorld(store.get().view, viewport(), point);
|
|
4066
|
+
const keys = [];
|
|
4067
|
+
const indices = [];
|
|
4068
|
+
for (const key of group) {
|
|
4069
|
+
const record = model.node(key);
|
|
4070
|
+
if (!record) continue;
|
|
4071
|
+
keys.push(key);
|
|
4072
|
+
indices.push(record.index);
|
|
4073
|
+
}
|
|
4074
|
+
const offsets = new Float32Array(indices.length * 2);
|
|
4075
|
+
indices.forEach((index, i) => {
|
|
4076
|
+
if (index < animationOwner.length) animationOwner[index] = 0;
|
|
4077
|
+
offsets[i * 2] = positions.x(index) - world.x;
|
|
4078
|
+
offsets[i * 2 + 1] = positions.y(index) - world.y;
|
|
4079
|
+
});
|
|
4080
|
+
return {
|
|
4081
|
+
target: {
|
|
4082
|
+
...hit,
|
|
4083
|
+
kind: "node"
|
|
4084
|
+
},
|
|
4085
|
+
indices,
|
|
4086
|
+
keys,
|
|
4087
|
+
offsets,
|
|
4088
|
+
started: false
|
|
4089
|
+
};
|
|
4090
|
+
}
|
|
4091
|
+
function dragEvent(drag, event) {
|
|
4092
|
+
const point = local(event);
|
|
4093
|
+
return {
|
|
4094
|
+
target: drag.target,
|
|
4095
|
+
keys: drag.keys,
|
|
4096
|
+
point,
|
|
4097
|
+
world: toWorld(store.get().view, viewport(), point),
|
|
4098
|
+
originalEvent: event
|
|
4099
|
+
};
|
|
4100
|
+
}
|
|
4101
|
+
function onPointerDown(event) {
|
|
4102
|
+
if (event.button !== 0 || gesture) return;
|
|
4103
|
+
viewAnimation = null;
|
|
4104
|
+
const point = local(event);
|
|
4105
|
+
const hit = pickAt(point);
|
|
4106
|
+
const drag = hit ? startDrag(hit, point) : null;
|
|
4107
|
+
gesture = {
|
|
4108
|
+
id: event.pointerId,
|
|
4109
|
+
start: point,
|
|
4110
|
+
last: point,
|
|
4111
|
+
target: hit,
|
|
4112
|
+
drag,
|
|
4113
|
+
moved: false
|
|
4114
|
+
};
|
|
4115
|
+
canvas.setPointerCapture?.(event.pointerId);
|
|
4116
|
+
}
|
|
4117
|
+
function onPointerMove(event) {
|
|
4118
|
+
const point = local(event);
|
|
4119
|
+
if (!gesture) {
|
|
4120
|
+
setHovered(pickAt(point), event);
|
|
4121
|
+
return;
|
|
4122
|
+
}
|
|
4123
|
+
if (gesture.id !== event.pointerId) return;
|
|
4124
|
+
if (!gesture.moved && Math.hypot(point.x - gesture.start.x, point.y - gesture.start.y) > clickSlop) {
|
|
4125
|
+
gesture.moved = true;
|
|
4126
|
+
updateCursor();
|
|
4127
|
+
}
|
|
4128
|
+
const drag = gesture.drag;
|
|
4129
|
+
if (drag) {
|
|
4130
|
+
if (!gesture.moved) return;
|
|
4131
|
+
if (!drag.started) {
|
|
4132
|
+
drag.started = true;
|
|
4133
|
+
emitter.emit("dragstart", dragEvent(drag, event));
|
|
4134
|
+
}
|
|
4135
|
+
const world = toWorld(store.get().view, viewport(), point);
|
|
4136
|
+
drag.indices.forEach((index, i) => {
|
|
4137
|
+
positions.set(index, world.x + (drag.offsets[i * 2] ?? 0), world.y + (drag.offsets[i * 2 + 1] ?? 0));
|
|
4138
|
+
});
|
|
4139
|
+
emitter.emit("drag", dragEvent(drag, event));
|
|
4140
|
+
if (emitter.has("positionchange")) {
|
|
4141
|
+
const moved = /* @__PURE__ */ new Map();
|
|
4142
|
+
drag.keys.forEach((key, i) => {
|
|
4143
|
+
const index = drag.indices[i] ?? 0;
|
|
4144
|
+
moved.set(key, {
|
|
4145
|
+
x: positions.x(index),
|
|
4146
|
+
y: positions.y(index)
|
|
4147
|
+
});
|
|
4148
|
+
});
|
|
4149
|
+
emitter.emit("positionchange", {
|
|
4150
|
+
origin: "drag",
|
|
4151
|
+
positions: moved
|
|
4152
|
+
});
|
|
4153
|
+
}
|
|
4154
|
+
request();
|
|
4155
|
+
} else if (definition.interaction.pan) commitView(panBy(store.get().view, point.x - gesture.last.x, point.y - gesture.last.y));
|
|
4156
|
+
gesture.last = point;
|
|
4157
|
+
}
|
|
4158
|
+
function endGesture(event, cancelled) {
|
|
4159
|
+
if (!gesture || gesture.id !== event.pointerId) return;
|
|
4160
|
+
const ended = gesture;
|
|
4161
|
+
gesture = null;
|
|
4162
|
+
if (canvas.hasPointerCapture?.(event.pointerId)) canvas.releasePointerCapture(event.pointerId);
|
|
4163
|
+
if (ended.drag?.started) emitter.emit("dragend", dragEvent(ended.drag, event));
|
|
4164
|
+
if (!cancelled && !ended.moved) {
|
|
4165
|
+
emitter.emit("click", pointerEvent(event, ended.target));
|
|
4166
|
+
selectOnClick(ended.target, event);
|
|
4167
|
+
}
|
|
4168
|
+
if (!cancelled) setHovered(pickAt(local(event)), event);
|
|
4169
|
+
updateCursor();
|
|
4170
|
+
}
|
|
4171
|
+
function onPointerUp(event) {
|
|
4172
|
+
endGesture(event, false);
|
|
4173
|
+
}
|
|
4174
|
+
function onPointerCancel(event) {
|
|
4175
|
+
endGesture(event, true);
|
|
4176
|
+
}
|
|
4177
|
+
function onPointerLeave(event) {
|
|
4178
|
+
if (!gesture) setHovered(null, event);
|
|
4179
|
+
}
|
|
4180
|
+
function onDoubleClick(event) {
|
|
4181
|
+
emitter.emit("doubleclick", pointerEvent(event, pickAt(local(event))));
|
|
4182
|
+
}
|
|
4183
|
+
function onContextMenu(event) {
|
|
4184
|
+
emitter.emit("contextmenu", pointerEvent(event, pickAt(local(event))));
|
|
4185
|
+
}
|
|
4186
|
+
function onWheel(event) {
|
|
4187
|
+
const zoom = definition.interaction.zoom;
|
|
4188
|
+
if (!zoom) return;
|
|
4189
|
+
event.preventDefault();
|
|
4190
|
+
viewAnimation = null;
|
|
4191
|
+
const { view, height } = store.get();
|
|
4192
|
+
const factor = wheelFactor(event.deltaY, event.deltaMode, height, zoom.speed);
|
|
4193
|
+
commitView(zoomAround(view, viewport(), local(event), factor));
|
|
4194
|
+
}
|
|
4195
|
+
function onContextLost(event) {
|
|
4196
|
+
event.preventDefault();
|
|
4197
|
+
contextLost = true;
|
|
4198
|
+
}
|
|
4199
|
+
function onContextRestored() {
|
|
4200
|
+
contextLost = false;
|
|
4201
|
+
renderer = createRenderer(canvas);
|
|
4202
|
+
full = true;
|
|
4203
|
+
refreshTheme();
|
|
4204
|
+
}
|
|
4205
|
+
const listeners = [
|
|
4206
|
+
["pointerdown", onPointerDown],
|
|
4207
|
+
["pointermove", onPointerMove],
|
|
4208
|
+
["pointerup", onPointerUp],
|
|
4209
|
+
["pointercancel", onPointerCancel],
|
|
4210
|
+
["pointerleave", onPointerLeave],
|
|
4211
|
+
["dblclick", onDoubleClick],
|
|
4212
|
+
["contextmenu", onContextMenu],
|
|
4213
|
+
[
|
|
4214
|
+
"wheel",
|
|
4215
|
+
onWheel,
|
|
4216
|
+
{ passive: false }
|
|
4217
|
+
],
|
|
4218
|
+
["webglcontextlost", onContextLost],
|
|
4219
|
+
["webglcontextrestored", onContextRestored]
|
|
4220
|
+
];
|
|
4221
|
+
for (const [name, listener, listenerOptions] of listeners) canvas.addEventListener(name, listener, listenerOptions);
|
|
4222
|
+
model.setMarks(definition.marks);
|
|
4223
|
+
scene.setDefinition(definition);
|
|
4224
|
+
applyLayers();
|
|
4225
|
+
applyStyles();
|
|
4226
|
+
refreshTheme();
|
|
4227
|
+
request(true);
|
|
4228
|
+
function declared(name) {
|
|
4229
|
+
if (isBuiltInState(name) || !Object.hasOwn(definition.states, name)) throw new Error(`graph: unknown state "${name}"`);
|
|
4230
|
+
return name;
|
|
4231
|
+
}
|
|
4232
|
+
return {
|
|
4233
|
+
store: readonlyStore,
|
|
4234
|
+
view: {
|
|
4235
|
+
get: () => store.get().view,
|
|
4236
|
+
set: (next, viewOptions = {}) => animateTo(next, viewOptions.duration),
|
|
4237
|
+
fit: (keys, fitOptions = {}) => {
|
|
4238
|
+
if (!fitTo(keys, fitOptions)) pendingFit = keys ? null : fitOptions;
|
|
4239
|
+
},
|
|
4240
|
+
zoomBy: (factor, viewOptions = {}) => {
|
|
4241
|
+
const size = viewport();
|
|
4242
|
+
const center = {
|
|
4243
|
+
x: size.width / 2,
|
|
4244
|
+
y: size.height / 2
|
|
4245
|
+
};
|
|
4246
|
+
animateTo(zoomAround(store.get().view, size, center, factor), viewOptions.duration);
|
|
4247
|
+
},
|
|
4248
|
+
toScreen: (point) => toScreen(store.get().view, viewport(), point),
|
|
4249
|
+
toWorld: (point) => toWorld(store.get().view, viewport(), point)
|
|
4250
|
+
},
|
|
4251
|
+
selection: {
|
|
4252
|
+
get: () => store.get().selection,
|
|
4253
|
+
set: (keys) => requestSelection(toKeys(keys)),
|
|
4254
|
+
clear: () => requestSelection(noKeys)
|
|
4255
|
+
},
|
|
4256
|
+
states: {
|
|
4257
|
+
set: (name, keys) => {
|
|
4258
|
+
if (registry.set(declared(name), keys)) publishState(name);
|
|
4259
|
+
},
|
|
4260
|
+
add: (name, keys) => {
|
|
4261
|
+
if (registry.add(declared(name), keys)) publishState(name);
|
|
4262
|
+
},
|
|
4263
|
+
remove: (name, keys) => {
|
|
4264
|
+
if (registry.remove(declared(name), keys)) publishState(name);
|
|
4265
|
+
},
|
|
4266
|
+
clear: (name) => {
|
|
4267
|
+
const names = name === void 0 ? freeStateNames(definition.states) : [declared(name)];
|
|
4268
|
+
for (const each of names) if (registry.clear(each)) publishState(each);
|
|
4269
|
+
},
|
|
4270
|
+
has: (name, element) => registry.has(declared(name), kindOf[element.kind], element.key)
|
|
4271
|
+
},
|
|
4272
|
+
positions: {
|
|
4273
|
+
get: (key) => {
|
|
4274
|
+
const record = model.node(key);
|
|
4275
|
+
return record ? {
|
|
4276
|
+
x: positions.x(record.index),
|
|
4277
|
+
y: positions.y(record.index)
|
|
4278
|
+
} : void 0;
|
|
4279
|
+
},
|
|
4280
|
+
set: (entries, positionOptions = {}) => {
|
|
4281
|
+
const indices = [];
|
|
4282
|
+
const to = [];
|
|
4283
|
+
const moved = /* @__PURE__ */ new Map();
|
|
4284
|
+
for (const [key, point] of entries) {
|
|
4285
|
+
const record = model.node(key);
|
|
4286
|
+
if (!record) continue;
|
|
4287
|
+
indices.push(record.index);
|
|
4288
|
+
to.push(point.x, point.y);
|
|
4289
|
+
moved.set(key, {
|
|
4290
|
+
x: point.x,
|
|
4291
|
+
y: point.y
|
|
4292
|
+
});
|
|
4293
|
+
}
|
|
4294
|
+
movePositions(indices, to, positionOptions.duration ?? 0);
|
|
4295
|
+
if (moved.size > 0) emitter.emit("positionchange", {
|
|
4296
|
+
origin: "api",
|
|
4297
|
+
positions: moved
|
|
4298
|
+
});
|
|
4299
|
+
}
|
|
4300
|
+
},
|
|
4301
|
+
element,
|
|
4302
|
+
pick: (point) => pickAt(point),
|
|
4303
|
+
neighbors: (key) => model.neighbors(key),
|
|
4304
|
+
edgesOf: (key) => model.edgesOf(key),
|
|
4305
|
+
endpoints: (key) => model.endpoints(key),
|
|
4306
|
+
on: (name, handler) => emitter.on(name, handler),
|
|
4307
|
+
update(next) {
|
|
4308
|
+
if (next.ariaLabel !== void 0) element.setAttribute("aria-label", next.ariaLabel);
|
|
4309
|
+
if (next.onSelectionChange !== void 0) onSelectionChange = next.onSelectionChange;
|
|
4310
|
+
if (controlled && next.selection !== void 0 && registry.set("selected", next.selection)) publishSelection();
|
|
4311
|
+
if (next.definition && next.definition !== definition) {
|
|
4312
|
+
definition = next.definition;
|
|
4313
|
+
model.setMarks(definition.marks);
|
|
4314
|
+
scene.setDefinition(definition);
|
|
4315
|
+
applyLayers();
|
|
4316
|
+
if (registry.define(freeStateNames(definition.states))) patch({ states: freeStates() });
|
|
4317
|
+
applyStyles();
|
|
4318
|
+
updateCursor();
|
|
4319
|
+
request(true);
|
|
4320
|
+
}
|
|
4321
|
+
},
|
|
4322
|
+
destroy() {
|
|
4323
|
+
if (destroyed) return;
|
|
4324
|
+
destroyed = true;
|
|
4325
|
+
if (frameRequest !== 0) cancelAnimationFrame(frameRequest);
|
|
4326
|
+
clearTimeout(laterRequest);
|
|
4327
|
+
resizeObserver.disconnect();
|
|
4328
|
+
stopTheme();
|
|
4329
|
+
for (const [name, listener] of listeners) canvas.removeEventListener(name, listener);
|
|
4330
|
+
emitter.clear();
|
|
4331
|
+
model.destroy();
|
|
4332
|
+
renderer.destroy();
|
|
4333
|
+
themeResolver.destroy();
|
|
4334
|
+
element.remove();
|
|
4335
|
+
}
|
|
4336
|
+
};
|
|
4337
|
+
}
|
|
4338
|
+
function mountGraph(container, options) {
|
|
4339
|
+
return createGraphHost(container, options, createWebglRenderer);
|
|
4340
|
+
}
|
|
4341
|
+
export { freeStateNames as n, maxFreeStates as r, mountGraph as t };
|
|
4342
|
+
|
|
4343
|
+
//# sourceMappingURL=mount-graph-DYOgD_ql.js.map
|