Skip to content

Commit ec5c1f1

Browse files
Merge pull request certinia#753 from lukecotter/chore-require-theme-colors-in-pipeline
refactor: consolidate color utilities and require theme colors in pipeline
2 parents 2fee04a + 1b95cb0 commit ec5c1f1

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)