mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-04 12:54:23 +01: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