@contraagent/elements 0.9.1 → 0.9.2

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.
@@ -25,7 +25,14 @@ const pairKey = (a: string, b: string) => `${a}\u0000${b}`;
25
25
  export function rankLayers(ids: readonly string[], edges: readonly (readonly [string, string])[], fixed: ReadonlyMap<string, number>): Map<string, number> {
26
26
  const known = new Set(ids);
27
27
  const all = new Map<string, string[]>(ids.map((id) => [id, []]));
28
- for (const [a, b] of edges) if (a !== b && known.has(a) && known.has(b) && !all.get(a)?.includes(b)) all.get(a)?.push(b);
28
+ const joined = new Set<string>();
29
+ for (const [a, b] of edges) {
30
+ if (a === b || !known.has(a) || !known.has(b)) continue;
31
+ const k = pairKey(a, b);
32
+ if (joined.has(k)) continue;
33
+ joined.add(k);
34
+ all.get(a)?.push(b);
35
+ }
29
36
  const back = new Set<string>();
30
37
  const seen = new Map<string, 1 | 2>();
31
38
  for (const root of ids) {
@@ -62,8 +69,11 @@ export function rankLayers(ids: readonly string[], edges: readonly (readonly [st
62
69
  const indeg = new Map(ids.map((id) => [id, pred.get(id)?.length ?? 0]));
63
70
  const queue = ids.filter((id) => !indeg.get(id));
64
71
  const layer = new Map<string, number>();
65
- for (let q = queue.shift(); q !== undefined; q = queue.shift()) {
66
- layer.set(q, fixed.get(q) ?? Math.max(0, ...(pred.get(q) ?? []).map((p) => (layer.get(p) ?? 0) + 1)));
72
+ for (let head = 0; head < queue.length; head++) {
73
+ const q = queue[head] as string;
74
+ let deepest = 0;
75
+ for (const p of pred.get(q) ?? []) deepest = Math.max(deepest, (layer.get(p) ?? 0) + 1);
76
+ layer.set(q, fixed.get(q) ?? deepest);
67
77
  for (const s of succ.get(q) ?? []) {
68
78
  indeg.set(s, (indeg.get(s) ?? 0) - 1);
69
79
  if (!indeg.get(s)) queue.push(s);
@@ -74,6 +84,7 @@ export function rankLayers(ids: readonly string[], edges: readonly (readonly [st
74
84
 
75
85
  /** Pairs (a, b) between adjacent layers that cross, for the order `pos`. */
76
86
  function crossings(rows: readonly string[][], links: readonly (readonly [string, string])[], pos: ReadonlyMap<string, number>, layerOf: ReadonlyMap<string, number>): number {
87
+ if (!rows.length) return 0;
77
88
  let n = 0;
78
89
  const by = new Map<number, [number, number][]>();
79
90
  for (const [a, b] of links) {
@@ -81,14 +92,46 @@ function crossings(rows: readonly string[][], links: readonly (readonly [string,
81
92
  if (l === undefined) continue;
82
93
  push(by, l, [pos.get(a) ?? 0, pos.get(b) ?? 0]);
83
94
  }
84
- for (const ls of by.values())
85
- for (let i = 0; i < ls.length; i++)
86
- for (let j = i + 1; j < ls.length; j++) {
87
- const [a1, b1] = ls[i] as [number, number];
88
- const [a2, b2] = ls[j] as [number, number];
89
- if ((a1 - a2) * (b1 - b2) < 0) n++;
90
- }
91
- return rows.length ? n : 0;
95
+ for (const ls of by.values()) n += crossingPairs(ls);
96
+ return n;
97
+ }
98
+
99
+ /**
100
+ * How many pairs of `ls` cross: one's ends are both before the other's, on opposite
101
+ * sides ((a1 - a2) * (b1 - b2) < 0). Sorted by `a` (then `b`), they are the pairs whose
102
+ * `b` goes down, counted with a Fenwick tree over the `b` values: O(n log n), not n².
103
+ */
104
+ export function crossingPairs(ls: readonly (readonly [number, number])[]): number {
105
+ if (ls.length < 2) return 0;
106
+ // Positions are small whole numbers; anything else is counted pair by pair.
107
+ let top = 0;
108
+ for (const [a, b] of ls) {
109
+ if (!Number.isInteger(a) || !Number.isInteger(b) || b < 0 || b > 1e7) return crossingPairsSlow(ls);
110
+ if (b > top) top = b;
111
+ }
112
+ const sorted = [...ls].sort((x, y) => x[0] - y[0] || x[1] - y[1]);
113
+ const tree = new Array<number>(top + 2).fill(0);
114
+ let n = 0;
115
+ sorted.forEach(([, b], seen) => {
116
+ // Earlier pairs (a smaller or the same) with a larger b: a crossing when a is smaller;
117
+ // a pair with the same a has a b no larger, sorted so.
118
+ let atMost = 0;
119
+ for (let i = b + 1; i > 0; i -= i & -i) atMost += tree[i] as number;
120
+ n += seen - atMost;
121
+ for (let i = b + 1; i < tree.length; i += i & -i) tree[i] = (tree[i] as number) + 1;
122
+ });
123
+ return n;
124
+ }
125
+
126
+ function crossingPairsSlow(ls: readonly (readonly [number, number])[]): number {
127
+ let n = 0;
128
+ for (let i = 0; i < ls.length; i++)
129
+ for (let j = i + 1; j < ls.length; j++) {
130
+ const [a1, b1] = ls[i] as readonly [number, number];
131
+ const [a2, b2] = ls[j] as readonly [number, number];
132
+ if ((a1 - a2) * (b1 - b2) < 0) n++;
133
+ }
134
+ return n;
92
135
  }
93
136
 
94
137
  /** True for a point where an edge crosses a layer. */
@@ -160,7 +203,31 @@ export interface GraphShape {
160
203
  stubs: readonly Pick<GraphStubInput, 'id' | 'to' | 'style'>[];
161
204
  }
162
205
 
163
- const arranged = new WeakMap<GraphShape, { prev: GraphOrder | undefined; down: boolean; out: Arrangement }[]>();
206
+ type Arranged = { prev: GraphOrder | undefined; down: boolean; out: Arrangement }[];
207
+
208
+ /** The structure each shape object was found to have (`shapeKey`). */
209
+ const keys = new WeakMap<GraphShape, string>();
210
+ /** The last few arrangements, by structure, the latest last. */
211
+ const arranged = new Map<string, Arranged>();
212
+ const KEPT = 16;
213
+
214
+ /** A value as the key writes it: null, undefined and a string kept apart. */
215
+ const tag = (v: string | null | undefined): string => (v === undefined ? 'u' : v === null ? 'n' : `s${v}`);
216
+
217
+ /** Everything `arrangeNew` reads of `g`, as one string: two shapes with the same key give the same arrangement. */
218
+ function shapeKey(g: GraphShape): string {
219
+ let k = keys.get(g);
220
+ if (k === undefined) {
221
+ k = JSON.stringify([
222
+ g.nodes.map((n) => [tag(n.id), tag(n.cluster)]),
223
+ g.clusters.map((c) => [tag(c.id), tag(c.parent), !!c.cycle]),
224
+ g.edges.map((e) => [tag(e.from), tag(e.to), tag(e.style), !!e.ranks]),
225
+ g.stubs.map((s) => [tag(s.id), tag(s.to), tag(s.style)]),
226
+ ]);
227
+ keys.set(g, k);
228
+ }
229
+ return k;
230
+ }
164
231
 
165
232
  /**
166
233
  * The order of `g`'s drawing: each container's layers (the top level, then each
@@ -168,14 +235,17 @@ const arranged = new WeakMap<GraphShape, { prev: GraphOrder | undefined; down: b
168
235
  * and the slots each edge passes. Pass a previous `order` of the same graph as
169
236
  * `prev` to keep its items where they are (new ones are appended to their layer);
170
237
  * `down` lays out the edge paths for a drawing top to bottom (a cluster's label is then on
171
- * the side its edges come in). The same `g`, `prev` and `down` give the same object.
238
+ * the side its edges come in). The same `g` (or a graph of the same ids, clusters and links),
239
+ * `prev` and `down` give the same object.
172
240
  */
173
241
  export function arrange(g: GraphShape, prev?: GraphOrder, down = false): Arrangement {
174
- const seen = arranged.get(g) ?? [];
242
+ const key = shapeKey(g);
243
+ const seen = arranged.get(key) ?? [];
244
+ arranged.delete(key);
175
245
  const hit = seen.find((x) => x.prev === prev && x.down === down);
176
- if (hit) return hit.out;
177
- const out = arrangeNew(g, prev, down);
178
- arranged.set(g, [...seen.slice(-3), { prev, down, out }]);
246
+ const out = hit?.out ?? arrangeNew(g, prev, down);
247
+ arranged.set(key, hit ? seen : [...seen.slice(-3), { prev, down, out }]);
248
+ if (arranged.size > KEPT) arranged.delete(arranged.keys().next().value as string);
179
249
  return out;
180
250
  }
181
251
 
@@ -187,16 +257,23 @@ function arrangeNew(g: GraphShape, prev: GraphOrder | undefined, down: boolean):
187
257
  const stubIds = [...new Set(g.stubs.map((s) => s.id))];
188
258
  const isStub = new Set(stubIds);
189
259
 
190
- // The clusters an id sits in, innermost first (for a cluster id, itself first).
191
- const boxPath = (id: string): string[] => {
192
- const out: string[] = [];
193
- let b: string | null = clusterIds.has(id) ? id : (boxOf.get(id) ?? null);
194
- while (b) {
195
- out.push(b);
196
- b = parentOf.get(b) ?? null;
260
+ // The clusters an id sits in, innermost first (for a cluster id, itself first); read only.
261
+ const paths = new Map<string, { list: string[]; set: Set<string> }>();
262
+ const pathOf = (id: string): { list: string[]; set: Set<string> } => {
263
+ let p = paths.get(id);
264
+ if (!p) {
265
+ const list: string[] = [];
266
+ let b: string | null = clusterIds.has(id) ? id : (boxOf.get(id) ?? null);
267
+ while (b) {
268
+ list.push(b);
269
+ b = parentOf.get(b) ?? null;
270
+ }
271
+ p = { list, set: new Set(list) };
272
+ paths.set(id, p);
197
273
  }
198
- return out;
274
+ return p;
199
275
  };
276
+ const boxPath = (id: string): readonly string[] => pathOf(id).list;
200
277
  // The direct child of `container` (a cluster, or '' for the top level) that holds a node `id`.
201
278
  const itemIn = (id: string, container: string): string | undefined => {
202
279
  const bp = boxPath(id);
@@ -207,7 +284,8 @@ function arrangeNew(g: GraphShape, prev: GraphOrder | undefined, down: boolean):
207
284
  // The deepest cluster holding both ends of an edge (the top level when none).
208
285
  const commonBox = (a: string, b: string): string => {
209
286
  const bp = boxPath(a);
210
- return bp.find((x) => boxPath(b).includes(x)) ?? '';
287
+ const other = pathOf(b).set;
288
+ return bp.find((x) => other.has(x)) ?? '';
211
289
  };
212
290
  // The container a slot is in: a stub's is the top level.
213
291
  const containerOf = (id: string): string => (isStub.has(id) ? '' : (boxOf.get(id) ?? ''));
@@ -216,21 +294,34 @@ function arrangeNew(g: GraphShape, prev: GraphOrder | undefined, down: boolean):
216
294
  // clusters nested in it). Each cluster is one item of its parent.
217
295
  const containers = ['', ...g.clusters.map((c) => c.id)];
218
296
  const kidsOf = new Map<string, string[]>(containers.map((c) => [c, []]));
297
+ const kidSet = new Map<string, Set<string>>(containers.map((c) => [c, new Set()]));
298
+ const addKid = (c: string, it: string): void => {
299
+ const set = kidSet.get(c);
300
+ if (!set || set.has(it)) return;
301
+ set.add(it);
302
+ kidsOf.get(c)?.push(it);
303
+ };
304
+ // A node is an item of the top level and of each cluster around it, and of no other container.
219
305
  for (const { id } of g.nodes) {
220
- for (const c of containers) {
221
- const it = itemIn(id, c);
222
- if (it !== undefined && !kidsOf.get(c)?.includes(it)) kidsOf.get(c)?.push(it);
223
- }
306
+ const bp = boxPath(id);
307
+ addKid('', bp[bp.length - 1] ?? id);
308
+ bp.forEach((c, i) => addKid(c, i === 0 ? id : (bp[i - 1] as string)));
224
309
  }
310
+ // Each edge's container: the deepest cluster around both ends.
311
+ const edgeBox = g.edges.map((e) => commonBox(e.from, e.to));
312
+ const edgesIn = new Map<string, number[]>();
313
+ edgeBox.forEach((c, i) => push(edgesIn, c, i));
225
314
  const lay = new Map<string, Map<string, number>>();
226
315
  for (const c of containers) {
227
316
  const kids = kidsOf.get(c) ?? [];
317
+ const own = kidSet.get(c) ?? new Set<string>();
228
318
  const prevRows = c === '' ? prev?.layers : prev?.clusters[c];
229
319
  const fixed = new Map<string, number>();
230
- prevRows?.forEach((row, i) => row.forEach((id) => kids.includes(id) && fixed.set(id, i)));
320
+ prevRows?.forEach((row, i) => row.forEach((id) => own.has(id) && fixed.set(id, i)));
231
321
  // An edge that closes a cycle does not set the layers.
232
- const es = g.edges
233
- .filter((e) => e.ranks && commonBox(e.from, e.to) === c)
322
+ const es = (edgesIn.get(c) ?? [])
323
+ .map((i) => g.edges[i] as (typeof g.edges)[number])
324
+ .filter((e) => e.ranks)
234
325
  .map((e) => [itemIn(e.from, c) as string, itemIn(e.to, c) as string] as const);
235
326
  lay.set(c, rankLayers(kids, es, fixed));
236
327
  }
@@ -238,35 +329,40 @@ function arrangeNew(g: GraphShape, prev: GraphOrder | undefined, down: boolean):
238
329
  const top = lay.get('') as Map<string, number>;
239
330
  const prevTop = new Map<string, number>();
240
331
  prev?.layers.forEach((row, i) => row.forEach((id) => prevTop.set(id, i)));
241
- const stubAt = new Map(
242
- stubIds.map((id) => [id, prevTop.get(id) ?? Math.min(...g.stubs.filter((s) => s.id === id).map((s) => top.get(itemIn(s.to, '') as string) ?? 0)) - 1]),
243
- );
332
+ const firstTo = new Map<string, number>();
333
+ for (const s of g.stubs) firstTo.set(s.id, Math.min(firstTo.get(s.id) ?? Infinity, top.get(itemIn(s.to, '') as string) ?? 0));
334
+ const stubAt = new Map(stubIds.map((id) => [id, prevTop.get(id) ?? (firstTo.get(id) as number) - 1]));
244
335
  if ([...stubAt.values()].some((l) => l < 0)) {
245
336
  for (const [id, l] of top) top.set(id, l + 1);
246
337
  for (const [id, l] of stubAt) stubAt.set(id, l + 1);
247
338
  }
248
339
  for (const [id, l] of stubAt) top.set(id, l);
249
340
  const layerOf = (c: string, id: string): number => lay.get(c)?.get(id) ?? 0;
250
- const lastLayer = (c: string): number => Math.max(0, ...(lay.get(c)?.values() ?? []));
341
+ const lastOf = new Map<string, number>();
342
+ const lastLayer = (c: string): number => {
343
+ let l = lastOf.get(c);
344
+ if (l === undefined) {
345
+ l = 0;
346
+ for (const v of lay.get(c)?.values() ?? []) l = Math.max(l, v);
347
+ lastOf.set(c, l);
348
+ }
349
+ return l;
350
+ };
251
351
 
252
352
  // Every edge whose end is not in a later layer than its start goes back: not drawn.
253
- const isBack = (from: string, to: string): boolean => {
254
- const c = commonBox(from, to);
255
- return layerOf(c, itemIn(from, c) as string) >= layerOf(c, itemIn(to, c) as string);
256
- };
257
- const back = g.edges.map((e) => isBack(e.from, e.to));
353
+ const back = g.edges.map((e, i) => {
354
+ const c = edgeBox[i] as string;
355
+ return layerOf(c, itemIn(e.from, c) as string) >= layerOf(c, itemIn(e.to, c) as string);
356
+ });
258
357
 
259
358
  // Each container's rows: its items by layer, a previous order kept, new items after it.
260
359
  const rows = new Map<string, string[][]>();
261
360
  for (const c of containers) {
262
361
  const prevRows = c === '' ? prev?.layers : prev?.clusters[c];
263
- const kept = (prevRows ?? []).flat();
362
+ const kept = new Map<string, number>();
363
+ (prevRows ?? []).flat().forEach((id, i) => kept.has(id) || kept.set(id, i));
264
364
  const ids = [...(c === '' ? [...stubIds] : []), ...(kidsOf.get(c) ?? [])];
265
- ids.sort((a, b) => {
266
- const ka = kept.indexOf(a);
267
- const kb = kept.indexOf(b);
268
- return (ka < 0 ? Infinity : ka) - (kb < 0 ? Infinity : kb);
269
- });
365
+ ids.sort((a, b) => (kept.get(a) ?? Infinity) - (kept.get(b) ?? Infinity));
270
366
  const r: string[][] = Array.from({ length: lastLayer(c) + 1 }, () => []);
271
367
  for (const id of ids) r[layerOf(c, id)]?.push(id);
272
368
  rows.set(c, r);
@@ -333,24 +429,23 @@ function arrangeNew(g: GraphShape, prev: GraphOrder | undefined, down: boolean):
333
429
  });
334
430
  for (const s of g.stubs) edgePaths.push({ from: s.id, to: s.to, style: s.style, steps: edgePath(s.id, s.to, s.style) });
335
431
 
336
- // The order within each layer.
432
+ // The order within each layer. Each container's joins: two steps of an edge in it, in two layers.
433
+ const levels = new Map<string, Map<string, number>>();
434
+ for (const c of containers) levels.set(c, new Map((rows.get(c) ?? []).flatMap((r, i) => r.map((id) => [id, i] as const))));
435
+ const linksIn = new Map<string, [string, string][]>(containers.map((c) => [c, []]));
436
+ for (const r of edgePaths)
437
+ for (let i = 1; i < r.steps.length; i++) {
438
+ const a = r.steps[i - 1] as EdgePathStep;
439
+ const b = r.steps[i] as EdgePathStep;
440
+ if (a.in !== b.in) continue;
441
+ const lv = levels.get(a.in);
442
+ if (lv && lv.get(a.id) !== lv.get(b.id)) linksIn.get(a.in)?.push([a.id, b.id]);
443
+ }
337
444
  const slots: Record<string, string[][]> = {};
338
445
  for (const c of containers) {
339
- const links: [string, string][] = [];
340
- const lv = new Map((rows.get(c) ?? []).flatMap((r, i) => r.map((id) => [id, i] as const)));
341
- for (const r of edgePaths)
342
- for (let i = 1; i < r.steps.length; i++) {
343
- const a = r.steps[i - 1] as EdgePathStep;
344
- const b = r.steps[i] as EdgePathStep;
345
- if (a.in === c && b.in === c && lv.get(a.id) !== lv.get(b.id)) links.push([a.id, b.id]);
346
- }
347
446
  const hasPrev = c === '' ? !!prev : !!prev?.clusters[c];
348
447
  // A point beside a cluster is right after it.
349
- slots[c] = orderRows(rows.get(c) ?? [], links, hasPrev).map((row) => {
350
- const by = row.filter((id) => id.includes('|by:'));
351
- const rest = row.filter((id) => !id.includes('|by:'));
352
- return rest.flatMap((id) => [id, ...by.filter((p) => p.includes(`|by:${id}|`))]);
353
- });
448
+ slots[c] = orderRows(rows.get(c) ?? [], linksIn.get(c) ?? [], hasPrev).map(byPointsAfter);
354
449
  }
355
450
  const real = (r: readonly string[][]) => r.map((row) => row.filter((id) => !isPoint(id)));
356
451
  const clusters = g.clusters.map((c) => ({ id: c.id, parent: c.parent, cycle: !!c.cycle, layers: real(slots[c.id] ?? []).filter((r) => r.length) }));
@@ -370,6 +465,28 @@ function arrangeNew(g: GraphShape, prev: GraphOrder | undefined, down: boolean):
370
465
  };
371
466
  }
372
467
 
468
+ /** A layer with each point beside a cluster (`|by:<cluster>|`) right after that cluster. */
469
+ function byPointsAfter(row: readonly string[]): string[] {
470
+ const by = row.filter((id) => id.includes('|by:'));
471
+ const rest = row.filter((id) => !id.includes('|by:'));
472
+ if (!by.length) return rest;
473
+ // An id without '|' is named in a point as the text between '|by:' and the next '|';
474
+ // the few others are looked for in each point.
475
+ const named = new Map<string, string[]>();
476
+ for (const p of by) {
477
+ const seen = new Set<string>();
478
+ for (let i = p.indexOf('|by:'); i >= 0; i = p.indexOf('|by:', i + 1)) {
479
+ const end = p.indexOf('|', i + 4);
480
+ if (end < 0) break;
481
+ const id = p.slice(i + 4, end);
482
+ if (seen.has(id)) continue;
483
+ seen.add(id);
484
+ push(named, id, p);
485
+ }
486
+ }
487
+ return rest.flatMap((id) => [id, ...(id.includes('|') ? by.filter((p) => p.includes(`|by:${id}|`)) : (named.get(id) ?? []))]);
488
+ }
489
+
373
490
  /**
374
491
  * How many layers the drawing spans along its flow, and how many items it stacks across
375
492
  * it at most, counting a cluster as what it holds: an estimate of the drawing's shape,
@@ -31,6 +31,27 @@ type Changed = PropertyValues<CaeLayeredGraph> & PropertyValueMap<{ down: boolea
31
31
 
32
32
  const EMPTY: GraphInput = { series: [], nodes: [], clusters: [], edges: [], stubs: [] };
33
33
 
34
+ /** An arrangement's slots and the slots each edge passes, as one string: the same string, the same layout from the same sizes. */
35
+ const structures = new WeakMap<Arrangement, string>();
36
+ function structureOf(a: Arrangement): string {
37
+ let s = structures.get(a);
38
+ if (s === undefined) {
39
+ s = JSON.stringify([a.slots, a.edgePaths.map((r) => r.steps.map((x) => x.id))]);
40
+ structures.set(a, s);
41
+ }
42
+ return s;
43
+ }
44
+
45
+ type LayKey = { key: string; structure: string; down: boolean; sizes: [string, number, number][] };
46
+
47
+ function sameLayKey(a: LayKey, b: LayKey | null): boolean {
48
+ if (!b || a.key !== b.key || a.down !== b.down || a.structure !== b.structure || a.sizes.length !== b.sizes.length) return false;
49
+ return a.sizes.every((x, i) => {
50
+ const y = b.sizes[i] as [string, number, number];
51
+ return x[0] === y[0] && x[1] === y[1] && x[2] === y[2];
52
+ });
53
+ }
54
+
34
55
  /**
35
56
  * A layered graph: each node in a layer by the longest path to it, edges bending only
36
57
  * in the gaps between layers and never passing a node or a cluster's label; a cluster
@@ -169,10 +190,13 @@ export class CaeLayeredGraph extends LitElement {
169
190
  private readonly active = new Map<string, number>();
170
191
  /** What the sizes were last measured for, what the layout was computed from, and the graph it is of. */
171
192
  private measured: { graph: GraphInput; key: string } | null = null;
172
- private layKey = '';
193
+ /** What the layout was computed from: the graph's key, its structure (`structureOf`), the direction and the sizes. */
194
+ private layKey: LayKey | null = null;
173
195
  private layFor: string | null = null;
174
196
  private litCache: { key: string | null; graph: GraphInput; related: unknown; on: LitMarks | null } | null = null;
175
197
  private scrolled: Element | null = null;
198
+ /** The marks by id, for the graph and the matrix they were found in (`findMark`). */
199
+ private markIx: { drawn: DrawnGraph; matrix: MarkMatrixInput | null; marks: Map<string, { drill?: boolean }> } | null = null;
176
200
 
177
201
  private readonly pz = new PanZoomController(this, {
178
202
  frame: () => this.renderRoot.querySelector<HTMLElement>('.frame'),
@@ -299,8 +323,8 @@ export class CaeLayeredGraph extends LitElement {
299
323
  const sizes = new Map<string, Size>();
300
324
  for (const el of dagEl.querySelectorAll<HTMLElement>('[data-id]')) sizes.set(el.dataset['id'] ?? '', { w: el.offsetWidth, h: el.offsetHeight });
301
325
  const a = this.arr;
302
- const key = JSON.stringify([this.key, a.slots, a.edgePaths.map((r) => r.steps.map((s) => s.id)), this.down, [...sizes]]);
303
- if (key === this.layKey) return this.fit();
326
+ const key = { key: this.key, structure: structureOf(a), down: this.down, sizes: [...sizes].map(([id, s]): [string, number, number] => [id, s.w, s.h]) };
327
+ if (sameLayKey(key, this.layKey)) return this.fit();
304
328
  this.layKey = key;
305
329
  this.layFor = this.key;
306
330
  this.lay = layoutGraph(a, sizes, this.down);
@@ -335,9 +359,18 @@ export class CaeLayeredGraph extends LitElement {
335
359
  }
336
360
 
337
361
  private findMark(id: string): { drill?: boolean } | null {
338
- for (const n of this.drawn.nodes) for (const m of [...n.marks, ...n.loose]) if (m?.id === id) return m;
339
- for (const r of this.matrix?.rows ?? []) for (const m of r.cells) if (m?.id === id) return m;
340
- return null;
362
+ let ix = this.markIx;
363
+ if (!ix || ix.drawn !== this.drawn || ix.matrix !== this.matrix) {
364
+ // Each mark by its id: the first of the nodes', then the matrix's.
365
+ const marks = new Map<string, { drill?: boolean }>();
366
+ for (const n of this.drawn.nodes) {
367
+ for (const m of n.marks) if (m && !marks.has(m.id)) marks.set(m.id, m);
368
+ for (const m of n.loose) if (m && !marks.has(m.id)) marks.set(m.id, m);
369
+ }
370
+ for (const r of this.matrix?.rows ?? []) for (const m of r.cells) if (m && !marks.has(m.id)) marks.set(m.id, m);
371
+ ix = this.markIx = { drawn: this.drawn, matrix: this.matrix, marks };
372
+ }
373
+ return ix.marks.get(id) ?? null;
341
374
  }
342
375
 
343
376
  /** The series (or mark id) highlighted: the one hovered or focused, else the selected mark's, else `highlight`; and the marks it lights. */