Skip to content

Commit 1b95cb0

Browse files
committed
refactor: consolidate color utilities and remove CATEGORY_COLORS fallback
Move blendWithBackground, cssColorToPixi, and DEFAULT_BACKGROUND_COLOR from BucketColorResolver to ColorUtils, making ColorUtils the single module for all color conversion and blending. Make batchColors required across the rendering pipeline (resolveColor, getCulledRectangles, TemporalSegmentTree.query, legacyCullRectangles, resolveBucketSearchColor) so theme colors are always explicitly passed rather than silently falling back to hardcoded defaults. Remove unused setBatchColors methods and cached batchColors field from both TemporalSegmentTree and RectangleCache.
1 parent 2fee04a commit 1b95cb0

14 files changed

Lines changed: 292 additions & 231 deletions

log-viewer/src/features/timeline/__tests__/batching.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@
1414

1515
import type { LogCategory, LogEvent } from 'apex-log-parser';
1616
import * as PIXI from 'pixi.js';
17+
import type { BatchColorInfo } from '../optimised/BucketColorResolver.js';
1718
import { EventBatchRenderer } from '../optimised/EventBatchRenderer.js';
1819
import { RectangleCache } from '../optimised/RectangleCache.js';
1920
import type { RenderBatch, ViewportState } from '../types/flamechart.types.js';
2021
import { TIMELINE_CONSTANTS } from '../types/flamechart.types.js';
2122

