feat: optimize the performance of large data interaction (#3)

* optimize the performance of large data interaction
* add pixel ratio and select api
This commit is contained in:
Rui-Sun 2023-06-07 18:52:22 +08:00 committed by GitHub
parent 9996b2b55c
commit fc27cf1d61
10 changed files with 448 additions and 413 deletions

2
.gitignore vendored
View File

@ -95,5 +95,5 @@ packages/vtable-docs/public/zh/documents
packages/vtable-docs/public/en/documents
packages/vtable-docs/public/*.html
#git-hook
# git-hook
common/scripts/pre-commit

View File

@ -78,6 +78,7 @@ import {
import { MenuHandler } from '../menu/dom/MenuHandler';
import type { BaseTableAPI, BaseTableConstructorOptions, IBaseTableProtected } from '../ts-types/base-table';
import { FocusInput } from './FouseInput';
import { defaultPixelRatio } from '../tools/pixel-ratio';
const { toBoxArray } = utilStyle;
const { isTouchEvent } = event;
const rangeReg = /^\$(\d+)\$(\d+)$/;
@ -156,7 +157,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
menu,
select: click,
customRender,
pixelRatio = 1
pixelRatio = defaultPixelRatio
} = options;
this.options = options;
this._widthMode = widthMode;
@ -700,7 +701,8 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
* @param pixelRatio
*/
setPixelRatio(pixelRatio: number) {
// do nothing
this.internalProps.pixelRatio = pixelRatio;
this.scenegraph.setPixelRatio(pixelRatio);
}
/**
*
@ -2030,7 +2032,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
*
*/
clearSelected() {
// do nothing
this.stateManeger.updateSelectPos(-1, -1);
}
/**
*
@ -2038,7 +2040,8 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI {
* @param row
*/
selectCell(col: number, row: number) {
// do nothing
this.stateManeger.updateSelectPos(col, row);
this.stateManeger.endSelectCells();
}
abstract isListTable(): boolean;

View File

@ -3,6 +3,7 @@ import type { Group } from '../../graphic/group';
import type { WrapText } from '../../graphic/text';
import { updateCellHeightForColumn } from '../../layout/update-height';
import type { Scenegraph } from '../../scenegraph';
import { emptyGroup } from '../../scenegraph';
import { getProp } from '../../utils/get-prop';
import { getPadding } from '../../utils/padding';
import { createColGroup } from '../column';
@ -251,6 +252,8 @@ export class SceneProxy {
// 不改变row更新body group范围
this.updateBody(y);
}
this.scenegraph.updateNextFrame();
}
updateBody(y: number) {
@ -304,7 +307,7 @@ export class SceneProxy {
for (let col = this.bodyLeftCol; col <= this.bodyRightCol; col++) {
for (let row = syncTopRow; row <= syncBottomRow; row++) {
// const cellGroup = this.table.scenegraph.getCell(col, row);
const cellGroup = this.highPerformanceGetCell(col, row);
const cellGroup = this.highPerformanceGetCell(col, row, distStartRow, distEndRow);
this.updateCellGroupContent(cellGroup);
}
}
@ -367,7 +370,7 @@ export class SceneProxy {
for (let col = this.bodyLeftCol; col <= this.bodyRightCol; col++) {
for (let row = syncTopRow; row <= syncBottomRow; row++) {
// const cellGroup = this.table.scenegraph.getCell(col, row);
const cellGroup = this.highPerformanceGetCell(col, row);
const cellGroup = this.highPerformanceGetCell(col, row, distStartRow, distEndRow);
this.updateCellGroupContent(cellGroup);
}
}
@ -398,8 +401,6 @@ export class SceneProxy {
(this.table as any).scenegraph.bodyGroup.firstChild.lastChild.row
);
this.scenegraph.renderSceneGraph();
if (!this.table.internalProps.autoRowHeight) {
await this.progress();
}
@ -524,7 +525,10 @@ export class SceneProxy {
}
}
highPerformanceGetCell(col: number, row: number) {
highPerformanceGetCell(col: number, row: number, rowStart: number = this.rowStart, rowEnd: number = this.rowEnd) {
if (row < rowStart || row > rowEnd) {
return emptyGroup;
}
if (this.cellCache.get(col)) {
const cacheCellGoup = this.cellCache.get(col);
if ((cacheCellGoup._next || cacheCellGoup._prev) && Math.abs(cacheCellGoup.row - row) < row) {

View File

@ -10,7 +10,6 @@ import {
createRowHeaderColGroup
} from './group-creater/column';
import type { WrapText } from './graphic/text';
import { updateAutoColWidth } from './layout/auto-width';
import { updateAutoRowHeight } from './layout/auto-height';
import { getCellMergeInfo } from './utils/get-cell-merge';
import { updateColWidth } from './layout/update-width';
@ -22,21 +21,23 @@ import { createFrameBorder } from './style/frame-border';
import { ResizeColumnHotSpotSize } from '../tools/global';
import splitModule from './graphic/contributions';
import { getProp } from './utils/get-prop';
import { createCellContent, dealWithIcon } from './utils/text-icon-layout';
import { dealWithIcon } from './utils/text-icon-layout';
import { SceneProxy } from './group-creater/progress/proxy';
import { SortOrder } from '../state/state';
import type { ListTable } from '../ListTable';
import type { TooltipOptions } from '../ts-types/tooltip';
import { computeColWidth, computeColsWidth } from './layout/compute-col-width';
import { getStyleTheme } from './group-creater/column-helper';
import { moveHeaderPosition } from './layout/move-cell';
import { updateCell } from './group-creater/cell-helper';
import type { BaseTableAPI } from '../ts-types/base-table';
import { updateAllSelectComponent, updateCellSelectBorder } from './select/update-select-border';
import { createCellSelectBorder } from './select/create-select-border';
import { moveSelectingRangeComponentsToSelectedRangeComponents } from './select/move-select-border';
import { deleteAllSelectBorder, deleteLastSelectedRangeComponents } from './select/delete-select-border';
container.load(splitModule);
const groupForDebug = new Group({});
groupForDebug.role = 'empty';
export const emptyGroup = new Group({});
emptyGroup.role = 'empty';
/**
* @description:
* @return {*}
@ -74,7 +75,8 @@ export class Scenegraph {
width: table.canvas.width,
height: table.canvas.height,
disableDirtyBounds: false,
background: table.theme.underlayBackgroundColor
background: table.theme.underlayBackgroundColor,
dpr: table.internalProps.pixelRatio
});
this.stage.defaultLayer.setTheme({
@ -349,7 +351,14 @@ export class Scenegraph {
cell = this.getCell(range.start.col, range.start.row);
}
return cell || groupForDebug;
return cell || emptyGroup;
}
highPerformanceGetCell(col: number, row: number): Group {
if (!this.isPivot && !this.transpose && !this.table.isHeader(col, row)) {
return this.proxy.highPerformanceGetCell(col, row);
}
return this.getCell(col, row);
}
getColGroup(col: number, isCornerOrColHeader = false): Group {
@ -396,179 +405,7 @@ export class Scenegraph {
this.stage.renderNextFrame();
}
resetAllSelectComponent() {
this.selectingRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => {
const [startCol, startRow, endCol, endRow] = key.split('-');
let cellsBounds;
for (let i = parseInt(startCol, 10); i <= parseInt(endCol, 10); i++) {
for (let j = parseInt(startRow, 10); j <= parseInt(endRow, 10); j++) {
const cellGroup = this.getCell(i, j);
cellGroup.AABBBounds.width(); // hack: globalAABBBounds可能不会自动更新这里强制更新一下
const bounds = cellGroup.globalAABBBounds;
if (!cellsBounds) {
cellsBounds = bounds;
} else {
cellsBounds.union(bounds);
}
}
}
selectComp.rect.setAttributes({
x: cellsBounds.x1 - this.tableGroup.attribute.x,
y: cellsBounds.y1 - this.tableGroup.attribute.y,
width: cellsBounds.width(),
height: cellsBounds.height(),
visible: true
});
//#region 判断是不是按着表头部分的选中框 因为绘制层级的原因 线宽会被遮住一半,因此需要动态调整层级
const isNearRowHeader =
// this.table.scrollLeft === 0 &&
parseInt(startCol, 10) === this.table.frozenColCount;
const isNearColHeader =
// this.table.scrollTop === 0 &&
parseInt(startRow, 10) === this.table.frozenRowCount;
if (
(isNearRowHeader && selectComp.rect.attribute.stroke[3]) ||
(isNearColHeader && selectComp.rect.attribute.stroke[0])
) {
if (isNearRowHeader) {
this.tableGroup.insertAfter(
selectComp.rect,
selectComp.role === 'columnHeader' ? this.cornerHeaderGroup : this.rowHeaderGroup
);
}
if (isNearColHeader) {
this.tableGroup.insertAfter(
selectComp.rect,
selectComp.role === 'rowHeader' ? this.cornerHeaderGroup : this.colHeaderGroup
);
}
//#region 调整层级后 滚动情况下会出现绘制范围出界 如body的选中框 渲染在了rowheader上面所有需要调整选中框rect的 边界
if (
selectComp.rect.attribute.x < this.rowHeaderGroup.attribute.width &&
this.table.scrollLeft > 0 &&
(selectComp.role === 'body' || selectComp.role === 'columnHeader')
) {
selectComp.rect.setAttributes({
x: selectComp.rect.attribute.x + (this.rowHeaderGroup.attribute.width - selectComp.rect.attribute.x),
width: selectComp.rect.attribute.width - (this.rowHeaderGroup.attribute.width - selectComp.rect.attribute.x)
});
}
if (
selectComp.rect.attribute.y < this.colHeaderGroup.attribute.height &&
this.table.scrollTop > 0 &&
(selectComp.role === 'body' || selectComp.role === 'rowHeader')
) {
selectComp.rect.setAttributes({
y: selectComp.rect.attribute.y + (this.colHeaderGroup.attribute.height - selectComp.rect.attribute.y),
height:
selectComp.rect.attribute.height - (this.colHeaderGroup.attribute.height - selectComp.rect.attribute.y)
});
}
//#endregion
} else {
this.tableGroup.insertAfter(
selectComp.rect,
selectComp.role === 'body'
? this.bodyGroup
: selectComp.role === 'columnHeader'
? this.colHeaderGroup
: selectComp.role === 'rowHeader'
? this.rowHeaderGroup
: this.cornerHeaderGroup
);
}
//#endregion
});
this.selectedRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => {
const [startCol, startRow, endCol, endRow] = key.split('-');
let cellsBounds;
for (let i = parseInt(startCol, 10); i <= parseInt(endCol, 10); i++) {
for (let j = parseInt(startRow, 10); j <= parseInt(endRow, 10); j++) {
const cellGroup = this.getCell(i, j);
cellGroup.AABBBounds.width(); // hack: globalAABBBounds可能不会自动更新这里强制更新一下
const bounds = cellGroup.globalAABBBounds;
if (!cellsBounds) {
cellsBounds = bounds;
} else {
cellsBounds.union(bounds);
}
}
}
selectComp.rect.setAttributes({
x: cellsBounds.x1 - this.tableGroup.attribute.x,
y: cellsBounds.y1 - this.tableGroup.attribute.y,
width: cellsBounds.width(),
height: cellsBounds.height(),
visible: true
});
//#region 判断是不是按着表头部分的选中框 因为绘制层级的原因 线宽会被遮住一半,因此需要动态调整层级
const isNearRowHeader =
// this.table.scrollLeft === 0 &&
parseInt(startCol, 10) === this.table.frozenColCount;
const isNearColHeader =
// this.table.scrollTop === 0 &&
parseInt(startRow, 10) === this.table.frozenRowCount;
if (
(isNearRowHeader && selectComp.rect.attribute.stroke[3]) ||
(isNearColHeader && selectComp.rect.attribute.stroke[0])
) {
if (isNearRowHeader) {
this.tableGroup.insertAfter(
selectComp.rect,
selectComp.role === 'columnHeader' ? this.cornerHeaderGroup : this.rowHeaderGroup
);
}
if (isNearColHeader) {
this.tableGroup.insertAfter(
selectComp.rect,
selectComp.role === 'rowHeader' ? this.cornerHeaderGroup : this.colHeaderGroup
);
}
//#region 调整层级后 滚动情况下会出现绘制范围出界 如body的选中框 渲染在了rowheader上面所有需要调整选中框rect的 边界
if (
selectComp.rect.attribute.x < this.rowHeaderGroup.attribute.width &&
this.table.scrollLeft > 0 &&
(selectComp.role === 'body' || selectComp.role === 'columnHeader')
) {
selectComp.rect.setAttributes({
x: selectComp.rect.attribute.x + (this.rowHeaderGroup.attribute.width - selectComp.rect.attribute.x),
width: selectComp.rect.attribute.width - (this.rowHeaderGroup.attribute.width - selectComp.rect.attribute.x)
});
}
if (
selectComp.rect.attribute.y < this.colHeaderGroup.attribute.height &&
this.table.scrollTop > 0 &&
(selectComp.role === 'body' || selectComp.role === 'rowHeader')
) {
selectComp.rect.setAttributes({
y: selectComp.rect.attribute.y + (this.colHeaderGroup.attribute.height - selectComp.rect.attribute.y),
height:
selectComp.rect.attribute.height - (this.colHeaderGroup.attribute.height - selectComp.rect.attribute.y)
});
}
//#endregion
} else {
this.tableGroup.insertAfter(
selectComp.rect,
selectComp.role === 'body'
? this.bodyGroup
: selectComp.role === 'columnHeader'
? this.colHeaderGroup
: selectComp.role === 'rowHeader'
? this.rowHeaderGroup
: this.cornerHeaderGroup
);
}
//#endregion
});
}
removeInteractionBorder(col: number, row: number) {
const cellGroup = this.getCell(col, row);
cellGroup.setAttribute('highlightStroke', undefined);
cellGroup.setAttribute('highlightStrokeArrayWidth', undefined);
cellGroup.setAttribute('highlightStrokeArrayColor', undefined);
updateAllSelectComponent(this);
}
hideHoverIcon(col: number, row: number) {
@ -626,6 +463,13 @@ export class Scenegraph {
(cellGroup?.firstChild as any)?.activate?.(this.table);
}
removeInteractionBorder(col: number, row: number) {
const cellGroup = this.getCell(col, row);
cellGroup.setAttribute('highlightStroke', undefined);
cellGroup.setAttribute('highlightStrokeArrayWidth', undefined);
cellGroup.setAttribute('highlightStrokeArrayColor', undefined);
}
createCellSelectBorder(
start_Col: number,
start_Row: number,
@ -635,238 +479,22 @@ export class Scenegraph {
selectId: string, //整体区域${endRow}-${startCol}${startRow}${endCol}${endRow}作为其编号
strokes?: boolean[]
) {
const startCol = Math.min(start_Col, end_Col);
const startRow = Math.min(start_Row, end_Row);
const endCol = Math.max(start_Col, end_Col);
const endRow = Math.max(start_Row, end_Row);
let cellsBounds;
for (let i = startCol; i <= endCol; i++) {
for (let j = startRow; j <= endRow; j++) {
const cellGroup = this.getCell(i, j);
if (cellGroup.role === 'shadow-cell') {
continue;
}
const bounds = cellGroup.globalAABBBounds;
if (!cellsBounds) {
cellsBounds = bounds;
} else {
cellsBounds.union(bounds);
}
}
}
const theme = this.table.theme;
// 框选外边框
const bodyClickBorderColor = theme.selectionStyle?.cellBorderColor;
const bodyClickLineWidth = theme.selectionStyle?.cellBorderLineWidth;
const rect = createRect({
pickable: false,
fill: true,
fillColor: (theme.selectionStyle?.cellBgColor as any) ?? 'rgba(0, 0, 255,0.1)',
strokeColor: bodyClickBorderColor as string,
lineWidth: bodyClickLineWidth as number,
stroke: strokes,
x: cellsBounds.x1 - this.tableGroup.attribute.x,
y: cellsBounds.y1 - this.tableGroup.attribute.y,
width: cellsBounds.width(),
height: cellsBounds.height(),
visible: true
});
this.lastSelectId = selectId;
this.selectingRangeComponents.set(`${startCol}-${startRow}-${endCol}-${endRow}-${selectId}`, {
rect,
role: selectRangeType
});
this.tableGroup.insertAfter(
rect,
selectRangeType === 'body'
? this.bodyGroup
: selectRangeType === 'columnHeader'
? this.colHeaderGroup
: selectRangeType === 'rowHeader'
? this.rowHeaderGroup
: this.cornerHeaderGroup
);
createCellSelectBorder(this, start_Col, start_Row, end_Col, end_Row, selectRangeType, selectId, strokes);
}
moveSelectingRangeComponentsToSelectedRangeComponents() {
this.selectingRangeComponents.forEach((rangeComponent, key) => {
if (this.selectedRangeComponents.get(key)) {
this.selectedRangeComponents.get(key).rect.delete();
}
this.selectedRangeComponents.set(key, rangeComponent);
});
this.selectingRangeComponents = new Map();
this.updateNextFrame();
moveSelectingRangeComponentsToSelectedRangeComponents(this);
}
/** 按住shift 则继续上次选中范围 需要将现有的删除掉 */
deleteLastSelectedRangeComponents() {
this.selectedRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => {
const lastSelectId = key.split('-')[4];
if (lastSelectId === this.lastSelectId) {
selectComp.rect.delete();
this.selectedRangeComponents.delete(key);
}
});
deleteLastSelectedRangeComponents(this);
}
deleteAllSelectBorder() {
this.selectedRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => {
selectComp.rect.delete();
});
this.selectedRangeComponents = new Map();
deleteAllSelectBorder(this);
}
updateCellSelectBorder(newStartCol: number, newStartRow: number, newEndCol: number, newEndRow: number) {
let startCol = Math.min(newEndCol, newStartCol);
let startRow = Math.min(newEndRow, newStartRow);
let endCol = Math.max(newEndCol, newStartCol);
let endRow = Math.max(newEndRow, newStartRow);
//#region region 校验四周的单元格有没有合并的情况,如有则扩大范围
const extendSelectRange = () => {
let isExtend = false;
for (let col = startCol; col <= endCol; col++) {
if (col === startCol) {
for (let row = startRow; row <= endRow; row++) {
const mergeInfo = getCellMergeInfo(this.table, col, row);
if (mergeInfo && mergeInfo.start.col < startCol) {
startCol = mergeInfo.start.col;
isExtend = true;
break;
}
}
}
if (!isExtend && col === endCol) {
for (let row = startRow; row <= endRow; row++) {
const mergeInfo = getCellMergeInfo(this.table, col, row);
if (mergeInfo && mergeInfo.end.col > endCol) {
endCol = mergeInfo.end.col;
isExtend = true;
break;
}
}
}
if (isExtend) {
break;
}
}
if (!isExtend) {
for (let row = startRow; row <= endRow; row++) {
if (row === startRow) {
for (let col = startCol; col <= endCol; col++) {
const mergeInfo = getCellMergeInfo(this.table, col, row);
if (mergeInfo && mergeInfo.start.row < startRow) {
startRow = mergeInfo.start.row;
isExtend = true;
break;
}
}
}
if (!isExtend && row === endRow) {
for (let col = startCol; col <= endCol; col++) {
const mergeInfo = getCellMergeInfo(this.table, col, row);
if (mergeInfo && mergeInfo.end.row > endRow) {
endRow = mergeInfo.end.row;
isExtend = true;
break;
}
}
}
if (isExtend) {
break;
}
}
}
if (isExtend) {
extendSelectRange();
}
};
extendSelectRange();
//#endregion
this.selectingRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => {
selectComp.rect.delete();
});
this.selectingRangeComponents = new Map();
let needRowHeader = false;
let needColumnHeader = false;
let needBody = false;
let needCornerHeader = false;
if (startCol <= this.table.frozenColCount - 1 && startRow <= this.table.frozenRowCount - 1) {
needCornerHeader = true;
}
if (startCol <= this.table.frozenColCount - 1 && endRow >= this.table.frozenRowCount) {
needRowHeader = true;
}
if (startRow <= this.table.frozenRowCount - 1 && endCol >= this.table.frozenColCount) {
needColumnHeader = true;
}
if (endCol >= this.table.frozenColCount && endRow >= this.table.frozenRowCount) {
needBody = true;
}
// TODO 可以尝试不拆分三个表头和body【前提是theme中合并配置】 用一个SelectBorder 需要结合clip并动态设置border的范围【依据区域范围 已经是否跨表头及body】
if (needCornerHeader) {
const cornerEndCol = Math.min(endCol, this.table.frozenColCount - 1);
const cornerEndRow = Math.min(endRow, this.table.frozenRowCount - 1);
const strokeArray = [true, !needColumnHeader, !needRowHeader, true];
this.createCellSelectBorder(
startCol,
startRow,
cornerEndCol,
cornerEndRow,
'cornerHeader',
`${startCol}${startRow}${endCol}${endRow}`,
strokeArray
);
}
if (needColumnHeader) {
const columnHeaderStartCol = Math.max(startCol, this.table.frozenColCount);
const columnHeaderEndRow = Math.min(endRow, this.table.frozenRowCount - 1);
const strokeArray = [true, true, !needBody, !needCornerHeader];
this.createCellSelectBorder(
columnHeaderStartCol,
startRow,
endCol,
columnHeaderEndRow,
'columnHeader',
`${startCol}${startRow}${endCol}${endRow}`,
strokeArray
);
}
if (needRowHeader) {
const columnHeaderStartRow = Math.max(startRow, this.table.frozenRowCount);
const columnHeaderEndCol = Math.min(endCol, this.table.frozenColCount - 1);
const strokeArray = [!needCornerHeader, !needBody, true, true];
this.createCellSelectBorder(
startCol,
columnHeaderStartRow,
columnHeaderEndCol,
endRow,
'rowHeader',
`${startCol}${startRow}${endCol}${endRow}`,
strokeArray
);
}
if (needBody) {
const columnHeaderStartCol = Math.max(startCol, this.table.frozenColCount);
const columnHeaderStartRow = Math.max(startRow, this.table.frozenRowCount);
const strokeArray = [!needColumnHeader, true, true, !needRowHeader];
this.createCellSelectBorder(
columnHeaderStartCol,
columnHeaderStartRow,
endCol,
endRow,
'body',
`${startCol}${startRow}${endCol}${endRow}`,
strokeArray
);
}
updateCellSelectBorder(this, newStartCol, newStartRow, newEndCol, newEndRow);
}
// hideCellsSelectBorder() {
// this.component.selectBorder.setAttribute('visible', false);
// }
/**
* @description: icon mark
@ -1151,6 +779,8 @@ export class Scenegraph {
// 更新滚动条状态
this.component.updateScrollBar();
this.updateNextFrame();
}
/**
@ -1769,6 +1399,16 @@ export class Scenegraph {
}
updateCell(col, row, this.table);
}
setPixelRatio(pixelRatio: number) {
// this.stage.setDpr(pixelRatio);
// 这里因为本时刻部分节点有更新bounds标记直接render回导致开启DirtyBounds无法完整重绘画布
// 所以这里先关闭DirtyBounds等待下一帧再开启
this.stage.disableDirtyBounds();
this.stage.window.setDpr(pixelRatio);
this.stage.render();
this.stage.enableDirtyBounds();
}
}
function showIcon(scene: Scenegraph, cellGroup: Group, visibleTime: 'mouseenter_cell' | 'click_cell') {

View File

@ -0,0 +1,68 @@
import { createRect } from '@visactor/vrender';
import type { CellType } from '../../ts-types';
import type { Scenegraph } from '../scenegraph';
export function createCellSelectBorder(
scene: Scenegraph,
start_Col: number,
start_Row: number,
end_Col: number,
end_Row: number,
selectRangeType: CellType,
selectId: string, //整体区域${endRow}-${startCol}${startRow}${endCol}${endRow}作为其编号
strokes?: boolean[]
) {
const startCol = Math.min(start_Col, end_Col);
const startRow = Math.min(start_Row, end_Row);
const endCol = Math.max(start_Col, end_Col);
const endRow = Math.max(start_Row, end_Row);
let cellsBounds;
for (let i = startCol; i <= endCol; i++) {
for (let j = startRow; j <= endRow; j++) {
const cellGroup = scene.highPerformanceGetCell(i, j);
if (cellGroup.role === 'shadow-cell') {
continue;
}
const bounds = cellGroup.globalAABBBounds;
if (!cellsBounds) {
cellsBounds = bounds;
} else {
cellsBounds.union(bounds);
}
}
}
const theme = scene.table.theme;
// 框选外边框
const bodyClickBorderColor = theme.selectionStyle?.cellBorderColor;
const bodyClickLineWidth = theme.selectionStyle?.cellBorderLineWidth;
const rect = createRect({
pickable: false,
fill: true,
fillColor: (theme.selectionStyle?.cellBgColor as any) ?? 'rgba(0, 0, 255,0.1)',
strokeColor: bodyClickBorderColor as string,
lineWidth: bodyClickLineWidth as number,
stroke: strokes,
x: cellsBounds.x1 - scene.tableGroup.attribute.x,
y: cellsBounds.y1 - scene.tableGroup.attribute.y,
width: cellsBounds.width(),
height: cellsBounds.height(),
visible: true
});
scene.lastSelectId = selectId;
scene.selectingRangeComponents.set(`${startCol}-${startRow}-${endCol}-${endRow}-${selectId}`, {
rect,
role: selectRangeType
});
scene.tableGroup.insertAfter(
rect,
selectRangeType === 'body'
? scene.bodyGroup
: selectRangeType === 'columnHeader'
? scene.colHeaderGroup
: selectRangeType === 'rowHeader'
? scene.rowHeaderGroup
: scene.cornerHeaderGroup
);
}

View File

@ -0,0 +1,21 @@
import type { IRect } from '@visactor/vrender';
import type { Scenegraph } from '../scenegraph';
import type { CellType } from '../../ts-types';
/** 按住shift 则继续上次选中范围 需要将现有的删除掉 */
export function deleteLastSelectedRangeComponents(scene: Scenegraph) {
scene.selectedRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => {
const lastSelectId = key.split('-')[4];
if (lastSelectId === scene.lastSelectId) {
selectComp.rect.delete();
scene.selectedRangeComponents.delete(key);
}
});
}
export function deleteAllSelectBorder(scene: Scenegraph) {
scene.selectedRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => {
selectComp.rect.delete();
});
scene.selectedRangeComponents = new Map();
}

View File

@ -0,0 +1,12 @@
import type { Scenegraph } from '../scenegraph';
export function moveSelectingRangeComponentsToSelectedRangeComponents(scene: Scenegraph) {
scene.selectingRangeComponents.forEach((rangeComponent, key) => {
if (scene.selectedRangeComponents.get(key)) {
scene.selectedRangeComponents.get(key).rect.delete();
}
scene.selectedRangeComponents.set(key, rangeComponent);
});
scene.selectingRangeComponents = new Map();
scene.updateNextFrame();
}

View File

@ -0,0 +1,265 @@
import type { IRect } from '@visactor/vrender';
import type { Scenegraph } from '../scenegraph';
import type { CellType } from '../../ts-types';
import { getCellMergeInfo } from '../utils/get-cell-merge';
export function updateAllSelectComponent(scene: Scenegraph) {
scene.selectingRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => {
updateComponent(selectComp, key, scene);
});
scene.selectedRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => {
updateComponent(selectComp, key, scene);
});
}
function updateComponent(selectComp: { rect: IRect; role: CellType }, key: string, scene: Scenegraph) {
const [startColStr, startRowStr, endColStr, endRowStr] = key.split('-');
const startCol = parseInt(startColStr, 10);
const startRow = parseInt(startRowStr, 10);
const endCol = parseInt(endColStr, 10);
const endRow = parseInt(endRowStr, 10);
let cellsBounds;
for (let i = startCol; i <= endCol; i++) {
for (let j = startRow; j <= endRow; j++) {
const cellGroup = scene.highPerformanceGetCell(i, j);
if (cellGroup.role !== 'cell') {
continue;
}
cellGroup.AABBBounds.width(); // hack: globalAABBBounds可能不会自动更新这里强制更新一下
const bounds = cellGroup.globalAABBBounds;
if (!cellsBounds) {
cellsBounds = bounds;
} else {
cellsBounds.union(bounds);
}
}
}
if (!cellsBounds) {
// 选中区域在实际单元格区域外,不显示选择框
selectComp.rect.setAttributes({
visible: false
});
} else {
selectComp.rect.setAttributes({
x: cellsBounds.x1 - scene.tableGroup.attribute.x,
y: cellsBounds.y1 - scene.tableGroup.attribute.y,
width: cellsBounds.width(),
height: cellsBounds.height(),
visible: true
});
}
//#region 判断是不是按着表头部分的选中框 因为绘制层级的原因 线宽会被遮住一半,因此需要动态调整层级
const isNearRowHeader =
// scene.table.scrollLeft === 0 &&
startCol === scene.table.frozenColCount;
const isNearColHeader =
// scene.table.scrollTop === 0 &&
startRow === scene.table.frozenRowCount;
if (
(isNearRowHeader && selectComp.rect.attribute.stroke[3]) ||
(isNearColHeader && selectComp.rect.attribute.stroke[0])
) {
if (isNearRowHeader) {
scene.tableGroup.insertAfter(
selectComp.rect,
selectComp.role === 'columnHeader' ? scene.cornerHeaderGroup : scene.rowHeaderGroup
);
}
if (isNearColHeader) {
scene.tableGroup.insertAfter(
selectComp.rect,
selectComp.role === 'rowHeader' ? scene.cornerHeaderGroup : scene.colHeaderGroup
);
}
//#region 调整层级后 滚动情况下会出现绘制范围出界 如body的选中框 渲染在了rowheader上面所有需要调整选中框rect的 边界
if (
selectComp.rect.attribute.x < scene.rowHeaderGroup.attribute.width &&
scene.table.scrollLeft > 0 &&
(selectComp.role === 'body' || selectComp.role === 'columnHeader')
) {
selectComp.rect.setAttributes({
x: selectComp.rect.attribute.x + (scene.rowHeaderGroup.attribute.width - selectComp.rect.attribute.x),
width: selectComp.rect.attribute.width - (scene.rowHeaderGroup.attribute.width - selectComp.rect.attribute.x)
});
}
if (
selectComp.rect.attribute.y < scene.colHeaderGroup.attribute.height &&
scene.table.scrollTop > 0 &&
(selectComp.role === 'body' || selectComp.role === 'rowHeader')
) {
selectComp.rect.setAttributes({
y: selectComp.rect.attribute.y + (scene.colHeaderGroup.attribute.height - selectComp.rect.attribute.y),
height: selectComp.rect.attribute.height - (scene.colHeaderGroup.attribute.height - selectComp.rect.attribute.y)
});
}
//#endregion
} else {
scene.tableGroup.insertAfter(
selectComp.rect,
selectComp.role === 'body'
? scene.bodyGroup
: selectComp.role === 'columnHeader'
? scene.colHeaderGroup
: selectComp.role === 'rowHeader'
? scene.rowHeaderGroup
: scene.cornerHeaderGroup
);
}
//#endregion
}
export function updateCellSelectBorder(
scene: Scenegraph,
newStartCol: number,
newStartRow: number,
newEndCol: number,
newEndRow: number
) {
let startCol = Math.min(newEndCol, newStartCol);
let startRow = Math.min(newEndRow, newStartRow);
let endCol = Math.max(newEndCol, newStartCol);
let endRow = Math.max(newEndRow, newStartRow);
//#region region 校验四周的单元格有没有合并的情况,如有则扩大范围
const extendSelectRange = () => {
let isExtend = false;
for (let col = startCol; col <= endCol; col++) {
if (col === startCol) {
for (let row = startRow; row <= endRow; row++) {
const mergeInfo = getCellMergeInfo(scene.table, col, row);
if (mergeInfo && mergeInfo.start.col < startCol) {
startCol = mergeInfo.start.col;
isExtend = true;
break;
}
}
}
if (!isExtend && col === endCol) {
for (let row = startRow; row <= endRow; row++) {
const mergeInfo = getCellMergeInfo(scene.table, col, row);
if (mergeInfo && mergeInfo.end.col > endCol) {
endCol = mergeInfo.end.col;
isExtend = true;
break;
}
}
}
if (isExtend) {
break;
}
}
if (!isExtend) {
for (let row = startRow; row <= endRow; row++) {
if (row === startRow) {
for (let col = startCol; col <= endCol; col++) {
const mergeInfo = getCellMergeInfo(scene.table, col, row);
if (mergeInfo && mergeInfo.start.row < startRow) {
startRow = mergeInfo.start.row;
isExtend = true;
break;
}
}
}
if (!isExtend && row === endRow) {
for (let col = startCol; col <= endCol; col++) {
const mergeInfo = getCellMergeInfo(scene.table, col, row);
if (mergeInfo && mergeInfo.end.row > endRow) {
endRow = mergeInfo.end.row;
isExtend = true;
break;
}
}
}
if (isExtend) {
break;
}
}
}
if (isExtend) {
extendSelectRange();
}
};
extendSelectRange();
//#endregion
scene.selectingRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => {
selectComp.rect.delete();
});
scene.selectingRangeComponents = new Map();
let needRowHeader = false;
let needColumnHeader = false;
let needBody = false;
let needCornerHeader = false;
if (startCol <= scene.table.frozenColCount - 1 && startRow <= scene.table.frozenRowCount - 1) {
needCornerHeader = true;
}
if (startCol <= scene.table.frozenColCount - 1 && endRow >= scene.table.frozenRowCount) {
needRowHeader = true;
}
if (startRow <= scene.table.frozenRowCount - 1 && endCol >= scene.table.frozenColCount) {
needColumnHeader = true;
}
if (endCol >= scene.table.frozenColCount && endRow >= scene.table.frozenRowCount) {
needBody = true;
}
// TODO 可以尝试不拆分三个表头和body【前提是theme中合并配置】 用一个SelectBorder 需要结合clip并动态设置border的范围【依据区域范围 已经是否跨表头及body】
if (needCornerHeader) {
const cornerEndCol = Math.min(endCol, scene.table.frozenColCount - 1);
const cornerEndRow = Math.min(endRow, scene.table.frozenRowCount - 1);
const strokeArray = [true, !needColumnHeader, !needRowHeader, true];
scene.createCellSelectBorder(
startCol,
startRow,
cornerEndCol,
cornerEndRow,
'cornerHeader',
`${startCol}${startRow}${endCol}${endRow}`,
strokeArray
);
}
if (needColumnHeader) {
const columnHeaderStartCol = Math.max(startCol, scene.table.frozenColCount);
const columnHeaderEndRow = Math.min(endRow, scene.table.frozenRowCount - 1);
const strokeArray = [true, true, !needBody, !needCornerHeader];
scene.createCellSelectBorder(
columnHeaderStartCol,
startRow,
endCol,
columnHeaderEndRow,
'columnHeader',
`${startCol}${startRow}${endCol}${endRow}`,
strokeArray
);
}
if (needRowHeader) {
const columnHeaderStartRow = Math.max(startRow, scene.table.frozenRowCount);
const columnHeaderEndCol = Math.min(endCol, scene.table.frozenColCount - 1);
const strokeArray = [!needCornerHeader, !needBody, true, true];
scene.createCellSelectBorder(
startCol,
columnHeaderStartRow,
columnHeaderEndCol,
endRow,
'rowHeader',
`${startCol}${startRow}${endCol}${endRow}`,
strokeArray
);
}
if (needBody) {
const columnHeaderStartCol = Math.max(startCol, scene.table.frozenColCount);
const columnHeaderStartRow = Math.max(startRow, scene.table.frozenRowCount);
const strokeArray = [!needColumnHeader, true, true, !needRowHeader];
scene.createCellSelectBorder(
columnHeaderStartCol,
columnHeaderStartRow,
endCol,
endRow,
'body',
`${startCol}${startRow}${endCol}${endRow}`,
strokeArray
);
}
}

