perf(editor): improve bounding box calc caching (#14668)
#### PR Dependency Tree * **PR #14668** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
@@ -162,10 +162,11 @@ export class AffineToolbarWidget extends WidgetComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setReferenceElementWithElements(gfx: GfxController, elements: GfxModel[]) {
|
setReferenceElementWithElements(gfx: GfxController, elements: GfxModel[]) {
|
||||||
|
const surfaceBounds = getCommonBoundWithRotation(elements);
|
||||||
|
|
||||||
const getBoundingClientRect = () => {
|
const getBoundingClientRect = () => {
|
||||||
const bounds = getCommonBoundWithRotation(elements);
|
|
||||||
const { x: offsetX, y: offsetY } = this.getBoundingClientRect();
|
const { x: offsetX, y: offsetY } = this.getBoundingClientRect();
|
||||||
const [x, y, w, h] = gfx.viewport.toViewBound(bounds).toXYWH();
|
const [x, y, w, h] = gfx.viewport.toViewBound(surfaceBounds).toXYWH();
|
||||||
const rect = new DOMRect(x + offsetX, y + offsetY, w, h);
|
const rect = new DOMRect(x + offsetX, y + offsetY, w, h);
|
||||||
return rect;
|
return rect;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -103,8 +103,9 @@ export abstract class GfxPrimitiveElementModel<
|
|||||||
}
|
}
|
||||||
|
|
||||||
get deserializedXYWH() {
|
get deserializedXYWH() {
|
||||||
if (!this._lastXYWH || this.xywh !== this._lastXYWH) {
|
|
||||||
const xywh = this.xywh;
|
const xywh = this.xywh;
|
||||||
|
|
||||||
|
if (!this._lastXYWH || xywh !== this._lastXYWH) {
|
||||||
this._local.set('deserializedXYWH', deserializeXYWH(xywh));
|
this._local.set('deserializedXYWH', deserializeXYWH(xywh));
|
||||||
this._lastXYWH = xywh;
|
this._lastXYWH = xywh;
|
||||||
}
|
}
|
||||||
@@ -386,6 +387,8 @@ export abstract class GfxGroupLikeElementModel<
|
|||||||
{
|
{
|
||||||
private _childIds: string[] = [];
|
private _childIds: string[] = [];
|
||||||
|
|
||||||
|
private _xywhDirty = true;
|
||||||
|
|
||||||
private readonly _mutex = createMutex();
|
private readonly _mutex = createMutex();
|
||||||
|
|
||||||
abstract children: Y.Map<any>;
|
abstract children: Y.Map<any>;
|
||||||
@@ -420,24 +423,9 @@ export abstract class GfxGroupLikeElementModel<
|
|||||||
|
|
||||||
get xywh() {
|
get xywh() {
|
||||||
this._mutex(() => {
|
this._mutex(() => {
|
||||||
const curXYWH =
|
if (this._xywhDirty || !this._local.has('xywh')) {
|
||||||
(this._local.get('xywh') as SerializedXYWH) ?? '[0,0,0,0]';
|
this._local.set('xywh', this._getXYWH().serialize());
|
||||||
const newXYWH = this._getXYWH().serialize();
|
this._xywhDirty = false;
|
||||||
|
|
||||||
if (curXYWH !== newXYWH || !this._local.has('xywh')) {
|
|
||||||
this._local.set('xywh', newXYWH);
|
|
||||||
|
|
||||||
if (curXYWH !== newXYWH) {
|
|
||||||
this._onChange({
|
|
||||||
props: {
|
|
||||||
xywh: newXYWH,
|
|
||||||
},
|
|
||||||
oldValues: {
|
|
||||||
xywh: curXYWH,
|
|
||||||
},
|
|
||||||
local: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -457,13 +445,39 @@ export abstract class GfxGroupLikeElementModel<
|
|||||||
bound = bound ? bound.unite(child.elementBound) : child.elementBound;
|
bound = bound ? bound.unite(child.elementBound) : child.elementBound;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (bound) {
|
return bound ?? new Bound(0, 0, 0, 0);
|
||||||
this._local.set('xywh', bound.serialize());
|
|
||||||
} else {
|
|
||||||
this._local.delete('xywh');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return bound ?? new Bound(0, 0, 0, 0);
|
invalidateXYWH() {
|
||||||
|
this._xywhDirty = true;
|
||||||
|
this._local.delete('deserializedXYWH');
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshXYWH(local: boolean) {
|
||||||
|
this._mutex(() => {
|
||||||
|
const oldXYWH =
|
||||||
|
(this._local.get('xywh') as SerializedXYWH) ?? '[0,0,0,0]';
|
||||||
|
const nextXYWH = this._getXYWH().serialize();
|
||||||
|
|
||||||
|
this._xywhDirty = false;
|
||||||
|
|
||||||
|
if (oldXYWH === nextXYWH && this._local.has('xywh')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._local.set('xywh', nextXYWH);
|
||||||
|
this._local.delete('deserializedXYWH');
|
||||||
|
|
||||||
|
this._onChange({
|
||||||
|
props: {
|
||||||
|
xywh: nextXYWH,
|
||||||
|
},
|
||||||
|
oldValues: {
|
||||||
|
xywh: oldXYWH,
|
||||||
|
},
|
||||||
|
local,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract addChild(element: GfxModel): void;
|
abstract addChild(element: GfxModel): void;
|
||||||
@@ -496,6 +510,7 @@ export abstract class GfxGroupLikeElementModel<
|
|||||||
setChildIds(value: string[], fromLocal: boolean) {
|
setChildIds(value: string[], fromLocal: boolean) {
|
||||||
const oldChildIds = this.childIds;
|
const oldChildIds = this.childIds;
|
||||||
this._childIds = value;
|
this._childIds = value;
|
||||||
|
this.invalidateXYWH();
|
||||||
|
|
||||||
this._onChange({
|
this._onChange({
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ export type MiddlewareCtx = {
|
|||||||
export type SurfaceMiddleware = (ctx: MiddlewareCtx) => void;
|
export type SurfaceMiddleware = (ctx: MiddlewareCtx) => void;
|
||||||
|
|
||||||
export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
||||||
|
private static readonly _groupBoundImpactKeys = new Set([
|
||||||
|
'xywh',
|
||||||
|
'rotate',
|
||||||
|
'hidden',
|
||||||
|
]);
|
||||||
|
|
||||||
protected _decoratorState = createDecoratorState();
|
protected _decoratorState = createDecoratorState();
|
||||||
|
|
||||||
protected _elementCtorMap: Record<
|
protected _elementCtorMap: Record<
|
||||||
@@ -308,6 +314,42 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
Object.keys(payload.props).forEach(key => {
|
Object.keys(payload.props).forEach(key => {
|
||||||
model.propsUpdated.next({ key });
|
model.propsUpdated.next({ key });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._refreshParentGroupBoundsForElement(model, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _refreshParentGroupBounds(id: string, local: boolean) {
|
||||||
|
const group = this.getGroup(id);
|
||||||
|
|
||||||
|
if (group instanceof GfxGroupLikeElementModel) {
|
||||||
|
group.refreshXYWH(local);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _refreshParentGroupBoundsForElement(
|
||||||
|
model: GfxPrimitiveElementModel,
|
||||||
|
payload: ElementUpdatedData
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
model instanceof GfxGroupLikeElementModel &&
|
||||||
|
('childIds' in payload.props || 'childIds' in payload.oldValues)
|
||||||
|
) {
|
||||||
|
model.refreshXYWH(payload.local);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const affectedKeys = new Set([
|
||||||
|
...Object.keys(payload.props),
|
||||||
|
...Object.keys(payload.oldValues),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
Array.from(affectedKeys).some(key =>
|
||||||
|
SurfaceBlockModel._groupBoundImpactKeys.has(key)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this._refreshParentGroupBounds(model.id, payload.local);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _initElementModels() {
|
private _initElementModels() {
|
||||||
@@ -458,6 +500,10 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (payload.model instanceof BlockModel) {
|
||||||
|
this._refreshParentGroupBounds(payload.id, payload.isLocal);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
if (isGfxGroupCompatibleModel(payload.model)) {
|
if (isGfxGroupCompatibleModel(payload.model)) {
|
||||||
@@ -482,6 +528,13 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
payload.props.key &&
|
||||||
|
SurfaceBlockModel._groupBoundImpactKeys.has(payload.props.key)
|
||||||
|
) {
|
||||||
|
this._refreshParentGroupBounds(payload.id, payload.isLocal);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
ConnectorElementModel,
|
ConnectorElementModel,
|
||||||
GroupElementModel,
|
GroupElementModel,
|
||||||
} from '@blocksuite/affine/model';
|
} from '@blocksuite/affine/model';
|
||||||
|
import { serializeXYWH } from '@blocksuite/global/gfx';
|
||||||
import { beforeEach, describe, expect, test } from 'vitest';
|
import { beforeEach, describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
import { wait } from '../utils/common.js';
|
import { wait } from '../utils/common.js';
|
||||||
@@ -138,6 +139,29 @@ describe('group', () => {
|
|||||||
|
|
||||||
expect(group.childIds).toEqual([id]);
|
expect(group.childIds).toEqual([id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('group xywh should update when child xywh changes', () => {
|
||||||
|
const shapeId = model.addElement({
|
||||||
|
type: 'shape',
|
||||||
|
xywh: serializeXYWH(0, 0, 100, 100),
|
||||||
|
});
|
||||||
|
const groupId = model.addElement({
|
||||||
|
type: 'group',
|
||||||
|
children: {
|
||||||
|
[shapeId]: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const group = model.getElementById(groupId) as GroupElementModel;
|
||||||
|
|
||||||
|
expect(group.xywh).toBe(serializeXYWH(0, 0, 100, 100));
|
||||||
|
|
||||||
|
model.updateElement(shapeId, {
|
||||||
|
xywh: serializeXYWH(50, 60, 100, 100),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(group.xywh).toBe(serializeXYWH(50, 60, 100, 100));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('connector', () => {
|
describe('connector', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user