@veltra/sheet 2.4.0 → 2.5.1

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.
Files changed (147) hide show
  1. package/dist/components/sheet/formula-bar.js +50 -7
  2. package/dist/components/sheet/style2.css +2 -2
  3. package/dist/index.d.ts +2 -19
  4. package/dist/index.js +2 -15
  5. package/dist/types/index.d.ts +1 -3
  6. package/package.json +5 -6
  7. package/src/components/sheet/__test__/formula-bar.test.ts +29 -0
  8. package/src/components/sheet/formula-bar.vue +67 -6
  9. package/src/components/sheet/style.scss +1 -1
  10. package/src/index.ts +1 -27
  11. package/src/types/index.ts +0 -2
  12. package/dist/components/report/binding-badge.js +0 -118
  13. package/dist/components/report/designer/cell-coords.js +0 -46
  14. package/dist/components/report/designer/col-widths.js +0 -29
  15. package/dist/components/report/designer/conditional-rules/helpers.js +0 -139
  16. package/dist/components/report/designer/drill-dialog.js +0 -258
  17. package/dist/components/report/designer/drop-highlight-overlay.js +0 -76
  18. package/dist/components/report/designer/float-panel.js +0 -434
  19. package/dist/components/report/designer/role.js +0 -17
  20. package/dist/components/report/designer/rule-preview.js +0 -33
  21. package/dist/components/report/designer/rule-row.js +0 -253
  22. package/dist/components/report/designer/rules-dialog.js +0 -118
  23. package/dist/components/report/designer/topology-overlay.js +0 -122
  24. package/dist/components/report/designer/topology.js +0 -64
  25. package/dist/components/report/designer/use-grid-overlay.js +0 -75
  26. package/dist/components/report/designer-hub.js +0 -177
  27. package/dist/components/report/field-panel-helpers.js +0 -33
  28. package/dist/components/report/field-panel.js +0 -132
  29. package/dist/components/report/filter-bar.js +0 -74
  30. package/dist/components/report/hub-connection-form.js +0 -145
  31. package/dist/components/report/hub-dataset-editor.js +0 -409
  32. package/dist/components/report/preview-grid-size.js +0 -32
  33. package/dist/components/report/print.js +0 -39
  34. package/dist/components/report/report-designer.js +0 -428
  35. package/dist/components/report/report-viewer.js +0 -253
  36. package/dist/components/report/style.d.ts +0 -17
  37. package/dist/components/report/style.js +0 -18
  38. package/dist/components/report/style2.css +0 -965
  39. package/dist/components/report/use-report-designer.js +0 -387
  40. package/dist/components/report/use-report-viewer.js +0 -140
  41. package/dist/report/binding.d.ts +0 -83
  42. package/dist/report/binding.js +0 -334
  43. package/dist/report/connector.d.ts +0 -66
  44. package/dist/report/connector.js +0 -159
  45. package/dist/report/drill.d.ts +0 -48
  46. package/dist/report/drill.js +0 -78
  47. package/dist/report/export-xlsx.d.ts +0 -13
  48. package/dist/report/export-xlsx.js +0 -13
  49. package/dist/report/filter-bar.d.ts +0 -10
  50. package/dist/report/filter-bar.js +0 -31
  51. package/dist/report/index.d.ts +0 -14
  52. package/dist/report/params.d.ts +0 -13
  53. package/dist/report/params.js +0 -61
  54. package/dist/report/print.d.ts +0 -23
  55. package/dist/report/print.js +0 -197
  56. package/dist/report/render/aggregate.js +0 -193
  57. package/dist/report/render/builder.d.ts +0 -12
  58. package/dist/report/render/builder.js +0 -139
  59. package/dist/report/render/coordinate.d.ts +0 -25
  60. package/dist/report/render/coordinate.js +0 -733
  61. package/dist/report/render/index.d.ts +0 -14
  62. package/dist/report/render/index.js +0 -21
  63. package/dist/report/render/style-resolver.js +0 -50
  64. package/dist/report/render/template-index.d.ts +0 -24
  65. package/dist/report/render/template-index.js +0 -137
  66. package/dist/report/rules/evaluate.d.ts +0 -25
  67. package/dist/report/rules/evaluate.js +0 -82
  68. package/dist/report/rules/index.d.ts +0 -2
  69. package/dist/report/template.d.ts +0 -63
  70. package/dist/report/template.js +0 -83
  71. package/dist/report/types.d.ts +0 -103
  72. package/dist/types/report-designer.d.ts +0 -51
  73. package/dist/types/report-viewer.d.ts +0 -57
  74. package/src/components/report/__test__/binding-badge.test.ts +0 -92
  75. package/src/components/report/__test__/conditional-rules-helpers.test.ts +0 -88
  76. package/src/components/report/__test__/field-panel-helpers.test.ts +0 -65
  77. package/src/components/report/__test__/float-panel-position.test.ts +0 -45
  78. package/src/components/report/__test__/preview-grid-size.test.ts +0 -63
  79. package/src/components/report/__test__/report-designer.test.ts +0 -559
  80. package/src/components/report/__test__/report-drill.test.ts +0 -692
  81. package/src/components/report/__test__/report-viewer.test.ts +0 -400
  82. package/src/components/report/__test__/topology.test.ts +0 -143
  83. package/src/components/report/__test__/use-report-designer.test.ts +0 -804
  84. package/src/components/report/binding-badge.ts +0 -122
  85. package/src/components/report/designer/cell-coords.ts +0 -95
  86. package/src/components/report/designer/col-widths.ts +0 -56
  87. package/src/components/report/designer/conditional-rules/helpers.ts +0 -189
  88. package/src/components/report/designer/drill-dialog.vue +0 -245
  89. package/src/components/report/designer/drop-highlight-overlay.vue +0 -75
  90. package/src/components/report/designer/float-panel.vue +0 -444
  91. package/src/components/report/designer/role.ts +0 -32
  92. package/src/components/report/designer/rule-preview.vue +0 -40
  93. package/src/components/report/designer/rule-row.vue +0 -252
  94. package/src/components/report/designer/rules-dialog.vue +0 -110
  95. package/src/components/report/designer/topology-overlay.vue +0 -128
  96. package/src/components/report/designer/topology.ts +0 -124
  97. package/src/components/report/designer/use-grid-overlay.ts +0 -114
  98. package/src/components/report/designer-hub.vue +0 -198
  99. package/src/components/report/field-panel-helpers.ts +0 -44
  100. package/src/components/report/field-panel.vue +0 -136
  101. package/src/components/report/filter-bar.vue +0 -77
  102. package/src/components/report/hub-connection-form.vue +0 -124
  103. package/src/components/report/hub-dataset-editor.vue +0 -387
  104. package/src/components/report/index.ts +0 -2
  105. package/src/components/report/preview-grid-size.ts +0 -33
  106. package/src/components/report/print.ts +0 -44
  107. package/src/components/report/report-designer.vue +0 -454
  108. package/src/components/report/report-viewer.vue +0 -289
  109. package/src/components/report/style-designer.scss +0 -653
  110. package/src/components/report/style-hub.scss +0 -292
  111. package/src/components/report/style-viewer.scss +0 -144
  112. package/src/components/report/style.scss +0 -3
  113. package/src/components/report/style.ts +0 -20
  114. package/src/components/report/use-report-designer.ts +0 -603
  115. package/src/components/report/use-report-viewer.ts +0 -227
  116. package/src/report/__test__/binding.test.ts +0 -320
  117. package/src/report/__test__/connector.test.ts +0 -239
  118. package/src/report/__test__/drill.test.ts +0 -194
  119. package/src/report/__test__/export-xlsx.test.ts +0 -96
  120. package/src/report/__test__/filter-bar.test.ts +0 -58
  121. package/src/report/__test__/params.test.ts +0 -156
  122. package/src/report/__test__/print.test.ts +0 -170
  123. package/src/report/__test__/render.test.ts +0 -856
  124. package/src/report/__test__/rules.test.ts +0 -131
  125. package/src/report/__test__/template.test.ts +0 -366
  126. package/src/report/binding.ts +0 -474
  127. package/src/report/connector.ts +0 -206
  128. package/src/report/drill.ts +0 -112
  129. package/src/report/export-xlsx.ts +0 -15
  130. package/src/report/filter-bar.ts +0 -36
  131. package/src/report/index.ts +0 -16
  132. package/src/report/params.ts +0 -96
  133. package/src/report/print.ts +0 -234
  134. package/src/report/render/__test__/aggregate.test.ts +0 -16
  135. package/src/report/render/__test__/coordinate.test.ts +0 -405
  136. package/src/report/render/aggregate.ts +0 -289
  137. package/src/report/render/builder.ts +0 -211
  138. package/src/report/render/coordinate.ts +0 -1239
  139. package/src/report/render/index.ts +0 -30
  140. package/src/report/render/style-resolver.ts +0 -68
  141. package/src/report/render/template-index.ts +0 -200
  142. package/src/report/rules/evaluate.ts +0 -113
  143. package/src/report/rules/index.ts +0 -7
  144. package/src/report/template.ts +0 -145
  145. package/src/report/types.ts +0 -116
  146. package/src/types/report-designer.ts +0 -55
  147. package/src/types/report-viewer.ts +0 -54
