argent-grid 0.1.0
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/.github/workflows/pages.yml +68 -0
- package/AGENTS.md +179 -0
- package/README.md +222 -0
- package/demo-app/README.md +70 -0
- package/demo-app/angular.json +78 -0
- package/demo-app/e2e/benchmark.spec.ts +53 -0
- package/demo-app/e2e/demo-page.spec.ts +77 -0
- package/demo-app/e2e/grid-features.spec.ts +269 -0
- package/demo-app/package-lock.json +14023 -0
- package/demo-app/package.json +36 -0
- package/demo-app/playwright-test-menu.js +19 -0
- package/demo-app/playwright.config.ts +23 -0
- package/demo-app/src/app/app.component.ts +10 -0
- package/demo-app/src/app/app.config.ts +13 -0
- package/demo-app/src/app/app.routes.ts +7 -0
- package/demo-app/src/app/demo-page/demo-page.component.css +313 -0
- package/demo-app/src/app/demo-page/demo-page.component.html +124 -0
- package/demo-app/src/app/demo-page/demo-page.component.ts +366 -0
- package/demo-app/src/index.html +19 -0
- package/demo-app/src/main.ts +6 -0
- package/demo-app/tsconfig.json +31 -0
- package/ng-package.json +8 -0
- package/package.json +60 -0
- package/plan.md +131 -0
- package/setup-vitest.ts +18 -0
- package/src/lib/argent-grid.module.ts +21 -0
- package/src/lib/components/argent-grid.component.css +483 -0
- package/src/lib/components/argent-grid.component.html +320 -0
- package/src/lib/components/argent-grid.component.spec.ts +189 -0
- package/src/lib/components/argent-grid.component.ts +1188 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +92 -0
- package/src/lib/rendering/canvas-renderer.ts +962 -0
- package/src/lib/rendering/render/blit.spec.ts +453 -0
- package/src/lib/rendering/render/blit.ts +393 -0
- package/src/lib/rendering/render/cells.ts +369 -0
- package/src/lib/rendering/render/index.ts +105 -0
- package/src/lib/rendering/render/lines.ts +363 -0
- package/src/lib/rendering/render/theme.spec.ts +282 -0
- package/src/lib/rendering/render/theme.ts +201 -0
- package/src/lib/rendering/render/types.ts +279 -0
- package/src/lib/rendering/render/walk.spec.ts +360 -0
- package/src/lib/rendering/render/walk.ts +360 -0
- package/src/lib/rendering/utils/damage-tracker.spec.ts +444 -0
- package/src/lib/rendering/utils/damage-tracker.ts +423 -0
- package/src/lib/rendering/utils/index.ts +7 -0
- package/src/lib/services/grid.service.spec.ts +1039 -0
- package/src/lib/services/grid.service.ts +1284 -0
- package/src/lib/types/ag-grid-types.ts +970 -0
- package/src/public-api.ts +22 -0
- package/tsconfig.json +32 -0
- package/tsconfig.lib.json +11 -0
- package/tsconfig.spec.json +8 -0
- package/vitest.config.ts +55 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Damage Tracking for Canvas Renderer
|
|
3
|
+
*
|
|
4
|
+
* Tracks which regions of the grid need to be redrawn.
|
|
5
|
+
* Enables efficient partial redraws for better performance.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { DirtyRegions, Rectangle } from '../render/types';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// DAMAGE TRACKER CLASS
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Tracks dirty regions for partial redraws
|
|
16
|
+
*/
|
|
17
|
+
export class DamageTracker {
|
|
18
|
+
private damagedCells: Set<string> = new Set();
|
|
19
|
+
private damagedRows: Set<number> = new Set();
|
|
20
|
+
private damagedColumns: Set<number> = new Set();
|
|
21
|
+
private fullRedrawNeeded: boolean = false;
|
|
22
|
+
private lastRenderTime: number = 0;
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// MARK DIRTY METHODS
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mark a single cell as dirty
|
|
30
|
+
*/
|
|
31
|
+
markCellDirty(col: number, row: number): void {
|
|
32
|
+
this.damagedCells.add(`${col},${row}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Mark multiple cells as dirty
|
|
37
|
+
*/
|
|
38
|
+
markCellsDirty(cells: Array<[number, number]>): void {
|
|
39
|
+
for (const [col, row] of cells) {
|
|
40
|
+
this.damagedCells.add(`${col},${row}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Mark an entire row as dirty
|
|
46
|
+
*/
|
|
47
|
+
markRowDirty(row: number): void {
|
|
48
|
+
this.damagedRows.add(row);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Mark multiple rows as dirty
|
|
53
|
+
*/
|
|
54
|
+
markRowsDirty(rows: number[]): void {
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
this.damagedRows.add(row);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Mark an entire column as dirty
|
|
62
|
+
*/
|
|
63
|
+
markColumnDirty(col: number): void {
|
|
64
|
+
this.damagedColumns.add(col);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Mark multiple columns as dirty
|
|
69
|
+
*/
|
|
70
|
+
markColumnsDirty(cols: number[]): void {
|
|
71
|
+
for (const col of cols) {
|
|
72
|
+
this.damagedColumns.add(col);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Mark entire grid as dirty (requires full redraw)
|
|
78
|
+
*/
|
|
79
|
+
markAllDirty(): void {
|
|
80
|
+
this.fullRedrawNeeded = true;
|
|
81
|
+
this.damagedCells.clear();
|
|
82
|
+
this.damagedRows.clear();
|
|
83
|
+
this.damagedColumns.clear();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Mark dirty based on selection change
|
|
88
|
+
*/
|
|
89
|
+
markSelectionChanged(
|
|
90
|
+
oldSelection: Set<number>,
|
|
91
|
+
newSelection: Set<number>
|
|
92
|
+
): void {
|
|
93
|
+
// Mark rows that changed selection state
|
|
94
|
+
for (const row of oldSelection) {
|
|
95
|
+
if (!newSelection.has(row)) {
|
|
96
|
+
this.markRowDirty(row);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const row of newSelection) {
|
|
100
|
+
if (!oldSelection.has(row)) {
|
|
101
|
+
this.markRowDirty(row);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// QUERY METHODS
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if a full redraw is needed
|
|
112
|
+
*/
|
|
113
|
+
needsFullRedraw(): boolean {
|
|
114
|
+
return this.fullRedrawNeeded;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if any damage exists
|
|
119
|
+
*/
|
|
120
|
+
hasDamage(): boolean {
|
|
121
|
+
return (
|
|
122
|
+
this.fullRedrawNeeded ||
|
|
123
|
+
this.damagedCells.size > 0 ||
|
|
124
|
+
this.damagedRows.size > 0 ||
|
|
125
|
+
this.damagedColumns.size > 0
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if a specific cell is dirty
|
|
131
|
+
*/
|
|
132
|
+
isCellDirty(col: number, row: number): boolean {
|
|
133
|
+
if (this.fullRedrawNeeded) return true;
|
|
134
|
+
if (this.damagedRows.has(row)) return true;
|
|
135
|
+
if (this.damagedColumns.has(col)) return true;
|
|
136
|
+
return this.damagedCells.has(`${col},${row}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if a row is dirty
|
|
141
|
+
*/
|
|
142
|
+
isRowDirty(row: number): boolean {
|
|
143
|
+
if (this.fullRedrawNeeded) return true;
|
|
144
|
+
return this.damagedRows.has(row);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if a column is dirty
|
|
149
|
+
*/
|
|
150
|
+
isColumnDirty(col: number): boolean {
|
|
151
|
+
if (this.fullRedrawNeeded) return true;
|
|
152
|
+
return this.damagedColumns.has(col);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// GET DIRTY REGIONS
|
|
157
|
+
// ============================================================================
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get all dirty regions
|
|
161
|
+
*/
|
|
162
|
+
getDirtyRegions(): DirtyRegions {
|
|
163
|
+
return {
|
|
164
|
+
full: this.fullRedrawNeeded,
|
|
165
|
+
rows: new Set(this.damagedRows),
|
|
166
|
+
columns: new Set(this.damagedColumns),
|
|
167
|
+
cells: new Set(this.damagedCells),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get dirty rows
|
|
173
|
+
*/
|
|
174
|
+
getDirtyRows(): number[] {
|
|
175
|
+
return Array.from(this.damagedRows);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get dirty columns
|
|
180
|
+
*/
|
|
181
|
+
getDirtyColumns(): number[] {
|
|
182
|
+
return Array.from(this.damagedColumns);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get dirty cells
|
|
187
|
+
*/
|
|
188
|
+
getDirtyCells(): Array<[number, number]> {
|
|
189
|
+
return Array.from(this.damagedCells).map(key => {
|
|
190
|
+
const [col, row] = key.split(',').map(Number);
|
|
191
|
+
return [col, row];
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ============================================================================
|
|
196
|
+
// OPTIMIZATION
|
|
197
|
+
// ============================================================================
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Optimize dirty regions
|
|
201
|
+
* - If too many individual cells, promote to row/column
|
|
202
|
+
* - If too many rows/columns, promote to full redraw
|
|
203
|
+
*/
|
|
204
|
+
optimize(thresholds: {
|
|
205
|
+
maxCells?: number;
|
|
206
|
+
maxRows?: number;
|
|
207
|
+
maxColumns?: number;
|
|
208
|
+
} = {}): void {
|
|
209
|
+
const {
|
|
210
|
+
maxCells = 50,
|
|
211
|
+
maxRows = 100,
|
|
212
|
+
maxColumns = 20,
|
|
213
|
+
} = thresholds;
|
|
214
|
+
|
|
215
|
+
// If too many cells, check if we should promote to rows
|
|
216
|
+
if (this.damagedCells.size > maxCells) {
|
|
217
|
+
const rowsWithDirtyCells = new Set<number>();
|
|
218
|
+
for (const cell of this.damagedCells) {
|
|
219
|
+
const [_, row] = cell.split(',').map(Number);
|
|
220
|
+
rowsWithDirtyCells.add(row);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// If cells are concentrated in few rows, promote to those rows
|
|
224
|
+
if (rowsWithDirtyCells.size <= 10) {
|
|
225
|
+
for (const row of rowsWithDirtyCells) {
|
|
226
|
+
this.damagedRows.add(row);
|
|
227
|
+
}
|
|
228
|
+
this.damagedCells.clear();
|
|
229
|
+
} else {
|
|
230
|
+
// Too spread out, just do full redraw
|
|
231
|
+
this.markAllDirty();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// If too many rows, do full redraw
|
|
237
|
+
if (this.damagedRows.size > maxRows) {
|
|
238
|
+
this.markAllDirty();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// If too many columns, do full redraw
|
|
243
|
+
if (this.damagedColumns.size > maxColumns) {
|
|
244
|
+
this.markAllDirty();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// CLEAR
|
|
251
|
+
// ============================================================================
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Clear all damage tracking
|
|
255
|
+
*/
|
|
256
|
+
clear(): void {
|
|
257
|
+
this.damagedCells.clear();
|
|
258
|
+
this.damagedRows.clear();
|
|
259
|
+
this.damagedColumns.clear();
|
|
260
|
+
this.fullRedrawNeeded = false;
|
|
261
|
+
this.lastRenderTime = performance.now();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Reset completely
|
|
266
|
+
*/
|
|
267
|
+
reset(): void {
|
|
268
|
+
this.clear();
|
|
269
|
+
this.lastRenderTime = 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ============================================================================
|
|
273
|
+
// RENDER TIMING
|
|
274
|
+
// ============================================================================
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get time since last render
|
|
278
|
+
*/
|
|
279
|
+
getTimeSinceLastRender(): number {
|
|
280
|
+
return performance.now() - this.lastRenderTime;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get last render time
|
|
285
|
+
*/
|
|
286
|
+
getLastRenderTime(): number {
|
|
287
|
+
return this.lastRenderTime;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// HELPER FUNCTIONS
|
|
293
|
+
// ============================================================================
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Calculate bounding rectangle for dirty regions
|
|
297
|
+
*/
|
|
298
|
+
export function getDirtyBounds(
|
|
299
|
+
dirty: DirtyRegions,
|
|
300
|
+
rowHeight: number,
|
|
301
|
+
columnWidths: number[],
|
|
302
|
+
scrollTop: number,
|
|
303
|
+
scrollLeft: number,
|
|
304
|
+
viewportWidth: number,
|
|
305
|
+
viewportHeight: number
|
|
306
|
+
): Rectangle[] {
|
|
307
|
+
if (dirty.full) {
|
|
308
|
+
return [{ x: 0, y: 0, width: viewportWidth, height: viewportHeight }];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const rects: Rectangle[] = [];
|
|
312
|
+
|
|
313
|
+
// Add rectangles for dirty rows
|
|
314
|
+
for (const row of dirty.rows) {
|
|
315
|
+
const y = row * rowHeight - scrollTop;
|
|
316
|
+
if (y + rowHeight >= 0 && y <= viewportHeight) {
|
|
317
|
+
rects.push({
|
|
318
|
+
x: 0,
|
|
319
|
+
y: Math.max(0, y),
|
|
320
|
+
width: viewportWidth,
|
|
321
|
+
height: rowHeight,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Add rectangles for dirty columns
|
|
327
|
+
for (const col of dirty.columns) {
|
|
328
|
+
if (col >= 0 && col < columnWidths.length) {
|
|
329
|
+
let x = 0;
|
|
330
|
+
for (let i = 0; i < col; i++) {
|
|
331
|
+
x += columnWidths[i] || 0;
|
|
332
|
+
}
|
|
333
|
+
x -= scrollLeft;
|
|
334
|
+
|
|
335
|
+
const width = columnWidths[col] || 0;
|
|
336
|
+
|
|
337
|
+
if (x + width >= 0 && x <= viewportWidth) {
|
|
338
|
+
rects.push({
|
|
339
|
+
x: Math.max(0, x),
|
|
340
|
+
y: 0,
|
|
341
|
+
width,
|
|
342
|
+
height: viewportHeight,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Add rectangles for dirty cells
|
|
349
|
+
for (const cell of dirty.cells) {
|
|
350
|
+
const [col, row] = cell.split(',').map(Number);
|
|
351
|
+
|
|
352
|
+
// Skip if row or column already covered
|
|
353
|
+
if (dirty.rows.has(row) || dirty.columns.has(col)) continue;
|
|
354
|
+
|
|
355
|
+
const y = row * rowHeight - scrollTop;
|
|
356
|
+
let x = 0;
|
|
357
|
+
for (let i = 0; i < col; i++) {
|
|
358
|
+
x += columnWidths[i] || 0;
|
|
359
|
+
}
|
|
360
|
+
x -= scrollLeft;
|
|
361
|
+
|
|
362
|
+
const width = columnWidths[col] || 0;
|
|
363
|
+
|
|
364
|
+
if (x + width >= 0 && x <= viewportWidth &&
|
|
365
|
+
y + rowHeight >= 0 && y <= viewportHeight) {
|
|
366
|
+
rects.push({
|
|
367
|
+
x: Math.max(0, x),
|
|
368
|
+
y: Math.max(0, y),
|
|
369
|
+
width,
|
|
370
|
+
height: rowHeight,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return rects;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Merge overlapping rectangles
|
|
380
|
+
*/
|
|
381
|
+
export function mergeRectangles(rects: Rectangle[]): Rectangle[] {
|
|
382
|
+
if (rects.length <= 1) return rects;
|
|
383
|
+
|
|
384
|
+
// Simple merge - find bounding box
|
|
385
|
+
// For more complex merging, use a proper rectangle packing algorithm
|
|
386
|
+
const merged: Rectangle[] = [];
|
|
387
|
+
const used = new Set<number>();
|
|
388
|
+
|
|
389
|
+
for (let i = 0; i < rects.length; i++) {
|
|
390
|
+
if (used.has(i)) continue;
|
|
391
|
+
|
|
392
|
+
let current = rects[i];
|
|
393
|
+
|
|
394
|
+
for (let j = i + 1; j < rects.length; j++) {
|
|
395
|
+
if (used.has(j)) continue;
|
|
396
|
+
|
|
397
|
+
const other = rects[j];
|
|
398
|
+
|
|
399
|
+
// Check if they overlap or are adjacent
|
|
400
|
+
if (
|
|
401
|
+
current.x <= other.x + other.width &&
|
|
402
|
+
current.x + current.width >= other.x &&
|
|
403
|
+
current.y <= other.y + other.height &&
|
|
404
|
+
current.y + current.height >= other.y
|
|
405
|
+
) {
|
|
406
|
+
// Merge
|
|
407
|
+
const newX = Math.min(current.x, other.x);
|
|
408
|
+
const newY = Math.min(current.y, other.y);
|
|
409
|
+
current = {
|
|
410
|
+
x: newX,
|
|
411
|
+
y: newY,
|
|
412
|
+
width: Math.max(current.x + current.width, other.x + other.width) - newX,
|
|
413
|
+
height: Math.max(current.y + current.height, other.y + other.height) - newY,
|
|
414
|
+
};
|
|
415
|
+
used.add(j);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
merged.push(current);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return merged;
|
|
423
|
+
}
|