mirror of
https://github.com/dream-num/univer.git
synced 2026-08-29 07:13:59 +08:00
feat(sheets-drawing): add explicit anchor placements
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
|
||||
+68
-13
@@ -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;
|
||||
},
|
||||
};
|
||||
+128
-2
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user