@quolu/lattice 0.12.16 → 0.12.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.12.16",
3
+ "version": "0.12.17",
4
4
  "description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -6,7 +6,7 @@ import {
6
6
  } from './todo-markdown-renderer.mjs';
7
7
  import { renderTodoGanttSvg, TODO_GANTT_STATUS_PRESENTATION } from './todo-gantt-svg.mjs';
8
8
 
9
- export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v13';
9
+ export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v14';
10
10
  export const TODO_GANTT_PROSE_MAX_BYTES = 8 * 1024 * 1024;
11
11
  export const TODO_GANTT_HTML_MAX_BYTES = 24 * 1024 * 1024;
12
12
 
@@ -4,6 +4,9 @@ const TASK_LIMIT = 2_000;
4
4
  const EDGE_LIMIT = 8_000;
5
5
  const SWEEP_ROUNDS = 4;
6
6
 
7
+ /** Columns for a stage made only of ToDos with no registered dependency. */
8
+ const LOOSE_COLUMNS_MINIMUM = 8;
9
+
7
10
  const GEOMETRY = Object.freeze({
8
11
  left: 16,
9
12
  top: 16,
@@ -11,6 +14,7 @@ const GEOMETRY = Object.freeze({
11
14
  lane_gap: 296,
12
15
  node_width: 272,
13
16
  node_height: 68,
17
+ stage_row_gap: 12,
14
18
  route_inset: 8,
15
19
  route_spacing: 12,
16
20
  });
@@ -410,9 +414,27 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
410
414
  const nodesByKey = new Map(nodes.map((node) => [node.key, node]));
411
415
  const { incoming, outgoing, wave } = assignWaves(nodes, nodesByKey, edges);
412
416
  const layers = orderLayers(nodes, wave, incoming, outgoing);
417
+ // A ToDo with no registered dependency has nothing to line up with. Laying
418
+ // every one of them out in a single row makes the canvas as wide as the plan
419
+ // is old — real stores have hundreds — so they wrap into a block underneath
420
+ // the wired ToDos of their stage. Nodes that carry edges keep the top row:
421
+ // edge routing assumes both endpoints of a stage share one baseline.
422
+ const wired = new Set();
423
+ for (const edge of edges) { wired.add(edge.from); wired.add(edge.to); }
413
424
  const transversePosition = new Map();
425
+ const stageRow = new Map();
426
+ const stageRowCount = [];
414
427
  for (const layer of layers) {
415
- for (let index = 0; index < layer.length; index += 1) transversePosition.set(layer[index], index);
428
+ const connected = layer.filter((key) => wired.has(key));
429
+ const loose = layer.filter((key) => !wired.has(key));
430
+ connected.forEach((key, index) => { transversePosition.set(key, index); stageRow.set(key, 0); });
431
+ const columns = Math.max(LOOSE_COLUMNS_MINIMUM, connected.length);
432
+ const firstLooseRow = connected.length === 0 ? 0 : 1;
433
+ loose.forEach((key, index) => {
434
+ transversePosition.set(key, index % columns);
435
+ stageRow.set(key, firstLooseRow + Math.floor(index / columns));
436
+ });
437
+ stageRowCount.push(Math.max(0, ...layer.map((key) => stageRow.get(key))) + 1);
416
438
  }
417
439
  const visibleKeys = new Set(nodes.map(({ key }) => key));
418
440
  const displayBranches = edges.flatMap((edge, semanticIndex) => {
@@ -458,15 +480,25 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
458
480
  const nodeWidth = Math.max(GEOMETRY.node_width,
459
481
  GEOMETRY.route_inset * 2 + (maximumPortTraffic + 1) * GEOMETRY.route_spacing);
460
482
  const laneGap = nodeWidth + 24;
461
- const waveGap = GEOMETRY.node_height + Math.max(36,
462
- (maximumGapOccupancy + 1) * GEOMETRY.route_spacing);
483
+ // Each routing band is sized for the edges that actually cross it. One
484
+ // crowded band used to set the spacing for every stage in the diagram, so a
485
+ // long plan paid the width of its busiest junction on all of its stages.
486
+ const gapBand = (gap) => Math.max(36, ((gapGroups.get(gap)?.length ?? 0)
487
+ + (connectorGroupKeysByGap.get(gap)?.size ?? 0) + 1) * GEOMETRY.route_spacing);
488
+ const stageHeight = (index) => stageRowCount[index] * GEOMETRY.node_height
489
+ + (stageRowCount[index] - 1) * GEOMETRY.stage_row_gap;
490
+ const stageTop = [GEOMETRY.top];
491
+ for (let index = 0; index + 1 < layers.length; index += 1) {
492
+ stageTop.push(stageTop[index] + stageHeight(index) + gapBand(index));
493
+ }
463
494
 
464
495
  const coordinates = new Map();
465
496
  const projectedNodes = nodes.map((node) => {
466
497
  const visible = visibleKeys.has(node.key);
467
498
  const geometry = visible ? {
468
499
  x: GEOMETRY.left + transversePosition.get(node.key) * laneGap,
469
- y: GEOMETRY.top + wave.get(node.key) * waveGap,
500
+ y: stageTop[wave.get(node.key)]
501
+ + stageRow.get(node.key) * (GEOMETRY.node_height + GEOMETRY.stage_row_gap),
470
502
  width: nodeWidth,
471
503
  height: GEOMETRY.node_height,
472
504
  } : null;
@@ -486,10 +518,11 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
486
518
  const gapPosition = (edgeKey, waveIndex) => {
487
519
  const keys = gapGroups.get(waveIndex);
488
520
  const index = keys.indexOf(edgeKey);
489
- return GEOMETRY.top + waveIndex * waveGap + GEOMETRY.node_height
521
+ return stageTop[waveIndex] + stageHeight(waveIndex)
490
522
  + GEOMETRY.route_spacing * (index + 1);
491
523
  };
492
- const maximumLayerWidth = Math.max(...layers.map((layer) => layer.length));
524
+ const maximumLayerWidth = Math.max(1,
525
+ ...[...transversePosition.values()].map((position) => position + 1));
493
526
  const routeRight = GEOMETRY.left + (maximumLayerWidth - 1) * laneGap + nodeWidth + 12;
494
527
  const branchCounts = new Map();
495
528
  for (const branch of displayBranches) {
@@ -551,7 +584,7 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
551
584
  connectorGapRanks.set(targetGap, groupRank + 1);
552
585
  const primaryTargetPortX = group[0].route.at(-1)[0];
553
586
  const junction = [primaryTargetPortX,
554
- GEOMETRY.top + targetGap * waveGap + GEOMETRY.node_height
587
+ stageTop[targetGap] + stageHeight(targetGap)
555
588
  + GEOMETRY.route_spacing * ((gapGroups.get(targetGap)?.length ?? 0) + groupRank + 1)];
556
589
  const contacts = [];
557
590
  for (const [index, edge] of group.entries()) {
@@ -602,8 +635,8 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
602
635
  sweep: { method: 'stable_median', rounds: SWEEP_ROUNDS, tie_break: 'previous_position_then_task_ref' },
603
636
  bounds: {
604
637
  width: nodes.length === 0 ? 0 : routeMaximumX + GEOMETRY.left,
605
- height: nodes.length === 0 ? 0 : GEOMETRY.top * 2
606
- + (layers.length - 1) * waveGap + GEOMETRY.node_height,
638
+ height: nodes.length === 0 ? 0
639
+ : GEOMETRY.top + stageTop.at(-1) + stageHeight(layers.length - 1),
607
640
  wave_count: layers.length,
608
641
  },
609
642
  nodes: projectedNodes,