diff --git a/mockdata/src/sheets/demo/default-workbook-data-demo.ts b/mockdata/src/sheets/demo/default-workbook-data-demo.ts index 2c73f698939..1b451590852 100644 --- a/mockdata/src/sheets/demo/default-workbook-data-demo.ts +++ b/mockdata/src/sheets/demo/default-workbook-data-demo.ts @@ -13985,7 +13985,6 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { rowCount: 1000, columnCount: 20, zoomRatio: 1, - defaultStyle: 'defaultSheetStyle', cellData: { 0: { 0: { diff --git a/packages/core/src/shared/__tests__/search-array.spec.ts b/packages/core/src/shared/__tests__/search-array.spec.ts new file mode 100644 index 00000000000..89bc6b3447e --- /dev/null +++ b/packages/core/src/shared/__tests__/search-array.spec.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from 'vitest'; +import { binSearchFirstGreaterThanTarget, orderSearchArray, searchArray } from '../array-search'; + +describe('test searchArray function', () => { + const array = [0, 1, 2, 3, 4, 4, 4, 5, 5, 5]; + + it('searchArray test', () => { + expect(searchArray(array, -1)).toBe(array.indexOf(0)); + expect(searchArray(array, 0)).toBe(array.indexOf(1)); + expect(searchArray(array, 4)).toBe(array.indexOf(5)); + expect(searchArray(array, 5)).toBe(array.length - 1); + expect(searchArray(array, 8)).toBe(array.length - 1); + }); + + it('orderSearchArray test', () => { + expect(orderSearchArray(array, -1)).toBe(array.indexOf(0)); + expect(orderSearchArray(array, 0)).toBe(array.indexOf(1)); + expect(orderSearchArray(array, 4)).toBe(array.indexOf(5)); + expect(orderSearchArray(array, 5)).toBe(array.length - 1); + expect(orderSearchArray(array, 8)).toBe(array.length - 1); + }); + + it('binSearchFirstGreaterThanTarget test', () => { + expect(searchArray(array, -1)).toBe(array.indexOf(0)); + expect(binSearchFirstGreaterThanTarget(array, 0)).toBe(array.indexOf(1)); + expect(binSearchFirstGreaterThanTarget(array, 4)).toBe(array.indexOf(5)); + expect(binSearchFirstGreaterThanTarget(array, 5)).toBe(array.length - 1); + expect(binSearchFirstGreaterThanTarget(array, 8)).toBe(array.length - 1); + }); +}); diff --git a/packages/core/src/shared/array-search.ts b/packages/core/src/shared/array-search.ts index f691180e07f..c759d30d6e0 100644 --- a/packages/core/src/shared/array-search.ts +++ b/packages/core/src/shared/array-search.ts @@ -14,6 +14,61 @@ * limitations under the License. */ +/** + * Return the index of the first value in an ascending array that is greater than the target value. If there is no value greater than the target, return -1. + * + * Alternatively, you can consider inserting a number to ensure the array remains sorted, and return the position for insertion. If the target is the same as the maximum value, return -1. (or arr.length -1) + * @param arr + * @param target + */ +export function orderSearchArray(arr: number[], target: number) { + let left = 0; + let right = arr.length - 1; + + if (target < arr[0]) return 0; + if (target >= arr[arr.length - 1]) return arr.length - 1; + + while (left <= right) { + // When an equal value is found, it is necessary to find the position immediately following the last occurrence of that equal value. + if (arr[left] === target) { + while (left < arr.length && arr[left] === target) { + left++; + } + return left; + } + + if (target > arr[left] && target < arr[left + 1]) { + return left + 1; + } + + if (arr[right] === target) { + while (right < arr.length && arr[right] === target) { + right++; + } + return right; + } + + if (target > arr[right - 1] && target < arr[right]) { + return right; + } + + left++; + right--; + } + + return -1; +} + +/** + * Return the index of the first value in an ascending array that is greater than the target value. If there is no value greater than the target, return -1. + * + * + * binarySearchArray([0,1,2,3,4, 4, 4,5, 5, 5], 1) return 2 + * binarySearchArray([0,1,2,3,4, 4, 4, 5, 5, 5], 5) return -1, because max value 5 is not greater than target 5 + * binarySearchArray([0,1,2,3,4, 4, 4, 5, 5, 5], 8) return -1, because max value 5 is not greater than target 8 + * @param arr + * @param pos + */ export function binarySearchArray(arr: number[], pos: number) { let low: number = 0; let high = arr.length - 1; @@ -36,70 +91,67 @@ export function binarySearchArray(arr: number[], pos: number) { return -1; } -export function orderSearchArray(arr: number[], pos: number) { - let i = 0; - let cur = 0; - let cur_pre = 0; - let cur_index = -1; - let i_ed = arr.length - 1; - - while (i < arr.length && i_ed >= 0 && i_ed >= i) { - cur = arr[i_ed]; - - if (i_ed === 0) { - cur_pre = 0; - } else { - cur_pre = arr[i_ed - 1]; - } - - if (pos >= cur_pre && pos <= cur) { - cur_index = i_ed; - break; - } +/** + * Return the index of the first value in an ascending array that is greater than the target value. If there is no value greater than the target, return -1. + * + * Alternatively, you can consider inserting a number to ensure the array remains sorted, and return the position for insertion. If the target is the same as the maximum value, return -1. + * + * binarySearchArray([0,1,2,3,4, 4, 4,5, 5, 5], 1) return 2 + * binarySearchArray([0,1,2,3,4, 4, 4, 5, 5, 5], 5) return -1, because max value 5 is not greater than target 5 + * binarySearchArray([0,1,2,3,4, 4, 4, 5, 5, 5], 8) return -1, because max value 5 is not greater than target 8 + * @param arr + * @param target + */ +export function binSearchFirstGreaterThanTarget(arr: number[], target: number) { + let left = 0; + let right = arr.length; - cur = arr[i]; + while (left < right) { + const mid = Math.floor((left + right) / 2); - if (i === 0) { - cur_pre = 0; + // If the middle value is less than or equal to the target value, continue searching in the right half. + if (arr[mid] <= target) { + left = mid + 1; } else { - cur_pre = arr[i - 1]; - } - - if (pos >= cur_pre && pos < cur) { - cur_index = i; - break; + right = mid; } - - i++; - i_ed--; } - return cur_index; + // Returns the index of the first element greater than the target value; + // returns last index if no value in array is greater than target. + return left < arr.length ? left : arr.length - 1; } /** - * return the first index which arr[index] > num - * ex: searchArray([1, 3, 5, 7, 9], 7) = 4 + * Return the index of the first value in an ascending array that is greater than the target value. + * If target is greater than biggest value, return arr.length -1(last biggest value index). + * If target is equal to biggest value, return arr.length -1(last biggest value index); + * + * searchArray([0, 1, 2, 3, 4, 4, 4, 5, 5, 5], 1) return 2 + * searchArray([0, 1, 2, 3, 4, 4, 4, 5, 5, 5], 1) return 7 (first 5 index) + * searchArray([0, 1, 2, 3, 4, 4, 4, 5, 5, 5], 5) return 9 + * searchArray([0, 1, 2, 3, 4, 4, 4, 5, 5, 5], 8) return 9 + * * @param arr - * @param num + * @param target * @returns {number} index */ -export function searchArray(arr: number[], num: number) { +export function searchArray(arr: number[], target: number, firstMatch = false) { let index: number = arr.length - 1; - if (num < 0) { - return -1; - } - if (num < arr[0]) { + if (target < 0 || target < arr[0]) { return 0; } - if (num > arr[arr.length - 1]) { - return Number.POSITIVE_INFINITY; - } - if (arr.length < 40 || num <= arr[20] || num >= arr[index - 20]) { - index = orderSearchArray(arr, num); + // Excluding the special conditions mentioned above, the next return values can only be between 1 and length - 1. + if (arr.length < 40 || target <= arr[20] || target >= arr[index - 20]) { + index = orderSearchArray(arr, target); } else { - index = binarySearchArray(arr, num); + index = binSearchFirstGreaterThanTarget(arr, target); + } + + if (firstMatch) { + const val = arr[index]; + return arr.indexOf(val); } return index; diff --git a/packages/engine-render/src/components/sheets/sheet-skeleton.ts b/packages/engine-render/src/components/sheets/sheet-skeleton.ts index cb12bffcef8..0b0376d1c30 100644 --- a/packages/engine-render/src/components/sheets/sheet-skeleton.ts +++ b/packages/engine-render/src/components/sheets/sheet-skeleton.ts @@ -244,6 +244,11 @@ export interface ICacheItem { border: boolean; } +export interface IGetRowColByPosOptions { + closeFirst?: boolean; + visibleOnly?: boolean; +} + export class SpreadsheetSkeleton extends Skeleton { private _rowHeightAccumulation: number[] = []; private _columnWidthAccumulation: number[] = []; @@ -478,7 +483,7 @@ export class SpreadsheetSkeleton extends Skeleton { } if (bounds != null) { - const range = this.getRowColumnSegment(bounds); + const range = this.getRangeByBounding(bounds); this._visibleRange = range; this._visibleRangeMap.set(bounds.viewportKey as SHEET_VIEWPORT_KEY, range); } @@ -913,9 +918,8 @@ export class SpreadsheetSkeleton extends Skeleton { return Math.max(rowHeader.width, widthByComputation); } - getRowColumnSegment(bounds?: IViewportInfo): IRange { - return this._getBounding(this._rowHeightAccumulation, this._columnWidthAccumulation, bounds?.cacheBound); - // return this._getBounding(this._rowHeightAccumulation, this._columnWidthAccumulation, bounds?.viewBound); + getRangeByBounding(bounds?: IViewportInfo): IRange { + return this._getRangeByViewBounding(this._rowHeightAccumulation, this._columnWidthAccumulation, bounds?.cacheBound); } /** @@ -926,8 +930,8 @@ export class SpreadsheetSkeleton extends Skeleton { return this._worksheetData; } - getRowColumnSegmentByViewBound(bound?: IBoundRectNoAngle): IRange { - return this._getBounding(this._rowHeightAccumulation, this._columnWidthAccumulation, bound); + getRangeByViewBound(bound?: IBoundRectNoAngle): IRange { + return this._getRangeByViewBounding(this._rowHeightAccumulation, this._columnWidthAccumulation, bound); } getMergeBounding(startRow: number, startColumn: number, endRow: number, endColumn: number): IRange { @@ -1099,29 +1103,6 @@ export class SpreadsheetSkeleton extends Skeleton { }; } - /** - * Get cell by pos(offsetX, offsetY). - * @param offsetX HTML coordinate system, mouse position x. - * @param offsetY HTML coordinate system, mouse position y. - * @param scaleX render scene scale x-axis, scene.getAncestorScale - * @param scaleY render scene scale y-axis, scene.getAncestorScale - * @param scrollXY render viewport scroll {x, y}, scene.getScrollXYByRelativeCoords, scene.getScrollXY - * @param scrollXY.x - * @param scrollXY.y - * @returns Selection data with coordinates - */ - calculateCellIndexByPosition( - offsetX: number, - offsetY: number, - scaleX: number, - scaleY: number, - scrollXY: { x: number; y: number } - ): Nullable { - const { row, column } = this.getCellPositionByOffset(offsetX, offsetY, scaleX, scaleY, scrollXY); - - return this.getCellByIndex(row, column); - } - /** * * @param offsetX HTML coordinate system, mouse position x. @@ -1139,10 +1120,10 @@ export class SpreadsheetSkeleton extends Skeleton { scaleX: number, scaleY: number, scrollXY: { x: number; y: number }, - closeFirst?: boolean + options?: IGetRowColByPosOptions ): { row: number; column: number } { - const row = this.getRowPositionByOffsetY(offsetY, scaleY, scrollXY, closeFirst); - const column = this.getColumnPositionByOffsetX(offsetX, scaleX, scrollXY, closeFirst); + const row = this.getRowPositionByOffsetY(offsetY, scaleY, scrollXY, options); + const column = this.getColumnPositionByOffsetX(offsetX, scaleX, scrollXY, options); return { row, @@ -1151,27 +1132,17 @@ export class SpreadsheetSkeleton extends Skeleton { } /** - * - * @param offsetX scaled offset x + * Get column index by offsetX + * @param offsetX no scaled offset x * @param scaleX scale x * @param scrollXY - * @returns */ - - getColumnPositionByOffsetX(offsetX: number, scaleX: number, scrollXY: { x: number; y: number }, closeFirst?: boolean): number { + getColumnPositionByOffsetX(offsetX: number, scaleX: number, scrollXY: { x: number; y: number }, options?: IGetRowColByPosOptions): number { offsetX = this.getTransformOffsetX(offsetX, scaleX, scrollXY); - const { columnWidthAccumulation } = this; - let column = searchArray(columnWidthAccumulation, offsetX); - if (column === Number.POSITIVE_INFINITY) { - column = columnWidthAccumulation.length - 1; - } else if (column === -1) { - column = 0; - } - - if (closeFirst) { + if (options?.closeFirst) { // check if upper column was closer than current if (Math.abs(columnWidthAccumulation[column] - offsetX) < Math.abs(offsetX - (columnWidthAccumulation[column - 1] ?? 0))) { column = column + 1; @@ -1182,28 +1153,21 @@ export class SpreadsheetSkeleton extends Skeleton { } /** - * + * Get row index by offsetY * @param offsetY scaled offset y * @param scaleY scale y * @param scrollXY * @param scrollXY.x * @param scrollXY.y */ - getRowPositionByOffsetY(offsetY: number, scaleY: number, scrollXY: { x: number; y: number }, closeFirst?: boolean): number { + getRowPositionByOffsetY(offsetY: number, scaleY: number, scrollXY: { x: number; y: number }, options?: IGetRowColByPosOptions): number { const { rowHeightAccumulation } = this; - offsetY = this.getTransformOffsetY(offsetY, scaleY, scrollXY); - let row = searchArray(rowHeightAccumulation, offsetY); - - if (row === Number.POSITIVE_INFINITY) { - row = rowHeightAccumulation.length - 1; - } else if (row === -1) { - row = 0; - } + let row = searchArray(rowHeightAccumulation, offsetY, options?.visibleOnly); - if (closeFirst) { - // check if upper row was closer than current + if (options?.closeFirst) { + // check if next row was closer than current if (Math.abs(rowHeightAccumulation[row] - offsetY) < Math.abs(offsetY - (rowHeightAccumulation[row - 1] ?? 0))) { row = row + 1; } @@ -1513,19 +1477,17 @@ export class SpreadsheetSkeleton extends Skeleton { } getDecomposedOffset(offsetX: number, offsetY: number): { row: number; column: number; columnOffset: number; rowOffset: number } { - let column = searchArray(this._columnWidthAccumulation, offsetX); + const column = searchArray(this._columnWidthAccumulation, offsetX); let columnOffset = 0; - if (column === -1 || column === 0) { - column = 0; + if (column === 0) { columnOffset = offsetX; } else { columnOffset = offsetX - this._columnWidthAccumulation[column - 1]; } - let row = searchArray(this._rowHeightAccumulation, offsetY); + const row = searchArray(this._rowHeightAccumulation, offsetY); let rowOffset = 0; - if (row === -1 || row === 0) { - row = 0; + if (row === 0) { rowOffset = offsetY; } else { rowOffset = offsetY - this._rowHeightAccumulation[row - 1]; @@ -1659,75 +1621,40 @@ export class SpreadsheetSkeleton extends Skeleton { } /** - * + * Get the range of the bounding area of the canvas. * @param rowHeightAccumulation Row layout information * @param columnWidthAccumulation Column layout information * @param viewBound The range of the visible area of the canvas * @returns The range cell index of the canvas visible area */ - protected _getBounding( + protected _getRangeByViewBounding( rowHeightAccumulation: number[], columnWidthAccumulation: number[], viewBound?: IBoundRectNoAngle ): IRange { - const rhaLength = rowHeightAccumulation.length; - const cwaLength = columnWidthAccumulation.length; + const lenOfRowData = rowHeightAccumulation.length; + const lenOfColData = columnWidthAccumulation.length; if (!viewBound) { return { startRow: 0, - endRow: rhaLength - 1, + endRow: lenOfRowData - 1, startColumn: 0, - endColumn: cwaLength - 1, + endColumn: lenOfColData - 1, }; } - let dataset_row_st = -1; - let dataset_row_ed = -1; - let dataset_col_st = -1; - let dataset_col_ed = -1; - - const row_st = searchArray(rowHeightAccumulation, Math.round(viewBound.top) - this.columnHeaderHeightAndMarginTop); - const row_ed = searchArray(rowHeightAccumulation, Math.round(viewBound.bottom) - this.columnHeaderHeightAndMarginTop); - - if (row_st === -1 && row_ed === 0) { - dataset_row_st = 0; - dataset_row_ed = 0; - } else { - dataset_row_st = Math.max(0, row_st); - - if (row_ed === Number.POSITIVE_INFINITY) { - dataset_row_ed = rhaLength - 1; - } else if (row_ed >= rhaLength) { - dataset_row_ed = rhaLength - 1; - } else { - dataset_row_ed = Math.max(0, row_ed); - } - } - - const col_st = searchArray(columnWidthAccumulation, Math.round(viewBound.left) - this.rowHeaderWidthAndMarginLeft); - const col_ed = searchArray(columnWidthAccumulation, Math.round(viewBound.right) - this.rowHeaderWidthAndMarginLeft); - - if (col_st === -1 && col_ed === 0) { - dataset_col_st = 0; - dataset_col_ed = 0; - } else { - dataset_col_st = Math.max(0, col_st); - - if (col_ed === Number.POSITIVE_INFINITY) { - dataset_col_ed = cwaLength - 1; - } else if (col_ed >= cwaLength) { - dataset_col_ed = cwaLength - 1; - } else { - dataset_col_ed = Math.max(0, col_ed); - } - } + // viewBound contains header, so need to subtract the header height and margin + const startRow = searchArray(rowHeightAccumulation, Math.round(viewBound.top) - this.columnHeaderHeightAndMarginTop); + const endRow = searchArray(rowHeightAccumulation, Math.round(viewBound.bottom) - this.columnHeaderHeightAndMarginTop); + const startColumn = searchArray(columnWidthAccumulation, Math.round(viewBound.left) - this.rowHeaderWidthAndMarginLeft); + const endColumn = searchArray(columnWidthAccumulation, Math.round(viewBound.right) - this.rowHeaderWidthAndMarginLeft); return { - startRow: dataset_row_st, - endRow: dataset_row_ed, - startColumn: dataset_col_st, - endColumn: dataset_col_ed, + startRow, + endRow, + startColumn, + endColumn, } as IRange; } diff --git a/packages/engine-render/src/components/sheets/spreadsheet.ts b/packages/engine-render/src/components/sheets/spreadsheet.ts index ee724dca7aa..0613ff900ce 100644 --- a/packages/engine-render/src/components/sheets/spreadsheet.ts +++ b/packages/engine-render/src/components/sheets/spreadsheet.ts @@ -117,10 +117,10 @@ export class Spreadsheet extends SheetComponent { const parentScale = this.getParentScale(); const diffRanges = this._refreshIncrementalState && viewportInfo.diffBounds - ? viewportInfo.diffBounds?.map((bound) => spreadsheetSkeleton.getRowColumnSegmentByViewBound(bound)) + ? viewportInfo.diffBounds?.map((bound) => spreadsheetSkeleton.getRangeByViewBound(bound)) : []; - const viewRanges = [spreadsheetSkeleton.getRowColumnSegmentByViewBound(viewportInfo.cacheBound)]; + const viewRanges = [spreadsheetSkeleton.getRangeByViewBound(viewportInfo.cacheBound)]; const extensions = this.getExtensionsByOrder(); // At this moment, ctx.transform is at topLeft of sheet content, cell(0, 0) diff --git a/packages/sheets-ui/src/controllers/render-controllers/mobile/mobile-scroll.render-controller.ts b/packages/sheets-ui/src/controllers/render-controllers/mobile/mobile-scroll.render-controller.ts index 62157b932a4..a809a9020d0 100644 --- a/packages/sheets-ui/src/controllers/render-controllers/mobile/mobile-scroll.render-controller.ts +++ b/packages/sheets-ui/src/controllers/render-controllers/mobile/mobile-scroll.render-controller.ts @@ -498,7 +498,7 @@ export class MobileSheetsScrollRenderController extends Disposable implements IR } const bounds = viewport.getBounding(); - return skeleton.getRowColumnSegment(bounds); + return skeleton.getRangeByBounding(bounds); } // eslint-disable-next-line max-lines-per-function, complexity diff --git a/packages/sheets-ui/src/controllers/render-controllers/scroll.render-controller.ts b/packages/sheets-ui/src/controllers/render-controllers/scroll.render-controller.ts index 40e94c56a44..5737550df24 100644 --- a/packages/sheets-ui/src/controllers/render-controllers/scroll.render-controller.ts +++ b/packages/sheets-ui/src/controllers/render-controllers/scroll.render-controller.ts @@ -475,7 +475,7 @@ export class SheetsScrollRenderController extends Disposable implements IRenderM } const bounds = viewport.getBounding(); - return skeleton.getRowColumnSegmentByViewBound(bounds.viewBound); + return skeleton.getRangeByViewBound(bounds.viewBound); } // eslint-disable-next-line max-lines-per-function, complexity diff --git a/packages/sheets-ui/src/controllers/render-controllers/sheet.render-controller.ts b/packages/sheets-ui/src/controllers/render-controllers/sheet.render-controller.ts index 65014a7af6e..fb73e146f42 100644 --- a/packages/sheets-ui/src/controllers/render-controllers/sheet.render-controller.ts +++ b/packages/sheets-ui/src/controllers/render-controllers/sheet.render-controller.ts @@ -238,7 +238,6 @@ export class SheetRenderController extends RxDisposable implements IRenderModule private _initViewports(scene: Scene, rowHeader: { width: number }, columnHeader: { height: number }) { const bufferEdgeX = 100; const bufferEdgeY = 100; - const viewMain = new Viewport(SHEET_VIEWPORT_KEY.VIEW_MAIN, scene, { left: rowHeader.width, top: columnHeader.height, diff --git a/packages/sheets-ui/src/controllers/utils/component-tools.ts b/packages/sheets-ui/src/controllers/utils/component-tools.ts index f463c2a8fe6..a064278f0f6 100644 --- a/packages/sheets-ui/src/controllers/utils/component-tools.ts +++ b/packages/sheets-ui/src/controllers/utils/component-tools.ts @@ -122,7 +122,7 @@ export function getCoordByOffset( scaleX, scaleY, scrollXY, - closeFirst + { closeFirst } ); const { row, column } = moveActualSelection; diff --git a/packages/sheets-ui/src/services/selection/base-selection-render.service.ts b/packages/sheets-ui/src/services/selection/base-selection-render.service.ts index 1fd8cff4abb..dac618973ff 100644 --- a/packages/sheets-ui/src/services/selection/base-selection-render.service.ts +++ b/packages/sheets-ui/src/services/selection/base-selection-render.service.ts @@ -606,6 +606,7 @@ export class BaseSelectionRenderService extends Disposable implements ISheetSele scene.getActiveViewportByCoord(Vector2.FromArray([moveOffsetX, moveOffsetY])) ?? this._getViewportByCell(selection?.endRow, selection?.endColumn); + // find viewports that can be crossed by selection. const isCrossableViewports = () => { if (!startViewport || !endViewport || !viewportMain) { return false; @@ -752,13 +753,9 @@ export class BaseSelectionRenderService extends Disposable implements ISheetSele const scrollXY = scene.getViewportScrollXY(scene.getViewport(SHEET_VIEWPORT_KEY.VIEW_MAIN)!); const { scaleX, scaleY } = scene.getAncestorScale(); - return skeleton.calculateCellIndexByPosition( - x, - y, - scaleX, - scaleY, - scrollXY - ) as Nullable; + const { row, column } = skeleton.getCellPositionByOffset(x, y, scaleX, scaleY, scrollXY); + const cell = skeleton.getCellByIndex(row, column) as Nullable; + return cell; } /** @@ -909,13 +906,8 @@ export class BaseSelectionRenderService extends Disposable implements ISheetSele scrollXY: { x: number; y: number } ): Nullable { if (this._shouldDetectMergedCells) { - const primaryWithCoord = this._skeleton?.calculateCellIndexByPosition( - offsetX, - offsetY, - scaleX, - scaleY, - scrollXY - ); + const { row, column } = this._skeleton.getCellPositionByOffset(offsetX, offsetY, scaleX, scaleY, scrollXY, { visibleOnly: true }); + const primaryWithCoord = this._skeleton.getCellByIndex(row, column) as Nullable; if (!primaryWithCoord) return;