@abreen/tada 1.6.4 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tada.ts +4 -11
- package/build/apply-base-path-plugin.ts +11 -19
- package/build/colors.ts +19 -22
- package/build/content-watch.ts +3 -16
- package/build/copy.ts +42 -20
- package/build/deflist-id-plugin.ts +3 -16
- package/build/features.ts +3 -6
- package/build/generate-content-assets.ts +9 -5
- package/build/generate-favicon.ts +5 -15
- package/build/generate-web-app-manifest.ts +19 -57
- package/build/globals.ts +19 -24
- package/build/log.ts +30 -46
- package/build/pagefind.ts +2 -2
- package/build/reachability.ts +1 -1
- package/build/serve.ts +1 -5
- package/build/site-variables.ts +2 -2
- package/build/templates.ts +2 -2
- package/build/text-to-id.ts +6 -0
- package/build/types.d.ts +1 -7
- package/build/utils/code.ts +3 -7
- package/build/utils/front-matter.ts +28 -0
- package/build/utils/link.ts +8 -0
- package/build/utils/literate-java.ts +42 -33
- package/build/utils/markdown.ts +13 -17
- package/build/utils/paths.ts +3 -11
- package/build/utils/render.ts +25 -34
- package/build/utils/trace-layout.ts +308 -246
- package/build/utils/trace-svg.ts +1 -6
- package/build/utils/trace.ts +22 -47
- package/build/validate-internal-links-plugin.ts +2 -9
- package/build/watch-reload-client.ts +4 -6
- package/build/watch.ts +67 -116
- package/init/content/index.md +2 -0
- package/init/content/labs/00/VowelCounter.java.md +2 -0
- package/init/content/labs/00/index.md +2 -0
- package/init/content/labs/01/index.md +2 -0
- package/init/content/labs/index.md +2 -0
- package/init/content/lectures/01/Pair.java.md +2 -0
- package/init/content/lectures/01/index.md +2 -0
- package/init/content/lectures/02/index.md +2 -0
- package/init/content/lectures/index.md +2 -0
- package/init/content/markdown.md +5 -1
- package/init/content/problem_sets/index.html +2 -0
- package/package.json +2 -2
- package/src/_alerts.scss +1 -1
- package/src/_base.scss +9 -10
- package/src/_mixins.scss +4 -0
- package/src/literate/style.scss +1 -0
- package/src/print/style.scss +37 -20
- package/src/toc/style.scss +4 -3
- package/templates/_theme.scss +1 -1
|
@@ -7,10 +7,10 @@ import type {
|
|
|
7
7
|
} from '../types';
|
|
8
8
|
|
|
9
9
|
/** Maximum number of array cells to display. */
|
|
10
|
-
const ARRAY_MAX_CELLS = 12;
|
|
10
|
+
export const ARRAY_MAX_CELLS = 12;
|
|
11
11
|
|
|
12
12
|
/** Default font size in pixels. */
|
|
13
|
-
const DEFAULT_FONT_SIZE = 15;
|
|
13
|
+
export const DEFAULT_FONT_SIZE = 15;
|
|
14
14
|
|
|
15
15
|
/** Gap between objects in the layout. */
|
|
16
16
|
const OBJ_GAP = 24;
|
|
@@ -286,6 +286,46 @@ export function getObjectSize(
|
|
|
286
286
|
return { width, height, valueX };
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
/** Size info returned by getObjectSize. */
|
|
290
|
+
type ObjSize = {
|
|
291
|
+
width: number;
|
|
292
|
+
height: number;
|
|
293
|
+
valueX?: number;
|
|
294
|
+
cellWidth?: number;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/** Chain of non-tree objects laid out horizontally to the right of a tree node. */
|
|
298
|
+
interface RefChain {
|
|
299
|
+
ids: string[];
|
|
300
|
+
totalWidth: number;
|
|
301
|
+
maxHeight: number;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Construct a TraceObjectLayout, copying optional size and child field info. */
|
|
305
|
+
function makeLayout(
|
|
306
|
+
x: number,
|
|
307
|
+
y: number,
|
|
308
|
+
size: ObjSize,
|
|
309
|
+
childFields?: Set<string>,
|
|
310
|
+
): TraceObjectLayout {
|
|
311
|
+
const ol: TraceObjectLayout = {
|
|
312
|
+
x,
|
|
313
|
+
y,
|
|
314
|
+
width: size.width,
|
|
315
|
+
height: size.height,
|
|
316
|
+
};
|
|
317
|
+
if (size.valueX !== undefined) {
|
|
318
|
+
ol.valueX = size.valueX;
|
|
319
|
+
}
|
|
320
|
+
if (size.cellWidth !== undefined) {
|
|
321
|
+
ol.cellWidth = size.cellWidth;
|
|
322
|
+
}
|
|
323
|
+
if (childFields && childFields.size > 0) {
|
|
324
|
+
ol.childFields = Array.from(childFields);
|
|
325
|
+
}
|
|
326
|
+
return ol;
|
|
327
|
+
}
|
|
328
|
+
|
|
289
329
|
/**
|
|
290
330
|
* The main entry point. Scans steps, builds supergraph, finds root objects,
|
|
291
331
|
* builds a tree for each root, runs d3-flextree, translates positions to
|
|
@@ -304,9 +344,72 @@ export function computeLayout(
|
|
|
304
344
|
|
|
305
345
|
const { graph, fieldToChild } = buildSupergraph(objects, ignoreFields);
|
|
306
346
|
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
347
|
+
// Phase 1: classify types and compute structural metadata
|
|
348
|
+
const { chainTypes, objectChildFields, structuralChildren, roots } =
|
|
349
|
+
classifyTypes(objects, fieldToChild);
|
|
350
|
+
|
|
351
|
+
// Compute sizes for all objects (now with child field info)
|
|
352
|
+
const sizes = new Map<string, ObjSize>();
|
|
353
|
+
for (const [id, info] of objects) {
|
|
354
|
+
const cFields = objectChildFields.get(id) ?? new Set();
|
|
355
|
+
sizes.set(id, getObjectSize(info, fontSize, cFields));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Phase 2: identify tree nodes and compute ref chains
|
|
359
|
+
const { refChains } = buildRefChains(
|
|
360
|
+
roots,
|
|
361
|
+
objects,
|
|
362
|
+
graph,
|
|
363
|
+
structuralChildren,
|
|
364
|
+
sizes,
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Phase 3: lay out all roots (chains and trees), then arrays and orphans
|
|
368
|
+
const result: Record<string, TraceObjectLayout> = {};
|
|
369
|
+
let yOffset = 0;
|
|
370
|
+
|
|
371
|
+
for (const rootId of roots) {
|
|
372
|
+
const rootInfo = objects.get(rootId)!;
|
|
373
|
+
if (chainTypes.has(rootInfo.type)) {
|
|
374
|
+
yOffset = layoutChainRoot(
|
|
375
|
+
rootId,
|
|
376
|
+
sizes,
|
|
377
|
+
refChains,
|
|
378
|
+
structuralChildren,
|
|
379
|
+
objects,
|
|
380
|
+
result,
|
|
381
|
+
yOffset,
|
|
382
|
+
);
|
|
383
|
+
} else {
|
|
384
|
+
yOffset = layoutTreeRoot(
|
|
385
|
+
rootId,
|
|
386
|
+
graph,
|
|
387
|
+
sizes,
|
|
388
|
+
refChains,
|
|
389
|
+
objectChildFields,
|
|
390
|
+
structuralChildren,
|
|
391
|
+
result,
|
|
392
|
+
yOffset,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
yOffset = layoutArrays(objects, graph, sizes, fontSize, result, yOffset);
|
|
398
|
+
layoutOrphans(objects, sizes, result, yOffset);
|
|
399
|
+
|
|
400
|
+
return { objects: result, ignoreFields };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Phase 1: determine which types are "chain" (1 self-referencing field) vs
|
|
405
|
+
* "tree" (2+ fields), compute per-object child fields, structural children,
|
|
406
|
+
* and find tree/chain roots.
|
|
407
|
+
*/
|
|
408
|
+
function classifyTypes(
|
|
409
|
+
objects: Map<string, ObjectInfo>,
|
|
410
|
+
fieldToChild: Map<string, Map<string, string>>,
|
|
411
|
+
) {
|
|
412
|
+
// Find which field names are structural per type (same-type references).
|
|
310
413
|
const typeChildFields = new Map<string, Set<string>>();
|
|
311
414
|
for (const [parentId, fMap] of fieldToChild) {
|
|
312
415
|
const parentInfo = objects.get(parentId);
|
|
@@ -324,9 +427,10 @@ export function computeLayout(
|
|
|
324
427
|
}
|
|
325
428
|
}
|
|
326
429
|
}
|
|
327
|
-
|
|
328
|
-
//
|
|
329
|
-
// Only tree types get child-slot
|
|
430
|
+
|
|
431
|
+
// Chain types (1 structural child field) are laid out horizontally.
|
|
432
|
+
// Tree types (2+ fields) use d3-flextree. Only tree types get child-slot
|
|
433
|
+
// rendering.
|
|
330
434
|
const chainTypes = new Set<string>();
|
|
331
435
|
for (const [type, fields] of typeChildFields) {
|
|
332
436
|
if (fields.size === 1) {
|
|
@@ -371,9 +475,7 @@ export function computeLayout(
|
|
|
371
475
|
}
|
|
372
476
|
|
|
373
477
|
// Find tree roots: objects that (a) belong to a type with structural child
|
|
374
|
-
// fields
|
|
375
|
-
// of another object. Cross-type refs (e.g., PreorderIterator -> Node) don't
|
|
376
|
-
// prevent an object from being a root.
|
|
478
|
+
// fields and (b) are not themselves a structural child of another object.
|
|
377
479
|
const structurallyReferenced = new Set<string>();
|
|
378
480
|
for (const childSet of structuralChildren.values()) {
|
|
379
481
|
for (const childId of childSet) {
|
|
@@ -395,17 +497,20 @@ export function computeLayout(
|
|
|
395
497
|
// Sort roots by first appearance for deterministic ordering
|
|
396
498
|
roots.sort((a, b) => objects.get(a)!.firstStep - objects.get(b)!.firstStep);
|
|
397
499
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
string,
|
|
401
|
-
{ width: number; height: number; valueX?: number; cellWidth?: number }
|
|
402
|
-
>();
|
|
403
|
-
for (const [id, info] of objects) {
|
|
404
|
-
const cFields = objectChildFields.get(id) ?? new Set();
|
|
405
|
-
sizes.set(id, getObjectSize(info, fontSize, cFields));
|
|
406
|
-
}
|
|
500
|
+
return { chainTypes, objectChildFields, structuralChildren, roots };
|
|
501
|
+
}
|
|
407
502
|
|
|
408
|
-
|
|
503
|
+
/**
|
|
504
|
+
* Phase 2: walk from roots to mark tree nodes, then compute horizontal
|
|
505
|
+
* ref chains for each tree node.
|
|
506
|
+
*/
|
|
507
|
+
function buildRefChains(
|
|
508
|
+
roots: string[],
|
|
509
|
+
objects: Map<string, ObjectInfo>,
|
|
510
|
+
graph: Map<string, string[]>,
|
|
511
|
+
structuralChildren: Map<string, Set<string>>,
|
|
512
|
+
sizes: Map<string, ObjSize>,
|
|
513
|
+
) {
|
|
409
514
|
const treeNodeIds = new Set<string>();
|
|
410
515
|
function markTreeNodes(id: string) {
|
|
411
516
|
if (treeNodeIds.has(id)) {
|
|
@@ -427,7 +532,6 @@ export function computeLayout(
|
|
|
427
532
|
}
|
|
428
533
|
}
|
|
429
534
|
|
|
430
|
-
// Compute horizontal ref chains (non-tree children laid out to the right)
|
|
431
535
|
const refChains = new Map<string, RefChain>();
|
|
432
536
|
const claimed = new Set<string>();
|
|
433
537
|
for (const treeId of treeNodeIds) {
|
|
@@ -447,187 +551,181 @@ export function computeLayout(
|
|
|
447
551
|
}
|
|
448
552
|
}
|
|
449
553
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
let yOffset = 0;
|
|
554
|
+
return { treeNodeIds, refChains };
|
|
555
|
+
}
|
|
453
556
|
|
|
454
|
-
|
|
455
|
-
|
|
557
|
+
/** Effective width of a node including its ref chain. */
|
|
558
|
+
function effectiveWidth(
|
|
559
|
+
nodeWidth: number,
|
|
560
|
+
chain: RefChain | undefined,
|
|
561
|
+
): number {
|
|
562
|
+
return nodeWidth + (chain ? Math.round(OBJ_GAP * 0.5) + chain.totalWidth : 0);
|
|
563
|
+
}
|
|
456
564
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
while (currentId && !visited.has(currentId) && objects.has(currentId)) {
|
|
465
|
-
visited.add(currentId);
|
|
466
|
-
const size = sizes.get(currentId)!;
|
|
467
|
-
const chain = refChains.get(currentId);
|
|
468
|
-
|
|
469
|
-
// Effective width: node + ref chain to the right (if any)
|
|
470
|
-
const nodeWidth = size.width;
|
|
471
|
-
const chainWidth = chain
|
|
472
|
-
? Math.round(OBJ_GAP * 0.5) + chain.totalWidth
|
|
473
|
-
: 0;
|
|
474
|
-
const effectiveWidth = nodeWidth + chainWidth;
|
|
475
|
-
|
|
476
|
-
const ol: TraceObjectLayout = {
|
|
477
|
-
x: Math.round(xOff),
|
|
478
|
-
y: Math.round(yOffset),
|
|
479
|
-
width: size.width,
|
|
480
|
-
height: size.height,
|
|
481
|
-
};
|
|
482
|
-
if (size.valueX !== undefined) {
|
|
483
|
-
ol.valueX = size.valueX;
|
|
484
|
-
}
|
|
485
|
-
if (size.cellWidth !== undefined) {
|
|
486
|
-
ol.cellWidth = size.cellWidth;
|
|
487
|
-
}
|
|
488
|
-
result[currentId] = ol;
|
|
489
|
-
|
|
490
|
-
// Place ref chain objects to the right of this chain node
|
|
491
|
-
if (chain) {
|
|
492
|
-
let refX = xOff + nodeWidth + Math.round(OBJ_GAP * 0.5);
|
|
493
|
-
for (const refId of chain.ids) {
|
|
494
|
-
const rs = sizes.get(refId)!;
|
|
495
|
-
const rol: TraceObjectLayout = {
|
|
496
|
-
x: Math.round(refX),
|
|
497
|
-
y: Math.round(yOffset),
|
|
498
|
-
width: rs.width,
|
|
499
|
-
height: rs.height,
|
|
500
|
-
};
|
|
501
|
-
if (rs.valueX !== undefined) {
|
|
502
|
-
rol.valueX = rs.valueX;
|
|
503
|
-
}
|
|
504
|
-
if (rs.cellWidth !== undefined) {
|
|
505
|
-
rol.cellWidth = rs.cellWidth;
|
|
506
|
-
}
|
|
507
|
-
result[refId] = rol;
|
|
508
|
-
refX += rs.width + Math.round(OBJ_GAP * 0.5);
|
|
509
|
-
}
|
|
510
|
-
maxHeight = Math.max(maxHeight, size.height, chain.maxHeight);
|
|
511
|
-
} else {
|
|
512
|
-
maxHeight = Math.max(maxHeight, size.height);
|
|
513
|
-
}
|
|
565
|
+
/** Effective height of a node including its ref chain. */
|
|
566
|
+
function effectiveHeight(
|
|
567
|
+
nodeHeight: number,
|
|
568
|
+
chain: RefChain | undefined,
|
|
569
|
+
): number {
|
|
570
|
+
return chain ? Math.max(nodeHeight, chain.maxHeight) : nodeHeight;
|
|
571
|
+
}
|
|
514
572
|
|
|
515
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Place ref chain objects in a horizontal line starting at (startX, y).
|
|
575
|
+
* Each object in the chain is separated by half the gap.
|
|
576
|
+
*/
|
|
577
|
+
function placeRefChain(
|
|
578
|
+
chain: RefChain,
|
|
579
|
+
startX: number,
|
|
580
|
+
y: number,
|
|
581
|
+
sizes: Map<string, ObjSize>,
|
|
582
|
+
result: Record<string, TraceObjectLayout>,
|
|
583
|
+
) {
|
|
584
|
+
let xOff = startX;
|
|
585
|
+
for (const childId of chain.ids) {
|
|
586
|
+
const cs = sizes.get(childId)!;
|
|
587
|
+
result[childId] = makeLayout(xOff, y, cs);
|
|
588
|
+
xOff += cs.width + Math.round(OBJ_GAP * 0.5);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
516
591
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
592
|
+
/**
|
|
593
|
+
* Lay out a chain-type root and its structural children left to right.
|
|
594
|
+
* Returns the updated yOffset.
|
|
595
|
+
*/
|
|
596
|
+
function layoutChainRoot(
|
|
597
|
+
rootId: string,
|
|
598
|
+
sizes: Map<string, ObjSize>,
|
|
599
|
+
refChains: Map<string, RefChain>,
|
|
600
|
+
structuralChildren: Map<string, Set<string>>,
|
|
601
|
+
objects: Map<string, ObjectInfo>,
|
|
602
|
+
result: Record<string, TraceObjectLayout>,
|
|
603
|
+
yOffset: number,
|
|
604
|
+
): number {
|
|
605
|
+
let xOff = 0;
|
|
606
|
+
let maxHeight = 0;
|
|
607
|
+
let currentId: string | undefined = rootId;
|
|
608
|
+
const visited = new Set<string>();
|
|
521
609
|
|
|
522
|
-
|
|
523
|
-
|
|
610
|
+
while (currentId && !visited.has(currentId) && objects.has(currentId)) {
|
|
611
|
+
visited.add(currentId);
|
|
612
|
+
const size = sizes.get(currentId)!;
|
|
613
|
+
const chain = refChains.get(currentId);
|
|
614
|
+
const ew = effectiveWidth(size.width, chain);
|
|
615
|
+
|
|
616
|
+
result[currentId] = makeLayout(Math.round(xOff), Math.round(yOffset), size);
|
|
617
|
+
|
|
618
|
+
if (chain) {
|
|
619
|
+
const refX = xOff + size.width + Math.round(OBJ_GAP * 0.5);
|
|
620
|
+
placeRefChain(
|
|
621
|
+
chain,
|
|
622
|
+
Math.round(refX),
|
|
623
|
+
Math.round(yOffset),
|
|
624
|
+
sizes,
|
|
625
|
+
result,
|
|
626
|
+
);
|
|
627
|
+
maxHeight = Math.max(maxHeight, size.height, chain.maxHeight);
|
|
628
|
+
} else {
|
|
629
|
+
maxHeight = Math.max(maxHeight, size.height);
|
|
524
630
|
}
|
|
525
631
|
|
|
526
|
-
|
|
527
|
-
const visited = new Set<string>();
|
|
528
|
-
const treeData = buildTreeNode(
|
|
529
|
-
rootId,
|
|
530
|
-
graph,
|
|
531
|
-
sizes,
|
|
532
|
-
visited,
|
|
533
|
-
structuralChildren,
|
|
534
|
-
);
|
|
632
|
+
xOff += ew + OBJ_GAP;
|
|
535
633
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
634
|
+
// Follow the single structural child
|
|
635
|
+
const sChildren = structuralChildren.get(currentId);
|
|
636
|
+
currentId = sChildren ? [...sChildren][0] : undefined;
|
|
637
|
+
}
|
|
539
638
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const chain = refChains.get(node.data.id);
|
|
543
|
-
const ew =
|
|
544
|
-
node.data.width +
|
|
545
|
-
(chain ? Math.round(OBJ_GAP * 0.5) + chain.totalWidth : 0);
|
|
546
|
-
const eh = chain
|
|
547
|
-
? Math.max(node.data.height, chain.maxHeight)
|
|
548
|
-
: node.data.height;
|
|
549
|
-
return [ew + OBJ_GAP, eh + OBJ_GAP];
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
const root = layoutEngine.hierarchy(treeData, d => d.children);
|
|
553
|
-
layoutEngine(root);
|
|
554
|
-
|
|
555
|
-
// Find bounds to translate to positive coordinates
|
|
556
|
-
let minX = Infinity;
|
|
557
|
-
let minY = Infinity;
|
|
558
|
-
let maxY = -Infinity;
|
|
559
|
-
|
|
560
|
-
root.each(node => {
|
|
561
|
-
const chain = refChains.get(node.data.id);
|
|
562
|
-
const ew =
|
|
563
|
-
node.data.width +
|
|
564
|
-
(chain ? Math.round(OBJ_GAP * 0.5) + chain.totalWidth : 0);
|
|
565
|
-
const eh = chain
|
|
566
|
-
? Math.max(node.data.height, chain.maxHeight)
|
|
567
|
-
: node.data.height;
|
|
568
|
-
minX = Math.min(minX, node.x - ew / 2);
|
|
569
|
-
minY = Math.min(minY, node.y);
|
|
570
|
-
maxY = Math.max(maxY, node.y + eh);
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
// Place tree nodes and their ref chains
|
|
574
|
-
root.each(node => {
|
|
575
|
-
const size = sizes.get(node.data.id)!;
|
|
576
|
-
const cFields = objectChildFields.get(node.data.id);
|
|
577
|
-
const chain = refChains.get(node.data.id);
|
|
578
|
-
const ew =
|
|
579
|
-
node.data.width +
|
|
580
|
-
(chain ? Math.round(OBJ_GAP * 0.5) + chain.totalWidth : 0);
|
|
581
|
-
|
|
582
|
-
// Tree node is left-aligned within its effective bounding box
|
|
583
|
-
const leftEdge = node.x - ew / 2;
|
|
584
|
-
|
|
585
|
-
const ol: TraceObjectLayout = {
|
|
586
|
-
x: Math.round(leftEdge - minX),
|
|
587
|
-
y: Math.round(node.y - minY + yOffset),
|
|
588
|
-
width: size.width,
|
|
589
|
-
height: size.height,
|
|
590
|
-
};
|
|
591
|
-
if (size.valueX !== undefined) {
|
|
592
|
-
ol.valueX = size.valueX;
|
|
593
|
-
}
|
|
594
|
-
if (size.cellWidth !== undefined) {
|
|
595
|
-
ol.cellWidth = size.cellWidth;
|
|
596
|
-
}
|
|
597
|
-
if (cFields && cFields.size > 0) {
|
|
598
|
-
ol.childFields = Array.from(cFields);
|
|
599
|
-
}
|
|
600
|
-
result[node.data.id] = ol;
|
|
601
|
-
|
|
602
|
-
// Place chain objects to the right of the tree node
|
|
603
|
-
if (chain) {
|
|
604
|
-
let xOff =
|
|
605
|
-
Math.round(leftEdge - minX) + size.width + Math.round(OBJ_GAP * 0.5);
|
|
606
|
-
for (const childId of chain.ids) {
|
|
607
|
-
const cs = sizes.get(childId)!;
|
|
608
|
-
const col: TraceObjectLayout = {
|
|
609
|
-
x: xOff,
|
|
610
|
-
y: Math.round(node.y - minY + yOffset),
|
|
611
|
-
width: cs.width,
|
|
612
|
-
height: cs.height,
|
|
613
|
-
};
|
|
614
|
-
if (cs.valueX !== undefined) {
|
|
615
|
-
col.valueX = cs.valueX;
|
|
616
|
-
}
|
|
617
|
-
if (cs.cellWidth !== undefined) {
|
|
618
|
-
col.cellWidth = cs.cellWidth;
|
|
619
|
-
}
|
|
620
|
-
result[childId] = col;
|
|
621
|
-
xOff += cs.width + Math.round(OBJ_GAP * 0.5);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
});
|
|
639
|
+
return yOffset + maxHeight + OBJ_GAP;
|
|
640
|
+
}
|
|
625
641
|
|
|
626
|
-
|
|
627
|
-
|
|
642
|
+
/**
|
|
643
|
+
* Lay out a tree-type root using d3-flextree.
|
|
644
|
+
* Returns the updated yOffset.
|
|
645
|
+
*/
|
|
646
|
+
function layoutTreeRoot(
|
|
647
|
+
rootId: string,
|
|
648
|
+
graph: Map<string, string[]>,
|
|
649
|
+
sizes: Map<string, ObjSize>,
|
|
650
|
+
refChains: Map<string, RefChain>,
|
|
651
|
+
objectChildFields: Map<string, Set<string>>,
|
|
652
|
+
structuralChildren: Map<string, Set<string>>,
|
|
653
|
+
result: Record<string, TraceObjectLayout>,
|
|
654
|
+
yOffset: number,
|
|
655
|
+
): number {
|
|
656
|
+
const visited = new Set<string>();
|
|
657
|
+
const treeData = buildTreeNode(
|
|
658
|
+
rootId,
|
|
659
|
+
graph,
|
|
660
|
+
sizes,
|
|
661
|
+
visited,
|
|
662
|
+
structuralChildren,
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
if (!treeData) {
|
|
666
|
+
return yOffset;
|
|
628
667
|
}
|
|
629
668
|
|
|
630
|
-
//
|
|
669
|
+
// d3-flextree nodeSize includes the chain width so nodes are spaced apart
|
|
670
|
+
const layoutEngine = flextree<TreeNode>({}).nodeSize(node => {
|
|
671
|
+
const chain = refChains.get(node.data.id);
|
|
672
|
+
const ew = effectiveWidth(node.data.width, chain);
|
|
673
|
+
const eh = effectiveHeight(node.data.height, chain);
|
|
674
|
+
return [ew + OBJ_GAP, eh + OBJ_GAP];
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const root = layoutEngine.hierarchy(treeData, d => d.children);
|
|
678
|
+
layoutEngine(root);
|
|
679
|
+
|
|
680
|
+
// Find bounds to translate to positive coordinates
|
|
681
|
+
let minX = Infinity;
|
|
682
|
+
let minY = Infinity;
|
|
683
|
+
let maxY = -Infinity;
|
|
684
|
+
|
|
685
|
+
root.each(node => {
|
|
686
|
+
const chain = refChains.get(node.data.id);
|
|
687
|
+
const ew = effectiveWidth(node.data.width, chain);
|
|
688
|
+
const eh = effectiveHeight(node.data.height, chain);
|
|
689
|
+
minX = Math.min(minX, node.x - ew / 2);
|
|
690
|
+
minY = Math.min(minY, node.y);
|
|
691
|
+
maxY = Math.max(maxY, node.y + eh);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// Place tree nodes and their ref chains
|
|
695
|
+
root.each(node => {
|
|
696
|
+
const size = sizes.get(node.data.id)!;
|
|
697
|
+
const cFields = objectChildFields.get(node.data.id);
|
|
698
|
+
const chain = refChains.get(node.data.id);
|
|
699
|
+
const ew = effectiveWidth(node.data.width, chain);
|
|
700
|
+
|
|
701
|
+
// Tree node is left-aligned within its effective bounding box
|
|
702
|
+
const leftEdge = node.x - ew / 2;
|
|
703
|
+
const nx = Math.round(leftEdge - minX);
|
|
704
|
+
const ny = Math.round(node.y - minY + yOffset);
|
|
705
|
+
|
|
706
|
+
result[node.data.id] = makeLayout(nx, ny, size, cFields);
|
|
707
|
+
|
|
708
|
+
if (chain) {
|
|
709
|
+
const chainX = nx + size.width + Math.round(OBJ_GAP * 0.5);
|
|
710
|
+
placeRefChain(chain, chainX, ny, sizes, result);
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
return yOffset + maxY - minY + OBJ_GAP;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Position arrays and their referenced objects below the cells.
|
|
719
|
+
* Returns the updated yOffset.
|
|
720
|
+
*/
|
|
721
|
+
function layoutArrays(
|
|
722
|
+
objects: Map<string, ObjectInfo>,
|
|
723
|
+
graph: Map<string, string[]>,
|
|
724
|
+
sizes: Map<string, ObjSize>,
|
|
725
|
+
fontSize: number,
|
|
726
|
+
result: Record<string, TraceObjectLayout>,
|
|
727
|
+
yOffset: number,
|
|
728
|
+
): number {
|
|
631
729
|
for (const [id, info] of objects) {
|
|
632
730
|
if (!info.isArray || id in result) {
|
|
633
731
|
continue;
|
|
@@ -636,16 +734,7 @@ export function computeLayout(
|
|
|
636
734
|
const size = sizes.get(id)!;
|
|
637
735
|
const cw = size.cellWidth ?? Math.round(fontSize * CHAR_WIDTH_RATIO * 3);
|
|
638
736
|
|
|
639
|
-
|
|
640
|
-
x: 0,
|
|
641
|
-
y: Math.round(yOffset),
|
|
642
|
-
width: size.width,
|
|
643
|
-
height: size.height,
|
|
644
|
-
};
|
|
645
|
-
if (size.cellWidth !== undefined) {
|
|
646
|
-
ol.cellWidth = size.cellWidth;
|
|
647
|
-
}
|
|
648
|
-
result[id] = ol;
|
|
737
|
+
result[id] = makeLayout(0, Math.round(yOffset), size);
|
|
649
738
|
|
|
650
739
|
// Get unreferenced children from graph, preserving cell order
|
|
651
740
|
const children = (graph.get(id) ?? []).filter(
|
|
@@ -678,19 +767,7 @@ export function computeLayout(
|
|
|
678
767
|
const refY = Math.round(yOffset + size.height + OBJ_GAP);
|
|
679
768
|
let maxRefHeight = 0;
|
|
680
769
|
for (const p of placements) {
|
|
681
|
-
|
|
682
|
-
x: Math.round(Math.max(0, p.x)),
|
|
683
|
-
y: refY,
|
|
684
|
-
width: p.size.width,
|
|
685
|
-
height: p.size.height,
|
|
686
|
-
};
|
|
687
|
-
if (p.size.valueX !== undefined) {
|
|
688
|
-
col.valueX = p.size.valueX;
|
|
689
|
-
}
|
|
690
|
-
if (p.size.cellWidth !== undefined) {
|
|
691
|
-
col.cellWidth = p.size.cellWidth;
|
|
692
|
-
}
|
|
693
|
-
result[p.id] = col;
|
|
770
|
+
result[p.id] = makeLayout(Math.round(Math.max(0, p.x)), refY, p.size);
|
|
694
771
|
maxRefHeight = Math.max(maxRefHeight, p.height);
|
|
695
772
|
}
|
|
696
773
|
|
|
@@ -699,36 +776,27 @@ export function computeLayout(
|
|
|
699
776
|
yOffset += size.height + OBJ_GAP;
|
|
700
777
|
}
|
|
701
778
|
}
|
|
779
|
+
return yOffset;
|
|
780
|
+
}
|
|
702
781
|
|
|
703
|
-
|
|
782
|
+
/**
|
|
783
|
+
* Place any orphan objects that were not reached by tree, chain, or array
|
|
784
|
+
* layout passes.
|
|
785
|
+
*/
|
|
786
|
+
function layoutOrphans(
|
|
787
|
+
objects: Map<string, ObjectInfo>,
|
|
788
|
+
sizes: Map<string, ObjSize>,
|
|
789
|
+
result: Record<string, TraceObjectLayout>,
|
|
790
|
+
yOffset: number,
|
|
791
|
+
) {
|
|
792
|
+
let y = yOffset;
|
|
704
793
|
for (const id of objects.keys()) {
|
|
705
794
|
if (!(id in result)) {
|
|
706
795
|
const size = sizes.get(id)!;
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
y: Math.round(yOffset),
|
|
710
|
-
width: size.width,
|
|
711
|
-
height: size.height,
|
|
712
|
-
};
|
|
713
|
-
if (size.valueX !== undefined) {
|
|
714
|
-
ol.valueX = size.valueX;
|
|
715
|
-
}
|
|
716
|
-
if (size.cellWidth !== undefined) {
|
|
717
|
-
ol.cellWidth = size.cellWidth;
|
|
718
|
-
}
|
|
719
|
-
result[id] = ol;
|
|
720
|
-
yOffset += size.height + OBJ_GAP;
|
|
796
|
+
result[id] = makeLayout(0, Math.round(y), size);
|
|
797
|
+
y += size.height + OBJ_GAP;
|
|
721
798
|
}
|
|
722
799
|
}
|
|
723
|
-
|
|
724
|
-
return { objects: result, ignoreFields };
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/** Chain of non-tree objects laid out horizontally to the right of a tree node. */
|
|
728
|
-
interface RefChain {
|
|
729
|
-
ids: string[];
|
|
730
|
-
totalWidth: number;
|
|
731
|
-
maxHeight: number;
|
|
732
800
|
}
|
|
733
801
|
|
|
734
802
|
/** Compute the chain of non-tree objects reachable from a tree node. */
|
|
@@ -736,10 +804,7 @@ function computeRefChain(
|
|
|
736
804
|
startId: string,
|
|
737
805
|
graph: Map<string, string[]>,
|
|
738
806
|
structuralChildren: Map<string, Set<string>>,
|
|
739
|
-
sizes: Map<
|
|
740
|
-
string,
|
|
741
|
-
{ width: number; height: number; valueX?: number; cellWidth?: number }
|
|
742
|
-
>,
|
|
807
|
+
sizes: Map<string, ObjSize>,
|
|
743
808
|
treeNodeIds: Set<string>,
|
|
744
809
|
claimed: Set<string>,
|
|
745
810
|
): RefChain {
|
|
@@ -808,10 +873,7 @@ function computeRefChain(
|
|
|
808
873
|
function buildTreeNode(
|
|
809
874
|
id: string,
|
|
810
875
|
graph: Map<string, string[]>,
|
|
811
|
-
sizes: Map<
|
|
812
|
-
string,
|
|
813
|
-
{ width: number; height: number; valueX?: number; cellWidth?: number }
|
|
814
|
-
>,
|
|
876
|
+
sizes: Map<string, ObjSize>,
|
|
815
877
|
visited: Set<string>,
|
|
816
878
|
structuralChildren: Map<string, Set<string>>,
|
|
817
879
|
): TreeNode | null {
|
package/build/utils/trace-svg.ts
CHANGED
|
@@ -6,12 +6,7 @@ import type {
|
|
|
6
6
|
TraceLayout,
|
|
7
7
|
TraceObjectLayout,
|
|
8
8
|
} from '../types';
|
|
9
|
-
|
|
10
|
-
/** Maximum number of array cells to display. */
|
|
11
|
-
const ARRAY_MAX_CELLS = 12;
|
|
12
|
-
|
|
13
|
-
/** Default font size in pixels. */
|
|
14
|
-
const DEFAULT_FONT_SIZE = 15;
|
|
9
|
+
import { ARRAY_MAX_CELLS, DEFAULT_FONT_SIZE } from './trace-layout';
|
|
15
10
|
|
|
16
11
|
/** Gap between stack area and heap area. */
|
|
17
12
|
const SECTION_GAP = 60;
|