23+
/** Empty batch colors — tests that don't assert color values use this. */
24+
const EMPTY_BATCH_COLORS: Map<string, BatchColorInfo> = new Map();
25+
2226
describe('EventBatchRenderer', () => {
2327
let container: PIXI.Container;
2428
let renderer: EventBatchRenderer;
@@ -75,7 +79,10 @@ describe('EventBatchRenderer', () => {
7579
rectangleManager = new RectangleCache(events, categories);
7680
renderer = new EventBatchRenderer(container, batches);
7781

78-
const { visibleRects, buckets } = rectangleManager.getCulledRectangles(viewport);
82+
const { visibleRects, buckets } = rectangleManager.getCulledRectangles(
83+
viewport,
84+
EMPTY_BATCH_COLORS,
85+
);
7986
renderer.render(visibleRects, buckets);
8087
}
8188

@@ -455,7 +462,7 @@ describe('EventBatchRenderer', () => {
455462
// Second render with different viewport (should recalculate)
456463
const viewport2 = createViewport(1, 150, 0); // Pan to cull first event
457464
const { visibleRects: visibleRects2, buckets: buckets2 } =
458-
rectangleManager.getCulledRectangles(viewport2);
465+
rectangleManager.getCulledRectangles(viewport2, EMPTY_BATCH_COLORS);
459466
renderer.render(visibleRects2, buckets2);
460467
expect(batches.get('Apex')?.rectangles).toHaveLength(1);
461468
});

log-viewer/src/features/timeline/optimised/BucketColorResolver.ts

Lines changed: 3 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,6 @@
1414
import type { CategoryStats } from '../types/flamechart.types.js';
1515
import { BUCKET_CONSTANTS } from '../types/flamechart.types.js';
1616

17-
/**
18-
* Map category names to their hex colors (numeric format).
19-
* Colors match TIMELINE_CONSTANTS.DEFAULT_COLORS but in numeric format.
20-
*
21-
* This is the single source of truth for category colors in numeric format.
22-
* All timeline code should use this via import or batchColors from theme.
23-
*/
24-
export const CATEGORY_COLORS: Record<string, number> = {
25-
Apex: 0x2b8f81, // #2B8F81
26-
'Code Unit': 0x88ae58, // #88AE58
27-
System: 0x8d6e63, // #8D6E63
28-
Automation: 0x51a16e, // #51A16E
29-
DML: 0xb06868, // #B06868
30-
SOQL: 0x6d4c7d, // #6D4C7D
31-
Callout: 0xcca033, // #CCA033
32-
Validation: 0x5c8fa6, // #5C8FA6
33-
};
34-
3517
/**
3618
* Default gray color for unknown categories.
3719
*/
@@ -72,12 +54,12 @@ export interface BatchColorInfo {
7254
* 4. Tie-break by event count (higher count wins)
7355
*
7456
* @param categoryStats - Statistics for all categories in the bucket
75-
* @param batchColors - Optional colors from RenderBatch (for theme support)
57+
* @param batchColors - Colors from RenderBatch (theme-aware category colors)
7658
* @returns Color and dominant category
7759
*/
7860
export function resolveColor(
7961
categoryStats: CategoryStats,
80-
batchColors?: Map<string, BatchColorInfo>,
62+
batchColors: Map<string, BatchColorInfo>,
8163
): ColorResolutionResult {
8264
const { byCategory } = categoryStats;
8365

@@ -119,121 +101,10 @@ export function resolveColor(
119101
}
120102
}
121103

122-
// Get color for winning category (prefer batch colors for theme support)
123-
const color =
124-
batchColors?.get(winningCategory)?.color ??
125-
CATEGORY_COLORS[winningCategory] ??
126-
UNKNOWN_CATEGORY_COLOR;
104+
const color = batchColors.get(winningCategory)?.color ?? UNKNOWN_CATEGORY_COLOR;
127105

128106
return {
129107
color,
130108
dominantCategory: winningCategory,
131109
};
132110
}
133-
134-
// ============================================================================
135-
// COLOR BLENDING UTILITIES
136-
// ============================================================================
137-
138-
/**
139-
* Default dark theme background color for alpha blending.
140-
* This is the standard VS Code dark theme background.
141-
*/
142-
const DEFAULT_BACKGROUND_COLOR = 0x1e1e1e;
143-
144-
/**
145-
* Blend a color with a background color based on opacity.
146-
* Returns an opaque color that simulates the visual appearance of
147-
* the original color at the given opacity over the background.
148-
*
149-
* Formula: result = foreground * alpha + background * (1 - alpha)
150-
*
151-
* @param foregroundColor - The foreground color (0xRRGGBB)
152-
* @param opacity - Opacity value (0 to 1)
153-
* @param backgroundColor - Background color to blend against (default: dark theme background)
154-
* @returns Opaque blended color (0xRRGGBB)
155-
*/
156-
export function blendWithBackground(
157-
foregroundColor: number,
158-
opacity: number,
159-
backgroundColor: number = DEFAULT_BACKGROUND_COLOR,
160-
): number {
161-
// Extract RGB components from foreground
162-
const fgR = (foregroundColor >> 16) & 0xff;
163-
const fgG = (foregroundColor >> 8) & 0xff;
164-
const fgB = foregroundColor & 0xff;
165-
166-
// Extract RGB components from background
167-
const bgR = (backgroundColor >> 16) & 0xff;
168-
const bgG = (backgroundColor >> 8) & 0xff;
169-
const bgB = backgroundColor & 0xff;
170-
171-
// Blend each channel: result = fg * alpha + bg * (1 - alpha)
172-
const invAlpha = 1 - opacity;
173-
const resultR = Math.round(fgR * opacity + bgR * invAlpha);
174-
const resultG = Math.round(fgG * opacity + bgG * invAlpha);
175-
const resultB = Math.round(fgB * opacity + bgB * invAlpha);
176-
177-
// Combine back to a single color value
178-
return (resultR << 16) | (resultG << 8) | resultB;
179-
}
180-
181-
/**
182-
* Parse CSS color string to PixiJS numeric color (opaque).
183-
* If the color has alpha < 1, it will be pre-blended with the background
184-
* to produce an opaque result for better GPU performance.
185-
*
186-
* Supported formats:
187-
* - #RGB (3 hex digits)
188-
* - #RGBA (4 hex digits)
189-
* - #RRGGBB (6 hex digits)
190-
* - #RRGGBBAA (8 hex digits)
191-
* - rgb(r, g, b)
192-
* - rgba(r, g, b, a)
193-
*
194-
* @param cssColor - CSS color string
195-
* @returns Opaque PixiJS numeric color (0xRRGGBB)
196-
*/
197-
export function cssColorToPixi(cssColor: string): number {
198-
let color = 0x000000;
199-
let alpha = 1;
200-
201-
if (cssColor.startsWith('#')) {
202-
const hex = cssColor.slice(1);
203-
if (hex.length === 8) {
204-
const rgb = hex.slice(0, 6);
205-
alpha = parseInt(hex.slice(6, 8), 16) / 255;
206-
color = parseInt(rgb, 16);
207-
} else if (hex.length === 6) {
208-
color = parseInt(hex, 16);
209-
} else if (hex.length === 4) {
210-
const r = hex[0]!;
211-
const g = hex[1]!;
212-
const b = hex[2]!;
213-
const a = hex[3]!;
214-
color = parseInt(r + r + g + g + b + b, 16);
215-
alpha = parseInt(a + a, 16) / 255;
216-
} else if (hex.length === 3) {
217-
const r = hex[0]!;
218-
const g = hex[1]!;
219-
const b = hex[2]!;
220-
color = parseInt(r + r + g + g + b + b, 16);
221-
}
222-
} else {
223-
const rgbMatch = cssColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d*(?:\.\d+)?))?\)/);
224-
if (rgbMatch) {
225-
const r = parseInt(rgbMatch[1] ?? '0', 10);
226-
const g = parseInt(rgbMatch[2] ?? '0', 10);
227-
const b = parseInt(rgbMatch[3] ?? '0', 10);
228-
alpha = rgbMatch[4] ? parseFloat(rgbMatch[4]) : 1;
229-
color = (r << 16) | (g << 8) | b;
230-
}
231-
}
232-
233-
// Pre-blend with background if color has alpha < 1
234-
if (alpha < 1) {
235-
return blendWithBackground(color, alpha);
236-
}
237-
238-
return color;
239-
}

