Broke out the magic numbers

This commit is contained in:
Colin Dawson 2026-02-26 12:15:49 +00:00
parent e2f5d2dc89
commit fcd0de35ea

View File

@ -26,6 +26,33 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
Map<string, { x: number; y: number; width: number; height: number }>
>(new Map());
// Vertical spacing between stacked verdict labels in merged outcome connectors
const VERDICT_LABEL_SPACING = 3;
// SVG coordinate constants
const SVG_CENTER_POSITION = 50;
const SVG_COORDINATE_MAX = 100;
// Column layout constants
const COLUMN_MIN_WIDTH = 220;
// Outcome connector curve constants
const CURVE_BASE_OFFSET = 20;
const CURVE_OFFSET_MULTIPLIER = 0.3;
const CURVE_CONTROL_OFFSET_FACTOR = 0.3;
const CURVE_OFFSET_STEP = 15;
// Bezier curve position constants
const BEZIER_LABEL_POSITION = 0.5; // Midpoint for label placement
const BEZIER_ARROW_POSITION = 0.3; // Position along curve for directional arrow
const BEZIER_ARROW_TANGENT_DELTA = 0.01; // Small offset for tangent calculation
// Arrow dimensions
const ARROW_HALF_WIDTH = 1.5;
const DIRECTIONAL_ARROW_SIZE = 3;
const END_ARROW_OFFSET = 3;
const END_ARROW_HALF_WIDTH = 1;
// Calculate column range directly from taskColumnMap in render
const allColumns = Array.from(taskColumnMap.current.values());
const columnRange = {
@ -206,7 +233,7 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
(nodeRect.left - levelRect.left + nodeRect.width / 2) /
levelRect.width;
// Convert to SVG coordinates (0-100)
nodePositions.push(relativeCenter * 100);
nodePositions.push(relativeCenter * SVG_COORDINATE_MAX);
});
if (nodePositions.length > 0) {
@ -270,9 +297,9 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
const getColumnPercent = (column: number): number => {
const numColumns = columnRange.max - columnRange.min + 1;
if (numColumns <= 1) return 50;
if (numColumns <= 1) return SVG_CENTER_POSITION;
const gridCol = column - columnRange.min;
return ((gridCol + 0.5) / numColumns) * 100;
return ((gridCol + 0.5) / numColumns) * SVG_COORDINATE_MAX;
};
// Calculate connector positions based on grid columns
@ -280,7 +307,7 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
levelTasks: CreateWorkflowTemplateVersion["tasks"][],
): number[] => {
const numColumns = columnRange.max - columnRange.min + 1;
if (numColumns <= 1) return [50];
if (numColumns <= 1) return [SVG_CENTER_POSITION];
return levelTasks.map((task) => {
const predecessors = (task.config.predecessors as string[]) ?? [];
@ -297,13 +324,13 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
predColumns.reduce((sum, col) => sum + col, 0) / predColumns.length;
const gridCol = avgPredColumn - columnRange.min;
// Map averaged column to percentage for centered merges
return ((gridCol + 0.5) / numColumns) * 100;
return ((gridCol + 0.5) / numColumns) * SVG_COORDINATE_MAX;
}
}
const gridCol = taskColumn - columnRange.min;
// Map grid column to percentage (e.g., 2 columns: col 0 = 25%, col 1 = 75%)
return ((gridCol + 0.5) / numColumns) * 100;
return ((gridCol + 0.5) / numColumns) * SVG_COORDINATE_MAX;
});
};
@ -312,7 +339,7 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
excludeTasks?: Set<string>,
): number[] => {
const numColumns = columnRange.max - columnRange.min + 1;
if (numColumns <= 1) return [50];
if (numColumns <= 1) return [SVG_CENTER_POSITION];
const predColumns = new Set<number>();
@ -402,7 +429,7 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
/>
<polygon
className="visualiser-connector-branch-arrow"
points={`${x - 1.5},28 ${x + 1.5},28 ${x},34`}
points={`${x - ARROW_HALF_WIDTH},28 ${x + ARROW_HALF_WIDTH},28 ${x},34`}
/>
</g>
))}
@ -412,8 +439,8 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
if (count <= 1) {
// For a single child, use predecessor position if available
let startX = 50;
let endX = 50;
let startX = SVG_CENTER_POSITION;
let endX = SVG_CENTER_POSITION;
// If single child with predecessor positions, connect from the appropriate predecessor
if (positions && positions.length === 1) {
@ -475,7 +502,7 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
/>
<polygon
className="visualiser-connector-branch-arrow"
points={`${endX - 1.5},34 ${endX + 1.5},34 ${endX},40`}
points={`${endX - ARROW_HALF_WIDTH},34 ${endX + ARROW_HALF_WIDTH},34 ${endX},40`}
/>
</svg>
);
@ -487,7 +514,7 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
? positions
: Array.from(
{ length: count },
(_, index) => ((index + 1) / (count + 1)) * 100,
(_, index) => ((index + 1) / (count + 1)) * SVG_COORDINATE_MAX,
);
// Guard against empty or invalid positions
@ -516,7 +543,7 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
const topX =
predecessorPositions && predecessorPositions.length > 0
? predecessorPositions[0]
: 50;
: SVG_CENTER_POSITION;
return (
<svg
@ -550,7 +577,7 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
/>
<polygon
className="visualiser-connector-branch-arrow"
points={`${x - 1.5},28 ${x + 1.5},28 ${x},34`}
points={`${x - ARROW_HALF_WIDTH},28 ${x + ARROW_HALF_WIDTH},28 ${x},34`}
/>
</g>
))}
@ -562,7 +589,7 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
const getOutcomeConnections = (): Array<{
sourceGuid: string;
targetGuid: string;
verdict: string;
verdicts: string[];
sourceX: number;
targetX: number;
sourceY: number;
@ -576,7 +603,7 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
const connections: Array<{
sourceGuid: string;
targetGuid: string;
verdict: string;
verdicts: string[];
sourceX: number;
targetX: number;
sourceY: number;
@ -588,6 +615,21 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
curveOffset: number;
}> = [];
// First, collect all outcome connections temporarily
const tempConnections: Array<{
sourceGuid: string;
targetGuid: string;
verdict: string;
sourceX: number;
targetX: number;
sourceY: number;
targetY: number;
sourceWidth: number;
sourceHeight: number;
targetWidth: number;
targetHeight: number;
}> = [];
tasks.forEach((task) => {
const outcomes =
(task.config.outcomeActions as Array<{
@ -601,7 +643,7 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
const targetPos = taskDOMPositions.get(outcome.task);
if (sourcePos && targetPos) {
connections.push({
tempConnections.push({
sourceGuid: task.config.guid as string,
targetGuid: outcome.task,
verdict: outcome.verdict,
@ -613,13 +655,40 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
sourceHeight: sourcePos.height,
targetWidth: targetPos.width,
targetHeight: targetPos.height,
curveOffset: 0, // Will be calculated below
});
}
}
});
});
// Group by source+target pair to combine connections with same start and end
const connectionGroups = new Map<string, typeof tempConnections>();
tempConnections.forEach((conn) => {
const key = `${conn.sourceGuid}${conn.targetGuid}`;
const group = connectionGroups.get(key) ?? [];
group.push(conn);
connectionGroups.set(key, group);
});
// Merge connections with same source and target
connectionGroups.forEach((group) => {
const first = group[0];
connections.push({
sourceGuid: first.sourceGuid,
targetGuid: first.targetGuid,
verdicts: group.map((c) => c.verdict),
sourceX: first.sourceX,
sourceY: first.sourceY,
targetX: first.targetX,
targetY: first.targetY,
sourceWidth: first.sourceWidth,
sourceHeight: first.sourceHeight,
targetWidth: first.targetWidth,
targetHeight: first.targetHeight,
curveOffset: 0, // Will be calculated below
});
});
// Detect overlapping connections to the same target and offset them
const targetGroups = new Map<string, typeof connections>();
connections.forEach((conn) => {
@ -635,7 +704,7 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
group.forEach((conn, idx) => {
// Spread offsets: -10, 0, 10 for 3 connections, etc.
const totalOffsets = group.length;
const offsetStep = 15;
const offsetStep = CURVE_OFFSET_STEP;
const baseOffset = -((totalOffsets - 1) * offsetStep) / 2;
conn.curveOffset = baseOffset + idx * offsetStep;
});
@ -668,8 +737,8 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
columnRange.max > columnRange.min
? `repeat(${
columnRange.max - columnRange.min + 1
}, minmax(220px, 1fr))`
: "220px",
}, minmax(${COLUMN_MIN_WIDTH}px, 1fr))`
: `${COLUMN_MIN_WIDTH}px`,
} as React.CSSProperties
}
>
@ -858,15 +927,18 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
? conn.targetX + conn.targetWidth / 2 // right edge for rightward curves
: conn.targetX - conn.targetWidth / 2; // left edge for leftward curves
const baseControlOffsetX = 20 + Math.abs(conn.curveOffset) * 0.3;
const baseControlOffsetX =
CURVE_BASE_OFFSET +
Math.abs(conn.curveOffset) * CURVE_OFFSET_MULTIPLIER;
const controlOffsetY =
(Math.abs(conn.targetY - conn.sourceY) / 2) * 0.3;
(Math.abs(conn.targetY - conn.sourceY) / 2) *
CURVE_CONTROL_OFFSET_FACTOR;
// Apply horizontal offset to spread overlapping curves
const offsetX = conn.curveOffset;
// Calculate text position at the curve's midpoint (t=0.5 on Bezier curve)
const t = 0.5;
const t = BEZIER_LABEL_POSITION;
const p0 = { x: sourceEdgeX, y: conn.sourceY };
const p1 = {
x: sourceEdgeX + baseControlOffsetX * curveDirection + offsetX,
@ -891,7 +963,7 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
Math.pow(t, 3) * p3.y;
// Calculate arrow position at t=0.3 for directional indicator
const tArrow = 0.3;
const tArrow = BEZIER_ARROW_POSITION;
const arrowX =
Math.pow(1 - tArrow, 3) * p0.x +
3 * Math.pow(1 - tArrow, 2) * tArrow * p1.x +
@ -904,7 +976,8 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
Math.pow(tArrow, 3) * p3.y;
// Calculate tangent for arrow rotation
const tArrowDelta = 0.31;
const tArrowDelta =
BEZIER_ARROW_POSITION + BEZIER_ARROW_TANGENT_DELTA;
const arrowX2 =
Math.pow(1 - tArrowDelta, 3) * p0.x +
3 * Math.pow(1 - tArrowDelta, 2) * tArrowDelta * p1.x +
@ -934,23 +1007,32 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
{/* Directional arrow along curve */}
<polygon
className="visualiser-outcome-connector-arrow"
points={`-3,-1.5 -3,1.5 0,0`}
points={`-${DIRECTIONAL_ARROW_SIZE},-${ARROW_HALF_WIDTH} -${DIRECTIONAL_ARROW_SIZE},${ARROW_HALF_WIDTH} 0,0`}
transform={`translate(${arrowX},${arrowY}) rotate(${angle})`}
/>
{/* End arrow */}
<polygon
className="visualiser-outcome-connector-arrow"
points={`${targetEdgeX - 1},${conn.targetY - 3} ${targetEdgeX + 1},${conn.targetY - 3} ${targetEdgeX},${conn.targetY}`}
points={`${targetEdgeX - END_ARROW_HALF_WIDTH},${conn.targetY - END_ARROW_OFFSET} ${targetEdgeX + END_ARROW_HALF_WIDTH},${conn.targetY - END_ARROW_OFFSET} ${targetEdgeX},${conn.targetY}`}
/>
<text
x={textX}
y={textY}
className="visualiser-outcome-connector-label"
textAnchor="middle"
dominantBaseline="middle"
>
{conn.verdict}
</text>
{/* Render verdict labels - multiple if merged */}
{conn.verdicts.map((verdict, vIdx) => {
const labelOffsetY =
(vIdx - (conn.verdicts.length - 1) / 2) *
VERDICT_LABEL_SPACING;
return (
<text
key={`verdict-${vIdx}`}
x={textX}
y={textY + labelOffsetY}
className="visualiser-outcome-connector-label"
textAnchor="middle"
dominantBaseline="middle"
>
{verdict}
</text>
);
})}
</g>
);
})}