feat(render-engine): move cache to viewport (#2188)

* fix(sheet): render viewport wrong size

* fix: new viewport need more param

* feat: cache by viewport

* fix: markdirty & scrolling was conflict before, so scrolling & dirty did not refresh whole viewport

* fix: 0510-1725

fix: #676 #677

fix: browser native zoom

* fix: blurry when applyCache especially in windows 175%

* fix: merge cells background

fix: #676

test: #677

fix: #700

fix: aux line background

fix: spreadsheet

fix: precise rect for background

* feat: fix cf render error on printing mode

fix: #700

* fix: viewport make dirty false

chore: eslint fix

* test(facade): fix canvas mock

fix: new viewport need more param222

test: mock canvas

fix: rm useless

* fix: should beginPath before clip()

* chore: comments to english

fix: expand shouldupdatecache

chore: better code

* fix: change viewport update timing

fix: update visible cache viewport area

---------

Co-authored-by: DR-Univer <wbfsa@qq.com>
Co-authored-by: zw <zhang9748@foxmail.com>
Co-authored-by: 白熱 <sonne@asaki.me>
This commit is contained in:
lumix
2024-05-25 10:34:38 +08:00
committed by GitHub
co-authored by DR-Univer zw 白熱
parent a1beac6606
commit a4e5b11eb4
61 changed files with 1723 additions and 744 deletions
+26
View File
@@ -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;
}
}
@@ -107,6 +107,8 @@ export class DocCanvasView extends RxDisposable {
top: 0,
bottom: 0,
right: 0,
isRelativeX: true,
isRelativeY: true,
isWheelPreventDefaultX: true,
});
+8 -3
View File
@@ -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 */
}
+77 -1
View File
@@ -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),
};
}
@@ -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;
+52 -2
View File
@@ -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;
}
+9 -12
View File
@@ -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);
@@ -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<T, U, V> extends BaseObject {
return this._extensions.get(uKey);
}
draw(ctx: UniverRenderingContext, bounds?: IViewportBound) {
draw(ctx: UniverRenderingContext, bounds?: IViewportInfo) {
/* abstract */
}
@@ -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);
}
}
@@ -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;
}
@@ -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);
}
@@ -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;
@@ -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;
@@ -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<T, U, V> {
uKey: string = '';
@@ -50,7 +55,7 @@ export class ComponentExtension<T, U, V> {
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 */
}
@@ -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;
@@ -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();
backgroundCache.forValue((rowIndex, columnIndex) => {
const cellInfo = backgroundPositions?.getValue(rowIndex, columnIndex);
const backgroundPaths = new Path2D();
backgroundCache.forValue((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;
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;
}
// 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.isRenderDiffRangesByCell(
{
startRow: mergeInfo.startRow,
endRow: mergeInfo.endRow,
startColumn: mergeInfo.startColumn,
endColumn: mergeInfo.endColumn,
},
diffRanges
)
) {
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();
}
@@ -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;
@@ -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);
}
}
@@ -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<string>();
// eslint-disable-next-line max-lines-per-function
Range.foreach(rowColumnSegment, (row, col) => {
let cellData = worksheet.getCell(row, col);
const cellInfo = this.getCellIndex(
@@ -37,14 +37,14 @@ export class SheetExtension extends ComponentExtension<SpreadsheetSkeleton, SHEE
return getCellByIndex(rowIndex, columnIndex, rowHeightAccumulation, columnWidthAccumulation, dataMergeCache);
}
isRenderDiffRangesByCell(range: IRange, diffRanges?: IRange[]) {
isRenderDiffRangesByCell(rangeP: IRange, diffRanges?: IRange[]) {
if (diffRanges == null || diffRanges.length === 0) {
return true;
}
for (const range of diffRanges) {
const { startRow, startColumn, endRow, endColumn } = range;
const isIntersect = Rectangle.intersects(range, {
const isIntersect = Rectangle.intersects(rangeP, {
startRow,
endRow,
startColumn,
@@ -139,4 +139,47 @@ export class SheetExtension extends ComponentExtension<SpreadsheetSkeleton, SHEE
return false;
}
/**
* 传入的 row 范围和 diffRanges 有相交, 返回 true
* @param curStartRow
* @param curEndRow
* @param viewranges
*/
isRowInRanges(curStartRow: number, curEndRow: number, viewranges?: IRange[]) {
if (viewranges == null || viewranges.length === 0) {
return true;
}
for (const range of viewranges) {
const { startRow, endRow } = range;
if (curStartRow >= 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;
}
}
@@ -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;
}
@@ -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;
@@ -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<SpreadsheetSkeleton
this._skeleton = spreadsheetSkeleton;
}
override render(mainCtx: UniverRenderingContext, bounds?: IViewportBound) {
override render(mainCtx: UniverRenderingContext, bounds?: IViewportInfo) {
if (!this.visible) {
this.makeDirty(false);
return this;
@@ -82,7 +82,7 @@ export abstract class SheetComponent extends RenderComponent<SpreadsheetSkeleton
endColumn: number
): Nullable<{ startRow: number; startColumn: number; endRow: number; endColumn: number }>;
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<SpreadsheetSkeleton
}
export abstract class SpreadsheetHeader extends SheetComponent {
protected override _draw(ctx: UniverRenderingContext, bounds?: IViewportBound): void {
protected override _draw(ctx: UniverRenderingContext, bounds?: IViewportInfo): void {
this.draw(ctx, bounds);
}
}
@@ -74,7 +74,7 @@ import {
isRectIntersect,
mergeInfoOffset,
} from '../../basics/tools';
import type { IBoundRectNoAngle, IViewportBound } from '../../basics/vector2';
import type { IBoundRectNoAngle, IViewportInfo } from '../../basics/vector2';
import { columnIterator } from '../docs/layout/tools';
import { DocumentSkeleton } from '../docs/layout/doc-skeleton';
import { DocumentViewModel } from '../docs/view-model/document-view-model';
@@ -219,6 +219,9 @@ export class SpreadsheetSkeleton extends Skeleton {
private _rowHeaderWidth = 0;
private _columnHeaderHeight = 0;
/**
* skeletonData(row col range) of visible area
*/
private _rowColumnSegment: IRowColumnSegment = {
startRow: -1,
endRow: -1,
@@ -405,7 +408,7 @@ export class SpreadsheetSkeleton extends Skeleton {
this._marginTop = top;
}
calculateSegment(bounds?: IViewportBound) {
calculateSegment(bounds?: IViewportInfo) {
if (!this._worksheetData) {
return;
}
@@ -423,7 +426,7 @@ export class SpreadsheetSkeleton extends Skeleton {
return true;
}
calculateWithoutClearingCache(bounds?: IViewportBound) {
calculateWithoutClearingCache(bounds?: IViewportInfo) {
if (!this.calculateSegment(bounds)) {
return;
}
@@ -437,7 +440,7 @@ export class SpreadsheetSkeleton extends Skeleton {
return this;
}
calculate(bounds?: IViewportBound) {
calculate(bounds?: IViewportInfo) {
this._resetCache();
this.calculateWithoutClearingCache(bounds);
@@ -605,8 +608,9 @@ export class SpreadsheetSkeleton extends Skeleton {
return Math.max(rowHeader.width, widthByComputation);
}
getRowColumnSegment(bounds?: IViewportBound) {
return this._getBounding(this._rowHeightAccumulation, this._columnWidthAccumulation, bounds?.viewBound);
getRowColumnSegment(bounds?: IViewportInfo) {
return this._getBounding(this._rowHeightAccumulation, this._columnWidthAccumulation, bounds?.cacheBound);
// return this._getBounding(this._rowHeightAccumulation, this._columnWidthAccumulation, bounds?.viewBound);
}
/**
@@ -1378,8 +1382,8 @@ export class SpreadsheetSkeleton extends Skeleton {
const row_ed = searchArray(rowHeightAccumulation, Math.round(viewBound.bottom) - this.columnHeaderHeightAndMarginTop);
if (row_st === -1 && row_ed === 0) {
dataset_row_st = -1;
dataset_row_ed = -1;
dataset_row_st = 0;
dataset_row_ed = 0;
} else {
if (row_st === -1) {
dataset_row_st = 0;
@@ -1400,8 +1404,8 @@ export class SpreadsheetSkeleton extends Skeleton {
const col_ed = searchArray(columnWidthAccumulation, Math.round(viewBound.right) - this.rowHeaderWidthAndMarginLeft);
if (col_st === -1 && col_ed === 0) {
dataset_col_st = -1;
dataset_col_ed = -1;
dataset_col_st = 0;
dataset_col_ed = 0;
} else {
if (col_st === -1) {
dataset_col_st = 0;
@@ -1423,7 +1427,7 @@ export class SpreadsheetSkeleton extends Skeleton {
endRow: dataset_row_ed,
startColumn: dataset_col_st,
endColumn: dataset_col_ed,
};
} as IRange;
}
private _generateRowMatrixCache(
@@ -17,19 +17,16 @@
import type { IRange, ISelectionCellWithCoord, Nullable, ObjectMatrix } from '@univerjs/core';
import { BooleanNumber, sortRules } from '@univerjs/core';
import type { BaseObject } from '../../base-object';
import { FIX_ONE_PIXEL_BLUR_OFFSET, RENDER_CLASS_TYPE } from '../../basics/const';
// import { clearLineByBorderType } from '../../basics/draw';
import { getCellPositionByIndex, getColor } from '../../basics/tools';
import type { IViewportBound, Vector2 } from '../../basics/vector2';
import { Canvas } from '../../canvas';
import type { IBoundRectNoAngle, IViewportInfo, Vector2 } from '../../basics/vector2';
import type { Canvas } from '../../canvas';
import type { UniverRenderingContext } from '../../context';
import type { Engine } from '../../engine';
import type { Scene } from '../../scene';
import type { SceneViewer } from '../../scene-viewer';
import type { Viewport } from '../../viewport';
import { Documents } from '../docs/document';
import { SpreadsheetExtensionRegistry } from '../extension';
import type { Background } from './extensions/background';
@@ -39,6 +36,7 @@ import type { Font } from './extensions/font';
// import type { BorderCacheItem } from './interfaces';
import { SheetComponent } from './sheet-component';
import type { SpreadsheetSkeleton } from './sheet-skeleton';
import { type IPaintForRefresh, type IPaintForScrolling, SHEET_VIEWPORT_KEY } from './interfaces';
const OBJECT_KEY = '__SHEET_EXTENSION_FONT_DOCUMENT_INSTANCE__';
@@ -49,11 +47,9 @@ export class Spreadsheet extends SheetComponent {
private _fontExtension!: Font;
private _cacheCanvas!: Canvas;
private _refreshIncrementalState = false;
private _forceDirty = false;
private _dirtyBounds: IBoundRectNoAngle[] = [];
private _forceDisableGridlines = false;
@@ -70,20 +66,8 @@ export class Spreadsheet extends SheetComponent {
private _allowCache: boolean = true
) {
super(oKey, spreadsheetSkeleton);
if (this._allowCache) {
this._cacheCanvas = new Canvas();
this.onIsAddedToParentObserver.add((parent) => {
(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<IViewportInfo>;
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);
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]
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();
// 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;
}
}
+1 -1
View File
@@ -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);
}
/**
@@ -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;
+3 -3
View File
@@ -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;
}
}
+36 -21
View File
@@ -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);
}
+3 -3
View File
@@ -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 ||
+6 -8
View File
@@ -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) {
@@ -45,10 +45,12 @@ export class Picture extends Shape<IPictureProps> {
};
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();
@@ -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;
+2 -2
View File
@@ -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<T> 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;
File diff suppressed because it is too large Load Diff
+19
View File
@@ -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);
+2 -2
View File
@@ -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"
}
}
@@ -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<IStyleData>;
let getSheetRenderComponent: (unitId: string, viewKey: SHEET_VIEW_KEY) => Nullable<RenderComponentType>;
beforeEach(() => {
vi.stubGlobal('jest', vi);
});
beforeEach(() => {
const testBed = createFacadeTestBed();
get = testBed.get;
@@ -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);
});
@@ -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<Nullable<IHoverCellPosition>>();
@@ -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<IStyleData>;
beforeEach(() => {
vi.stubGlobal('jest', vi);
});
beforeEach(() => {
const testBed = createFacadeTestBed();
get = testBed.get;
@@ -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<IStyleData>;
beforeEach(() => {
vi.stubGlobal('jest', vi);
});
beforeEach(() => {
const testBed = createFacadeTestBed();
get = testBed.get;
+4 -18
View File
@@ -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,
+19 -1
View File
@@ -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]);
});
@@ -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<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
if (workbook) {
register(workbook.getUnitId());
}
}
_initSkeleton() {
@@ -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);
@@ -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);
+5 -5
View File
@@ -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 = {
@@ -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);
@@ -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());
}
}
@@ -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(
@@ -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<string, any>;
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<number, Record<number, object>>) {
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);
}
}
}
}
}
@@ -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 {
@@ -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)) {
@@ -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;
+126 -65
View File
@@ -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);
}
// 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,
});
const viewRowTop = new Viewport(SHEET_VIEWPORT_KEY.VIEW_ROW_TOP, scene, {
active: false,
isWheelPreventDefaultX: true,
isRelativeX: false,
isRelativeY: false,
});
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,
});
const viewColumnLeft = new Viewport(SHEET_VIEWPORT_KEY.VIEW_COLUMN_LEFT, scene, {
active: false,
isWheelPreventDefaultX: true,
isRelativeX: false,
isRelativeY: false,
});
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,
});
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,
});
return {
viewMain,
viewLeftTop,
viewMainLeftTop,
viewMainLeft,
viewMainTop,
viewColumnLeft,
viewRowTop,
viewRowBottom,
viewColumnRight,
};
}
/**
*
* 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 = new Viewport(VIEWPORT_KEY.VIEW_MAIN, scene, {
left: rowHeader.width,
top: columnHeader.height,
bottom: 0,
right: 0,
isWheelPreventDefaultX: true,
});
new Viewport(VIEWPORT_KEY.VIEW_ROW_TOP, scene, {
active: false,
isWheelPreventDefaultX: true,
});
new Viewport(VIEWPORT_KEY.VIEW_ROW_BOTTOM, scene, {
left: 0,
top: columnHeader.height,
bottom: 0,
width: rowHeader.width,
isWheelPreventDefaultX: true,
});
new Viewport(VIEWPORT_KEY.VIEW_COLUMN_LEFT, scene, {
active: false,
isWheelPreventDefaultX: true,
});
new Viewport(VIEWPORT_KEY.VIEW_COLUMN_RIGHT, scene, {
left: rowHeader.width,
top: 0,
height: columnHeader.height,
right: 0,
isWheelPreventDefaultX: true,
});
new Viewport(VIEWPORT_KEY.VIEW_LEFT_TOP, scene, {
left: 0,
top: 0,
width: rowHeader.width,
height: columnHeader.height,
isWheelPreventDefaultX: true,
});
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,
});
new Viewport(VIEWPORT_KEY.VIEW_MAIN_TOP, scene, {
isWheelPreventDefaultX: true,
active: false,
});
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;
}
}
@@ -115,6 +115,8 @@ export class DocsAdaptor extends ObjectAdaptor {
top: 0,
bottom: 0,
right: 0,
isRelativeX: true,
isRelativeY: true,
isWheelPreventDefaultX: true,
});
@@ -130,6 +130,8 @@ export class SlideAdaptor extends ObjectAdaptor {
top: 0,
bottom: 0,
right: 0,
isRelativeX: true,
isRelativeY: true,
});
viewMain.closeClip();
@@ -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,
});
@@ -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<SlideDataModel>(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();
+3 -13
View File
@@ -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