log-viewer/src/features/timeline/optimised/FlameChart.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import { MeshAxisRenderer } from './time-axis/MeshAxisRenderer.js';
3232

3333
import { TextLabelRenderer } from './TextLabelRenderer.js';
3434

35-
import { cssColorToPixi } from './BucketColorResolver.js';
3635
import { HitDetector } from './interaction/HitDetector.js';
3736
import {
3837
KEYBOARD_CONSTANTS,
@@ -44,6 +43,7 @@ import { TimelineInteractionHandler } from './interaction/TimelineInteractionHan
4443
import { TimelineResizeHandler } from './interaction/TimelineResizeHandler.js';
4544
import type { MeasurementSnapshot } from './measurement/MeasurementState.js';
4645
import { RectangleCache, type PrecomputedRect } from './RectangleCache.js';
46+
import { cssColorToPixi } from './rendering/ColorUtils.js';
4747
import { CursorLineRenderer } from './rendering/CursorLineRenderer.js';
4848
import { TimelineEventIndex } from './TimelineEventIndex.js';
4949
import { TimelineViewport } from './TimelineViewport.js';

log-viewer/src/features/timeline/optimised/LegacyViewportCuller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ import { calculateViewportBounds } from './ViewportUtils.js';
3535
*
3636
* @param rectsByCategory - Spatial index of rectangles by category
3737
* @param viewport - Current viewport state
38-
* @param batchColors - Optional colors from RenderBatch (for theme support)
38+
* @param batchColors - Theme-aware category colors
3939
* @returns CulledRenderData with visible rectangles, buckets, and stats
4040
*/
4141
export function legacyCullRectangles(
4242
rectsByCategory: Map<string, PrecomputedRect[]>,
4343
viewport: ViewportState,
44-
batchColors?: Map<string, BatchColorInfo>,
44+
batchColors: Map<string, BatchColorInfo>,
4545
): CulledRenderData {
4646
const bounds = calculateViewportBounds(viewport);
4747
const visibleRects = new Map<string, PrecomputedRect[]>();

log-viewer/src/features/timeline/optimised/RectangleCache.ts

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,7 @@ export class RectangleCache {
133133
}
134134

135135
// Pass pre-grouped rectsByDepth if available (saves ~12ms grouping iteration)
136-
this.segmentTree = new TemporalSegmentTree(
137-
this.rectsByCategory,
138-
undefined, // batchColors
139-
precomputed?.rectsByDepth,
140-
);
136+
this.segmentTree = new TemporalSegmentTree(this.rectsByCategory, precomputed?.rectsByDepth);
141137
}
142138

143139
/**
@@ -148,14 +144,14 @@ export class RectangleCache {
148144
* Events <= MIN_RECT_SIZE are aggregated into time-aligned buckets.
149145
*
150146
* @param viewport - Current viewport state
151-
* @param batchColors - Optional colors from RenderBatch (for theme support)
147+
* @param batchColors - Theme-aware category colors for bucket color resolution
152148
* @returns CulledRenderData with visible rectangles, buckets, and stats
153149
*
154150
* Performance target: <5ms for 50,000 events
155151
*/
156152
public getCulledRectangles(
157153
viewport: ViewportState,
158-
batchColors?: Map<string, BatchColorInfo>,
154+
batchColors: Map<string, BatchColorInfo>,
159155
): CulledRenderData {
160156
return this.segmentTree.query(viewport, batchColors);
161157
}
@@ -189,17 +185,6 @@ export class RectangleCache {
189185
return this.rectMapById;
190186
}
191187

192-
/**
193-
* Update batch colors for segment tree (for theme changes).
194-
*
195-
* @param batchColors - New batch colors from theme
196-
*/
197-
public setBatchColors(batchColors: Map<string, BatchColorInfo>): void {
198-
if (this.segmentTree) {
199-
this.segmentTree.setBatchColors(batchColors);
200-
}
201-
}
202-
203188
/**
204189
* Get spatial index of rectangles by category.
205190
* Used for search functionality and segment tree construction.

log-viewer/src/features/timeline/optimised/TemporalSegmentTree.ts

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,7 @@ import {
4343
SEGMENT_TREE_CONSTANTS,
4444
TIMELINE_CONSTANTS,
4545
} from '../types/flamechart.types.js';
46-
import {
47-
CATEGORY_COLORS,
48-
UNKNOWN_CATEGORY_COLOR,
49-
type BatchColorInfo,
50-
} from './BucketColorResolver.js';
46+
import { UNKNOWN_CATEGORY_COLOR, type BatchColorInfo } from './BucketColorResolver.js';
5147
import type { PrecomputedRect } from './RectangleCache.js';
5248
import { calculateViewportBounds } from './ViewportUtils.js';
5349

@@ -94,9 +90,6 @@ export class TemporalSegmentTree {
9490
/** Maximum depth in the tree */
9591
private maxDepth = 0;
9692

97-
/** Cached batch colors for theme support */
98-
private batchColors?: Map<string, BatchColorInfo>;
99-
10093
/**
10194
* Unsorted frames collected during tree construction.
10295
* Sorting is deferred to first getAllFramesSorted() call.
@@ -113,38 +106,26 @@ export class TemporalSegmentTree {
113106
* Build segment trees from pre-computed rectangles.
114107
*
115108
* @param rectsByCategory - Rectangles grouped by category (from RectangleCache)
116-
* @param batchColors - Optional colors for theme support
117109
* @param rectsByDepth - Optional pre-grouped by depth (from unified conversion, saves ~12ms)
118110
*/
119111
constructor(
120112
rectsByCategory: Map<string, PrecomputedRect[]>,
121-
batchColors?: Map<string, BatchColorInfo>,
122113
rectsByDepth?: Map<number, PrecomputedRect[]>,
123114
) {
124-
this.batchColors = batchColors;
125115
this.buildTrees(rectsByCategory, rectsByDepth);
126116
}
127117

128-
/**
129-
* Update batch colors (for theme changes).
130-
*/
131-
public setBatchColors(batchColors: Map<string, BatchColorInfo>): void {
132-
this.batchColors = batchColors;
133-
// Note: We don't rebuild trees - colors are resolved at query time
134-
}
135-
136118
/**
137119
* Query the segment tree for nodes to render at current viewport.
138120
*
139121
* @param viewport - Current viewport state
140-
* @param batchColors - Optional colors from RenderBatch (for theme support)
122+
* @param batchColors - Theme-aware category colors for bucket color resolution
141123
* @returns CulledRenderData compatible with existing rendering pipeline
142124
*/
143125
public query(
144126
viewport: ViewportState,
145-
batchColors?: Map<string, BatchColorInfo>,
127+
batchColors: Map<string, BatchColorInfo>,
146128
): CulledRenderData {
147-
const effectiveBatchColors = batchColors ?? this.batchColors;
148129
const bounds = calculateViewportBounds(viewport);
149130
// T = 2px / zoom (ns) - used for both threshold check and bucket width
150131
const bucketTimeWidth = BUCKET_CONSTANTS.BUCKET_WIDTH / viewport.zoom;
@@ -162,10 +143,7 @@ export class TemporalSegmentTree {
162143
visibleRects.set(category, []);
163144
bucketsByCategory.set(category, []);
164145
// Pre-cache base color for each known category
165-
const baseColor =
166-
effectiveBatchColors?.get(category)?.color ??
167-
CATEGORY_COLORS[category] ??
168-
UNKNOWN_CATEGORY_COLOR;
146+
const baseColor = batchColors.get(category)?.color ?? UNKNOWN_CATEGORY_COLOR;
169147
categoryBaseColors.set(category, baseColor);
170148
}
171149

0 commit comments

Comments
 (0)