diff --git a/packages/core/src/shared/rectangle.ts b/packages/core/src/shared/rectangle.ts index 6688b473a8..c2f0194dcb 100644 --- a/packages/core/src/shared/rectangle.ts +++ b/packages/core/src/shared/rectangle.ts @@ -283,4 +283,30 @@ export class Rectangle { return result; } + + static hasIntersectionBetweenTwoBounds( + rect1: { + left: number; + top: number; + right: number; + bottom: number; + }, + rect2: { + left: number; + top: number; + right: number; + bottom: number; + } + ) { + if ( + rect1.left > rect2.right || // rect1 在 rect2 右侧 + rect1.right < rect2.left || // rect1 在 rect2 左侧 + rect1.top > rect2.bottom || // rect1 在 rect2 下方 + rect1.bottom < rect2.top // rect1 在 rect2 上方 + ) { + return false; + } + + return true; + } } diff --git a/packages/docs-ui/src/views/doc-canvas-view.ts b/packages/docs-ui/src/views/doc-canvas-view.ts index fed67edaf5..62ed751b58 100644 --- a/packages/docs-ui/src/views/doc-canvas-view.ts +++ b/packages/docs-ui/src/views/doc-canvas-view.ts @@ -107,6 +107,8 @@ export class DocCanvasView extends RxDisposable { top: 0, bottom: 0, right: 0, + isRelativeX: true, + isRelativeY: true, isWheelPreventDefaultX: true, }); diff --git a/packages/engine-render/src/base-object.ts b/packages/engine-render/src/base-object.ts index 732df6cfc2..2cc0b4b1ef 100644 --- a/packages/engine-render/src/base-object.ts +++ b/packages/engine-render/src/base-object.ts @@ -24,7 +24,7 @@ import type { IObjectFullState, ITransformChangeState } from './basics/interface import { TRANSFORM_CHANGE_OBSERVABLE_TYPE } from './basics/interfaces'; import { generateRandomKey, toPx } from './basics/tools'; import { Transform } from './basics/transform'; -import type { IViewportBound, Vector2 } from './basics/vector2'; +import type { IViewportInfo, Vector2 } from './basics/vector2'; import type { UniverRenderingContext } from './context'; import type { Layer } from './layer'; @@ -87,6 +87,7 @@ export abstract class BaseObject extends Disposable { protected _oKey: string; protected _dirty: boolean = true; + protected _forceDirty: boolean = true; private _top: number = 0; @@ -367,6 +368,10 @@ export abstract class BaseObject extends Disposable { return this; } + makeForceDirty(state: boolean = true) { + this._forceDirty = state; + } + makeDirtyNoDebounce(state: boolean = true) { this._dirty = state; if (state) { @@ -514,7 +519,7 @@ export abstract class BaseObject extends Disposable { return this; } - isRender(bounds?: IViewportBound) { + isRender(bounds?: IViewportInfo) { if (this._forceRender) { return false; } @@ -551,7 +556,7 @@ export abstract class BaseObject extends Disposable { this._makeDirtyMix(); } - render(ctx: UniverRenderingContext, bounds?: IViewportBound) { + render(ctx: UniverRenderingContext, bounds?: IViewportInfo) { /* abstract */ } diff --git a/packages/engine-render/src/basics/tools.ts b/packages/engine-render/src/basics/tools.ts index f5afd4564f..8188bd139f 100644 --- a/packages/engine-render/src/basics/tools.ts +++ b/packages/engine-render/src/basics/tools.ts @@ -24,7 +24,7 @@ import type { LocaleService, Nullable, } from '@univerjs/core'; -import { BaselineOffset, DEFAULT_STYLES, FontStyleType, Tools } from '@univerjs/core'; +import { BaselineOffset, DEFAULT_STYLES, FontStyleType, Rectangle, Tools } from '@univerjs/core'; import * as cjk from 'cjk-regex'; import { FontCache } from '../components/docs/layout/shaping-engine/font-cache'; @@ -757,3 +757,79 @@ export function ptToPixel(pt: number) { export function pixelToPt(px: number) { return px * PX_TO_PT_RATIO; } + +/** + * 当前单元格在任意一个 viewRanges 中 + * @param ranges + * @param rowIndex + * @param colIndex + * @returns + */ +export function inViewRanges(ranges: IRange[], rowIndex: number, colIndex: number) { + for (const range of ranges) { + if (rowIndex >= range.startRow && rowIndex <= range.endRow && + colIndex >= range.startColumn && colIndex <= range.endColumn) { + return true; + } + } + return false; +} + +/** + * 在非下方区域中 + * @param ranges + * @param rowIndex + * @returns + */ +export function inCurrentAndAboveViewRanges(ranges: IRange[], rowIndex: number) { + for (const range of ranges) { + if (rowIndex > range.endRow) { + return false; + } + } + return true; +} + +/** + * row 在任意一个 Range 中 + * @param ranges + * @param rowIndex + * @returns + */ +export function inRowViewRanges(ranges: IRange[], rowIndex: number) { + let flag = false; + for (const range of ranges) { + if (rowIndex >= range.startRow && rowIndex <= range.endRow) { + flag = true; + break; + } + } + return flag; +} + +/** + * 如果 range 有相交, 那么扩展到第一组 range 中. + * @param ranges + */ +export function mergeRangeIfIntersects(mainRanges: IRange[], ranges: IRange[]) { + for (const mainRange of mainRanges) { + for (const range of ranges) { + if (Rectangle.intersects(mainRange, range)) { + mainRange.startRow = Math.min(mainRange.startRow, range.startRow); + mainRange.endRow = Math.max(mainRange.endRow, range.endRow); + mainRange.startColumn = Math.min(mainRange.startColumn, range.startColumn); + mainRange.endColumn = Math.max(mainRange.endColumn, range.endColumn); + } + } + } + return mainRanges; +} + +export function clampRanges(range: IRange) { + return { + startRow: Math.max(0, range.startRow), + startColumn: Math.max(0, range.startColumn), + endRow: Math.max(0, range.endRow), + endColumn: Math.max(0, range.endColumn), + }; +} diff --git a/packages/engine-render/src/basics/transform.ts b/packages/engine-render/src/basics/transform.ts index 07e9eb94c9..5c0f1f0b5b 100644 --- a/packages/engine-render/src/basics/transform.ts +++ b/packages/engine-render/src/basics/transform.ts @@ -111,6 +111,8 @@ export class Transform { this._m[1] *= sx; this._m[2] *= sy; this._m[3] *= sy; + this._m[4] *= sx; + this._m[5] *= sy; return this; } @@ -387,6 +389,18 @@ export class Transform { return scaleMatrix; } + convert2DOMMatrix2D() { + const m = this.getMatrix(); + return { + a: m[0], + b: m[1], + c: m[2], + d: m[3], + e: m[4], + f: m[5], + }; + } + // static createTransformByState(state: positionState) { // const newTr = new this(); // let { x, y, angle, scaleX, scaleY, skewX, skewY, flipX, flipY } = state; diff --git a/packages/engine-render/src/basics/vector2.ts b/packages/engine-render/src/basics/vector2.ts index ca56c230e3..b9f9561a10 100644 --- a/packages/engine-render/src/basics/vector2.ts +++ b/packages/engine-render/src/basics/vector2.ts @@ -14,7 +14,10 @@ * limitations under the License. */ +import type { Canvas } from '../canvas'; +import type { SHEET_VIEWPORT_KEY } from '../components/sheets/interfaces'; import type { DeepImmutable, FloatArray } from './i-events'; +import type { Transform } from './transform'; export interface IPoint { x: number; @@ -844,17 +847,64 @@ export interface IBoundRect { } export interface IBoundRectNoAngle { + /** + * 冻结区域相对 MainCanvas 的物理位置, + * left = n * colWidth + rowHeaderWidth + */ left: number; top: number; right: number; bottom: number; } -export interface IViewportBound { +export interface IViewportInfo { viewBound: IBoundRectNoAngle; diffBounds: IBoundRectNoAngle[]; + + /** + * scroll right further diffX < 0 + * previewBound.x - viewbound.x + */ diffX: number; diffY: number; + + /** + * The physical position of the frozen rows and columns on the canvas, used for drawImage. + * For example, if the freezing starts from the fourth column, the left position would be 4 * column + rowHeaderWidth. + * The physical position means the top and left values have already considered the scaling factor. + */ viewPortPosition: IBoundRectNoAngle; - viewPortKey?: string; + viewportKey: string | SHEET_VIEWPORT_KEY; + /** + * In the future, a number will be used to indicate the reason for the "dirty" status + * Here, a binary value is used to facilitate computation. + */ + isDirty?: number; + isForceDirty?: boolean; + + allowCache?: boolean; + cacheBound: IBoundRectNoAngle; + diffCacheBounds: IBoundRectNoAngle[]; + cacheViewPortPosition: IBoundRectNoAngle; + + shouldCacheUpdate: number; + sceneTrans: Transform; + cacheCanvas?: Canvas; + + leftOrigin: number; + topOrigin: number; + + bufferEdgeX: number; + bufferEdgeY: number; + + updatePrevCacheBounds?: (viewbound: IBoundRectNoAngle) => void; +} + +export interface IViewportInfos { + left: number; + right: number; + top: number; + bottom: number; + width: number; + height: number; } diff --git a/packages/engine-render/src/canvas.ts b/packages/engine-render/src/canvas.ts index a89b692d7c..7714019d3c 100644 --- a/packages/engine-render/src/canvas.ts +++ b/packages/engine-render/src/canvas.ts @@ -130,21 +130,18 @@ export class Canvas { // this.setWidth(width || 0); // this.setHeight(height || 0); this._pixelRatio = pixelRatioParam || getDevicePixelRatio(); + const canvsElement = this.getCanvasEle(); - if (width) { - this.getCanvasEle().width = width * this._pixelRatio; - - this._width = this.getCanvasEle().width / this._pixelRatio; - - this.getCanvasEle().style.width = `${this._width}px`; + if (canvsElement && width !== undefined) { + canvsElement.width = width * this._pixelRatio; + this._width = canvsElement.width / this._pixelRatio; + canvsElement.style.width = `${this._width}px`; } - if (height) { - this.getCanvasEle().height = height * this._pixelRatio; - - this._height = this.getCanvasEle().height / this._pixelRatio; - - this.getCanvasEle().style.height = `${this._height}px`; + if (canvsElement && height !== undefined) { + canvsElement.height = height * this._pixelRatio; + this._height = canvsElement.height / this._pixelRatio; + canvsElement.style.height = `${this._height}px`; } this.getContext().setTransform(this._pixelRatio, 0, 0, this._pixelRatio, 0, 0); diff --git a/packages/engine-render/src/components/component.ts b/packages/engine-render/src/components/component.ts index 819145d587..418075aea9 100644 --- a/packages/engine-render/src/components/component.ts +++ b/packages/engine-render/src/components/component.ts @@ -17,7 +17,7 @@ import { DisposableCollection, sortRules, toDisposable } from '@univerjs/core'; import type { IDisposable } from '@wendellhu/redi'; import { BaseObject } from '../base-object'; -import type { IViewportBound } from '../basics/vector2'; +import type { IViewportInfo } from '../basics/vector2'; import type { UniverRenderingContext } from '../context'; import type { ComponentExtension } from './extension'; @@ -58,7 +58,7 @@ export class RenderComponent extends BaseObject { return this._extensions.get(uKey); } - draw(ctx: UniverRenderingContext, bounds?: IViewportBound) { + draw(ctx: UniverRenderingContext, bounds?: IViewportInfo) { /* abstract */ } diff --git a/packages/engine-render/src/components/docs/doc-background.ts b/packages/engine-render/src/components/docs/doc-background.ts index b1edc05902..a39df36c83 100644 --- a/packages/engine-render/src/components/docs/doc-background.ts +++ b/packages/engine-render/src/components/docs/doc-background.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { IViewportBound } from '../../basics/vector2'; +import type { IViewportInfo } from '../../basics/vector2'; import type { UniverRenderingContext } from '../../context'; import { Rect } from '../../shape'; import { Liquid } from './liquid'; @@ -40,7 +40,7 @@ export class DocBackground extends DocComponent { return new DocBackground(oKey, documentSkeleton, config); } - override draw(ctx: UniverRenderingContext, bounds?: IViewportBound) { + override draw(ctx: UniverRenderingContext, bounds?: IViewportInfo) { const skeletonData = this.getSkeleton()?.getSkeletonData(); if (skeletonData == null) { @@ -103,7 +103,7 @@ export class DocBackground extends DocComponent { return this; } - protected override _draw(ctx: UniverRenderingContext, bounds?: IViewportBound) { + protected override _draw(ctx: UniverRenderingContext, bounds?: IViewportInfo) { this.draw(ctx, bounds); } } diff --git a/packages/engine-render/src/components/docs/doc-component.ts b/packages/engine-render/src/components/docs/doc-component.ts index 76307b4886..365c210d65 100644 --- a/packages/engine-render/src/components/docs/doc-component.ts +++ b/packages/engine-render/src/components/docs/doc-component.ts @@ -17,7 +17,7 @@ import { RENDER_CLASS_TYPE } from '../../basics/const'; import type { IDocumentSkeletonGlyph, IDocumentSkeletonLine, IDocumentSkeletonPage } from '../../basics/i-document-skeleton-cached'; import { PageLayoutType } from '../../basics/i-document-skeleton-cached'; -import type { IBoundRectNoAngle, IViewportBound } from '../../basics/vector2'; +import type { IBoundRectNoAngle, IViewportInfo } from '../../basics/vector2'; import type { UniverRenderingContext } from '../../context'; import { RenderComponent } from '../component'; import type { DOCS_EXTENSION_TYPE } from './doc-extension'; @@ -82,7 +82,7 @@ export abstract class DocComponent extends RenderComponent< } } - override render(mainCtx: UniverRenderingContext, bounds?: IViewportBound) { + override render(mainCtx: UniverRenderingContext, bounds?: IViewportInfo) { if (!this.visible) { this.makeDirty(false); return this; @@ -118,7 +118,7 @@ export abstract class DocComponent extends RenderComponent< }; } - isSkipByDiffBounds(page: IDocumentSkeletonPage, pageTop: number, pageLeft: number, bounds?: IViewportBound) { + isSkipByDiffBounds(page: IDocumentSkeletonPage, pageTop: number, pageLeft: number, bounds?: IViewportInfo) { if (bounds === null || bounds === undefined) { return false; } @@ -146,5 +146,5 @@ export abstract class DocComponent extends RenderComponent< return false; } - protected abstract _draw(ctx: UniverRenderingContext, bounds?: IViewportBound): void; + protected abstract _draw(ctx: UniverRenderingContext, bounds?: IViewportInfo): void; } diff --git a/packages/engine-render/src/components/docs/document.ts b/packages/engine-render/src/components/docs/document.ts index 3b29cc9995..1b0df2d4b7 100644 --- a/packages/engine-render/src/components/docs/document.ts +++ b/packages/engine-render/src/components/docs/document.ts @@ -24,7 +24,7 @@ import type { IDocumentSkeletonCached, IDocumentSkeletonPage } from '../../basic import { LineType } from '../../basics/i-document-skeleton-cached'; import { degToRad } from '../../basics/tools'; import type { Transform } from '../../basics/transform'; -import type { IViewportBound } from '../../basics/vector2'; +import type { IViewportInfo } from '../../basics/vector2'; import { Vector2 } from '../../basics/vector2'; import type { UniverRenderingContext } from '../../context'; import type { Scene } from '../../scene'; @@ -164,7 +164,7 @@ export class Documents extends DocComponent { return (this.getScene() as Scene).getEngine(); } - override draw(ctx: UniverRenderingContext, bounds?: IViewportBound) { + override draw(ctx: UniverRenderingContext, bounds?: IViewportInfo) { const skeletonData = this.getSkeleton()?.getSkeletonData(); if (skeletonData == null) { @@ -466,7 +466,7 @@ export class Documents extends DocComponent { return this; } - protected override _draw(ctx: UniverRenderingContext, bounds?: IViewportBound) { + protected override _draw(ctx: UniverRenderingContext, bounds?: IViewportInfo) { this.draw(ctx, bounds); } diff --git a/packages/engine-render/src/components/docs/layout/doc-skeleton.ts b/packages/engine-render/src/components/docs/layout/doc-skeleton.ts index 2f900eea27..bf698228b9 100644 --- a/packages/engine-render/src/components/docs/layout/doc-skeleton.ts +++ b/packages/engine-render/src/components/docs/layout/doc-skeleton.ts @@ -24,7 +24,7 @@ import type { } from '../../../basics/i-document-skeleton-cached'; import { GlyphType, LineType, PageLayoutType } from '../../../basics/i-document-skeleton-cached'; import type { IDocsConfig, INodeInfo, INodePosition, INodeSearch } from '../../../basics/interfaces'; -import type { IViewportBound, Vector2 } from '../../../basics/vector2'; +import type { IViewportInfo, Vector2 } from '../../../basics/vector2'; import { Skeleton } from '../../skeleton'; import { Liquid } from '../liquid'; import type { DocumentViewModel } from '../view-model/document-view-model'; @@ -94,7 +94,7 @@ export class DocumentSkeleton extends Skeleton { } // Layout the document. - calculate(bounds?: IViewportBound) { + calculate(bounds?: IViewportInfo) { if (!this.dirty) { return; } @@ -555,7 +555,7 @@ export class DocumentSkeleton extends Skeleton { * @returns view model: skeleton */ - private _createSkeleton(ctx: ILayoutContext, _bounds?: IViewportBound): IDocumentSkeletonCached { + private _createSkeleton(ctx: ILayoutContext, _bounds?: IViewportInfo): IDocumentSkeletonCached { // console.log('createSkeleton: iterate ', this._iteratorCount, 'times'); const { viewModel, skeleton, skeletonResourceReference } = ctx; diff --git a/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts b/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts index 902041ecfc..74c2dd92a3 100644 --- a/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts +++ b/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts @@ -854,7 +854,7 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel return; } const unitId = this._docSkeleton.getViewModel().getDataModel().getUnitId(); - const key = `${unitId}_${viewport.viewPortKey}`; + const key = `${unitId}_${viewport.viewportKey}`; if (this._viewPortObserverMap.has(key)) { return; diff --git a/packages/engine-render/src/components/extension.ts b/packages/engine-render/src/components/extension.ts index 6f272926f7..3029e8a306 100644 --- a/packages/engine-render/src/components/extension.ts +++ b/packages/engine-render/src/components/extension.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { IDocumentRenderConfig, IScale, Nullable } from '@univerjs/core'; +import type { IDocumentRenderConfig, IRange, IScale, Nullable } from '@univerjs/core'; import { Registry } from '@univerjs/core'; import type { BaseObject } from '../base-object'; @@ -31,6 +31,11 @@ export interface IExtensionConfig { renderConfig?: IDocumentRenderConfig; } +export interface IDrawInfo { + viewRanges: IRange[]; + viewportKey: string; + checkOutOfViewBound?: boolean; +} export class ComponentExtension { uKey: string = ''; @@ -50,7 +55,7 @@ export class ComponentExtension { return this.Z_INDEX; } - draw(ctx: UniverRenderingContext, parentScale: IScale, skeleton: T, diffBounds?: V) { + draw(ctx: UniverRenderingContext, parentScale: IScale, skeleton: T, diffBounds?: V, more?: IDrawInfo) { /* abstract */ } diff --git a/packages/engine-render/src/components/sheets/column-header.ts b/packages/engine-render/src/components/sheets/column-header.ts index 05330caa0f..7d70624ee0 100644 --- a/packages/engine-render/src/components/sheets/column-header.ts +++ b/packages/engine-render/src/components/sheets/column-header.ts @@ -15,7 +15,7 @@ */ import type { Nullable } from '@univerjs/core'; -import type { IViewportBound, Vector2 } from '../../basics/vector2'; +import type { IViewportInfo, Vector2 } from '../../basics/vector2'; import type { UniverRenderingContext } from '../../context'; import { SheetColumnHeaderExtensionRegistry } from '../extension'; import type { ColumnHeaderLayout } from './extensions/column-header-layout'; @@ -49,7 +49,7 @@ export class SpreadsheetColumnHeader extends SpreadsheetHeader { return this._columnHeaderLayoutExtension; } - override draw(ctx: UniverRenderingContext, bounds?: IViewportBound) { + override draw(ctx: UniverRenderingContext, bounds?: IViewportInfo) { const spreadsheetSkeleton = this.getSkeleton(); if (!spreadsheetSkeleton) { return; diff --git a/packages/engine-render/src/components/sheets/extensions/background.ts b/packages/engine-render/src/components/sheets/extensions/background.ts index 76e53ee72e..9fdad10dec 100644 --- a/packages/engine-render/src/components/sheets/extensions/background.ts +++ b/packages/engine-render/src/components/sheets/extensions/background.ts @@ -16,8 +16,9 @@ import type { IRange, IScale } from '@univerjs/core'; -import { getColor } from '../../../basics/tools'; +import { fixLineWidthByScale, getColor, inViewRanges, mergeRangeIfIntersects } from '../../../basics/tools'; import type { UniverRenderingContext } from '../../../context'; +import type { IDrawInfo } from '../../extension'; import { SpreadsheetExtensionRegistry } from '../../extension'; import type { SpreadsheetSkeleton } from '../sheet-skeleton'; import type { Spreadsheet } from '../spreadsheet'; @@ -25,8 +26,12 @@ import { SheetExtension } from './sheet-extension'; const UNIQUE_KEY = 'DefaultBackgroundExtension'; -const DOC_EXTENSION_Z_INDEX = 40; -const PRINTING_Z_INDEX = 20; +/** + * in prev version background ext is higer than font ext. now turing back lower than font ext. + * font ext zindex is 30. + */ +const DOC_EXTENSION_Z_INDEX = 21; +const PRINTING_Z_INDEX = 21; export class Background extends SheetExtension { override uKey = UNIQUE_KEY; @@ -43,7 +48,8 @@ export class Background extends SheetExtension { ctx: UniverRenderingContext, parentScale: IScale, spreadsheetSkeleton: SpreadsheetSkeleton, - diffRanges?: IRange[] + diffRanges: IRange[], + { viewRanges, checkOutOfViewBound }: IDrawInfo ) { const { stylesCache } = spreadsheetSkeleton; const { background, backgroundPositions } = stylesCache; @@ -63,61 +69,54 @@ export class Background extends SheetExtension { return; } ctx.save(); - - ctx.setGlobalCompositeOperation('destination-over'); + const { scaleX, scaleY } = ctx.getScale(); background && Object.keys(background).forEach((rgb: string) => { const backgroundCache = background[rgb]; ctx.fillStyle = rgb || getColor([255, 255, 255])!; - ctx.beginPath(); + + const backgroundPaths = new Path2D(); backgroundCache.forValue((rowIndex, columnIndex) => { - const cellInfo = backgroundPositions?.getValue(rowIndex, columnIndex); + if (!checkOutOfViewBound && !inViewRanges(viewRanges, rowIndex, columnIndex)) { + return true; + } + const cellInfo = backgroundPositions?.getValue(rowIndex, columnIndex); if (cellInfo == null) { return true; } let { startY, endY, startX, endX } = cellInfo; const { isMerged, isMergedMainCell, mergeInfo } = cellInfo; - if (isMerged) { + const mergeTo = diffRanges && diffRanges.length > 0 ? diffRanges : viewRanges; + const combineWithMergeRanges = mergeRangeIfIntersects(mergeTo, [mergeInfo]); + + // If curr cell is not in the viewrange (viewport + merged cells), exit early. + if (!inViewRanges(combineWithMergeRanges!, rowIndex, columnIndex)) { return true; } - if ( - !this.isRenderDiffRangesByCell( - { - startRow: mergeInfo.startRow, - endRow: mergeInfo.endRow, - startColumn: mergeInfo.startColumn, - endColumn: mergeInfo.endColumn, - }, - diffRanges - ) - ) { + // For merged cells && cells that are not top-left, + // we need to use the background color of the top-left cell. + if (isMerged) { return true; } - // if ( - // !this.isRenderDiffRangesByColumn(mergeInfo.startColumn, diffRanges) && - // !this.isRenderDiffRangesByColumn(mergeInfo.endColumn, diffRanges) - // ) { - // return true; - // } - + // For merged cells, and the current cell is the top-left cell in the merged region. if (isMergedMainCell) { startY = mergeInfo.startY; endY = mergeInfo.endY; startX = mergeInfo.startX; endX = mergeInfo.endX; } - - ctx.moveToByPrecision(startX, startY); - ctx.lineToByPrecision(startX, endY); - ctx.lineToByPrecision(endX, endY); - ctx.lineToByPrecision(endX, startY); + // precise is a workaround for windows, macOS does not have this issue. + const startXPrecise = fixLineWidthByScale(startX, scaleX); + const startYPrecise = fixLineWidthByScale(startY, scaleY); + const endXPrecise = fixLineWidthByScale(endX, scaleX); + const endYPrecise = fixLineWidthByScale(endY, scaleY); + backgroundPaths.rect(startXPrecise, startYPrecise, endXPrecise - startXPrecise, endYPrecise - startYPrecise); }); - ctx.closePath(); - ctx.fill(); + ctx.fill(backgroundPaths); }); ctx.restore(); } diff --git a/packages/engine-render/src/components/sheets/extensions/border.ts b/packages/engine-render/src/components/sheets/extensions/border.ts index 4126897f1f..08d4278755 100644 --- a/packages/engine-render/src/components/sheets/extensions/border.ts +++ b/packages/engine-render/src/components/sheets/extensions/border.ts @@ -34,6 +34,7 @@ export class Border extends SheetExtension { override Z_INDEX = BORDER_Z_INDEX; + // eslint-disable-next-line max-lines-per-function override draw( ctx: UniverRenderingContext, parentScale: IScale, @@ -66,6 +67,7 @@ export class Border extends SheetExtension { const precisionScale = this._getScale(ctx.getScale()); + // eslint-disable-next-line max-lines-per-function border?.forValue((rowIndex, columnIndex, borderCaches) => { if (!borderCaches) { return true; diff --git a/packages/engine-render/src/components/sheets/extensions/font.ts b/packages/engine-render/src/components/sheets/extensions/font.ts index 2cc28bcf4c..ddc564b127 100644 --- a/packages/engine-render/src/components/sheets/extensions/font.ts +++ b/packages/engine-render/src/components/sheets/extensions/font.ts @@ -19,19 +19,21 @@ import type { ICellData, IRange, IScale, ObjectMatrix } from '@univerjs/core'; import { HorizontalAlign, WrapStrategy } from '@univerjs/core'; +import { VERTICAL_ROTATE_ANGLE } from '../../../basics/text-rotation'; +import { inRowViewRanges, inViewRanges, mergeRangeIfIntersects } from '../../../basics/tools'; import type { UniverRenderingContext } from '../../../context'; import type { Documents } from '../../docs/document'; +import type { IDrawInfo } from '../../extension'; import { SpreadsheetExtensionRegistry } from '../../extension'; import type { IFontCacheItem } from '../interfaces'; import type { SheetComponent } from '../sheet-component'; import { getDocsSkeletonPageSize, type SpreadsheetSkeleton } from '../sheet-skeleton'; -import { VERTICAL_ROTATE_ANGLE } from '../../../basics/text-rotation'; import { FIX_ONE_PIXEL_BLUR_OFFSET } from '../../../basics'; import { SheetExtension } from './sheet-extension'; const UNIQUE_KEY = 'DefaultFontExtension'; -const EXTENSION_Z_INDEX = 30; +const EXTENSION_Z_INDEX = 45; export interface ISheetFontRenderExtension { fontRenderExtension?: { leftOffset?: number; @@ -55,8 +57,10 @@ export class Font extends SheetExtension { ctx: UniverRenderingContext, parentScale: IScale, spreadsheetSkeleton: SpreadsheetSkeleton, - diffRanges?: IRange[] + diffRanges: IRange[], + moreBoundsInfo: IDrawInfo ) { + const { viewRanges = [], checkOutOfViewBound } = moreBoundsInfo; const { stylesCache, dataMergeCache, overflowCache, worksheet } = spreadsheetSkeleton; const { font: fontList } = stylesCache; if (!spreadsheetSkeleton || !worksheet) { @@ -76,15 +80,27 @@ export class Font extends SheetExtension { return; } ctx.save(); - const scale = this._getScale(parentScale); - fontList && Object.keys(fontList).forEach((fontFormat: string) => { const fontObjectArray = fontList[fontFormat]; + // Since the overflow can spill out to both the left and right sides, + // we need to consider the content outside the viewBounds. + // At the same time, there are also merged cells, so we need to merge the current + // viewrange and a single merged area when calculating. + + // Early exit from font condition + // If it's not an overflow and not within the field of view, we can exit early + // (the field of view needs to consider the impact of merged cells). + // eslint-disable-next-line complexity fontObjectArray.forValue((rowIndex, columnIndex, docsConfig) => { + if (!checkOutOfViewBound) { + if (!inViewRanges(viewRanges!, rowIndex, columnIndex)) { + return true; + } + } const cellInfo = this.getCellIndex( rowIndex, columnIndex, @@ -99,18 +115,33 @@ export class Font extends SheetExtension { return true; } + // If the merged cell area intersects with the current viewRange, + // then merge it into the current viewRange. + // After the merge, the font extension within the current viewBounds + // also needs to be drawn once. + // But at this moment, we cannot assume that it is not within the viewRanges and exit, because there may still be horizontal overflow. + // At this moment, we can only exclude the cells that are not within the current row. + const mergeTo = diffRanges && diffRanges.length > 0 ? diffRanges : viewRanges; + const combineWithMergeRanges = mergeRangeIfIntersects(mergeTo, [mergeInfo]); + if (!inRowViewRanges(combineWithMergeRanges, rowIndex)) { + return true; + } + if (isMergedMainCell) { startY = mergeInfo.startY; endY = mergeInfo.endY; startX = mergeInfo.startX; endX = mergeInfo.endX; } - /** * Incremental content rendering for texture mapping + * startRow endRow 和 diffRanges 在 row 上不相交, 那么返回不渲染 + * PS 如果这个单元格并不在 merge 区域内, mergeInfo start 和 end 就是单元格本身 */ - if (!this.isRenderDiffRangesByRow(mergeInfo.startRow, mergeInfo.endRow, diffRanges)) { - return true; + if (diffRanges) { + if (!this.isRowInRanges(mergeInfo.startRow, mergeInfo.endRow, diffRanges)) { + return true; + } } /** @@ -128,9 +159,18 @@ export class Font extends SheetExtension { return true; } + // If the cell is overflowing, but the overflowRectangle has not been set, + // then overflowRectangle is set to undefined. const overflowRectangle = overflowCache.getValue(rowIndex, columnIndex); const { horizontalAlign, vertexAngle = 0, centerAngle = 0 } = docsConfig; + // If it's neither an overflow nor within the current range, + // then we can exit early (taking into account the range extension + // caused by the merged cells). + if (!overflowRectangle && !inViewRanges(combineWithMergeRanges, rowIndex, columnIndex)) { + return true; + } + /** * https://github.com/dream-num/univer-pro/issues/334 * When horizontal alignment is not set, the default alignment for rotation angles varies to accommodate overflow scenarios. @@ -176,15 +216,15 @@ export class Font extends SheetExtension { cellHeight - 2 / scale ); ctx.clip(); - ctx.clearRectForTexture( - startX + 1 / scale, - startY + 1 / scale, - cellWidth - 2 / scale, - cellHeight - 2 / scale - ); + // ctx.clearRectForTexture( + // startX + 1 / scale, + // startY + 1 / scale, + // cellWidth - 2 / scale, + // cellHeight - 2 / scale + // ); } else { if (horizontalAlignOverFlow === HorizontalAlign.CENTER) { - this._clipRectangle( + this._clipRectangleForOverflow( ctx, startRow, endRow, @@ -195,7 +235,7 @@ export class Font extends SheetExtension { columnWidthAccumulation ); } else if (horizontalAlignOverFlow === HorizontalAlign.RIGHT) { - this._clipRectangle( + this._clipRectangleForOverflow( ctx, startRow, rowIndex, @@ -206,7 +246,7 @@ export class Font extends SheetExtension { columnWidthAccumulation ); } else { - this._clipRectangle( + this._clipRectangleForOverflow( ctx, rowIndex, endRow, @@ -220,17 +260,18 @@ export class Font extends SheetExtension { } } else { ctx.rectByPrecision(startX + 1 / scale, startY + 1 / scale, cellWidth - 2 / scale, cellHeight - 2 / scale); + // for normal cell, forbid text overflow cellarea ctx.clip(); - ctx.clearRectForTexture( - startX + 1 / scale, - startY + 1 / scale, - cellWidth - 2 / scale, - cellHeight - 2 / scale - ); + // ctx.clearRectForTexture( + // startX + 1 / scale, + // startY + 1 / scale, + // cellWidth - 2 / scale, + // cellHeight - 2 / scale + // ); } - ctx.translate(startX + FIX_ONE_PIXEL_BLUR_OFFSET, startY + FIX_ONE_PIXEL_BLUR_OFFSET); this._renderDocuments(ctx, docsConfig, startX, startY, endX, endY, rowIndex, columnIndex, overflowCache); + ctx.closePath(); ctx.restore(); }); }); @@ -291,7 +332,7 @@ export class Font extends SheetExtension { documents.changeSkeleton(documentSkeleton).render(ctx); } - private _clipRectangle( + private _clipRectangleForOverflow( ctx: UniverRenderingContext, startRow: number, endRow: number, @@ -309,7 +350,7 @@ export class Font extends SheetExtension { ctx.rectByPrecision(startX, startY, endX - startX, endY - startY); ctx.clip(); - ctx.clearRectForTexture(startX, startY, endX - startX, endY - startY); + // ctx.clearRectForTexture(startX, startY, endX - startX, endY - startY); } } diff --git a/packages/engine-render/src/components/sheets/extensions/marker.ts b/packages/engine-render/src/components/sheets/extensions/marker.ts index a96c43dc9f..7f75abe4d2 100644 --- a/packages/engine-render/src/components/sheets/extensions/marker.ts +++ b/packages/engine-render/src/components/sheets/extensions/marker.ts @@ -34,8 +34,7 @@ export class Marker extends SheetExtension { override uKey: string = UNIQUE_KEY; - // eslint-disable-next-line max-lines-per-function - override draw(ctx: UniverRenderingContext, parentScale: IScale, skeleton: SpreadsheetSkeleton, diffRanges?: IRange[] | undefined): void { + override draw(ctx: UniverRenderingContext, parentScale: IScale, skeleton: SpreadsheetSkeleton, diffRanges: IRange[]): void { const { worksheet, rowColumnSegment } = skeleton; if (!worksheet) { return; @@ -43,7 +42,6 @@ export class Marker extends SheetExtension { const mergeCellRendered = new Set(); - // eslint-disable-next-line max-lines-per-function Range.foreach(rowColumnSegment, (row, col) => { let cellData = worksheet.getCell(row, col); const cellInfo = this.getCellIndex( diff --git a/packages/engine-render/src/components/sheets/extensions/sheet-extension.ts b/packages/engine-render/src/components/sheets/extensions/sheet-extension.ts index 65882d76e0..a973139381 100644 --- a/packages/engine-render/src/components/sheets/extensions/sheet-extension.ts +++ b/packages/engine-render/src/components/sheets/extensions/sheet-extension.ts @@ -37,14 +37,14 @@ export class SheetExtension extends ComponentExtension= startRow && curStartRow <= endRow) { + return true; + } + if (curEndRow >= startRow && curEndRow <= endRow) { + return true; + } + + const isIntersect = Rectangle.intersects( + { + startRow: curStartRow, + endRow: curEndRow, + startColumn: 0, + endColumn: 0, + }, + { + startRow, + endRow, + startColumn: 0, + endColumn: 0, + } + ); + + if (isIntersect) { + return true; + } + } + + return false; + } } diff --git a/packages/engine-render/src/components/sheets/interfaces.ts b/packages/engine-render/src/components/sheets/interfaces.ts index 18409ca4f6..68c583fe98 100644 --- a/packages/engine-render/src/components/sheets/interfaces.ts +++ b/packages/engine-render/src/components/sheets/interfaces.ts @@ -25,6 +25,8 @@ import type { import type { BORDER_TYPE } from '../../basics/const'; import type { DocumentSkeleton } from '../docs/layout/doc-skeleton'; +import type { Canvas } from '../../canvas'; +import type { UniverRenderingContext } from '../../context'; export interface BorderCache { [key: string]: BorderCacheItem | {}; @@ -69,3 +71,39 @@ export enum ShowGridlinesState { OFF, ON, } + +export enum SHEET_VIEWPORT_KEY { + VIEW_MAIN = 'viewMain', + VIEW_MAIN_LEFT_TOP = 'viewMainLeftTop', + VIEW_MAIN_TOP = 'viewMainTop', + VIEW_MAIN_LEFT = 'viewMainLeft', + + VIEW_ROW_TOP = 'viewRowTop', + VIEW_ROW_BOTTOM = 'viewRowBottom', + VIEW_COLUMN_LEFT = 'viewColumnLeft', + VIEW_COLUMN_RIGHT = 'viewColumnRight', + VIEW_LEFT_TOP = 'viewLeftTop', +} + +export interface IPaintForRefresh { + cacheCanvas: Canvas; + cacheCtx: UniverRenderingContext; + mainCtx: UniverRenderingContext; + topOrigin: number; + leftOrigin: number; + bufferEdgeX: number; + bufferEdgeY: number; +} +export interface IPaintForScrolling { + cacheCanvas: Canvas; + cacheCtx: UniverRenderingContext; + mainCtx: UniverRenderingContext; + topOrigin: number; + leftOrigin: number; + bufferEdgeX: number; + bufferEdgeY: number; + rowHeaderWidth: number; + columnHeaderHeight: number; + scaleX: number; + scaleY: number; +} diff --git a/packages/engine-render/src/components/sheets/row-header.ts b/packages/engine-render/src/components/sheets/row-header.ts index 75cc065526..29029e99a8 100644 --- a/packages/engine-render/src/components/sheets/row-header.ts +++ b/packages/engine-render/src/components/sheets/row-header.ts @@ -15,7 +15,7 @@ */ import type { Nullable } from '@univerjs/core'; -import type { IViewportBound, Vector2 } from '../../basics/vector2'; +import type { IViewportInfo, Vector2 } from '../../basics/vector2'; import type { UniverRenderingContext } from '../../context'; import { SheetRowHeaderExtensionRegistry } from '../extension'; import type { RowHeaderLayout } from './extensions/row-header-layout'; @@ -50,7 +50,7 @@ export class SpreadsheetRowHeader extends SpreadsheetHeader { return this._rowHeaderLayoutExtension; } - override draw(ctx: UniverRenderingContext, bounds?: IViewportBound) { + override draw(ctx: UniverRenderingContext, bounds?: IViewportInfo) { const spreadsheetSkeleton = this.getSkeleton(); if (!spreadsheetSkeleton) { return; diff --git a/packages/engine-render/src/components/sheets/sheet-component.ts b/packages/engine-render/src/components/sheets/sheet-component.ts index d3f5e39302..6dab0de118 100644 --- a/packages/engine-render/src/components/sheets/sheet-component.ts +++ b/packages/engine-render/src/components/sheets/sheet-component.ts @@ -17,7 +17,7 @@ import type { IRange, Nullable } from '@univerjs/core'; import { RENDER_CLASS_TYPE } from '../../basics/const'; -import type { IViewportBound, Vector2 } from '../../basics/vector2'; +import type { IViewportInfo, Vector2 } from '../../basics/vector2'; import type { UniverRenderingContext } from '../../context'; import { RenderComponent } from '../component'; import type { SHEET_EXTENSION_TYPE } from './extensions/sheet-extension'; @@ -39,7 +39,7 @@ export abstract class SheetComponent extends RenderComponent; - protected abstract _draw(ctx: UniverRenderingContext, bounds?: IViewportBound): void; + protected abstract _draw(ctx: UniverRenderingContext, bounds?: IViewportInfo): void; /** * TODO: DR-Univer, fix as unknown as @@ -94,7 +94,7 @@ export abstract class SheetComponent extends RenderComponent { - (parent as Scene)?.getEngine()?.onTransformChangeObservable.add(() => { - this._resizeCacheCanvas(); - }); - this._resizeCacheCanvas(); - this._addMakeDirtyToScroll(); - }); - } - this._initialDefaultExtension(); + this.makeDirty(true); } @@ -118,14 +102,22 @@ export class Spreadsheet extends SheetComponent { super.dispose(); this._documents?.dispose(); this._documents = null as unknown as Documents; - this._cacheCanvas?.dispose(); - this._cacheCanvas = null as unknown as Canvas; + // cacheCanvas 已经移动到 viewport 中了, cacheCanvas 的 dispose 在 viewport@dispose 中处理 + // this._cacheCanvas?.dispose(); + // this._cacheCanvas = null as unknown as Canvas; this._backgroundExtension = null as unknown as Background; this._borderExtension = null as unknown as Border; this._fontExtension = null as unknown as Font; } - override draw(ctx: UniverRenderingContext, bounds?: IViewportBound) { + /** + * draw by viewport + * cacheBound ---> viewBound + * diffCacheBounds ---> diffRange + * @param ctx + * @param viewportInfo + */ + override draw(ctx: UniverRenderingContext, viewportInfo: IViewportInfo) { // const { parent = { scaleX: 1, scaleY: 1 } } = this; // const mergeData = this.getMergeData(); // const showGridlines = this.getShowGridlines() || 1; @@ -133,15 +125,23 @@ export class Spreadsheet extends SheetComponent { if (!spreadsheetSkeleton) { return; } - + // this._drawAuxiliary(ctx); const parentScale = this.getParentScale(); - const diffRanges = this._refreshIncrementalState - ? bounds?.diffBounds.map((bound) => spreadsheetSkeleton.getRowColumnSegmentByViewBound(bound)) + const diffRanges = this._refreshIncrementalState && viewportInfo?.diffBounds + ? viewportInfo?.diffBounds?.map((bound) => spreadsheetSkeleton.getRowColumnSegmentByViewBound(bound)) : undefined; + const viewRanges = [spreadsheetSkeleton.getRowColumnSegmentByViewBound(viewportInfo?.cacheBound)]; const extensions = this.getExtensionsByOrder(); + + // At this moment, ctx.transform is at topLeft of sheet content, cell(0, 0) for (const extension of extensions) { - extension.draw(ctx, parentScale, spreadsheetSkeleton, diffRanges); + // const timeKey = `extension ${viewportInfo.viewPortKey}:${extension.constructor.name}`; + extension.draw(ctx, parentScale, spreadsheetSkeleton, diffRanges, { + viewRanges, + checkOutOfViewBound: true, + viewportKey: viewportInfo.viewportKey, + }); } } @@ -163,10 +163,6 @@ export class Spreadsheet extends SheetComponent { if (!spreadsheetSkeleton) { return; } - - // this.onPointerDownObserver.add((evt) => { - // evt.offsetX; - // }); const { rowHeightAccumulation, columnWidthAccumulation, rowHeaderWidth, columnHeaderHeight } = spreadsheetSkeleton; @@ -196,8 +192,8 @@ export class Spreadsheet extends SheetComponent { let y = 0; const viewPort = scene.getActiveViewportByRelativeCoord(coord); if (viewPort) { - const actualX = viewPort.actualScrollX || 0; - const actualY = viewPort.actualScrollY || 0; + const actualX = viewPort.viewportScrollX || 0; + const actualY = viewPort.viewportScrollY || 0; x += actualX; y += actualY; } @@ -207,7 +203,15 @@ export class Spreadsheet extends SheetComponent { }; } - makeForceDirty(state = true) { + isForceDirty(): boolean { + return this._forceDirty; + } + + /** + * canvas resize & zoom would call forceDirty + * @param state + */ + override makeForceDirty(state = true) { this.makeDirty(state); this._forceDirty = state; } @@ -220,138 +224,201 @@ export class Spreadsheet extends SheetComponent { return this.getSkeleton()?.getMergeBounding(startRow, startColumn, endRow, endColumn); } - override render(mainCtx: UniverRenderingContext, bounds?: IViewportBound) { + /** + * @param state + */ + override makeDirty(state: boolean = true) { + (this.getParent() as Scene)?.getViewports().forEach((vp) => vp.markDirty(state)); + super.makeDirty(state); + if (state === false) { + this._dirtyBounds = []; + } + return this; + } + + setDirtyArea(dirtyBounds: IBoundRectNoAngle[]) { + this._dirtyBounds = dirtyBounds; + } + + renderByViewport(mainCtx: UniverRenderingContext, viewportInfo: IViewportInfo, spreadsheetSkeleton: SpreadsheetSkeleton) { + const { diffBounds, diffX, diffY, viewPortPosition, cacheCanvas, leftOrigin, topOrigin, bufferEdgeX, bufferEdgeY, isDirty: isViewportDirty, isForceDirty: isViewportForceDirty } = viewportInfo as Required; + const { rowHeaderWidth, columnHeaderHeight } = spreadsheetSkeleton; + const { a: scaleX = 1, d: scaleY = 1 } = mainCtx.getTransform(); + const bufferEdgeSizeX = bufferEdgeX * scaleX / window.devicePixelRatio; + const bufferEdgeSizeY = bufferEdgeY * scaleY / window.devicePixelRatio; + + const cacheCtx = cacheCanvas.getContext(); + cacheCtx.save(); + const { left, top, right, bottom } = viewPortPosition; + const dw = right - left + rowHeaderWidth; + const dh = bottom - top + columnHeaderHeight; + const isForceDirty = isViewportForceDirty || this.isForceDirty(); + const isDirty = isViewportDirty || this.isDirty(); + if (diffBounds.length === 0 || (diffX === 0 && diffY === 0) || isForceDirty || isDirty) { + if (isDirty || isForceDirty) { + this.refreshCacheCanvas(viewportInfo, { cacheCanvas, cacheCtx, mainCtx, topOrigin, leftOrigin, bufferEdgeX, bufferEdgeY }); + } + } else if (diffBounds.length !== 0 || diffX !== 0 || diffY !== 0) { + // scrolling && no dirty + this.paintNewAreaForScrolling(viewportInfo, { + cacheCanvas, cacheCtx, mainCtx, topOrigin, leftOrigin, bufferEdgeX, bufferEdgeY, scaleX, scaleY, columnHeaderHeight, rowHeaderWidth, + }); + } + // support for browser native zoom (only windows has this problem) + const sourceLeft = bufferEdgeSizeX * Math.min(1, window.devicePixelRatio); + const sourceTop = bufferEdgeSizeY * Math.min(1, window.devicePixelRatio); + this._applyCache(cacheCanvas, mainCtx, sourceLeft, sourceTop, dw, dh, left, top, dw, dh); + cacheCtx.restore(); + } + + paintNewAreaForScrolling(viewportInfo: IViewportInfo, param: IPaintForScrolling) { + const { cacheCanvas, cacheCtx, mainCtx, topOrigin, leftOrigin, bufferEdgeX, bufferEdgeY, scaleX, scaleY, columnHeaderHeight, rowHeaderWidth } = param; + const { shouldCacheUpdate, diffCacheBounds, diffX, diffY } = viewportInfo; + cacheCtx.save(); + cacheCtx.setTransform(1, 0, 0, 1, 0, 0); + cacheCtx.globalCompositeOperation = 'copy'; + cacheCtx.drawImage(cacheCanvas.getCanvasEle(), diffX * scaleX, diffY * scaleY); + cacheCtx.restore(); + + this._refreshIncrementalState = true; + // Reset the ctx position to the spreadsheet content origin before drawing. + // trasnlation should be (rowHeaderWidth, colHeaderHeight) at start. + const m = mainCtx.getTransform(); + cacheCtx.setTransform(m.a, m.b, m.c, m.d, 0, 0); + + // leftOrigin 是 viewport 相对 sheetcorner 的偏移(不考虑缩放) + // - (leftOrigin - bufferEdgeX) ----> 简化 - leftOrigin + bufferEdgeX + cacheCtx.translateWithPrecision(m.e / m.a - leftOrigin + bufferEdgeX, m.f / m.d - topOrigin + bufferEdgeY); + + if (shouldCacheUpdate) { + for (const diffBound of diffCacheBounds) { + const { left: diffLeft, right: diffRight, bottom: diffBottom, top: diffTop } = diffBound; + + // this.draw 的时候 ctx.translate 单元格偏移是相对 spreadsheet content + // 但是 diffBounds 包括 rowHeader columnWidth, 因此绘制前需要减去行头列头的偏移 + const x = diffLeft - rowHeaderWidth; + const y = diffTop - columnHeaderHeight; + const w = diffRight - diffLeft; + const h = diffBottom - diffTop; // w h 必须精确和 diffarea 大小匹配, 否则会造成往回滚时, clear 的区域过大, 导致上一帧有效内容被擦除 + + cacheCtx.clearRectByPrecision(x, y, w, h); + // cacheCtx.fillStyle = this.testGetRandomLightColor(); + // cacheCtx.fillRectByPrecision(x, y, w, h); // x, y is diffBounds, means it's relative to scrolling distance. + + cacheCtx.save(); + cacheCtx.beginPath(); + cacheCtx.rectByPrecision(x, y, w, h); + cacheCtx.closePath(); + // The reason for clipping here is to avoid duplicate drawing (otherwise the text would be jagged, especially on Windows) + cacheCtx.clip(); + this.draw(cacheCtx, { + ...viewportInfo, + diffBounds: [diffBound], + }); + cacheCtx.restore(); + } + } + + // this.testShowRuler(cacheCtx, viewportInfo); + this._refreshIncrementalState = false; + } + + /** + * Redraw the entire viewport. + */ + refreshCacheCanvas(viewportInfo: IViewportInfo, param: IPaintForRefresh) { + const { cacheCanvas, cacheCtx, mainCtx, topOrigin, leftOrigin, bufferEdgeX, bufferEdgeY } = param; + cacheCtx.save(); + cacheCtx.setTransform(1, 0, 0, 1, 0, 0); + cacheCanvas.clear(); + cacheCtx.restore(); + + cacheCtx.save(); + // 所以 cacheCtx.setTransform 已经包含了 rowHeaderWidth + viewport + scroll 距离 + const m = mainCtx.getTransform(); + // cacheCtx.setTransform(m.a, m.b, m.c, m.d, m.e, m.f); + cacheCtx.setTransform(m.a, m.b, m.c, m.d, 0, 0); + + // The 'leftOrigin' is the offset of the viewport relative to the sheet corner, which is the position of cell(0, 0), and it does not consider scaling. + // - (leftOrigin - bufferEdgeX) ----> - leftOrigin + bufferEdgeX + cacheCtx.translateWithPrecision(m.e / m.a - leftOrigin + bufferEdgeX, m.f / m.d - topOrigin + bufferEdgeY); + // extension 绘制时按照内容的左上角计算, 不考虑 rowHeaderWidth + this.draw(cacheCtx, viewportInfo); + // this.testShowRuler(cacheCtx, viewportInfo); + cacheCtx.restore(); + } + + override render(mainCtx: UniverRenderingContext, viewportInfo: IViewportInfo) { if (!this.visible) { this.makeDirty(false); return this; } const spreadsheetSkeleton = this.getSkeleton(); - if (!spreadsheetSkeleton) return; - spreadsheetSkeleton.calculateWithoutClearingCache(bounds); + if (!spreadsheetSkeleton) { + return; + } + spreadsheetSkeleton.calculateWithoutClearingCache(viewportInfo); const segment = spreadsheetSkeleton.rowColumnSegment; if ( (segment.startRow === -1 && segment.endRow === -1) || - (segment.startColumn === -1 && segment.endColumn === -1) + (segment.startColumn === -1 && segment.endColumn === -1) ) { return; } - mainCtx.save(); - const { a: scaleX = 1, d: scaleY = 1 } = mainCtx.getTransform(); - const { rowHeaderWidth, columnHeaderHeight } = spreadsheetSkeleton; - mainCtx.translateWithPrecision(rowHeaderWidth, columnHeaderHeight); - this._drawAuxiliary(mainCtx, bounds); - - if (bounds && this._allowCache === true) { - const { viewBound, diffBounds, diffX, diffY, viewPortPosition, viewPortKey } = bounds; - - // if (viewPortKey === 'viewColumnRight' || viewPortKey === 'viewRowBottom' || viewPortKey === 'viewLeftTop') { - // // console.warn('ignore object', this); - // return; - // } - - // if ( - // viewPortKey === 'viewRowTop' || - // viewPortKey === 'viewRowBottom' || - // viewPortKey === 'viewMainLeft' || - // viewPortKey === 'viewMainTop' - // ) { - // // console.warn('ignore object', this); - // return; - // } - - if (viewPortKey === 'viewMain') { - const ctx = this._cacheCanvas.getContext(); - ctx.save(); - - const { left, top, right, bottom } = viewPortPosition; - - const dw = right - left + rowHeaderWidth; - - const dh = bottom - top + columnHeaderHeight; - - if (diffBounds.length === 0 || (diffX === 0 && diffY === 0) || this._forceDirty) { - if (this.isDirty() || this._forceDirty) { - this._cacheCanvas.clear(); - ctx.setTransform(mainCtx.getTransform()); - this._draw(ctx, bounds); - this._forceDirty = false; - } - this._applyCache(mainCtx, left, top, dw, dh, left, top, dw, dh); - } else { - if (this.isDirty()) { - ctx.save(); - ctx.globalCompositeOperation = 'copy'; - ctx.setTransform(1, 0, 0, 1, 0, 0); - ctx.drawImage(this._cacheCanvas.getCanvasEle(), diffX * scaleX, diffY * scaleY); - ctx.restore(); - - this._refreshIncrementalState = true; - ctx.setTransform(mainCtx.getTransform()); - - for (const diffBound of diffBounds) { - const { left: diffLeft, right: diffRight, bottom: diffBottom, top: diffTop } = diffBound; - ctx.save(); - ctx.beginPath(); - ctx.rectByPrecision( - diffLeft - rowHeaderWidth - FIX_ONE_PIXEL_BLUR_OFFSET, - diffTop - columnHeaderHeight - FIX_ONE_PIXEL_BLUR_OFFSET, - diffRight - diffLeft + rowHeaderWidth + FIX_ONE_PIXEL_BLUR_OFFSET * 2, - diffBottom - diffTop + columnHeaderHeight + FIX_ONE_PIXEL_BLUR_OFFSET * 2 - ); - // ctx.fillStyle = 'rgb(0,0,0)'; - - ctx.clip(); - this._draw(ctx, { - viewBound: bounds.viewBound, - diffBounds: [diffBound], - diffX: bounds.diffX, - diffY: bounds.diffY, - viewPortPosition: bounds.viewPortPosition, - viewPortKey: bounds.viewPortKey, - }); - ctx.restore(); - } - - this._refreshIncrementalState = false; - } - this._applyCache(mainCtx, left, top, dw, dh, left, top, dw, dh); - } - - ctx.restore(); + const { viewportKey } = viewportInfo; + // scene --> layer, getObjects --> viewport.render(object) --> spreadsheet + // zIndex 0 spreadsheet this.getObjectsByOrder() ---> [spreadsheet] + // zIndex 2 rowHeader & colHeader & freezeBorder this.getObjectsByOrder() ---> [SpreadsheetRowHeader, SpreadsheetColumnHeader, _Rect] + // zIndex 3 selection this.getObjectsByOrder() ---> [group] + + // SpreadsheetRowHeader SpreadsheetColumnHeader is not render by spreadsheet + if (this.sheetContentViewport().includes(viewportKey as SHEET_VIEWPORT_KEY)) { + if (viewportInfo && viewportInfo.cacheCanvas) { + this.renderByViewport(mainCtx, viewportInfo, spreadsheetSkeleton); } else { - this._draw(mainCtx, bounds); + this._draw(mainCtx, viewportInfo); } + } else if (this.sheetHeaderViewport().includes(viewportKey as SHEET_VIEWPORT_KEY)) { + // doing nothing, other components(SpreadsheetRowHeader...) will render } else { - this._draw(mainCtx, bounds); + // embed in doc & slide + // 现阶段 embed 开启贴图有问题, 3f12ad80188a83283bcd95c65e6c5dcc2d23ad72 + if (viewportInfo && viewportInfo.cacheCanvas) { + this.renderByViewport(mainCtx, viewportInfo, spreadsheetSkeleton); + } else { + this._draw(mainCtx, viewportInfo); + } } mainCtx.restore(); - - this.makeDirty(false); return this; } - private _resizeCacheCanvas() { - const parentSize = this._getAncestorSize(); - if (!parentSize || this._cacheCanvas == null) { - return; - } - const { width, height } = parentSize; - this._cacheCanvas.setSize(width, height); - this.makeForceDirty(true); - } - + /** + * applyCache from cache canvas + * @param cacheCanvas Source Image + * @param ctx MainCtx + * @param sx + * @param sy + * @param sw + * @param sh + * @param dx + * @param dy + * @param dw + * @param dh + */ protected _applyCache( - ctx?: UniverRenderingContext, + cacheCanvas: Canvas, + ctx: UniverRenderingContext, sx: number = 0, sy: number = 0, sw: number = 0, @@ -365,21 +432,26 @@ export class Spreadsheet extends SheetComponent { return; } - const pixelRatio = this._cacheCanvas.getPixelRatio(); - - const cacheCtx = this._cacheCanvas.getContext(); + const pixelRatio = cacheCanvas.getPixelRatio(); + const cacheCtx = cacheCanvas.getContext(); cacheCtx.save(); ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); cacheCtx.setTransform(1, 0, 0, 1, 0, 0); + const fn = (num: number, scale: number) => { + return Math.round(num * scale); + }; + ctx.imageSmoothingEnabled = false; + // ctx.imageSmoothingEnabled = true; + // ctx.imageSmoothingQuality = 'high'; ctx.drawImage( - this._cacheCanvas.getCanvasEle(), - sx * pixelRatio, - sy * pixelRatio, + cacheCanvas.getCanvasEle(), + fn(sx, pixelRatio), + fn(sy, pixelRatio), sw * pixelRatio, sh * pixelRatio, - dx * pixelRatio, - dy * pixelRatio, + fn(dx, pixelRatio), + fn(dy, pixelRatio), dw * pixelRatio, dh * pixelRatio ); @@ -387,8 +459,8 @@ export class Spreadsheet extends SheetComponent { cacheCtx.restore(); } - protected override _draw(ctx: UniverRenderingContext, bounds?: IViewportBound) { - this.draw(ctx, bounds); + protected override _draw(ctx: UniverRenderingContext, bounds?: IViewportInfo) { + this.draw(ctx, bounds!); } private _getAncestorSize() { @@ -434,49 +506,18 @@ export class Spreadsheet extends SheetComponent { this._fontExtension = this.getExtensionByKey('DefaultFontExtension') as Font; } - private _addMakeDirtyToScroll() { - this._hasScrollViewportOperator(this, (viewport: Viewport) => { - viewport.onScrollBeforeObserver.add(() => { - this.makeDirty(true); - }); - }); - } - - private _hasScrollViewportOperator(object: BaseObject, fn: (viewPort: Viewport) => void) { - let parent: any = object.getParent(); - while (parent) { - if (parent.classType === RENDER_CLASS_TYPE.SCENE) { - const viewports = parent.getViewports(); - const viewPorts = this._getHasScrollViewports(viewports); - for (const viewport of viewPorts) { - if (viewport) { - fn(viewport); - } - } - } - parent = parent?.getParent && parent?.getParent(); - } - } - - private _getHasScrollViewports(viewports: Viewport[]) { - const newViewports: Viewport[] = []; - for (const viewport of viewports) { - const scrollBar = viewport.getScrollBar(); - if (scrollBar) { - newViewports.push(viewport); - } - } - return newViewports; - } - - private _drawAuxiliary(ctx: UniverRenderingContext, bounds?: IViewportBound) { + /** + * draw gridlines + * @param ctx + */ + // eslint-disable-next-line max-lines-per-function + private _drawAuxiliary(ctx: UniverRenderingContext) { const spreadsheetSkeleton = this.getSkeleton(); if (spreadsheetSkeleton == null) { return; } - const { rowColumnSegment, dataMergeCache, overflowCache, stylesCache, showGridlines } = spreadsheetSkeleton; - const { border, backgroundPositions } = stylesCache; + const { rowColumnSegment, dataMergeCache, overflowCache, showGridlines } = spreadsheetSkeleton; const { startRow, endRow, startColumn, endColumn } = rowColumnSegment; if (!spreadsheetSkeleton || showGridlines === BooleanNumber.FALSE || this._forceDisableGridlines) { return; @@ -592,7 +633,7 @@ export class Spreadsheet extends SheetComponent { // overflow cell this._clearRectangle(ctx, rowHeightAccumulation, columnWidthAccumulation, overflowCache.toNativeArray()); - this._clearBackground(ctx, backgroundPositions); + // this._clearBackground(ctx, backgroundPositions); ctx.restore(); } @@ -651,4 +692,88 @@ export class Spreadsheet extends SheetComponent { ctx.clearRectForTexture(startX, startY, endX - startX + 0.5, endY - startY + 0.5); }); } + + sheetContentViewport() { + return [SHEET_VIEWPORT_KEY.VIEW_MAIN, SHEET_VIEWPORT_KEY.VIEW_MAIN_LEFT_TOP, SHEET_VIEWPORT_KEY.VIEW_MAIN_TOP, SHEET_VIEWPORT_KEY.VIEW_MAIN_LEFT]; + } + + sheetHeaderViewport() { + return [SHEET_VIEWPORT_KEY.VIEW_ROW_TOP, SHEET_VIEWPORT_KEY.VIEW_ROW_BOTTOM, SHEET_VIEWPORT_KEY.VIEW_COLUMN_LEFT, SHEET_VIEWPORT_KEY.VIEW_COLUMN_RIGHT, SHEET_VIEWPORT_KEY.VIEW_LEFT_TOP]; + } + + testShowRuler(cacheCtx: UniverRenderingContext, viewportInfo: IViewportInfo): void { + const { cacheBound } = viewportInfo; + const spreadsheetSkeleton = this.getSkeleton()!; + const { rowHeaderWidth, columnHeaderHeight } = spreadsheetSkeleton; + const { left, top, right, bottom } = cacheBound; + // left -= rowHeaderWidth; + // top -= columnHeaderHeight; + // right -= rowHeaderWidth; + // bottom -= columnHeaderHeight; + const findClosestHundred = (number: number) => { + const remainder = number % 100; + return number + (100 - remainder); + }; + const startX = findClosestHundred(left); + const endX = findClosestHundred(right); + const startY = findClosestHundred(top); + const endY = findClosestHundred(bottom); + cacheCtx.save(); + cacheCtx.beginPath(); + cacheCtx.strokeStyle = '#000000'; + cacheCtx.fillStyle = '#000000'; + cacheCtx.font = '16px Arial'; + cacheCtx.lineWidth = 1; + cacheCtx.textAlign = 'center'; + cacheCtx.textBaseline = 'middle'; + + for (let i = startX; i <= endX; i += 50) { + cacheCtx.beginPath(); + cacheCtx.strokeStyle = (i % 100 === 0) ? 'red' : '#aaa'; + cacheCtx.moveTo(i - rowHeaderWidth, top - columnHeaderHeight); + cacheCtx.lineTo(i - rowHeaderWidth, bottom - columnHeaderHeight); + cacheCtx.stroke(); + cacheCtx.closePath(); + } + for (let j = startY; j <= endY; j += 50) { + cacheCtx.beginPath(); + cacheCtx.strokeStyle = (j % 100 === 0) ? 'red' : '#aaa'; + cacheCtx.moveTo(left - rowHeaderWidth, j - columnHeaderHeight); + cacheCtx.lineTo(right - rowHeaderWidth, j - columnHeaderHeight); + cacheCtx.stroke(); + cacheCtx.closePath(); + } + cacheCtx.fillStyle = '#666'; + for (let i = startX; i <= endX; i += 100) { + for (let j = startY; j <= endY; j += 100) { + cacheCtx.fillText(`${i},${j}`, i - rowHeaderWidth, j - columnHeaderHeight); + } + } + // start + // cacheCtx.textAlign = 'left'; + // for (let j = startY; j <= endY; j += 100) { + // cacheCtx.clearRect(left - rowHeaderWidth, j - columnHeaderHeight - 15, 30, 30); + // cacheCtx.fillText(`${left}`, left - rowHeaderWidth, j - columnHeaderHeight); + // } + cacheCtx.closePath(); + cacheCtx.restore(); + } + + testGetRandomLightColor(): string { + const letters = 'ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 6)]; + } + + // 确保生成的颜色足够亮 + const r = Number.parseInt(color.substring(1, 3), 16); + const g = Number.parseInt(color.substring(3, 5), 16); + const b = Number.parseInt(color.substring(5, 7), 16); + if (r + g + b < 610) { + return this.testGetRandomLightColor(); // 递归调用直到生成足够亮的颜色 + } + + return color; + } } diff --git a/packages/engine-render/src/context.ts b/packages/engine-render/src/context.ts index ab049a13e6..6d2e50d21d 100644 --- a/packages/engine-render/src/context.ts +++ b/packages/engine-render/src/context.ts @@ -358,7 +358,7 @@ export class UniverRenderingContext2D implements CanvasRenderingContext2D { * @method */ arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterClockwise?: boolean) { - this._context.arc(x, y, radius, startAngle, endAngle, counterClockwise); + this._context.arc(x, y, Math.max(0, radius), startAngle, endAngle, counterClockwise); } /** diff --git a/packages/engine-render/src/custom/custom-object.ts b/packages/engine-render/src/custom/custom-object.ts index 853b49d5ee..89e14b0d40 100644 --- a/packages/engine-render/src/custom/custom-object.ts +++ b/packages/engine-render/src/custom/custom-object.ts @@ -15,7 +15,7 @@ */ import { BaseObject } from '../base-object'; -import type { IViewportBound, Vector2 } from '../basics/vector2'; +import type { IViewportInfo, Vector2 } from '../basics/vector2'; import type { UniverRenderingContext } from '../context'; export class CustomObject extends BaseObject { @@ -33,7 +33,7 @@ export class CustomObject extends BaseObject { }; } - override render(mainCtx: UniverRenderingContext, bounds?: IViewportBound) { + override render(mainCtx: UniverRenderingContext, bounds?: IViewportInfo) { if (!this.visible) { this.makeDirty(false); return this; diff --git a/packages/engine-render/src/group.ts b/packages/engine-render/src/group.ts index 13e610cfbd..fefb72b78e 100644 --- a/packages/engine-render/src/group.ts +++ b/packages/engine-render/src/group.ts @@ -21,7 +21,7 @@ import { BaseObject } from './base-object'; import type { CURSOR_TYPE } from './basics/const'; import { RENDER_CLASS_TYPE } from './basics/const'; import { isString } from './basics/tools'; -import type { IViewportBound, Vector2 } from './basics/vector2'; +import type { IViewportInfo, Vector2 } from './basics/vector2'; import type { UniverRenderingContext } from './context'; import type { ThinScene } from './thin-scene'; @@ -115,7 +115,7 @@ export class Group extends BaseObject { return this._objects; } - override render(ctx: UniverRenderingContext, bounds?: IViewportBound) { + override render(ctx: UniverRenderingContext, bounds?: IViewportInfo) { ctx.save(); const m = this.transform.getMatrix(); ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); @@ -160,7 +160,7 @@ export class Group extends BaseObject { super.dispose(); } - private _transformBounds(bounds?: IViewportBound) { + private _transformBounds(bounds?: IViewportInfo) { return bounds; } } diff --git a/packages/engine-render/src/layer.ts b/packages/engine-render/src/layer.ts index ef8c092a59..7058a0cd62 100644 --- a/packages/engine-render/src/layer.ts +++ b/packages/engine-render/src/layer.ts @@ -21,7 +21,8 @@ import { BaseObject } from './base-object'; import { RENDER_CLASS_TYPE } from './basics/const'; import { Canvas } from './canvas'; import type { UniverRenderingContext } from './context'; -import type { ThinScene } from './thin-scene'; +import type { Scene } from './scene'; +import type { SceneViewer } from './scene-viewer'; export class Layer extends Disposable { private _objects: BaseObject[] = []; @@ -33,7 +34,7 @@ export class Layer extends Disposable { private _debounceDirtyFunc: Nullable<() => void>; constructor( - private _scene: ThinScene, + private _scene: Scene, objects: BaseObject[] = [], private _zIndex: number = 1, private _allowCache: boolean = false @@ -176,7 +177,7 @@ export class Layer extends Disposable { */ const parent = this.scene.getParent(); if (parent.classType === RENDER_CLASS_TYPE.SCENE_VIEWER) { - parent.makeDirty(true); + (parent as SceneViewer).makeDirty(true); } return this; @@ -200,25 +201,26 @@ export class Layer extends Disposable { render(parentCtx?: UniverRenderingContext, isMaxLayer = false) { const mainCtx = parentCtx || this._scene.getEngine()?.getCanvas().getContext(); + if (mainCtx) { + if (this._allowCache && this._cacheCanvas) { + if (this.isDirty()) { + const ctx = this._cacheCanvas.getContext(); - if (this._allowCache && this._cacheCanvas) { - if (this.isDirty()) { - const ctx = this._cacheCanvas.getContext(); + this._cacheCanvas.clear(); - this._cacheCanvas.clear(); + ctx.save(); - ctx.save(); + ctx.setTransform(mainCtx.getTransform()); + this._draw(ctx, isMaxLayer); - ctx.setTransform(mainCtx.getTransform()); - this._draw(ctx, isMaxLayer); - - ctx.restore(); + ctx.restore(); + } + this._applyCache(mainCtx); + } else { + mainCtx.save(); + this._draw(mainCtx, isMaxLayer); + mainCtx.restore(); } - this._applyCache(mainCtx); - } else { - mainCtx.save(); - this._draw(mainCtx, isMaxLayer); - mainCtx.restore(); } this.makeDirty(false); @@ -241,7 +243,7 @@ export class Layer extends Disposable { this._cacheCanvas = new Canvas(); this.disposeWithMe( toDisposable( - this._scene.getEngine().onTransformChangeObservable.add(() => { + this._scene.getEngine()?.onTransformChangeObservable.add(() => { this._resizeCacheCanvas(); }) ) @@ -249,7 +251,15 @@ export class Layer extends Disposable { } private _draw(mainCtx: UniverRenderingContext, isMaxLayer: boolean) { - this._scene.getViewports()?.forEach((vp) => vp.render(mainCtx, this.getObjectsByOrder(), isMaxLayer)); + const viewports = this._scene.getViewports().filter((vp) => vp.shouldIntoRender()); + const objects = this.getObjectsByOrder(); + for (const [_index, vp] of viewports.entries()) { + vp.render(mainCtx, objects, isMaxLayer); + } + objects.forEach((o) => { + o.makeDirty(false); + o.makeForceDirty?.(false); + }); } private _applyCache(ctx?: UniverRenderingContext) { @@ -258,12 +268,17 @@ export class Layer extends Disposable { } const width = this._cacheCanvas.getWidth(); const height = this._cacheCanvas.getHeight(); - ctx.drawImage(this._cacheCanvas.getCanvasEle(), 0, 0, width, height); + // it throw an error if canvas size is zero, and canvas size is zero when viewport isActive is false. + if (width !== 0 && height !== 0) { + ctx.drawImage(this._cacheCanvas.getCanvasEle(), 0, 0, width, height); + } } private _resizeCacheCanvas() { const engine = this._scene.getEngine(); - this._cacheCanvas?.setSize(engine.width, engine.height); + if (engine) { + this._cacheCanvas?.setSize(engine.width, engine.height); + } this.makeDirty(true); } diff --git a/packages/engine-render/src/scene-viewer.ts b/packages/engine-render/src/scene-viewer.ts index 6e27194ebb..a05bad0df5 100644 --- a/packages/engine-render/src/scene-viewer.ts +++ b/packages/engine-render/src/scene-viewer.ts @@ -19,7 +19,7 @@ import type { IKeyValue, Nullable } from '@univerjs/core'; import { BaseObject } from './base-object'; import { RENDER_CLASS_TYPE } from './basics/const'; import type { IObjectFullState } from './basics/interfaces'; -import type { IViewportBound, Vector2 } from './basics/vector2'; +import type { IViewportInfo, Vector2 } from './basics/vector2'; import type { UniverRenderingContext } from './context'; import type { ThinScene } from './thin-scene'; @@ -40,14 +40,14 @@ export class SceneViewer extends BaseObject { return RENDER_CLASS_TYPE.SCENE_VIEWER; } - override render(mainCtx: UniverRenderingContext, bounds?: IViewportBound) { + override render(mainCtx: UniverRenderingContext, bounds?: IViewportInfo) { if (!this.visible) { this.makeDirty(false); return this; } if (bounds) { - const { left, top, right, bottom } = bounds.viewBound; + const { left, top, right, bottom } = bounds.cacheBound || bounds.viewBound; if ( this.width + this.strokeWidth + this.left < left || diff --git a/packages/engine-render/src/scene.ts b/packages/engine-render/src/scene.ts index 7298a38fce..8429b29056 100644 --- a/packages/engine-render/src/scene.ts +++ b/packages/engine-render/src/scene.ts @@ -460,7 +460,7 @@ export class Scene extends ThinScene { override removeViewport(key: string) { for (let i = 0, len = this._viewports.length; i < len; i++) { const viewport = this._viewports[i]; - if (viewport.viewPortKey === key) { + if (viewport.viewportKey === key) { this._viewports.splice(i, 1); return viewport; } @@ -473,7 +473,7 @@ export class Scene extends ThinScene { getViewport(key: string) { for (const viewport of this._viewports) { - if (viewport.viewPortKey === key) { + if (viewport.viewportKey === key) { return viewport; } } @@ -491,8 +491,6 @@ export class Scene extends ThinScene { for (let i = 0, len = layers.length; i < len; i++) { layers[i].render(parentCtx, i === len - 1); } - - // this.getViewports()?.forEach((vp: Viewport) => vp.render(parentCtx)); } async requestRender(parentCtx?: UniverRenderingContext) { @@ -565,8 +563,8 @@ export class Scene extends ThinScene { let x = 0; let y = 0; if (viewPort) { - const actualX = viewPort.actualScrollX || 0; - const actualY = viewPort.actualScrollY || 0; + const actualX = viewPort.viewportScrollX || 0; + const actualY = viewPort.viewportScrollY || 0; x += actualX; y += actualY; } @@ -600,8 +598,8 @@ export class Scene extends ThinScene { const scene = parent as Scene; const viewPort = scene.getActiveViewportByCoord(coord); if (viewPort) { - const actualX = viewPort.actualScrollX || 0; - const actualY = viewPort.actualScrollY || 0; + const actualX = viewPort.viewportScrollX || 0; + const actualY = viewPort.viewportScrollY || 0; coord = coord.addByPoint(actualX, actualY); } } else if (parent.classType === RENDER_CLASS_TYPE.SCENE_VIEWER) { diff --git a/packages/engine-render/src/shape/picture.ts b/packages/engine-render/src/shape/picture.ts index 64f6c721af..9a14cde28b 100644 --- a/packages/engine-render/src/shape/picture.ts +++ b/packages/engine-render/src/shape/picture.ts @@ -45,10 +45,12 @@ export class Picture extends Shape { }; if (config.image) { this._native = config.image; + this._native.crossOrigin = 'anonymous'; this._init(); } else if (config.url) { this._native = document.createElement('img'); this._native.src = config.url; + this._native.crossOrigin = 'anonymous'; this._native.onload = () => { config.success?.(); this._init(); diff --git a/packages/engine-render/src/shape/rich-text.ts b/packages/engine-render/src/shape/rich-text.ts index 467cc655fe..97591d3ad1 100644 --- a/packages/engine-render/src/shape/rich-text.ts +++ b/packages/engine-render/src/shape/rich-text.ts @@ -29,7 +29,7 @@ import { BooleanNumber, DEFAULT_EMPTY_DOCUMENT_VALUE, DocumentDataModel } from ' import { BaseObject } from '../base-object'; import { TRANSFORM_CHANGE_OBSERVABLE_TYPE } from '../basics/interfaces'; -import type { IViewportBound } from '../basics/vector2'; +import type { IViewportInfo } from '../basics/vector2'; import { DocumentSkeleton } from '../components/docs/layout/doc-skeleton'; import { Documents } from '../components/docs/document'; import { DocumentViewModel } from '../components/docs/view-model/document-view-model'; @@ -167,7 +167,7 @@ export class RichText extends BaseObject { return this; } - override render(mainCtx: UniverRenderingContext, bounds?: IViewportBound) { + override render(mainCtx: UniverRenderingContext, bounds?: IViewportInfo) { if (!this.visible) { this.makeDirty(false); return this; diff --git a/packages/engine-render/src/shape/shape.ts b/packages/engine-render/src/shape/shape.ts index a232f112de..b4e1a04551 100644 --- a/packages/engine-render/src/shape/shape.ts +++ b/packages/engine-render/src/shape/shape.ts @@ -19,7 +19,7 @@ import type { IKeyValue, IScale, Nullable } from '@univerjs/core'; import { BASE_OBJECT_ARRAY, BaseObject } from '../base-object'; import { SHAPE_TYPE } from '../basics/const'; import type { IObjectFullState } from '../basics/interfaces'; -import type { IViewportBound, Vector2 } from '../basics/vector2'; +import type { IViewportInfo, Vector2 } from '../basics/vector2'; import type { UniverRenderingContext } from '../context'; export type LineJoin = 'round' | 'bevel' | 'miter'; @@ -304,7 +304,7 @@ export abstract class Shape extends BaseObject { ctx.strokeStyle = stroke!; } - override render(mainCtx: UniverRenderingContext, bounds?: IViewportBound) { + override render(mainCtx: UniverRenderingContext, bounds?: IViewportInfo) { if (!this.visible) { this.makeDirty(false); return this; diff --git a/packages/engine-render/src/viewport.ts b/packages/engine-render/src/viewport.ts index 4ed3ec02a3..18c315b0e1 100644 --- a/packages/engine-render/src/viewport.ts +++ b/packages/engine-render/src/viewport.ts @@ -18,17 +18,19 @@ import type { EventState, IPosition, IRange, Nullable } from '@univerjs/core'; import { Observable, Tools } from '@univerjs/core'; import type { BaseObject } from './base-object'; -import { RENDER_CLASS_TYPE } from './basics/const'; +import { FIX_ONE_PIXEL_BLUR_OFFSET, RENDER_CLASS_TYPE } from './basics/const'; import type { IWheelEvent } from './basics/i-events'; import { PointerInput } from './basics/i-events'; import { fixLineWidthByScale, toPx } from './basics/tools'; import { Transform } from './basics/transform'; -import type { IBoundRectNoAngle, IViewportBound } from './basics/vector2'; +import type { IBoundRectNoAngle, IViewportInfo } from './basics/vector2'; import { Vector2 } from './basics/vector2'; +import { subtractViewportRange } from './basics/viewport-subtract'; +import { Canvas as UniverCanvas } from './canvas'; import type { UniverRenderingContext } from './context'; +import type { Scene } from './scene'; import type { BaseScrollBar } from './shape/base-scroll-bar'; import type { ThinScene } from './thin-scene'; -import { subtractViewportRange } from './basics/viewport-subtract'; interface IViewPosition { top?: number; @@ -45,14 +47,27 @@ interface IViewProps extends IViewPosition { isWheelPreventDefaultX?: boolean; isWheelPreventDefaultY?: boolean; active?: boolean; + + isRelativeX?: boolean; + isRelativeY?: boolean; + + allowCache?: boolean; + bufferEdgeX?: number; + bufferEdgeY?: number; } export interface IScrollObserverParam { viewport?: Viewport; + /** + * scrollX for scrollbar + */ scrollX?: number; scrollY?: number; x?: number; y?: number; + /** + * scrollX for viewport + */ actualScrollX?: number; actualScrollY?: number; limitX?: number; @@ -74,21 +89,24 @@ const MOUSE_WHEEL_SPEED_SMOOTHING_FACTOR = 3; export class Viewport { /** - * The offset of the scrollbar equals the distance from the top to the scrollbar - * use getActualScroll, convert to actualScrollX, actualScrollY - * + * scrollX means scroll x value for scrollbar in viewMain + * use getBarScroll to get scrolling value(scrollX, scrollY) for scrollbar */ scrollX: number = 0; - scrollY: number = 0; + _preScrollX: number = 0; + _preScrollY: number = 0; /** * The actual scroll offset equals the distance from the content area position to the top, and there is a conversion relationship with scrollX and scrollY - * use getBarScroll, convert to scrollX, scrollY + * use getActualScroll to get scrolling value for spreadsheet. */ - actualScrollX: number = 0; - - actualScrollY: number = 0; + viewportScrollX: number = 0; + viewportScrollY: number = 0; + preViewportScrollX: number = 0; + preViewportScrollY: number = 0; + _deltaViewportScrollX: number = 0; + _deltaViewportScrollY: number = 0; onMouseWheelObserver = new Observable(); @@ -100,10 +118,16 @@ export class Viewport { onScrollByBarObserver = new Observable(); - private _viewPortKey: string = ''; + private _viewportKey: string = ''; + /** + * viewport top origin value in logic, scale does not affect it. + */ private _topOrigin: number = 0; + /** + * viewport left origin value in logic, scale does not affect it. + */ private _leftOrigin: number = 0; private _bottomOrigin: number = 0; @@ -136,10 +160,6 @@ export class Viewport { private _scrollStopNum: NodeJS.Timeout | number = 0; - private _preScrollX: number = 0; - - private _preScrollY: number = 0; - private _renderClipState = true; private _active = true; @@ -156,10 +176,42 @@ export class Viewport { private _isRelativeY: boolean = false; - private _preViewportBound: Nullable; + // private _preViewportInfo: Nullable; + + /** + * viewbound of cache area, cache area is slightly bigger than viewbound. + */ + private _cacheBound: IBoundRectNoAngle | null; + private _preCacheBound: IBoundRectNoAngle | null; + private _preCacheVisibleBound: IBoundRectNoAngle | null; + + /** + * bound of visible area + */ + private _viewBound: IBoundRectNoAngle; + private _preViewBound: IBoundRectNoAngle; + + /** + * Whether the viewport needs to be updated. + * In future, viewMain dirty would not affect othew viewports. + */ + private _isDirty = true; + private _cacheCanvas: UniverCanvas | null = null; + + /** + * The configuration comes from the props.allowCache passed in during viewport initialization. + * When _allowCache is true, a cacheCanvas will be created. + */ + private _allowCache: boolean = false; + + /** + * Buffer Area size, default is zero + */ + bufferEdgeX: number = 0; + bufferEdgeY: number = 0; - constructor(viewPortKey: string, scene: ThinScene, props?: IViewProps) { - this._viewPortKey = viewPortKey; + constructor(viewportKey: string, scene: ThinScene, props?: IViewProps) { + this._viewportKey = viewportKey; this._scene = scene; @@ -179,12 +231,72 @@ export class Viewport { // this._heightOrigin = this.height; // } + if (props?.isRelativeX != null) { + this._isRelativeX = props.isRelativeX; + } + + if (props?.isRelativeY != null) { + this._isRelativeY = props.isRelativeY; + } + this._setWithAndHeight(props); + this.initCacheCanvas(props); this._isWheelPreventDefaultX = props?.isWheelPreventDefaultX || false; this._isWheelPreventDefaultY = props?.isWheelPreventDefaultY || false; - this._resizeCacheCanvasAndScrollBar(); + this._resizeCacheCanvas(); + this.getBounding(); + + this.scene.getEngine()?.onTransformChangeObservable.add(() => { + this._mainCanvasResizeHandler(); + }); + this._mainCanvasResizeHandler(); + } + + initCacheCanvas(props?: IViewProps) { + this._allowCache = props?.allowCache || false; + if (this._allowCache) { + this._cacheCanvas = new UniverCanvas(); + this.bufferEdgeX = props?.bufferEdgeX || 0; + this.bufferEdgeY = props?.bufferEdgeY || 0; + } + // this._testDisplayCache(); + } + + /** + * test + */ + _testDisplayCache() { + const globalThis = window as any; + if (!globalThis.cacheSet) { + globalThis.cacheSet = new Set(); + } + const showCache = (cacheCanvas: UniverCanvas) => { + cacheCanvas.getCanvasEle().classList.remove('univer-render-canvas'); + cacheCanvas.getCanvasEle().classList.add('viewport-cache-canvas'); + cacheCanvas.getCanvasEle().classList.add('cache-canvas', this.viewportKey); + cacheCanvas.getCanvasEle().style.zIndex = '100'; + cacheCanvas.getCanvasEle().style.position = 'fixed'; + cacheCanvas.getCanvasEle().style.background = 'pink'; + cacheCanvas.getCanvasEle().style.pointerEvents = 'none'; + cacheCanvas.getCanvasEle().style.border = '1px solid black'; + cacheCanvas.getCanvasEle().style.transformOrigin = '100% 100%'; + cacheCanvas.getCanvasEle().style.transform = 'scale(0.5)'; + cacheCanvas.getCanvasEle().style.translate = '-20% 0%'; + cacheCanvas.getCanvasEle().style.opacity = '1'; + document.body.appendChild(cacheCanvas.getCanvasEle()); + }; + if (['viewMain', 'viewMainLeftTop', 'viewMainTop', 'viewMainLeft'].includes(this.viewportKey)) { + if (this._cacheCanvas) { + showCache(this._cacheCanvas); + } + } + if (this.viewportKey === 'spreadInSlideViewMaintable1') { + if (this._cacheCanvas) { + showCache(this._cacheCanvas); + } + } } get scene() { @@ -199,8 +311,8 @@ export class Viewport { return this._height; } - get viewPortKey() { - return this._viewPortKey; + get viewportKey() { + return this._viewportKey; } get topOrigin() { @@ -282,6 +394,27 @@ export class Viewport { this._right = toPx(num, this.scene.getParent()?.width); } + get viewBound() { + return this._viewBound; + } + + get cacheBound() { + return this._cacheBound; + } + + set cacheBound(val) { + this._cacheBound = val; + } + + get preCacheBound() { + return this._preCacheBound; + } + + set preCacheBound(val: IBoundRectNoAngle | null) { + this._preCacheBound = val; + this._preCacheVisibleBound = Object.assign({}, val); + } + enable() { this._active = true; } @@ -290,19 +423,31 @@ export class Viewport { this._active = false; } + /** + * 物理 canvas 大小改变时调用(调整 window 大小时触发) + */ resetSizeAndScrollBar() { - this._resizeCacheCanvasAndScrollBar(); + this._resizeCacheCanvas(); } setScrollBar(instance: BaseScrollBar) { this._scrollBar = instance; - this._resizeCacheCanvasAndScrollBar(); + this._resizeCacheCanvas(); } removeScrollBar() { this._scrollBar = null; } + /** + * 和 resetSizeAndScrollBar 不同 + * 此方法是调整冻结行列设置时 & 初始化时触发, resize window 时并不会触发 + * + * 注意参数 position 不一定有 height & width 对于 viewMain 只有 left top bottom right + * this.width this.height 也有可能是 undefined + * 因此应通过 _getViewPortSize 获取宽高 + * @param position + */ resize(position: IViewPosition) { const positionKeys = Object.keys(position); if (positionKeys.length === 0) { @@ -315,10 +460,8 @@ export class Viewport { // (this as IKeyValue)[pKey] = position[pKey as keyof IViewPosition]; // } // }); - this._setWithAndHeight(position); - - this._resizeCacheCanvasAndScrollBar(); + this._resizeCacheCanvas(); } setPadding(param: IPosition) { @@ -328,7 +471,7 @@ export class Viewport { this._paddingStartY = startY; this._paddingEndY = endY; - this._resizeCacheCanvasAndScrollBar(); + this._resizeCacheCanvas(); } resetPadding() { @@ -341,7 +484,14 @@ export class Viewport { } /** + * + * 改动 scrollbar 的位置,不是 viewport content 滚动 * scroll to position, absolute + * 只有 viewMain 才会被调用 scrollTo 其他 viewport 都不会调用此方法 + * 具体在 scroll.controller 中 + * + * Debug + * window.scene.getViewports()[0].scrollTo({x: 14.2, y: 1.8}, true) * @param pos * @returns */ @@ -367,8 +517,8 @@ export class Viewport { scrollY: this.scrollY, x, y, - actualScrollX: this.actualScrollX, - actualScrollY: this.actualScrollY, + actualScrollX: this.viewportScrollX, + actualScrollY: this.viewportScrollY, limitX: this._scrollBar?.limitX, limitY: this._scrollBar?.limitY, isTrigger, @@ -427,8 +577,8 @@ export class Viewport { if (this._scrollBar.ratioScrollX !== 0) { x /= this._scrollBar.ratioScrollX; // 转换为内容区实际滚动距离 x /= scaleX; - } else if (this.actualScrollX !== undefined) { - x = this.actualScrollX; + } else if (this.viewportScrollX !== undefined) { + x = this.viewportScrollX; } else { x = 0; } @@ -437,8 +587,8 @@ export class Viewport { y /= this._scrollBar.ratioScrollY; y /= scaleY; - } else if (this.actualScrollY !== undefined) { - y = this.actualScrollY; + } else if (this.viewportScrollY !== undefined) { + y = this.viewportScrollY; } else { y = 0; } @@ -447,20 +597,20 @@ export class Viewport { // x *= this._scrollBar.miniThumbRatioX; // y *= this._scrollBar.miniThumbRatioY; } else { - if (this.actualScrollX !== undefined) { - x = this.actualScrollX; + if (this.viewportScrollX !== undefined) { + x = this.viewportScrollX; } else { x = 0; } - if (this.actualScrollY !== undefined) { - y = this.actualScrollY; + if (this.viewportScrollY !== undefined) { + y = this.viewportScrollY; } else { y = 0; } } - const { scaleX, scaleY } = this._scene.getPrecisionScale(); + const { scaleX, scaleY } = (this._scene as Scene).getPrecisionScale(); return { x: fixLineWidthByScale(x + this._paddingStartX, scaleX), @@ -468,6 +618,10 @@ export class Viewport { }; } + /** + * get actual scroll value by scrollXY + * @returns + */ getTransformedScroll() { const x = this.scrollX; const y = this.scrollY; @@ -479,7 +633,11 @@ export class Viewport { return this._scrollBar; } + // _scrollTo ---> _scroll ---> onScrollAfterObserver.notifyObservers ---> updateScroll updateScroll(param: IScrollObserverParam) { + // scrollvalue for scrollbar, when rows over 5000(big sheet), deltaScrollY always 0 when scrolling. Do not use this value to judge scrolling + // this._deltaScrollX = this.scrollX - this._preScrollX; + // this._deltaScrollY = this.scrollY - this._preScrollY; this._preScrollX = this.scrollX; this._preScrollY = this.scrollY; const { scrollX, scrollY, actualScrollX, actualScrollY } = param; @@ -492,11 +650,15 @@ export class Viewport { } if (actualScrollX !== undefined) { - this.actualScrollX = actualScrollX; + this.preViewportScrollX = this.viewportScrollX; + this.viewportScrollX = actualScrollX; + this._deltaViewportScrollX = actualScrollX - this.preViewportScrollX; } if (actualScrollY !== undefined) { - this.actualScrollY = actualScrollY; + this.preViewportScrollY = this.viewportScrollY; + this.viewportScrollY = actualScrollY; + this._deltaViewportScrollY = actualScrollY - this.preViewportScrollY; } return this; } @@ -509,7 +671,7 @@ export class Viewport { return composeResult; } - render(parentCtx?: UniverRenderingContext, objects: BaseObject[] = [], isMaxLayer = false) { + shouldIntoRender() { if ( this.isActive === false || this.width == null || @@ -517,54 +679,224 @@ export class Viewport { this.width <= 1 || this.height <= 1 ) { + return false; + } + return true; + } + + /** + * engine.renderLoop ---> scene.render ---> layer.render ---> viewport.render + * that means each layer call all viewports to render + * @param parentCtx 如果 layer._allowCache true, 那么 parentCtx 是 layer 中的 cacheCtx + * @param objects + * @param isMaxLayer + * @param isLast last viewport would + */ + render(parentCtx?: UniverRenderingContext, objects: BaseObject[] = [], isMaxLayer = false) { + if (!this.shouldIntoRender()) { return; } const mainCtx = parentCtx || (this._scene.getEngine()?.getCanvas().getContext() as UniverRenderingContext); const sceneTrans = this._scene.transform.clone(); + sceneTrans.multiply(Transform.create([1, 0, 0, 1, -this.viewportScrollX || 0, -this.viewportScrollY || 0])); - sceneTrans.multiply(Transform.create([1, 0, 0, 1, -this.actualScrollX || 0, -this.actualScrollY || 0])); - - const ctx = mainCtx; + // Logical translation & scaling, unrelated to dpr. + const tm = sceneTrans.getMatrix(); + const scrollbarTM = this.getScrollBarTransForm().getMatrix(); - const m = sceneTrans.getMatrix(); - const n = this.getScrollBarTransForm().getMatrix(); - - ctx.save(); + mainCtx.save(); if (this._renderClipState) { - ctx.beginPath(); + mainCtx.beginPath(); // DEPT: left is set by upper views but width and height is not - - const { scaleX, scaleY } = this._getBoundScale(m[0], m[3]); - ctx.rect(this.left, this.top, (this.width || 0) * scaleX, (this.height || 0) * scaleY); - ctx.clip(); + // this.left has handle scale already, no need to `this.width * scale` + // const { scaleX, scaleY } = this._getBoundScale(m[0], m[3]); + mainCtx.rect(this.left, this.top, (this.width || 0), (this.height || 0)); + mainCtx.clip(); } - ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - - const viewBound = this._calViewportRelativeBounding(); + mainCtx.transform(tm[0], tm[1], tm[2], tm[3], tm[4], tm[5]); + const viewPortInfo = this._calcViewportInfo(); objects.forEach((o) => { - o.render(ctx, viewBound); + o.render(mainCtx, viewPortInfo); }); - ctx.restore(); + + this.markDirty(false); + this.markForceDirty(false); + + this._preViewBound = this._viewBound; + if (viewPortInfo.shouldCacheUpdate) { + this.preCacheBound = this._cacheBound; + } + mainCtx.restore(); if (this._scrollBar && isMaxLayer) { - ctx.save(); + mainCtx.save(); - ctx.transform(n[0], n[1], n[2], n[3], n[4], n[5]); - this._drawScrollbar(ctx); - ctx.restore(); + mainCtx.transform(scrollbarTM[0], scrollbarTM[1], scrollbarTM[2], scrollbarTM[3], scrollbarTM[4], scrollbarTM[5]); + this._drawScrollbar(mainCtx); + mainCtx.restore(); } this._scrollRendered(); + } - this._preViewportBound = viewBound; + private _makeDefaultViewport() { + return { + viewBound: { + left: -1, + top: -1, + right: -1, + bottom: -1, + }, + diffBounds: [], + diffX: -1, + diffY: -1, + viewPortPosition: { + top: 0, + left: 0, + bottom: 0, + right: 0, + }, + viewportKey: this.viewportKey, + isDirty: 0, + isForceDirty: this.isForceDirty, + allowCache: false, + cacheBound: { + left: -1, + top: -1, + right: -1, + bottom: -1, + }, + diffCacheBounds: [], + cacheViewPortPosition: { + top: 0, + left: 0, + bottom: 0, + right: 0, + }, + shouldCacheUpdate: 0, + sceneTrans: Transform.create([1, 0, 0, 1, 0, 0]), + leftOrigin: 0, + topOrigin: 0, + bufferEdgeX: this.bufferEdgeX, + bufferEdgeY: this.bufferEdgeY, + } satisfies IViewportInfo; + } + + // eslint-disable-next-line max-lines-per-function + private _calcViewportInfo(): IViewportInfo { + if (this.isActive === false) { + return this._makeDefaultViewport(); + } + + const sceneTrans = this._scene.transform.clone(); + + const m = sceneTrans.getMatrix(); + + // const scaleFromX = this._isRelativeX ? (m[0] < 1 ? m[0] : 1) : 1; + + // const scaleFromY = this._isRelativeY ? (m[3] < 1 ? m[3] : 1) : 1; + + // const scaleToX = this._isRelativeX ? 1 : m[0] < 1 ? m[0] : 1; + + // const scaleToY = this._isRelativeY ? 1 : m[3] < 1 ? m[3] : 1; + + let width = this._width; + + let height = this._height; + + const size = this._getViewPortSize(); + + // if (m[0] > 1) { + width = size.width; + // } + + // if (m[3] > 1) { + height = size.height; + // } + + const xFrom: number = this.left; + const xTo: number = ((width || 0) + this.left); + const yFrom: number = this.top; + const yTo: number = ((height || 0) + this.top); + + // this.getRelativeVector 加上了 scroll 后的坐标 + const topLeft = this.getRelativeVector(Vector2.FromArray([xFrom, yFrom])); + const bottomRight = this.getRelativeVector(Vector2.FromArray([xTo, yTo])); + + const viewBound = { + left: topLeft.x, + right: bottomRight.x, + top: topLeft.y, + bottom: bottomRight.y, + }; + this._viewBound = viewBound; + const preViewBound = this._preViewBound; + const diffBounds = this._diffViewBound(viewBound, preViewBound); + const diffX = (preViewBound?.left || 0) - viewBound.left; + const diffY = (preViewBound?.top || 0) - viewBound.top; + const viewPortPosition = { + top: yFrom, + left: xFrom, + bottom: yTo, + right: xTo, + }; + const cacheBound = this.expandBounds(viewBound); + this.cacheBound = cacheBound; + if (!this.preCacheBound) { + this.preCacheBound = this.expandBounds(viewBound); + } + let diffCacheBounds: IBoundRectNoAngle[] = [];// = this._diffViewBound(cacheBounds, prevCacheBounds); + if (this._preCacheVisibleBound) { + if (diffX < 0) { // scrolling right (further) + this._preCacheVisibleBound.left -= diffX; + } else if (diffX > 0) { + this._preCacheVisibleBound.right -= diffX; + } + + if (diffY < 0) { // scrolling down (further) + this._preCacheVisibleBound.top -= diffY; + } else if (diffY > 0) { + this._preCacheVisibleBound.bottom -= diffY; + } + } + const cacheViewPortPosition = this.expandBounds(viewPortPosition); + const shouldCacheUpdate = this._calcCacheUpdate(viewBound, this._preCacheVisibleBound, diffX, diffY); + if (shouldCacheUpdate) { + diffCacheBounds = this._calcDiffCacheBound(this._preCacheBound, cacheBound); + } + + return { + viewBound, + diffBounds, + diffX, + diffY, + viewPortPosition, + viewportKey: this.viewportKey, + isDirty: this.isDirty ? 0b10 : 0b00, + isForceDirty: this.isForceDirty, + allowCache: this._allowCache, + cacheBound, + diffCacheBounds, + cacheViewPortPosition, + shouldCacheUpdate, + sceneTrans, + cacheCanvas: this._cacheCanvas!, + leftOrigin: this._leftOrigin, + topOrigin: this._topOrigin, + bufferEdgeX: this.bufferEdgeX, + bufferEdgeY: this.bufferEdgeY, + updatePrevCacheBounds: (viewbound: IBoundRectNoAngle) => { + this.updatePrevCacheBounds(viewbound); + }, + } satisfies IViewportInfo; } getBounding() { - return this._calViewportRelativeBounding(); + return this._calcViewportInfo(); } getRelativeVector(coord: Vector2) { @@ -583,6 +915,7 @@ export class Viewport { return svCoord; } + // eslint-disable-next-line complexity, max-lines-per-function onMouseWheel(evt: IWheelEvent, state: EventState) { if (!this._scrollBar || this.isActive === false) { return; @@ -590,11 +923,9 @@ export class Viewport { let isLimitedStore; if (evt.inputIndex === PointerInput.MouseWheelX) { const deltaFactor = Math.abs(evt.deltaX); - // let magicNumber = deltaFactor < 40 ? 2 : deltaFactor < 80 ? 3 : 4; const allWidth = this._scene.width; const viewWidth = this.width || 1; const scrollNum = (viewWidth / allWidth) * deltaFactor; - if (evt.deltaX > 0) { isLimitedStore = this.scrollBy({ x: scrollNum, @@ -669,7 +1000,6 @@ export class Viewport { } if (evt.inputIndex === PointerInput.MouseWheelZ) { // TODO - // ... } this._scene.makeDirty(true); @@ -720,8 +1050,8 @@ export class Viewport { this.onScrollBeforeObserver.clear(); this.onScrollStopObserver.clear(); this._scrollBar?.dispose(); - - this._scene.removeViewport(this._viewPortKey); + this._cacheCanvas?.dispose(); + this._scene.removeViewport(this._viewportKey); } limitedScroll() { @@ -757,27 +1087,58 @@ export class Viewport { }; } - private _resizeCacheCanvasAndScrollBar() { - const actualScrollX = this.actualScrollX; + markDirty(state?: boolean) { + if (state === undefined) { + state = true; + } + this._isDirty = state; + } - const actualScrollY = this.actualScrollY; + get isDirty() { + return this._isDirty; + } + private _isForceDirty = true; + markForceDirty(state?: boolean) { + if (state === undefined) { + state = true; + } + this._isForceDirty = state; + } + + resetPrevCacheBounds() { + this._preCacheBound = null;//this.expandBounds(this._viewBound); + } + + get isForceDirty() { + return this._isForceDirty; + } + + private _resizeCacheCanvas() { + const actualScrollX = this.viewportScrollX; + const actualScrollY = this.viewportScrollY; const { width, height } = this._getViewPortSize(); - const contentWidth = (this._scene.width - this._paddingEndX) * this._scene.scaleX; + const scaleX = this.scene.scaleX; + const scaleY = this.scene.scaleY; + const canvasW = width !== 0 ? width + this.bufferEdgeX * 2 * scaleX : 0; + const canvasH = height !== 0 ? height + this.bufferEdgeY * 2 * scaleY : 0; + this._cacheCanvas?.setSize(canvasW, canvasH); + this.cacheBound = this._viewBound; + this.preCacheBound = null; + const contentWidth = (this._scene.width - this._paddingEndX) * this._scene.scaleX; const contentHeight = (this._scene.height - this._paddingEndY) * this._scene.scaleY; if (this._scrollBar) { this._scrollBar.resize(width, height, contentWidth, contentHeight); - const { x, y } = this.getBarScroll(actualScrollX, actualScrollY); - this.scrollTo({ x, y, }); } + this.markForceDirty(true); } private _getViewPortSize() { @@ -790,35 +1151,27 @@ export class Viewport { let width; let height; - let left = this.left * scaleX; - let top = this.top * scaleY; - - if (this._leftOrigin != null) { - left = this._leftOrigin * scaleX; - } - - if (this._topOrigin != null) { - top = this._topOrigin * scaleY; - } + const left = this._leftOrigin * scaleX; + const top = this._topOrigin * scaleY; - if (this._widthOrigin != null) { - width = this._widthOrigin * scaleX; + this._left = left; + this._top = top; + if (this._isRelativeX) { + width = parentWidth - (this._left + this._right); } else { - width = parentWidth - (left + this._right); - this.width = width; + width = (this._widthOrigin || 0) * scaleX; } - if (this._heightOrigin != null) { - height = this._heightOrigin * scaleY; + if (this._isRelativeY) { + height = parentHeight - (this._top + this._bottom); } else { - height = parentHeight - (top + this._bottom); - this.height = height; + height = (this._heightOrigin || 0) * scaleY; } + width = Math.max(0, width); + height = Math.max(0, height); - this._left = left; - this._top = top; - // this._width = width; - // this._height = height; + this._width = width; + this._height = height; // if (!forceCalculate && this._widthOrigin != null) { // width = this._widthOrigin; @@ -841,6 +1194,7 @@ export class Viewport { return { width, height, + parentHeight, }; } @@ -875,9 +1229,17 @@ export class Viewport { }, 2); } + /** + * Scroll Viewport + * Only the 'viewMain' will enter this function, other viewports will not. + * + * caller: scroll.controller viewportMain.proscrollTo(config) + * @param scrollType + * @param pos viewMain 滚动条的位置 + * @param isTrigger + */ private _scroll(scrollType: SCROLL_TYPE, pos: IScrollBarPosition, isTrigger = true) { const { x, y } = pos; - if (this._scrollBar == null) { return; } @@ -907,7 +1269,6 @@ export class Viewport { } const limited = this.limitedScroll(); // 限制滚动范围 - this.onScrollBeforeObserver.notifyObservers({ viewport: this, scrollX: this.scrollX, @@ -924,8 +1285,8 @@ export class Viewport { } const scroll = this.getTransformedScroll(); - this.actualScrollX = scroll.x; - this.actualScrollY = scroll.y; + this.viewportScrollX = scroll.x; + this.viewportScrollY = scroll.y; this.onScrollAfterObserver.notifyObservers({ viewport: this, @@ -945,108 +1306,49 @@ export class Viewport { return limited; } - private _calViewportRelativeBounding(): IViewportBound { - if (this.isActive === false) { - return { - viewBound: { - left: -1, - top: -1, - right: -1, - bottom: -1, - }, - diffBounds: [], - diffX: -1, - diffY: -1, - viewPortPosition: { - top: 0, - left: 0, - bottom: 0, - right: 0, - }, - viewPortKey: this.viewPortKey, - }; - } - - const sceneTrans = this._scene.transform.clone(); - - const m = sceneTrans.getMatrix(); - - const scaleFromX = this._isRelativeX ? (m[0] < 1 ? m[0] : 1) : 1; - - const scaleFromY = this._isRelativeY ? (m[3] < 1 ? m[3] : 1) : 1; - - const scaleToX = this._isRelativeX ? 1 : m[0] < 1 ? m[0] : 1; - - const scaleToY = this._isRelativeY ? 1 : m[3] < 1 ? m[3] : 1; - - let width = this._width; - - let height = this._height; - - const size = this._getViewPortSize(); - - if (m[0] > 1) { - width = size.width; - } + expandBounds(value: { top: number; left: number; bottom: number; right: number }) { + const onePixelFix = FIX_ONE_PIXEL_BLUR_OFFSET * 2; + return { + left: value.left - this.bufferEdgeX - onePixelFix, + right: value.right + this.bufferEdgeX + onePixelFix, + // left: Math.max(this.leftOrigin, value.left - this.bufferEdgeX) - onePixelFix, + // top: Math.max(this.topOrigin, value.top - this.bufferEdgeY) - onePixelFix, + top: value.top - this.bufferEdgeY - onePixelFix, + bottom: value.bottom + this.bufferEdgeY + onePixelFix, + } as IBoundRectNoAngle; + } - if (m[3] > 1) { - height = size.height; + updatePrevCacheBounds(viewBound?: IBoundRectNoAngle) { + if (viewBound) { + this.preCacheBound = this.cacheBound = this.expandBounds(viewBound); } + } - const xFrom: number = this.left * scaleFromX; - const xTo: number = ((width || 0) + this.left) * scaleToX; - const yFrom: number = this.top * scaleFromY; - const yTo: number = ((height || 0) + this.top) * scaleToY; - - /** - * @DR-Univer The coordinates here need to be consistent with the clip in the render, - * which may be caused by other issues that will be optimized later. - */ - // const sceneTrans = this._scene.transform.clone(); - // const m = sceneTrans.getMatrix(); - // const { scaleX, scaleY } = this._getBoundScale(m[0], m[3]); - - // let differenceX = 0; - - // let differenceY = 0; - - // const ratioScrollX = this._scrollBar?.ratioScrollX ?? 1; - - // const ratioScrollY = this._scrollBar?.ratioScrollY ?? 1; - - // if (this._preScrollX != null) { - // differenceX = (this._preScrollX - this.scrollX) / ratioScrollX; - // } - - // if (this._preScrollY != null) { - // differenceY = (this._preScrollY - this.scrollY) / ratioScrollY; - // } - - const topLeft = this.getRelativeVector(Vector2.FromArray([xFrom, yFrom])); - const bottomRight = this.getRelativeVector(Vector2.FromArray([xTo, yTo])); - - const viewBound = { - top: topLeft.y, - left: topLeft.x, - right: bottomRight.x, - bottom: bottomRight.y, - }; - - const preViewBound = this._preViewportBound?.viewBound; - - return { - viewBound, - diffBounds: this._diffViewBound(viewBound, preViewBound), - diffX: (preViewBound?.left || 0) - viewBound.left, - diffY: (preViewBound?.top || 0) - viewBound.top, - viewPortPosition: { - top: yFrom, - left: xFrom, - bottom: yTo, - right: xTo, - }, - viewPortKey: this.viewPortKey, - }; + private _calcCacheUpdate(viewBound: IBoundRectNoAngle, preCacheVisibleBound: + IBoundRectNoAngle | null, _diffX: number, _diffY: number): number { + if (!this._cacheCanvas) return 0b00; + if (!preCacheVisibleBound) return 0b01; + const viewBoundOutCacheArea = + viewBound.right > preCacheVisibleBound.right || + viewBound.top < preCacheVisibleBound.top || + viewBound.left < preCacheVisibleBound.left || + viewBound.bottom > preCacheVisibleBound.bottom + ? 0b01 + : 0b00; + + const edgeX = this.bufferEdgeX / 50; + const edgeY = this.bufferEdgeY / 50; + + const nearEdge = (preCacheVisibleBound.right - viewBound.right < edgeX) || + (viewBound.left - preCacheVisibleBound.left < edgeX) || + (viewBound.top - preCacheVisibleBound.top < edgeY) || + (preCacheVisibleBound.bottom - viewBound.bottom < edgeY) + ? 0b101 + : 0b00; + + const shouldCacheUpdate = nearEdge | viewBoundOutCacheArea; + // console.log(`shouldCacheUpdate${shouldCacheUpdate}`, `${this.viewportKey}:`, this.preCacheBound, this.cacheBound, this.viewBound, this._preCacheVisibleBound); + return shouldCacheUpdate; } private _diffViewBound(mainBound: IBoundRectNoAngle, subBound: Nullable) { @@ -1081,6 +1383,59 @@ export class Viewport { }); } + private _calcDiffCacheBound(prevBound: Nullable, currBound: IBoundRectNoAngle) { + if (!prevBound) { + return [currBound]; + } + const additionalAreas: IBoundRectNoAngle[] = []; + + // curr has an extra part on the left compared to prev. + if (currBound.left < prevBound.left) { + additionalAreas.push({ + top: currBound.top, + bottom: currBound.bottom, + left: currBound.left, + right: prevBound.left, + }); + } + + // curr has an extra part on the right compared to prev. + if (currBound.right > prevBound.right) { + additionalAreas.push({ + top: currBound.top, + bottom: currBound.bottom, + left: prevBound.right, + right: currBound.right, + }); + } + + if (currBound.top < prevBound.top) { + additionalAreas.push({ + top: currBound.top, + bottom: prevBound.top, + left: Math.max(prevBound.left, currBound.left), + right: Math.min(prevBound.right, currBound.right), + }); + } + + if (currBound.bottom > prevBound.bottom) { + additionalAreas.push({ + top: prevBound.bottom, + bottom: currBound.bottom, + left: Math.max(prevBound.left, currBound.left), + right: Math.min(prevBound.right, currBound.right), + }); + } + for (const bound of additionalAreas) { + bound.left = bound.left - this.bufferEdgeX; + bound.right = bound.right + this.bufferEdgeX; + bound.top = bound.top - this.bufferEdgeY; + bound.bottom = bound.bottom + this.bufferEdgeY; + } + + return additionalAreas; + } + private _drawScrollbar(ctx: UniverRenderingContext) { if (!this._scrollBar) { return; @@ -1094,29 +1449,36 @@ export class Viewport { } private _setWithAndHeight(props?: IViewProps) { - this.top = props?.top || 0; - this.left = props?.left || 0; - this.bottom = props?.bottom || 0; - this.right = props?.right || 0; + if (props?.top != null) { + this.top = props.top; + } + + if (props?.left != null) { + this.left = props.left; + } - if (Tools.isDefine(props?.width)) { + if (props?.bottom != null) { + this.bottom = props.bottom; + } + + if (props?.right != null) { + this.right = props.right; + } + + if (Tools.isDefine(props?.width) && !this._isRelativeX) { this.width = props?.width; this._widthOrigin = this.width; - this._isRelativeX = false; } else { this.width = null; this._widthOrigin = null; - this._isRelativeX = true; } - if (Tools.isDefine(props?.height)) { + if (Tools.isDefine(props?.height) && !this._isRelativeY) { this.height = props?.height; this._heightOrigin = this.height; - this._isRelativeY = false; } else { this.height = null; this._heightOrigin = null; - this._isRelativeY = true; } } @@ -1126,4 +1488,12 @@ export class Viewport { return { scaleX, scaleY }; } + + /** + * main canvas element resize + * called by this.scene.getEngine()?.onTransformChangeObservable.add + */ + private _mainCanvasResizeHandler() { + this.markForceDirty(true); + } } diff --git a/packages/facade/jest.setup.ts b/packages/facade/jest.setup.ts new file mode 100644 index 0000000000..c1dd967c6d --- /dev/null +++ b/packages/facade/jest.setup.ts @@ -0,0 +1,19 @@ +/** + * 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 { vi } from 'vitest'; + +vi.stubGlobal('jest', vi); diff --git a/packages/facade/package.json b/packages/facade/package.json index d205ac66f4..d552b80dd1 100644 --- a/packages/facade/package.json +++ b/packages/facade/package.json @@ -88,11 +88,11 @@ "@univerjs/sheets-ui": "workspace:*", "@univerjs/ui": "workspace:*", "@wendellhu/redi": "0.15.2", + "jest-canvas-mock": "^2.5.2", "jsdom": "^24.0.0", "rxjs": "^7.8.1", "typescript": "^5.4.5", "vite": "^5.2.11", - "vitest": "^1.6.0", - "vitest-canvas-mock": "^0.3.3" + "vitest": "^1.6.0" } } diff --git a/packages/facade/src/apis/__tests__/facade.spec.ts b/packages/facade/src/apis/__tests__/facade.spec.ts index 6f03570b0c..4e383af37d 100644 --- a/packages/facade/src/apis/__tests__/facade.spec.ts +++ b/packages/facade/src/apis/__tests__/facade.spec.ts @@ -14,11 +14,11 @@ * limitations under the License. */ +import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { ICellData, IStyleData, Nullable } from '@univerjs/core'; import { ICommandService, IUniverInstanceService } from '@univerjs/core'; import { SetRangeValuesCommand, SetRangeValuesMutation, SetStyleCommand } from '@univerjs/sheets'; import type { Injector } from '@wendellhu/redi'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { RenderComponentType, SheetComponent } from '@univerjs/engine-render'; import { IRenderManagerService } from '@univerjs/engine-render'; @@ -48,10 +48,6 @@ describe('Test FUniver', () => { ) => Nullable; let getSheetRenderComponent: (unitId: string, viewKey: SHEET_VIEW_KEY) => Nullable; - beforeEach(() => { - vi.stubGlobal('jest', vi); - }); - beforeEach(() => { const testBed = createFacadeTestBed(); get = testBed.get; diff --git a/packages/facade/src/apis/sheets/__tests__/f-range.spec.ts b/packages/facade/src/apis/sheets/__tests__/f-range.spec.ts index 046ff42852..e5da2eaea7 100644 --- a/packages/facade/src/apis/sheets/__tests__/f-range.spec.ts +++ b/packages/facade/src/apis/sheets/__tests__/f-range.spec.ts @@ -83,7 +83,7 @@ describe('Test FRange', () => { it('Range setValue', () => { const activeSheet = univerAPI.getActiveWorkbook()?.getActiveSheet(); - // A1 sets the number + // // A1 sets the number const range1 = activeSheet?.getRange(0, 0, 1, 1); range1?.setValue(1); @@ -190,7 +190,6 @@ describe('Test FRange', () => { activeSheet?.getRange(0, 0)?.setBackgroundColor('red'); const range = activeSheet?.getRange(0, 0); expect(range?.getCellStyleData()?.bg?.rgb).toBe('red'); - activeSheet?.getRange(0, 0, 2, 2)?.setFontWeight('bold'); expect(range?.getCellStyleData()?.bl).toBe(1); }); diff --git a/packages/facade/src/apis/sheets/__tests__/f-sheet-hooks.spec.ts b/packages/facade/src/apis/sheets/__tests__/f-sheet-hooks.spec.ts index 9729a0a01b..9456b18aaf 100644 --- a/packages/facade/src/apis/sheets/__tests__/f-sheet-hooks.spec.ts +++ b/packages/facade/src/apis/sheets/__tests__/f-sheet-hooks.spec.ts @@ -18,7 +18,7 @@ import type { ICellData, IStyleData, Nullable, UnitModel, Workbook } from '@univ import { ICommandService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; import { SetHorizontalTextAlignCommand, SetRangeValuesCommand, SetRangeValuesMutation, SetStyleCommand, SetTextWrapCommand, SetVerticalTextAlignCommand } from '@univerjs/sheets'; import type { Injector } from '@wendellhu/redi'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it } from 'vitest'; import { Subject } from 'rxjs'; import type { IDragCellPosition, IHoverCellPosition } from '@univerjs/sheets-ui'; @@ -77,10 +77,6 @@ describe('Test FSheetHooks', () => { let sheetHooks: FSheetHooks; let workbook: Workbook; - beforeEach(() => { - vi.stubGlobal('jest', vi); - }); - beforeEach(() => { // Initialize the subject hoverCurrentCell$ = new Subject>(); diff --git a/packages/facade/src/apis/sheets/__tests__/f-workbook.spec.ts b/packages/facade/src/apis/sheets/__tests__/f-workbook.spec.ts index 579378e652..00d62f6c07 100644 --- a/packages/facade/src/apis/sheets/__tests__/f-workbook.spec.ts +++ b/packages/facade/src/apis/sheets/__tests__/f-workbook.spec.ts @@ -18,7 +18,7 @@ import type { ICellData, IStyleData, Nullable } from '@univerjs/core'; import { ICommandService, IUniverInstanceService } from '@univerjs/core'; import { InsertSheetCommand, InsertSheetMutation, RemoveSheetCommand, RemoveSheetMutation, SetHorizontalTextAlignCommand, SetRangeValuesCommand, SetRangeValuesMutation, SetStyleCommand, SetTextWrapCommand, SetVerticalTextAlignCommand, SetWorksheetActiveOperation } from '@univerjs/sheets'; import type { Injector } from '@wendellhu/redi'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it } from 'vitest'; import type { FUniver } from '../../facade'; import { createFacadeTestBed } from '../../__tests__/create-test-bed'; @@ -40,10 +40,6 @@ describe('Test FWorkbook', () => { endColumn: number ) => Nullable; - beforeEach(() => { - vi.stubGlobal('jest', vi); - }); - beforeEach(() => { const testBed = createFacadeTestBed(); get = testBed.get; diff --git a/packages/facade/src/apis/sheets/__tests__/f-worksheet.spec.ts b/packages/facade/src/apis/sheets/__tests__/f-worksheet.spec.ts index de44e07e98..b12d3f7073 100644 --- a/packages/facade/src/apis/sheets/__tests__/f-worksheet.spec.ts +++ b/packages/facade/src/apis/sheets/__tests__/f-worksheet.spec.ts @@ -18,7 +18,7 @@ import type { ICellData, IStyleData, Nullable } from '@univerjs/core'; import { ICommandService, IUniverInstanceService } from '@univerjs/core'; import { SetHorizontalTextAlignCommand, SetRangeValuesCommand, SetRangeValuesMutation, SetStyleCommand, SetTextWrapCommand, SetVerticalTextAlignCommand } from '@univerjs/sheets'; import type { Injector } from '@wendellhu/redi'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it } from 'vitest'; import type { FUniver } from '../../facade'; import { createFacadeTestBed } from '../../__tests__/create-test-bed'; @@ -40,10 +40,6 @@ describe('Test FWorksheet', () => { endColumn: number ) => Nullable; - beforeEach(() => { - vi.stubGlobal('jest', vi); - }); - beforeEach(() => { const testBed = createFacadeTestBed(); get = testBed.get; diff --git a/packages/facade/vite.config.ts b/packages/facade/vite.config.ts index a426a59388..076b17a4de 100644 --- a/packages/facade/vite.config.ts +++ b/packages/facade/vite.config.ts @@ -3,25 +3,11 @@ import pkg from './package.json'; export default ({ mode }) => createViteConfig({ test: { - setupFiles: ['./vitest.setup.ts'], + setupFiles: [ + './jest.setup.ts', + './vitest.setup.ts', + ], environment: 'jsdom', - deps: { - optimizer: { - web: { - include: ['vitest-canvas-mock'], - }, - }, - }, - poolOptions: { - threads: { - singleThread: true, - }, - }, - environmentOptions: { - jsdom: { - resources: 'usable', - }, - }, }, }, { mode, diff --git a/packages/facade/vitest.setup.ts b/packages/facade/vitest.setup.ts index 32df6b30e4..a27cfb2a58 100644 --- a/packages/facade/vitest.setup.ts +++ b/packages/facade/vitest.setup.ts @@ -14,5 +14,23 @@ * limitations under the License. */ -import 'vitest-canvas-mock'; +import getCanvasWindow from 'jest-canvas-mock/lib/window'; +import { vi } from 'vitest'; +const apis = [ + 'Path2D', + 'CanvasGradient', + 'CanvasPattern', + 'CanvasRenderingContext2D', + 'DOMMatrix', + 'ImageData', + 'TextMetrics', + 'ImageBitmap', + 'createImageBitmap', +] as const; + +const canvasWindow = getCanvasWindow({ document: window.document }); + +apis.forEach((api) => { + vi.stubGlobal(api, canvasWindow[api]); +}); diff --git a/packages/sheets-conditional-formatting-ui/src/controllers/cf.render.controller.ts b/packages/sheets-conditional-formatting-ui/src/controllers/cf.render.controller.ts index 38610a0495..bb34268b23 100644 --- a/packages/sheets-conditional-formatting-ui/src/controllers/cf.render.controller.ts +++ b/packages/sheets-conditional-formatting-ui/src/controllers/cf.render.controller.ts @@ -20,10 +20,10 @@ import { INTERCEPTOR_POINT, SheetInterceptorService } from '@univerjs/sheets'; import { SheetSkeletonManagerService } from '@univerjs/sheets-ui'; import { Inject } from '@wendellhu/redi'; import { bufferTime, filter } from 'rxjs/operators'; -import type { ISheetFontRenderExtension, Spreadsheet } from '@univerjs/engine-render'; +import type { ISheetFontRenderExtension } from '@univerjs/engine-render'; import { IRenderManagerService } from '@univerjs/engine-render'; -import { ConditionalFormattingIcon, ConditionalFormattingRuleModel, ConditionalFormattingService, ConditionalFormattingViewModel, DataBar, dataBarUKey, DEFAULT_PADDING, DEFAULT_WIDTH, IconUKey } from '@univerjs/sheets-conditional-formatting'; +import { ConditionalFormattingRuleModel, ConditionalFormattingService, ConditionalFormattingViewModel, DEFAULT_PADDING, DEFAULT_WIDTH } from '@univerjs/sheets-conditional-formatting'; import type { IConditionalFormattingCellData } from '@univerjs/sheets-conditional-formatting'; @@ -42,29 +42,6 @@ export class RenderController extends Disposable { super(); this._initViewModelInterceptor(); this._initSkeleton(); - this._initRender(); - } - - _initRender() { - const list: [string, new () => any][] = [[dataBarUKey, DataBar], [IconUKey, ConditionalFormattingIcon]]; - const register = (renderId: string) => { - const render = renderId && this._renderManagerService.getRenderById(renderId); - const spreadsheetRender = render && render.mainComponent as Spreadsheet; - if (spreadsheetRender) { - list.forEach(([key, Instance]) => { - if (!spreadsheetRender.getExtensionByKey(key)) { - spreadsheetRender.register(new Instance()); - } - }); - } - }; - this.disposeWithMe(this._renderManagerService.currentRender$.subscribe((renderId) => { - renderId && register(renderId); - })); - const workbook = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)!; - if (workbook) { - register(workbook.getUnitId()); - } } _initSkeleton() { diff --git a/packages/sheets-conditional-formatting/src/render/data-bar.render.ts b/packages/sheets-conditional-formatting/src/render/data-bar.render.ts index 68d30dd4ec..63a1d1ba0b 100644 --- a/packages/sheets-conditional-formatting/src/render/data-bar.render.ts +++ b/packages/sheets-conditional-formatting/src/render/data-bar.render.ts @@ -17,7 +17,7 @@ import type { IRange, IScale } from '@univerjs/core'; import { Range } from '@univerjs/core'; import type { SpreadsheetSkeleton, UniverRenderingContext } from '@univerjs/engine-render'; -import { FIX_ONE_PIXEL_BLUR_OFFSET, SheetExtension } from '@univerjs/engine-render'; +import { FIX_ONE_PIXEL_BLUR_OFFSET, SheetExtension, SpreadsheetExtensionRegistry } from '@univerjs/engine-render'; import type { IDataBarCellData } from './type'; export const dataBarUKey = 'sheet-conditional-rule-data-bar'; @@ -42,7 +42,7 @@ export class DataBar extends SheetExtension { return false; } ctx.save(); - ctx.globalCompositeOperation = 'destination-over'; + // ctx.globalCompositeOperation = 'destination-over'; Range.foreach(spreadsheetSkeleton.rowColumnSegment, (row, col) => { const cellData = worksheet.getCell(row, col) as IDataBarCellData; if (cellData && cellData.dataBar) { @@ -150,3 +150,5 @@ export class DataBar extends SheetExtension { ctx.fill(); } } + +SpreadsheetExtensionRegistry.add(DataBar); diff --git a/packages/sheets-conditional-formatting/src/render/icon.render.ts b/packages/sheets-conditional-formatting/src/render/icon.render.ts index 6776aed1ac..d3b4c54199 100644 --- a/packages/sheets-conditional-formatting/src/render/icon.render.ts +++ b/packages/sheets-conditional-formatting/src/render/icon.render.ts @@ -17,7 +17,7 @@ import type { IRange, IScale } from '@univerjs/core'; import { Range } from '@univerjs/core'; import type { SpreadsheetSkeleton, UniverRenderingContext } from '@univerjs/engine-render'; -import { SheetExtension } from '@univerjs/engine-render'; +import { SheetExtension, SpreadsheetExtensionRegistry } from '@univerjs/engine-render'; import type { IIconType } from '../models/icon-map'; import { EMPTY_ICON_TYPE, iconMap } from '../models/icon-map'; import type { IIconSetCellData } from './type'; @@ -54,7 +54,7 @@ export class ConditionalFormattingIcon extends SheetExtension { return false; } ctx.save(); - ctx.globalCompositeOperation = 'destination-over'; + // ctx.globalCompositeOperation = 'destination-over'; Range.foreach(spreadsheetSkeleton.rowColumnSegment, (row, col) => { const cellData = worksheet.getCell(row, col) as IIconSetCellData; if (cellData?.iconSet) { @@ -115,3 +115,5 @@ export class ConditionalFormattingIcon extends SheetExtension { return `${iconType}_${iconIndex}`; } } + +SpreadsheetExtensionRegistry.add(ConditionalFormattingIcon); diff --git a/packages/sheets-ui/src/common/utils.ts b/packages/sheets-ui/src/common/utils.ts index b61a8c095a..e902d7f59b 100644 --- a/packages/sheets-ui/src/common/utils.ts +++ b/packages/sheets-ui/src/common/utils.ts @@ -106,8 +106,8 @@ export function getCellIndexByOffsetWithMerge(offsetX: number, offsetY: number, const { scaleX, scaleY } = scene.getAncestorScale(); const scrollXY = { - x: activeViewport.actualScrollX, - y: activeViewport.actualScrollY, + x: activeViewport.viewportScrollX, + y: activeViewport.viewportScrollY, }; const cellPos = skeleton.getCellPositionByOffset(offsetX, offsetY, scaleX, scaleY, scrollXY); @@ -182,7 +182,7 @@ export function transformPosition2Offset(x: number, y: number, scene: Scene, ske const freezeWidth = endSheetView.startX - startSheetView.startX; const freezeHeight = endSheetView.startY - startSheetView.startY; - const { top, left, actualScrollX, actualScrollY } = viewMain; + const { top, left, viewportScrollX: actualScrollX, viewportScrollY: actualScrollY } = viewMain; let offsetX: number; // viewMain or viewTop if (x > left) { @@ -250,8 +250,8 @@ export function getHoverCellPosition(currentRender: IRender, workbook: Workbook, const { scaleX, scaleY } = scene.getAncestorScale(); const scrollXY = { - x: activeViewport.actualScrollX, - y: activeViewport.actualScrollY, + x: activeViewport.viewportScrollX, + y: activeViewport.viewportScrollY, }; const position: IPosition = { diff --git a/packages/sheets-ui/src/controllers/cell-custom-render.controller.ts b/packages/sheets-ui/src/controllers/cell-custom-render.controller.ts index c75c5a372a..c7504490c8 100644 --- a/packages/sheets-ui/src/controllers/cell-custom-render.controller.ts +++ b/packages/sheets-ui/src/controllers/cell-custom-render.controller.ts @@ -72,8 +72,8 @@ export class CellCustomRenderController extends Disposable implements IRenderCon const { scaleX, scaleY } = scene.getAncestorScale(); const scrollXY = { - x: activeViewport.actualScrollX, - y: activeViewport.actualScrollY, + x: activeViewport.viewportScrollX, + y: activeViewport.viewportScrollY, }; const cellPos = skeleton.getCellPositionByOffset(offsetX, offsetY, scaleX, scaleY, scrollXY); diff --git a/packages/sheets-ui/src/controllers/render-controllers/freeze.render-controller.ts b/packages/sheets-ui/src/controllers/render-controllers/freeze.render-controller.ts index a8a56caa76..7c27b69c9f 100644 --- a/packages/sheets-ui/src/controllers/render-controllers/freeze.render-controller.ts +++ b/packages/sheets-ui/src/controllers/render-controllers/freeze.render-controller.ts @@ -90,7 +90,7 @@ const FREEZE_COLUMN_MAIN_NAME = '__SpreadsheetFreezeColumnMainName__'; const FREEZE_COLUMN_HEADER_NAME = '__SpreadsheetFreezeColumnHeaderName__'; -const FREEZE_SIZE_NORMAL = 4; +const FREEZE_SIZE_NORMAL = 2; const AUXILIARY_CLICK_HIDDEN_OBJECT_TRANSPARENCY = 0.01; @@ -214,9 +214,13 @@ export class HeaderFreezeRenderController extends Disposable implements IRenderC const scale = Math.max(scene.scaleX, scene.scaleY); - const FREEZE_SIZE = FREEZE_SIZE_NORMAL / (scale < 1 ? 1 : scale); + let FREEZE_SIZE = FREEZE_SIZE_NORMAL / (scale < 1 ? 1 : scale); if (freezeDirectionType === FREEZE_DIRECTION_TYPE.ROW) { + if (freezeRow === -1 || freezeRow === 0) { + FREEZE_SIZE = FREEZE_SIZE * 2; + } + const FREEZE_OFFSET = FREEZE_SIZE; this._rowFreezeHeaderRect = new Rect(FREEZE_ROW_HEADER_NAME, { @@ -235,7 +239,7 @@ export class HeaderFreezeRenderController extends Disposable implements IRenderC this._rowFreezeMainRect = new Rect(FREEZE_ROW_MAIN_NAME, { fill, - width: shapeWidth * 2, + width: shapeWidth * 2 / scale, height: FREEZE_SIZE, left: rowHeaderWidthAndMarginLeft, top: startY - FREEZE_OFFSET, @@ -244,6 +248,10 @@ export class HeaderFreezeRenderController extends Disposable implements IRenderC scene.addObjects([this._rowFreezeHeaderRect, this._rowFreezeMainRect], SHEET_COMPONENT_HEADER_LAYER_INDEX); } else { + if (freezeColumn === -1 || freezeColumn === 0) { + FREEZE_SIZE = FREEZE_SIZE * 2; + } + const FREEZE_OFFSET = FREEZE_SIZE; this._columnFreezeHeaderRect = new Rect(FREEZE_COLUMN_HEADER_NAME, { @@ -263,7 +271,7 @@ export class HeaderFreezeRenderController extends Disposable implements IRenderC this._columnFreezeMainRect = new Rect(FREEZE_COLUMN_MAIN_NAME, { fill, width: FREEZE_SIZE, - height: shapeHeight * 2, + height: shapeHeight * 2 / scale, left: startX - FREEZE_OFFSET, top: columnHeaderHeightAndMarginTop, zIndex: 3, @@ -564,7 +572,7 @@ export class HeaderFreezeRenderController extends Disposable implements IRenderC const oldFreeze = worksheet.getConfig()?.freeze; let xSplit = oldFreeze?.xSplit || 0; let ySplit = oldFreeze?.ySplit || 0; - const viewPortKey = this._activeViewport?.viewPortKey; + const viewPortKey = this._activeViewport?.viewportKey; if (freezeDirectionType === FREEZE_DIRECTION_TYPE.ROW) { if ( !viewPortKey || @@ -829,7 +837,7 @@ export class HeaderFreezeRenderController extends Disposable implements IRenderC .updateScroll({ actualScrollY: startSheetView.startY, x: viewMain.scrollX, - actualScrollX: viewMain.actualScrollX, + actualScrollX: viewMain.viewportScrollX, }); viewRowTop.resize({ left: 0, @@ -884,7 +892,7 @@ export class HeaderFreezeRenderController extends Disposable implements IRenderC .updateScroll({ actualScrollX: startSheetView.startX, y: viewMain.scrollY, - actualScrollY: viewMain.actualScrollY, + actualScrollY: viewMain.viewportScrollY, }); viewColumnLeft.resize({ left: rowHeaderWidthAndMarginLeft, @@ -948,7 +956,7 @@ export class HeaderFreezeRenderController extends Disposable implements IRenderC .updateScroll({ actualScrollX: startSheetView.startX, y: viewMain.scrollY, - actualScrollY: viewMain.actualScrollY, + actualScrollY: viewMain.viewportScrollY, }); viewMainTop.resize({ left: rowHeaderWidthAndMarginLeft + leftGap, @@ -960,7 +968,7 @@ export class HeaderFreezeRenderController extends Disposable implements IRenderC .updateScroll({ actualScrollY: startSheetView.startY, x: viewMain.scrollX, - actualScrollX: viewMain.actualScrollX, + actualScrollX: viewMain.viewportScrollX, }); viewMainLeftTop.resize({ left: rowHeaderWidthAndMarginLeft, @@ -1510,6 +1518,15 @@ export class HeaderFreezeRenderController extends Disposable implements IRenderC return getSheetObject(this._context.unit, this._context); } + /** + * 调整冻结 & 缩放都会进入 + * 但是窗口 resize 并不会进入 + * @param startRow + * @param startColumn + * @param ySplit + * @param xSplit + * @param resetScroll + */ private _refreshFreeze( startRow: number, startColumn: number, @@ -1529,7 +1546,11 @@ export class HeaderFreezeRenderController extends Disposable implements IRenderC this._createFreeze(FREEZE_DIRECTION_TYPE.COLUMN, newFreeze); this._updateViewport(startRow, startColumn, ySplit, xSplit, resetScroll); - this._getSheetObject()?.spreadsheet.makeForceDirty(); + + // no need to set viewport markForceDirty again + // when change freeze area ---> viewport.resize ---> viewport.markForceDity() + // this._getSheetObject()?.spreadsheet.makeForceDirty(); + // this._getSheetObject()?.scene.getViewports().forEach(vp => vp.makeForceDirty()); } } 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 d39750558f..0e7b091a5b 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 @@ -242,6 +242,7 @@ export class SheetsScrollRenderController extends Disposable implements IRenderC return; } + // 数据源 packages/sheets-ui/src/services/scroll-manager.service.ts@_addByParam const { sheetViewStartRow, sheetViewStartColumn, offsetX, offsetY } = param; const { startX, startY } = skeleton.getCellByIndexWithNoHeader( diff --git a/packages/sheets-ui/src/controllers/sheet-render.controller.ts b/packages/sheets-ui/src/controllers/sheet-render.controller.ts index 6a674d2362..993bcc0ec6 100644 --- a/packages/sheets-ui/src/controllers/sheet-render.controller.ts +++ b/packages/sheets-ui/src/controllers/sheet-render.controller.ts @@ -15,6 +15,7 @@ */ import type { ICommandInfo, + IRange, Workbook } from '@univerjs/core'; import { CommandType, ICommandService, @@ -22,15 +23,18 @@ import { CommandType, IUniverInstanceService, LifecycleStages, OnLifecycle, + Rectangle, RxDisposable, toDisposable, UniverInstanceType, } from '@univerjs/core'; -import type { Rect, SpreadsheetColumnHeader, SpreadsheetRowHeader } from '@univerjs/engine-render'; +import type { IViewportInfos, Rect, Scene, SpreadsheetColumnHeader, SpreadsheetRowHeader, Viewport } from '@univerjs/engine-render'; import { IRenderManagerService, RENDER_RAW_FORMULA_KEY, Spreadsheet } from '@univerjs/engine-render'; import { COMMAND_LISTENER_SKELETON_CHANGE, COMMAND_LISTENER_VALUE_CHANGE, + MoveRangeMutation, + SetRangeValuesMutation, SetWorksheetActiveOperation, } from '@univerjs/sheets'; import type { IDisposable } from '@wendellhu/redi'; @@ -167,6 +171,8 @@ export class SheetRenderController extends RxDisposable { // Change the skeleton to render when the sheet is changed. // Should also check the init sheet. + // setCurrent ---> currentSkeletonBefore$ ---> zoom.controller.subscribe ---> scene._setTransForm ---> viewports markDirty + // setCurrent ---> currentSkeleton$ ---> scroll.controller.subscribe ---> scene?.transformByState ---> scene._setTransFor this._sheetSkeletonManagerService.setCurrent({ unitId, sheetId, @@ -177,7 +183,7 @@ export class SheetRenderController extends RxDisposable { } if (command.type === CommandType.MUTATION) { - this._renderManagerService.getRenderById(unitId)?.mainComponent?.makeDirty(); // refresh spreadsheet + this._markSpreadsheetDirty(unitId, command); } }) ); @@ -190,9 +196,99 @@ export class SheetRenderController extends RxDisposable { .subscribe(() => { this._renderManagerService.getRenderAll().forEach((renderer) => { if (renderer.mainComponent instanceof Spreadsheet) { - renderer.mainComponent.makeForceDirty(true); + (renderer.mainComponent as Spreadsheet).makeForceDirty(true); } }); }); } + + private _spreadsheetViewports(scene: Scene) { + return scene.getViewports().filter((v) => ['viewMain', 'viewMainLeftTop', 'viewMainTop', 'viewMainLeft'].includes(v.viewportKey)); + } + + private _markSpreadsheetDirty(unitId: string, command: ICommandInfo) { + const currentRender = this._renderManagerService.getRenderById(unitId); + if (!currentRender) return; + const { mainComponent: spreadsheet, scene } = currentRender; + // 现在 spreadsheet.markDirty 会调用 vport.markDirty + // 因为其他 controller 中存在 mainComponent?.makeDirty() 的调用, 不止是 sheet-render.controller 在标脏 + if (spreadsheet) { + spreadsheet.makeDirty(); // refresh spreadsheet + } + scene.makeDirty(); + if (!command.params) return; + const cmdParams = command.params as Record; + const viewports = this._spreadsheetViewports(scene); + if (command.id === SetRangeValuesMutation.id && cmdParams.cellValue) { + const dirtyRange: IRange = this._cellValueToRange(cmdParams.cellValue); + const dirtyBounds = this._rangeToBounds([dirtyRange]); + this._markViewportDirty(viewports, dirtyBounds); + (spreadsheet as unknown as Spreadsheet).setDirtyArea(dirtyBounds); + } + + if (command.id === MoveRangeMutation.id && cmdParams.from && cmdParams.to) { + const fromRange = this._cellValueToRange(cmdParams.from.value); + const toRange = this._cellValueToRange(cmdParams.to.value); + const dirtyBounds = this._rangeToBounds([fromRange, toRange]); + this._markViewportDirty(viewports, dirtyBounds); + (spreadsheet as unknown as Spreadsheet).setDirtyArea(dirtyBounds); + } + } + + /** + * cellValue data structure: + * {[row]: { [col]: value}} + * @param cellValue + * @returns + */ + private _cellValueToRange(cellValue: Record>) { + const rows = Object.keys(cellValue).map(Number); + const columns = []; + + for (const [_row, columnObj] of Object.entries(cellValue)) { + for (const column in columnObj) { + columns.push(Number(column)); + } + } + + const startRow = Math.min(...rows); + const endRow = Math.max(...rows); + const startColumn = Math.min(...columns); + const endColumn = Math.max(...columns); + + return { + startRow, + endRow, + startColumn, + endColumn, + } as IRange; + } + + private _rangeToBounds(ranges: IRange[]) { + const sk = this._sheetSkeletonManagerService.getCurrent()?.skeleton!; + + const { rowHeightAccumulation, columnWidthAccumulation, rowHeaderWidth, columnHeaderHeight } = sk; + // rowHeightAccumulation 已经表示的是行底部的高度 + const dirtyBounds: IViewportInfos[] = []; + for (const r of ranges) { + const { startRow, endRow, startColumn, endColumn } = r; + const top = startRow === 0 ? 0 : rowHeightAccumulation[startRow - 1] + columnHeaderHeight; + const bottom = rowHeightAccumulation[endRow] + columnHeaderHeight; + const left = startColumn === 0 ? 0 : columnWidthAccumulation[startColumn - 1] + rowHeaderWidth; + const right = columnWidthAccumulation[endColumn] + rowHeaderWidth; + dirtyBounds.push({ top, left, bottom, right, width: right - left, height: bottom - top }); + } + return dirtyBounds; + } + + private _markViewportDirty(viewports: Viewport[], dirtyBounds: IViewportInfos[]) { + const activeViewports = viewports.filter((vp) => vp.isActive && vp.cacheBound); + for (const vp of activeViewports) { + for (const b of dirtyBounds) { + if (Rectangle.hasIntersectionBetweenTwoBounds(vp.cacheBound!, b)) { + vp.markDirty(true); + } + } + } + } } diff --git a/packages/sheets-ui/src/services/canvas-pop-manager.service.ts b/packages/sheets-ui/src/services/canvas-pop-manager.service.ts index 0c322115ac..db98eab479 100644 --- a/packages/sheets-ui/src/services/canvas-pop-manager.service.ts +++ b/packages/sheets-ui/src/services/canvas-pop-manager.service.ts @@ -262,8 +262,8 @@ export class SheetCanvasPopManagerService extends Disposable { const { scaleX, scaleY } = scene.getAncestorScale(); const scrollXY = { - x: activeViewport.actualScrollX, - y: activeViewport.actualScrollY, + x: activeViewport.viewportScrollX, + y: activeViewport.viewportScrollY, }; return { diff --git a/packages/sheets-ui/src/services/selection/selection-render.service.ts b/packages/sheets-ui/src/services/selection/selection-render.service.ts index fe2dbc3ef5..1f5b17610a 100644 --- a/packages/sheets-ui/src/services/selection/selection-render.service.ts +++ b/packages/sheets-ui/src/services/selection/selection-render.service.ts @@ -808,8 +808,8 @@ export class SelectionRenderService implements ISelectionRenderService { yCrossTime += 1; } - const startKey = startViewport.viewPortKey; - const endKey = endViewport.viewPortKey; + const startKey = startViewport.viewportKey; + const endKey = endViewport.viewportKey; if (startKey === VIEWPORT_KEY.VIEW_ROW_TOP) { if (moveOffsetY < viewportMain.top && (selection?.endRow ?? 0) < (freeze?.startRow ?? 0)) { diff --git a/packages/sheets-ui/src/services/selection/selection-shape-extension.ts b/packages/sheets-ui/src/services/selection/selection-shape-extension.ts index 56e6a4c22d..59005add7d 100644 --- a/packages/sheets-ui/src/services/selection/selection-shape-extension.ts +++ b/packages/sheets-ui/src/services/selection/selection-shape-extension.ts @@ -114,7 +114,7 @@ export class SelectionShapeExtension { ySplit: 0, }; - switch (viewport.viewPortKey) { + switch (viewport.viewportKey) { case VIEWPORT_KEY.VIEW_MAIN: return selection.endRow >= freeze.startRow && selection.endColumn >= freeze.startColumn; @@ -789,7 +789,7 @@ export class SelectionShapeExtension { const scrollTimer = ScrollTimer.create( scene, - this._activeViewport.viewPortKey === VIEWPORT_KEY.VIEW_MAIN ? ScrollTimerType.ALL : ScrollTimerType.NONE + this._activeViewport.viewportKey === VIEWPORT_KEY.VIEW_MAIN ? ScrollTimerType.ALL : ScrollTimerType.NONE ); scrollTimer.startScroll(newEvtOffsetX, newEvtOffsetY, viewportMain); @@ -816,7 +816,7 @@ export class SelectionShapeExtension { const newSelection = this._targetSelection; - if (viewportMain && currentViewport && this._activeViewport?.viewPortKey !== currentViewport?.viewPortKey) { + if (viewportMain && currentViewport && this._activeViewport?.viewportKey !== currentViewport?.viewportKey) { let movingRange: IRangeWithCoord; if (newSelection.startRow !== originStartRow) { scrollTimer.scrollTimerType = ScrollTimerType.Y; diff --git a/packages/sheets-ui/src/views/sheet-canvas-view.ts b/packages/sheets-ui/src/views/sheet-canvas-view.ts index eaa011c2aa..cc5575c60a 100644 --- a/packages/sheets-ui/src/views/sheet-canvas-view.ts +++ b/packages/sheets-ui/src/views/sheet-canvas-view.ts @@ -16,7 +16,7 @@ import type { Workbook, Worksheet } from '@univerjs/core'; import { ICommandService, RxDisposable, toDisposable } from '@univerjs/core'; -import type { IRenderContext, IRenderController, IWheelEvent } from '@univerjs/engine-render'; +import type { IRenderContext, IRenderController, IWheelEvent, Scene } from '@univerjs/engine-render'; import { IRenderManagerService, Layer, @@ -24,6 +24,7 @@ import { Rect, RENDER_CLASS_TYPE, ScrollBar, + SHEET_VIEWPORT_KEY, Spreadsheet, SpreadsheetColumnHeader, SpreadsheetRowHeader, @@ -37,7 +38,6 @@ import { SHEET_COMPONENT_HEADER_LAYER_INDEX, SHEET_COMPONENT_MAIN_LAYER_INDEX, SHEET_VIEW_KEY, - VIEWPORT_KEY, } from '../common/keys'; import { SheetSkeletonManagerService } from '../services/sheet-skeleton-manager.service'; @@ -86,9 +86,9 @@ export class SheetCanvasView extends RxDisposable implements IRenderController { const sheetId = worksheet.getSheetId(); - this._addViewport(worksheet); - const spreadsheet = new Spreadsheet(SHEET_VIEW_KEY.MAIN); + const _viewMain = this._addViewport(worksheet); + const spreadsheetRowHeader = new SpreadsheetRowHeader(SHEET_VIEW_KEY.ROW); const spreadsheetColumnHeader = new SpreadsheetColumnHeader(SHEET_VIEW_KEY.COLUMN); const SpreadsheetLeftTopPlaceholder = new Rect(SHEET_VIEW_KEY.LEFT_TOP, { @@ -115,67 +115,129 @@ export class SheetCanvasView extends RxDisposable implements IRenderController { scene.enableLayerCache(SHEET_COMPONENT_MAIN_LAYER_INDEX, SHEET_COMPONENT_HEADER_LAYER_INDEX); } - private _addViewport(worksheet: Worksheet) { - const scene = this._context.scene; - - const { rowHeader, columnHeader } = worksheet.getConfig(); - - const viewMain = new Viewport(VIEWPORT_KEY.VIEW_MAIN, scene, { + // eslint-disable-next-line max-lines-per-function + private _initViewports(scene: Scene, rowHeader: { width: number }, columnHeader: { height: number }) { + const bufferEdgeX = 100; + const bufferEdgeY = 100; + // window.sc = scene; + const viewMain = new Viewport(SHEET_VIEWPORT_KEY.VIEW_MAIN, scene, { left: rowHeader.width, top: columnHeader.height, bottom: 0, right: 0, isWheelPreventDefaultX: true, + isRelativeX: true, + isRelativeY: true, + allowCache: true, + bufferEdgeX, + bufferEdgeY, + }); + const viewMainLeftTop = new Viewport(SHEET_VIEWPORT_KEY.VIEW_MAIN_LEFT_TOP, scene, { + isWheelPreventDefaultX: true, + active: false, + isRelativeX: false, + isRelativeY: false, + allowCache: true, + bufferEdgeX: 0, + bufferEdgeY: 0, + }); + + const viewMainLeft = new Viewport(SHEET_VIEWPORT_KEY.VIEW_MAIN_LEFT, scene, { + isWheelPreventDefaultX: true, + active: false, + isRelativeX: false, + isRelativeY: true, + allowCache: true, + bufferEdgeX: 0, + bufferEdgeY, + }); + + const viewMainTop = new Viewport(SHEET_VIEWPORT_KEY.VIEW_MAIN_TOP, scene, { + isWheelPreventDefaultX: true, + active: false, + isRelativeX: true, + isRelativeY: false, + allowCache: true, + bufferEdgeX, + bufferEdgeY: 0, }); - new Viewport(VIEWPORT_KEY.VIEW_ROW_TOP, scene, { + const viewRowTop = new Viewport(SHEET_VIEWPORT_KEY.VIEW_ROW_TOP, scene, { active: false, isWheelPreventDefaultX: true, + isRelativeX: false, + isRelativeY: false, }); - new Viewport(VIEWPORT_KEY.VIEW_ROW_BOTTOM, scene, { + const viewRowBottom = new Viewport(SHEET_VIEWPORT_KEY.VIEW_ROW_BOTTOM, scene, { left: 0, top: columnHeader.height, bottom: 0, width: rowHeader.width, isWheelPreventDefaultX: true, + isRelativeX: false, + isRelativeY: true, }); - new Viewport(VIEWPORT_KEY.VIEW_COLUMN_LEFT, scene, { + const viewColumnLeft = new Viewport(SHEET_VIEWPORT_KEY.VIEW_COLUMN_LEFT, scene, { active: false, isWheelPreventDefaultX: true, + isRelativeX: false, + isRelativeY: false, }); - new Viewport(VIEWPORT_KEY.VIEW_COLUMN_RIGHT, scene, { + const viewColumnRight = new Viewport(SHEET_VIEWPORT_KEY.VIEW_COLUMN_RIGHT, scene, { left: rowHeader.width, top: 0, height: columnHeader.height, right: 0, isWheelPreventDefaultX: true, + isRelativeX: true, + isRelativeY: false, }); - new Viewport(VIEWPORT_KEY.VIEW_LEFT_TOP, scene, { + const viewLeftTop = new Viewport(SHEET_VIEWPORT_KEY.VIEW_LEFT_TOP, scene, { left: 0, top: 0, width: rowHeader.width, height: columnHeader.height, isWheelPreventDefaultX: true, + isRelativeX: false, + isRelativeY: false, }); - new Viewport(VIEWPORT_KEY.VIEW_MAIN_LEFT_TOP, scene, { - isWheelPreventDefaultX: true, - active: false, - }); - - new Viewport(VIEWPORT_KEY.VIEW_MAIN_LEFT, scene, { - isWheelPreventDefaultX: true, - active: false, - }); + return { + viewMain, + viewLeftTop, + viewMainLeftTop, + viewMainLeft, + viewMainTop, + viewColumnLeft, + viewRowTop, + viewRowBottom, + viewColumnRight, + }; + } - new Viewport(VIEWPORT_KEY.VIEW_MAIN_TOP, scene, { - isWheelPreventDefaultX: true, - active: false, - }); + /** + * + * initViewport & wheel event + * +-----------------+--------------------+-------------------+ + * | VIEW_LEFT_TOP | VIEW_COLUMN_LEFT | VIEW_COLUMN_RIGHT | + * +-----------------+--------------------+-------------------+ + * | VIEW_ROW_TOP | VIEW_MAIN_LEFT_TOP | VIEW_MAIN_TOP | + * +-----------------+--------------------+-------------------+ + * | VIEW_ROW_BOTTOM | VIEW_MAIN_LEFT | VIEW_MAIN | + * +-----------------+--------------------+-------------------+ + */ + // eslint-disable-next-line max-lines-per-function + private _addViewport(worksheet: Worksheet) { + const scene = this._context.scene; + if (scene == null) { + return; + } + const { rowHeader, columnHeader } = worksheet.getConfig(); + const { viewMain } = this._initViewports(scene, rowHeader, columnHeader); // mouse scroll this.disposeWithMe( @@ -193,7 +255,8 @@ export class SheetCanvasView extends RxDisposable implements IRenderController { const deltaFactor = Math.abs(evt.deltaX); // let magicNumber = deltaFactor < 40 ? 2 : deltaFactor < 80 ? 3 : 4; const scrollNum = deltaFactor; - + // 展示更多右侧内容,evt.deltaX > 0 + // 展示更多左侧内容, evt.deltaX < 0 if (evt.deltaX > 0) { offsetX = scrollNum; } else { @@ -263,9 +326,7 @@ export class SheetCanvasView extends RxDisposable implements IRenderController { // create a scroll bar new ScrollBar(viewMain); - scene.attachControl(); - return viewMain; } } diff --git a/packages/slides/src/views/render/adaptors/docs-adaptor.ts b/packages/slides/src/views/render/adaptors/docs-adaptor.ts index a8b06cd4c5..27d9964164 100644 --- a/packages/slides/src/views/render/adaptors/docs-adaptor.ts +++ b/packages/slides/src/views/render/adaptors/docs-adaptor.ts @@ -115,6 +115,8 @@ export class DocsAdaptor extends ObjectAdaptor { top: 0, bottom: 0, right: 0, + isRelativeX: true, + isRelativeY: true, isWheelPreventDefaultX: true, }); diff --git a/packages/slides/src/views/render/adaptors/slide-adaptor.ts b/packages/slides/src/views/render/adaptors/slide-adaptor.ts index 58531f395c..7235c5ea31 100644 --- a/packages/slides/src/views/render/adaptors/slide-adaptor.ts +++ b/packages/slides/src/views/render/adaptors/slide-adaptor.ts @@ -130,6 +130,8 @@ export class SlideAdaptor extends ObjectAdaptor { top: 0, bottom: 0, right: 0, + isRelativeX: true, + isRelativeY: true, }); viewMain.closeClip(); diff --git a/packages/slides/src/views/render/adaptors/spreadsheet-adaptor.ts b/packages/slides/src/views/render/adaptors/spreadsheet-adaptor.ts index 48bfcc1d4a..5c0b6e8c74 100644 --- a/packages/slides/src/views/render/adaptors/spreadsheet-adaptor.ts +++ b/packages/slides/src/views/render/adaptors/spreadsheet-adaptor.ts @@ -172,6 +172,8 @@ export class SpreadsheetAdaptor extends ObjectAdaptor { top: columnHeaderHeightScale, bottom: 0, right: 0, + isRelativeX: true, + isRelativeY: true, isWheelPreventDefaultX: true, }); const viewTop = new Viewport(SHEET_VIEW_KEY.VIEW_TOP + id, scene, { @@ -179,6 +181,7 @@ export class SpreadsheetAdaptor extends ObjectAdaptor { height: columnHeaderHeightScale, top: 0, right: 0, + isRelativeX: true, isWheelPreventDefaultX: true, }); const viewLeft = new Viewport(SHEET_VIEW_KEY.VIEW_LEFT + id, scene, { @@ -186,6 +189,7 @@ export class SpreadsheetAdaptor extends ObjectAdaptor { bottom: 0, top: columnHeaderHeightScale, width: rowHeaderWidthScale, + isRelativeY: true, isWheelPreventDefaultX: true, }); diff --git a/packages/slides/src/views/render/canvas-view.ts b/packages/slides/src/views/render/canvas-view.ts index 5af53eb31d..6c3fcfe80b 100644 --- a/packages/slides/src/views/render/canvas-view.ts +++ b/packages/slides/src/views/render/canvas-view.ts @@ -137,6 +137,7 @@ export class CanvasView extends RxDisposable { return this._renderManagerService.getRenderById(slideDataModel.getUnitId()); } + // eslint-disable-next-line max-lines-per-function private _addNewRender(unitId: string) { const slideDataModel = this._univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_SLIDE); @@ -175,6 +176,8 @@ export class CanvasView extends RxDisposable { top: 0, bottom: 0, right: 0, + isRelativeX: true, + isRelativeY: true, isWheelPreventDefaultX: true, }); @@ -412,6 +415,8 @@ export class CanvasView extends RxDisposable { top: 0, bottom: 0, right: 0, + isRelativeX: true, + isRelativeY: true, }); viewMain.closeClip(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 387bda1126..d486678ac6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -764,6 +764,9 @@ importers: '@wendellhu/redi': specifier: 0.15.2 version: 0.15.2 + jest-canvas-mock: + specifier: ^2.5.2 + version: 2.5.2 jsdom: specifier: ^24.0.0 version: 24.0.0 @@ -779,9 +782,6 @@ importers: vitest: specifier: ^1.6.0 version: 1.6.0(@types/node@20.12.12)(happy-dom@13.3.8)(jsdom@24.0.0)(less@4.2.0)(sass@1.72.0)(terser@5.30.0) - vitest-canvas-mock: - specifier: ^0.3.3 - version: 0.3.3(vitest@1.6.0(@types/node@20.12.12)(happy-dom@13.3.8)(jsdom@24.0.0)(less@4.2.0)(sass@1.72.0)(terser@5.30.0)) packages/find-replace: dependencies: @@ -9922,11 +9922,6 @@ packages: terser: optional: true - vitest-canvas-mock@0.3.3: - resolution: {integrity: sha512-3P968tYBpqYyzzOaVtqnmYjqbe13576/fkjbDEJSfQAkHtC5/UjuRHOhFEN/ZV5HVZIkaROBUWgazDKJ+Ibw+Q==} - peerDependencies: - vitest: '*' - vitest@1.6.0: resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -19489,11 +19484,6 @@ snapshots: sass: 1.72.0 terser: 5.30.0 - vitest-canvas-mock@0.3.3(vitest@1.6.0(@types/node@20.12.12)(happy-dom@13.3.8)(jsdom@24.0.0)(less@4.2.0)(sass@1.72.0)(terser@5.30.0)): - dependencies: - jest-canvas-mock: 2.5.2 - vitest: 1.6.0(@types/node@20.12.12)(happy-dom@13.3.8)(jsdom@24.0.0)(less@4.2.0)(sass@1.72.0)(terser@5.30.0) - vitest@1.6.0(@types/node@20.12.12)(happy-dom@13.3.8)(jsdom@24.0.0)(less@4.2.0)(sass@1.72.0)(terser@5.30.0): dependencies: '@vitest/expect': 1.6.0