@@ -1,1239 +0,0 @@
1
- import type { CellAddress, CellRange } from '@veltra/sheet-core'
2
- import { cellKey, createRange } from '@veltra/sheet-core'
3
-
4
- import type { DatasetRecords, ReportBinding } from '../types'
5
- import { sameAddress, type TemplateIndex } from './template-index'
6
-
7
- /** 扩展实例:携带祖先父格过滤条件 */
8
- export interface ExpansionInstance {
9
- index: number
10
- /** `group` 聚合时的分组值 */
11
- value?: unknown
12
- /** 同数据集内生效的字段过滤 */
13
- filter: Record<string, unknown>
14
- }
15
-
16
- /** 逻辑格 → 物理区间映射项(纯几何,不含取值) */
17
- export interface PhysicalPlacement {
18
- logical: CellAddress
19
- physical: CellRange
20
- rowPath: readonly number[]
21
- colPath: readonly number[]
22
- mergeSpan: boolean
23
- binding?: ReportBinding
24
- /** `list` 明细实例下标;非 list 时为 0 */
25
- listIndex: number
26
- }
27
-
28
- /** 扩展坐标系计算结果 */
29
- export interface ExpansionLayout {
30
- rowCount: number
31
- colCount: number
32
- placements: PhysicalPlacement[]
33
- /** 模板合并区域映射到物理网格后的区间 */
34
- mappedMerges: CellRange[]
35
- }
36
-
37
- function isRowExpanding(binding: ReportBinding): boolean {
38
- return (
39
- binding.expand === 'down' && (binding.aggregate === 'group' || binding.aggregate === 'list')
40
- )
41
- }
42
-
43
- function isColExpanding(binding: ReportBinding): boolean {
44
- return (
45
- binding.expand === 'right' && (binding.aggregate === 'group' || binding.aggregate === 'list')
46
- )
47
- }
48
-
49
- function mergeSpanEnabled(binding: ReportBinding | undefined): boolean {
50
- return binding?.mergeSpan !== false
51
- }
52
-
53
- function filterRows(
54
- rows: Record<string, unknown>[],
55
- filter: Record<string, unknown>
56
- ): Record<string, unknown>[] {
57
- if (Object.keys(filter).length === 0) return rows
58
- return rows.filter((row) =>
59
- Object.entries(filter).every(([field, value]) => row[field] === value)
60
- )
61
- }
62
-
63
- function distinctFieldValues(rows: Record<string, unknown>[], field: string): unknown[] {
64
- const seen = new Set<string>()
65
- const values: unknown[] = []
66
- for (const row of rows) {
67
- const key = JSON.stringify(row[field])
68
- if (seen.has(key)) continue
69
- seen.add(key)
70
- values.push(row[field])
71
- }
72
- return values
73
- }
74
-
75
- function sortValues(values: unknown[], sort: ReportBinding['sort']): unknown[] {
76
- if (sort === 'asc') return [...values].sort((a, b) => String(a).localeCompare(String(b), 'zh-CN'))
77
- if (sort === 'desc')
78
- return [...values].sort((a, b) => String(b).localeCompare(String(a), 'zh-CN'))
79
- return values
80
- }
81
-
82
- function singleCellRange(row: number, col: number): CellRange {
83
- return { start: { row, col }, end: { row, col } }
84
- }
85
-
86
- function rangeRows(range: CellRange): number {
87
- return range.end.row - range.start.row + 1
88
- }
89
-
90
- function rangeCols(range: CellRange): number {
91
- return range.end.col - range.start.col + 1
92
- }
93
-
94
- function mergeFilters(
95
- rowFilter: Record<string, unknown>,
96
- colFilter: Record<string, unknown>
97
- ): Record<string, unknown> {
98
- return { ...rowFilter, ...colFilter }
99
- }
100
-
101
- function parentFilterForBinding(
102
- binding: ReportBinding,
103
- index: TemplateIndex,
104
- rowFilter: Record<string, unknown>,
105
- colFilter: Record<string, unknown>
106
- ): Record<string, unknown> {
107
- const filter: Record<string, unknown> = {}
108
- if (binding.rowParent) {
109
- const parent = index.bindingAt(binding.rowParent)
110
- if (parent && parent.dataset === binding.dataset) Object.assign(filter, rowFilter)
111
- }
112
- if (binding.colParent) {
113
- const parent = index.bindingAt(binding.colParent)
114
- if (parent && parent.dataset === binding.dataset) Object.assign(filter, colFilter)
115
- }
116
- return filter
117
- }
118
-
119
- /** 枚举扩展格实例 */
120
- export function enumerateExpansionInstances(
121
- binding: ReportBinding,
122
- data: DatasetRecords,
123
- filter: Record<string, unknown>
124
- ): ExpansionInstance[] {
125
- const rows = filterRows(data[binding.dataset] ?? [], filter)
126
-
127
- if (!isRowExpanding(binding) && !isColExpanding(binding)) {
128
- return [{ index: 0, filter }]
129
- }
130
-
131
- if (binding.aggregate === 'group') {
132
- const values = sortValues(distinctFieldValues(rows, binding.field), binding.sort)
133
- return values.map((value, index) => ({
134
- index,
135
- value,
136
- filter: { ...filter, [binding.field]: value }
137
- }))
138
- }
139
-
140
- if (binding.aggregate === 'list') {
141
- return rows.map((_, index) => ({ index, filter }))
142
- }
143
-
144
- return [{ index: 0, filter }]
145
- }
146
-
147
- function isRowSubtreeRoot(binding: ReportBinding, index: TemplateIndex): boolean {
148
- if (!isRowExpanding(binding)) return false
149
- if (!binding.rowParent) return true
150
- const parent = index.bindingAt(binding.rowParent)
151
- return !parent || !isRowExpanding(parent)
152
- }
153
-
154
- function isColSubtreeRoot(binding: ReportBinding, index: TemplateIndex): boolean {
155
- if (!isColExpanding(binding)) return false
156
- if (!binding.colParent) return true
157
- const parent = index.bindingAt(binding.colParent)
158
- return !parent || !isColExpanding(parent)
159
- }
160
-
161
- function isNestedRowExpander(binding: ReportBinding, index: TemplateIndex): boolean {
162
- return (
163
- isRowExpanding(binding) &&
164
- binding.aggregate === 'group' &&
165
- !!binding.rowParent &&
166
- !isRowSubtreeRoot(binding, index)
167
- )
168
- }
169
-
170
- function isNestedColExpander(binding: ReportBinding, index: TemplateIndex): boolean {
171
- return (
172
- isColExpanding(binding) &&
173
- binding.aggregate === 'group' &&
174
- !!binding.colParent &&
175
- !isColSubtreeRoot(binding, index)
176
- )
177
- }
178
-
179
- function rowSubtreeRows(rootAddr: CellAddress, index: TemplateIndex): number[] {
180
- let maxRow = rootAddr.row
181
- const walk = (addr: CellAddress): void => {
182
- maxRow = Math.max(maxRow, addr.row)
183
- for (const child of index.rowChildren.get(cellKey(addr)) ?? []) walk(child)
184
- }
185
- walk(rootAddr)
186
- return index.logicalRows.filter((row) => row >= rootAddr.row && row <= maxRow)
187
- }
188
-
189
- function colSubtreeCols(rootAddr: CellAddress, index: TemplateIndex): number[] {
190
- let maxCol = rootAddr.col
191
- const walk = (addr: CellAddress): void => {
192
- maxCol = Math.max(maxCol, addr.col)
193
- for (const child of index.colChildren.get(cellKey(addr)) ?? []) walk(child)
194
- }
195
- walk(rootAddr)
196
- return index.logicalCols.filter((col) => col >= rootAddr.col && col <= maxCol)
197
- }
198
-
199
- function columnsOnRow(index: TemplateIndex, row: number): number[] {
200
- const cols = new Set<number>()
201
- for (const entry of index.bindingsOnRow(row)) cols.add(entry.addr.col)
202
- for (const cell of index.staticCells.values()) {
203
- if (cell.row === row) cols.add(cell.col)
204
- }
205
- return [...cols].sort((a, b) => a - b)
206
- }
207
-
208
- function findRowSubtreeRootOnRow(index: TemplateIndex, row: number): CellAddress | null {
209
- for (const entry of index.bindingsOnRow(row)) {
210
- if (isRowSubtreeRoot(entry.binding, index)) return entry.addr
211
- }
212
- return null
213
- }
214
-
215
- /** 扩展带终止行:新的纵向子树根,或无父格的汇总行(如总计) */
216
- function isExpansionBandTerminatorRow(index: TemplateIndex, row: number, rootRow: number): boolean {
217
- if (row <= rootRow) return false
218
- const rowRoot = findRowSubtreeRootOnRow(index, row)
219
- if (rowRoot && rowRoot.row !== rootRow) return true
220
- return !isCompanionRow(index, row)
221
- }
222
-
223
- /** 扩展带内随行:全部绑定均带 rowParent(纯静态行不算带内行) */
224
- function isCompanionRow(index: TemplateIndex, row: number): boolean {
225
- const bindings = index.bindingsOnRow(row)
226
- if (bindings.length === 0) return false
227
- return bindings.every(({ binding }) => binding.rowParent !== undefined)
228
- }
229
-
230
- /** 纵向扩展带覆盖的模板行:子树行 + 紧随其后的带内汇总行 */
231
- function expansionBandRows(rootAddr: CellAddress, index: TemplateIndex): number[] {
232
- const subtreeRows = rowSubtreeRows(rootAddr, index)
233
- const maxSubtreeRow = Math.max(...subtreeRows)
234
- const rows = new Set(subtreeRows)
235
- for (const row of index.logicalRows) {
236
- if (row <= maxSubtreeRow) continue
237
- if (isExpansionBandTerminatorRow(index, row, rootAddr.row)) break
238
- if (!isCompanionRow(index, row)) break
239
- rows.add(row)
240
- }
241
- return index.logicalRows.filter((row) => rows.has(row))
242
- }
243
-
244
- function findNestedColChild(rootAddr: CellAddress, index: TemplateIndex): CellAddress | null {
245
- for (const child of index.colChildren.get(cellKey(rootAddr)) ?? []) {
246
- const binding = index.bindingAt(child)
247
- if (binding && isColExpanding(binding) && child.row > rootAddr.row) return child
248
- }
249
- return null
250
- }
251
-
252
- interface LayoutContext {
253
- rowFilter: Record<string, unknown>
254
- colFilter: Record<string, unknown>
255
- rowPath: number[]
256
- colPath: number[]
257
- }
258
-
259
- interface SegmentResult {
260
- rowSpan: number
261
- colSpan: number
262
- placements: PhysicalPlacement[]
263
- }
264
-
265
- class CoordinateEngine {
266
- constructor(
267
- private readonly index: TemplateIndex,
268
- private readonly data: DatasetRecords
269
- ) {}
270
-
271
- compute(): ExpansionLayout {
272
- const placements: PhysicalPlacement[] = []
273
- const consumedRows = new Set<number>()
274
- let physRow = 0
275
- let maxCol = 0
276
-
277
- for (const logicalRow of this.index.logicalRows) {
278
- if (consumedRows.has(logicalRow)) continue
279
-
280
- const rowRoot = findRowSubtreeRootOnRow(this.index, logicalRow)
281
- if (rowRoot) {
282
- const result = this.layoutRowSubtree(rowRoot, emptyCtx(), physRow, 0)
283
- placements.push(...result.placements)
284
- physRow += result.rowSpan
285
- maxCol = Math.max(maxCol, result.colSpan)
286
- for (const row of expansionBandRows(rowRoot, this.index)) consumedRows.add(row)
287
- continue
288
- }
289
-
290
- const result = this.layoutStandaloneRow(logicalRow, emptyCtx(), physRow, 0)
291
- placements.push(...result.placements)
292
- physRow += result.rowSpan
293
- maxCol = Math.max(maxCol, result.colSpan)
294
- }
295
-
296
- return {
297
- rowCount: physRow,
298
- colCount: Math.max(maxCol, this.baseColCount()),
299
- placements,
300
- mappedMerges: mapTemplateMerges(this.index, placements)
301
- }
302
- }
303
-
304
- private baseColCount(): number {
305
- if (this.index.logicalCols.length === 0) return 0
306
- return Math.max(...this.index.logicalCols) + 1
307
- }
308
-
309
- private layoutRowSubtree(
310
- rootAddr: CellAddress,
311
- ctx: LayoutContext,
312
- physRowStart: number,
313
- physColStart: number
314
- ): SegmentResult {
315
- const rootBinding = this.index.bindingAt(rootAddr)!
316
- const parentFilter = parentFilterForBinding(
317
- rootBinding,
318
- this.index,
319
- ctx.rowFilter,
320
- ctx.colFilter
321
- )
322
- const instances = enumerateExpansionInstances(
323
- rootBinding,
324
- this.data,
325
- mergeFilters(parentFilter, ctx.rowFilter)
326
- )
327
- const bandRows = expansionBandRows(rootAddr, this.index)
328
-
329
- if (instances.length === 0) return { rowSpan: 0, colSpan: 0, placements: [] }
330
-
331
- if (rootBinding.aggregate === 'list') {
332
- const listCtx: LayoutContext = {
333
- rowFilter: mergeFilters(parentFilter, ctx.rowFilter),
334
- colFilter: ctx.colFilter,
335
- rowPath: ctx.rowPath,
336
- colPath: ctx.colPath
337
- }
338
- const rowResult = this.layoutTemplateRow(
339
- rootAddr.row,
340
- listCtx,
341
- physRowStart,
342
- physColStart,
343
- rootAddr,
344
- instances.length
345
- )
346
- const placements = [...rowResult.placements]
347
- let totalRowSpan = instances.length
348
-
349
- for (const templateRow of bandRows) {
350
- if (templateRow === rootAddr.row) continue
351
- const companionResult = this.layoutTemplateRow(
352
- templateRow,
353
- listCtx,
354
- physRowStart + totalRowSpan,
355
- physColStart,
356
- null,
357
- 1
358
- )
359
- placements.push(...companionResult.placements)
360
- totalRowSpan += companionResult.rowSpan
361
- }
362
-
363
- return { rowSpan: totalRowSpan, colSpan: rowResult.colSpan, placements }
364
- }
365
-
366
- const placements: PhysicalPlacement[] = []
367
- let totalRowSpan = 0
368
- let maxColSpan = 0
369
-
370
- for (const instance of instances) {
371
- const instanceCtx: LayoutContext = {
372
- rowFilter: instance.filter,
373
- colFilter: ctx.colFilter,
374
- rowPath: [...ctx.rowPath, instance.index],
375
- colPath: ctx.colPath
376
- }
377
- const instanceHeight = this.measureSubtreeInstanceHeight(bandRows, rootAddr, instanceCtx)
378
- const groupMergeHeight = this.measureTemplateRowHeight(rootAddr.row, instanceCtx, null)
379
-
380
- if (this.bandHasMultiRowList(bandRows, instanceCtx)) {
381
- const bandResult = this.layoutMultiRowListBand(
382
- bandRows,
383
- rootAddr,
384
- instanceCtx,
385
- physRowStart + totalRowSpan,
386
- physColStart,
387
- groupMergeHeight
388
- )
389
- placements.push(...bandResult.placements)
390
- maxColSpan = Math.max(maxColSpan, bandResult.colSpan)
391
- totalRowSpan += instanceHeight
392
- continue
393
- }
394
-
395
- let rowOffset = 0
396
-
397
- for (const templateRow of bandRows) {
398
- const rowHeight = this.measureTemplateRowHeight(templateRow, instanceCtx, rootAddr)
399
- const mergeHeight =
400
- templateRow === rootAddr.row ? groupMergeHeight : instanceHeight - rowOffset
401
- const rowResult = this.layoutTemplateRow(
402
- templateRow,
403
- instanceCtx,
404
- physRowStart + totalRowSpan + rowOffset,
405
- physColStart,
406
- rootAddr,
407
- mergeHeight
408
- )
409
- placements.push(...rowResult.placements)
410
- rowOffset += rowHeight
411
- maxColSpan = Math.max(maxColSpan, rowResult.colSpan)
412
- }
413
-
414
- totalRowSpan += instanceHeight
415
- }
416
-
417
- return { rowSpan: totalRowSpan, colSpan: maxColSpan, placements }
418
- }
419
-
420
- private measureSubtreeInstanceHeight(
421
- bandRows: number[],
422
- rootAddr: CellAddress,
423
- ctx: LayoutContext
424
- ): number {
425
- let listDrivenHeight = 0
426
- let companionHeight = 0
427
-
428
- for (const row of bandRows) {
429
- const listCount = this.listInstanceCountOnRow(row, ctx)
430
- if (listCount > 0) {
431
- listDrivenHeight = Math.max(listDrivenHeight, listCount)
432
- continue
433
- }
434
- if (row === rootAddr.row) continue
435
- companionHeight += this.measureTemplateRowHeight(row, ctx, rootAddr)
436
- }
437
-
438
- if (listDrivenHeight === 0) {
439
- return this.measureTemplateRowHeight(rootAddr.row, ctx, null)
440
- }
441
-
442
- return listDrivenHeight + companionHeight
443
- }
444
-
445
- private bandHasMultiRowList(bandRows: number[], ctx: LayoutContext): boolean {
446
- let count = 0
447
- for (const row of bandRows) {
448
- if (this.listInstanceCountOnRow(row, ctx) > 0) count++
449
- if (count > 1) return true
450
- }
451
- return false
452
- }
453
-
454
- private layoutMultiRowListBand(
455
- bandRows: number[],
456
- rootAddr: CellAddress,
457
- ctx: LayoutContext,
458
- physRowStart: number,
459
- physColStart: number,
460
- groupMergeHeight: number
461
- ): SegmentResult {
462
- const listRows = bandRows.filter((row) => this.listInstanceCountOnRow(row, ctx) > 0)
463
- const companionRows = bandRows.filter((row) => !listRows.includes(row))
464
- const listCount = Math.max(...listRows.map((row) => this.listInstanceCountOnRow(row, ctx)), 1)
465
-
466
- const placements: PhysicalPlacement[] = []
467
- let maxColSpan = 0
468
-
469
- for (let listIndex = 0; listIndex < listCount; listIndex++) {
470
- for (const templateRow of listRows) {
471
- const rowResult = this.layoutTemplateRow(
472
- templateRow,
473
- ctx,
474
- physRowStart + listIndex,
475
- physColStart,
476
- templateRow === rootAddr.row ? rootAddr : null,
477
- groupMergeHeight,
478
- false,
479
- listIndex,
480
- listCount
481
- )
482
- placements.push(...rowResult.placements)
483
- maxColSpan = Math.max(maxColSpan, rowResult.colSpan)
484
- }
485
- }
486
-
487
- let rowOffset = listCount
488
- for (const templateRow of companionRows) {
489
- const rowResult = this.layoutTemplateRow(
490
- templateRow,
491
- ctx,
492
- physRowStart + rowOffset,
493
- physColStart,
494
- null,
495
- 1
496
- )
497
- placements.push(...rowResult.placements)
498
- maxColSpan = Math.max(maxColSpan, rowResult.colSpan)
499
- rowOffset += rowResult.rowSpan
500
- }
501
-
502
- return { rowSpan: rowOffset, colSpan: maxColSpan, placements }
503
- }
504
-
505
- private measureTemplateRowHeight(
506
- templateRow: number,
507
- ctx: LayoutContext,
508
- rootAddr: CellAddress | null
509
- ): number {
510
- for (const entry of this.index.bindingsOnRow(templateRow)) {
511
- if (isNestedRowExpander(entry.binding, this.index)) {
512
- return this.measureNestedRowBandHeight(entry.addr, entry.binding, templateRow, ctx)
513
- }
514
- }
515
-
516
- const listCount = this.listInstanceCountOnRow(templateRow, ctx)
517
- if (listCount > 0) return listCount
518
-
519
- if (rootAddr && templateRow === rootAddr.row) return 0
520
- return 1
521
- }
522
-
523
- private measureNestedRowBandHeight(
524
- groupAddr: CellAddress,
525
- groupBinding: ReportBinding,
526
- templateRow: number,
527
- ctx: LayoutContext
528
- ): number {
529
- const parentFilter = parentFilterForBinding(
530
- groupBinding,
531
- this.index,
532
- ctx.rowFilter,
533
- ctx.colFilter
534
- )
535
- const instances = enumerateExpansionInstances(
536
- groupBinding,
537
- this.data,
538
- mergeFilters(parentFilter, ctx.rowFilter)
539
- )
540
- if (instances.length === 0) return 0
541
-
542
- let total = 0
543
- for (const instance of instances) {
544
- const instanceCtx: LayoutContext = {
545
- ...ctx,
546
- rowFilter: instance.filter,
547
- rowPath: [...ctx.rowPath, instance.index]
548
- }
549
- total += this.listInstanceCountOnRow(templateRow, instanceCtx) || 1
550
- }
551
- return total
552
- }
553
-
554
- private layoutStandaloneRow(
555
- templateRow: number,
556
- ctx: LayoutContext,
557
- physRowStart: number,
558
- physColStart: number
559
- ): SegmentResult {
560
- return this.layoutTemplateRow(
561
- templateRow,
562
- ctx,
563
- physRowStart,
564
- physColStart,
565
- null,
566
- 1,
567
- !this.rowUsesPackedColumns(templateRow)
568
- )
569
- }
570
-
571
- private rowUsesPackedColumns(templateRow: number): boolean {
572
- for (const entry of this.index.bindingsOnRow(templateRow)) {
573
- if (isColSubtreeRoot(entry.binding, this.index)) return true
574
- if (isNestedColExpander(entry.binding, this.index)) return true
575
- if (entry.binding.colParent) {
576
- const parent = this.index.bindingAt(entry.binding.colParent)
577
- if (parent && isColExpanding(parent)) return true
578
- }
579
- }
580
- return false
581
- }
582
-
583
- private layoutTemplateRow(
584
- templateRow: number,
585
- ctx: LayoutContext,
586
- physRowStart: number,
587
- physColStart: number,
588
- rowSubtreeRoot: CellAddress | null,
589
- groupMergeHeight: number,
590
- preserveLogicalCols = false,
591
- fixedListIndex?: number,
592
- mergeListCount?: number
593
- ): SegmentResult {
594
- for (const entry of this.index.bindingsOnRow(templateRow)) {
595
- if (isNestedRowExpander(entry.binding, this.index)) {
596
- return this.layoutNestedRowBand(
597
- templateRow,
598
- entry.addr,
599
- entry.binding,
600
- ctx,
601
- physRowStart,
602
- physColStart
603
- )
604
- }
605
- }
606
-
607
- const cols = columnsOnRow(this.index, templateRow)
608
- const placements: PhysicalPlacement[] = []
609
- let physCol = physColStart
610
- const packedCols = !preserveLogicalCols && this.rowUsesPackedColumns(templateRow)
611
-
612
- const listCount = this.listInstanceCountOnRow(templateRow, ctx)
613
- const rowSpan = fixedListIndex !== undefined ? 1 : Math.max(listCount, 1)
614
- const leadingGap = cols.length > 0 && cols[0]! > physColStart
615
-
616
- for (const col of cols) {
617
- const addr = { row: templateRow, col }
618
- const binding = this.index.bindingAt(addr)
619
- const targetCol = packedCols && !leadingGap ? physCol : col
620
-
621
- if (binding && isColSubtreeRoot(binding, this.index)) {
622
- const colResult = this.layoutColSubtree(addr, ctx, physRowStart, targetCol)
623
- placements.push(...colResult.placements)
624
- if (packedCols) {
625
- physCol = leadingGap ? targetCol + colResult.colSpan : physCol + colResult.colSpan
626
- }
627
- continue
628
- }
629
-
630
- if (binding && isNestedColExpander(binding, this.index)) {
631
- const colResult = this.layoutNestedColBand(addr, binding, ctx, physRowStart, targetCol)
632
- placements.push(...colResult.placements)
633
- if (packedCols) {
634
- physCol = leadingGap ? targetCol + colResult.colSpan : physCol + colResult.colSpan
635
- }
636
- continue
637
- }
638
-
639
- if (binding && this.isInlineColChild(templateRow, col)) continue
640
-
641
- if (binding && binding.colParent && !isColExpanding(binding)) {
642
- const colResult = this.layoutColParentAlignedCell(
643
- addr,
644
- binding,
645
- ctx,
646
- physRowStart,
647
- targetCol,
648
- rowSpan
649
- )
650
- placements.push(...colResult.placements)
651
- if (packedCols) {
652
- physCol = leadingGap ? targetCol + colResult.colSpan : physCol + colResult.colSpan
653
- }
654
- continue
655
- }
656
-
657
- if (binding) {
658
- const cellResult = this.layoutBindingCell(
659
- addr,
660
- binding,
661
- ctx,
662
- physRowStart,
663
- targetCol,
664
- rowSpan,
665
- rowSubtreeRoot,
666
- groupMergeHeight,
667
- fixedListIndex,
668
- mergeListCount
669
- )
670
- placements.push(...cellResult.placements)
671
- if (packedCols) {
672
- physCol = leadingGap ? targetCol + cellResult.colSpan : physCol + cellResult.colSpan
673
- }
674
- continue
675
- }
676
-
677
- const staticCell = this.index.cellAt(addr)
678
- if (staticCell) {
679
- if (rowSpan > 1) {
680
- for (let i = 0; i < rowSpan; i++) {
681
- placements.push(
682
- this.makePlacement(
683
- addr,
684
- singleCellRange(physRowStart + i, targetCol),
685
- ctx,
686
- undefined,
687
- i,
688
- false
689
- )
690
- )
691
- }
692
- } else {
693
- placements.push(
694
- this.makePlacement(
695
- addr,
696
- singleCellRange(physRowStart, targetCol),
697
- ctx,
698
- undefined,
699
- 0,
700
- false
701
- )
702
- )
703
- }
704
- if (packedCols) {
705
- physCol = leadingGap ? targetCol + 1 : physCol + 1
706
- }
707
- }
708
- }
709
-
710
- const effectiveColSpan = packedCols
711
- ? leadingGap && cols.length > 0
712
- ? Math.max(...cols) + 1 - physColStart
713
- : physCol - physColStart
714
- : cols.length > 0
715
- ? Math.max(...cols) + 1 - physColStart
716
- : 0
717
-
718
- const effectiveRowSpan =
719
- rowSubtreeRoot && templateRow === rowSubtreeRoot.row && listCount === 0 ? 0 : rowSpan
720
- return { rowSpan: effectiveRowSpan, colSpan: effectiveColSpan, placements }
721
- }
722
-
723
- private layoutNestedRowBand(
724
- templateRow: number,
725
- groupAddr: CellAddress,
726
- groupBinding: ReportBinding,
727
- ctx: LayoutContext,
728
- physRowStart: number,
729
- physColStart: number
730
- ): SegmentResult {
731
- const parentFilter = parentFilterForBinding(
732
- groupBinding,
733
- this.index,
734
- ctx.rowFilter,
735
- ctx.colFilter
736
- )
737
- const instances = enumerateExpansionInstances(
738
- groupBinding,
739
- this.data,
740
- mergeFilters(parentFilter, ctx.rowFilter)
741
- )
742
- if (instances.length === 0) return { rowSpan: 0, colSpan: 0, placements: [] }
743
-
744
- const placements: PhysicalPlacement[] = []
745
- let rowOffset = 0
746
- let maxColSpan = 0
747
-
748
- const colOffsets = this.rowLogicalColOffsets(templateRow, physColStart)
749
-
750
- for (const instance of instances) {
751
- const instanceCtx: LayoutContext = {
752
- ...ctx,
753
- rowFilter: instance.filter,
754
- rowPath: [...ctx.rowPath, instance.index]
755
- }
756
- const detailCount = this.listInstanceCountOnRow(templateRow, instanceCtx) || 1
757
-
758
- if (mergeSpanEnabled(groupBinding)) {
759
- placements.push(
760
- this.makePlacement(
761
- groupAddr,
762
- createRange(
763
- { row: physRowStart + rowOffset, col: physColStart },
764
- { row: physRowStart + rowOffset + detailCount - 1, col: physColStart }
765
- ),
766
- instanceCtx,
767
- groupBinding,
768
- 0,
769
- true
770
- )
771
- )
772
- } else {
773
- for (let i = 0; i < detailCount; i++) {
774
- placements.push(
775
- this.makePlacement(
776
- groupAddr,
777
- singleCellRange(physRowStart + rowOffset + i, physColStart),
778
- instanceCtx,
779
- groupBinding,
780
- i,
781
- false
782
- )
783
- )
784
- }
785
- }
786
-
787
- for (const entry of this.index.bindingsOnRow(templateRow)) {
788
- if (sameAddress(entry.addr, groupAddr)) continue
789
- if (!isRowExpanding(entry.binding) || entry.binding.aggregate !== 'list') continue
790
- for (let i = 0; i < detailCount; i++) {
791
- placements.push(
792
- this.makePlacement(
793
- entry.addr,
794
- singleCellRange(physRowStart + rowOffset + i, colOffsets.get(entry.addr.col)!),
795
- instanceCtx,
796
- entry.binding,
797
- i,
798
- false
799
- )
800
- )
801
- }
802
- }
803
-
804
- for (const col of columnsOnRow(this.index, templateRow)) {
805
- if (this.index.bindingAt({ row: templateRow, col })) continue
806
- const staticCell = this.index.cellAt({ row: templateRow, col })
807
- if (!staticCell) continue
808
- for (let i = 0; i < detailCount; i++) {
809
- placements.push(
810
- this.makePlacement(
811
- { row: templateRow, col },
812
- singleCellRange(physRowStart + rowOffset + i, colOffsets.get(col)!),
813
- instanceCtx,
814
- undefined,
815
- i,
816
- false
817
- )
818
- )
819
- }
820
- }
821
-
822
- maxColSpan = Math.max(maxColSpan, colOffsets.size)
823
- rowOffset += detailCount
824
- }
825
-
826
- return { rowSpan: rowOffset, colSpan: maxColSpan, placements }
827
- }
828
-
829
- private layoutBindingCell(
830
- addr: CellAddress,
831
- binding: ReportBinding,
832
- ctx: LayoutContext,
833
- physRowStart: number,
834
- physColStart: number,
835
- rowSpan: number,
836
- rowSubtreeRoot: CellAddress | null,
837
- groupMergeHeight: number,
838
- fixedListIndex?: number,
839
- mergeListCount?: number
840
- ): SegmentResult {
841
- if (
842
- rowSubtreeRoot &&
843
- isRowExpanding(binding) &&
844
- binding.aggregate === 'group' &&
845
- sameAddress(addr, rowSubtreeRoot)
846
- ) {
847
- if (fixedListIndex !== undefined && fixedListIndex > 0) {
848
- return { rowSpan: 0, colSpan: 1, placements: [] }
849
- }
850
- const mergeRows = Math.max(
851
- fixedListIndex !== undefined ? (mergeListCount ?? groupMergeHeight) : groupMergeHeight,
852
- 1
853
- )
854
- if (!mergeSpanEnabled(binding)) {
855
- const placements = Array.from({ length: mergeRows }, (_, listIndex) =>
856
- this.makePlacement(
857
- addr,
858
- singleCellRange(physRowStart + listIndex, physColStart),
859
- ctx,
860
- binding,
861
- listIndex,
862
- false
863
- )
864
- )
865
- return { rowSpan: 0, colSpan: 1, placements }
866
- }
867
- const range = createRange(
868
- { row: physRowStart, col: physColStart },
869
- { row: physRowStart + mergeRows - 1, col: physColStart }
870
- )
871
- return {
872
- rowSpan: 0,
873
- colSpan: 1,
874
- placements: [this.makePlacement(addr, range, ctx, binding, 0, true)]
875
- }
876
- }
877
-
878
- if (isRowExpanding(binding) && binding.aggregate === 'list') {
879
- if (fixedListIndex !== undefined) {
880
- return {
881
- rowSpan: 0,
882
- colSpan: 1,
883
- placements: [
884
- this.makePlacement(
885
- addr,
886
- singleCellRange(physRowStart, physColStart),
887
- ctx,
888
- binding,
889
- fixedListIndex,
890
- false
891
- )
892
- ]
893
- }
894
- }
895
- const placements = Array.from({ length: rowSpan }, (_, listIndex) =>
896
- this.makePlacement(
897
- addr,
898
- singleCellRange(physRowStart + listIndex, physColStart),
899
- ctx,
900
- binding,
901
- listIndex,
902
- false
903
- )
904
- )
905
- return { rowSpan: 0, colSpan: 1, placements }
906
- }
907
-
908
- return {
909
- rowSpan: 0,
910
- colSpan: 1,
911
- placements: [
912
- this.makePlacement(
913
- addr,
914
- expandRows(singleCellRange(physRowStart, physColStart), rowSpan),
915
- ctx,
916
- binding,
917
- 0,
918
- mergeSpanEnabled(binding) && rowSpan > 1
919
- )
920
- ]
921
- }
922
- }
923
-
924
- private layoutColSubtree(
925
- rootAddr: CellAddress,
926
- ctx: LayoutContext,
927
- physRowStart: number,
928
- physColStart: number
929
- ): SegmentResult {
930
- const rootBinding = this.index.bindingAt(rootAddr)!
931
- const parentFilter = parentFilterForBinding(
932
- rootBinding,
933
- this.index,
934
- ctx.rowFilter,
935
- ctx.colFilter
936
- )
937
- const instances = enumerateExpansionInstances(
938
- rootBinding,
939
- this.data,
940
- mergeFilters(parentFilter, ctx.colFilter)
941
- )
942
-
943
- if (instances.length === 0) return { rowSpan: 0, colSpan: 0, placements: [] }
944
-
945
- const placements: PhysicalPlacement[] = []
946
- let totalColSpan = 0
947
-
948
- for (const instance of instances) {
949
- const instanceCtx: LayoutContext = {
950
- rowFilter: ctx.rowFilter,
951
- colFilter: instance.filter,
952
- rowPath: ctx.rowPath,
953
- colPath: [...ctx.colPath, instance.index]
954
- }
955
- const width = this.colInstanceWidth(rootAddr, instanceCtx)
956
- const range = createRange(
957
- { row: physRowStart, col: physColStart + totalColSpan },
958
- { row: physRowStart, col: physColStart + totalColSpan + width - 1 }
959
- )
960
- placements.push(
961
- this.makePlacement(
962
- rootAddr,
963
- range,
964
- instanceCtx,
965
- rootBinding,
966
- rootBinding.aggregate === 'list' ? instance.index : 0,
967
- mergeSpanEnabled(rootBinding)
968
- )
969
- )
970
- totalColSpan += width
971
- }
972
-
973
- return { rowSpan: 1, colSpan: totalColSpan, placements }
974
- }
975
-
976
- private layoutNestedColBand(
977
- addr: CellAddress,
978
- binding: ReportBinding,
979
- ctx: LayoutContext,
980
- physRowStart: number,
981
- physColStart: number
982
- ): SegmentResult {
983
- const parentAddr = binding.colParent!
984
- const parentBinding = this.index.bindingAt(parentAddr)!
985
- const parentFilter = parentFilterForBinding(
986
- parentBinding,
987
- this.index,
988
- ctx.rowFilter,
989
- ctx.colFilter
990
- )
991
- const parentInstances = enumerateExpansionInstances(
992
- parentBinding,
993
- this.data,
994
- mergeFilters(parentFilter, ctx.colFilter)
995
- )
996
-
997
- const placements: PhysicalPlacement[] = []
998
- let colOffset = 0
999
-
1000
- for (const parentInstance of parentInstances) {
1001
- const bandCtx: LayoutContext = {
1002
- ...ctx,
1003
- colFilter: parentInstance.filter,
1004
- colPath: [...ctx.colPath, parentInstance.index]
1005
- }
1006
- const childFilter = parentFilterForBinding(
1007
- binding,
1008
- this.index,
1009
- bandCtx.rowFilter,
1010
- bandCtx.colFilter
1011
- )
1012
- const childInstances = enumerateExpansionInstances(
1013
- binding,
1014
- this.data,
1015
- mergeFilters(childFilter, bandCtx.colFilter)
1016
- )
1017
-
1018
- for (const childInstance of childInstances) {
1019
- const childCtx: LayoutContext = {
1020
- ...bandCtx,
1021
- colFilter: childInstance.filter,
1022
- colPath: [...bandCtx.colPath, childInstance.index]
1023
- }
1024
- placements.push(
1025
- this.makePlacement(
1026
- addr,
1027
- singleCellRange(physRowStart, physColStart + colOffset),
1028
- childCtx,
1029
- binding,
1030
- 0,
1031
- false
1032
- )
1033
- )
1034
- colOffset++
1035
- }
1036
- }
1037
-
1038
- return { rowSpan: 1, colSpan: colOffset, placements }
1039
- }
1040
-
1041
- private colInstanceWidth(rootAddr: CellAddress, ctx: LayoutContext): number {
1042
- const nestedChild = findNestedColChild(rootAddr, this.index)
1043
- if (nestedChild) {
1044
- const binding = this.index.bindingAt(nestedChild)!
1045
- const parentFilter = parentFilterForBinding(binding, this.index, ctx.rowFilter, ctx.colFilter)
1046
- const instances = enumerateExpansionInstances(
1047
- binding,
1048
- this.data,
1049
- mergeFilters(parentFilter, ctx.colFilter)
1050
- )
1051
- return Math.max(instances.length, 1)
1052
- }
1053
-
1054
- const rootBinding = this.index.bindingAt(rootAddr)!
1055
- if (!mergeSpanEnabled(rootBinding)) return 1
1056
-
1057
- let width = 0
1058
- for (const col of colSubtreeCols(rootAddr, this.index)) {
1059
- if (col <= rootAddr.col) continue
1060
- if (
1061
- this.index.bindingAt({ row: rootAddr.row, col }) ||
1062
- this.index.cellAt({ row: rootAddr.row, col })
1063
- ) {
1064
- width++
1065
- }
1066
- }
1067
- return Math.max(width, 1)
1068
- }
1069
-
1070
- private layoutColParentAlignedCell(
1071
- addr: CellAddress,
1072
- binding: ReportBinding,
1073
- ctx: LayoutContext,
1074
- physRowStart: number,
1075
- physColStart: number,
1076
- rowSpan: number
1077
- ): SegmentResult {
1078
- const colParent = binding.colParent!
1079
- const parentBinding = this.index.bindingAt(colParent)!
1080
- const parentFilter = parentFilterForBinding(
1081
- parentBinding,
1082
- this.index,
1083
- ctx.rowFilter,
1084
- ctx.colFilter
1085
- )
1086
- const instances = enumerateExpansionInstances(
1087
- parentBinding,
1088
- this.data,
1089
- mergeFilters(parentFilter, ctx.colFilter)
1090
- )
1091
-
1092
- if (instances.length === 0) return { rowSpan: 0, colSpan: 0, placements: [] }
1093
-
1094
- const placements: PhysicalPlacement[] = []
1095
- let colOffset = 0
1096
-
1097
- for (const instance of instances) {
1098
- const instanceCtx: LayoutContext = {
1099
- rowFilter: ctx.rowFilter,
1100
- colFilter: instance.filter,
1101
- rowPath: ctx.rowPath,
1102
- colPath: [...ctx.colPath, instance.index]
1103
- }
1104
- const width = this.colInstanceWidth(colParent, instanceCtx)
1105
- placements.push(
1106
- this.makePlacement(
1107
- addr,
1108
- expandRows(singleCellRange(physRowStart, physColStart + colOffset), rowSpan),
1109
- instanceCtx,
1110
- binding,
1111
- 0,
1112
- mergeSpanEnabled(binding) && rowSpan > 1
1113
- )
1114
- )
1115
- colOffset += width
1116
- }
1117
-
1118
- return { rowSpan: 0, colSpan: colOffset, placements }
1119
- }
1120
-
1121
- private isInlineColChild(row: number, col: number): boolean {
1122
- const binding = this.index.bindingAt({ row, col })
1123
- if (!binding?.colParent) return false
1124
- const parent = this.index.bindingAt(binding.colParent)
1125
- if (!parent || !isColExpanding(parent)) return false
1126
- return binding.colParent.row === row
1127
- }
1128
-
1129
- private listInstanceCountOnRow(templateRow: number, ctx: LayoutContext): number {
1130
- let count = 0
1131
- for (const entry of this.index.bindingsOnRow(templateRow)) {
1132
- if (!isRowExpanding(entry.binding) || entry.binding.aggregate !== 'list') continue
1133
- const parentFilter = parentFilterForBinding(
1134
- entry.binding,
1135
- this.index,
1136
- ctx.rowFilter,
1137
- ctx.colFilter
1138
- )
1139
- const instances = enumerateExpansionInstances(
1140
- entry.binding,
1141
- this.data,
1142
- mergeFilters(parentFilter, ctx.rowFilter)
1143
- )
1144
- count = Math.max(count, instances.length)
1145
- }
1146
- return count
1147
- }
1148
-
1149
- private rowLogicalColOffsets(templateRow: number, physColStart: number): Map<number, number> {
1150
- const offsets = new Map<number, number>()
1151
- let physCol = physColStart
1152
- for (const col of columnsOnRow(this.index, templateRow)) {
1153
- offsets.set(col, physCol)
1154
- physCol++
1155
- }
1156
- return offsets
1157
- }
1158
-
1159
- private makePlacement(
1160
- logical: CellAddress,
1161
- physical: CellRange,
1162
- ctx: LayoutContext,
1163
- binding: ReportBinding | undefined,
1164
- listIndex: number,
1165
- mergeSpan: boolean
1166
- ): PhysicalPlacement {
1167
- return {
1168
- logical,
1169
- physical,
1170
- rowPath: ctx.rowPath,
1171
- colPath: ctx.colPath,
1172
- mergeSpan,
1173
- binding,
1174
- listIndex
1175
- }
1176
- }
1177
- }
1178
-
1179
- function emptyCtx(): LayoutContext {
1180
- return { rowFilter: {}, colFilter: {}, rowPath: [], colPath: [] }
1181
- }
1182
-
1183
- function expandRows(range: CellRange, rowSpan: number): CellRange {
1184
- if (rowSpan <= 1) return range
1185
- return createRange(range.start, { row: range.start.row + rowSpan - 1, col: range.end.col })
1186
- }
1187
-
1188
- /** 将模板合并区域映射到物理网格(基于锚点格的物理落点) */
1189
- export function mapTemplateMerges(
1190
- index: TemplateIndex,
1191
- placements: PhysicalPlacement[]
1192
- ): CellRange[] {
1193
- const layout: ExpansionLayout = { rowCount: 0, colCount: 0, placements, mappedMerges: [] }
1194
- const mapped: CellRange[] = []
1195
- for (const merge of index.merges) {
1196
- const mappedRange = mapLogicalMerge(merge, layout)
1197
- if (mappedRange) mapped.push(mappedRange)
1198
- }
1199
- return mapped
1200
- }
1201
-
1202
- function mapLogicalMerge(merge: CellRange, layout: ExpansionLayout): CellRange | null {
1203
- const startHits = placementsAt(layout, merge.start)
1204
- if (startHits.length === 0) return null
1205
- const startPlacement = startHits[0]!
1206
- const endHits = placementsAt(layout, merge.end)
1207
- if (endHits.length > 0) {
1208
- return createRange(startPlacement.physical.start, endHits[endHits.length - 1]!.physical.end)
1209
- }
1210
- const logicalRowSpan = merge.end.row - merge.start.row
1211
- const logicalColSpan = merge.end.col - merge.start.col
1212
- return createRange(startPlacement.physical.start, {
1213
- row: startPlacement.physical.start.row + logicalRowSpan,
1214
- col: startPlacement.physical.start.col + logicalColSpan
1215
- })
1216
- }
1217
-
1218
- /** 计算扩展坐标系:逻辑格 → 物理区间映射(纯计算、无快照输出) */
1219
- export function computeExpansionLayout(
1220
- index: TemplateIndex,
1221
- data: DatasetRecords
1222
- ): ExpansionLayout {
1223
- return new CoordinateEngine(index, data).compute()
1224
- }
1225
-
1226
- /** 按逻辑地址查找全部物理落点 */
1227
- export function placementsAt(layout: ExpansionLayout, addr: CellAddress): PhysicalPlacement[] {
1228
- return layout.placements.filter((p) => sameAddress(p.logical, addr))
1229
- }
1230
-
1231
- /** 物理区间行高(含合并) */
1232
- export function physicalRowSpan(placement: PhysicalPlacement): number {
1233
- return rangeRows(placement.physical)
1234
- }
1235
-
1236
- /** 物理区间列宽(含合并) */
1237
- export function physicalColSpan(placement: PhysicalPlacement): number {
1238
- return rangeCols(placement.physical)
1239
- }