Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .changeset/fresh-table-resize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plait/draw': patch
---

Adjust table cell edge resize behavior
143 changes: 90 additions & 53 deletions packages/draw/src/plugins/with-table-resize.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PlaitBoard, Point, RectangleClient, Transforms, isSelectedElement, getSelectedElements, hasValidAngle } from '@plait/core';
import { PlaitBaseTable, PlaitTableBoard, PlaitTableCellWithPoints } from '../interfaces/table';
import { PlaitBoard, Point, RectangleClient, Transforms, getSelectedElements, hasValidAngle } from '@plait/core';
import { PlaitBaseTable, PlaitTableBoard, PlaitTableCell, PlaitTableCellWithPoints } from '../interfaces/table';
import {
getIndexByResizeHandle,
isCornerHandle,
Expand All @@ -11,7 +11,7 @@ import {
WithResizeOptions,
normalizeShapePoints
} from '@plait/common';
import { getCellsWithPoints, updateColumns, updateRows } from '../utils/table';
import { getCellsWithPoints, getCurrentRowOrColumnSize, ResizeEdge, updateColumns, updateRows } from '../utils/table';
import { getHitRectangleResizeHandleRef } from '../utils/position/geometry';
import { getResizeOriginPointAndHandlePoint, getResizeZoom, movePointByZoomAndOriginPoint } from './with-draw-resize';
import { getSnapResizingRef, getSnapResizingRefOptions } from '../utils/snap-resizing';
Expand Down Expand Up @@ -41,25 +41,10 @@ export function withTableResize(board: PlaitTableBoard) {
const hitElement = selectedElements[0];
// debugGenerator.clear();
if (hitElement && PlaitDrawElement.isElementByTable(hitElement)) {
let rectangle = board.getRectangle(hitElement) as RectangleClient;
// debugGenerator.drawRectangle(board, rectangle);
// debugGenerator.drawCircles(board, [point], 5);
let handleRef = getHitRectangleResizeHandleRef(board, rectangle, point, hitElement.angle);
if (handleRef) {
const selectElement = isSelectedElement(board, hitElement);
if ((selectElement && isSingleSelectTable(board)) || (!selectElement && !isCornerHandle(board, handleRef.handle))) {
return {
element: hitElement,
handle: handleRef.handle,
cursorClass: handleRef.cursorClass,
rectangle
};
}
}
const cells = getCellsWithPoints(board, hitElement);
for (let i = 0; i < cells.length; i++) {
rectangle = RectangleClient.getRectangleByPoints(cells[i].points);
handleRef = getHitRectangleResizeHandleRef(board, rectangle, point, 0);
const rectangle = RectangleClient.getRectangleByPoints(cells[i].points);
const handleRef = getHitRectangleResizeHandleRef(board, rectangle, point, 0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A shared divider hits both adjacent cells, so this picks whichever cell comes first in cells. addSwimlaneRow/Column(index) can leave cells in a different order from the rows and columns, which means the same divider may resize the opposite side. Should the target be resolved from the row or column indexes instead?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good eye, thanks! I removed the cell-order-based target selection. Divider hits are now resolved from the row/column divider index: the outer start divider maps to the first row/column start, and internal/end dividers map to the previous row/column end. This keeps the same divider stable regardless of cell order after row/column insertion.

if (handleRef && !isCornerHandle(board, handleRef.handle)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At grid intersections this helper returns the cell corner first, then we discard it without checking the side handle. That makes the drag fall through instead of resizing the row or column. I can reproduce it on the swimlane edge: dragging there moves the whole swimlane. Could we handle side hits explicitly here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good eye, thanks! I updated the hit testing to resolve table dividers directly after preserving table corner handles. Grid intersections now choose the nearest divider axis, so side drags no longer fall through to whole swimlane/table movement.

return {
element: hitElement,
Expand All @@ -72,6 +57,19 @@ export function withTableResize(board: PlaitTableBoard) {
};
}
}

const rectangle = board.getRectangle(hitElement) as RectangleClient;
// debugGenerator.drawRectangle(board, rectangle);
// debugGenerator.drawCircles(board, [point], 5);
const handleRef = getHitRectangleResizeHandleRef(board, rectangle, point, hitElement.angle);
if (handleRef && isCornerHandle(board, handleRef.handle)) {
return {
element: hitElement,
handle: handleRef.handle,
cursorClass: handleRef.cursorClass,
rectangle
};
}
}
return null;
},
Expand All @@ -80,26 +78,47 @@ export function withTableResize(board: PlaitTableBoard) {
const path = PlaitBoard.findPath(board, resizeRef.element);
if (resizeRef.options?.cell && resizeRef.rectangle) {
const handleIndex = getIndexByResizeHandle(resizeRef.handle);
const { originPoint, handlePoint } = getResizeOriginPointAndHandlePoint(board, handleIndex, resizeRef.rectangle!);
const { originPoint, handlePoint } = getResizeOriginPointAndHandlePoint(board, handleIndex, resizeRef.rectangle);
const resizePoints: [Point, Point] = [resizeState.startPoint, resizeState.endPoint];
const { xZoom, yZoom } = getResizeZoom(resizePoints, originPoint, handlePoint, false, false);
const originPoints = resizeRef.options?.cell.points;
const originPoints = resizeRef.options.cell.points;
const targetPoints = originPoints.map((p) => {
return movePointByZoomAndOriginPoint(p, originPoint, xZoom, yZoom);
}) as [Point, Point];
const offsetX = targetPoints[1][0] - originPoints[1][0];
const offsetY = targetPoints[1][1] - originPoints[1][1];
const width = targetPoints[1][0] - targetPoints[0][0];
const height = targetPoints[1][1] - targetPoints[0][1];
if (offsetX !== 0 && width >= MIN_CELL_SIZE) {
const { columns, points } = updateColumns(resizeRef.element, resizeRef.options?.cell.columnId, width, offsetX);
Transforms.setNode(board, { columns, points }, path);
} else if (offsetY !== 0 && height >= MIN_CELL_SIZE) {
const { rows, points } = updateRows(resizeRef.element, resizeRef.options?.cell.rowId, height, offsetY);
Transforms.setNode(board, { rows, points }, path);

const originRect = RectangleClient.getRectangleByPoints(originPoints);
const targetRect = RectangleClient.getRectangleByPoints(targetPoints);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getRectangleByPoints normalizes reversed points. If the dragged edge crosses the fixed edge, the size becomes positive again, so the edge jumps to the other side and starts growing in reverse. Could we keep a signed delta here, or clamp at MIN_CELL_SIZE?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good eye, thanks! I replaced the normalized-rectangle delta with a signed pointer delta and clamp the target size at MIN_CELL_SIZE before applying the offset, so crossing the fixed edge no longer flips and grows in reverse.

const offsetWidth = targetRect.width - originRect.width;
const offsetHeight = targetRect.height - originRect.height;
const edge = getResizeEdge(handleIndex);
if (offsetWidth !== 0 && edge) {
const columnIndex = getResizeTargetRowOrColumnIndex(resizeRef.element, resizeRef.options.cell, edge, false);
const width = getCurrentRowOrColumnSize(resizeRef.element, columnIndex, false) + offsetWidth;
if (width >= MIN_CELL_SIZE) {
const { columns, points } = updateColumns(
resizeRef.element,
resizeRef.element.columns[columnIndex].id,
width,
offsetWidth,
edge
);
Transforms.setNode(board, { columns, points }, path);
}
} else if (offsetHeight !== 0 && edge) {
const rowIndex = getResizeTargetRowOrColumnIndex(resizeRef.element, resizeRef.options.cell, edge, true);
const height = getCurrentRowOrColumnSize(resizeRef.element, rowIndex, true) + offsetHeight;
if (height >= MIN_CELL_SIZE) {
const { rows, points } = updateRows(
resizeRef.element,
resizeRef.element.rows[rowIndex].id,
height,
offsetHeight,
edge
);
Transforms.setNode(board, { rows, points }, path);
}
}
} else {
const isFromCorner = isCornerHandle(board, resizeRef.handle);
} else if (isCornerHandle(board, resizeRef.handle)) {
const isAspectRatio = resizeState.isShift;
const handleIndex = getIndexByResizeHandle(resizeRef.handle);
const { originPoint, handlePoint } = getResizeOriginPointAndHandlePoint(board, handleIndex, resizeRef.rectangle!);
Expand All @@ -112,7 +131,7 @@ export function withTableResize(board: PlaitTableBoard) {
handlePoint
},
isAspectRatio,
isFromCorner
true
);
const resizeSnapRef = getSnapResizingRef(board, [resizeRef.element], resizeSnapRefOptions);
snapG = resizeSnapRef.snapG;
Expand All @@ -126,26 +145,10 @@ export function withTableResize(board: PlaitTableBoard) {
let columns = [...resizeRef.element.columns];
let rows = [...resizeRef.element.rows];
if (offsetWidth !== 0) {
columns = columns.map((item) => {
if (item.width) {
return {
...item,
width: item.width + offsetWidth * (item.width / originRect.width)
};
}
return item;
});
columns = scaleRowsOrColumns(columns, offsetWidth, originRect.width, false);
}
if (offsetHeight !== 0) {
rows = rows.map((item) => {
if (item.height) {
return {
...item,
height: item.height + offsetHeight * (item.height / originRect.height)
};
}
return item;
});
rows = scaleRowsOrColumns(rows, offsetHeight, originRect.height, true);
}
Transforms.setNode(board, { points: normalizeShapePoints(points), columns, rows }, path);
}
Expand All @@ -160,3 +163,37 @@ export function withTableResize(board: PlaitTableBoard) {

return board;
}

function scaleRowsOrColumns(data: { id: string; width?: number; height?: number }[], offset: number, originSize: number, isRow: boolean) {
const dimension = isRow ? 'height' : 'width';
return data.map((item) => {
if (item[dimension]) {
return {
...item,
[dimension]: item[dimension]! + offset * (item[dimension]! / originSize)
};
}
return item;
});
}

function getResizeEdge(handleIndex: number): ResizeEdge | undefined {
if ([Number(ResizeHandle.s), Number(ResizeHandle.e)].includes(handleIndex)) {
return 'end';
}
if ([Number(ResizeHandle.n), Number(ResizeHandle.w)].includes(handleIndex)) {
return 'start';
}
return undefined;
}

function getResizeTargetRowOrColumnIndex(element: PlaitBaseTable, resizeCell: PlaitTableCell, edge: ResizeEdge, isRow: boolean) {
const data = isRow ? element.rows : element.columns;
const id = isRow ? resizeCell.rowId : resizeCell.columnId;
const span = isRow ? resizeCell.rowspan : resizeCell.colspan;
let index = data.findIndex((item) => item.id === id);
if (edge === 'end' && span && span !== 1) {
index += span - 1;
}
return index;
}
23 changes: 19 additions & 4 deletions packages/draw/src/utils/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,30 @@ export function getTextManageByCell(board: PlaitBoard, cell: PlaitTableCell) {
return getTextManage(board, undefined, cell);
}

export const updateColumns = (table: PlaitBaseTable, columnId: string, width: number, offset: number) => {
export type ResizeEdge = 'start' | 'end';

export function getCurrentRowOrColumnSize(table: PlaitBaseTable, index: number, isRow: boolean) {
const rectangle = RectangleClient.getRectangleByPoints(table.points);
const data = isRow ? table.rows : table.columns;
const sizes = calculateCellsSize(data, isRow ? rectangle.height : rectangle.width, data.length, !isRow);
return sizes[index] || 0;
}

export const updateColumns = (table: PlaitBaseTable, columnId: string, width: number, offset: number, edge: ResizeEdge = 'end') => {
const columns = table.columns.map((item) => (item.id === columnId ? { ...item, width } : item));
const points = [table.points[0], [table.points[1][0] + offset, table.points[1][1]]] as Point[];
let points = [table.points[0], [table.points[1][0] + offset, table.points[1][1]]] as Point[];
if (edge === 'start') {
points = [[table.points[0][0] - offset, table.points[0][1]], table.points[1]] as Point[];
}
return { columns, points };
};

export const updateRows = (table: PlaitBaseTable, rowId: string, height: number, offset: number) => {
export const updateRows = (table: PlaitBaseTable, rowId: string, height: number, offset: number, edge: ResizeEdge = 'end') => {
const rows = table.rows.map((item) => (item.id === rowId ? { ...item, height } : item));
const points = [table.points[0], [table.points[1][0], table.points[1][1] + offset]] as Point[];
let points = [table.points[0], [table.points[1][0], table.points[1][1] + offset]] as Point[];
if (edge === 'start') {
points = [[table.points[0][0], table.points[0][1] - offset], table.points[1]] as Point[];
}
return { rows, points };
};

Expand Down
Loading