mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-13 16:16:46 +08:00
bbe88c57b7
Fix [BS-2294](https://linear.app/affine-design/issue/BS-2294/edgeless-%E7%BC%A9%E6%94%BE%E5%AD%97%E5%8F%B7%E6%A8%A1%E7%B3%8A)
231 lines
6.2 KiB
TypeScript
231 lines
6.2 KiB
TypeScript
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
|
import { Bound } from '@blocksuite/global/utils';
|
|
import { nothing } from 'lit';
|
|
|
|
import type { BlockService } from '../../extension/index.js';
|
|
import { GfxControllerIdentifier } from '../../gfx/identifiers.js';
|
|
import type { GfxBlockElementModel } from '../../gfx/index.js';
|
|
import { BlockComponent } from './block-component.js';
|
|
|
|
export function isGfxBlockComponent(
|
|
element: unknown
|
|
): element is GfxBlockComponent {
|
|
return (element as GfxBlockComponent)?.[GfxElementSymbol] === true;
|
|
}
|
|
|
|
export const GfxElementSymbol = Symbol('GfxElement');
|
|
|
|
function updateTransform(element: GfxBlockComponent) {
|
|
element.style.transformOrigin = '0 0';
|
|
element.style.transform = element.getCSSTransform();
|
|
}
|
|
|
|
function handleGfxConnection(instance: GfxBlockComponent) {
|
|
instance.style.position = 'absolute';
|
|
|
|
instance.disposables.add(
|
|
instance.gfx.viewport.viewportUpdated.on(() => {
|
|
updateTransform(instance);
|
|
})
|
|
);
|
|
|
|
instance.disposables.add(
|
|
instance.doc.slots.blockUpdated.on(({ type, id }) => {
|
|
if (id === instance.model.id && type === 'update') {
|
|
updateTransform(instance);
|
|
}
|
|
})
|
|
);
|
|
|
|
updateTransform(instance);
|
|
}
|
|
|
|
export abstract class GfxBlockComponent<
|
|
Model extends GfxBlockElementModel = GfxBlockElementModel,
|
|
Service extends BlockService = BlockService,
|
|
WidgetName extends string = string,
|
|
> extends BlockComponent<Model, Service, WidgetName> {
|
|
[GfxElementSymbol] = true;
|
|
|
|
get gfx() {
|
|
return this.std.get(GfxControllerIdentifier);
|
|
}
|
|
|
|
override connectedCallback(): void {
|
|
super.connectedCallback();
|
|
handleGfxConnection(this);
|
|
}
|
|
|
|
getCSSTransform() {
|
|
const viewport = this.gfx.viewport;
|
|
const { translateX, translateY, zoom } = viewport;
|
|
const bound = Bound.deserialize(this.model.xywh);
|
|
|
|
const scaledX = bound.x * zoom;
|
|
const scaledY = bound.y * zoom;
|
|
const deltaX = scaledX - bound.x;
|
|
const deltaY = scaledY - bound.y;
|
|
|
|
return `translate(${translateX + deltaX}px, ${translateY + deltaY}px) scale(${zoom})`;
|
|
}
|
|
|
|
getRenderingRect() {
|
|
const { xywh$ } = this.model;
|
|
|
|
if (!xywh$) {
|
|
throw new BlockSuiteError(
|
|
ErrorCode.GfxBlockElementError,
|
|
`Error on rendering '${this.model.flavour}': Gfx block's model should have 'xywh' property.`
|
|
);
|
|
}
|
|
|
|
const [x, y, w, h] = JSON.parse(xywh$.value);
|
|
|
|
return { x, y, w, h, zIndex: this.toZIndex() };
|
|
}
|
|
|
|
override renderBlock() {
|
|
const { x, y, w, h, zIndex } = this.getRenderingRect();
|
|
|
|
if (this.style.left !== `${x}px`) this.style.left = `${x}px`;
|
|
if (this.style.top !== `${y}px`) this.style.top = `${y}px`;
|
|
if (this.style.width !== `${w}px`) this.style.width = `${w}px`;
|
|
if (this.style.height !== `${h}px`) this.style.height = `${h}px`;
|
|
if (this.style.zIndex !== zIndex) this.style.zIndex = zIndex;
|
|
|
|
return this.renderGfxBlock();
|
|
}
|
|
|
|
renderGfxBlock(): unknown {
|
|
return nothing;
|
|
}
|
|
|
|
renderPageContent(): unknown {
|
|
return nothing;
|
|
}
|
|
|
|
override async scheduleUpdate() {
|
|
const parent = this.parentElement;
|
|
|
|
if (this.hasUpdated || !parent || !('scheduleUpdateChildren' in parent)) {
|
|
return super.scheduleUpdate();
|
|
} else {
|
|
await (parent.scheduleUpdateChildren as (id: string) => Promise<void>)(
|
|
this.model.id
|
|
);
|
|
|
|
return super.scheduleUpdate();
|
|
}
|
|
}
|
|
|
|
toZIndex(): string {
|
|
return this.gfx.layer.getZIndex(this.model).toString() ?? '0';
|
|
}
|
|
|
|
updateZIndex(): void {
|
|
this.style.zIndex = this.toZIndex();
|
|
}
|
|
}
|
|
|
|
export function toGfxBlockComponent<
|
|
Model extends GfxBlockElementModel,
|
|
Service extends BlockService,
|
|
WidgetName extends string,
|
|
B extends typeof BlockComponent<Model, Service, WidgetName>,
|
|
>(CustomBlock: B) {
|
|
// @ts-expect-error ignore
|
|
return class extends CustomBlock {
|
|
[GfxElementSymbol] = true;
|
|
|
|
get gfx() {
|
|
return this.std.get(GfxControllerIdentifier);
|
|
}
|
|
|
|
override connectedCallback(): void {
|
|
super.connectedCallback();
|
|
handleGfxConnection(this);
|
|
}
|
|
|
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
|
getCSSTransform() {
|
|
const viewport = this.gfx.viewport;
|
|
const { translateX, translateY, zoom } = viewport;
|
|
const bound = Bound.deserialize(this.model.xywh);
|
|
|
|
const scaledX = bound.x * zoom;
|
|
const scaledY = bound.y * zoom;
|
|
const deltaX = scaledX - bound.x;
|
|
const deltaY = scaledY - bound.y;
|
|
|
|
return `translate(${translateX + deltaX}px, ${translateY + deltaY}px) scale(${zoom})`;
|
|
}
|
|
|
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
|
getRenderingRect(): {
|
|
x: number;
|
|
y: number;
|
|
w: number | string;
|
|
h: number | string;
|
|
zIndex: string;
|
|
} {
|
|
const { xywh$ } = this.model;
|
|
|
|
if (!xywh$) {
|
|
throw new BlockSuiteError(
|
|
ErrorCode.GfxBlockElementError,
|
|
`Error on rendering '${this.model.flavour}': Gfx block's model should have 'xywh' property.`
|
|
);
|
|
}
|
|
|
|
const [x, y, w, h] = JSON.parse(xywh$.value);
|
|
|
|
return { x, y, w, h, zIndex: this.toZIndex() };
|
|
}
|
|
|
|
override renderBlock() {
|
|
const { x, y, w, h, zIndex } = this.getRenderingRect();
|
|
|
|
this.style.left = `${x}px`;
|
|
this.style.top = `${y}px`;
|
|
this.style.width = typeof w === 'number' ? `${w}px` : w;
|
|
this.style.height = typeof h === 'number' ? `${h}px` : h;
|
|
this.style.zIndex = zIndex;
|
|
|
|
return this.renderGfxBlock();
|
|
}
|
|
|
|
renderGfxBlock(): unknown {
|
|
return this.renderPageContent();
|
|
}
|
|
|
|
renderPageContent() {
|
|
return super.renderBlock();
|
|
}
|
|
|
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
|
override async scheduleUpdate() {
|
|
const parent = this.parentElement;
|
|
|
|
if (this.hasUpdated || !parent || !('scheduleUpdateChildren' in parent)) {
|
|
return super.scheduleUpdate();
|
|
} else {
|
|
await (parent.scheduleUpdateChildren as (id: string) => Promise<void>)(
|
|
this.model.id
|
|
);
|
|
|
|
return super.scheduleUpdate();
|
|
}
|
|
}
|
|
|
|
toZIndex(): string {
|
|
return this.gfx.layer.getZIndex(this.model).toString() ?? '0';
|
|
}
|
|
|
|
updateZIndex(): void {
|
|
this.style.zIndex = this.toZIndex();
|
|
}
|
|
} as B & {
|
|
new (...args: any[]): GfxBlockComponent;
|
|
};
|
|
}
|