Files
AFFiNE-Mirror/blocksuite/affine/model/src/blocks/frame/frame-model.ts
DarkSky 25227a09f7 feat: improve grouping perf in edgeless (#14442)
fix #14433 

#### PR Dependency Tree


* **PR #14442** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Level-of-detail thumbnails for large images.
  * Adaptive pacing for snapping, distribution and other alignment work.
  * RAF coalescer utility to batch high-frequency updates.
  * Operation timing utility to measure synchronous work.

* **Improvements**
* Batch group/ungroup reparenting that preserves element order and
selection.
  * Coalesced panning and drag updates to reduce jitter.
* Connector/group indexing for more reliable updates, deletions and
sync.
  * Throttled viewport refresh behavior.

* **Documentation**
  * Docs added for RAF coalescer and measureOperation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-15 03:17:22 +08:00

177 lines
4.3 KiB
TypeScript

import { Bound } from '@blocksuite/global/gfx';
import type {
GfxBlockElementModel,
GfxCompatibleProps,
GfxElementGeometry,
GfxGroupCompatibleInterface,
GfxModel,
PointTestOptions,
} from '@blocksuite/std/gfx';
import {
canSafeAddToContainer,
descendantElementsImpl,
generateKeyBetweenV2,
GfxCompatible,
gfxGroupCompatibleSymbol,
hasDescendantElementImpl,
} from '@blocksuite/std/gfx';
import {
BlockModel,
BlockSchemaExtension,
defineBlockSchema,
type Text,
} from '@blocksuite/store';
import { z } from 'zod';
import { type Color, ColorSchema, DefaultTheme } from '../../themes/index.js';
export type FrameBlockProps = {
title: Text;
background: Color;
childElementIds?: Record<string, boolean>;
presentationIndex?: string;
comments?: Record<string, boolean>;
} & GfxCompatibleProps;
export const FrameZodSchema = z
.object({
background: ColorSchema,
})
.default({
background: DefaultTheme.transparent,
});
export const FrameBlockSchema = defineBlockSchema({
flavour: 'affine:frame',
props: (internal): FrameBlockProps => ({
title: internal.Text(),
background: 'transparent',
xywh: `[0,0,100,100]`,
index: 'a0',
childElementIds: Object.create(null),
presentationIndex: generateKeyBetweenV2(null, null),
lockedBySelf: false,
comments: undefined,
}),
metadata: {
version: 1,
role: 'content',
parent: ['affine:surface'],
children: [],
},
toModel: () => {
return new FrameBlockModel();
},
});
export const FrameBlockSchemaExtension = BlockSchemaExtension(FrameBlockSchema);
export class FrameBlockModel
extends GfxCompatible<FrameBlockProps>(BlockModel)
implements GfxElementGeometry, GfxGroupCompatibleInterface
{
[gfxGroupCompatibleSymbol] = true as const;
get childElements() {
if (!this.surface) return [];
const elements: GfxModel[] = [];
for (const key of this.childIds) {
const element =
this.surface.getElementById(key) ||
(this.surface.store.getModelById(key) as GfxBlockElementModel);
element && elements.push(element);
}
return elements;
}
get childIds() {
return this.props.childElementIds
? Object.keys(this.props.childElementIds)
: [];
}
get descendantElements(): GfxModel[] {
return descendantElementsImpl(this);
}
addChild(element: GfxModel) {
if (!canSafeAddToContainer(this, element)) return;
this.store.transact(() => {
this.props.childElementIds = {
...this.props.childElementIds,
[element.id]: true,
};
});
}
addChildren(elements: GfxModel[]): void {
elements = [...new Set(elements)].filter(element =>
canSafeAddToContainer(this, element)
);
const newChildren: Record<string, boolean> = {};
for (const element of elements) {
const id = typeof element === 'string' ? element : element.id;
newChildren[id] = true;
}
this.store.transact(() => {
this.props.childElementIds = {
...this.props.childElementIds,
...newChildren,
};
});
}
override containsBound(bound: Bound): boolean {
return this.elementBound.contains(bound);
}
hasChild(element: GfxModel): boolean {
return this.props.childElementIds
? element.id in this.props.childElementIds
: false;
}
hasDescendant(element: GfxModel): boolean {
return hasDescendantElementImpl(this, element);
}
override includesPoint(x: number, y: number, _: PointTestOptions): boolean {
const bound = Bound.deserialize(this.xywh);
return bound.isPointInBound([x, y]);
}
override intersectsBound(selectedBound: Bound): boolean {
const bound = Bound.deserialize(this.xywh);
return (
bound.isIntersectWithBound(selectedBound) || selectedBound.contains(bound)
);
}
removeChild(element: GfxModel): void {
this.removeChildren([element]);
}
removeChildren(elements: GfxModel[]): void {
const childIds = [...new Set(elements.map(element => element.id))];
if (!this.props.childElementIds || childIds.length === 0) {
return;
}
this.store.transact(() => {
const childElementIds = this.props.childElementIds;
if (!childElementIds) return;
childIds.forEach(childId => {
delete childElementIds[childId];
});
});
}
}