| 1 | /** |
| 2 | * Shared constants and SVG helpers for the snake diagram component. |
| 3 | * |
| 4 | * Layout: CSS Grid with N columns x SNAKE_GRID_ROWS rows. |
| 5 | * Each column holds one item. Even-indexed items leave row 1 open for the |
| 6 | * upper arrow, odd-indexed items leave row 5 open for the lower arrow. |
| 7 | */ |
| 8 | |
| 9 | /** Number of rows in the snake grid */ |
| 10 | export const SNAKE_GRID_ROWS = 5; |
| 11 | |
| 12 | /** Logical height of one cell in the SVG coordinate system */ |
| 13 | const SNAKE_CELL_HEIGHT = 60; |
| 14 | |
| 15 | /** Total SVG height = SNAKE_GRID_ROWS * SNAKE_CELL_HEIGHT */ |
| 16 | export const SNAKE_SVG_HEIGHT = SNAKE_GRID_ROWS * SNAKE_CELL_HEIGHT; |
| 17 | |
| 18 | /** Width allocated per column in SVG units */ |
| 19 | export const SNAKE_COL_WIDTH = 220; |
| 20 | |
| 21 | /** Fixed layout height in real pixels */ |
| 22 | export const SNAKE_LAYOUT_HEIGHT_PX = 280; |
| 23 | |
| 24 | export const SNAKE_EDGE_PADDING_X = 48; |
| 25 | const SNAKE_BASELINE_Y = SNAKE_SVG_HEIGHT / 2; |
| 26 | export const SNAKE_START_DOT_RADIUS = 8; |
| 27 | export const SNAKE_START_DOT_GAP = 18; |
| 28 | export const SNAKE_END_ARROW_LENGTH = 38; |
| 29 | const SNAKE_SEMICIRCLE_START_CUT = 0; |
| 30 | const SNAKE_ROW_GAP_Y = 24; |
| 31 | |
| 32 | /** |
| 33 | * Returns the first drawable X position for a column semicircle. |
| 34 | */ |
| 35 | export function getSnakeArrowStartX(index: number): number { |
| 36 | return index * SNAKE_COL_WIDTH + SNAKE_SEMICIRCLE_START_CUT; |
| 37 | } |
| 38 | |
| 39 | /** |
| 40 | * Returns the last drawable X position for a column semicircle. |
| 41 | */ |
| 42 | export function getSnakeArrowEndX(index: number): number { |
| 43 | return (index + 1) * SNAKE_COL_WIDTH; |
| 44 | } |
| 45 | |
| 46 | /** |
| 47 | * Returns the centerline Y position for each alternating semicircle lane. |
| 48 | */ |
| 49 | export function getSnakeArrowBaselineY(index: number): number { |
| 50 | return index % 2 === 0 |
| 51 | ? SNAKE_BASELINE_Y |
| 52 | : SNAKE_BASELINE_Y + SNAKE_ROW_GAP_Y; |
| 53 | } |
| 54 | |
| 55 | /** |
| 56 | * Total SVG width based on total number of items. |
| 57 | */ |
| 58 | export function getSvgWidth(total: number): number { |
| 59 | return Math.max(total, 1) * SNAKE_COL_WIDTH; |
| 60 | } |
| 61 | |
| 62 | /** |
| 63 | * Builds the alternating top/bottom semicircle path for a snake item. |
| 64 | */ |
| 65 | export function buildSnakeArrowPath(index: number): string { |
| 66 | const startX = getSnakeArrowStartX(index); |
| 67 | const endX = getSnakeArrowEndX(index); |
| 68 | const baselineY = getSnakeArrowBaselineY(index); |
| 69 | const radius = (endX - startX) / 2; |
| 70 | const sweepFlag = index % 2 === 0 ? 1 : 0; |
| 71 | |
| 72 | return [ |
| 73 | `M ${startX.toFixed(1)} ${baselineY.toFixed(1)}`, |
| 74 | `A ${radius.toFixed(1)} ${radius.toFixed(1)}`, |
| 75 | `0 0 ${sweepFlag}`, |
| 76 | `${endX.toFixed(1)} ${baselineY.toFixed(1)}`, |
| 77 | ].join(" "); |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * Returns CSS gridRow value for a snake item. |
| 82 | * Even items leave row 1 for the upper arrow; odd items leave row 5. |
| 83 | */ |
| 84 | export function getSnakeGridRow(index: number): string { |
| 85 | return index % 2 === 0 ? "2 / 6" : "1 / 5"; |
| 86 | } |
| 87 | |
| 88 | /** |
| 89 | * Returns CSS gridColumn value for a snake item. |
| 90 | */ |
| 91 | export function getSnakeGridColumn(index: number): string { |
| 92 | return `${index + 1}`; |
| 93 | } |
| 94 |