feat(drawing): add group facade api (#6944)

This commit is contained in:
VicKun
2026-06-02 20:49:01 +08:00
committed by GitHub
parent cc421d1163
commit 4882ace135
2 changed files with 563 additions and 3 deletions
@@ -0,0 +1,198 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { Injector, Univer } from '@univerjs/core';
import type { ISheetDrawing } from '../../services/sheet-drawing.service';
import { DrawingTypeEnum, ICommandService, RedoCommand, UndoCommand } from '@univerjs/core';
import { InsertSheetDrawingCommand } from '../../commands/commands/insert-sheet-drawing.command';
import { ISheetDrawingService } from '../../services/sheet-drawing.service';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createSheetsDrawingTestBed } from '../../__tests__/create-sheets-drawing-test-bed';
import { FWorksheetDrawingMixin } from '../f-worksheet';
describe('FWorksheetDrawingMixin group drawings', () => {
let univer: Univer;
let injector: Injector;
beforeEach(() => {
const testBed = createSheetsDrawingTestBed();
univer = testBed.univer;
injector = testBed.injector;
});
afterEach(() => {
univer.dispose();
});
it('groups drawings and exposes parent and child relationships', () => {
const drawings = [
createDrawing('drawing-1', 10),
createDrawing('drawing-2', 80),
];
const commandService = injector.get(ICommandService);
const sheetDrawingService = injector.get(ISheetDrawingService);
const fWorksheet = createFacade(injector);
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings })).toBe(true);
const groupId = fWorksheet.groupDrawings(['drawing-1', 'drawing-2'], 'group-1');
expect(groupId).toBe('group-1');
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-1' })?.drawingType)
.toBe(DrawingTypeEnum.DRAWING_GROUP);
expect(fWorksheet.isDrawingGrouped('drawing-1')).toBe(true);
expect(fWorksheet.getDrawingParentGroup('drawing-1')?.drawingId).toBe('group-1');
expect(fWorksheet.getDrawingGroupChildren('group-1').map((drawing: ISheetDrawing) => drawing.drawingId))
.toEqual(['drawing-1', 'drawing-2']);
});
it('does not group unsupported drawing types', () => {
const drawings = [
createDrawing('drawing-1', 10),
createDrawing('chart-1', 80, DrawingTypeEnum.DRAWING_CHART),
];
const commandService = injector.get(ICommandService);
const fWorksheet = createFacade(injector);
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings })).toBe(true);
const groupId = fWorksheet.groupDrawings(['drawing-1', 'chart-1'], 'group-1');
expect(groupId).toBeNull();
expect(fWorksheet.isDrawingGrouped('drawing-1')).toBe(false);
expect(fWorksheet.isDrawingGrouped('chart-1')).toBe(false);
});
it('supports undo and redo for group and ungroup facade operations', async () => {
const drawings = [
createDrawing('drawing-1', 10),
createDrawing('drawing-2', 80),
];
const commandService = injector.get(ICommandService);
const sheetDrawingService = injector.get(ISheetDrawingService);
const fWorksheet = createFacade(injector);
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings })).toBe(true);
expect(fWorksheet.groupDrawings(['drawing-1', 'drawing-2'], 'group-1')).toBe('group-1');
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-1' })).toBeUndefined();
expect(fWorksheet.isDrawingGrouped('drawing-1')).toBe(false);
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-1' })?.drawingType)
.toBe(DrawingTypeEnum.DRAWING_GROUP);
expect(fWorksheet.isDrawingGrouped('drawing-1')).toBe(true);
expect(fWorksheet.ungroupDrawings(['group-1'])).toBe(true);
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-1' })).toBeUndefined();
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-1' })?.drawingType)
.toBe(DrawingTypeEnum.DRAWING_GROUP);
expect(fWorksheet.isDrawingGrouped('drawing-1')).toBe(true);
});
it('does not group with duplicate drawing ids or an existing group id', () => {
const drawings = [
createDrawing('drawing-1', 10),
createDrawing('drawing-2', 80),
];
const commandService = injector.get(ICommandService);
const fWorksheet = createFacade(injector);
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings })).toBe(true);
expect(fWorksheet.groupDrawings(['drawing-1', 'drawing-1'], 'group-1')).toBeNull();
expect(fWorksheet.groupDrawings(['drawing-1', 'drawing-2'], 'drawing-1')).toBeNull();
expect(fWorksheet.isDrawingGrouped('drawing-1')).toBe(false);
expect(fWorksheet.isDrawingGrouped('drawing-2')).toBe(false);
});
it('ungroups drawings and clears parent relationships', () => {
const drawings = [
createDrawing('drawing-1', 10),
createDrawing('drawing-2', 80),
];
const commandService = injector.get(ICommandService);
const sheetDrawingService = injector.get(ISheetDrawingService);
const fWorksheet = createFacade(injector);
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings })).toBe(true);
expect(fWorksheet.groupDrawings(['drawing-1', 'drawing-2'], 'group-1')).toBe('group-1');
expect(fWorksheet.ungroupDrawings(['group-1'])).toBe(true);
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-1' })).toBeUndefined();
expect(fWorksheet.isDrawingGrouped('drawing-1')).toBe(false);
expect(fWorksheet.isDrawingGrouped('drawing-2')).toBe(false);
});
it('returns nested group children when recursive is enabled', () => {
const drawings = [
createDrawing('drawing-1', 10),
createDrawing('drawing-2', 80),
createDrawing('drawing-3', 150),
];
const commandService = injector.get(ICommandService);
const fWorksheet = createFacade(injector);
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings })).toBe(true);
expect(fWorksheet.groupDrawings(['drawing-1', 'drawing-2'], 'group-1')).toBe('group-1');
expect(fWorksheet.groupDrawings(['group-1', 'drawing-3'], 'group-2')).toBe('group-2');
expect(fWorksheet.getDrawingGroupChildren('group-2').map((drawing: ISheetDrawing) => drawing.drawingId))
.toEqual(['drawing-3', 'group-1']);
const recursiveChildren = fWorksheet.getDrawingGroupChildren('group-2', true).map((drawing: ISheetDrawing) => drawing.drawingId);
expect(recursiveChildren).toHaveLength(4);
expect(recursiveChildren).toEqual(expect.arrayContaining(['drawing-1', 'drawing-2', 'drawing-3', 'group-1']));
});
});
function createFacade(injector: Injector): FWorksheetDrawingMixin {
const instance = Object.create(FWorksheetDrawingMixin.prototype) as any;
instance._commandService = injector.get(ICommandService);
instance._fWorkbook = { getId: () => 'test' };
instance._workbook = { getUnitId: () => 'test' };
instance._worksheet = { getSheetId: () => 'sheet1' };
instance._injector = injector;
return instance as FWorksheetDrawingMixin;
}
function createDrawing(drawingId: string, left: number, drawingType = DrawingTypeEnum.DRAWING_SHAPE): ISheetDrawing {
return {
unitId: 'test',
subUnitId: 'sheet1',
drawingId,
drawingType,
transform: {
left,
top: 20,
width: 40,
height: 30,
},
sheetTransform: {
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
to: { row: 3, column: 3, rowOffset: 0, columnOffset: 0 },
},
axisAlignSheetTransform: {
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
to: { row: 3, column: 3, rowOffset: 0, columnOffset: 0 },
},
};
}
@@ -15,12 +15,21 @@
*/
import type { IFBlobSource } from '@univerjs/core/facade';
import type { ISheetImage } from '@univerjs/sheets-drawing';
import { DrawingTypeEnum, ImageSourceType } from '@univerjs/core';
import { InsertSheetDrawingCommand, ISheetDrawingService, RemoveSheetDrawingCommand, SetSheetDrawingCommand } from '@univerjs/sheets-drawing';
import type { IDrawingParam } from '@univerjs/core';
import type { IDrawingGroupUpdateParam, IDrawingJsonUndo1 } from '@univerjs/drawing';
import type { ISheetDrawing, ISheetImage } from '@univerjs/sheets-drawing';
import { DrawingTypeEnum, generateRandomId, ImageSourceType, IUndoRedoService } from '@univerjs/core';
import { getGroupState, transformObjectOutOfGroup } from '@univerjs/engine-render';
import { DrawingApplyType, InsertSheetDrawingCommand, ISheetDrawingService, RemoveSheetDrawingCommand, SetDrawingApplyMutation, SetSheetDrawingCommand } from '@univerjs/sheets-drawing';
import { FWorksheet } from '@univerjs/sheets/facade';
import { FOverGridImage, FOverGridImageBuilder } from './f-over-grid-image';
const GROUPABLE_DRAWING_TYPES = new Set([
DrawingTypeEnum.DRAWING_IMAGE,
DrawingTypeEnum.DRAWING_SHAPE,
DrawingTypeEnum.DRAWING_GROUP,
]);
/**
* @ignore
*/
@@ -217,6 +226,166 @@ export interface IFWorksheetDrawingMixin {
* ```
*/
newOverGridImage(): FOverGridImageBuilder;
/**
* Group drawings on the current sheet.
* @param {string[]} drawingIds - The drawing ids to group. At least two drawings are required.
* @param {string} [groupId] - Optional group drawing id. If omitted, a new id will be generated.
* @returns {string | null} The group id if the operation succeeds, otherwise null.
* @example
* ```ts
* // Group two over-grid images on the active sheet.
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getActiveSheet();
* const image1 = await fWorksheet.newOverGridImage()
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
* .setColumn(1)
* .setRow(1)
* .setWidth(100)
* .setHeight(100)
* .buildAsync();
* const image2 = await fWorksheet.newOverGridImage()
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
* .setColumn(3)
* .setRow(1)
* .setWidth(100)
* .setHeight(100)
* .buildAsync();
* fWorksheet.insertImages([image1, image2]);
* const groupId = fWorksheet.groupDrawings([image1.drawingId, image2.drawingId]);
* console.log(groupId);
* ```
*/
groupDrawings(drawingIds: string[], groupId?: string): string | null;
/**
* Ungroup drawing groups on the current sheet.
* @param {string[]} groupIds - The group drawing ids to ungroup.
* @returns {boolean} true if the operation succeeds, otherwise false.
* @example
* ```ts
* // Group two images, then ungroup the generated group.
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getActiveSheet();
* const image1 = await fWorksheet.newOverGridImage()
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
* .setColumn(1)
* .setRow(1)
* .setWidth(100)
* .setHeight(100)
* .buildAsync();
* const image2 = await fWorksheet.newOverGridImage()
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
* .setColumn(3)
* .setRow(1)
* .setWidth(100)
* .setHeight(100)
* .buildAsync();
* fWorksheet.insertImages([image1, image2]);
* const groupId = fWorksheet.groupDrawings([image1.drawingId, image2.drawingId]);
* if (groupId) {
* const result = fWorksheet.ungroupDrawings([groupId]);
* console.log(result);
* }
* ```
*/
ungroupDrawings(groupIds: string[]): boolean;
/**
* Get children of a drawing group on the current sheet.
* @param {string} groupId - The group drawing id.
* @param {boolean} [recursive] - Whether to return all descendants.
* @returns {ISheetDrawing[]} The child drawings.
* @example
* ```ts
* // Get the direct children of a drawing group.
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getActiveSheet();
* const image1 = await fWorksheet.newOverGridImage()
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
* .setColumn(1)
* .setRow(1)
* .setWidth(100)
* .setHeight(100)
* .buildAsync();
* const image2 = await fWorksheet.newOverGridImage()
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
* .setColumn(3)
* .setRow(1)
* .setWidth(100)
* .setHeight(100)
* .buildAsync();
* fWorksheet.insertImages([image1, image2]);
* const groupId = fWorksheet.groupDrawings([image1.drawingId, image2.drawingId]);
* if (groupId) {
* const children = fWorksheet.getDrawingGroupChildren(groupId);
* console.log(children.map((drawing) => drawing.drawingId));
* }
* ```
*/
getDrawingGroupChildren(groupId: string, recursive?: boolean): ISheetDrawing[];
/**
* Get the parent group of a drawing on the current sheet.
* @param {string} drawingId - The child drawing id.
* @returns {ISheetDrawing | null} The parent group drawing, or null if the drawing is not grouped.
* @example
* ```ts
* // Get the parent group of a drawing.
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getActiveSheet();
* const image1 = await fWorksheet.newOverGridImage()
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
* .setColumn(1)
* .setRow(1)
* .setWidth(100)
* .setHeight(100)
* .buildAsync();
* const image2 = await fWorksheet.newOverGridImage()
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
* .setColumn(3)
* .setRow(1)
* .setWidth(100)
* .setHeight(100)
* .buildAsync();
* fWorksheet.insertImages([image1, image2]);
* fWorksheet.groupDrawings([image1.drawingId, image2.drawingId]);
* const parentGroup = fWorksheet.getDrawingParentGroup(image1.drawingId);
* console.log(parentGroup?.drawingId);
* ```
*/
getDrawingParentGroup(drawingId: string): ISheetDrawing | null;
/**
* Returns whether a drawing is inside a group on the current sheet.
* @param {string} drawingId - The drawing id.
* @returns {boolean} true if the drawing has a parent group.
* @example
* ```ts
* // Check whether a drawing is inside a group.
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getActiveSheet();
* const image1 = await fWorksheet.newOverGridImage()
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
* .setColumn(1)
* .setRow(1)
* .setWidth(100)
* .setHeight(100)
* .buildAsync();
* const image2 = await fWorksheet.newOverGridImage()
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
* .setColumn(3)
* .setRow(1)
* .setWidth(100)
* .setHeight(100)
* .buildAsync();
* fWorksheet.insertImages([image1, image2]);
* fWorksheet.groupDrawings([image1.drawingId, image2.drawingId]);
* const isGrouped = fWorksheet.isDrawingGrouped(image1.drawingId);
* console.log(isGrouped);
* ```
*/
isDrawingGrouped(drawingId: string): boolean;
}
export class FWorksheetDrawingMixin extends FWorksheet implements IFWorksheetDrawingMixin {
@@ -329,6 +498,199 @@ export class FWorksheetDrawingMixin extends FWorksheet implements IFWorksheetDra
const subUnitId = this.getSheetId();
return this._injector.createInstance(FOverGridImageBuilder, unitId, subUnitId);
}
override groupDrawings(drawingIds: string[], groupId = generateRandomId(10)): string | null {
const uniqueDrawingIds = Array.from(new Set(drawingIds));
if (uniqueDrawingIds.length < 2) return null;
const unitId = this._fWorkbook.getId();
const subUnitId = this.getSheetId();
const sheetDrawingService = this._injector.get(ISheetDrawingService);
if (sheetDrawingService.getDrawingByParam({ unitId, subUnitId, drawingId: groupId })) return null;
const drawings = uniqueDrawingIds.map((drawingId) => sheetDrawingService.getDrawingByParam({ unitId, subUnitId, drawingId }));
if (drawings.some((drawing) => !drawing)) return null;
if (drawings.some((drawing) => !GROUPABLE_DRAWING_TYPES.has(drawing!.drawingType))) return null;
const validDrawings = drawings as ISheetDrawing[];
const groupTransform = getGroupState(0, 0, validDrawings.map((drawing) => drawing.transform || {}));
const groupParam = {
unitId,
subUnitId,
drawingId: groupId,
drawingType: DrawingTypeEnum.DRAWING_GROUP,
transform: groupTransform,
groupBaseBound: {
left: groupTransform.left,
top: groupTransform.top,
width: groupTransform.width,
height: groupTransform.height,
},
} as IDrawingParam;
const children = validDrawings.map((drawing) => ({
...drawing,
groupId,
})) as IDrawingParam[];
const result = this._applyGroupDrawingOperation([{
parent: groupParam,
children,
}], DrawingApplyType.GROUP);
return result ? groupId : null;
}
override ungroupDrawings(groupIds: string[]): boolean {
const unitId = this._fWorkbook.getId();
const subUnitId = this.getSheetId();
const sheetDrawingService = this._injector.get(ISheetDrawingService);
const groupParams: IDrawingGroupUpdateParam[] = [];
for (const groupId of groupIds) {
const groupDrawing = sheetDrawingService.getDrawingByParam({ unitId, subUnitId, drawingId: groupId });
if (!groupDrawing || groupDrawing.drawingType !== DrawingTypeEnum.DRAWING_GROUP) continue;
const groupTransform = groupDrawing.transform || { width: 0, height: 0 };
const children = sheetDrawingService.getDrawingsByGroup({ unitId, subUnitId, drawingId: groupId })
.map((drawing) => {
const newTransform = transformObjectOutOfGroup(
drawing.transform || {},
groupTransform,
groupTransform.width || 0,
groupTransform.height || 0,
groupDrawing.groupBaseBound
);
return {
...drawing,
transform: {
...drawing.transform,
...newTransform,
},
groupId: undefined,
};
});
if (children.length > 0) {
groupParams.push({
parent: groupDrawing,
children,
});
}
}
return this._applyGroupDrawingOperation(groupParams, DrawingApplyType.UNGROUP);
}
override getDrawingGroupChildren(groupId: string, recursive = false): ISheetDrawing[] {
const unitId = this._fWorkbook.getId();
const subUnitId = this.getSheetId();
const sheetDrawingService = this._injector.get(ISheetDrawingService);
if (!recursive) {
return sheetDrawingService.getDrawingsByGroup({ unitId, subUnitId, drawingId: groupId }) as ISheetDrawing[];
}
const nested = sheetDrawingService.getDrawingsByGroupNested({ unitId, subUnitId, drawingId: groupId });
if (!nested) return [];
return [
...(nested.flatChildren || []),
...nested.groups.filter((group) => group.drawingId !== groupId),
] as ISheetDrawing[];
}
override getDrawingParentGroup(drawingId: string): ISheetDrawing | null {
const unitId = this._fWorkbook.getId();
const subUnitId = this.getSheetId();
const sheetDrawingService = this._injector.get(ISheetDrawingService);
const drawing = sheetDrawingService.getDrawingByParam({ unitId, subUnitId, drawingId });
if (!drawing?.groupId) return null;
const groupDrawing = sheetDrawingService.getDrawingByParam({ unitId, subUnitId, drawingId: drawing.groupId });
if (!groupDrawing || groupDrawing.drawingType !== DrawingTypeEnum.DRAWING_GROUP) return null;
return groupDrawing;
}
override isDrawingGrouped(drawingId: string): boolean {
return this.getDrawingParentGroup(drawingId) !== null;
}
private _applyGroupDrawingOperation(groupParams: IDrawingGroupUpdateParam[], type: DrawingApplyType.GROUP | DrawingApplyType.UNGROUP): boolean {
if (groupParams.length === 0) return false;
const sheetDrawingService = this._injector.get(ISheetDrawingService);
const commandService = this._commandService;
const undoRedoService = this._injector.get(IUndoRedoService);
const jsonOp = (type === DrawingApplyType.GROUP
? sheetDrawingService.getGroupDrawingOp(groupParams)
: sheetDrawingService.getUngroupDrawingOp(groupParams)) as IDrawingJsonUndo1;
const { unitId, subUnitId, undo, redo, objects } = jsonOp;
const result = commandService.syncExecuteCommand(SetDrawingApplyMutation.id, { op: redo, unitId, subUnitId, objects, type });
if (result) {
const inverseType = type === DrawingApplyType.GROUP ? DrawingApplyType.UNGROUP : DrawingApplyType.GROUP;
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: [
{ id: SetDrawingApplyMutation.id, params: { op: undo, unitId, subUnitId, objects: this._invertGroupOperationObjects(objects as IDrawingGroupUpdateParam[], type), type: inverseType } },
],
redoMutations: [
{ id: SetDrawingApplyMutation.id, params: { op: redo, unitId, subUnitId, objects, type } },
],
});
return true;
}
return false;
}
private _invertGroupOperationObjects(groupParams: IDrawingGroupUpdateParam[], type: DrawingApplyType.GROUP | DrawingApplyType.UNGROUP): IDrawingGroupUpdateParam[] {
return type === DrawingApplyType.GROUP
? groupParams.map((groupParam) => {
const { parent, children } = groupParam;
const groupTransform = parent.transform || { width: 0, height: 0 };
return {
parent: {
...parent,
transform: {
left: 0,
top: 0,
},
},
children: children.map((child) => ({
...child,
transform: transformObjectOutOfGroup(
child.transform || {},
groupTransform,
groupTransform.width || 0,
groupTransform.height || 0,
parent.groupBaseBound
),
groupId: undefined,
})),
};
})
: groupParams.map((groupParam) => {
const { parent, children } = groupParam;
const groupTransform = getGroupState(0, 0, children.map((child) => child.transform || {}));
return {
parent: {
...parent,
transform: groupTransform,
},
children: children.map((child) => ({
...child,
groupId: parent.drawingId,
})),
};
});
}
}
FWorksheet.extend(FWorksheetDrawingMixin);