Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/core/src/shared/shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ export interface ISrcRect extends IOffset {
bottom?: number;
}

export interface IGroupBaseBound {
left: number;
top: number;
width: number;
height: number;
}

export interface IAbsoluteTransform extends ISize, IOffset, IScale {}

export interface IRectXYWH {
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/types/interfaces/i-drawing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import type { IAbsoluteTransform } from '../../shared/shape';
import type { IAbsoluteTransform, IGroupBaseBound } from '../../shared/shape';
import type { Nullable } from '../../shared/types';
import type { BooleanNumber } from '../enum/text-style';

Expand Down Expand Up @@ -124,6 +124,11 @@ export interface IDrawingParam extends IDrawingSearch {
isMultiTransform?: BooleanNumber;
groupId?: string;
allowTransform?: boolean;
/**
* The base bound of the group, used to calculate the relative position of children in the group.
* It is only used when drawingType is DRAWING_GROUP.
*/
groupBaseBound?: Nullable<IGroupBaseBound>;
}

// #endregion
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
*/

import type { IAccessor, ICommand } from '@univerjs/core';
import type { IDrawingGroupUpdateParam } from '@univerjs/drawing';
import {
CommandType,
ICommandService,
IUndoRedoService,
} from '@univerjs/core';
import type { IDrawingGroupUpdateParam, IDrawingJsonUndo1 } from '@univerjs/drawing';
import { IDocDrawingService } from '@univerjs/docs-drawing';

/**
Expand All @@ -36,6 +36,8 @@ export const GroupDocDrawingCommand: ICommand = {

if (!params) return false;

// if the subunit is not a doc type, return false

const unitIds: string[] = [];
params.forEach(({ parent, children }) => {
unitIds.push(parent.unitId);
Expand All @@ -45,9 +47,9 @@ export const GroupDocDrawingCommand: ICommand = {
});

// execute do mutations and add undo mutations to undo stack if completed
const jsonOp = docDrawingService.getGroupDrawingOp(params) as IDrawingJsonUndo1;
// const jsonOp = docDrawingService.getGroupDrawingOp(params) as IDrawingJsonUndo1;

const { unitId, subUnitId, undo, redo, objects } = jsonOp;
// const { unitId, subUnitId, undo, redo, objects } = jsonOp;

// const result = commandService.syncExecuteCommand(SetDocDrawingApplyMutation.id, { op: redo, unitId, subUnitId, objects, type: DocDrawingApplyType.GROUP });

Expand Down
10 changes: 8 additions & 2 deletions packages/docs-drawing-ui/src/commands/commands/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export function ungroupToGroup(ungroupParams: IDrawingGroupUpdateParam[]) {
drawingId,
transform: {
...transform,
left: transform.left! - groupTransform.left,
top: transform.top! - groupTransform.top,
// left: transform.left! - groupTransform.left,
// top: transform.top! - groupTransform.top,
},
groupId,
};
Expand All @@ -49,6 +49,12 @@ export function ungroupToGroup(ungroupParams: IDrawingGroupUpdateParam[]) {
drawingId: groupId,
drawingType: DrawingTypeEnum.DRAWING_GROUP,
transform: groupTransform,
groupBaseBound: {
left: groupTransform.left,
top: groupTransform.top,
width: groupTransform.width,
height: groupTransform.height,
},
} as IDrawingParam;

newGroupParams.push({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
toDisposable,
} from '@univerjs/core';
import { getDrawingShapeKeyByDrawingSearch, IDrawingManagerService, SetDrawingSelectedOperation } from '@univerjs/drawing';
import { DRAWING_OBJECT_LAYER_INDEX, Group, IRenderManagerService, RENDER_CLASS_TYPE } from '@univerjs/engine-render';
import { DRAWING_OBJECT_LAYER_INDEX, DrawingGroupObject, Group, IRenderManagerService, RENDER_CLASS_TYPE } from '@univerjs/engine-render';
import { AlignType, SetDrawingAlignOperation } from '../commands/operations/drawing-align.operation';
import { CloseImageCropOperation } from '../commands/operations/image-crop.operation';
import { getUpdateParams } from '../utils/get-update-params';
Expand Down Expand Up @@ -229,11 +229,14 @@ export class DrawingUpdateController extends Disposable {
}

const groupKey = getDrawingShapeKeyByDrawingSearch({ unitId, subUnitId, drawingId });
const group = new Group(groupKey);
const group = new DrawingGroupObject(groupKey);

scene.addObject(group, DRAWING_OBJECT_LAYER_INDEX).attachTransformerTo(group);

group.addObjects(...objects);
if (parent.groupBaseBound) {
group.setBaseBound(parent.groupBaseBound);
}
// group.reCalculateObjects();
parent.transform && group.transformByState({ left: parent.transform.left, top: parent.transform.top });

Expand Down
10 changes: 7 additions & 3 deletions packages/drawing-ui/src/controllers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type { IDrawingManagerService } from '@univerjs/drawing';
import type { BaseObject, Scene } from '@univerjs/engine-render';
import { UniverInstanceType } from '@univerjs/core';
import { getDrawingShapeKeyByDrawingSearch } from '@univerjs/drawing';
import { DRAWING_OBJECT_LAYER_INDEX, Group } from '@univerjs/engine-render';
import { DRAWING_OBJECT_LAYER_INDEX, DrawingGroupObject, Group } from '@univerjs/engine-render';

export function insertGroupObject(objectParam: IDrawingSearch, object: BaseObject, scene: Scene, drawingManagerService: IDrawingManagerService) {
const groupParam = drawingManagerService.getDrawingByParam(objectParam);
Expand All @@ -39,13 +39,17 @@ export function insertGroupObject(objectParam: IDrawingSearch, object: BaseObjec
return;
}

const group = new Group(groupKey);
const group = new DrawingGroupObject(groupKey);

scene.addObject(group, DRAWING_OBJECT_LAYER_INDEX).attachTransformerTo(group);

group.addObject(object);

const { transform } = groupParam;
const { transform, groupBaseBound } = groupParam;

if (groupBaseBound) {
group.setBaseBound(groupBaseBound);
}

transform && group.transformByState(
{
Expand Down
10 changes: 8 additions & 2 deletions packages/drawing-ui/src/views/panel/DrawingGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ export const DrawingGroup = (props: IDrawingGroupProps) => {
drawingId: groupId,
drawingType: DrawingTypeEnum.DRAWING_GROUP,
transform: groupTransform,
groupBaseBound: {
left: groupTransform.left,
top: groupTransform.top,
width: groupTransform.width,
height: groupTransform.height,
},
} as IDrawingParam;

const children = focusDrawings.map((drawing) => {
Expand All @@ -64,8 +70,8 @@ export const DrawingGroup = (props: IDrawingGroupProps) => {
drawingId,
transform: {
...transform,
left: transform.left! - groupTransform.left,
top: transform.top! - groupTransform.top,
// left: transform.left! - groupTransform.left,
// top: transform.top! - groupTransform.top,
},
groupId,
};
Expand Down
44 changes: 43 additions & 1 deletion packages/engine-render/src/base-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import type { IKeyValue, ITransformState, Nullable } from '@univerjs/core';
import type { IGroupBaseBound, IKeyValue, ITransformState, Nullable } from '@univerjs/core';
import type { IDragEvent, IMouseEvent, IPointerEvent, IWheelEvent } from './basics/i-events';

import type { IObjectFullState, ITransformChangeState } from './basics/interfaces';
Expand All @@ -25,6 +25,7 @@ import type { Engine } from './engine';
import type { Layer } from './layer';
import type { Scene } from './scene';
import { Disposable, EventSubject } from '@univerjs/core';
import { getRenderTransformBaseOnParentBound } from './basics';
import { CURSOR_TYPE, RENDER_CLASS_TYPE } from './basics/const';
import { TRANSFORM_CHANGE_OBSERVABLE_TYPE } from './basics/interfaces';
import { generateRandomKey, toPx } from './basics/tools';
Expand Down Expand Up @@ -58,6 +59,7 @@ export enum ObjectType {
export abstract class BaseObject extends Disposable {
groupKey?: string;
isInGroup: boolean = false;
isDrawingObject: boolean = false;

objectType: ObjectType = ObjectType.UNKNOWN;

Expand Down Expand Up @@ -603,6 +605,46 @@ export abstract class BaseObject extends Disposable {
};
}

getRealBound(): IGroupBaseBound {
let { width: realWidth, height: realHeight, left: realLeft, top: realTop } = this;
let baseBound;
if (this.isInGroup && this.parent?.classType === RENDER_CLASS_TYPE.GROUP && this.parent?.getBaseBound) {
baseBound = this.parent.getBaseBound();
}
if (baseBound) {
const parentState = this.getParent();
const parentBound = {
top: parentState.top || 0,
left: parentState.left,
width: parentState.width || 0,
height: parentState.height || 0,
};
const realBound = getRenderTransformBaseOnParentBound(baseBound, parentBound, { width: realWidth, height: realHeight, left: realLeft, top: realTop });

realWidth = realBound.width;
realHeight = realBound.height;
realLeft = realBound.left - parentBound.left - parentBound.width / 2;
realTop = realBound.top - parentBound.top - parentBound.height / 2;

// const isParentFlipX = this.parent?.flipX;
// const isParentFlipY = this.parent?.flipY;
// const parentCenterX = parentBound.left + parentBound.width / 2;
// const parentCenterY = parentBound.top + parentBound.height / 2;
// if (isParentFlipX) {
// realLeft = 2 * parentCenterX - realLeft;
// }
// if (isParentFlipY) {
// realTop = 2 * parentCenterY - realTop;
// }
}
return {
left: realLeft,
top: realTop,
width: realWidth,
height: realHeight,
};
}

hide() {
this._visible = false;
this._makeDirtyMix();
Expand Down
102 changes: 101 additions & 1 deletion packages/engine-render/src/basics/group-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import type { ITransformState } from '@univerjs/core';
import type { IGroupBaseBound, ITransformState } from '@univerjs/core';
import { offsetRotationAxis } from './offset-rotation-axis';
import { Vector2 } from './vector2';

Expand Down Expand Up @@ -46,6 +46,33 @@ export function getGroupState(parentLeft: number, parentTop: number, objectState
};
}

export function getDrawingGroupState(parentLeft: number, parentTop: number, objectStates: ITransformState[]) {
let groupLeft = Number.MAX_SAFE_INTEGER;
let groupTop = Number.MAX_SAFE_INTEGER;
let groupRight = Number.MIN_SAFE_INTEGER;
let groupBottom = Number.MIN_SAFE_INTEGER;

objectStates.forEach((o) => {
const { left = 0, top = 0, width = 0, height = 0 } = o;
groupLeft = Math.min(groupLeft, left);
groupTop = Math.min(groupTop, top);
groupRight = Math.max(groupRight, left + width);
groupBottom = Math.max(groupBottom, top + height);
});

const groupWidth = groupRight - groupLeft;
const groupHeight = groupBottom - groupTop;

return {
left: groupLeft,
top: groupTop,
width: groupWidth,
height: groupHeight,
angle: 0,
scaleX: 1,
scaleY: 1,
};
}
export function transformObjectOutOfGroup(child: ITransformState, parent: ITransformState, groupOriginWidth: number, groupOriginHeight: number) {
const { left = 0, top = 0, width = 0, height = 0, angle = 0 } = child;
const { left: groupLeft = 0, top: groupTop = 0, angle: groupAngle = 0 } = parent;
Expand All @@ -67,3 +94,76 @@ export function transformObjectOutOfGroup(child: ITransformState, parent: ITrans
angle: groupAngle + angle,
};
}

/**
* Get the rendered position and size of an object based on the group's baseBound and the parent's bound.
* @param baseBound The group's baseBound defining the coordinate space for its children,In Excel, this corresponds to chOff (child offset) and chExt (child extent) in OOXML.
* @param parentBound The bounding box of the parent context (e.g., the group or canvas) within which the object is rendered.
* @param objectBound The original bounding box of the object in the group's coordinate space.
* @returns {IGroupBaseBound} The transformed bound for rendering the object within the group context
*/
export function getRenderTransformBaseOnParentBound(baseBound: IGroupBaseBound, parentBound: IGroupBaseBound, objectBound: IGroupBaseBound): IGroupBaseBound {
if (!baseBound) {
return {
left: objectBound.left,
top: objectBound.top,
width: objectBound.width,
height: objectBound.height,
};
}
const { left, top, width, height } = baseBound;
const { left: parentLeft, top: parentTop, width: parentWidth, height: parentHeight } = parentBound;

const objectRelativeLeft = parentLeft + (objectBound.left - left) / width * parentWidth;
const objectRelativeTop = parentTop + (objectBound.top - top) / height * parentHeight;
const objectRelativeWidth = objectBound.width / width * parentWidth;
const objectRelativeHeight = objectBound.height / height * parentHeight;
return {
left: objectRelativeLeft,
top: objectRelativeTop,
width: objectRelativeWidth,
height: objectRelativeHeight,
};
}

/**
* In Excel, a rotated shape's bounding box for group calculations uses major axis switching.
* The axis-aligned bound depends on which 90° increment the rotation angle is closest to:
*
* [-45°, 45°) → 0° horizontal (original width/height)
* [45°, 135°) → 90° vertical (width/height swapped)
* [135°, 225°) → 180° horizontal (original width/height)
* [225°, 315°) → 270° vertical (width/height swapped)
*
* @param bound - The original axis-aligned bound { left, top, width, height }
* @param angle - The rotation angle in degrees
*/
export function getRotatedBoundInGroup(bound: IGroupBaseBound, angle: number): IGroupBaseBound {
const normalizedAngle = ((angle % 360) + 360) % 360;

const cx = bound.left + bound.width / 2;
const cy = bound.top + bound.height / 2;

const isHorizontalLike =
(normalizedAngle >= 315 || normalizedAngle < 45) ||
(normalizedAngle >= 135 && normalizedAngle < 225);

let width: number;
let height: number;

if (isHorizontalLike) {
width = bound.width;
height = bound.height;
} else {
// Major axis switch: swap width/height
width = bound.height;
height = bound.width;
}

return {
left: cx - width / 2,
top: cy - height / 2,
width,
height,
};
}
Loading