View File

@ -1,4 +1,4 @@
import type { CellRange } from '../../ts-types';
import type { CellRange, TextColumnDefine } from '../../ts-types';
import type { BaseTableAPI } from '../../ts-types/base-table';
/**
@ -9,6 +9,10 @@ import type { BaseTableAPI } from '../../ts-types/base-table';
* @return {false | CellRange}
*/
export function getCellMergeInfo(table: BaseTableAPI, col: number, row: number): false | CellRange {
// 先判断非表头且非cellMerge配置返回false
if (!table.isHeader(col, row) && (table.getBodyColumnDefine(col, row) as TextColumnDefine).mergeCell !== true) {
return false;
}
const range = table.getCellRange(col, row);
const isMerge = range.start.col !== range.end.col || range.start.row !== range.end.row;
if (!isMerge) {

View File

@ -0,0 +1,18 @@
import { isNode } from './helper';
export let defaultPixelRatio = 1;
/*
* @Description:
*/
function setPixelRatio(): void {
if (isNode) {
defaultPixelRatio = 1;
} else {
defaultPixelRatio = Math.ceil(window.devicePixelRatio || 1);
if (defaultPixelRatio > 1 && defaultPixelRatio % 2 !== 0) {
// 非整数倍的像素比,向上取整
defaultPixelRatio += 1;
}
}
}
setPixelRatio();