feat(sheets-drawing): add explicit anchor placements

This commit is contained in:
Univer
2026-07-28 12:38:46 +08:00
parent 22472c3652
commit 724da16e27
12 changed files with 1011 additions and 76 deletions
@@ -14,15 +14,21 @@
* limitations under the License.
*/
import type { IDrawingParam, Nullable } from '@univerjs/core';
import type { BaseObject } from '@univerjs/engine-render';
import type { ISheetDrawing } from '@univerjs/sheets-drawing';
/* eslint-disable import/consistent-type-specifier-style -- Keep type and value imports from one package in one declaration. */
import type { LocaleKey } from '../../locale/types';
import { ICommandService, LocaleService } from '@univerjs/core';
import { ICommandService, type IDrawingParam, LocaleService, type Nullable } from '@univerjs/core';
import { clsx, Radio, RadioGroup } from '@univerjs/design';
import { IDrawingManagerService } from '@univerjs/drawing';
import { IRenderManagerService } from '@univerjs/engine-render';
import { SetSheetDrawingCommand, SheetDrawingAnchorType } from '@univerjs/sheets-drawing';
import { type BaseObject, IRenderManagerService, type SpreadsheetSkeleton } from '@univerjs/engine-render';
import { SheetSkeletonService } from '@univerjs/sheets';
import {
getSheetDrawingPlacement,
type ISheetDrawing,
type ISheetDrawingPlacement,
SetSheetDrawingPlacementCommand,
SheetDrawingAnchorKind,
transformToDrawingPosition,
} from '@univerjs/sheets-drawing';
import { useDependency } from '@univerjs/ui';
import { useEffect, useState } from 'react';
@@ -35,17 +41,20 @@ export const SheetDrawingAnchor = (props: ISheetDrawingAnchorProps) => {
const localeService = useDependency(LocaleService);
const drawingManagerService = useDependency(IDrawingManagerService);
const renderManagerService = useDependency(IRenderManagerService);
const sheetSkeletonService = useDependency(SheetSkeletonService);
const { drawings } = props;
const drawingParam = drawings[0] as ISheetDrawing | undefined;
const drawingParam = isSheetDrawing(drawings[0]) ? drawings[0] : undefined;
const renderObject = drawingParam ? renderManagerService.getRenderUnitById(drawingParam.unitId) : undefined;
const scene = renderObject?.scene;
const transformer = scene?.getTransformerByCreate();
const [anchorShow, setAnchorShow] = useState(true);
const type = drawingParam?.anchorType ?? SheetDrawingAnchorType.Position;
const type = drawingParam
? getSheetDrawingPlacement(drawingParam).kind
: SheetDrawingAnchorKind.OneCell;
const [value, setValue] = useState(type);
function getUpdateParams(objects: Map<string, BaseObject>, drawingManagerService: IDrawingManagerService): Nullable<ISheetDrawing>[] {
@@ -60,7 +69,12 @@ export const SheetDrawingAnchor = (props: ISheetDrawingAnchorProps) => {
return true;
}
const { unitId, subUnitId, drawingId, drawingType, anchorType, sheetTransform, axisAlignSheetTransform } = searchParam as ISheetDrawing;
if (!isSheetDrawing(searchParam)) {
params.push(null);
return true;
}
const { unitId, subUnitId, drawingId, drawingType, anchorType, sheetTransform, axisAlignSheetTransform } = searchParam;
params.push({
unitId,
@@ -95,8 +109,10 @@ export const SheetDrawingAnchor = (props: ISheetDrawingAnchorProps) => {
setAnchorShow(false);
} else if (params.length >= 1) {
setAnchorShow(true);
const anchorType = params[0]?.anchorType || SheetDrawingAnchorType.Position;
setValue(anchorType);
const drawing = params[0];
setValue(drawing
? getSheetDrawingPlacement(drawing).kind
: SheetDrawingAnchorKind.OneCell);
}
});
@@ -111,26 +127,41 @@ export const SheetDrawingAnchor = (props: ISheetDrawingAnchorProps) => {
}
function handleChange(value: string | number | boolean) {
setValue((value as SheetDrawingAnchorType));
const focusDrawings = drawingManagerService.getFocusDrawings();
if (focusDrawings.length === 0) {
const kind = getAnchorKind(value);
if (!kind) {
return;
}
const updateParams = focusDrawings.map((drawing) => {
return {
unitId: drawing.unitId,
subUnitId: drawing.subUnitId,
drawingId: drawing.drawingId,
anchorType: value,
};
});
const focusDrawings = drawingManagerService.getFocusDrawings();
if (!focusDrawings.length || !focusDrawings.every(isSheetDrawing)) {
return;
}
commandService.executeCommand(SetSheetDrawingCommand.id, {
unitId: focusDrawings[0].unitId,
drawings: updateParams,
const { unitId, subUnitId } = focusDrawings[0];
const skeleton = kind === SheetDrawingAnchorKind.Absolute
? undefined
: sheetSkeletonService.ensureSkeleton(unitId, subUnitId);
if (kind !== SheetDrawingAnchorKind.Absolute && !skeleton) {
return;
}
const placementUpdates: Array<{ drawingId: string; placement: ISheetDrawingPlacement }> = [];
for (const drawing of focusDrawings) {
const placement = createPlacement(drawing, kind, skeleton);
if (!placement) {
return;
}
placementUpdates.push({ drawingId: drawing.drawingId, placement });
}
const changed = commandService.syncExecuteCommand(SetSheetDrawingPlacementCommand.id, {
unitId,
subUnitId,
drawings: placementUpdates,
});
if (changed) {
setValue(kind);
}
}
return (
@@ -150,11 +181,50 @@ export const SheetDrawingAnchor = (props: ISheetDrawingAnchorProps) => {
<div>
<RadioGroup value={value} onChange={handleChange} direction="vertical">
<Radio value={SheetDrawingAnchorType.Both}>{localeService.t<LocaleKey>('sheets-drawing-ui.drawing-anchor.both')}</Radio>
<Radio value={SheetDrawingAnchorType.Position}>{localeService.t<LocaleKey>('sheets-drawing-ui.drawing-anchor.position')}</Radio>
<Radio value={SheetDrawingAnchorType.None}>{localeService.t<LocaleKey>('sheets-drawing-ui.drawing-anchor.none')}</Radio>
<Radio value={SheetDrawingAnchorKind.TwoCell}>{localeService.t<LocaleKey>('sheets-drawing-ui.drawing-anchor.both')}</Radio>
<Radio value={SheetDrawingAnchorKind.OneCell}>{localeService.t<LocaleKey>('sheets-drawing-ui.drawing-anchor.position')}</Radio>
<Radio value={SheetDrawingAnchorKind.Absolute}>{localeService.t<LocaleKey>('sheets-drawing-ui.drawing-anchor.none')}</Radio>
</RadioGroup>
</div>
</div>
);
};
function isSheetDrawing(drawing: IDrawingParam | undefined): drawing is ISheetDrawing {
return Boolean(drawing && 'sheetTransform' in drawing && 'axisAlignSheetTransform' in drawing);
}
function getAnchorKind(value: string | number | boolean): SheetDrawingAnchorKind | null {
if (
value === SheetDrawingAnchorKind.OneCell ||
value === SheetDrawingAnchorKind.TwoCell ||
value === SheetDrawingAnchorKind.Absolute
) {
return value;
}
return null;
}
function createPlacement(
drawing: ISheetDrawing,
kind: SheetDrawingAnchorKind,
skeleton?: SpreadsheetSkeleton
): ISheetDrawingPlacement | null {
const transform = drawing.transform;
if (!transform) {
return null;
}
const { left = 0, top = 0, width = 0, height = 0 } = transform;
if (kind === SheetDrawingAnchorKind.Absolute) {
return { kind, left, top, width, height };
}
if (!skeleton) {
return null;
}
const { from, to } = transformToDrawingPosition(transform, skeleton);
return kind === SheetDrawingAnchorKind.OneCell
? { kind, from, width, height }
: { kind, from, to };
}
@@ -14,15 +14,15 @@
* limitations under the License.
*/
import type { ISheetDrawing } from '@univerjs/sheets-drawing';
import type { Root } from 'react-dom/client';
/* eslint-disable import/consistent-type-specifier-style -- Keep type and value imports from one package in one declaration. */
import { DrawingTypeEnum, ImageSourceType } from '@univerjs/core';
import { getDrawingShapeKeyByDrawingSearch, IDrawingManagerService } from '@univerjs/drawing';
import { IRenderManagerService } from '@univerjs/engine-render';
import { InsertSheetDrawingCommand, ISheetDrawingService, SheetDrawingAnchorType } from '@univerjs/sheets-drawing';
import { SheetSkeletonService } from '@univerjs/sheets';
import { InsertSheetDrawingCommand, type ISheetDrawing, ISheetDrawingService, SheetDrawingAnchorType, transformToDrawingPosition } from '@univerjs/sheets-drawing';
import { RediContext } from '@univerjs/ui';
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { createRoot, type Root } from 'react-dom/client';
import { Subject } from 'rxjs';
import { afterEach, describe, expect, it } from 'vitest';
import { createSheetsDrawingUiTestBed } from '../../../__tests__/create-sheets-drawing-ui-test-bed';
@@ -87,6 +87,12 @@ class TestRenderManagerService {
}
}
function createTestBed() {
return createSheetsDrawingUiTestBed(undefined, [
[IRenderManagerService, { useClass: TestRenderManagerService }],
]);
}
describe('SheetDrawingAnchor', () => {
let root: Root | undefined;
let container: HTMLDivElement | undefined;
@@ -101,9 +107,7 @@ describe('SheetDrawingAnchor', () => {
});
it('updates every focused sheet image when the anchor mode changes', async () => {
const testBed = createSheetsDrawingUiTestBed(undefined, [
[IRenderManagerService, { useClass: TestRenderManagerService as never }],
]);
const testBed = createTestBed();
const sheetDrawingService = testBed.get(ISheetDrawingService);
const drawingManagerService = testBed.get(IDrawingManagerService);
const drawings = [
@@ -156,10 +160,63 @@ describe('SheetDrawingAnchor', () => {
testBed.univer.dispose();
});
it('recomputes cell markers when Absolute changes to OneCell', async () => {
const testBed = createTestBed();
const sheetDrawingService = testBed.get(ISheetDrawingService);
const drawingManagerService = testBed.get(IDrawingManagerService);
const drawing = createSheetDrawing('absolute-drawing', SheetDrawingAnchorType.None);
drawing.transform = { ...drawing.transform, left: 320, top: 220 };
await testBed.commandService.executeCommand(InsertSheetDrawingCommand.id, {
unitId: testBed.unitId,
drawings: [drawing],
});
const skeleton = testBed.get(SheetSkeletonService).ensureSkeleton(testBed.unitId, testBed.subUnitId);
const transform = sheetDrawingService.getDrawingByParam({
unitId: drawing.unitId,
subUnitId: drawing.subUnitId,
drawingId: drawing.drawingId,
})?.transform;
if (!transform || !skeleton) {
throw new Error('Sheet drawing test fixture is incomplete.');
}
const expected = transformToDrawingPosition(transform, skeleton);
drawingManagerService.focusDrawing([{
unitId: drawing.unitId,
subUnitId: drawing.subUnitId,
drawingId: drawing.drawingId,
}]);
container = document.createElement('div');
document.body.appendChild(container);
const mountedRoot = createRoot(container);
root = mountedRoot;
await act(async () => {
mountedRoot.render(
<RediContext.Provider value={{ injector: testBed.injector }}>
<SheetDrawingAnchor drawings={[drawing]} />
</RediContext.Provider>
);
await Promise.resolve();
});
const oneCellOption = container.querySelectorAll<HTMLInputElement>('input[type="radio"]')[1];
await act(async () => {
oneCellOption.click();
await Promise.resolve();
});
const updated = sheetDrawingService.getDrawingByParam({
unitId: drawing.unitId,
subUnitId: drawing.subUnitId,
drawingId: drawing.drawingId,
});
expect(updated?.anchorType).toBe(SheetDrawingAnchorType.Position);
expect(updated?.sheetTransform.from).toEqual(expected.from);
testBed.univer.dispose();
});
it('hides anchor controls when the transformer clears the sheet image selection', async () => {
const testBed = createSheetsDrawingUiTestBed(undefined, [
[IRenderManagerService, { useClass: TestRenderManagerService as never }],
]);
const testBed = createTestBed();
const renderManagerService = testBed.get(IRenderManagerService) as unknown as TestRenderManagerService;
const drawings = [
createSheetDrawing('drawing-a', SheetDrawingAnchorType.Position),
@@ -191,9 +248,7 @@ describe('SheetDrawingAnchor', () => {
});
it('syncs the selected anchor mode when the transformer starts editing another sheet image', async () => {
const testBed = createSheetsDrawingUiTestBed(undefined, [
[IRenderManagerService, { useClass: TestRenderManagerService as never }],
]);
const testBed = createTestBed();
const renderManagerService = testBed.get(IRenderManagerService) as unknown as TestRenderManagerService;
const drawings = [
createSheetDrawing('drawing-a', SheetDrawingAnchorType.Both),
@@ -0,0 +1,88 @@
/**
* 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.
*/
/* eslint-disable import/consistent-type-specifier-style -- Keep type and value imports from one package in one declaration. */
import { CommandType, type ICommand, ICommandService, IUndoRedoService, sequenceExecute } from '@univerjs/core';
import { SheetSkeletonService } from '@univerjs/sheets';
import { applySheetDrawingPlacement, type ISheetDrawingPlacement, SheetDrawingAnchorKind } from '../../services/sheet-drawing-placement';
import { ISheetDrawingService } from '../../services/sheet-drawing.service';
import { DrawingApplyType, SetDrawingApplyMutation } from '../mutations/set-drawing-apply.mutation';
import { ClearSheetDrawingTransformerOperation } from '../operations/clear-drawing-transformer.operation';
export interface ISetSheetDrawingPlacementCommandParams {
unitId: string;
subUnitId: string;
drawings: Array<{
drawingId: string;
placement: ISheetDrawingPlacement;
}>;
}
export const SetSheetDrawingPlacementCommand: ICommand<ISetSheetDrawingPlacementCommandParams> = {
id: 'sheet.command.set-drawing-placement',
type: CommandType.COMMAND,
handler: (accessor, params) => {
if (!params?.drawings.length) {
return false;
}
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const drawingService = accessor.get(ISheetDrawingService);
const skeleton = params.drawings.every(({ placement }) => placement.kind === SheetDrawingAnchorKind.Absolute)
? undefined
: accessor.get(SheetSkeletonService).ensureSkeleton(params.unitId, params.subUnitId);
const updatedDrawings = [];
for (const { drawingId, placement } of params.drawings) {
const drawing = drawingService.getDrawingByParam({
unitId: params.unitId,
subUnitId: params.subUnitId,
drawingId,
});
if (!drawing) {
return false;
}
updatedDrawings.push(applySheetDrawingPlacement(drawing, placement, skeleton));
}
const drawingOp = drawingService.getBatchUpdateOp(updatedDrawings);
const { unitId, subUnitId, undo, redo, objects } = drawingOp;
const redoMutations = [
{
id: SetDrawingApplyMutation.id,
params: { unitId, subUnitId, op: redo, objects, type: DrawingApplyType.UPDATE },
},
{ id: ClearSheetDrawingTransformerOperation.id, params: [unitId] },
];
const undoMutations = [
{
id: SetDrawingApplyMutation.id,
params: { unitId, subUnitId, op: undo, objects, type: DrawingApplyType.UPDATE },
},
{ id: ClearSheetDrawingTransformerOperation.id, params: [unitId] },
];
const result = sequenceExecute(redoMutations, commandService);
if (!result.result) {
return false;
}
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations,
redoMutations,
});
return true;
},
};
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { ISheetDrawing } from '../../services/sheet-drawing.service';
/* eslint-disable import/consistent-type-specifier-style -- Keep type and value imports from one package in one declaration. */
import { Direction, DrawingTypeEnum, ImageSourceType, RANGE_TYPE, RedoCommandId, UndoCommandId } from '@univerjs/core';
import {
DeleteRangeMoveLeftCommand,
@@ -39,7 +39,14 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createSheetsDrawingTestBed } from '../../__tests__/create-sheets-drawing-test-bed';
import { drawingPositionToTransform } from '../../basics/transform-position';
import { InsertSheetDrawingCommand } from '../../commands/commands/insert-sheet-drawing.command';
import { ISheetDrawingService, SheetDrawingAnchorType } from '../../services/sheet-drawing.service';
import { SetSheetDrawingPlacementCommand } from '../../commands/commands/set-sheet-drawing-placement.command';
import {
applySheetDrawingPlacement,
getSheetDrawingPlacement,
type ISheetDrawingPlacement,
SheetDrawingAnchorKind,
} from '../../services/sheet-drawing-placement';
import { type ISheetDrawing, ISheetDrawingService, SheetDrawingAnchorType } from '../../services/sheet-drawing.service';
describe('sheet drawing transforms without UI plugins', () => {
let testBed: ReturnType<typeof createSheetsDrawingTestBed>;
@@ -98,6 +105,100 @@ describe('sheet drawing transforms without UI plugins', () => {
expect(getDrawing(service)).toEqual(after);
});
it.each([
['insert row', InsertRowCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
range: { startRow: 1, endRow: 1, startColumn: 0, endColumn: 19 },
direction: Direction.DOWN,
}],
['delete row', RemoveRowCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
range: { startRow: 1, endRow: 1, startColumn: 0, endColumn: 19 },
}],
['insert column', InsertColCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
range: { startRow: 0, endRow: 19, startColumn: 1, endColumn: 1 },
direction: Direction.RIGHT,
}],
['delete column', RemoveColCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
range: { startRow: 0, endRow: 19, startColumn: 1, endColumn: 1 },
}],
])('%s preserves the three placement contracts in headless mode', async (_name, commandId, params) => {
const skeleton = testBed.get(SheetSkeletonService).ensureSkeleton('test', 'sheet1')!;
const base = {
unitId: 'test',
subUnitId: 'sheet1',
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
imageSourceType: ImageSourceType.URL,
source: 'https://example.com/anchor.png',
sheetTransform: {
from: { row: 0, column: 0, rowOffset: 0, columnOffset: 0 },
to: { row: 0, column: 0, rowOffset: 1, columnOffset: 1 },
},
axisAlignSheetTransform: {
from: { row: 0, column: 0, rowOffset: 0, columnOffset: 0 },
to: { row: 0, column: 0, rowOffset: 1, columnOffset: 1 },
},
transform: { left: 0, top: 0, width: 1, height: 1 },
};
const placements: ISheetDrawingPlacement[] = [
{
kind: SheetDrawingAnchorKind.OneCell,
from: { row: 2, column: 2, rowOffset: 4, columnOffset: 6 },
width: 240,
height: 120,
},
{
kind: SheetDrawingAnchorKind.TwoCell,
from: { row: 2, column: 2, rowOffset: 4, columnOffset: 6 },
to: { row: 8, column: 6, rowOffset: 0, columnOffset: 0 },
},
{
kind: SheetDrawingAnchorKind.Absolute,
left: 640,
top: 96,
width: 240,
height: 120,
},
];
const drawings = placements.map((placement, index) => applySheetDrawingPlacement({
...base,
drawingId: `placement-${index}`,
}, placement, placement.kind === SheetDrawingAnchorKind.Absolute ? undefined : skeleton));
expect(await testBed.commandService.executeCommand(InsertSheetDrawingCommand.id, {
unitId: 'test',
drawings,
})).toBe(true);
const service = testBed.get(ISheetDrawingService);
const readPlacements = () => drawings.map((drawing) =>
getSheetDrawingPlacement(service.getDrawingByParam(drawing)!));
const before = readPlacements();
expect(await testBed.commandService.executeCommand(commandId, params)).toBe(true);
const after = readPlacements();
expect(after[0]).not.toEqual(before[0]);
expect(after[1]).not.toEqual(before[1]);
expect(after[2]).toEqual(before[2]);
if (
before[0].kind === SheetDrawingAnchorKind.OneCell &&
after[0].kind === SheetDrawingAnchorKind.OneCell
) {
expect(after[0].width).toBe(before[0].width);
expect(after[0].height).toBe(before[0].height);
}
expect(await testBed.commandService.executeCommand(UndoCommandId)).toBe(true);
expect(readPlacements()).toEqual(before);
expect(await testBed.commandService.executeCommand(RedoCommandId)).toBe(true);
expect(readPlacements()).toEqual(after);
});
it('resizes both-anchored drawings when row height changes', async () => {
const service = await insertDrawing();
const before = structuredClone(getDrawing(service));
@@ -117,6 +218,31 @@ describe('sheet drawing transforms without UI plugins', () => {
expect(getDrawing(service)).toEqual(after);
});
it('updates placement through a command and round-trips through undo and redo', async () => {
const service = await insertDrawing();
const before = structuredClone(getDrawing(service));
const placement: ISheetDrawingPlacement = {
kind: SheetDrawingAnchorKind.Absolute,
left: 640,
top: 96,
width: 240,
height: 120,
};
expect(await testBed.commandService.executeCommand(SetSheetDrawingPlacementCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
drawings: [{ drawingId: 'drawing-1', placement }],
})).toBe(true);
const after = structuredClone(getDrawing(service));
expect(getSheetDrawingPlacement(after)).toEqual(placement);
expect(await testBed.commandService.executeCommand(UndoCommandId)).toBe(true);
expect(getDrawing(service)).toEqual(before);
expect(await testBed.commandService.executeCommand(RedoCommandId)).toBe(true);
expect(getDrawing(service)).toEqual(after);
});
it.each([
['remove row', RemoveRowCommand.id, {
unitId: 'test',
@@ -14,20 +14,18 @@
* limitations under the License.
*/
import type { Workbook } from '@univerjs/core';
import type { IDrawingJsonUndo1, IDrawingSubunitMap } from '@univerjs/drawing';
import type { ICopySheetCommandInterceptorParams, IRemoveSheetCommandParams } from '@univerjs/sheets';
import type { ISheetDrawing } from '../services/sheet-drawing.service';
import { Disposable, ICommandService, Inject, IResourceManagerService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { getOrCreateDrawingCopyPlan, IDrawingManagerService } from '@univerjs/drawing';
import { CopySheetCommand, RemoveSheetCommand, SheetInterceptorService } from '@univerjs/sheets';
/* eslint-disable import/consistent-type-specifier-style -- Keep type and value imports from one package in one declaration. */
import { Disposable, ICommandService, Inject, IResourceManagerService, IUniverInstanceService, UniverInstanceType, type Workbook } from '@univerjs/core';
import { getOrCreateDrawingCopyPlan, type IDrawingJsonUndo1, IDrawingManagerService, type IDrawingSubunitMap } from '@univerjs/drawing';
import { CopySheetCommand, type ICopySheetCommandInterceptorParams, type IRemoveSheetCommandParams, RemoveSheetCommand, SheetInterceptorService } from '@univerjs/sheets';
import { InsertSheetDrawingCommand } from '../commands/commands/insert-sheet-drawing.command';
import { RemoveSheetDrawingCommand } from '../commands/commands/remove-sheet-drawing.command';
import { SetDrawingArrangeCommand } from '../commands/commands/set-drawing-arrange.command';
import { SetSheetDrawingPlacementCommand } from '../commands/commands/set-sheet-drawing-placement.command';
import { SetSheetDrawingCommand } from '../commands/commands/set-sheet-drawing.command';
import { DrawingApplyType, SetDrawingApplyMutation } from '../commands/mutations/set-drawing-apply.mutation';
import { ClearSheetDrawingTransformerOperation } from '../commands/operations/clear-drawing-transformer.operation';
import { ISheetDrawingService } from '../services/sheet-drawing.service';
import { type ISheetDrawing, ISheetDrawingService } from '../services/sheet-drawing.service';
export const SHEET_DRAWING_PLUGIN = 'SHEET_DRAWING_PLUGIN';
@@ -76,6 +74,7 @@ export class SheetsDrawingLoadController extends Disposable {
InsertSheetDrawingCommand,
RemoveSheetDrawingCommand,
SetDrawingArrangeCommand,
SetSheetDrawingPlacementCommand,
ClearSheetDrawingTransformerOperation,
].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command)));
}
@@ -14,23 +14,27 @@
* limitations under the License.
*/
import type { Injector, Univer, Workbook } from '@univerjs/core';
/* eslint-disable import/consistent-type-specifier-style -- Keep type and value imports from one package in one declaration. */
import type { FWorkbook } from '@univerjs/sheets/facade';
import type { ISheetDrawing } from '../../services/sheet-drawing.service';
import {
DrawingTypeEnum,
ICommandService,
ImageSourceType,
type Injector,
IUniverInstanceService,
RedoCommand,
UndoCommand,
type Univer,
UniverInstanceType,
type Workbook,
} from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SheetSkeletonService } from '@univerjs/sheets';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createSheetsDrawingTestBed } from '../../__tests__/create-sheets-drawing-test-bed';
import { InsertSheetDrawingCommand } from '../../commands/commands/insert-sheet-drawing.command';
import { resolveSheetDrawingRotateEnabled } from '../../common/rotate-enabled';
import { ISheetDrawingService } from '../../services/sheet-drawing.service';
import { type ISheetDrawingPlacement, SheetDrawingAnchorKind } from '../../services/sheet-drawing-placement';
import { type ISheetDrawing, ISheetDrawingService } from '../../services/sheet-drawing.service';
import { FWorksheetDrawingMixin } from '../f-worksheet';
describe('FWorksheetDrawingMixin group drawings', () => {
@@ -219,6 +223,46 @@ describe('FWorksheetDrawingMixin group drawings', () => {
expect(fWorksheet.getImages()).toEqual([]);
});
it('builds and round-trips an Absolute image placement without a Sheet skeleton', async () => {
const fWorksheet = createFacade(injector);
const ensureSkeleton = vi.spyOn(injector.get(SheetSkeletonService), 'ensureSkeleton')
.mockImplementation(() => {
throw new Error('SKELETON_MUST_NOT_BE_READ');
});
const placement: ISheetDrawingPlacement = {
kind: SheetDrawingAnchorKind.Absolute,
left: 640,
top: 96,
width: 320,
height: 180,
};
const image = await fWorksheet.newOverGridImage()
.setSource('https://example.com/absolute.png', ImageSourceType.URL)
.setPlacement(placement)
.buildAsync();
expect(ensureSkeleton).not.toHaveBeenCalled();
expect(image.transform).toEqual(expect.objectContaining({
left: placement.left,
top: placement.top,
width: placement.width,
height: placement.height,
}));
fWorksheet.insertImages([image]);
expect(fWorksheet.getDrawingPlacement(image.drawingId)).toEqual(placement);
expect(fWorksheet.getImageById(image.drawingId)?.getPlacement()).toEqual(placement);
const moved: ISheetDrawingPlacement = {
...placement,
left: 720,
top: 128,
};
expect(fWorksheet.setDrawingPlacement(image.drawingId, moved)).toBe(true);
expect(fWorksheet.getDrawingPlacement(image.drawingId)).toEqual(moved);
});
it('returns only images from sheet drawing data and exposes active images from the drawing selection', () => {
const commandService = injector.get(ICommandService);
const fWorksheet = createFacade(injector);
+4 -1
View File
@@ -16,7 +16,7 @@
import { DrawingTypeEnum } from '@univerjs/core';
import { FEnum } from '@univerjs/core/facade';
import { SheetDrawingAnchorType } from '@univerjs/sheets-drawing';
import { SheetDrawingAnchorKind, SheetDrawingAnchorType } from '@univerjs/sheets-drawing';
/**
* @ignore
@@ -27,12 +27,15 @@ export interface IFSheetsDrawingEnumMixin {
/** Please refer to {@link SheetDrawingAnchorType}. */
SheetDrawingAnchorType: typeof SheetDrawingAnchorType;
/** Please refer to {@link SheetDrawingAnchorKind}. */
SheetDrawingAnchorKind: typeof SheetDrawingAnchorKind;
}
export class FSheetsDrawingEnumMixin extends FEnum implements IFSheetsDrawingEnumMixin {
override get DrawingType(): typeof DrawingTypeEnum { return DrawingTypeEnum; };
override get SheetDrawingAnchorType(): typeof SheetDrawingAnchorType { return SheetDrawingAnchorType; };
override get SheetDrawingAnchorKind(): typeof SheetDrawingAnchorKind { return SheetDrawingAnchorKind; };
}
FEnum.extend(FSheetsDrawingEnumMixin);
@@ -14,10 +14,8 @@
* limitations under the License.
*/
import type { IRotationSkewFlipTransform, ISize } from '@univerjs/core';
/* eslint-disable import/consistent-type-specifier-style -- Keep type and value imports from one package in one declaration. */
import type { SpreadsheetSkeleton } from '@univerjs/engine-render';
import type { ICellOverGridPosition } from '@univerjs/sheets';
import type { ISheetImage, SheetDrawingAnchorType } from '@univerjs/sheets-drawing';
import {
ArrangeTypeEnum,
DrawingTypeEnum,
@@ -26,11 +24,26 @@ import {
ImageSourceType,
Inject,
Injector,
type IRotationSkewFlipTransform,
type ISize,
} from '@univerjs/core';
import { FBase } from '@univerjs/core/facade';
import { getImageSize } from '@univerjs/drawing';
import { convertPositionCellToSheetOverGrid, convertPositionSheetOverGridToAbsolute, SheetSkeletonService } from '@univerjs/sheets';
import { RemoveSheetDrawingCommand, SetDrawingArrangeCommand, SetSheetDrawingCommand, transformToAxisAlignPosition } from '@univerjs/sheets-drawing';
import { convertPositionCellToSheetOverGrid, convertPositionSheetOverGridToAbsolute, type ICellOverGridPosition, SheetSkeletonService } from '@univerjs/sheets';
import {
applySheetDrawingPlacement,
getSheetDrawingPlacement,
type ISheetDrawingPlacement,
ISheetDrawingService,
type ISheetImage,
RemoveSheetDrawingCommand,
SetDrawingArrangeCommand,
SetSheetDrawingCommand,
SetSheetDrawingPlacementCommand,
SheetDrawingAnchorKind,
type SheetDrawingAnchorType,
transformToAxisAlignPosition,
} from '@univerjs/sheets-drawing';
export interface IFOverGridImage extends Omit<ISheetImage, 'sheetTransform' | 'transform'>, ICellOverGridPosition, IRotationSkewFlipTransform, Required<ISize> {
@@ -124,6 +137,7 @@ function convertFOverGridImageToSheetImage(fOverGridImage: IFOverGridImage, shee
*/
export class FOverGridImageBuilder {
private _image: IFOverGridImage;
private _placement?: ISheetDrawingPlacement;
constructor(
unitId: string,
subUnitId: string,
@@ -501,6 +515,61 @@ export class FOverGridImageBuilder {
return this;
}
/**
* Set an explicit OneCell, TwoCell, or Absolute placement for the image.
*
* This placement takes precedence over the individual row, column, size,
* and anchor type builder fields.
* @param {ISheetDrawingPlacement} placement Image placement.
* @returns {FOverGridImageBuilder} This builder.
* @example
* ```ts
* const sheet = univerAPI.getActiveWorkbook().getActiveSheet();
* const image = await sheet.newOverGridImage()
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=96&v=4')
* .setPlacement({
* kind: univerAPI.Enum.SheetDrawingAnchorKind.OneCell,
* from: { row: 2, column: 2, rowOffset: 8, columnOffset: 8 },
* width: 240,
* height: 120,
* })
* .buildAsync();
* sheet.insertImages([image]);
* ```
* @example TwoCell
* ```ts
* const sheet = univerAPI.getActiveWorkbook().getActiveSheet();
* const image = await sheet.newOverGridImage()
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=96&v=4')
* .setPlacement({
* kind: univerAPI.Enum.SheetDrawingAnchorKind.TwoCell,
* from: { row: 2, column: 2, rowOffset: 8, columnOffset: 8 },
* to: { row: 8, column: 6, rowOffset: 0, columnOffset: 0 },
* })
* .buildAsync();
* sheet.insertImages([image]);
* ```
* @example Absolute
* ```ts
* const sheet = univerAPI.getActiveWorkbook().getActiveSheet();
* const image = await sheet.newOverGridImage()
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=96&v=4')
* .setPlacement({
* kind: univerAPI.Enum.SheetDrawingAnchorKind.Absolute,
* left: 640,
* top: 96,
* width: 240,
* height: 120,
* })
* .buildAsync();
* sheet.insertImages([image]);
* ```
*/
setPlacement(placement: ISheetDrawingPlacement): FOverGridImageBuilder {
this._placement = placement;
return this;
}
/**
* Set the cropping region of the image by defining the top edges, thereby displaying the specific part of the image you want.
* @param {number} top - The number of pixels to crop from the top of the image
@@ -654,7 +723,7 @@ export class FOverGridImageBuilder {
async buildAsync(): Promise<ISheetImage> {
const sheetSkeletonService = this._injector.get(SheetSkeletonService);
if (this._image.width === 0 || this._image.height === 0) {
if (!this._placement && (this._image.width === 0 || this._image.height === 0)) {
const size = await getImageSize(this._image.source);
const width = size.width;
const height = size.height;
@@ -668,7 +737,48 @@ export class FOverGridImageBuilder {
}
}
return convertFOverGridImageToSheetImage(this._image, sheetSkeletonService);
if (this._placement?.kind === SheetDrawingAnchorKind.Absolute) {
const { left, top, width, height } = this._placement;
const sheetTransform = {
from: {
column: 0,
columnOffset: left,
row: 0,
rowOffset: top,
},
to: {
column: 0,
columnOffset: left + width,
row: 0,
rowOffset: top + height,
},
};
const image: ISheetImage = {
...this._image,
transform: {
left,
top,
width,
height,
flipY: this._image.flipY,
flipX: this._image.flipX,
angle: this._image.angle,
skewX: this._image.skewX,
skewY: this._image.skewY,
},
sheetTransform,
axisAlignSheetTransform: sheetTransform,
};
return applySheetDrawingPlacement(image, this._placement);
}
const image = convertFOverGridImageToSheetImage(this._image, sheetSkeletonService);
if (!this._placement) {
return image;
}
const skeleton = sheetSkeletonService.ensureSkeleton(image.unitId, image.subUnitId);
return applySheetDrawingPlacement(image, this._placement, skeleton);
}
}
@@ -679,7 +789,8 @@ export class FOverGridImage extends FBase {
constructor(
private _image: ISheetImage,
@ICommandService protected readonly _commandService: ICommandService,
@Inject(Injector) protected readonly _injector: Injector
@Inject(Injector) protected readonly _injector: Injector,
@ISheetDrawingService private readonly _sheetDrawingService: ISheetDrawingService
) {
super();
}
@@ -720,6 +831,67 @@ export class FOverGridImage extends FBase {
return this._image.drawingType;
}
/**
* Get this image's explicit placement.
* @returns {ISheetDrawingPlacement} OneCell, TwoCell, or Absolute placement.
* @example
* ```ts
* const image = univerAPI.getActiveWorkbook().getActiveSheet().getImages()[0];
* console.log(image.getPlacement());
* ```
*/
getPlacement(): ISheetDrawingPlacement {
const current = this._sheetDrawingService.getDrawingByParam({
unitId: this._image.unitId,
subUnitId: this._image.subUnitId,
drawingId: this._image.drawingId,
});
return getSheetDrawingPlacement(current ?? this._image);
}
/**
* Set this image's explicit placement through the drawing command.
* @param {ISheetDrawingPlacement} placement OneCell, TwoCell, or Absolute placement.
* @returns {boolean} `true` when the command succeeds.
* @example OneCell
* ```ts
* const image = univerAPI.getActiveWorkbook().getActiveSheet().getImages()[0];
* image.setPlacement({
* kind: univerAPI.Enum.SheetDrawingAnchorKind.OneCell,
* from: { row: 4, column: 3, rowOffset: 8, columnOffset: 8 },
* width: 320,
* height: 180,
* });
* ```
* @example TwoCell
* ```ts
* const image = univerAPI.getActiveWorkbook().getActiveSheet().getImages()[0];
* image.setPlacement({
* kind: univerAPI.Enum.SheetDrawingAnchorKind.TwoCell,
* from: { row: 4, column: 3, rowOffset: 8, columnOffset: 8 },
* to: { row: 10, column: 8, rowOffset: 0, columnOffset: 0 },
* });
* ```
* @example Absolute
* ```ts
* const image = univerAPI.getActiveWorkbook().getActiveSheet().getImages()[0];
* image.setPlacement({
* kind: univerAPI.Enum.SheetDrawingAnchorKind.Absolute,
* left: 640,
* top: 96,
* width: 320,
* height: 180,
* });
* ```
*/
setPlacement(placement: ISheetDrawingPlacement): boolean {
return this._commandService.syncExecuteCommand(SetSheetDrawingPlacementCommand.id, {
unitId: this._image.unitId,
subUnitId: this._image.subUnitId,
drawings: [{ drawingId: this._image.drawingId, placement }],
});
}
/**
* Remove the image from the sheet
* @returns {boolean} true if the image is removed successfully, otherwise false
@@ -14,14 +14,12 @@
* limitations under the License.
*/
import type { IDrawingParam } from '@univerjs/core';
/* eslint-disable import/consistent-type-specifier-style -- Keep type and value imports from one package in one declaration. */
import type { IFBlobSource } from '@univerjs/core/facade';
import type { IDrawingGroupUpdateParam, IDrawingJsonUndo1 } from '@univerjs/drawing';
import type { ISheetDrawing, ISheetImage } from '@univerjs/sheets-drawing';
import { DrawingTypeEnum, generateRandomId, ImageSourceType, IUndoRedoService } from '@univerjs/core';
import { isGroupableDrawingType } from '@univerjs/drawing';
import { DrawingTypeEnum, generateRandomId, type IDrawingParam, ImageSourceType, IUndoRedoService } from '@univerjs/core';
import { type IDrawingGroupUpdateParam, type IDrawingJsonUndo1, isGroupableDrawingType } from '@univerjs/drawing';
import { getGroupState, transformObjectOutOfGroup } from '@univerjs/engine-render';
import { DrawingApplyType, InsertSheetDrawingCommand, ISheetDrawingService, RemoveSheetDrawingCommand, SetDrawingApplyMutation, SetSheetDrawingCommand } from '@univerjs/sheets-drawing';
import { DrawingApplyType, getSheetDrawingPlacement, InsertSheetDrawingCommand, type ISheetDrawing, type ISheetDrawingPlacement, ISheetDrawingService, type ISheetImage, RemoveSheetDrawingCommand, SetDrawingApplyMutation, SetSheetDrawingCommand, SetSheetDrawingPlacementCommand } from '@univerjs/sheets-drawing';
import { FWorksheet } from '@univerjs/sheets/facade';
import { FOverGridImage, FOverGridImageBuilder } from './f-over-grid-image';
@@ -185,6 +183,69 @@ export interface IFWorksheetDrawingMixin {
*/
updateImages(sheetImages: ISheetImage[]): FWorksheet;
/**
* Get the placement of any drawing on this sheet.
*
* Image, Shape, Chart, and Group use the same placement contract.
* @param {string} drawingId Drawing id.
* @returns {ISheetDrawingPlacement | null} The placement, or `null` when the drawing does not exist.
* @example
* ```ts
* const sheet = univerAPI.getActiveWorkbook().getActiveSheet();
* const placement = sheet.getDrawingPlacement('drawing-id');
* if (placement?.kind === univerAPI.Enum.SheetDrawingAnchorKind.TwoCell) {
* console.log(placement.from, placement.to);
* }
* ```
*/
getDrawingPlacement(drawingId: string): ISheetDrawingPlacement | null;
/**
* Set the placement of any drawing on this sheet through the drawing command.
*
* @param {string} drawingId Drawing id.
* @param {ISheetDrawingPlacement} placement Explicit OneCell, TwoCell, or Absolute placement.
* @returns {boolean} `true` when the command succeeds.
* @example OneCell: move with cells, keep pixel size
* ```ts
* const sheet = univerAPI.getActiveWorkbook().getActiveSheet();
* const drawingId = sheet.getImages()[0]?.getId();
* if (!drawingId) throw new Error('No drawing found.');
* const changed = sheet.setDrawingPlacement(drawingId, {
* kind: univerAPI.Enum.SheetDrawingAnchorKind.OneCell,
* from: { row: 2, column: 2, rowOffset: 8, columnOffset: 8 },
* width: 240,
* height: 120,
* });
* console.log(changed);
* ```
* @example TwoCell: move and resize with both cell markers
* ```ts
* const sheet = univerAPI.getActiveWorkbook().getActiveSheet();
* const drawingId = sheet.getImages()[0]?.getId();
* if (!drawingId) throw new Error('No drawing found.');
* sheet.setDrawingPlacement(drawingId, {
* kind: univerAPI.Enum.SheetDrawingAnchorKind.TwoCell,
* from: { row: 2, column: 2, rowOffset: 8, columnOffset: 8 },
* to: { row: 8, column: 6, rowOffset: 0, columnOffset: 0 },
* });
* ```
* @example Absolute: do not move or resize after row or column changes
* ```ts
* const sheet = univerAPI.getActiveWorkbook().getActiveSheet();
* const drawingId = sheet.getImages()[0]?.getId();
* if (!drawingId) throw new Error('No drawing found.');
* sheet.setDrawingPlacement(drawingId, {
* kind: univerAPI.Enum.SheetDrawingAnchorKind.Absolute,
* left: 640,
* top: 96,
* width: 240,
* height: 120,
* });
* ```
*/
setDrawingPlacement(drawingId: string, placement: ISheetDrawingPlacement): boolean;
/**
* Get the current selected images.
* @returns {FOverGridImage[]} The FOverGridImage instances
@@ -488,6 +549,23 @@ export class FWorksheetDrawingMixin extends FWorksheet implements IFWorksheetDra
return this;
}
override getDrawingPlacement(drawingId: string): ISheetDrawingPlacement | null {
const drawing = this._injector.get(ISheetDrawingService).getDrawingByParam({
unitId: this._fWorkbook.getId(),
subUnitId: this.getSheetId(),
drawingId,
});
return drawing ? getSheetDrawingPlacement(drawing) : null;
}
override setDrawingPlacement(drawingId: string, placement: ISheetDrawingPlacement): boolean {
return this._commandService.syncExecuteCommand(SetSheetDrawingPlacementCommand.id, {
unitId: this._fWorkbook.getId(),
subUnitId: this.getSheetId(),
drawings: [{ drawingId, placement }],
});
}
override newOverGridImage(): FOverGridImageBuilder {
const unitId = this._fWorkbook.getId();
const subUnitId = this.getSheetId();
+12
View File
@@ -25,6 +25,7 @@ export { RemoveSheetDrawingCommand } from './commands/commands/remove-sheet-draw
export type { IRemoveSheetDrawingCommandParam, IRemoveSheetDrawingCommandParams } from './commands/commands/remove-sheet-drawing.command';
export { SetDrawingArrangeCommand } from './commands/commands/set-drawing-arrange.command';
export type { ISetDrawingArrangeCommandParams } from './commands/commands/set-drawing-arrange.command';
export { SetSheetDrawingPlacementCommand } from './commands/commands/set-sheet-drawing-placement.command';
export { SetSheetDrawingCommand } from './commands/commands/set-sheet-drawing.command';
export type { ISetDrawingCommandParams } from './commands/commands/set-sheet-drawing.command';
export { DrawingApplyType, SetDrawingApplyMutation } from './commands/mutations/set-drawing-apply.mutation';
@@ -34,6 +35,17 @@ export { isKnownSheetNonRotatableDrawingType, resolveSheetDrawingRotateEnabled }
export type { IUniverSheetsDrawingConfig } from './config/config';
export { SHEET_DRAWING_PLUGIN } from './controllers/sheet-drawing.controller';
export { UniverSheetsDrawingPlugin } from './plugin';
export {
applySheetDrawingPlacement,
getSheetDrawingPlacement,
SheetDrawingAnchorKind,
} from './services/sheet-drawing-placement';
export type {
ISheetDrawingAbsolutePlacement,
ISheetDrawingOneCellPlacement,
ISheetDrawingPlacement,
ISheetDrawingTwoCellPlacement,
} from './services/sheet-drawing-placement';
export { SheetDrawingTransformPlanService } from './services/sheet-drawing-transform-plan.service';
export type {
ISheetDrawingTransformExtension,
@@ -0,0 +1,287 @@
/**
* 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.
*/
/* eslint-disable import/consistent-type-specifier-style -- Keep type and value imports from one package in one declaration. */
import type { ITransformState } from '@univerjs/core';
import type { SpreadsheetSkeleton } from '@univerjs/engine-render';
import { convertPositionCellToSheetOverGrid, convertPositionSheetOverGridToAbsolute, type ICellOverGridPosition } from '@univerjs/sheets';
import { transformToAxisAlignPosition } from '../basics/transform-position';
import {
type ISheetDrawing,
type ISheetDrawingPosition,
type ISheetFloatDom,
type ISheetImage,
SheetDrawingAnchorType,
} from './sheet-drawing.service';
/**
* Public Sheet drawing anchor semantics.
*
* The values intentionally match {@link SheetDrawingAnchorType} so snapshots
* keep a single persisted source of truth.
*/
export enum SheetDrawingAnchorKind {
// eslint-disable-next-line ts/prefer-literal-enum-member -- Keep the public enum aligned with the persisted model value.
OneCell = SheetDrawingAnchorType.Position,
// eslint-disable-next-line ts/prefer-literal-enum-member -- Keep the public enum aligned with the persisted model value.
TwoCell = SheetDrawingAnchorType.Both,
// eslint-disable-next-line ts/prefer-literal-enum-member -- Keep the public enum aligned with the persisted model value.
Absolute = SheetDrawingAnchorType.None,
}
/**
* Anchor a drawing to one cell while keeping a fixed pixel extent.
*/
export interface ISheetDrawingOneCellPlacement {
/** Placement discriminator. */
kind: SheetDrawingAnchorKind.OneCell;
/** Zero-based anchor cell and pixel offsets from its top-left corner. */
from: ICellOverGridPosition;
/** Drawing width in pixels. */
width: number;
/** Drawing height in pixels. */
height: number;
}
/**
* Anchor a drawing between two cell markers.
*/
export interface ISheetDrawingTwoCellPlacement {
/** Placement discriminator. */
kind: SheetDrawingAnchorKind.TwoCell;
/** Zero-based start cell and pixel offsets. */
from: ICellOverGridPosition;
/** Zero-based end cell and pixel offsets. */
to: ICellOverGridPosition;
}
/**
* Position a drawing in the Sheet canvas pixel coordinate system.
*/
export interface ISheetDrawingAbsolutePlacement {
/** Placement discriminator. */
kind: SheetDrawingAnchorKind.Absolute;
/** Horizontal pixel offset from the Sheet canvas origin. */
left: number;
/** Vertical pixel offset from the Sheet canvas origin. */
top: number;
/** Drawing width in pixels. */
width: number;
/** Drawing height in pixels. */
height: number;
}
/**
* Explicit Sheet drawing placement.
*/
export type ISheetDrawingPlacement =
| ISheetDrawingOneCellPlacement
| ISheetDrawingTwoCellPlacement
| ISheetDrawingAbsolutePlacement;
export function getSheetDrawingPlacement(drawing: ISheetDrawing): ISheetDrawingPlacement {
const anchorType = drawing.anchorType ?? SheetDrawingAnchorType.Position;
if (anchorType === SheetDrawingAnchorType.None) {
return {
kind: SheetDrawingAnchorKind.Absolute,
left: drawing.transform?.left ?? 0,
top: drawing.transform?.top ?? 0,
width: drawing.transform?.width ?? 0,
height: drawing.transform?.height ?? 0,
};
}
if (anchorType === SheetDrawingAnchorType.Both) {
return {
kind: SheetDrawingAnchorKind.TwoCell,
from: { ...drawing.sheetTransform.from },
to: { ...drawing.sheetTransform.to },
};
}
return {
kind: SheetDrawingAnchorKind.OneCell,
from: { ...drawing.sheetTransform.from },
width: drawing.transform?.width ?? 0,
height: drawing.transform?.height ?? 0,
};
}
export function applySheetDrawingPlacement(
drawing: ISheetImage,
placement: ISheetDrawingPlacement,
skeleton?: SpreadsheetSkeleton
): ISheetImage;
export function applySheetDrawingPlacement(
drawing: ISheetFloatDom,
placement: ISheetDrawingPlacement,
skeleton?: SpreadsheetSkeleton
): ISheetFloatDom;
export function applySheetDrawingPlacement(
drawing: ISheetDrawing,
placement: ISheetDrawingPlacement,
skeleton?: SpreadsheetSkeleton
): ISheetDrawing;
export function applySheetDrawingPlacement(
drawing: ISheetDrawing,
placement: ISheetDrawingPlacement,
skeleton?: SpreadsheetSkeleton
): ISheetDrawing {
validatePlacement(placement);
if (placement.kind === SheetDrawingAnchorKind.Absolute) {
const transform = withExistingTransform(drawing.transform, placement);
const sheetTransform = absoluteSheetTransform(placement, drawing.sheetTransform);
return {
...drawing,
anchorType: SheetDrawingAnchorType.None,
transform,
sheetTransform,
axisAlignSheetTransform: sheetTransform,
};
}
if (!skeleton) {
throw new Error('SHEET_DRAWING_PLACEMENT_SKELETON_REQUIRED');
}
if (placement.kind === SheetDrawingAnchorKind.OneCell) {
const converted = convertPositionCellToSheetOverGrid(
drawing.unitId,
drawing.subUnitId,
placement.from,
placement.width,
placement.height,
skeleton
);
const sheetTransform = withExistingSheetTransform(drawing.sheetTransform, converted.sheetTransform);
const transform = withExistingTransform(drawing.transform, converted.transform);
return {
...drawing,
anchorType: SheetDrawingAnchorType.Position,
sheetTransform,
transform,
axisAlignSheetTransform: transformToAxisAlignPosition(transform, skeleton),
};
}
const sheetTransform = withExistingSheetTransform(drawing.sheetTransform, {
from: placement.from,
to: placement.to,
});
const bounds = convertPositionSheetOverGridToAbsolute(
drawing.unitId,
drawing.subUnitId,
sheetTransform,
skeleton
);
const transform = withExistingTransform(drawing.transform, bounds);
return {
...drawing,
anchorType: SheetDrawingAnchorType.Both,
sheetTransform,
transform,
axisAlignSheetTransform: transformToAxisAlignPosition(transform, skeleton),
};
}
function withExistingSheetTransform(
current: ISheetDrawingPosition,
placement: Pick<ISheetDrawingPosition, 'from' | 'to'>
): ISheetDrawingPosition {
return {
...current,
from: { ...placement.from },
to: { ...placement.to },
};
}
function withExistingTransform(
current: ITransformState | null | undefined | void,
bounds: Pick<ITransformState, 'left' | 'top' | 'width' | 'height'>
): ITransformState {
return {
...current,
left: bounds.left,
top: bounds.top,
width: bounds.width,
height: bounds.height,
};
}
function absoluteSheetTransform(
placement: ISheetDrawingAbsolutePlacement,
current: ISheetDrawingPosition
): ISheetDrawingPosition {
return {
...current,
from: {
column: 0,
columnOffset: placement.left,
row: 0,
rowOffset: placement.top,
},
to: {
column: 0,
columnOffset: placement.left + placement.width,
row: 0,
rowOffset: placement.top + placement.height,
},
};
}
function validatePlacement(placement: ISheetDrawingPlacement): void {
if (placement.kind === SheetDrawingAnchorKind.OneCell) {
validateCellPosition(placement.from);
validateExtent(placement.width, placement.height);
return;
}
if (placement.kind === SheetDrawingAnchorKind.TwoCell) {
validateCellPosition(placement.from);
validateCellPosition(placement.to);
return;
}
if (placement.kind === SheetDrawingAnchorKind.Absolute) {
if (!Number.isFinite(placement.left) || !Number.isFinite(placement.top)) {
throw new TypeError('SHEET_DRAWING_PLACEMENT_POSITION_INVALID');
}
validateExtent(placement.width, placement.height);
return;
}
throw new Error('SHEET_DRAWING_PLACEMENT_KIND_INVALID');
}
function validateCellPosition(position: ICellOverGridPosition): void {
if (
!Number.isInteger(position.row) ||
!Number.isInteger(position.column) ||
position.row < 0 ||
position.column < 0 ||
!Number.isFinite(position.rowOffset) ||
!Number.isFinite(position.columnOffset)
) {
throw new Error('SHEET_DRAWING_PLACEMENT_CELL_INVALID');
}
}
function validateExtent(width: number, height: number): void {
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
throw new Error('SHEET_DRAWING_PLACEMENT_EXTENT_INVALID');
}
}
@@ -14,11 +14,10 @@
* limitations under the License.
*/
import type { IDrawingParam, IRotationSkewFlipTransform, Serializable } from '@univerjs/core';
import type { IImageData, IUnitDrawingService } from '@univerjs/drawing';
/* eslint-disable import/consistent-type-specifier-style -- Keep type and value imports from one package in one declaration. */
import type { ISheetOverGridPosition } from '@univerjs/sheets';
import { createIdentifier } from '@univerjs/core';
import { UnitDrawingService } from '@univerjs/drawing';
import { createIdentifier, type IDrawingParam, type IRotationSkewFlipTransform, type Serializable } from '@univerjs/core';
import { type IDrawingJsonUndo1, type IImageData, type IUnitDrawingService, UnitDrawingService } from '@univerjs/drawing';
export enum SheetDrawingAnchorType {
/**
@@ -81,6 +80,8 @@ export type ISheetUpdateDrawing = OptionalField<ISheetImage | ISheetShape, 'shee
export class SheetDrawingService extends UnitDrawingService<ISheetDrawing> { }
export interface ISheetDrawingService extends IUnitDrawingService<ISheetDrawing> { }
export interface ISheetDrawingService extends IUnitDrawingService<ISheetDrawing> {
getBatchUpdateOp(updateParams: ISheetDrawing[]): IDrawingJsonUndo1;
}
export const ISheetDrawingService = createIdentifier<ISheetDrawingService>('sheets-drawing.sheet-drawing.service');