mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-17 06:21:16 +02:00
feat: [cont.] support inserting multiple images (#9875)
* feat: support inserting multiple images * Initial * handleAppOnDrop, onImageToolbarButtonClick, pasteFromClipboard * Initial get history working * insertMultipleImages -> insertImages * Bug fixes, improvements * Remove redundant branch * Refactor addElementsFromMixedContentPaste * History, drag & drop bug fixes * Update snapshots * Remove redundant try-catch * Refactor pasteFromClipboard * Plain paste check in mermaid paste * Move comment * processClipboardData -> insertClipboardContent * Redundant variable * Redundant variable * Refactor insertImages * createImagePlaceholder -> newImagePlaceholder * Get rid of unneeded NEVER schedule, filter out failed images * Trigger CI * Position placeholders before initializing * Don't mutate scene with positionElementsOnGrid, captureUpdate: CaptureUpdateAction.IMMEDIATELY * Comment * Move positionOnGrid out of file * Rename file * Get rid of generic * Initial tests * More asserts, test paste * Test image tool * De-duplicate * Stricter assert, move rest of logic outside of waitFor * Modify history tests * De-duplicate update snapshots * Trigger CI * Fix package build * Make setupImageTest more explicit * Re-introduce generic to use latest placeholder versions * newElementWith instead of mutateElement to delete failed placeholder * Insert failed images separately with CaptureUpdateAction.NEVER * Refactor * Don't re-order elements * WIP * Get rid of 'never' for failed * refactor type check * align max file size constant * make grid padding scale to zoom --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
@@ -1126,7 +1126,9 @@ export interface BoundingBox {
|
||||
}
|
||||
|
||||
export const getCommonBoundingBox = (
|
||||
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
|
||||
elements:
|
||||
| readonly ExcalidrawElement[]
|
||||
| readonly NonDeleted<ExcalidrawElement>[],
|
||||
): BoundingBox => {
|
||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||
return {
|
||||
|
@@ -97,6 +97,7 @@ export * from "./image";
|
||||
export * from "./linearElementEditor";
|
||||
export * from "./mutateElement";
|
||||
export * from "./newElement";
|
||||
export * from "./positionElementsOnGrid";
|
||||
export * from "./renderElement";
|
||||
export * from "./resizeElements";
|
||||
export * from "./resizeTest";
|
||||
|
112
packages/element/src/positionElementsOnGrid.ts
Normal file
112
packages/element/src/positionElementsOnGrid.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { getCommonBounds } from "./bounds";
|
||||
import { type ElementUpdate, newElementWith } from "./mutateElement";
|
||||
|
||||
import type { ExcalidrawElement } from "./types";
|
||||
|
||||
// TODO rewrite (mostly vibe-coded)
|
||||
export const positionElementsOnGrid = <TElement extends ExcalidrawElement>(
|
||||
elements: TElement[] | TElement[][],
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
padding = 50,
|
||||
): TElement[] => {
|
||||
// Ensure there are elements to position
|
||||
if (!elements || elements.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const res: TElement[] = [];
|
||||
// Normalize input to work with atomic units (groups of elements)
|
||||
// If elements is a flat array, treat each element as its own atomic unit
|
||||
const atomicUnits: TElement[][] = Array.isArray(elements[0])
|
||||
? (elements as TElement[][])
|
||||
: (elements as TElement[]).map((element) => [element]);
|
||||
|
||||
// Determine the number of columns for atomic units
|
||||
// A common approach for a "grid-like" layout without specific column constraints
|
||||
// is to aim for a roughly square arrangement.
|
||||
const numUnits = atomicUnits.length;
|
||||
const numColumns = Math.max(1, Math.ceil(Math.sqrt(numUnits)));
|
||||
|
||||
// Group atomic units into rows based on the calculated number of columns
|
||||
const rows: TElement[][][] = [];
|
||||
for (let i = 0; i < numUnits; i += numColumns) {
|
||||
rows.push(atomicUnits.slice(i, i + numColumns));
|
||||
}
|
||||
|
||||
// Calculate properties for each row (total width, max height)
|
||||
// and the total actual height of all row content.
|
||||
let totalGridActualHeight = 0; // Sum of max heights of rows, without inter-row padding
|
||||
const rowProperties = rows.map((rowUnits) => {
|
||||
let rowWidth = 0;
|
||||
let maxUnitHeightInRow = 0;
|
||||
|
||||
const unitBounds = rowUnits.map((unit) => {
|
||||
const [minX, minY, maxX, maxY] = getCommonBounds(unit);
|
||||
return {
|
||||
elements: unit,
|
||||
bounds: [minX, minY, maxX, maxY] as const,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
};
|
||||
});
|
||||
|
||||
unitBounds.forEach((unitBound, index) => {
|
||||
rowWidth += unitBound.width;
|
||||
// Add padding between units in the same row, but not after the last one
|
||||
if (index < unitBounds.length - 1) {
|
||||
rowWidth += padding;
|
||||
}
|
||||
if (unitBound.height > maxUnitHeightInRow) {
|
||||
maxUnitHeightInRow = unitBound.height;
|
||||
}
|
||||
});
|
||||
|
||||
totalGridActualHeight += maxUnitHeightInRow;
|
||||
return {
|
||||
unitBounds,
|
||||
width: rowWidth,
|
||||
maxHeight: maxUnitHeightInRow,
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate the total height of the grid including padding between rows
|
||||
const totalGridHeightWithPadding =
|
||||
totalGridActualHeight + Math.max(0, rows.length - 1) * padding;
|
||||
|
||||
// Calculate the starting Y position to center the entire grid vertically around centerY
|
||||
let currentY = centerY - totalGridHeightWithPadding / 2;
|
||||
|
||||
// Position atomic units row by row
|
||||
rowProperties.forEach((rowProp) => {
|
||||
const { unitBounds, width: rowWidth, maxHeight: rowMaxHeight } = rowProp;
|
||||
|
||||
// Calculate the starting X for the current row to center it horizontally around centerX
|
||||
let currentX = centerX - rowWidth / 2;
|
||||
|
||||
unitBounds.forEach((unitBound) => {
|
||||
// Calculate the offset needed to position this atomic unit
|
||||
const [originalMinX, originalMinY] = unitBound.bounds;
|
||||
const offsetX = currentX - originalMinX;
|
||||
const offsetY = currentY - originalMinY;
|
||||
|
||||
// Apply the offset to all elements in this atomic unit
|
||||
unitBound.elements.forEach((element) => {
|
||||
res.push(
|
||||
newElementWith(element, {
|
||||
x: element.x + offsetX,
|
||||
y: element.y + offsetY,
|
||||
} as ElementUpdate<TElement>),
|
||||
);
|
||||
});
|
||||
|
||||
// Move X for the next unit in the row
|
||||
currentX += unitBound.width + padding;
|
||||
});
|
||||
|
||||
// Move Y to the starting position for the next row
|
||||
// This accounts for the tallest unit in the current row and the inter-row padding
|
||||
currentY += rowMaxHeight + padding;
|
||||
});
|
||||
return res;
|
||||
};
|
Reference in New Issue
Block a user