From 6871efa098c7caa5273bcc75dbab237457736e09 Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Sun, 12 Jul 2026 22:37:01 +0800 Subject: [PATCH 1/2] fix(draw): adjust table cell edge resize --- .changeset/fresh-table-resize.md | 5 + .../draw/src/plugins/with-table-resize.ts | 143 +++++++++++------- packages/draw/src/utils/table.ts | 23 ++- 3 files changed, 114 insertions(+), 57 deletions(-) create mode 100644 .changeset/fresh-table-resize.md diff --git a/.changeset/fresh-table-resize.md b/.changeset/fresh-table-resize.md new file mode 100644 index 000000000..6dbf3acc9 --- /dev/null +++ b/.changeset/fresh-table-resize.md @@ -0,0 +1,5 @@ +--- +'@plait/draw': patch +--- + +Adjust table cell edge resize behavior diff --git a/packages/draw/src/plugins/with-table-resize.ts b/packages/draw/src/plugins/with-table-resize.ts index fa012a072..bdbefdcc5 100644 --- a/packages/draw/src/plugins/with-table-resize.ts +++ b/packages/draw/src/plugins/with-table-resize.ts @@ -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, @@ -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'; @@ -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); if (handleRef && !isCornerHandle(board, handleRef.handle)) { return { element: hitElement, @@ -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; }, @@ -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); + 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!); @@ -112,7 +131,7 @@ export function withTableResize(board: PlaitTableBoard) { handlePoint }, isAspectRatio, - isFromCorner + true ); const resizeSnapRef = getSnapResizingRef(board, [resizeRef.element], resizeSnapRefOptions); snapG = resizeSnapRef.snapG; @@ -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); } @@ -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; +} diff --git a/packages/draw/src/utils/table.ts b/packages/draw/src/utils/table.ts index ab1a84009..6f85c0698 100644 --- a/packages/draw/src/utils/table.ts +++ b/packages/draw/src/utils/table.ts @@ -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 }; }; From c7d6edd4e51fc8eea8726444a1b2c69ccd8fb1b2 Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Thu, 16 Jul 2026 00:06:33 +0800 Subject: [PATCH 2/2] fix(draw): stabilize table divider resize --- .../draw/src/plugins/with-table-resize.ts | 184 ++++++++++++------ 1 file changed, 128 insertions(+), 56 deletions(-) diff --git a/packages/draw/src/plugins/with-table-resize.ts b/packages/draw/src/plugins/with-table-resize.ts index bdbefdcc5..eb8cc2651 100644 --- a/packages/draw/src/plugins/with-table-resize.ts +++ b/packages/draw/src/plugins/with-table-resize.ts @@ -3,6 +3,8 @@ import { PlaitBaseTable, PlaitTableBoard, PlaitTableCell, PlaitTableCellWithPoin import { getIndexByResizeHandle, isCornerHandle, + RESIZE_HANDLE_DIAMETER, + getRectangleResizeHandleRefs, ResizeOptions, ResizeHandle, ResizeRef, @@ -12,8 +14,8 @@ import { normalizeShapePoints } from '@plait/common'; import { getCellsWithPoints, getCurrentRowOrColumnSize, ResizeEdge, updateColumns, updateRows } from '../utils/table'; -import { getHitRectangleResizeHandleRef } from '../utils/position/geometry'; -import { getResizeOriginPointAndHandlePoint, getResizeZoom, movePointByZoomAndOriginPoint } from './with-draw-resize'; +import { getHitRectangleResizeHandleRef, ResizeHandleRef } from '../utils/position/geometry'; +import { getResizeOriginPointAndHandlePoint } from './with-draw-resize'; import { getSnapResizingRef, getSnapResizingRefOptions } from '../utils/snap-resizing'; import { PlaitDrawElement } from '../interfaces'; import { isSingleSelectTable } from '../utils'; @@ -27,6 +29,14 @@ interface TableResizeOptions extends ResizeOptions { const MIN_CELL_SIZE = 20; +interface TableCellResizeHit { + cell: PlaitTableCellWithPoints; + rectangle: RectangleClient; + handleRef: ResizeHandleRef; + targetIndex: number; + distance: number; +} + export function withTableResize(board: PlaitTableBoard) { let snapG: SVGGElement | null; @@ -41,33 +51,53 @@ export function withTableResize(board: PlaitTableBoard) { const hitElement = selectedElements[0]; // debugGenerator.clear(); if (hitElement && PlaitDrawElement.isElementByTable(hitElement)) { + const tableRectangle = board.getRectangle(hitElement) as RectangleClient; + // debugGenerator.drawRectangle(board, tableRectangle); + // debugGenerator.drawCircles(board, [point], 5); + const tableHandleRef = getHitRectangleResizeHandleRef(board, tableRectangle, point, hitElement.angle); + if (tableHandleRef && isCornerHandle(board, tableHandleRef.handle)) { + return { + element: hitElement, + handle: tableHandleRef.handle, + cursorClass: tableHandleRef.cursorClass, + rectangle: tableRectangle + }; + } + const cells = getCellsWithPoints(board, hitElement); + const cellResizeHits: TableCellResizeHit[] = []; for (let i = 0; i < cells.length; i++) { const rectangle = RectangleClient.getRectangleByPoints(cells[i].points); - const handleRef = getHitRectangleResizeHandleRef(board, rectangle, point, 0); - if (handleRef && !isCornerHandle(board, handleRef.handle)) { - return { - element: hitElement, - handle: handleRef.handle, - cursorClass: handleRef.cursorClass, - rectangle, - options: { - cell: cells[i] + const handleRef = getCellResizeHandleRef(board, rectangle, point); + if (handleRef) { + const handleIndex = getIndexByResizeHandle(handleRef.handle); + const edge = getResizeEdge(handleIndex); + if (edge) { + const isRow = isRowResizeHandle(handleIndex); + const targetIndex = getResizeTargetRowOrColumnIndex(hitElement, cells[i], edge, isRow); + if (targetIndex >= 0) { + cellResizeHits.push({ + cell: cells[i], + rectangle, + handleRef, + targetIndex, + distance: getResizeHandleDistance(rectangle, handleIndex, point) + }); } - }; + } } } - 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)) { + const cellResizeHit = getStableCellResizeHit(cellResizeHits); + if (cellResizeHit) { return { element: hitElement, - handle: handleRef.handle, - cursorClass: handleRef.cursorClass, - rectangle + handle: cellResizeHit.handleRef.handle, + cursorClass: cellResizeHit.handleRef.cursorClass, + rectangle: cellResizeHit.rectangle, + options: { + cell: cellResizeHit.cell + } }; } } @@ -78,44 +108,40 @@ 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 resizePoints: [Point, Point] = [resizeState.startPoint, resizeState.endPoint]; - const { xZoom, yZoom } = getResizeZoom(resizePoints, originPoint, handlePoint, false, false); - const originPoints = resizeRef.options.cell.points; - const targetPoints = originPoints.map((p) => { - return movePointByZoomAndOriginPoint(p, originPoint, xZoom, yZoom); - }) as [Point, Point]; - - const originRect = RectangleClient.getRectangleByPoints(originPoints); - const targetRect = RectangleClient.getRectangleByPoints(targetPoints); - 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); + if (edge) { + const isRow = isRowResizeHandle(handleIndex); + const pointerOffset = isRow + ? resizeState.endPoint[1] - resizeState.startPoint[1] + : resizeState.endPoint[0] - resizeState.startPoint[0]; + const targetIndex = getResizeTargetRowOrColumnIndex(resizeRef.element, resizeRef.options.cell, edge, isRow); + if (targetIndex < 0) { + return; } - } 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); + const currentSize = getCurrentRowOrColumnSize(resizeRef.element, targetIndex, isRow); + const sizeOffset = edge === 'start' ? -pointerOffset : pointerOffset; + const targetSize = Math.max(MIN_CELL_SIZE, currentSize + sizeOffset); + const appliedOffset = targetSize - currentSize; + if (appliedOffset !== 0) { + if (isRow) { + const { rows, points } = updateRows( + resizeRef.element, + resizeRef.element.rows[targetIndex].id, + targetSize, + appliedOffset, + edge + ); + Transforms.setNode(board, { rows, points }, path); + } else { + const { columns, points } = updateColumns( + resizeRef.element, + resizeRef.element.columns[targetIndex].id, + targetSize, + appliedOffset, + edge + ); + Transforms.setNode(board, { columns, points }, path); + } } } } else if (isCornerHandle(board, resizeRef.handle)) { @@ -164,6 +190,52 @@ export function withTableResize(board: PlaitTableBoard) { return board; } +function getCellResizeHandleRef(board: PlaitBoard, rectangle: RectangleClient, point: Point) { + const handleRef = getHitRectangleResizeHandleRef(board, rectangle, point, 0); + if (handleRef && !isCornerHandle(board, handleRef.handle)) { + return handleRef; + } + return getHitRectangleSideResizeHandleRef(board, rectangle, point); +} + +function getHitRectangleSideResizeHandleRef(board: PlaitBoard, rectangle: RectangleClient, point: Point): ResizeHandleRef | undefined { + const resizeHandleRefs = getRectangleResizeHandleRefs(rectangle, RESIZE_HANDLE_DIAMETER); + return resizeHandleRefs.find((resizeHandleRef) => { + return ( + !isCornerHandle(board, resizeHandleRef.handle) && + RectangleClient.isHit(RectangleClient.getRectangleByPoints([point, point]), resizeHandleRef.rectangle) + ); + }); +} + +function getStableCellResizeHit(cellResizeHits: TableCellResizeHit[]) { + if (!cellResizeHits.length) { + return null; + } + return cellResizeHits.sort((previous, next) => { + return previous.distance - next.distance || previous.targetIndex - next.targetIndex; + })[0]; +} + +function isRowResizeHandle(handleIndex: number) { + return [Number(ResizeHandle.n), Number(ResizeHandle.s)].includes(handleIndex); +} + +function getResizeHandleDistance(rectangle: RectangleClient, handleIndex: number, point: Point) { + switch (handleIndex) { + case Number(ResizeHandle.n): + return Math.abs(point[1] - rectangle.y); + case Number(ResizeHandle.e): + return Math.abs(point[0] - (rectangle.x + rectangle.width)); + case Number(ResizeHandle.s): + return Math.abs(point[1] - (rectangle.y + rectangle.height)); + case Number(ResizeHandle.w): + return Math.abs(point[0] - rectangle.x); + default: + return 0; + } +} + function scaleRowsOrColumns(data: { id: string; width?: number; height?: number }[], offset: number, originSize: number, isRow: boolean) { const dimension = isRow ? 'height' : 'width'; return data.map((item) => {