@ethlete/bracket 1.0.0-next.1 → 1.0.0-next.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.
- package/CHANGELOG.md +15 -0
- package/fesm2022/ethlete-bracket.mjs +230 -318
- package/fesm2022/ethlete-bracket.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ethlete-bracket.d.ts +36 -26
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// codes 3400-3499 (3400 was LAYOUT_MODE_UNSUPPORTED, retired when layouts became explicit values -
|
|
2
|
-
// an unsupported layout×mode combination no longer exists as a factory)
|
|
3
1
|
const BRACKET_ERROR_CODES = {
|
|
4
2
|
/** The source has no rounds/matches to render. */
|
|
5
3
|
SOURCE_EMPTY: 3401,
|
|
@@ -27,6 +25,8 @@ const BRACKET_ERROR_CODES = {
|
|
|
27
25
|
MISSING_MATCH_NORMALIZER: 3412,
|
|
28
26
|
/** No registered {@link BracketLayout} matches the source's tournament `mode`. */
|
|
29
27
|
LAYOUT_NOT_REGISTERED: 3413,
|
|
28
|
+
/** A cell the bracket draws has no card - neither an input, the layout, nor the config names one. */
|
|
29
|
+
CARD_NOT_REGISTERED: 3414,
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
class BracketRuntimeError extends Error {
|
|
@@ -101,6 +101,11 @@ const SWISS_BRACKET_ROUND_TYPE = {
|
|
|
101
101
|
const GROUP_BRACKET_ROUND_TYPE = {
|
|
102
102
|
GROUP: 'group',
|
|
103
103
|
};
|
|
104
|
+
const TERMINAL_ROUND_SORT_PRIORITY = {
|
|
105
|
+
[COMMON_BRACKET_ROUND_TYPE.FINAL]: 1,
|
|
106
|
+
[DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL]: 2,
|
|
107
|
+
[COMMON_BRACKET_ROUND_TYPE.THIRD_PLACE]: 3,
|
|
108
|
+
};
|
|
104
109
|
const createRoundsMapBase = (source, options) => {
|
|
105
110
|
const map = new BracketMap();
|
|
106
111
|
const shouldSplitRoundsInTwo = options.layout === BRACKET_DATA_LAYOUT.MIRRORED;
|
|
@@ -112,12 +117,7 @@ const createRoundsMapBase = (source, options) => {
|
|
|
112
117
|
// of one that has no side. Tracked per bracket half, because the two converge separately.
|
|
113
118
|
const foldClosed = { upper: false, lower: false };
|
|
114
119
|
const splitRoundsRest = [];
|
|
115
|
-
const
|
|
116
|
-
[COMMON_BRACKET_ROUND_TYPE.FINAL]: 1,
|
|
117
|
-
[DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL]: 2,
|
|
118
|
-
[COMMON_BRACKET_ROUND_TYPE.THIRD_PLACE]: 3,
|
|
119
|
-
};
|
|
120
|
-
const orderedRounds = [...source.rounds].sort((a, b) => (terminalRoundSortPriority[a.type] ?? 0) - (terminalRoundSortPriority[b.type] ?? 0));
|
|
120
|
+
const orderedRounds = [...source.rounds].sort((a, b) => (TERMINAL_ROUND_SORT_PRIORITY[a.type] ?? 0) - (TERMINAL_ROUND_SORT_PRIORITY[b.type] ?? 0));
|
|
121
121
|
for (const [roundIndex, round] of orderedRounds.entries()) {
|
|
122
122
|
const isUpperBracket = round.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET;
|
|
123
123
|
const isLowerBracket = round.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET;
|
|
@@ -213,9 +213,14 @@ const createRoundsMapBase = (source, options) => {
|
|
|
213
213
|
return map;
|
|
214
214
|
};
|
|
215
215
|
|
|
216
|
-
const createNewMatchParticipantBase = (source, participantId, match, rounds, matchRoundId, participants) => {
|
|
216
|
+
const createNewMatchParticipantBase = (source, participantId, match, rounds, matchRoundId, participants, sourceMatchesById) => {
|
|
217
217
|
if (!participantId)
|
|
218
218
|
return null;
|
|
219
|
+
const findSourceMatch = (matchId) => matchId === undefined
|
|
220
|
+
? undefined
|
|
221
|
+
: sourceMatchesById
|
|
222
|
+
? sourceMatchesById.get(matchId)
|
|
223
|
+
: source.matches.find((m) => m.id === matchId);
|
|
219
224
|
const participantBase = participants.getOrThrow(participantId);
|
|
220
225
|
const roundBase = rounds.getOrThrow(matchRoundId);
|
|
221
226
|
const matchIndex = participantBase.matchIds.indexOf(match.id);
|
|
@@ -230,7 +235,7 @@ const createNewMatchParticipantBase = (source, participantId, match, rounds, mat
|
|
|
230
235
|
let tiesTilNow = isTie ? 1 : 0;
|
|
231
236
|
for (let i = 0; i < matchIndex; i++) {
|
|
232
237
|
const previousMatchId = participantBase.matchIds[i];
|
|
233
|
-
const previousMatch =
|
|
238
|
+
const previousMatch = findSourceMatch(previousMatchId);
|
|
234
239
|
const myPreviousMatchSide = previousMatch?.home === participantId ? 'home' : previousMatch?.away === participantId ? 'away' : null;
|
|
235
240
|
if (!previousMatch || !myPreviousMatchSide)
|
|
236
241
|
continue;
|
|
@@ -250,9 +255,7 @@ const createNewMatchParticipantBase = (source, participantId, match, rounds, mat
|
|
|
250
255
|
const hasElimination = source.mode === TOURNAMENT_MODE.SINGLE_ELIMINATION ||
|
|
251
256
|
source.mode === TOURNAMENT_MODE.DOUBLE_ELIMINATION ||
|
|
252
257
|
source.mode === TOURNAMENT_MODE.SWISS_WITH_ELIMINATION;
|
|
253
|
-
// Means the current match is loss and it's the last match of the participant
|
|
254
258
|
let isEliminated = false;
|
|
255
|
-
// Always true for single elimination, never for e.g. groups, depends on the round for double elimination, depends on the loss count for swiss with elimination
|
|
256
259
|
let isEliminationMatch = false;
|
|
257
260
|
if (hasElimination) {
|
|
258
261
|
switch (source.mode) {
|
|
@@ -278,7 +281,7 @@ const createNewMatchParticipantBase = (source, participantId, match, rounds, mat
|
|
|
278
281
|
break;
|
|
279
282
|
const previousMatchIndex = currentMatchIndex - 1;
|
|
280
283
|
const previousMatchId = participantBase.matchIds[previousMatchIndex];
|
|
281
|
-
const previousMatch =
|
|
284
|
+
const previousMatch = findSourceMatch(previousMatchId);
|
|
282
285
|
if (!previousMatch)
|
|
283
286
|
throw new BracketRuntimeError(BRACKET_ERROR_CODES.MATCH_RELATION_INVALID, `Previous match with id ${previousMatchId} not found`);
|
|
284
287
|
const previousRound = rounds.getOrThrow(previousMatch.roundId);
|
|
@@ -311,6 +314,7 @@ const createNewMatchParticipantBase = (source, participantId, match, rounds, mat
|
|
|
311
314
|
|
|
312
315
|
const createMatchesMapBase = (source, rounds, participants) => {
|
|
313
316
|
const map = new BracketMap();
|
|
317
|
+
const sourceMatchesById = new Map(source.matches.map((match) => [match.id, match]));
|
|
314
318
|
for (const match of source.matches) {
|
|
315
319
|
const genericRound = rounds.get(match.roundId);
|
|
316
320
|
const splitRound = rounds.get(`${match.roundId}--half-1`);
|
|
@@ -336,8 +340,8 @@ const createMatchesMapBase = (source, rounds, participants) => {
|
|
|
336
340
|
: -1;
|
|
337
341
|
if (indexInRound === -1)
|
|
338
342
|
throw new BracketRuntimeError(BRACKET_ERROR_CODES.MATCH_RELATION_INVALID, `Match with id ${match.id} not found in round with id ${roundToUse.id}`);
|
|
339
|
-
const home = createNewMatchParticipantBase(source, match.home, match, rounds, roundToUse.id, participants);
|
|
340
|
-
const away = createNewMatchParticipantBase(source, match.away, match, rounds, roundToUse.id, participants);
|
|
343
|
+
const home = createNewMatchParticipantBase(source, match.home, match, rounds, roundToUse.id, participants, sourceMatchesById);
|
|
344
|
+
const away = createNewMatchParticipantBase(source, match.away, match, rounds, roundToUse.id, participants, sourceMatchesById);
|
|
341
345
|
const winner = match.winner === 'home' ? home : match.winner === 'away' ? away : null;
|
|
342
346
|
const matchBase = {
|
|
343
347
|
home,
|
|
@@ -365,24 +369,27 @@ const createParticipantsMapBase = (source) => {
|
|
|
365
369
|
.map((m) => [m.home, m.away])
|
|
366
370
|
.flat()
|
|
367
371
|
.filter((p) => !!p);
|
|
372
|
+
const matchIdsByParticipant = new Map();
|
|
373
|
+
for (const match of source.matches) {
|
|
374
|
+
for (const participantId of new Set([match.home, match.away])) {
|
|
375
|
+
if (!participantId)
|
|
376
|
+
continue;
|
|
377
|
+
const matchIds = matchIdsByParticipant.get(participantId) ?? [];
|
|
378
|
+
matchIds.push(match.id);
|
|
379
|
+
matchIdsByParticipant.set(participantId, matchIds);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
368
382
|
for (const [index, participantId] of participantIds.entries()) {
|
|
369
383
|
const participantBase = {
|
|
370
384
|
id: participantId,
|
|
371
385
|
shortId: `p${index}`,
|
|
372
|
-
matchIds:
|
|
373
|
-
.filter((m) => m.home === participantId || m.away === participantId)
|
|
374
|
-
.map((m) => m.id),
|
|
386
|
+
matchIds: matchIdsByParticipant.get(participantId) ?? [],
|
|
375
387
|
};
|
|
376
388
|
map.set(participantId, participantBase);
|
|
377
389
|
}
|
|
378
390
|
return map;
|
|
379
391
|
};
|
|
380
392
|
|
|
381
|
-
const TERMINAL_ROUND_SORT_PRIORITY = {
|
|
382
|
-
final: 1,
|
|
383
|
-
'reverse-final': 2,
|
|
384
|
-
'third-place': 3,
|
|
385
|
-
};
|
|
386
393
|
const sortSourceMatchesByRoundOrder = (source) => {
|
|
387
394
|
const orderedRoundIds = [...source.rounds]
|
|
388
395
|
.sort((a, b) => (TERMINAL_ROUND_SORT_PRIORITY[a.type] ?? 0) - (TERMINAL_ROUND_SORT_PRIORITY[b.type] ?? 0))
|
|
@@ -391,7 +398,26 @@ const sortSourceMatchesByRoundOrder = (source) => {
|
|
|
391
398
|
const sortedMatches = [...source.matches].sort((a, b) => (roundIndexMap.get(a.roundId) ?? 0) - (roundIndexMap.get(b.roundId) ?? 0));
|
|
392
399
|
return { ...source, matches: sortedMatches };
|
|
393
400
|
};
|
|
401
|
+
const assertUniqueIds = (options) => {
|
|
402
|
+
const seen = new Set();
|
|
403
|
+
for (const id of options.ids) {
|
|
404
|
+
if (seen.has(id)) {
|
|
405
|
+
throw new BracketRuntimeError(options.code, `${options.kind} with id ${id} exists more than once in the source.`);
|
|
406
|
+
}
|
|
407
|
+
seen.add(id);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
394
410
|
const createBracketBase = (source, options) => {
|
|
411
|
+
assertUniqueIds({
|
|
412
|
+
ids: source.rounds.map((round) => round.id),
|
|
413
|
+
code: BRACKET_ERROR_CODES.DUPLICATE_ROUND,
|
|
414
|
+
kind: 'Round',
|
|
415
|
+
});
|
|
416
|
+
assertUniqueIds({
|
|
417
|
+
ids: source.matches.map((match) => match.id),
|
|
418
|
+
code: BRACKET_ERROR_CODES.DUPLICATE_MATCH,
|
|
419
|
+
kind: 'Match',
|
|
420
|
+
});
|
|
395
421
|
const normalizedSource = sortSourceMatchesByRoundOrder(source);
|
|
396
422
|
const participants = createParticipantsMapBase(normalizedSource);
|
|
397
423
|
const rounds = createRoundsMapBase(normalizedSource, options);
|
|
@@ -421,34 +447,29 @@ const path = (d, options) => {
|
|
|
421
447
|
|
|
422
448
|
const curvePath = (from, to, direction, options) => {
|
|
423
449
|
const inverted = options.inverted ?? false;
|
|
424
|
-
// Inline/block coordinates depending on direction and inversion
|
|
425
450
|
const fromInline = inverted ? from.inline.start : from.inline.end;
|
|
426
451
|
const toInline = inverted ? to.inline.end : to.inline.start;
|
|
427
452
|
const fromBlock = from.block.center;
|
|
428
453
|
const toBlock = to.block.center;
|
|
429
|
-
// Both bends have to fit inside the block distance between the two cards, or the line
|
|
430
|
-
// target and comes back. Scaling them down instead - to nothing, for two cards on the
|
|
431
|
-
// also what keeps the six commands below identical for every connector: a CSS `d`
|
|
432
|
-
// interpolates only between two paths of the same shape.
|
|
454
|
+
// Both bends have to fit inside the block and the inline distance between the two cards, or the line
|
|
455
|
+
// turns past its target and comes back. Scaling them down instead - to nothing, for two cards on the
|
|
456
|
+
// same row - is also what keeps the six commands below identical for every connector: a CSS `d`
|
|
457
|
+
// transition interpolates only between two paths of the same shape.
|
|
433
458
|
const requestedCurve = options.lineStartingCurveAmount + options.lineEndingCurveAmount;
|
|
434
|
-
const
|
|
435
|
-
const
|
|
459
|
+
const totalInline = Math.abs(toInline - fromInline);
|
|
460
|
+
const room = Math.min(Math.abs(toBlock - fromBlock), totalInline);
|
|
461
|
+
const curveScale = requestedCurve > room ? room / requestedCurve : 1;
|
|
436
462
|
const startCurve = options.lineStartingCurveAmount * curveScale;
|
|
437
463
|
const endCurve = options.lineEndingCurveAmount * curveScale;
|
|
438
|
-
const totalInline = Math.abs(toInline - fromInline);
|
|
439
464
|
const straightLength = (totalInline - startCurve - endCurve) / 2;
|
|
440
|
-
// Calculate key points for the path
|
|
441
465
|
const straightEnd = inverted ? fromInline - straightLength : fromInline + straightLength;
|
|
442
466
|
const straightStart = inverted ? toInline + straightLength : toInline - straightLength;
|
|
443
|
-
// First curve (from start)
|
|
444
467
|
const firstCurveEndX = inverted ? straightEnd - startCurve : straightEnd + startCurve;
|
|
445
468
|
const firstCurveEndY = direction === 'down' ? fromBlock + startCurve : fromBlock - startCurve;
|
|
446
|
-
// Second curve (to end)
|
|
447
469
|
const secondCurveStartY = direction === 'down' ? toBlock - endCurve : toBlock + endCurve;
|
|
448
470
|
const secondCurveEndX = straightStart;
|
|
449
471
|
const secondCurveEndY = toBlock;
|
|
450
472
|
const secondCurveBezierX = inverted ? straightStart + endCurve : straightStart - endCurve;
|
|
451
|
-
// SVG path string
|
|
452
473
|
const d = [
|
|
453
474
|
`M ${fromInline} ${fromBlock}`,
|
|
454
475
|
`H ${straightEnd}`,
|
|
@@ -460,20 +481,14 @@ const curvePath = (from, to, direction, options) => {
|
|
|
460
481
|
return path(d, options.path);
|
|
461
482
|
};
|
|
462
483
|
|
|
463
|
-
const createBracketElementPart = (config) => {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
},
|
|
472
|
-
};
|
|
473
|
-
return {
|
|
474
|
-
elementPart: newElementPart,
|
|
475
|
-
};
|
|
476
|
-
};
|
|
484
|
+
const createBracketElementPart = (config) => ({
|
|
485
|
+
dimensions: {
|
|
486
|
+
width: 0,
|
|
487
|
+
height: config.elementPartHeight,
|
|
488
|
+
top: 0,
|
|
489
|
+
left: 0,
|
|
490
|
+
},
|
|
491
|
+
});
|
|
477
492
|
|
|
478
493
|
const createBracketElement = (config) => {
|
|
479
494
|
const { type, area, elementHeight, partHeights } = config;
|
|
@@ -532,18 +547,10 @@ const createBracketElement = (config) => {
|
|
|
532
547
|
throw new BracketRuntimeError(BRACKET_ERROR_CODES.GRID_INVALID, `Unknown element type: ${type}`);
|
|
533
548
|
}
|
|
534
549
|
})();
|
|
535
|
-
const pushPart = (...newParts) => {
|
|
536
|
-
parts.push(...newParts);
|
|
537
|
-
};
|
|
538
550
|
for (const partHeight of partHeights) {
|
|
539
|
-
|
|
540
|
-
elementPartHeight: partHeight,
|
|
541
|
-
});
|
|
542
|
-
pushPart(elementPart);
|
|
551
|
+
parts.push(createBracketElementPart({ elementPartHeight: partHeight }));
|
|
543
552
|
}
|
|
544
|
-
return
|
|
545
|
-
element: newElement,
|
|
546
|
-
};
|
|
553
|
+
return newElement;
|
|
547
554
|
};
|
|
548
555
|
|
|
549
556
|
const finalizeBracketGrid = (grid) => {
|
|
@@ -625,8 +632,7 @@ const finalizeBracketGrid = (grid) => {
|
|
|
625
632
|
|
|
626
633
|
const createBracketGrid = (config) => {
|
|
627
634
|
const masterColumns = [];
|
|
628
|
-
const
|
|
629
|
-
const spanStartLeftCache = new Map();
|
|
635
|
+
const contentWidths = new WeakMap();
|
|
630
636
|
const newGrid = {
|
|
631
637
|
dimensions: { width: 0, height: 0, top: 0, left: 0 },
|
|
632
638
|
masterColumns,
|
|
@@ -642,7 +648,9 @@ const createBracketGrid = (config) => {
|
|
|
642
648
|
const { padding } = masterColumn;
|
|
643
649
|
masterColumn.dimensions.left = currentMasterColumnLeft;
|
|
644
650
|
masterColumn.dimensions.top = 0;
|
|
645
|
-
|
|
651
|
+
const contentWidth = contentWidths.get(masterColumn) ?? masterColumn.dimensions.width;
|
|
652
|
+
contentWidths.set(masterColumn, contentWidth);
|
|
653
|
+
masterColumn.dimensions.width = contentWidth + padding.left + padding.right;
|
|
646
654
|
const sections = masterColumn.sections;
|
|
647
655
|
let runningTop = 0;
|
|
648
656
|
const firstElement = sections[0]?.subColumns[0]?.elements[0];
|
|
@@ -660,10 +668,10 @@ const createBracketGrid = (config) => {
|
|
|
660
668
|
section.dimensions.left = masterColumn.dimensions.left;
|
|
661
669
|
section.dimensions.top = runningTop;
|
|
662
670
|
runningTop += sectionPaddingTop;
|
|
663
|
-
const
|
|
671
|
+
const sectionContentWidth = masterColumn.dimensions.width - sectionPadding.left - sectionPadding.right;
|
|
664
672
|
const subColumns = section.subColumns;
|
|
665
673
|
const totalSubColumns = subColumns.length;
|
|
666
|
-
const subColumnWidth =
|
|
674
|
+
const subColumnWidth = sectionContentWidth / totalSubColumns;
|
|
667
675
|
let currentSubColumnLeft = masterColumn.dimensions.left + sectionPadding.left;
|
|
668
676
|
let maxSectionHeight = 0;
|
|
669
677
|
for (const subColumn of subColumns) {
|
|
@@ -726,8 +734,7 @@ const createBracketGrid = (config) => {
|
|
|
726
734
|
const isStartPosition = mcIdx === span.masterColumnStart && secIdx === span.sectionStart && scIdx === span.subColumnStart;
|
|
727
735
|
const spanKey = `${span.masterColumnStart}-${span.masterColumnEnd}-${span.sectionStart}-${span.sectionEnd}-${span.subColumnStart}-${span.subColumnEnd}`;
|
|
728
736
|
if (isStartPosition && !spanDimensions.has(spanKey)) {
|
|
729
|
-
const totalSpannedWidth =
|
|
730
|
-
const spanStartLeft = calculateSpanStartLeft(span, masterCols);
|
|
737
|
+
const { left: spanStartLeft, width: totalSpannedWidth } = calculateSpanExtent(span, masterCols);
|
|
731
738
|
const width = config.spanElementWidth;
|
|
732
739
|
// Winner bracket rounds span two lower bracket columns. Left-align them within that
|
|
733
740
|
// span (factor 0) instead of centering (factor 0.5) so each winner round n sits above
|
|
@@ -742,75 +749,18 @@ const createBracketGrid = (config) => {
|
|
|
742
749
|
if (storedDimensions) {
|
|
743
750
|
element.dimensions.width = storedDimensions.width;
|
|
744
751
|
element.dimensions.left = storedDimensions.left;
|
|
745
|
-
element.isHidden = !isStartPosition;
|
|
746
752
|
}
|
|
747
753
|
}
|
|
748
754
|
}
|
|
749
755
|
}
|
|
750
756
|
}
|
|
751
757
|
};
|
|
752
|
-
const
|
|
753
|
-
const
|
|
754
|
-
const
|
|
755
|
-
if (
|
|
756
|
-
return
|
|
757
|
-
|
|
758
|
-
const masterColumn = masterColumns[span.masterColumnStart];
|
|
759
|
-
if (masterColumn) {
|
|
760
|
-
const section = masterColumn.sections[span.sectionStart];
|
|
761
|
-
if (section) {
|
|
762
|
-
const subColumnWidth = section.dimensions.width / section.subColumns.length;
|
|
763
|
-
const totalWidth = subColumnWidth * (span.subColumnEnd - span.subColumnStart + 1);
|
|
764
|
-
spannedWidthCache.set(key, totalWidth);
|
|
765
|
-
return totalWidth;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
spannedWidthCache.set(key, 0);
|
|
769
|
-
return 0;
|
|
770
|
-
}
|
|
771
|
-
let totalWidth = 0;
|
|
772
|
-
for (let mcIdx = span.masterColumnStart; mcIdx <= span.masterColumnEnd; mcIdx++) {
|
|
773
|
-
const masterColumn = masterColumns[mcIdx];
|
|
774
|
-
if (!masterColumn)
|
|
775
|
-
continue;
|
|
776
|
-
if (mcIdx === span.masterColumnStart) {
|
|
777
|
-
const section = masterColumn.sections[span.sectionStart];
|
|
778
|
-
if (section) {
|
|
779
|
-
const subColumnWidth = section.dimensions.width / section.subColumns.length;
|
|
780
|
-
totalWidth += subColumnWidth * (section.subColumns.length - span.subColumnStart);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
else if (mcIdx === span.masterColumnEnd) {
|
|
784
|
-
const section = masterColumn.sections[span.sectionEnd];
|
|
785
|
-
if (section) {
|
|
786
|
-
const subColumnWidth = section.dimensions.width / section.subColumns.length;
|
|
787
|
-
totalWidth += subColumnWidth * (span.subColumnEnd + 1);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
else {
|
|
791
|
-
totalWidth += masterColumn.dimensions.width;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
spannedWidthCache.set(key, totalWidth);
|
|
795
|
-
return totalWidth;
|
|
796
|
-
};
|
|
797
|
-
const calculateSpanStartLeft = (span, masterColumns) => {
|
|
798
|
-
const key = `${span.masterColumnStart}-${span.sectionStart}-${span.subColumnStart}`;
|
|
799
|
-
const cachedLeft = spanStartLeftCache.get(key);
|
|
800
|
-
if (cachedLeft !== undefined)
|
|
801
|
-
return cachedLeft;
|
|
802
|
-
const startMasterColumn = masterColumns[span.masterColumnStart];
|
|
803
|
-
if (!startMasterColumn) {
|
|
804
|
-
spanStartLeftCache.set(key, 0);
|
|
805
|
-
return 0;
|
|
806
|
-
}
|
|
807
|
-
let startLeft = startMasterColumn.dimensions.left;
|
|
808
|
-
const section = startMasterColumn.sections[span.sectionStart];
|
|
809
|
-
if (section) {
|
|
810
|
-
startLeft += (section.dimensions.width / section.subColumns.length) * span.subColumnStart;
|
|
811
|
-
}
|
|
812
|
-
spanStartLeftCache.set(key, startLeft);
|
|
813
|
-
return startLeft;
|
|
758
|
+
const calculateSpanExtent = (span, masterColumns) => {
|
|
759
|
+
const start = masterColumns[span.masterColumnStart]?.sections[span.sectionStart]?.subColumns[span.subColumnStart];
|
|
760
|
+
const end = masterColumns[span.masterColumnEnd]?.sections[span.sectionEnd]?.subColumns[span.subColumnEnd];
|
|
761
|
+
if (!start || !end)
|
|
762
|
+
return { left: start?.dimensions.left ?? 0, width: 0 };
|
|
763
|
+
return { left: start.dimensions.left, width: end.dimensions.left + end.dimensions.width - start.dimensions.left };
|
|
814
764
|
};
|
|
815
765
|
const setupElementSpans = () => {
|
|
816
766
|
const masterCols = newGrid.masterColumns;
|
|
@@ -957,11 +907,16 @@ const createBracketContinueMasterColumn = (config) => {
|
|
|
957
907
|
throw new BracketRuntimeError(BRACKET_ERROR_CODES.GRID_INVALID, 'No last master column found in existing master columns');
|
|
958
908
|
}
|
|
959
909
|
// Dimensions are not calculated yet at this point, but all part heights are static, so the
|
|
960
|
-
// total column height is the sum of the tallest sub column of every section
|
|
961
|
-
|
|
910
|
+
// total column height is the sum of the tallest sub column of every section plus its padding - the
|
|
911
|
+
// same sum `calculateDimensions` makes, which skips the top padding of a leading header section.
|
|
912
|
+
const firstSectionIsHeader = lastMasterColumn.sections[0]?.subColumns[0]?.elements[0]?.type === 'header';
|
|
913
|
+
let totalHeight = 0;
|
|
914
|
+
for (const [sectionIndex, section] of lastMasterColumn.sections.entries()) {
|
|
915
|
+
const padding = section.padding ?? lastMasterColumn.padding;
|
|
916
|
+
const paddingTop = sectionIndex === 0 && firstSectionIsHeader ? 0 : padding.top;
|
|
962
917
|
const sectionHeight = Math.max(0, ...section.subColumns.map((subColumn) => subColumn.elements.reduce((subColumnHeight, element) => subColumnHeight + element.parts.reduce((sum, part) => sum + part.dimensions.height, 0), 0)));
|
|
963
|
-
|
|
964
|
-
}
|
|
918
|
+
totalHeight += paddingTop + sectionHeight + padding.bottom;
|
|
919
|
+
}
|
|
965
920
|
const { masterColumn, pushSection } = createBracketMasterColumn({
|
|
966
921
|
columnWidth,
|
|
967
922
|
padding: {
|
|
@@ -981,7 +936,7 @@ const createBracketContinueMasterColumn = (config) => {
|
|
|
981
936
|
},
|
|
982
937
|
});
|
|
983
938
|
if (headerOffset > 0) {
|
|
984
|
-
const
|
|
939
|
+
const headerGapElement = createBracketElement({
|
|
985
940
|
area: '.',
|
|
986
941
|
type: 'colGap',
|
|
987
942
|
elementHeight: headerOffset,
|
|
@@ -990,7 +945,7 @@ const createBracketContinueMasterColumn = (config) => {
|
|
|
990
945
|
pushElement(headerGapElement);
|
|
991
946
|
}
|
|
992
947
|
const containerHeight = Math.max(elementHeight, totalHeight - headerOffset);
|
|
993
|
-
const
|
|
948
|
+
const continueElement = createBracketElement({
|
|
994
949
|
area: 'continue',
|
|
995
950
|
type: 'continue',
|
|
996
951
|
elementHeight,
|
|
@@ -1087,7 +1042,6 @@ const drawMan = (dimensions) => {
|
|
|
1087
1042
|
.filter((id) => !!id)
|
|
1088
1043
|
.join(' ');
|
|
1089
1044
|
const currentPos = makePos(el.dimensions);
|
|
1090
|
-
// No lines for the third place match
|
|
1091
1045
|
if (el.round.type === COMMON_BRACKET_ROUND_TYPE.THIRD_PLACE)
|
|
1092
1046
|
continue;
|
|
1093
1047
|
if (continuePos && isBracketContinueMatch(el.match)) {
|
|
@@ -1100,8 +1054,7 @@ const drawMan = (dimensions) => {
|
|
|
1100
1054
|
// Crossing the middle of a fold. Every other line here is drawn by the match it flows *into*, but a
|
|
1101
1055
|
// round in the middle of a mirrored bracket is fed from both sides and its relation names only one
|
|
1102
1056
|
// of them - so the round on the way back draws its own line. Deliberately outside the switch below:
|
|
1103
|
-
// which relation a match has says nothing about which side of a fold it sits on
|
|
1104
|
-
// `two-to-one` is why a folded lower bracket used to lose the line into its centre round.
|
|
1057
|
+
// which relation a match has says nothing about which side of a fold it sits on.
|
|
1105
1058
|
if (el.round.mirrorRoundType === BRACKET_ROUND_MIRROR_TYPE.RIGHT && 'nextMatch' in el.match.relation) {
|
|
1106
1059
|
const { nextMatch, nextRound } = el.match.relation;
|
|
1107
1060
|
if (nextRound.mirrorRoundType === null) {
|
|
@@ -1118,6 +1071,7 @@ const drawMan = (dimensions) => {
|
|
|
1118
1071
|
}
|
|
1119
1072
|
}
|
|
1120
1073
|
switch (el.match.relation.type) {
|
|
1074
|
+
case 'none':
|
|
1121
1075
|
case 'nothing-to-one': {
|
|
1122
1076
|
continue;
|
|
1123
1077
|
}
|
|
@@ -1125,8 +1079,8 @@ const drawMan = (dimensions) => {
|
|
|
1125
1079
|
case 'one-to-one': {
|
|
1126
1080
|
const prev = dimensions.bracketGrid.matchElementMap.getOrThrow(el.match.relation.previousMatch.id);
|
|
1127
1081
|
const prevPos = makePos(prev.dimensions);
|
|
1128
|
-
// The winner of the match it comes from, like every other connector
|
|
1129
|
-
//
|
|
1082
|
+
// The winner of the match it comes from, like every other connector, or the journey highlight
|
|
1083
|
+
// lights the line for whoever arrived from somewhere else.
|
|
1130
1084
|
pushEdge(straightPath(prevPos, currentPos, {
|
|
1131
1085
|
path: {
|
|
1132
1086
|
...dimensions.path,
|
|
@@ -1160,7 +1114,6 @@ const drawMan = (dimensions) => {
|
|
|
1160
1114
|
}));
|
|
1161
1115
|
}
|
|
1162
1116
|
else {
|
|
1163
|
-
// draw two lines that merge into one in the middle
|
|
1164
1117
|
pushEdge(mergePath(prevUpperPos, currentPos, 'down', {
|
|
1165
1118
|
...curveOptions,
|
|
1166
1119
|
path: {
|
|
@@ -1265,7 +1218,6 @@ const factorialCache = /* @__PURE__ */ new Map();
|
|
|
1265
1218
|
const getAvailableSwissGroupsForRound = (roundNumber, totalMatchesInRound) => {
|
|
1266
1219
|
const advanceWins = SWISS_ADVANCE_WINS;
|
|
1267
1220
|
const eliminateLosses = SWISS_ELIMINATE_LOSSES;
|
|
1268
|
-
// Cache factorial calculations
|
|
1269
1221
|
const getFactorial = (n) => {
|
|
1270
1222
|
if (n <= 1)
|
|
1271
1223
|
return 1;
|
|
@@ -1276,11 +1228,9 @@ const getAvailableSwissGroupsForRound = (roundNumber, totalMatchesInRound) => {
|
|
|
1276
1228
|
factorialCache.set(n, result);
|
|
1277
1229
|
return result;
|
|
1278
1230
|
};
|
|
1279
|
-
// Pre-calculate roundFactorial
|
|
1280
1231
|
const roundFactorial = getFactorial(roundNumber);
|
|
1281
1232
|
let totalCombinations = 0;
|
|
1282
1233
|
const validGroups = [];
|
|
1283
|
-
// Single loop to gather valid groups and total combinations
|
|
1284
1234
|
for (let wins = roundNumber; wins >= 0; wins--) {
|
|
1285
1235
|
const losses = roundNumber - wins;
|
|
1286
1236
|
const remainingGames = advanceWins + eliminateLosses - (wins + losses) - 1;
|
|
@@ -1292,7 +1242,6 @@ const getAvailableSwissGroupsForRound = (roundNumber, totalMatchesInRound) => {
|
|
|
1292
1242
|
totalCombinations += combinations;
|
|
1293
1243
|
validGroups.push({ wins, losses, combinations });
|
|
1294
1244
|
}
|
|
1295
|
-
// Create final groups with calculated proportions
|
|
1296
1245
|
return validGroups.map(({ wins, losses, combinations }) => ({
|
|
1297
1246
|
id: `${wins}-${losses}`,
|
|
1298
1247
|
name: `${wins}-${losses}`,
|
|
@@ -1388,9 +1337,8 @@ const collectSwissRoundGeometries = (bracketGrid) => {
|
|
|
1388
1337
|
if (!group || !round)
|
|
1389
1338
|
continue;
|
|
1390
1339
|
const [wins, losses] = group.id.split('-').map(Number);
|
|
1391
|
-
if (wins === undefined || losses === undefined || isNaN(wins) || isNaN(losses))
|
|
1392
|
-
|
|
1393
|
-
}
|
|
1340
|
+
if (wins === undefined || losses === undefined || isNaN(wins) || isNaN(losses))
|
|
1341
|
+
continue;
|
|
1394
1342
|
const matchesTop = Math.min(...matchElements.map((element) => element.dimensions.top));
|
|
1395
1343
|
const matchesBottom = Math.max(...matchElements.map((element) => element.dimensions.top + element.dimensions.height));
|
|
1396
1344
|
const winnerShortIds = [];
|
|
@@ -1536,42 +1484,20 @@ const drawSwissMan = (dimensions) => {
|
|
|
1536
1484
|
};
|
|
1537
1485
|
|
|
1538
1486
|
const calculateUpperLowerRatio = (upperRoundsCount, lowerRoundsCount) => {
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
if (remainingUpperRounds === 0) {
|
|
1544
|
-
return 1; // Edge case: only one upper round
|
|
1545
|
-
}
|
|
1487
|
+
const remainingUpperRounds = upperRoundsCount - 1;
|
|
1488
|
+
const remainingLowerRounds = lowerRoundsCount - 1;
|
|
1489
|
+
if (remainingUpperRounds === 0)
|
|
1490
|
+
return 1;
|
|
1546
1491
|
return remainingLowerRounds / remainingUpperRounds;
|
|
1547
1492
|
};
|
|
1548
|
-
const calculateColumnSplitFactor = (upperToLowerRatio) =>
|
|
1549
|
-
// The split factor determines how many sub-columns each match column will have
|
|
1550
|
-
// For example, if the ratio is 1.5, we split each match column
|
|
1551
|
-
// into 2 sub-columns so that matches can be centered correctly
|
|
1552
|
-
if (upperToLowerRatio === 1.5) {
|
|
1553
|
-
return 2; // Split into 2 sub-columns for 1.5 ratio
|
|
1554
|
-
}
|
|
1555
|
-
if (upperToLowerRatio === 2) {
|
|
1556
|
-
return 1; // Split into 1 sub-columns for 2 ratio
|
|
1557
|
-
}
|
|
1558
|
-
return 1;
|
|
1559
|
-
};
|
|
1493
|
+
const calculateColumnSplitFactor = (upperToLowerRatio) => (upperToLowerRatio === 1.5 ? 2 : 1);
|
|
1560
1494
|
const calculateLowerRoundIndex = (subColumnIndex, splitFactor) => Math.floor(subColumnIndex / splitFactor);
|
|
1561
1495
|
// eslint-disable-next-line max-params -- index math over positional grid coordinates
|
|
1562
1496
|
const calculateUpperRoundIndex = (subColumnIndex, upperToLowerRatio, splitFactor) => {
|
|
1563
|
-
// Calculate which complete column we're in
|
|
1564
1497
|
const completeColumnIndex = Math.floor(subColumnIndex / splitFactor);
|
|
1565
|
-
|
|
1566
|
-
if (completeColumnIndex === 0) {
|
|
1498
|
+
if (completeColumnIndex === 0)
|
|
1567
1499
|
return 0;
|
|
1568
|
-
|
|
1569
|
-
// For subsequent columns, map based on the ratio
|
|
1570
|
-
// We subtract 1 because the first column is handled separately
|
|
1571
|
-
const adjustedColumnIndex = completeColumnIndex - 1;
|
|
1572
|
-
// Calculate which upper round this column should use
|
|
1573
|
-
// We add 1 because we start from the second upper round (index 1)
|
|
1574
|
-
return Math.floor(adjustedColumnIndex / upperToLowerRatio) + 1;
|
|
1500
|
+
return Math.floor((completeColumnIndex - 1) / upperToLowerRatio) + 1;
|
|
1575
1501
|
};
|
|
1576
1502
|
|
|
1577
1503
|
const createRoundBracketSubColumnRelativeToFirstRound = (config) => {
|
|
@@ -1588,7 +1514,6 @@ const createRoundBracketSubColumnRelativeToFirstRound = (config) => {
|
|
|
1588
1514
|
? Math.max(options.roundHeaderGap, options.finalRoundHeaderGap ?? 0)
|
|
1589
1515
|
: options.roundHeaderGap;
|
|
1590
1516
|
const elementsToCreate = [];
|
|
1591
|
-
// Only include a header row if headers exist
|
|
1592
1517
|
if (options.roundHeaderHeight > 0) {
|
|
1593
1518
|
elementsToCreate.push({
|
|
1594
1519
|
type: 'header',
|
|
@@ -1605,7 +1530,6 @@ const createRoundBracketSubColumnRelativeToFirstRound = (config) => {
|
|
|
1605
1530
|
elementHeight: roundHeaderGap,
|
|
1606
1531
|
});
|
|
1607
1532
|
}
|
|
1608
|
-
// Add match elements to create
|
|
1609
1533
|
for (const [matchIndex, match] of matches.entries()) {
|
|
1610
1534
|
const isLastMatch = matchIndex === matches.length - 1;
|
|
1611
1535
|
// One row per match this one is fed by. A fractional factor has no whole feeder row to align to,
|
|
@@ -1649,16 +1573,14 @@ const createRoundBracketSubColumnRelativeToFirstRound = (config) => {
|
|
|
1649
1573
|
elementHeight: config.bottomPadding,
|
|
1650
1574
|
});
|
|
1651
1575
|
}
|
|
1652
|
-
// Create all elements at once
|
|
1653
1576
|
for (const elementData of elementsToCreate) {
|
|
1654
|
-
const
|
|
1577
|
+
const element = createBracketElement(elementData);
|
|
1655
1578
|
pushElement(element);
|
|
1656
1579
|
}
|
|
1657
1580
|
return subColumn;
|
|
1658
1581
|
};
|
|
1659
1582
|
|
|
1660
1583
|
const heightOfElement = (element) => element.parts.reduce((total, part) => total + part.dimensions.height, 0);
|
|
1661
|
-
/** Read off the built rows rather than re-derived, so the two stay in step. */
|
|
1662
1584
|
const cardGeometry = (subColumn) => {
|
|
1663
1585
|
let height = 0;
|
|
1664
1586
|
let cardTop = null;
|
|
@@ -1721,6 +1643,7 @@ const createBracketGapMasterColumn = (config) => {
|
|
|
1721
1643
|
for (const section of lastMasterColumn.sections) {
|
|
1722
1644
|
const { masterColumnSection, pushSubColumn } = createBracketMasterColumnSection({
|
|
1723
1645
|
type: 'gap',
|
|
1646
|
+
padding: section.padding && { ...section.padding, left: 0, right: 0 },
|
|
1724
1647
|
});
|
|
1725
1648
|
const lastSubColumn = section.subColumns[section.subColumns.length - 1];
|
|
1726
1649
|
if (!lastSubColumn) {
|
|
@@ -1780,7 +1703,7 @@ const createBracketGapMasterColumn = (config) => {
|
|
|
1780
1703
|
partHeights,
|
|
1781
1704
|
};
|
|
1782
1705
|
})();
|
|
1783
|
-
pushElement(createBracketElement(elementToCreate)
|
|
1706
|
+
pushElement(createBracketElement(elementToCreate));
|
|
1784
1707
|
}
|
|
1785
1708
|
pushSubColumn(subColumn);
|
|
1786
1709
|
pushSection(masterColumnSection);
|
|
@@ -1798,8 +1721,8 @@ const resolveBracketGridRowSpan = (bracket, options) => {
|
|
|
1798
1721
|
const createDoubleEliminationGrid = (bracketData, options, components) => {
|
|
1799
1722
|
const resolvedOptions = resolveBracketGridRowSpan(bracketData, options);
|
|
1800
1723
|
const grid = createBracketGrid({ spanElementWidth: resolvedOptions.columnWidth });
|
|
1801
|
-
const presentUpperBracketRounds = Array.from(bracketData.roundsByType.
|
|
1802
|
-
const lowerBracketRounds = Array.from(bracketData.roundsByType.
|
|
1724
|
+
const presentUpperBracketRounds = Array.from(bracketData.roundsByType.get(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET)?.values() ?? []);
|
|
1725
|
+
const lowerBracketRounds = Array.from(bracketData.roundsByType.get(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET)?.values() ?? []);
|
|
1803
1726
|
// A complete winner bracket has (lowerRounds / 2) + 1 rounds. If fewer are present, the winner
|
|
1804
1727
|
// bracket starts later than round 1 - e.g. a small double elimination where lower-round 1 is
|
|
1805
1728
|
// seeded directly from a group phase and has no winner round feeding it. Pad the front of the
|
|
@@ -1844,7 +1767,7 @@ const createDoubleEliminationGrid = (bracketData, options, components) => {
|
|
|
1844
1767
|
firstUpperRound.matchCount * options.matchHeight +
|
|
1845
1768
|
Math.max(0, firstUpperRound.matchCount - 1) * options.rowGap;
|
|
1846
1769
|
const { subColumn, pushElement } = createBracketSubColumn({ span: config.span });
|
|
1847
|
-
const
|
|
1770
|
+
const element = createBracketElement({
|
|
1848
1771
|
area: '.',
|
|
1849
1772
|
type: 'colGap',
|
|
1850
1773
|
elementHeight: emptyUpperHeight,
|
|
@@ -1858,7 +1781,7 @@ const createDoubleEliminationGrid = (bracketData, options, components) => {
|
|
|
1858
1781
|
const { subColumn, pushElement } = createBracketSubColumn({
|
|
1859
1782
|
span: { isStart: true, isEnd: true },
|
|
1860
1783
|
});
|
|
1861
|
-
const
|
|
1784
|
+
const element = createBracketElement({
|
|
1862
1785
|
area: '.',
|
|
1863
1786
|
type: 'roundGap',
|
|
1864
1787
|
elementHeight: options.rowRoundGap,
|
|
@@ -1908,17 +1831,14 @@ const createDoubleEliminationGrid = (bracketData, options, components) => {
|
|
|
1908
1831
|
}
|
|
1909
1832
|
// May be null for a leading winner slot that does not exist (front-truncated winner bracket).
|
|
1910
1833
|
const upperRound = upperBracketRounds[currentUpperRoundIndex];
|
|
1911
|
-
// For upper bracket spans - check if this round is different from the previous occurrence
|
|
1912
1834
|
const isUpperSpanStart = isFirstSubColumnInMasterColumn
|
|
1913
1835
|
? lowerRoundIndex === 0 || lastRoundLastSubColumnUpperIndex !== currentUpperRoundIndex
|
|
1914
1836
|
: previousUpperRoundIndex !== currentUpperRoundIndex;
|
|
1915
|
-
// For upper bracket spans - check if this round will be different in the next occurrence
|
|
1916
1837
|
const isUpperSpanEnd = isLastSubColumnInMasterColumn
|
|
1917
1838
|
? isLastLowerRound ||
|
|
1918
1839
|
calculateUpperRoundIndex((lowerRoundIndex + 1) * columnSplitFactor, upperToLowerRatio, columnSplitFactor) !==
|
|
1919
1840
|
currentUpperRoundIndex
|
|
1920
1841
|
: nextUpperRoundIndex !== currentUpperRoundIndex;
|
|
1921
|
-
// For lower bracket spans - similar logic
|
|
1922
1842
|
const isLowerSpanStart = isFirstSubColumnInMasterColumn
|
|
1923
1843
|
? lowerRoundIndex === 0 || lastRoundLastSubColumnLowerIndex !== currentLowerRoundIndex
|
|
1924
1844
|
: previousLowerRoundIndex !== currentLowerRoundIndex;
|
|
@@ -2008,7 +1928,7 @@ const createDoubleEliminationGrid = (bracketData, options, components) => {
|
|
|
2008
1928
|
elementHeight: options.rowRoundGap,
|
|
2009
1929
|
partHeights: [options.rowRoundGap],
|
|
2010
1930
|
});
|
|
2011
|
-
upperLowerGapSubColumn.pushElement(upperLowerGapElement
|
|
1931
|
+
upperLowerGapSubColumn.pushElement(upperLowerGapElement);
|
|
2012
1932
|
pushUpperLowerSubColumn(upperLowerGapSubColumn.subColumn);
|
|
2013
1933
|
if (thirdPlaceRound) {
|
|
2014
1934
|
const lowerSubColumn = createRoundBracketSubColumnRelativeToFirstRound({
|
|
@@ -2047,7 +1967,7 @@ const createDoubleEliminationGrid = (bracketData, options, components) => {
|
|
|
2047
1967
|
elementHeight: element.dimensions.height,
|
|
2048
1968
|
partHeights: element.parts.map((p) => p.dimensions.height),
|
|
2049
1969
|
});
|
|
2050
|
-
lowerSubColumn.pushElement(el
|
|
1970
|
+
lowerSubColumn.pushElement(el);
|
|
2051
1971
|
}
|
|
2052
1972
|
pushLowerSubColumn(lowerSubColumn.subColumn);
|
|
2053
1973
|
}
|
|
@@ -2087,35 +2007,22 @@ const createDoubleEliminationGrid = (bracketData, options, components) => {
|
|
|
2087
2007
|
};
|
|
2088
2008
|
};
|
|
2089
2009
|
|
|
2090
|
-
/**
|
|
2091
|
-
* Where a round that never folded sits in its block's centre chain. A bracket's own rounds go first, by
|
|
2092
|
-
* depth; the rounds that decide the tournament follow in the order they are played.
|
|
2093
|
-
*/
|
|
2094
2010
|
const CENTRE_CHAIN_ORDER = {
|
|
2095
2011
|
[COMMON_BRACKET_ROUND_TYPE.FINAL]: 1,
|
|
2096
2012
|
[DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL]: 2,
|
|
2097
2013
|
[COMMON_BRACKET_ROUND_TYPE.THIRD_PLACE]: 3,
|
|
2098
2014
|
};
|
|
2099
|
-
/** The card drawn for a round's matches - the deciding one is the taller `finalMatch`. */
|
|
2100
2015
|
const isFinalMatchRound = (round, hasReverseFinal) => hasReverseFinal
|
|
2101
2016
|
? round.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL
|
|
2102
2017
|
: round.type === COMMON_BRACKET_ROUND_TYPE.FINAL;
|
|
2103
|
-
/**
|
|
2104
|
-
* A chain round takes exactly the room its cards need. Every other column reserves the same height for
|
|
2105
|
-
* every round (`createRoundBracketSubColumnRelativeToFirstRound`), which the chain cannot do - it stacks
|
|
2106
|
-
* several rounds in one column.
|
|
2107
|
-
*/
|
|
2108
2018
|
const chainRoundHeight = (config) => {
|
|
2109
2019
|
const { round, options, hasReverseFinal } = config;
|
|
2110
2020
|
const cardHeight = isFinalMatchRound(round, hasReverseFinal) ? options.finalMatchHeight : options.matchHeight;
|
|
2111
2021
|
return round.matchCount * cardHeight + Math.max(0, round.matchCount - 1) * options.rowGap;
|
|
2112
2022
|
};
|
|
2113
|
-
/**
|
|
2114
|
-
* Splits one bracket's rounds into the fold's three parts and works out the heights that keep the two
|
|
2115
|
-
* blocks aligned. `null` when the bracket has no rounds at all.
|
|
2116
|
-
*/
|
|
2117
2023
|
const createStackedBlock = (config) => {
|
|
2118
|
-
const { rounds,
|
|
2024
|
+
const { rounds, hasReverseFinal } = config;
|
|
2025
|
+
const options = resolveBracketGridRowSpan({ rounds }, config.options);
|
|
2119
2026
|
const halves = rounds.filter((round) => !!round.mirrorRoundType);
|
|
2120
2027
|
const left = halves
|
|
2121
2028
|
.filter((round) => round.mirrorRoundType === BRACKET_ROUND_MIRROR_TYPE.LEFT)
|
|
@@ -2136,7 +2043,10 @@ const createStackedBlock = (config) => {
|
|
|
2136
2043
|
const firstRound = leftSlots[0] ?? chain[0];
|
|
2137
2044
|
if (!firstRound)
|
|
2138
2045
|
return null;
|
|
2139
|
-
const
|
|
2046
|
+
const rows = options.rowSpanMatchCount ?? firstRound.matchCount;
|
|
2047
|
+
const rowsHeight = (count) => count * options.matchHeight + Math.max(0, count - 1) * options.rowGap;
|
|
2048
|
+
const content = rowsHeight(rows);
|
|
2049
|
+
const columnContent = (round) => rowsHeight(Math.max(rows, round.matchCount));
|
|
2140
2050
|
let chainHeight = 0;
|
|
2141
2051
|
for (const [index, round] of chain.entries()) {
|
|
2142
2052
|
chainHeight += (index ? options.rowRoundGap : 0) + chainRoundHeight({ round, options, hasReverseFinal });
|
|
@@ -2147,15 +2057,17 @@ const createStackedBlock = (config) => {
|
|
|
2147
2057
|
// there grows the block rather than sliding up: an anchor a few px off centre turns every one of those
|
|
2148
2058
|
// connectors into a diagonal, in a drawing where nothing else is.
|
|
2149
2059
|
const spacerTop = Math.max(0, (content - anchorHeight) / 2);
|
|
2060
|
+
const body = Math.max(spacerTop + chainHeight, ...[...leftSlots, ...right].map(columnContent));
|
|
2150
2061
|
return {
|
|
2151
2062
|
firstRound,
|
|
2152
2063
|
left: leftSlots,
|
|
2153
2064
|
right,
|
|
2154
2065
|
chain,
|
|
2155
|
-
|
|
2156
|
-
|
|
2066
|
+
options,
|
|
2067
|
+
body,
|
|
2068
|
+
columnContent,
|
|
2157
2069
|
spacerTop,
|
|
2158
|
-
spacerBottom: Math.max(0,
|
|
2070
|
+
spacerBottom: Math.max(0, body - spacerTop - chainHeight),
|
|
2159
2071
|
};
|
|
2160
2072
|
};
|
|
2161
2073
|
/**
|
|
@@ -2166,8 +2078,7 @@ const createStackedBlock = (config) => {
|
|
|
2166
2078
|
* @internal
|
|
2167
2079
|
*/
|
|
2168
2080
|
const createStackedDoubleEliminationGrid = (bracketData, options, components) => {
|
|
2169
|
-
const
|
|
2170
|
-
const grid = createBracketGrid({ spanElementWidth: resolvedOptions.columnWidth });
|
|
2081
|
+
const grid = createBracketGrid({ spanElementWidth: options.columnWidth });
|
|
2171
2082
|
const roundsOfType = (type) => Array.from(bracketData.roundsByType.get(type)?.values() ?? []);
|
|
2172
2083
|
const hasReverseFinal = !!roundsOfType(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL).length;
|
|
2173
2084
|
const winners = createStackedBlock({
|
|
@@ -2176,7 +2087,7 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
|
|
|
2176
2087
|
...roundsOfType(COMMON_BRACKET_ROUND_TYPE.FINAL),
|
|
2177
2088
|
...roundsOfType(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL),
|
|
2178
2089
|
],
|
|
2179
|
-
options
|
|
2090
|
+
options,
|
|
2180
2091
|
hasReverseFinal,
|
|
2181
2092
|
});
|
|
2182
2093
|
const losers = createStackedBlock({
|
|
@@ -2184,7 +2095,7 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
|
|
|
2184
2095
|
...roundsOfType(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET),
|
|
2185
2096
|
...roundsOfType(COMMON_BRACKET_ROUND_TYPE.THIRD_PLACE),
|
|
2186
2097
|
],
|
|
2187
|
-
options
|
|
2098
|
+
options,
|
|
2188
2099
|
hasReverseFinal,
|
|
2189
2100
|
});
|
|
2190
2101
|
if (!winners || !losers) {
|
|
@@ -2192,12 +2103,9 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
|
|
|
2192
2103
|
}
|
|
2193
2104
|
const blocks = [winners, losers];
|
|
2194
2105
|
const headerBlock = options.roundHeaderHeight > 0 ? options.roundHeaderHeight + options.roundHeaderGap : 0;
|
|
2195
|
-
const blockHeight = (block) => headerBlock + block.
|
|
2196
|
-
// Both blocks put their centre in the same master column, so the chains - and the grand final's line
|
|
2197
|
-
// down to the losers champion - run up one strip rather than two.
|
|
2106
|
+
const blockHeight = (block) => headerBlock + block.body;
|
|
2198
2107
|
const centreIndex = Math.max(...blocks.map((block) => block.left.length));
|
|
2199
2108
|
const columnCount = centreIndex + 1 + Math.max(...blocks.map((block) => block.right.length));
|
|
2200
|
-
/** A column this block does not reach into, holding the height its rounds would have taken. */
|
|
2201
2109
|
const createPlaceholderSubColumn = (block) => {
|
|
2202
2110
|
const { subColumn, pushElement } = createBracketSubColumn({
|
|
2203
2111
|
span: { isStart: true, isEnd: true },
|
|
@@ -2208,7 +2116,7 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
|
|
|
2208
2116
|
type: 'colGap',
|
|
2209
2117
|
elementHeight: height,
|
|
2210
2118
|
partHeights: [height],
|
|
2211
|
-
})
|
|
2119
|
+
}));
|
|
2212
2120
|
return subColumn;
|
|
2213
2121
|
};
|
|
2214
2122
|
const createChainSubColumn = (block) => {
|
|
@@ -2286,11 +2194,10 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
|
|
|
2286
2194
|
});
|
|
2287
2195
|
}
|
|
2288
2196
|
for (const elementData of elementsToCreate) {
|
|
2289
|
-
pushElement(createBracketElement(elementData)
|
|
2197
|
+
pushElement(createBracketElement(elementData));
|
|
2290
2198
|
}
|
|
2291
2199
|
return subColumn;
|
|
2292
2200
|
};
|
|
2293
|
-
/** What this block draws in the column at `columnIndex`, centred on the shared middle column. */
|
|
2294
2201
|
const createBlockSubColumn = (block, columnIndex) => {
|
|
2295
2202
|
const slotIndex = columnIndex - (centreIndex - block.left.length);
|
|
2296
2203
|
if (slotIndex === block.left.length)
|
|
@@ -2303,14 +2210,13 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
|
|
|
2303
2210
|
return createRoundBracketSubColumnRelativeToFirstRound({
|
|
2304
2211
|
firstRound: block.firstRound,
|
|
2305
2212
|
round,
|
|
2306
|
-
options:
|
|
2213
|
+
options: block.options,
|
|
2307
2214
|
hasReverseFinal,
|
|
2308
|
-
bottomPadding: block.
|
|
2215
|
+
bottomPadding: block.body - block.columnContent(round),
|
|
2309
2216
|
span: { isStart: true, isEnd: true },
|
|
2310
2217
|
components,
|
|
2311
2218
|
});
|
|
2312
2219
|
};
|
|
2313
|
-
/** The band the print reference leaves empty: the two blocks are independent, so nothing crosses it. */
|
|
2314
2220
|
const createBandSubColumn = () => {
|
|
2315
2221
|
const { subColumn, pushElement } = createBracketSubColumn({
|
|
2316
2222
|
span: { isStart: true, isEnd: true },
|
|
@@ -2320,7 +2226,7 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
|
|
|
2320
2226
|
type: 'roundGap',
|
|
2321
2227
|
elementHeight: options.rowRoundGap,
|
|
2322
2228
|
partHeights: [options.rowRoundGap],
|
|
2323
|
-
})
|
|
2229
|
+
}));
|
|
2324
2230
|
return subColumn;
|
|
2325
2231
|
};
|
|
2326
2232
|
for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
|
|
@@ -2343,7 +2249,6 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
|
|
|
2343
2249
|
}));
|
|
2344
2250
|
}
|
|
2345
2251
|
}
|
|
2346
|
-
// No spans: a stacked block draws one round per column, so nothing reaches across its neighbours.
|
|
2347
2252
|
grid.calculateDimensions();
|
|
2348
2253
|
const finalizedGrid = finalizeBracketGrid(grid);
|
|
2349
2254
|
return {
|
|
@@ -2451,32 +2356,45 @@ const createSingleEliminationGrid = (bracketData, options, components) => {
|
|
|
2451
2356
|
|
|
2452
2357
|
const FALLBACK_MATCH_RELATION_POSITION = -1;
|
|
2453
2358
|
const generateMatchPosition = (match, factor) => Math.ceil(match.position * factor);
|
|
2359
|
+
const generatePreviousRoundMatchPositions = (relation, match) => {
|
|
2360
|
+
const feederFactor = relation.currentRound.matchCount / relation.previousRound.matchCount;
|
|
2361
|
+
const feederPositions = [];
|
|
2362
|
+
for (const feeder of relation.previousRound.matches.values()) {
|
|
2363
|
+
if (generateMatchPosition(feeder, feederFactor) === match.position)
|
|
2364
|
+
feederPositions.push(feeder.position);
|
|
2365
|
+
}
|
|
2366
|
+
const first = feederPositions[0];
|
|
2367
|
+
const last = feederPositions[feederPositions.length - 1];
|
|
2368
|
+
if (first !== undefined && last !== undefined) {
|
|
2369
|
+
return { previousUpperRoundMatchPosition: first, previousLowerRoundMatchPosition: last };
|
|
2370
|
+
}
|
|
2371
|
+
const double = relation.previousRoundMatchFactor === 2 ? 1 : 0;
|
|
2372
|
+
return {
|
|
2373
|
+
previousUpperRoundMatchPosition: (generateMatchPosition(match, relation.previousRoundMatchFactor) -
|
|
2374
|
+
double),
|
|
2375
|
+
previousLowerRoundMatchPosition: generateMatchPosition(match, relation.previousRoundMatchFactor),
|
|
2376
|
+
};
|
|
2377
|
+
};
|
|
2454
2378
|
const generateMatchRelationPositions = (relation, match) => {
|
|
2455
2379
|
switch (relation.type) {
|
|
2380
|
+
case 'none':
|
|
2381
|
+
return null;
|
|
2456
2382
|
case 'nothing-to-one':
|
|
2457
2383
|
return {
|
|
2458
2384
|
nextRoundMatchPosition: generateMatchPosition(match, relation.nextRoundMatchFactor),
|
|
2459
2385
|
previousUpperRoundMatchPosition: FALLBACK_MATCH_RELATION_POSITION,
|
|
2460
2386
|
previousLowerRoundMatchPosition: FALLBACK_MATCH_RELATION_POSITION,
|
|
2461
2387
|
};
|
|
2462
|
-
case 'one-to-nothing':
|
|
2463
|
-
const double = relation.previousRoundMatchFactor === 2 ? 1 : 0;
|
|
2388
|
+
case 'one-to-nothing':
|
|
2464
2389
|
return {
|
|
2465
2390
|
nextRoundMatchPosition: FALLBACK_MATCH_RELATION_POSITION,
|
|
2466
|
-
|
|
2467
|
-
double),
|
|
2468
|
-
previousLowerRoundMatchPosition: generateMatchPosition(match, relation.previousRoundMatchFactor),
|
|
2391
|
+
...generatePreviousRoundMatchPositions(relation, match),
|
|
2469
2392
|
};
|
|
2470
|
-
|
|
2471
|
-
case 'one-to-one': {
|
|
2472
|
-
const double = relation.previousRoundMatchFactor === 2 ? 1 : 0;
|
|
2393
|
+
case 'one-to-one':
|
|
2473
2394
|
return {
|
|
2474
2395
|
nextRoundMatchPosition: generateMatchPosition(match, relation.nextRoundMatchFactor),
|
|
2475
|
-
|
|
2476
|
-
double),
|
|
2477
|
-
previousLowerRoundMatchPosition: generateMatchPosition(match, relation.previousRoundMatchFactor),
|
|
2396
|
+
...generatePreviousRoundMatchPositions(relation, match),
|
|
2478
2397
|
};
|
|
2479
|
-
}
|
|
2480
2398
|
case 'two-to-one':
|
|
2481
2399
|
return {
|
|
2482
2400
|
nextRoundMatchPosition: generateMatchPosition(match, relation.nextRoundMatchFactor),
|
|
@@ -2620,7 +2538,7 @@ const createTwoToNothingRelation$1 = (params) => {
|
|
|
2620
2538
|
};
|
|
2621
2539
|
const generateMatchRelationsNew = (bracketData, options = {}) => {
|
|
2622
2540
|
const sourceMatches = options.source?.matches ?? [];
|
|
2623
|
-
const hasDeclaredGraph = options.previousMatchIds !== undefined || sourceMatches.some((match) =>
|
|
2541
|
+
const hasDeclaredGraph = options.previousMatchIds !== undefined || sourceMatches.some((match) => matchOutcomeSlots(match).length > 0);
|
|
2624
2542
|
if (hasDeclaredGraph) {
|
|
2625
2543
|
return generateDeclaredMatchRelations({
|
|
2626
2544
|
bracket: bracketData,
|
|
@@ -2637,9 +2555,6 @@ const generateMatchRelationsNew = (bracketData, options = {}) => {
|
|
|
2637
2555
|
for (const match of bracketData.matches.values()) {
|
|
2638
2556
|
const relation = match.round.relation;
|
|
2639
2557
|
const positions = generateMatchRelationPositions(relation, match);
|
|
2640
|
-
// A round without a resolved relation (e.g. a truncated bracket where a round has no
|
|
2641
|
-
// successor/predecessor to wire up) keeps the placeholder relation from createBracket.
|
|
2642
|
-
// Skip it so the match renders without connector lines instead of crashing the whole bracket.
|
|
2643
2558
|
if (!positions)
|
|
2644
2559
|
continue;
|
|
2645
2560
|
const { nextRoundMatchPosition, previousUpperRoundMatchPosition, previousLowerRoundMatchPosition } = positions;
|
|
@@ -2694,9 +2609,10 @@ const generateMatchRelationsNew = (bracketData, options = {}) => {
|
|
|
2694
2609
|
}
|
|
2695
2610
|
return matchRelations;
|
|
2696
2611
|
};
|
|
2697
|
-
const
|
|
2698
|
-
|
|
2699
|
-
.
|
|
2612
|
+
const matchOutcomeSlots = (match) => [match.homeSource, match.awaySource].filter((slot) => slot?.kind === 'match-outcome' && !!slot.matchId);
|
|
2613
|
+
const defaultPreviousMatchIds = (match) => matchOutcomeSlots(match)
|
|
2614
|
+
.filter((slot) => slot.role !== 'loser')
|
|
2615
|
+
.map((slot) => slot.matchId);
|
|
2700
2616
|
const generateDeclaredMatchRelations = (options) => {
|
|
2701
2617
|
const { bracket, sourceMatches, previousMatchIds } = options;
|
|
2702
2618
|
const previousByMatchId = new Map(sourceMatches.map((match) => [match.id, previousMatchIds(match)]));
|
|
@@ -2927,43 +2843,28 @@ const handleFinalRound = (params) => {
|
|
|
2927
2843
|
firstLowerRound,
|
|
2928
2844
|
}));
|
|
2929
2845
|
}
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
throw new BracketRuntimeError(BRACKET_ERROR_CODES.ROUND_RELATION_INVALID, 'preFinalLowerRound is null');
|
|
2935
|
-
relations.push(createOneToOneRelation({
|
|
2936
|
-
currentRound: finalLowerRound,
|
|
2937
|
-
previousRound: preFinalLowerRound,
|
|
2938
|
-
nextRound: currentUpperRound,
|
|
2939
|
-
rootRound: firstLowerRound,
|
|
2940
|
-
}));
|
|
2941
|
-
if (prePreFinalLowerRound) {
|
|
2942
|
-
relations.push(createOneToOneRelation({
|
|
2943
|
-
currentRound: preFinalLowerRound,
|
|
2944
|
-
previousRound: prePreFinalLowerRound,
|
|
2945
|
-
nextRound: finalLowerRound,
|
|
2946
|
-
rootRound: firstLowerRound,
|
|
2947
|
-
}));
|
|
2948
|
-
}
|
|
2949
|
-
else {
|
|
2950
|
-
relations.push(createNothingToOneRelation({
|
|
2951
|
-
currentRound: preFinalLowerRound,
|
|
2952
|
-
nextRound: finalLowerRound,
|
|
2953
|
-
}));
|
|
2954
|
-
}
|
|
2846
|
+
const preFinalLowerRound = lowerRounds[lowerRounds.length - 2] || null;
|
|
2847
|
+
if (!preFinalLowerRound) {
|
|
2848
|
+
relations.push(createNothingToOneRelation({ currentRound: finalLowerRound, nextRound: currentUpperRound }));
|
|
2849
|
+
return;
|
|
2955
2850
|
}
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2851
|
+
relations.push(createOneToOneRelation({
|
|
2852
|
+
currentRound: finalLowerRound,
|
|
2853
|
+
previousRound: preFinalLowerRound,
|
|
2854
|
+
nextRound: currentUpperRound,
|
|
2855
|
+
rootRound: firstLowerRound,
|
|
2856
|
+
}));
|
|
2857
|
+
if (!isAsyncBracket)
|
|
2858
|
+
return;
|
|
2859
|
+
const prePreFinalLowerRound = lowerRounds[lowerRounds.length - 3] || null;
|
|
2860
|
+
relations.push(prePreFinalLowerRound
|
|
2861
|
+
? createOneToOneRelation({
|
|
2862
|
+
currentRound: preFinalLowerRound,
|
|
2863
|
+
previousRound: prePreFinalLowerRound,
|
|
2864
|
+
nextRound: finalLowerRound,
|
|
2964
2865
|
rootRound: firstLowerRound,
|
|
2965
|
-
})
|
|
2966
|
-
|
|
2866
|
+
})
|
|
2867
|
+
: createNothingToOneRelation({ currentRound: preFinalLowerRound, nextRound: finalLowerRound }));
|
|
2967
2868
|
};
|
|
2968
2869
|
const handleFirstRound = (params) => {
|
|
2969
2870
|
const { relations, currentUpperRound, nextUpperRound, lowerRounds, currentUpperRoundIndex } = params;
|
|
@@ -3137,11 +3038,9 @@ const createBracket = (source, options) => {
|
|
|
3137
3038
|
const rounds = new BracketMap();
|
|
3138
3039
|
const roundsByType = new BracketMap();
|
|
3139
3040
|
for (const roundBase of bracketNewBase.rounds.values()) {
|
|
3140
|
-
const
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
relation: { type: 'dummy' },
|
|
3144
|
-
};
|
|
3041
|
+
const relation = { type: 'none' };
|
|
3042
|
+
const newRound = { ...roundBase, matches: new BracketMap(), relation };
|
|
3043
|
+
relation.currentRound = newRound;
|
|
3145
3044
|
rounds.set(roundBase.id, newRound);
|
|
3146
3045
|
if (!roundsByType.has(roundBase.type)) {
|
|
3147
3046
|
roundsByType.set(roundBase.type, new BracketMap());
|
|
@@ -3164,14 +3063,16 @@ const createBracket = (source, options) => {
|
|
|
3164
3063
|
const awayParticipant = matchBase.away
|
|
3165
3064
|
? { ...matchBase.away, matches: new BracketMap() }
|
|
3166
3065
|
: null;
|
|
3066
|
+
const relation = { type: 'none', currentRound: round };
|
|
3167
3067
|
const newMatch = {
|
|
3168
3068
|
...matchBase,
|
|
3169
3069
|
home: homeParticipant,
|
|
3170
3070
|
away: awayParticipant,
|
|
3171
3071
|
winner: null,
|
|
3172
3072
|
round,
|
|
3173
|
-
relation
|
|
3073
|
+
relation,
|
|
3174
3074
|
};
|
|
3075
|
+
relation.currentMatch = newMatch;
|
|
3175
3076
|
if (matchBase.winner) {
|
|
3176
3077
|
const winnerParticipant = homeParticipant?.id === matchBase.winner.id ? homeParticipant : awayParticipant;
|
|
3177
3078
|
if (!winnerParticipant)
|
|
@@ -3278,26 +3179,30 @@ const resolveSlotOccupant = (options) => {
|
|
|
3278
3179
|
const resolveSlot = (options) => {
|
|
3279
3180
|
const { walk, match, side } = options;
|
|
3280
3181
|
const visitKey = `${match.id}:${side}`;
|
|
3281
|
-
|
|
3282
|
-
|
|
3182
|
+
const openDepth = walk.visitDepths.get(visitKey);
|
|
3183
|
+
if (openDepth !== undefined) {
|
|
3184
|
+
walk.shallowestCycleHit = Math.min(walk.shallowestCycleHit, openDepth);
|
|
3283
3185
|
return null;
|
|
3284
3186
|
}
|
|
3285
3187
|
const resolved = walk.resolved.get(visitKey);
|
|
3286
3188
|
if (resolved !== undefined)
|
|
3287
3189
|
return resolved;
|
|
3288
|
-
walk.
|
|
3289
|
-
const
|
|
3190
|
+
const depth = walk.visitDepths.size;
|
|
3191
|
+
const shallowestCycleHitBefore = walk.shallowestCycleHit;
|
|
3192
|
+
walk.visitDepths.set(visitKey, depth);
|
|
3193
|
+
walk.shallowestCycleHit = Infinity;
|
|
3290
3194
|
try {
|
|
3291
3195
|
const result = resolveSlotOccupant({ walk, match, side });
|
|
3292
|
-
//
|
|
3293
|
-
//
|
|
3294
|
-
// with a cycle's `null`.
|
|
3295
|
-
if (walk.
|
|
3196
|
+
// A result whose cycle guard only fired on this slot or below it is a function of the slot alone.
|
|
3197
|
+
// One that hit a slot above it depends on the path that reached it, so caching it would answer a
|
|
3198
|
+
// later path with a cycle's `null`.
|
|
3199
|
+
if (walk.shallowestCycleHit >= depth)
|
|
3296
3200
|
walk.resolved.set(visitKey, result);
|
|
3297
3201
|
return result;
|
|
3298
3202
|
}
|
|
3299
3203
|
finally {
|
|
3300
|
-
walk.
|
|
3204
|
+
walk.visitDepths.delete(visitKey);
|
|
3205
|
+
walk.shallowestCycleHit = Math.min(walk.shallowestCycleHit, shallowestCycleHitBefore);
|
|
3301
3206
|
}
|
|
3302
3207
|
};
|
|
3303
3208
|
const createResolveWalk = (options) => ({
|
|
@@ -3305,15 +3210,23 @@ const createResolveWalk = (options) => ({
|
|
|
3305
3210
|
picks: options.picks,
|
|
3306
3211
|
realParticipantOutranksPick: options.realParticipantOutranksPick ?? (() => false),
|
|
3307
3212
|
keepPickWhileFeederSideIsOpen: options.keepPickWhileFeederSideIsOpen ?? false,
|
|
3308
|
-
|
|
3213
|
+
visitDepths: new Map(),
|
|
3309
3214
|
resolved: new Map(),
|
|
3310
|
-
|
|
3215
|
+
shallowestCycleHit: Infinity,
|
|
3311
3216
|
});
|
|
3312
|
-
/**
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3217
|
+
/**
|
|
3218
|
+
* @internal A {@link resolveBracketSlot} that shares one memo across its calls. Valid only while the
|
|
3219
|
+
* picks it was created with do not change.
|
|
3220
|
+
*/
|
|
3221
|
+
const createBracketSlotResolver = (options) => {
|
|
3222
|
+
const walk = createResolveWalk(options);
|
|
3223
|
+
return (matchId, side) => {
|
|
3224
|
+
const match = options.bracket.matches.get(matchId);
|
|
3225
|
+
return match ? resolveSlot({ walk, match, side }) : null;
|
|
3226
|
+
};
|
|
3316
3227
|
};
|
|
3228
|
+
/** Who the viewer's own picks put in a slot, or `null` while their picks do not reach it. */
|
|
3229
|
+
const resolveBracketSlot = (options) => createBracketSlotResolver(options)(options.matchId, options.side);
|
|
3317
3230
|
|
|
3318
3231
|
// Grouped by type and depth rather than by round id: a mirrored layout splits one round into two
|
|
3319
3232
|
// half-rounds to draw either side of the fold, and a pick has to be able to travel across it. Ordered
|
|
@@ -3378,16 +3291,14 @@ const migrateBracketPicks = (options) => {
|
|
|
3378
3291
|
const travelling = [];
|
|
3379
3292
|
const strandedHere = new Set();
|
|
3380
3293
|
const sidesOf = (matchId) => sidesByMatchId.get(matchId) ?? [null, null];
|
|
3294
|
+
const resolve = createBracketSlotResolver({
|
|
3295
|
+
bracket,
|
|
3296
|
+
picks: settled,
|
|
3297
|
+
realParticipantOutranksPick: options.realParticipantOutranksPick,
|
|
3298
|
+
keepPickWhileFeederSideIsOpen: options.keepPickWhileFeederSideIsOpen,
|
|
3299
|
+
});
|
|
3381
3300
|
for (const match of round) {
|
|
3382
|
-
|
|
3383
|
-
bracket,
|
|
3384
|
-
picks: settled,
|
|
3385
|
-
matchId: match.id,
|
|
3386
|
-
side,
|
|
3387
|
-
realParticipantOutranksPick: options.realParticipantOutranksPick,
|
|
3388
|
-
keepPickWhileFeederSideIsOpen: options.keepPickWhileFeederSideIsOpen,
|
|
3389
|
-
});
|
|
3390
|
-
sidesByMatchId.set(match.id, [resolve('home'), resolve('away')]);
|
|
3301
|
+
sidesByMatchId.set(match.id, [resolve(match.id, 'home'), resolve(match.id, 'away')]);
|
|
3391
3302
|
}
|
|
3392
3303
|
for (const match of round) {
|
|
3393
3304
|
const pick = pickAsMade(match.id);
|
|
@@ -3522,7 +3433,7 @@ const createSwissGrid = (bracketData, options, components) => {
|
|
|
3522
3433
|
partHeights: [options.roundHeaderGap],
|
|
3523
3434
|
elementHeight: options.roundHeaderGap,
|
|
3524
3435
|
});
|
|
3525
|
-
headerSubColumn.pushElement(headerElement
|
|
3436
|
+
headerSubColumn.pushElement(headerElement, headerGapElement);
|
|
3526
3437
|
headerColumnSection.pushSubColumn(headerSubColumn.subColumn);
|
|
3527
3438
|
pushSection(headerColumnSection.masterColumnSection);
|
|
3528
3439
|
}
|
|
@@ -3564,7 +3475,7 @@ const createSwissGrid = (bracketData, options, components) => {
|
|
|
3564
3475
|
}
|
|
3565
3476
|
}
|
|
3566
3477
|
for (const elementData of elementsToCreate) {
|
|
3567
|
-
const
|
|
3478
|
+
const element = createBracketElement(elementData);
|
|
3568
3479
|
pushElement(element);
|
|
3569
3480
|
}
|
|
3570
3481
|
pushSubColumn(subColumn);
|
|
@@ -3585,7 +3496,7 @@ const createSwissGrid = (bracketData, options, components) => {
|
|
|
3585
3496
|
elementHeight: rowRoundGap,
|
|
3586
3497
|
partHeights: [rowRoundGap],
|
|
3587
3498
|
});
|
|
3588
|
-
groupGapSubColumn.pushElement(groupGapElement
|
|
3499
|
+
groupGapSubColumn.pushElement(groupGapElement);
|
|
3589
3500
|
groupGapColumnSection.pushSubColumn(groupGapSubColumn.subColumn);
|
|
3590
3501
|
pushSection(groupGapColumnSection.masterColumnSection);
|
|
3591
3502
|
}
|
|
@@ -3609,7 +3520,7 @@ const createSwissGrid = (bracketData, options, components) => {
|
|
|
3609
3520
|
elementHeight: fillerHeight,
|
|
3610
3521
|
partHeights: [fillerHeight],
|
|
3611
3522
|
});
|
|
3612
|
-
fillerSubColumn.pushElement(fillerElement
|
|
3523
|
+
fillerSubColumn.pushElement(fillerElement);
|
|
3613
3524
|
fillerColumnSection.pushSubColumn(fillerSubColumn.subColumn);
|
|
3614
3525
|
pushSection(fillerColumnSection.masterColumnSection);
|
|
3615
3526
|
}
|
|
@@ -3684,7 +3595,7 @@ const standingPickOutcome = (options) => {
|
|
|
3684
3595
|
* The order to show a group table in before anyone moves a row: every stored pick takes the position it was
|
|
3685
3596
|
* stored on, and the participants without one follow in the order the backend listed them, filling the gaps.
|
|
3686
3597
|
*
|
|
3687
|
-
* A pick the field cannot hold is dropped rather than drawn - a position
|
|
3598
|
+
* A pick the field cannot hold is dropped rather than drawn - a position that is not an integer in
|
|
3688
3599
|
* `1..participantIds.length`, an id that is not in the field, a second pick for a position or for a
|
|
3689
3600
|
* participant already placed (the lowest position keeps it). The result is therefore always a permutation
|
|
3690
3601
|
* of `participantIds`.
|
|
@@ -3695,7 +3606,8 @@ const standingPickStartOrder = (options) => {
|
|
|
3695
3606
|
const byPosition = new Map();
|
|
3696
3607
|
const placed = new Set();
|
|
3697
3608
|
for (const pick of [...picks].sort((left, right) => left.position - right.position)) {
|
|
3698
|
-
const holds = pick.position
|
|
3609
|
+
const holds = Number.isInteger(pick.position) &&
|
|
3610
|
+
pick.position >= 1 &&
|
|
3699
3611
|
pick.position <= participantIds.length &&
|
|
3700
3612
|
field.has(pick.participantId) &&
|
|
3701
3613
|
!placed.has(pick.participantId) &&
|
|
@@ -3719,5 +3631,5 @@ const standingPickStartOrder = (options) => {
|
|
|
3719
3631
|
* Generated bundle index. Do not edit.
|
|
3720
3632
|
*/
|
|
3721
3633
|
|
|
3722
|
-
export { BRACKET_DATA_LAYOUT, BRACKET_ERROR_CODES, BRACKET_ROUND_MIRROR_TYPE, BRACKET_SWISS_GROUP_COLOR_TYPE, BracketMap, COMMON_BRACKET_ROUND_TYPE, DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE, GROUP_BRACKET_ROUND_TYPE, SINGLE_ELIMINATION_BRACKET_ROUND_TYPE, STANDING_PICK_OUTCOME, SWISS_ADVANCE_WINS, SWISS_BRACKET_ROUND_TYPE, SWISS_ELIMINATE_LOSSES, TOURNAMENT_MODE, bracketGridNaturalWidth, calculateColumnSplitFactor, calculateLowerRoundIndex, calculateUpperLowerRatio, calculateUpperRoundIndex, createBracket, createBracketBase, createBracketContinueMasterColumn, createBracketElement, createBracketElementPart, createBracketGapMasterColumn, createBracketGrid, createBracketMasterColumn, createBracketMasterColumnSection, createBracketSubColumn, createDoubleEliminationGrid, createFoldedThirdPlaceSection, createMatchesMapBase, createNewMatchParticipantBase, createParticipantsMapBase, createRoundBracketSubColumnRelativeToFirstRound, createRoundsMapBase, createSingleEliminationGrid, createStackedDoubleEliminationGrid, createSwissGrid, curvePath, drawMan, drawSwissMan, finalizeBracketGrid, generateBracketRoundSwissGroupMaps, generateMatchPosition, generateMatchRelationPositions, generateMatchRelationsNew, generateRoundRelationsNew, getAvailableSwissGroupsForRound, getBracketContinueMatches, getSwissGroupColorType, gutterPath, isBracketContinueMatch, isBracketSlotPredictable, linePath, migrateBracketPicks, path, resolveBracketGridRowSpan, resolveBracketLayout, resolveBracketSlot, standingPickOutcome, standingPickStartOrder, verticalPath };
|
|
3634
|
+
export { BRACKET_DATA_LAYOUT, BRACKET_ERROR_CODES, BRACKET_ROUND_MIRROR_TYPE, BRACKET_SWISS_GROUP_COLOR_TYPE, BracketMap, COMMON_BRACKET_ROUND_TYPE, DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE, GROUP_BRACKET_ROUND_TYPE, SINGLE_ELIMINATION_BRACKET_ROUND_TYPE, STANDING_PICK_OUTCOME, SWISS_ADVANCE_WINS, SWISS_BRACKET_ROUND_TYPE, SWISS_ELIMINATE_LOSSES, TERMINAL_ROUND_SORT_PRIORITY, TOURNAMENT_MODE, bracketGridNaturalWidth, calculateColumnSplitFactor, calculateLowerRoundIndex, calculateUpperLowerRatio, calculateUpperRoundIndex, createBracket, createBracketBase, createBracketContinueMasterColumn, createBracketElement, createBracketElementPart, createBracketGapMasterColumn, createBracketGrid, createBracketMasterColumn, createBracketMasterColumnSection, createBracketSlotResolver, createBracketSubColumn, createDoubleEliminationGrid, createFoldedThirdPlaceSection, createMatchesMapBase, createNewMatchParticipantBase, createParticipantsMapBase, createRoundBracketSubColumnRelativeToFirstRound, createRoundsMapBase, createSingleEliminationGrid, createStackedDoubleEliminationGrid, createSwissGrid, curvePath, drawMan, drawSwissMan, finalizeBracketGrid, generateBracketRoundSwissGroupMaps, generateMatchPosition, generateMatchRelationPositions, generateMatchRelationsNew, generateRoundRelationsNew, getAvailableSwissGroupsForRound, getBracketContinueMatches, getSwissGroupColorType, gutterPath, isBracketContinueMatch, isBracketSlotPredictable, linePath, migrateBracketPicks, path, resolveBracketGridRowSpan, resolveBracketLayout, resolveBracketSlot, standingPickOutcome, standingPickStartOrder, verticalPath };
|
|
3723
3635
|
//# sourceMappingURL=ethlete-bracket.mjs.map
|