panelgrid 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,6 +11,7 @@ A flexible and performant React grid layout library with drag-and-drop and resiz
11
11
  - 🔧 **TypeScript**: Full type safety with comprehensive type definitions
12
12
  - 📦 **Tree-shakeable**: ESM and CommonJS builds available
13
13
  - 🎛️ **Customizable Rearrangement**: Override default collision resolution logic
14
+ - ⚛️ **React Server Components**: Full support for Next.js App Router and RSC
14
15
 
15
16
  ## Documentation & Demo
16
17
 
@@ -60,6 +61,8 @@ const initialPanels: PanelCoordinate[] = [
60
61
  { id: 3, x: 0, y: 2, w: 1, h: 1 },
61
62
  ];
62
63
 
64
+ // Mark panel component with "use client" for Next.js App Router
65
+ "use client";
63
66
  function PanelContent({ id }: { id: number | string }) {
64
67
  return <div>Panel {id}</div>;
65
68
  }
@@ -71,7 +74,7 @@ function App() {
71
74
  columnCount={4}
72
75
  gap={8}
73
76
  >
74
- <PanelGridRenderer itemRenderer={PanelContent} />
77
+ <PanelGridRenderer>{PanelContent}</PanelGridRenderer>
75
78
  </PanelGridProvider>
76
79
  );
77
80
  }
@@ -79,11 +82,60 @@ function App() {
79
82
 
80
83
  **Note:** Don't forget to import the CSS file to enable proper styling for the panels.
81
84
 
85
+ ### Next.js App Router / React Server Components
86
+
87
+ PanelGrid is fully compatible with Next.js App Router and React Server Components. The library exports are marked with `"use client"` where necessary.
88
+
89
+ **Important:** Your panel content components must also be marked with `"use client"` if they:
90
+ - Use React hooks (`useState`, `useEffect`, etc.)
91
+ - Access browser APIs
92
+ - Use event handlers
93
+
94
+ ```tsx
95
+ // app/dashboard/page.tsx (Server Component)
96
+ import { PanelGridProvider, PanelGridRenderer } from 'panelgrid';
97
+ import { PanelContent } from './PanelContent'; // Client Component
98
+ import 'panelgrid/styles.css';
99
+
100
+ export default function DashboardPage() {
101
+ const panels = [
102
+ { id: 1, x: 0, y: 0, w: 2, h: 2 },
103
+ { id: 2, x: 2, y: 0, w: 2, h: 2 },
104
+ ];
105
+
106
+ return (
107
+ <PanelGridProvider panels={panels} columnCount={4} gap={8}>
108
+ <PanelGridRenderer>{PanelContent}</PanelGridRenderer>
109
+ </PanelGridProvider>
110
+ );
111
+ }
112
+ ```
113
+
114
+ ```tsx
115
+ // app/dashboard/PanelContent.tsx (Client Component)
116
+ "use client";
117
+ import { usePanelGridControls } from 'panelgrid';
118
+ import type { PanelId } from 'panelgrid';
119
+
120
+ export function PanelContent({ id }: { id: PanelId }) {
121
+ const { removePanel } = usePanelGridControls();
122
+
123
+ return (
124
+ <div>
125
+ <h3>Panel {id}</h3>
126
+ <button onClick={() => removePanel(id)}>Remove</button>
127
+ </div>
128
+ );
129
+ }
130
+ ```
131
+
82
132
  ## Advanced Usage
83
133
 
84
134
  ### Custom Rearrangement Logic
85
135
 
86
- You can override the default collision resolution logic by providing a custom `rearrangement` function:
136
+ You can override the default collision resolution logic by providing a custom `rearrangement` function.
137
+
138
+ For advanced use cases, PanelGrid exports [helper functions](./docs/helpers.md) for collision detection, grid calculations, and more. See the [Helper Functions API Reference](./docs/helpers.md) for detailed documentation and examples.
87
139
 
88
140
  ```tsx
89
141
  import { PanelGridProvider, rearrangePanels } from 'panelgrid';
@@ -116,7 +168,7 @@ function App() {
116
168
  gap={8}
117
169
  rearrangement={customRearrange}
118
170
  >
119
- <PanelGridRenderer itemRenderer={PanelContent} />
171
+ <PanelGridRenderer>{PanelContent}</PanelGridRenderer>
120
172
  </PanelGridProvider>
121
173
  );
122
174
  }
@@ -281,7 +333,30 @@ Renderer component that displays the panels.
281
333
 
282
334
  **Props:**
283
335
 
284
- - `itemRenderer`: `React.ComponentType<{ id: PanelId }>` - Component to render each panel
336
+ - `children`: `React.ComponentType<{ id: PanelId }>` - Component type (not instance) to render each panel
337
+
338
+ **Example:**
339
+
340
+ ```tsx
341
+ "use client";
342
+ function MyPanel({ id }: { id: PanelId }) {
343
+ return <div>Panel {id}</div>;
344
+ }
345
+
346
+ // Pass the component type (not JSX)
347
+ <PanelGridRenderer>{MyPanel}</PanelGridRenderer>
348
+ ```
349
+
350
+ For panels with custom props, create a wrapper component:
351
+
352
+ ```tsx
353
+ "use client";
354
+ function CustomPanel({ id }: { id: PanelId }) {
355
+ return <MyPanel id={id} customProp="value" />;
356
+ }
357
+
358
+ <PanelGridRenderer>{CustomPanel}</PanelGridRenderer>
359
+ ```
285
360
 
286
361
  ### `usePanelGridControls()`
287
362
 
@@ -316,8 +391,25 @@ type RearrangementFunction = (
316
391
 
317
392
  ### Exported Functions
318
393
 
394
+ **Main Export:**
319
395
  - `rearrangePanels(movingPanel, allPanels, columnCount)`: Default rearrangement function that can be imported and extended
320
396
 
397
+ **Helper Functions:**
398
+
399
+ PanelGrid exports a comprehensive set of helper functions for building custom rearrangement logic, including collision detection, grid calculations, panel detection, and animation utilities.
400
+
401
+ See the [Helper Functions API Reference](./docs/helpers.md) for complete documentation.
402
+
403
+ Quick example:
404
+ ```tsx
405
+ import {
406
+ detectCollisions,
407
+ hasCollision,
408
+ snapToGrid,
409
+ findNewPosition
410
+ } from 'panelgrid/helpers';
411
+ ```
412
+
321
413
  ## Development
322
414
 
323
415
  ```bash
@@ -0,0 +1,20 @@
1
+ //#region src/helpers/animation.d.ts
2
+ /**
3
+ * Options for applying snap-back animation
4
+ */
5
+ interface ApplySnapAnimationOptions {
6
+ element: HTMLElement;
7
+ droppedLeft: number;
8
+ droppedTop: number;
9
+ nextLeft: number;
10
+ nextTop: number;
11
+ originalTransition: string;
12
+ }
13
+ /**
14
+ * Applies snap-back animation to element
15
+ * Smoothly animates the element from its dropped position to the snapped grid position
16
+ */
17
+ declare function applySnapAnimation(options: ApplySnapAnimationOptions): void;
18
+ //#endregion
19
+ export { applySnapAnimation };
20
+ //# sourceMappingURL=animation.d.cts.map
@@ -0,0 +1,20 @@
1
+ //#region src/helpers/animation.d.ts
2
+ /**
3
+ * Options for applying snap-back animation
4
+ */
5
+ interface ApplySnapAnimationOptions {
6
+ element: HTMLElement;
7
+ droppedLeft: number;
8
+ droppedTop: number;
9
+ nextLeft: number;
10
+ nextTop: number;
11
+ originalTransition: string;
12
+ }
13
+ /**
14
+ * Applies snap-back animation to element
15
+ * Smoothly animates the element from its dropped position to the snapped grid position
16
+ */
17
+ declare function applySnapAnimation(options: ApplySnapAnimationOptions): void;
18
+ //#endregion
19
+ export { applySnapAnimation };
20
+ //# sourceMappingURL=animation.d.mts.map
@@ -60,6 +60,16 @@ function gridPositionToPixels(gridCoord, baseSize, gap) {
60
60
  return Math.max(0, gridCoord * (baseSize + gap));
61
61
  }
62
62
  /**
63
+ * Snaps a pixel value to the nearest grid position
64
+ * Useful for aligning elements to the grid after drag/resize operations
65
+ *
66
+ * ピクセル値を最も近いグリッド位置にスナップします。
67
+ * ドラッグ・リサイズ操作後に要素をグリッドに整列させる際に利用する。
68
+ */
69
+ function snapToGrid(pixels, baseSize, gap) {
70
+ return gridPositionToPixels(pixelsToGridPosition(pixels, baseSize, gap), baseSize, gap);
71
+ }
72
+ /**
63
73
  * Gets the maximum Y coordinate of the panels
64
74
  * パネルの最大Y座標を取得します。
65
75
  */
@@ -73,4 +83,5 @@ exports.gridPositionToPixels = gridPositionToPixels;
73
83
  exports.gridToPixels = gridToPixels;
74
84
  exports.pixelsToGridPosition = pixelsToGridPosition;
75
85
  exports.pixelsToGridSize = pixelsToGridSize;
86
+ exports.snapToGrid = snapToGrid;
76
87
  //# sourceMappingURL=gridCalculations.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"gridCalculations.cjs","names":[],"sources":["../../src/helpers/gridCalculations.ts"],"sourcesContent":["import type { PanelCoordinate } from \"../types\";\n\n/**\n * Converts a pixel value to grid units by dividing by the cell size (baseSize + gap)\n * and rounding up with Math.ceil. Ensures the result is at least 1 and does not exceed available space.\n * When columnCount and xPosition are provided, ensures x + width <= columnCount.\n *\n * ピクセル値をグリッド単位に変換します。セルサイズ (baseSize + gap) で割り、小数点は切り上げて整数にします。\n * 結果は最小1で、columnCountとxPositionが指定された場合はx + width <= columnCountを満たすように制限されます。\n */\nexport function pixelsToGridSize(\n pixels: number,\n baseSize: number,\n gap: number,\n columnCount?: number,\n xPosition?: number\n): number {\n const gridSize = Math.ceil(pixels / (baseSize + gap));\n const constrainedSize = Math.max(1, gridSize);\n\n if (columnCount !== undefined && xPosition !== undefined) {\n // Ensure x + width <= columnCount\n const maxWidth = Math.max(1, columnCount - xPosition);\n return Math.min(constrainedSize, maxWidth);\n }\n\n if (columnCount !== undefined) {\n // Legacy behavior: constrain to columnCount\n return Math.min(constrainedSize, columnCount);\n }\n\n return constrainedSize;\n}\n\n/**\n * Converts a pixel coordinate to grid coordinate by dividing by the cell size\n * and rounding down with Math.floor, ensuring the result is not negative and does not cause overflow.\n * When columnCount and width are provided, ensures x + width <= columnCount\n *\n * ピクセル座標をグリッド座標に変換します。セルサイズで割り、小数点は切り捨てて整数にします。\n * 結果が負にならず、columnCountとwidthが指定された場合はx + width <= columnCountを満たすようにします。\n */\nexport function pixelsToGridPosition(\n pixels: number,\n baseSize: number,\n gap: number,\n columnCount?: number,\n width?: number\n): number {\n const gridPosition = Math.max(0, Math.floor(pixels / (baseSize + gap)));\n\n if (columnCount !== undefined && width !== undefined) {\n // Ensure x + width <= columnCount\n const maxPosition = Math.max(0, columnCount - width);\n return Math.min(gridPosition, maxPosition);\n }\n\n if (columnCount !== undefined) {\n // Legacy behavior: constrain to columnCount - 1\n return Math.min(gridPosition, columnCount - 1);\n }\n\n return gridPosition;\n}\n\n/**\n * Converts grid units to pixels\n * Formula: gridUnits * baseSize + max(0, gridUnits - 1) * gap\n * This accounts for gaps between grid cells but not after the last cell\n *\n * グリッド単位をピクセルに変換します。\n * 計算式: gridUnits * baseSize + max(0, gridUnits - 1) * gap\n * グリッドセル間の gap を考慮しますが、最後のセルの後には gap を含めません。\n */\nexport function gridToPixels(gridUnits: number, baseSize: number, gap: number): number {\n return gridUnits * baseSize + Math.max(0, gridUnits - 1) * gap;\n}\n\n/**\n * Converts grid coordinate to pixel coordinate for positioning\n * Formula: max(0, gridCoord * (baseSize + gap))\n * This includes the gap after each cell for proper positioning in the grid\n *\n * グリッド座標をピクセル座標に変換します(位置計算用)。\n * 計算式: max(0, gridCoord * (baseSize + gap))\n * 各セルの後に gap を含めて、グリッド内での適切な位置決めを行います。\n */\nexport function gridPositionToPixels(gridCoord: number, baseSize: number, gap: number): number {\n return Math.max(0, gridCoord * (baseSize + gap));\n}\n\n/**\n * Snaps a pixel value to the nearest grid position\n * Useful for aligning elements to the grid after drag/resize operations\n *\n * ピクセル値を最も近いグリッド位置にスナップします。\n * ドラッグ・リサイズ操作後に要素をグリッドに整列させる際に利用する。\n */\nexport function snapToGrid(pixels: number, baseSize: number, gap: number): number {\n const gridPosition = pixelsToGridPosition(pixels, baseSize, gap);\n return gridPositionToPixels(gridPosition, baseSize, gap);\n}\n\n/**\n * Gets the maximum Y coordinate of the panels\n * パネルの最大Y座標を取得します。\n */\nexport function getGridRowCount(panels: PanelCoordinate[]): number {\n return Math.max(...panels.map((p) => p.y + p.h));\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAgB,iBACd,QACA,UACA,KACA,aACA,WACQ;CACR,MAAM,WAAW,KAAK,KAAK,UAAU,WAAW,KAAK;CACrD,MAAM,kBAAkB,KAAK,IAAI,GAAG,SAAS;AAE7C,KAAI,gBAAgB,UAAa,cAAc,QAAW;EAExD,MAAM,WAAW,KAAK,IAAI,GAAG,cAAc,UAAU;AACrD,SAAO,KAAK,IAAI,iBAAiB,SAAS;;AAG5C,KAAI,gBAAgB,OAElB,QAAO,KAAK,IAAI,iBAAiB,YAAY;AAG/C,QAAO;;;;;;;;;;AAWT,SAAgB,qBACd,QACA,UACA,KACA,aACA,OACQ;CACR,MAAM,eAAe,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,WAAW,KAAK,CAAC;AAEvE,KAAI,gBAAgB,UAAa,UAAU,QAAW;EAEpD,MAAM,cAAc,KAAK,IAAI,GAAG,cAAc,MAAM;AACpD,SAAO,KAAK,IAAI,cAAc,YAAY;;AAG5C,KAAI,gBAAgB,OAElB,QAAO,KAAK,IAAI,cAAc,cAAc,EAAE;AAGhD,QAAO;;;;;;;;;;;AAYT,SAAgB,aAAa,WAAmB,UAAkB,KAAqB;AACrF,QAAO,YAAY,WAAW,KAAK,IAAI,GAAG,YAAY,EAAE,GAAG;;;;;;;;;;;AAY7D,SAAgB,qBAAqB,WAAmB,UAAkB,KAAqB;AAC7F,QAAO,KAAK,IAAI,GAAG,aAAa,WAAW,KAAK;;;;;;AAmBlD,SAAgB,gBAAgB,QAAmC;AACjE,QAAO,KAAK,IAAI,GAAG,OAAO,KAAK,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC"}
1
+ {"version":3,"file":"gridCalculations.cjs","names":[],"sources":["../../src/helpers/gridCalculations.ts"],"sourcesContent":["import type { PanelCoordinate } from \"../types\";\n\n/**\n * Converts a pixel value to grid units by dividing by the cell size (baseSize + gap)\n * and rounding up with Math.ceil. Ensures the result is at least 1 and does not exceed available space.\n * When columnCount and xPosition are provided, ensures x + width <= columnCount.\n *\n * ピクセル値をグリッド単位に変換します。セルサイズ (baseSize + gap) で割り、小数点は切り上げて整数にします。\n * 結果は最小1で、columnCountとxPositionが指定された場合はx + width <= columnCountを満たすように制限されます。\n */\nexport function pixelsToGridSize(\n pixels: number,\n baseSize: number,\n gap: number,\n columnCount?: number,\n xPosition?: number\n): number {\n const gridSize = Math.ceil(pixels / (baseSize + gap));\n const constrainedSize = Math.max(1, gridSize);\n\n if (columnCount !== undefined && xPosition !== undefined) {\n // Ensure x + width <= columnCount\n const maxWidth = Math.max(1, columnCount - xPosition);\n return Math.min(constrainedSize, maxWidth);\n }\n\n if (columnCount !== undefined) {\n // Legacy behavior: constrain to columnCount\n return Math.min(constrainedSize, columnCount);\n }\n\n return constrainedSize;\n}\n\n/**\n * Converts a pixel coordinate to grid coordinate by dividing by the cell size\n * and rounding down with Math.floor, ensuring the result is not negative and does not cause overflow.\n * When columnCount and width are provided, ensures x + width <= columnCount\n *\n * ピクセル座標をグリッド座標に変換します。セルサイズで割り、小数点は切り捨てて整数にします。\n * 結果が負にならず、columnCountとwidthが指定された場合はx + width <= columnCountを満たすようにします。\n */\nexport function pixelsToGridPosition(\n pixels: number,\n baseSize: number,\n gap: number,\n columnCount?: number,\n width?: number\n): number {\n const gridPosition = Math.max(0, Math.floor(pixels / (baseSize + gap)));\n\n if (columnCount !== undefined && width !== undefined) {\n // Ensure x + width <= columnCount\n const maxPosition = Math.max(0, columnCount - width);\n return Math.min(gridPosition, maxPosition);\n }\n\n if (columnCount !== undefined) {\n // Legacy behavior: constrain to columnCount - 1\n return Math.min(gridPosition, columnCount - 1);\n }\n\n return gridPosition;\n}\n\n/**\n * Converts grid units to pixels\n * Formula: gridUnits * baseSize + max(0, gridUnits - 1) * gap\n * This accounts for gaps between grid cells but not after the last cell\n *\n * グリッド単位をピクセルに変換します。\n * 計算式: gridUnits * baseSize + max(0, gridUnits - 1) * gap\n * グリッドセル間の gap を考慮しますが、最後のセルの後には gap を含めません。\n */\nexport function gridToPixels(gridUnits: number, baseSize: number, gap: number): number {\n return gridUnits * baseSize + Math.max(0, gridUnits - 1) * gap;\n}\n\n/**\n * Converts grid coordinate to pixel coordinate for positioning\n * Formula: max(0, gridCoord * (baseSize + gap))\n * This includes the gap after each cell for proper positioning in the grid\n *\n * グリッド座標をピクセル座標に変換します(位置計算用)。\n * 計算式: max(0, gridCoord * (baseSize + gap))\n * 各セルの後に gap を含めて、グリッド内での適切な位置決めを行います。\n */\nexport function gridPositionToPixels(gridCoord: number, baseSize: number, gap: number): number {\n return Math.max(0, gridCoord * (baseSize + gap));\n}\n\n/**\n * Snaps a pixel value to the nearest grid position\n * Useful for aligning elements to the grid after drag/resize operations\n *\n * ピクセル値を最も近いグリッド位置にスナップします。\n * ドラッグ・リサイズ操作後に要素をグリッドに整列させる際に利用する。\n */\nexport function snapToGrid(pixels: number, baseSize: number, gap: number): number {\n const gridPosition = pixelsToGridPosition(pixels, baseSize, gap);\n return gridPositionToPixels(gridPosition, baseSize, gap);\n}\n\n/**\n * Gets the maximum Y coordinate of the panels\n * パネルの最大Y座標を取得します。\n */\nexport function getGridRowCount(panels: PanelCoordinate[]): number {\n return Math.max(...panels.map((p) => p.y + p.h));\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAgB,iBACd,QACA,UACA,KACA,aACA,WACQ;CACR,MAAM,WAAW,KAAK,KAAK,UAAU,WAAW,KAAK;CACrD,MAAM,kBAAkB,KAAK,IAAI,GAAG,SAAS;AAE7C,KAAI,gBAAgB,UAAa,cAAc,QAAW;EAExD,MAAM,WAAW,KAAK,IAAI,GAAG,cAAc,UAAU;AACrD,SAAO,KAAK,IAAI,iBAAiB,SAAS;;AAG5C,KAAI,gBAAgB,OAElB,QAAO,KAAK,IAAI,iBAAiB,YAAY;AAG/C,QAAO;;;;;;;;;;AAWT,SAAgB,qBACd,QACA,UACA,KACA,aACA,OACQ;CACR,MAAM,eAAe,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,WAAW,KAAK,CAAC;AAEvE,KAAI,gBAAgB,UAAa,UAAU,QAAW;EAEpD,MAAM,cAAc,KAAK,IAAI,GAAG,cAAc,MAAM;AACpD,SAAO,KAAK,IAAI,cAAc,YAAY;;AAG5C,KAAI,gBAAgB,OAElB,QAAO,KAAK,IAAI,cAAc,cAAc,EAAE;AAGhD,QAAO;;;;;;;;;;;AAYT,SAAgB,aAAa,WAAmB,UAAkB,KAAqB;AACrF,QAAO,YAAY,WAAW,KAAK,IAAI,GAAG,YAAY,EAAE,GAAG;;;;;;;;;;;AAY7D,SAAgB,qBAAqB,WAAmB,UAAkB,KAAqB;AAC7F,QAAO,KAAK,IAAI,GAAG,aAAa,WAAW,KAAK;;;;;;;;;AAUlD,SAAgB,WAAW,QAAgB,UAAkB,KAAqB;AAEhF,QAAO,qBADc,qBAAqB,QAAQ,UAAU,IAAI,EACtB,UAAU,IAAI;;;;;;AAO1D,SAAgB,gBAAgB,QAAmC;AACjE,QAAO,KAAK,IAAI,GAAG,OAAO,KAAK,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC"}
@@ -0,0 +1,53 @@
1
+ import "../types.cjs";
2
+
3
+ //#region src/helpers/gridCalculations.d.ts
4
+
5
+ /**
6
+ * Converts a pixel value to grid units by dividing by the cell size (baseSize + gap)
7
+ * and rounding up with Math.ceil. Ensures the result is at least 1 and does not exceed available space.
8
+ * When columnCount and xPosition are provided, ensures x + width <= columnCount.
9
+ *
10
+ * ピクセル値をグリッド単位に変換します。セルサイズ (baseSize + gap) で割り、小数点は切り上げて整数にします。
11
+ * 結果は最小1で、columnCountとxPositionが指定された場合はx + width <= columnCountを満たすように制限されます。
12
+ */
13
+ declare function pixelsToGridSize(pixels: number, baseSize: number, gap: number, columnCount?: number, xPosition?: number): number;
14
+ /**
15
+ * Converts a pixel coordinate to grid coordinate by dividing by the cell size
16
+ * and rounding down with Math.floor, ensuring the result is not negative and does not cause overflow.
17
+ * When columnCount and width are provided, ensures x + width <= columnCount
18
+ *
19
+ * ピクセル座標をグリッド座標に変換します。セルサイズで割り、小数点は切り捨てて整数にします。
20
+ * 結果が負にならず、columnCountとwidthが指定された場合はx + width <= columnCountを満たすようにします。
21
+ */
22
+ declare function pixelsToGridPosition(pixels: number, baseSize: number, gap: number, columnCount?: number, width?: number): number;
23
+ /**
24
+ * Converts grid units to pixels
25
+ * Formula: gridUnits * baseSize + max(0, gridUnits - 1) * gap
26
+ * This accounts for gaps between grid cells but not after the last cell
27
+ *
28
+ * グリッド単位をピクセルに変換します。
29
+ * 計算式: gridUnits * baseSize + max(0, gridUnits - 1) * gap
30
+ * グリッドセル間の gap を考慮しますが、最後のセルの後には gap を含めません。
31
+ */
32
+ declare function gridToPixels(gridUnits: number, baseSize: number, gap: number): number;
33
+ /**
34
+ * Converts grid coordinate to pixel coordinate for positioning
35
+ * Formula: max(0, gridCoord * (baseSize + gap))
36
+ * This includes the gap after each cell for proper positioning in the grid
37
+ *
38
+ * グリッド座標をピクセル座標に変換します(位置計算用)。
39
+ * 計算式: max(0, gridCoord * (baseSize + gap))
40
+ * 各セルの後に gap を含めて、グリッド内での適切な位置決めを行います。
41
+ */
42
+ declare function gridPositionToPixels(gridCoord: number, baseSize: number, gap: number): number;
43
+ /**
44
+ * Snaps a pixel value to the nearest grid position
45
+ * Useful for aligning elements to the grid after drag/resize operations
46
+ *
47
+ * ピクセル値を最も近いグリッド位置にスナップします。
48
+ * ドラッグ・リサイズ操作後に要素をグリッドに整列させる際に利用する。
49
+ */
50
+ declare function snapToGrid(pixels: number, baseSize: number, gap: number): number;
51
+ //#endregion
52
+ export { gridPositionToPixels, gridToPixels, pixelsToGridPosition, pixelsToGridSize, snapToGrid };
53
+ //# sourceMappingURL=gridCalculations.d.cts.map
@@ -0,0 +1,53 @@
1
+ import "../types.mjs";
2
+
3
+ //#region src/helpers/gridCalculations.d.ts
4
+
5
+ /**
6
+ * Converts a pixel value to grid units by dividing by the cell size (baseSize + gap)
7
+ * and rounding up with Math.ceil. Ensures the result is at least 1 and does not exceed available space.
8
+ * When columnCount and xPosition are provided, ensures x + width <= columnCount.
9
+ *
10
+ * ピクセル値をグリッド単位に変換します。セルサイズ (baseSize + gap) で割り、小数点は切り上げて整数にします。
11
+ * 結果は最小1で、columnCountとxPositionが指定された場合はx + width <= columnCountを満たすように制限されます。
12
+ */
13
+ declare function pixelsToGridSize(pixels: number, baseSize: number, gap: number, columnCount?: number, xPosition?: number): number;
14
+ /**
15
+ * Converts a pixel coordinate to grid coordinate by dividing by the cell size
16
+ * and rounding down with Math.floor, ensuring the result is not negative and does not cause overflow.
17
+ * When columnCount and width are provided, ensures x + width <= columnCount
18
+ *
19
+ * ピクセル座標をグリッド座標に変換します。セルサイズで割り、小数点は切り捨てて整数にします。
20
+ * 結果が負にならず、columnCountとwidthが指定された場合はx + width <= columnCountを満たすようにします。
21
+ */
22
+ declare function pixelsToGridPosition(pixels: number, baseSize: number, gap: number, columnCount?: number, width?: number): number;
23
+ /**
24
+ * Converts grid units to pixels
25
+ * Formula: gridUnits * baseSize + max(0, gridUnits - 1) * gap
26
+ * This accounts for gaps between grid cells but not after the last cell
27
+ *
28
+ * グリッド単位をピクセルに変換します。
29
+ * 計算式: gridUnits * baseSize + max(0, gridUnits - 1) * gap
30
+ * グリッドセル間の gap を考慮しますが、最後のセルの後には gap を含めません。
31
+ */
32
+ declare function gridToPixels(gridUnits: number, baseSize: number, gap: number): number;
33
+ /**
34
+ * Converts grid coordinate to pixel coordinate for positioning
35
+ * Formula: max(0, gridCoord * (baseSize + gap))
36
+ * This includes the gap after each cell for proper positioning in the grid
37
+ *
38
+ * グリッド座標をピクセル座標に変換します(位置計算用)。
39
+ * 計算式: max(0, gridCoord * (baseSize + gap))
40
+ * 各セルの後に gap を含めて、グリッド内での適切な位置決めを行います。
41
+ */
42
+ declare function gridPositionToPixels(gridCoord: number, baseSize: number, gap: number): number;
43
+ /**
44
+ * Snaps a pixel value to the nearest grid position
45
+ * Useful for aligning elements to the grid after drag/resize operations
46
+ *
47
+ * ピクセル値を最も近いグリッド位置にスナップします。
48
+ * ドラッグ・リサイズ操作後に要素をグリッドに整列させる際に利用する。
49
+ */
50
+ declare function snapToGrid(pixels: number, baseSize: number, gap: number): number;
51
+ //#endregion
52
+ export { gridPositionToPixels, gridToPixels, pixelsToGridPosition, pixelsToGridSize, snapToGrid };
53
+ //# sourceMappingURL=gridCalculations.d.mts.map
@@ -59,6 +59,16 @@ function gridPositionToPixels(gridCoord, baseSize, gap) {
59
59
  return Math.max(0, gridCoord * (baseSize + gap));
60
60
  }
61
61
  /**
62
+ * Snaps a pixel value to the nearest grid position
63
+ * Useful for aligning elements to the grid after drag/resize operations
64
+ *
65
+ * ピクセル値を最も近いグリッド位置にスナップします。
66
+ * ドラッグ・リサイズ操作後に要素をグリッドに整列させる際に利用する。
67
+ */
68
+ function snapToGrid(pixels, baseSize, gap) {
69
+ return gridPositionToPixels(pixelsToGridPosition(pixels, baseSize, gap), baseSize, gap);
70
+ }
71
+ /**
62
72
  * Gets the maximum Y coordinate of the panels
63
73
  * パネルの最大Y座標を取得します。
64
74
  */
@@ -67,5 +77,5 @@ function getGridRowCount(panels) {
67
77
  }
68
78
 
69
79
  //#endregion
70
- export { getGridRowCount, gridPositionToPixels, gridToPixels, pixelsToGridPosition, pixelsToGridSize };
80
+ export { getGridRowCount, gridPositionToPixels, gridToPixels, pixelsToGridPosition, pixelsToGridSize, snapToGrid };
71
81
  //# sourceMappingURL=gridCalculations.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"gridCalculations.mjs","names":[],"sources":["../../src/helpers/gridCalculations.ts"],"sourcesContent":["import type { PanelCoordinate } from \"../types\";\n\n/**\n * Converts a pixel value to grid units by dividing by the cell size (baseSize + gap)\n * and rounding up with Math.ceil. Ensures the result is at least 1 and does not exceed available space.\n * When columnCount and xPosition are provided, ensures x + width <= columnCount.\n *\n * ピクセル値をグリッド単位に変換します。セルサイズ (baseSize + gap) で割り、小数点は切り上げて整数にします。\n * 結果は最小1で、columnCountとxPositionが指定された場合はx + width <= columnCountを満たすように制限されます。\n */\nexport function pixelsToGridSize(\n pixels: number,\n baseSize: number,\n gap: number,\n columnCount?: number,\n xPosition?: number\n): number {\n const gridSize = Math.ceil(pixels / (baseSize + gap));\n const constrainedSize = Math.max(1, gridSize);\n\n if (columnCount !== undefined && xPosition !== undefined) {\n // Ensure x + width <= columnCount\n const maxWidth = Math.max(1, columnCount - xPosition);\n return Math.min(constrainedSize, maxWidth);\n }\n\n if (columnCount !== undefined) {\n // Legacy behavior: constrain to columnCount\n return Math.min(constrainedSize, columnCount);\n }\n\n return constrainedSize;\n}\n\n/**\n * Converts a pixel coordinate to grid coordinate by dividing by the cell size\n * and rounding down with Math.floor, ensuring the result is not negative and does not cause overflow.\n * When columnCount and width are provided, ensures x + width <= columnCount\n *\n * ピクセル座標をグリッド座標に変換します。セルサイズで割り、小数点は切り捨てて整数にします。\n * 結果が負にならず、columnCountとwidthが指定された場合はx + width <= columnCountを満たすようにします。\n */\nexport function pixelsToGridPosition(\n pixels: number,\n baseSize: number,\n gap: number,\n columnCount?: number,\n width?: number\n): number {\n const gridPosition = Math.max(0, Math.floor(pixels / (baseSize + gap)));\n\n if (columnCount !== undefined && width !== undefined) {\n // Ensure x + width <= columnCount\n const maxPosition = Math.max(0, columnCount - width);\n return Math.min(gridPosition, maxPosition);\n }\n\n if (columnCount !== undefined) {\n // Legacy behavior: constrain to columnCount - 1\n return Math.min(gridPosition, columnCount - 1);\n }\n\n return gridPosition;\n}\n\n/**\n * Converts grid units to pixels\n * Formula: gridUnits * baseSize + max(0, gridUnits - 1) * gap\n * This accounts for gaps between grid cells but not after the last cell\n *\n * グリッド単位をピクセルに変換します。\n * 計算式: gridUnits * baseSize + max(0, gridUnits - 1) * gap\n * グリッドセル間の gap を考慮しますが、最後のセルの後には gap を含めません。\n */\nexport function gridToPixels(gridUnits: number, baseSize: number, gap: number): number {\n return gridUnits * baseSize + Math.max(0, gridUnits - 1) * gap;\n}\n\n/**\n * Converts grid coordinate to pixel coordinate for positioning\n * Formula: max(0, gridCoord * (baseSize + gap))\n * This includes the gap after each cell for proper positioning in the grid\n *\n * グリッド座標をピクセル座標に変換します(位置計算用)。\n * 計算式: max(0, gridCoord * (baseSize + gap))\n * 各セルの後に gap を含めて、グリッド内での適切な位置決めを行います。\n */\nexport function gridPositionToPixels(gridCoord: number, baseSize: number, gap: number): number {\n return Math.max(0, gridCoord * (baseSize + gap));\n}\n\n/**\n * Snaps a pixel value to the nearest grid position\n * Useful for aligning elements to the grid after drag/resize operations\n *\n * ピクセル値を最も近いグリッド位置にスナップします。\n * ドラッグ・リサイズ操作後に要素をグリッドに整列させる際に利用する。\n */\nexport function snapToGrid(pixels: number, baseSize: number, gap: number): number {\n const gridPosition = pixelsToGridPosition(pixels, baseSize, gap);\n return gridPositionToPixels(gridPosition, baseSize, gap);\n}\n\n/**\n * Gets the maximum Y coordinate of the panels\n * パネルの最大Y座標を取得します。\n */\nexport function getGridRowCount(panels: PanelCoordinate[]): number {\n return Math.max(...panels.map((p) => p.y + p.h));\n}\n"],"mappings":";;;;;;;;;AAUA,SAAgB,iBACd,QACA,UACA,KACA,aACA,WACQ;CACR,MAAM,WAAW,KAAK,KAAK,UAAU,WAAW,KAAK;CACrD,MAAM,kBAAkB,KAAK,IAAI,GAAG,SAAS;AAE7C,KAAI,gBAAgB,UAAa,cAAc,QAAW;EAExD,MAAM,WAAW,KAAK,IAAI,GAAG,cAAc,UAAU;AACrD,SAAO,KAAK,IAAI,iBAAiB,SAAS;;AAG5C,KAAI,gBAAgB,OAElB,QAAO,KAAK,IAAI,iBAAiB,YAAY;AAG/C,QAAO;;;;;;;;;;AAWT,SAAgB,qBACd,QACA,UACA,KACA,aACA,OACQ;CACR,MAAM,eAAe,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,WAAW,KAAK,CAAC;AAEvE,KAAI,gBAAgB,UAAa,UAAU,QAAW;EAEpD,MAAM,cAAc,KAAK,IAAI,GAAG,cAAc,MAAM;AACpD,SAAO,KAAK,IAAI,cAAc,YAAY;;AAG5C,KAAI,gBAAgB,OAElB,QAAO,KAAK,IAAI,cAAc,cAAc,EAAE;AAGhD,QAAO;;;;;;;;;;;AAYT,SAAgB,aAAa,WAAmB,UAAkB,KAAqB;AACrF,QAAO,YAAY,WAAW,KAAK,IAAI,GAAG,YAAY,EAAE,GAAG;;;;;;;;;;;AAY7D,SAAgB,qBAAqB,WAAmB,UAAkB,KAAqB;AAC7F,QAAO,KAAK,IAAI,GAAG,aAAa,WAAW,KAAK;;;;;;AAmBlD,SAAgB,gBAAgB,QAAmC;AACjE,QAAO,KAAK,IAAI,GAAG,OAAO,KAAK,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC"}
1
+ {"version":3,"file":"gridCalculations.mjs","names":[],"sources":["../../src/helpers/gridCalculations.ts"],"sourcesContent":["import type { PanelCoordinate } from \"../types\";\n\n/**\n * Converts a pixel value to grid units by dividing by the cell size (baseSize + gap)\n * and rounding up with Math.ceil. Ensures the result is at least 1 and does not exceed available space.\n * When columnCount and xPosition are provided, ensures x + width <= columnCount.\n *\n * ピクセル値をグリッド単位に変換します。セルサイズ (baseSize + gap) で割り、小数点は切り上げて整数にします。\n * 結果は最小1で、columnCountとxPositionが指定された場合はx + width <= columnCountを満たすように制限されます。\n */\nexport function pixelsToGridSize(\n pixels: number,\n baseSize: number,\n gap: number,\n columnCount?: number,\n xPosition?: number\n): number {\n const gridSize = Math.ceil(pixels / (baseSize + gap));\n const constrainedSize = Math.max(1, gridSize);\n\n if (columnCount !== undefined && xPosition !== undefined) {\n // Ensure x + width <= columnCount\n const maxWidth = Math.max(1, columnCount - xPosition);\n return Math.min(constrainedSize, maxWidth);\n }\n\n if (columnCount !== undefined) {\n // Legacy behavior: constrain to columnCount\n return Math.min(constrainedSize, columnCount);\n }\n\n return constrainedSize;\n}\n\n/**\n * Converts a pixel coordinate to grid coordinate by dividing by the cell size\n * and rounding down with Math.floor, ensuring the result is not negative and does not cause overflow.\n * When columnCount and width are provided, ensures x + width <= columnCount\n *\n * ピクセル座標をグリッド座標に変換します。セルサイズで割り、小数点は切り捨てて整数にします。\n * 結果が負にならず、columnCountとwidthが指定された場合はx + width <= columnCountを満たすようにします。\n */\nexport function pixelsToGridPosition(\n pixels: number,\n baseSize: number,\n gap: number,\n columnCount?: number,\n width?: number\n): number {\n const gridPosition = Math.max(0, Math.floor(pixels / (baseSize + gap)));\n\n if (columnCount !== undefined && width !== undefined) {\n // Ensure x + width <= columnCount\n const maxPosition = Math.max(0, columnCount - width);\n return Math.min(gridPosition, maxPosition);\n }\n\n if (columnCount !== undefined) {\n // Legacy behavior: constrain to columnCount - 1\n return Math.min(gridPosition, columnCount - 1);\n }\n\n return gridPosition;\n}\n\n/**\n * Converts grid units to pixels\n * Formula: gridUnits * baseSize + max(0, gridUnits - 1) * gap\n * This accounts for gaps between grid cells but not after the last cell\n *\n * グリッド単位をピクセルに変換します。\n * 計算式: gridUnits * baseSize + max(0, gridUnits - 1) * gap\n * グリッドセル間の gap を考慮しますが、最後のセルの後には gap を含めません。\n */\nexport function gridToPixels(gridUnits: number, baseSize: number, gap: number): number {\n return gridUnits * baseSize + Math.max(0, gridUnits - 1) * gap;\n}\n\n/**\n * Converts grid coordinate to pixel coordinate for positioning\n * Formula: max(0, gridCoord * (baseSize + gap))\n * This includes the gap after each cell for proper positioning in the grid\n *\n * グリッド座標をピクセル座標に変換します(位置計算用)。\n * 計算式: max(0, gridCoord * (baseSize + gap))\n * 各セルの後に gap を含めて、グリッド内での適切な位置決めを行います。\n */\nexport function gridPositionToPixels(gridCoord: number, baseSize: number, gap: number): number {\n return Math.max(0, gridCoord * (baseSize + gap));\n}\n\n/**\n * Snaps a pixel value to the nearest grid position\n * Useful for aligning elements to the grid after drag/resize operations\n *\n * ピクセル値を最も近いグリッド位置にスナップします。\n * ドラッグ・リサイズ操作後に要素をグリッドに整列させる際に利用する。\n */\nexport function snapToGrid(pixels: number, baseSize: number, gap: number): number {\n const gridPosition = pixelsToGridPosition(pixels, baseSize, gap);\n return gridPositionToPixels(gridPosition, baseSize, gap);\n}\n\n/**\n * Gets the maximum Y coordinate of the panels\n * パネルの最大Y座標を取得します。\n */\nexport function getGridRowCount(panels: PanelCoordinate[]): number {\n return Math.max(...panels.map((p) => p.y + p.h));\n}\n"],"mappings":";;;;;;;;;AAUA,SAAgB,iBACd,QACA,UACA,KACA,aACA,WACQ;CACR,MAAM,WAAW,KAAK,KAAK,UAAU,WAAW,KAAK;CACrD,MAAM,kBAAkB,KAAK,IAAI,GAAG,SAAS;AAE7C,KAAI,gBAAgB,UAAa,cAAc,QAAW;EAExD,MAAM,WAAW,KAAK,IAAI,GAAG,cAAc,UAAU;AACrD,SAAO,KAAK,IAAI,iBAAiB,SAAS;;AAG5C,KAAI,gBAAgB,OAElB,QAAO,KAAK,IAAI,iBAAiB,YAAY;AAG/C,QAAO;;;;;;;;;;AAWT,SAAgB,qBACd,QACA,UACA,KACA,aACA,OACQ;CACR,MAAM,eAAe,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,WAAW,KAAK,CAAC;AAEvE,KAAI,gBAAgB,UAAa,UAAU,QAAW;EAEpD,MAAM,cAAc,KAAK,IAAI,GAAG,cAAc,MAAM;AACpD,SAAO,KAAK,IAAI,cAAc,YAAY;;AAG5C,KAAI,gBAAgB,OAElB,QAAO,KAAK,IAAI,cAAc,cAAc,EAAE;AAGhD,QAAO;;;;;;;;;;;AAYT,SAAgB,aAAa,WAAmB,UAAkB,KAAqB;AACrF,QAAO,YAAY,WAAW,KAAK,IAAI,GAAG,YAAY,EAAE,GAAG;;;;;;;;;;;AAY7D,SAAgB,qBAAqB,WAAmB,UAAkB,KAAqB;AAC7F,QAAO,KAAK,IAAI,GAAG,aAAa,WAAW,KAAK;;;;;;;;;AAUlD,SAAgB,WAAW,QAAgB,UAAkB,KAAqB;AAEhF,QAAO,qBADc,qBAAqB,QAAQ,UAAU,IAAI,EACtB,UAAU,IAAI;;;;;;AAO1D,SAAgB,gBAAgB,QAAmC;AACjE,QAAO,KAAK,IAAI,GAAG,OAAO,KAAK,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC"}
@@ -0,0 +1,17 @@
1
+ const require_rearrangement = require('./rearrangement.cjs');
2
+ const require_animation = require('./animation.cjs');
3
+ const require_gridCalculations = require('./gridCalculations.cjs');
4
+ const require_panelDetection = require('./panelDetection.cjs');
5
+
6
+ exports.applySnapAnimation = require_animation.applySnapAnimation;
7
+ exports.detectAnimatingPanels = require_panelDetection.detectAnimatingPanels;
8
+ exports.detectCollisions = require_rearrangement.detectCollisions;
9
+ exports.findNewPosition = require_rearrangement.findNewPosition;
10
+ exports.gridPositionToPixels = require_gridCalculations.gridPositionToPixels;
11
+ exports.gridToPixels = require_gridCalculations.gridToPixels;
12
+ exports.hasCollision = require_rearrangement.hasCollision;
13
+ exports.pixelsToGridPosition = require_gridCalculations.pixelsToGridPosition;
14
+ exports.pixelsToGridSize = require_gridCalculations.pixelsToGridSize;
15
+ exports.rearrangePanels = require_rearrangement.rearrangePanels;
16
+ exports.rectanglesOverlap = require_rearrangement.rectanglesOverlap;
17
+ exports.snapToGrid = require_gridCalculations.snapToGrid;
@@ -0,0 +1,5 @@
1
+ import { applySnapAnimation } from "./animation.cjs";
2
+ import { gridPositionToPixels, gridToPixels, pixelsToGridPosition, pixelsToGridSize, snapToGrid } from "./gridCalculations.cjs";
3
+ import { detectAnimatingPanels } from "./panelDetection.cjs";
4
+ import { detectCollisions, findNewPosition, hasCollision, rearrangePanels, rectanglesOverlap } from "./rearrangement.cjs";
5
+ export { applySnapAnimation, detectAnimatingPanels, detectCollisions, findNewPosition, gridPositionToPixels, gridToPixels, hasCollision, pixelsToGridPosition, pixelsToGridSize, rearrangePanels, rectanglesOverlap, snapToGrid };
@@ -0,0 +1,5 @@
1
+ import { applySnapAnimation } from "./animation.mjs";
2
+ import { gridPositionToPixels, gridToPixels, pixelsToGridPosition, pixelsToGridSize, snapToGrid } from "./gridCalculations.mjs";
3
+ import { detectAnimatingPanels } from "./panelDetection.mjs";
4
+ import { detectCollisions, findNewPosition, hasCollision, rearrangePanels, rectanglesOverlap } from "./rearrangement.mjs";
5
+ export { applySnapAnimation, detectAnimatingPanels, detectCollisions, findNewPosition, gridPositionToPixels, gridToPixels, hasCollision, pixelsToGridPosition, pixelsToGridSize, rearrangePanels, rectanglesOverlap, snapToGrid };
@@ -0,0 +1,6 @@
1
+ import { detectCollisions, findNewPosition, hasCollision, rearrangePanels, rectanglesOverlap } from "./rearrangement.mjs";
2
+ import { applySnapAnimation } from "./animation.mjs";
3
+ import { gridPositionToPixels, gridToPixels, pixelsToGridPosition, pixelsToGridSize, snapToGrid } from "./gridCalculations.mjs";
4
+ import { detectAnimatingPanels } from "./panelDetection.mjs";
5
+
6
+ export { applySnapAnimation, detectAnimatingPanels, detectCollisions, findNewPosition, gridPositionToPixels, gridToPixels, hasCollision, pixelsToGridPosition, pixelsToGridSize, rearrangePanels, rectanglesOverlap, snapToGrid };
@@ -0,0 +1,20 @@
1
+ import { PanelCoordinate, PanelId } from "../types.cjs";
2
+
3
+ //#region src/helpers/panelDetection.d.ts
4
+
5
+ /**
6
+ * Options for detecting animating panels
7
+ */
8
+ interface DetectAnimatingPanelsOptions {
9
+ oldPanels: PanelCoordinate[];
10
+ newPanels: PanelCoordinate[];
11
+ excludePanelId: PanelId;
12
+ }
13
+ /**
14
+ * Detects which panels have changed position/size and marks them for animation
15
+ * Returns a Set of panel IDs that should be animated
16
+ */
17
+ declare function detectAnimatingPanels(options: DetectAnimatingPanelsOptions): Set<PanelId>;
18
+ //#endregion
19
+ export { detectAnimatingPanels };
20
+ //# sourceMappingURL=panelDetection.d.cts.map
@@ -0,0 +1,20 @@
1
+ import { PanelCoordinate, PanelId } from "../types.mjs";
2
+
3
+ //#region src/helpers/panelDetection.d.ts
4
+
5
+ /**
6
+ * Options for detecting animating panels
7
+ */
8
+ interface DetectAnimatingPanelsOptions {
9
+ oldPanels: PanelCoordinate[];
10
+ newPanels: PanelCoordinate[];
11
+ excludePanelId: PanelId;
12
+ }
13
+ /**
14
+ * Detects which panels have changed position/size and marks them for animation
15
+ * Returns a Set of panel IDs that should be animated
16
+ */
17
+ declare function detectAnimatingPanels(options: DetectAnimatingPanelsOptions): Set<PanelId>;
18
+ //#endregion
19
+ export { detectAnimatingPanels };
20
+ //# sourceMappingURL=panelDetection.d.mts.map
@@ -89,38 +89,85 @@ function constrainToGrid(panel, columnCount) {
89
89
  /**
90
90
  * Rearrange panels to resolve collisions when a panel is moved or resized
91
91
  * Panels are moved horizontally first, then vertically if needed
92
+ * For compound resizes (both width and height change), uses two-phase processing
92
93
  * パネルの移動・リサイズ時に衝突を解決するようにパネルを再配置
93
94
  * 横方向を優先し、必要に応じて縦方向に移動
95
+ * 幅と高さが同時に変更される場合は、二段階処理を使用
94
96
  */
95
97
  function rearrangePanels(movingPanel, allPanels, columnCount) {
96
98
  const constrainedMovingPanel = constrainToGrid(movingPanel, columnCount);
99
+ const originalPanel = allPanels.find((p) => p.id === movingPanel.id);
100
+ if (originalPanel) {
101
+ const widthChanged = originalPanel.w !== constrainedMovingPanel.w;
102
+ const heightChanged = originalPanel.h !== constrainedMovingPanel.h;
103
+ if (widthChanged && heightChanged) {
104
+ const afterWidthChange = rearrangePanelsInternal({
105
+ ...constrainedMovingPanel,
106
+ h: originalPanel.h
107
+ }, allPanels, columnCount);
108
+ return rearrangePanelsInternal({
109
+ ...constrainedMovingPanel,
110
+ x: afterWidthChange.find((p) => p.id === movingPanel.id)?.x ?? constrainedMovingPanel.x,
111
+ y: afterWidthChange.find((p) => p.id === movingPanel.id)?.y ?? constrainedMovingPanel.y
112
+ }, afterWidthChange, columnCount);
113
+ }
114
+ }
115
+ return rearrangePanelsInternal(constrainedMovingPanel, allPanels, columnCount);
116
+ }
117
+ /**
118
+ * Internal implementation of panel rearrangement
119
+ * パネル再配置の内部実装
120
+ */
121
+ function rearrangePanelsInternal(movingPanel, allPanels, columnCount) {
97
122
  const panelMap = /* @__PURE__ */ new Map();
98
123
  for (const panel of allPanels) panelMap.set(panel.id, { ...panel });
99
- panelMap.set(constrainedMovingPanel.id, { ...constrainedMovingPanel });
100
- const queue = [{ ...constrainedMovingPanel }];
124
+ panelMap.set(movingPanel.id, { ...movingPanel });
125
+ const queue = [{ ...movingPanel }];
101
126
  const processed = /* @__PURE__ */ new Set();
127
+ const processCount = /* @__PURE__ */ new Map();
128
+ const MAX_PROCESS_COUNT = 10;
102
129
  while (queue.length > 0) {
103
130
  const current = queue.shift();
131
+ const count = processCount.get(current.id) || 0;
132
+ if (count >= MAX_PROCESS_COUNT) continue;
133
+ processCount.set(current.id, count + 1);
134
+ const currentInMap = panelMap.get(current.id);
135
+ if (currentInMap && (currentInMap.x !== current.x || currentInMap.y !== current.y)) continue;
104
136
  if (processed.has(current.id)) continue;
105
- processed.add(current.id);
106
137
  const collidingIds = detectCollisions(current, panelMap);
107
138
  if (collidingIds.length === 0) {
108
139
  panelMap.set(current.id, current);
140
+ processed.add(current.id);
109
141
  continue;
110
142
  }
111
- for (const collidingId of collidingIds) {
143
+ const sortedCollidingIds = collidingIds.sort((a, b) => {
144
+ const panelA = panelMap.get(a);
145
+ const panelB = panelMap.get(b);
146
+ if (panelA.y !== panelB.y) return panelA.y - panelB.y;
147
+ return panelA.x - panelB.x;
148
+ });
149
+ const pushedInIteration = /* @__PURE__ */ new Set();
150
+ for (const collidingId of sortedCollidingIds) {
112
151
  const colliding = panelMap.get(collidingId);
113
152
  if (!colliding) continue;
114
153
  const newPos = findNewPosition(colliding, current, columnCount);
115
- const updated = {
154
+ const candidate = {
116
155
  ...colliding,
117
156
  x: newPos.x,
118
157
  y: newPos.y
119
158
  };
120
- panelMap.set(collidingId, updated);
121
- queue.push(updated);
159
+ let wouldCollideWithPushed = false;
160
+ for (const pushedId of pushedInIteration) if (rectanglesOverlap(candidate, panelMap.get(pushedId))) {
161
+ wouldCollideWithPushed = true;
162
+ break;
163
+ }
164
+ if (wouldCollideWithPushed) continue;
165
+ panelMap.set(collidingId, candidate);
166
+ queue.push(candidate);
167
+ pushedInIteration.add(collidingId);
122
168
  }
123
169
  panelMap.set(current.id, current);
170
+ processed.add(current.id);
124
171
  }
125
172
  return Array.from(panelMap.values());
126
173
  }
@@ -156,6 +203,10 @@ function findNewPositionToAddPanel(panelToAdd, allPanels, columnCount) {
156
203
  }
157
204
 
158
205
  //#endregion
206
+ exports.detectCollisions = detectCollisions;
207
+ exports.findNewPosition = findNewPosition;
159
208
  exports.findNewPositionToAddPanel = findNewPositionToAddPanel;
209
+ exports.hasCollision = hasCollision;
160
210
  exports.rearrangePanels = rearrangePanels;
211
+ exports.rectanglesOverlap = rectanglesOverlap;
161
212
  //# sourceMappingURL=rearrangement.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"rearrangement.cjs","names":["queue: PanelCoordinate[]","candidate: PanelCoordinate"],"sources":["../../src/helpers/rearrangement.ts"],"sourcesContent":["import type { PanelCoordinate, PanelId } from \"../types\";\n\n/**\n * Check if two rectangles overlap using AABB (Axis-Aligned Bounding Box) test\n * 2つの矩形が重なっているかをAABBテストで判定\n */\nexport function rectanglesOverlap(\n a: { x: number; y: number; w: number; h: number },\n b: { x: number; y: number; w: number; h: number }\n): boolean {\n return !(\n (\n a.x + a.w <= b.x || // a is to the left of b\n b.x + b.w <= a.x || // b is to the left of a\n a.y + a.h <= b.y || // a is above b\n b.y + b.h <= a.y\n ) // b is above a\n );\n}\n\n/**\n * Detect all panels that collide with the given panel\n * 指定されたパネルと衝突する全てのパネルを検出\n */\nexport function detectCollisions(panel: PanelCoordinate, panelMap: Map<PanelId, PanelCoordinate>): PanelId[] {\n const collisions = new Set<PanelId>();\n\n for (const [id, other] of panelMap) {\n if (id === panel.id) continue;\n\n if (rectanglesOverlap(panel, other)) {\n collisions.add(id);\n }\n }\n\n return Array.from(collisions);\n}\n\n/**\n * Check if a panel at the given position would collide with any existing panels\n * 指定された位置にパネルを配置した場合に衝突があるかをチェック\n */\nexport function hasCollision(\n candidate: { x: number; y: number; w: number; h: number },\n excludeId: PanelId,\n panelMap: Map<PanelId, PanelCoordinate>\n): boolean {\n for (const [id, panel] of panelMap) {\n if (id === excludeId) continue;\n if (rectanglesOverlap(candidate, panel)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Calculate the minimum distance to push a panel to avoid collision\n * パネルを押しのけるための最小距離を計算\n */\nfunction calculatePushDistance(\n pusher: PanelCoordinate,\n pushed: PanelCoordinate,\n columnCount: number\n): { direction: \"right\" | \"down\"; distance: number } | null {\n // Calculate how far to push horizontally (to the right)\n // 横方向(右)にどれだけ押すか計算\n const pushRight = pusher.x + pusher.w - pushed.x;\n const canPushRight = pushed.x + pushed.w + pushRight <= columnCount;\n\n // Calculate how far to push vertically (down)\n // 縦方向(下)にどれだけ押すか計算\n const pushDown = pusher.y + pusher.h - pushed.y;\n\n // Priority 1: Horizontal push if possible\n // 優先順位1: 可能なら横方向に押す\n if (canPushRight && pushRight > 0) {\n return { direction: \"right\", distance: pushRight };\n }\n\n // Priority 2: Vertical push\n // 優先順位2: 縦方向に押す\n if (pushDown > 0) {\n return { direction: \"down\", distance: pushDown };\n }\n\n return null;\n}\n\n/**\n * Find a new position for a panel by pushing it away from the colliding panel\n * Priority: horizontal (right) first, then vertical (down)\n * 衝突したパネルを押しのける方向に移動させる\n * 優先順位: 横方向(右)→縦方向(下)\n */\nexport function findNewPosition(\n panel: PanelCoordinate,\n pusher: PanelCoordinate,\n columnCount: number\n): { x: number; y: number } {\n const pushInfo = calculatePushDistance(pusher, panel, columnCount);\n\n if (!pushInfo) {\n // No push needed or can't determine direction\n // Fallback: move down\n return { x: panel.x, y: panel.y + 1 };\n }\n\n if (pushInfo.direction === \"right\") {\n // Push horizontally to the right\n // 横方向(右)に押す\n const newX = panel.x + pushInfo.distance;\n if (newX + panel.w <= columnCount) {\n return { x: newX, y: panel.y };\n }\n // Can't fit horizontally, push down instead\n // 横方向に入らない場合は下に押す\n }\n\n // Push vertically down\n // 縦方向(下)に押す\n return { x: panel.x, y: panel.y + pushInfo.distance };\n}\n\n/**\n * Constrain a panel to stay within grid boundaries\n * パネルをグリッド境界内に制約\n */\nfunction constrainToGrid(panel: PanelCoordinate, columnCount: number): PanelCoordinate {\n // Ensure x + w doesn't exceed columnCount\n // x + w が columnCount を超えないようにする\n const maxX = Math.max(0, columnCount - panel.w);\n const constrainedX = Math.max(0, Math.min(panel.x, maxX));\n\n // Ensure y is non-negative\n // y が負にならないようにする\n const constrainedY = Math.max(0, panel.y);\n\n return {\n ...panel,\n x: constrainedX,\n y: constrainedY,\n };\n}\n\n/**\n * Rearrange panels to resolve collisions when a panel is moved or resized\n * Panels are moved horizontally first, then vertically if needed\n * パネルの移動・リサイズ時に衝突を解決するようにパネルを再配置\n * 横方向を優先し、必要に応じて縦方向に移動\n */\nexport function rearrangePanels(\n movingPanel: PanelCoordinate,\n allPanels: PanelCoordinate[],\n columnCount: number\n): PanelCoordinate[] {\n // Constrain the moving panel to grid boundaries\n // 移動中のパネルをグリッド境界内に制約\n const constrainedMovingPanel = constrainToGrid(movingPanel, columnCount);\n\n // Create a map for fast panel lookup\n // パネルIDから座標への高速マップを作成\n const panelMap = new Map<PanelId, PanelCoordinate>();\n for (const panel of allPanels) {\n panelMap.set(panel.id, { ...panel });\n }\n\n // Update the moving panel's position in the map\n // 移動中のパネルの位置をマップに反映\n panelMap.set(constrainedMovingPanel.id, { ...constrainedMovingPanel });\n\n // Queue for processing panels that need to be repositioned\n // 再配置が必要なパネルの処理キュー\n const queue: PanelCoordinate[] = [{ ...constrainedMovingPanel }];\n const processed = new Set<PanelId>();\n\n // Process panels until no more collisions\n // 衝突がなくなるまでパネルを処理\n while (queue.length > 0) {\n const current = queue.shift()!;\n\n // Skip if already processed\n if (processed.has(current.id)) {\n continue;\n }\n processed.add(current.id);\n\n // Detect collisions with current panel position\n // 現在のパネル位置での衝突を検出\n const collidingIds = detectCollisions(current, panelMap);\n\n if (collidingIds.length === 0) {\n // No collisions, keep current position\n // 衝突なし、現在の位置を維持\n panelMap.set(current.id, current);\n continue;\n }\n\n // Resolve collisions by pushing colliding panels\n // 衝突したパネルを押しのけて衝突を解決\n for (const collidingId of collidingIds) {\n const colliding = panelMap.get(collidingId);\n if (!colliding) continue;\n\n // Find new position by pushing the colliding panel away\n // 衝突したパネルを押しのける方向に移動\n const newPos = findNewPosition(colliding, current, columnCount);\n\n // Update the panel's position\n // パネルの位置を更新\n const updated = { ...colliding, x: newPos.x, y: newPos.y };\n panelMap.set(collidingId, updated);\n\n // Add to queue for further collision checking\n // 再度衝突チェックのためキューに追加\n queue.push(updated);\n }\n\n // Confirm current panel's position\n // 現在のパネルの位置を確定\n panelMap.set(current.id, current);\n }\n\n // Return the rearranged panels\n // 再配置後のパネルを返す\n return Array.from(panelMap.values());\n}\n\n/**\n * Find a new position for a panel to be added\n * 追加するパネルの新しい位置を見つける\n */\nexport function findNewPositionToAddPanel(\n panelToAdd: Partial<PanelCoordinate>,\n allPanels: PanelCoordinate[],\n columnCount: number\n): { x: number; y: number } {\n const id = panelToAdd.id || Math.random().toString(36).substring(2, 15);\n const w = panelToAdd.w || 1;\n const h = panelToAdd.h || 1;\n\n // Create a map for fast panel lookup\n const panelMap = new Map<PanelId, PanelCoordinate>();\n for (const panel of allPanels) {\n panelMap.set(panel.id, panel);\n }\n\n // Calculate maximum row based on existing panels\n // Add some buffer rows to ensure we can find a position\n const maxExistingY = allPanels.length > 0 ? Math.max(...allPanels.map((p) => p.y + p.h)) : 0;\n const MAX_ROWS = Math.max(maxExistingY + 100, 1000);\n\n // Try to find a position starting from top-left\n for (let y = 0; y < MAX_ROWS; y++) {\n for (let x = 0; x <= columnCount - w; x++) {\n const candidate: PanelCoordinate = {\n id,\n x,\n y,\n w,\n h,\n };\n\n // Check if this position has any collisions\n if (!hasCollision(candidate, candidate.id, panelMap)) {\n return { x, y };\n }\n }\n }\n\n // Fallback: if no position found, place at the bottom\n return { x: 0, y: maxExistingY };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rearrangement.cjs","names":["queue: PanelCoordinate[]","candidate: PanelCoordinate"],"sources":["../../src/helpers/rearrangement.ts"],"sourcesContent":["import type { PanelCoordinate, PanelId } from \"../types\";\n\n/**\n * Check if two rectangles overlap using AABB (Axis-Aligned Bounding Box) test\n * 2つの矩形が重なっているかをAABBテストで判定\n */\nexport function rectanglesOverlap(\n a: { x: number; y: number; w: number; h: number },\n b: { x: number; y: number; w: number; h: number }\n): boolean {\n return !(\n (\n a.x + a.w <= b.x || // a is to the left of b\n b.x + b.w <= a.x || // b is to the left of a\n a.y + a.h <= b.y || // a is above b\n b.y + b.h <= a.y\n ) // b is above a\n );\n}\n\n/**\n * Detect all panels that collide with the given panel\n * 指定されたパネルと衝突する全てのパネルを検出\n */\nexport function detectCollisions(panel: PanelCoordinate, panelMap: Map<PanelId, PanelCoordinate>): PanelId[] {\n const collisions = new Set<PanelId>();\n\n for (const [id, other] of panelMap) {\n if (id === panel.id) continue;\n\n if (rectanglesOverlap(panel, other)) {\n collisions.add(id);\n }\n }\n\n return Array.from(collisions);\n}\n\n/**\n * Check if a panel at the given position would collide with any existing panels\n * 指定された位置にパネルを配置した場合に衝突があるかをチェック\n */\nexport function hasCollision(\n candidate: { x: number; y: number; w: number; h: number },\n excludeId: PanelId,\n panelMap: Map<PanelId, PanelCoordinate>\n): boolean {\n for (const [id, panel] of panelMap) {\n if (id === excludeId) continue;\n if (rectanglesOverlap(candidate, panel)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Calculate the minimum distance to push a panel to avoid collision\n * パネルを押しのけるための最小距離を計算\n */\nfunction calculatePushDistance(\n pusher: PanelCoordinate,\n pushed: PanelCoordinate,\n columnCount: number\n): { direction: \"right\" | \"down\"; distance: number } | null {\n // Calculate how far to push horizontally (to the right)\n // 横方向(右)にどれだけ押すか計算\n const pushRight = pusher.x + pusher.w - pushed.x;\n const canPushRight = pushed.x + pushed.w + pushRight <= columnCount;\n\n // Calculate how far to push vertically (down)\n // 縦方向(下)にどれだけ押すか計算\n const pushDown = pusher.y + pusher.h - pushed.y;\n\n // Priority 1: Horizontal push if possible\n // 優先順位1: 可能なら横方向に押す\n if (canPushRight && pushRight > 0) {\n return { direction: \"right\", distance: pushRight };\n }\n\n // Priority 2: Vertical push\n // 優先順位2: 縦方向に押す\n if (pushDown > 0) {\n return { direction: \"down\", distance: pushDown };\n }\n\n return null;\n}\n\n/**\n * Find a new position for a panel by pushing it away from the colliding panel\n * Priority: horizontal (right) first, then vertical (down)\n * 衝突したパネルを押しのける方向に移動させる\n * 優先順位: 横方向(右)→縦方向(下)\n */\nexport function findNewPosition(\n panel: PanelCoordinate,\n pusher: PanelCoordinate,\n columnCount: number\n): { x: number; y: number } {\n const pushInfo = calculatePushDistance(pusher, panel, columnCount);\n\n if (!pushInfo) {\n // No push needed or can't determine direction\n // Fallback: move down\n return { x: panel.x, y: panel.y + 1 };\n }\n\n if (pushInfo.direction === \"right\") {\n // Push horizontally to the right\n // 横方向(右)に押す\n const newX = panel.x + pushInfo.distance;\n if (newX + panel.w <= columnCount) {\n return { x: newX, y: panel.y };\n }\n // Can't fit horizontally, push down instead\n // 横方向に入らない場合は下に押す\n }\n\n // Push vertically down\n // 縦方向(下)に押す\n return { x: panel.x, y: panel.y + pushInfo.distance };\n}\n\n/**\n * Constrain a panel to stay within grid boundaries\n * パネルをグリッド境界内に制約\n */\nfunction constrainToGrid(panel: PanelCoordinate, columnCount: number): PanelCoordinate {\n // Ensure x + w doesn't exceed columnCount\n // x + w が columnCount を超えないようにする\n const maxX = Math.max(0, columnCount - panel.w);\n const constrainedX = Math.max(0, Math.min(panel.x, maxX));\n\n // Ensure y is non-negative\n // y が負にならないようにする\n const constrainedY = Math.max(0, panel.y);\n\n return {\n ...panel,\n x: constrainedX,\n y: constrainedY,\n };\n}\n\n/**\n * Rearrange panels to resolve collisions when a panel is moved or resized\n * Panels are moved horizontally first, then vertically if needed\n * For compound resizes (both width and height change), uses two-phase processing\n * パネルの移動・リサイズ時に衝突を解決するようにパネルを再配置\n * 横方向を優先し、必要に応じて縦方向に移動\n * 幅と高さが同時に変更される場合は、二段階処理を使用\n */\nexport function rearrangePanels(\n movingPanel: PanelCoordinate,\n allPanels: PanelCoordinate[],\n columnCount: number\n): PanelCoordinate[] {\n // Constrain the moving panel to grid boundaries\n // 移動中のパネルをグリッド境界内に制約\n const constrainedMovingPanel = constrainToGrid(movingPanel, columnCount);\n\n // Check if this is a compound resize (both width and height changed)\n // 幅と高さの両方が変更されたかチェック\n const originalPanel = allPanels.find((p) => p.id === movingPanel.id);\n if (originalPanel) {\n const widthChanged = originalPanel.w !== constrainedMovingPanel.w;\n const heightChanged = originalPanel.h !== constrainedMovingPanel.h;\n\n if (widthChanged && heightChanged) {\n // Two-phase processing: width first, then height\n // 二段階処理: 幅を先に、次に高さ\n\n // Phase 1: Apply width change only\n // フェーズ1: 幅の変更のみを適用\n const widthOnlyPanel = {\n ...constrainedMovingPanel,\n h: originalPanel.h, // Keep original height\n };\n const afterWidthChange = rearrangePanelsInternal(widthOnlyPanel, allPanels, columnCount);\n\n // Phase 2: Apply height change to the result\n // フェーズ2: 結果に高さの変更を適用\n const heightChangedPanel = {\n ...constrainedMovingPanel,\n // Use the new position if panel-1 was moved during width phase\n x: afterWidthChange.find((p) => p.id === movingPanel.id)?.x ?? constrainedMovingPanel.x,\n y: afterWidthChange.find((p) => p.id === movingPanel.id)?.y ?? constrainedMovingPanel.y,\n };\n return rearrangePanelsInternal(heightChangedPanel, afterWidthChange, columnCount);\n }\n }\n\n // Single dimension change or move - use normal processing\n // 単一次元の変更または移動 - 通常の処理を使用\n return rearrangePanelsInternal(constrainedMovingPanel, allPanels, columnCount);\n}\n\n/**\n * Internal implementation of panel rearrangement\n * パネル再配置の内部実装\n */\nfunction rearrangePanelsInternal(\n movingPanel: PanelCoordinate,\n allPanels: PanelCoordinate[],\n columnCount: number\n): PanelCoordinate[] {\n // Create a map for fast panel lookup\n // パネルIDから座標への高速マップを作成\n const panelMap = new Map<PanelId, PanelCoordinate>();\n for (const panel of allPanels) {\n panelMap.set(panel.id, { ...panel });\n }\n\n // Update the moving panel's position in the map\n // 移動中のパネルの位置をマップに反映\n panelMap.set(movingPanel.id, { ...movingPanel });\n\n // Queue for processing panels that need to be repositioned\n // 再配置が必要なパネルの処理キュー\n const queue: PanelCoordinate[] = [{ ...movingPanel }];\n\n // Track processed panels to avoid reprocessing unnecessarily\n // 不要な再処理を避けるため処理済みパネルを追跡\n const processed = new Set<PanelId>();\n\n // Track processing count to prevent infinite loops\n // 無限ループを防ぐため処理回数を追跡\n const processCount = new Map<PanelId, number>();\n const MAX_PROCESS_COUNT = 10;\n\n // Process panels until no more collisions\n // 衝突がなくなるまでパネルを処理\n while (queue.length > 0) {\n const current = queue.shift()!;\n\n // Safety check: prevent infinite loops\n // 安全チェック: 無限ループを防止\n const count = processCount.get(current.id) || 0;\n if (count >= MAX_PROCESS_COUNT) continue;\n processCount.set(current.id, count + 1);\n\n // Skip if the position in queue doesn't match current position in map (outdated entry)\n // キューの位置がマップの現在位置と一致しない場合はスキップ(古いエントリ)\n const currentInMap = panelMap.get(current.id);\n if (currentInMap && (currentInMap.x !== current.x || currentInMap.y !== current.y)) {\n continue;\n }\n\n // Skip if already processed at this position\n // この位置で既に処理済みの場合はスキップ\n if (processed.has(current.id)) {\n continue;\n }\n\n // Detect collisions with current panel position\n // 現在のパネル位置での衝突を検出\n const collidingIds = detectCollisions(current, panelMap);\n\n if (collidingIds.length === 0) {\n panelMap.set(current.id, current);\n processed.add(current.id);\n continue;\n }\n\n // Sort colliding panels by position (top-left to bottom-right) for consistent processing\n // 一貫した処理のため、衝突パネルを位置順(左上から右下)にソート\n const sortedCollidingIds = collidingIds.sort((a, b) => {\n const panelA = panelMap.get(a)!;\n const panelB = panelMap.get(b)!;\n if (panelA.y !== panelB.y) return panelA.y - panelB.y;\n return panelA.x - panelB.x;\n });\n\n // Track panels pushed in this iteration to detect secondary collisions\n // この反復で押されたパネルを追跡し、二次衝突を検出\n const pushedInIteration = new Set<PanelId>();\n\n // Resolve collisions by pushing colliding panels\n // 衝突したパネルを押しのけて衝突を解決\n for (const collidingId of sortedCollidingIds) {\n const colliding = panelMap.get(collidingId);\n if (!colliding) continue;\n\n // Calculate new position for the colliding panel\n // 衝突パネルの新しい位置を計算\n const newPos = findNewPosition(colliding, current, columnCount);\n const candidate = { ...colliding, x: newPos.x, y: newPos.y };\n\n // Check if this would collide with a panel we just pushed in this same iteration\n // 同じ反復内で押したパネルと衝突するかチェック\n let wouldCollideWithPushed = false;\n for (const pushedId of pushedInIteration) {\n const pushedPanel = panelMap.get(pushedId)!;\n if (rectanglesOverlap(candidate, pushedPanel)) {\n wouldCollideWithPushed = true;\n break;\n }\n }\n\n if (wouldCollideWithPushed) {\n // Skip pushing this panel to avoid creating secondary collisions\n // Let the queue handle it in a subsequent iteration\n continue;\n }\n\n // Update panel map and add to queue for further processing\n // パネルマップを更新し、さらなる処理のためキューに追加\n panelMap.set(collidingId, candidate);\n queue.push(candidate);\n pushedInIteration.add(collidingId);\n }\n\n panelMap.set(current.id, current);\n processed.add(current.id);\n }\n\n // Return the rearranged panels\n // 再配置後のパネルを返す\n return Array.from(panelMap.values());\n}\n\n/**\n * Find a new position for a panel to be added\n * 追加するパネルの新しい位置を見つける\n */\nexport function findNewPositionToAddPanel(\n panelToAdd: Partial<PanelCoordinate>,\n allPanels: PanelCoordinate[],\n columnCount: number\n): { x: number; y: number } {\n const id = panelToAdd.id || Math.random().toString(36).substring(2, 15);\n const w = panelToAdd.w || 1;\n const h = panelToAdd.h || 1;\n\n // Create a map for fast panel lookup\n const panelMap = new Map<PanelId, PanelCoordinate>();\n for (const panel of allPanels) {\n panelMap.set(panel.id, panel);\n }\n\n // Calculate maximum row based on existing panels\n // Add some buffer rows to ensure we can find a position\n const maxExistingY = allPanels.length > 0 ? Math.max(...allPanels.map((p) => p.y + p.h)) : 0;\n const MAX_ROWS = Math.max(maxExistingY + 100, 1000);\n\n // Try to find a position starting from top-left\n for (let y = 0; y < MAX_ROWS; y++) {\n for (let x = 0; x <= columnCount - w; x++) {\n const candidate: PanelCoordinate = {\n id,\n x,\n y,\n w,\n h,\n };\n\n // Check if this position has any collisions\n if (!hasCollision(candidate, candidate.id, panelMap)) {\n return { x, y };\n }\n }\n }\n\n // Fallback: if no position found, place at the bottom\n return { x: 0, y: maxExistingY };\n}\n"],"mappings":"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"}
@@ -1,14 +1,56 @@
1
- import { PanelCoordinate } from "../types.cjs";
1
+ import { PanelCoordinate, PanelId } from "../types.cjs";
2
2
 
3
3
  //#region src/helpers/rearrangement.d.ts
4
4
 
5
+ /**
6
+ * Check if two rectangles overlap using AABB (Axis-Aligned Bounding Box) test
7
+ * 2つの矩形が重なっているかをAABBテストで判定
8
+ */
9
+ declare function rectanglesOverlap(a: {
10
+ x: number;
11
+ y: number;
12
+ w: number;
13
+ h: number;
14
+ }, b: {
15
+ x: number;
16
+ y: number;
17
+ w: number;
18
+ h: number;
19
+ }): boolean;
20
+ /**
21
+ * Detect all panels that collide with the given panel
22
+ * 指定されたパネルと衝突する全てのパネルを検出
23
+ */
24
+ declare function detectCollisions(panel: PanelCoordinate, panelMap: Map<PanelId, PanelCoordinate>): PanelId[];
25
+ /**
26
+ * Check if a panel at the given position would collide with any existing panels
27
+ * 指定された位置にパネルを配置した場合に衝突があるかをチェック
28
+ */
29
+ declare function hasCollision(candidate: {
30
+ x: number;
31
+ y: number;
32
+ w: number;
33
+ h: number;
34
+ }, excludeId: PanelId, panelMap: Map<PanelId, PanelCoordinate>): boolean;
35
+ /**
36
+ * Find a new position for a panel by pushing it away from the colliding panel
37
+ * Priority: horizontal (right) first, then vertical (down)
38
+ * 衝突したパネルを押しのける方向に移動させる
39
+ * 優先順位: 横方向(右)→縦方向(下)
40
+ */
41
+ declare function findNewPosition(panel: PanelCoordinate, pusher: PanelCoordinate, columnCount: number): {
42
+ x: number;
43
+ y: number;
44
+ };
5
45
  /**
6
46
  * Rearrange panels to resolve collisions when a panel is moved or resized
7
47
  * Panels are moved horizontally first, then vertically if needed
48
+ * For compound resizes (both width and height change), uses two-phase processing
8
49
  * パネルの移動・リサイズ時に衝突を解決するようにパネルを再配置
9
50
  * 横方向を優先し、必要に応じて縦方向に移動
51
+ * 幅と高さが同時に変更される場合は、二段階処理を使用
10
52
  */
11
53
  declare function rearrangePanels(movingPanel: PanelCoordinate, allPanels: PanelCoordinate[], columnCount: number): PanelCoordinate[];
12
54
  //#endregion
13
- export { rearrangePanels };
55
+ export { detectCollisions, findNewPosition, hasCollision, rearrangePanels, rectanglesOverlap };
14
56
  //# sourceMappingURL=rearrangement.d.cts.map
@@ -1,14 +1,56 @@
1
- import { PanelCoordinate } from "../types.mjs";
1
+ import { PanelCoordinate, PanelId } from "../types.mjs";
2
2
 
3
3
  //#region src/helpers/rearrangement.d.ts
4
4
 
5
+ /**
6
+ * Check if two rectangles overlap using AABB (Axis-Aligned Bounding Box) test
7
+ * 2つの矩形が重なっているかをAABBテストで判定
8
+ */
9
+ declare function rectanglesOverlap(a: {
10
+ x: number;
11
+ y: number;
12
+ w: number;
13
+ h: number;
14
+ }, b: {
15
+ x: number;
16
+ y: number;
17
+ w: number;
18
+ h: number;
19
+ }): boolean;
20
+ /**
21
+ * Detect all panels that collide with the given panel
22
+ * 指定されたパネルと衝突する全てのパネルを検出
23
+ */
24
+ declare function detectCollisions(panel: PanelCoordinate, panelMap: Map<PanelId, PanelCoordinate>): PanelId[];
25
+ /**
26
+ * Check if a panel at the given position would collide with any existing panels
27
+ * 指定された位置にパネルを配置した場合に衝突があるかをチェック
28
+ */
29
+ declare function hasCollision(candidate: {
30
+ x: number;
31
+ y: number;
32
+ w: number;
33
+ h: number;
34
+ }, excludeId: PanelId, panelMap: Map<PanelId, PanelCoordinate>): boolean;
35
+ /**
36
+ * Find a new position for a panel by pushing it away from the colliding panel
37
+ * Priority: horizontal (right) first, then vertical (down)
38
+ * 衝突したパネルを押しのける方向に移動させる
39
+ * 優先順位: 横方向(右)→縦方向(下)
40
+ */
41
+ declare function findNewPosition(panel: PanelCoordinate, pusher: PanelCoordinate, columnCount: number): {
42
+ x: number;
43
+ y: number;
44
+ };
5
45
  /**
6
46
  * Rearrange panels to resolve collisions when a panel is moved or resized
7
47
  * Panels are moved horizontally first, then vertically if needed
48
+ * For compound resizes (both width and height change), uses two-phase processing
8
49
  * パネルの移動・リサイズ時に衝突を解決するようにパネルを再配置
9
50
  * 横方向を優先し、必要に応じて縦方向に移動
51
+ * 幅と高さが同時に変更される場合は、二段階処理を使用
10
52
  */
11
53
  declare function rearrangePanels(movingPanel: PanelCoordinate, allPanels: PanelCoordinate[], columnCount: number): PanelCoordinate[];
12
54
  //#endregion
13
- export { rearrangePanels };
55
+ export { detectCollisions, findNewPosition, hasCollision, rearrangePanels, rectanglesOverlap };
14
56
  //# sourceMappingURL=rearrangement.d.mts.map
@@ -88,38 +88,85 @@ function constrainToGrid(panel, columnCount) {
88
88
  /**
89
89
  * Rearrange panels to resolve collisions when a panel is moved or resized
90
90
  * Panels are moved horizontally first, then vertically if needed
91
+ * For compound resizes (both width and height change), uses two-phase processing
91
92
  * パネルの移動・リサイズ時に衝突を解決するようにパネルを再配置
92
93
  * 横方向を優先し、必要に応じて縦方向に移動
94
+ * 幅と高さが同時に変更される場合は、二段階処理を使用
93
95
  */
94
96
  function rearrangePanels(movingPanel, allPanels, columnCount) {
95
97
  const constrainedMovingPanel = constrainToGrid(movingPanel, columnCount);
98
+ const originalPanel = allPanels.find((p) => p.id === movingPanel.id);
99
+ if (originalPanel) {
100
+ const widthChanged = originalPanel.w !== constrainedMovingPanel.w;
101
+ const heightChanged = originalPanel.h !== constrainedMovingPanel.h;
102
+ if (widthChanged && heightChanged) {
103
+ const afterWidthChange = rearrangePanelsInternal({
104
+ ...constrainedMovingPanel,
105
+ h: originalPanel.h
106
+ }, allPanels, columnCount);
107
+ return rearrangePanelsInternal({
108
+ ...constrainedMovingPanel,
109
+ x: afterWidthChange.find((p) => p.id === movingPanel.id)?.x ?? constrainedMovingPanel.x,
110
+ y: afterWidthChange.find((p) => p.id === movingPanel.id)?.y ?? constrainedMovingPanel.y
111
+ }, afterWidthChange, columnCount);
112
+ }
113
+ }
114
+ return rearrangePanelsInternal(constrainedMovingPanel, allPanels, columnCount);
115
+ }
116
+ /**
117
+ * Internal implementation of panel rearrangement
118
+ * パネル再配置の内部実装
119
+ */
120
+ function rearrangePanelsInternal(movingPanel, allPanels, columnCount) {
96
121
  const panelMap = /* @__PURE__ */ new Map();
97
122
  for (const panel of allPanels) panelMap.set(panel.id, { ...panel });
98
- panelMap.set(constrainedMovingPanel.id, { ...constrainedMovingPanel });
99
- const queue = [{ ...constrainedMovingPanel }];
123
+ panelMap.set(movingPanel.id, { ...movingPanel });
124
+ const queue = [{ ...movingPanel }];
100
125
  const processed = /* @__PURE__ */ new Set();
126
+ const processCount = /* @__PURE__ */ new Map();
127
+ const MAX_PROCESS_COUNT = 10;
101
128
  while (queue.length > 0) {
102
129
  const current = queue.shift();
130
+ const count = processCount.get(current.id) || 0;
131
+ if (count >= MAX_PROCESS_COUNT) continue;
132
+ processCount.set(current.id, count + 1);
133
+ const currentInMap = panelMap.get(current.id);
134
+ if (currentInMap && (currentInMap.x !== current.x || currentInMap.y !== current.y)) continue;
103
135
  if (processed.has(current.id)) continue;
104
- processed.add(current.id);
105
136
  const collidingIds = detectCollisions(current, panelMap);
106
137
  if (collidingIds.length === 0) {
107
138
  panelMap.set(current.id, current);
139
+ processed.add(current.id);
108
140
  continue;
109
141
  }
110
- for (const collidingId of collidingIds) {
142
+ const sortedCollidingIds = collidingIds.sort((a, b) => {
143
+ const panelA = panelMap.get(a);
144
+ const panelB = panelMap.get(b);
145
+ if (panelA.y !== panelB.y) return panelA.y - panelB.y;
146
+ return panelA.x - panelB.x;
147
+ });
148
+ const pushedInIteration = /* @__PURE__ */ new Set();
149
+ for (const collidingId of sortedCollidingIds) {
111
150
  const colliding = panelMap.get(collidingId);
112
151
  if (!colliding) continue;
113
152
  const newPos = findNewPosition(colliding, current, columnCount);
114
- const updated = {
153
+ const candidate = {
115
154
  ...colliding,
116
155
  x: newPos.x,
117
156
  y: newPos.y
118
157
  };
119
- panelMap.set(collidingId, updated);
120
- queue.push(updated);
158
+ let wouldCollideWithPushed = false;
159
+ for (const pushedId of pushedInIteration) if (rectanglesOverlap(candidate, panelMap.get(pushedId))) {
160
+ wouldCollideWithPushed = true;
161
+ break;
162
+ }
163
+ if (wouldCollideWithPushed) continue;
164
+ panelMap.set(collidingId, candidate);
165
+ queue.push(candidate);
166
+ pushedInIteration.add(collidingId);
121
167
  }
122
168
  panelMap.set(current.id, current);
169
+ processed.add(current.id);
123
170
  }
124
171
  return Array.from(panelMap.values());
125
172
  }
@@ -155,5 +202,5 @@ function findNewPositionToAddPanel(panelToAdd, allPanels, columnCount) {
155
202
  }
156
203
 
157
204
  //#endregion
158
- export { findNewPositionToAddPanel, rearrangePanels };
205
+ export { detectCollisions, findNewPosition, findNewPositionToAddPanel, hasCollision, rearrangePanels, rectanglesOverlap };
159
206
  //# sourceMappingURL=rearrangement.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"rearrangement.mjs","names":["queue: PanelCoordinate[]","candidate: PanelCoordinate"],"sources":["../../src/helpers/rearrangement.ts"],"sourcesContent":["import type { PanelCoordinate, PanelId } from \"../types\";\n\n/**\n * Check if two rectangles overlap using AABB (Axis-Aligned Bounding Box) test\n * 2つの矩形が重なっているかをAABBテストで判定\n */\nexport function rectanglesOverlap(\n a: { x: number; y: number; w: number; h: number },\n b: { x: number; y: number; w: number; h: number }\n): boolean {\n return !(\n (\n a.x + a.w <= b.x || // a is to the left of b\n b.x + b.w <= a.x || // b is to the left of a\n a.y + a.h <= b.y || // a is above b\n b.y + b.h <= a.y\n ) // b is above a\n );\n}\n\n/**\n * Detect all panels that collide with the given panel\n * 指定されたパネルと衝突する全てのパネルを検出\n */\nexport function detectCollisions(panel: PanelCoordinate, panelMap: Map<PanelId, PanelCoordinate>): PanelId[] {\n const collisions = new Set<PanelId>();\n\n for (const [id, other] of panelMap) {\n if (id === panel.id) continue;\n\n if (rectanglesOverlap(panel, other)) {\n collisions.add(id);\n }\n }\n\n return Array.from(collisions);\n}\n\n/**\n * Check if a panel at the given position would collide with any existing panels\n * 指定された位置にパネルを配置した場合に衝突があるかをチェック\n */\nexport function hasCollision(\n candidate: { x: number; y: number; w: number; h: number },\n excludeId: PanelId,\n panelMap: Map<PanelId, PanelCoordinate>\n): boolean {\n for (const [id, panel] of panelMap) {\n if (id === excludeId) continue;\n if (rectanglesOverlap(candidate, panel)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Calculate the minimum distance to push a panel to avoid collision\n * パネルを押しのけるための最小距離を計算\n */\nfunction calculatePushDistance(\n pusher: PanelCoordinate,\n pushed: PanelCoordinate,\n columnCount: number\n): { direction: \"right\" | \"down\"; distance: number } | null {\n // Calculate how far to push horizontally (to the right)\n // 横方向(右)にどれだけ押すか計算\n const pushRight = pusher.x + pusher.w - pushed.x;\n const canPushRight = pushed.x + pushed.w + pushRight <= columnCount;\n\n // Calculate how far to push vertically (down)\n // 縦方向(下)にどれだけ押すか計算\n const pushDown = pusher.y + pusher.h - pushed.y;\n\n // Priority 1: Horizontal push if possible\n // 優先順位1: 可能なら横方向に押す\n if (canPushRight && pushRight > 0) {\n return { direction: \"right\", distance: pushRight };\n }\n\n // Priority 2: Vertical push\n // 優先順位2: 縦方向に押す\n if (pushDown > 0) {\n return { direction: \"down\", distance: pushDown };\n }\n\n return null;\n}\n\n/**\n * Find a new position for a panel by pushing it away from the colliding panel\n * Priority: horizontal (right) first, then vertical (down)\n * 衝突したパネルを押しのける方向に移動させる\n * 優先順位: 横方向(右)→縦方向(下)\n */\nexport function findNewPosition(\n panel: PanelCoordinate,\n pusher: PanelCoordinate,\n columnCount: number\n): { x: number; y: number } {\n const pushInfo = calculatePushDistance(pusher, panel, columnCount);\n\n if (!pushInfo) {\n // No push needed or can't determine direction\n // Fallback: move down\n return { x: panel.x, y: panel.y + 1 };\n }\n\n if (pushInfo.direction === \"right\") {\n // Push horizontally to the right\n // 横方向(右)に押す\n const newX = panel.x + pushInfo.distance;\n if (newX + panel.w <= columnCount) {\n return { x: newX, y: panel.y };\n }\n // Can't fit horizontally, push down instead\n // 横方向に入らない場合は下に押す\n }\n\n // Push vertically down\n // 縦方向(下)に押す\n return { x: panel.x, y: panel.y + pushInfo.distance };\n}\n\n/**\n * Constrain a panel to stay within grid boundaries\n * パネルをグリッド境界内に制約\n */\nfunction constrainToGrid(panel: PanelCoordinate, columnCount: number): PanelCoordinate {\n // Ensure x + w doesn't exceed columnCount\n // x + w が columnCount を超えないようにする\n const maxX = Math.max(0, columnCount - panel.w);\n const constrainedX = Math.max(0, Math.min(panel.x, maxX));\n\n // Ensure y is non-negative\n // y が負にならないようにする\n const constrainedY = Math.max(0, panel.y);\n\n return {\n ...panel,\n x: constrainedX,\n y: constrainedY,\n };\n}\n\n/**\n * Rearrange panels to resolve collisions when a panel is moved or resized\n * Panels are moved horizontally first, then vertically if needed\n * パネルの移動・リサイズ時に衝突を解決するようにパネルを再配置\n * 横方向を優先し、必要に応じて縦方向に移動\n */\nexport function rearrangePanels(\n movingPanel: PanelCoordinate,\n allPanels: PanelCoordinate[],\n columnCount: number\n): PanelCoordinate[] {\n // Constrain the moving panel to grid boundaries\n // 移動中のパネルをグリッド境界内に制約\n const constrainedMovingPanel = constrainToGrid(movingPanel, columnCount);\n\n // Create a map for fast panel lookup\n // パネルIDから座標への高速マップを作成\n const panelMap = new Map<PanelId, PanelCoordinate>();\n for (const panel of allPanels) {\n panelMap.set(panel.id, { ...panel });\n }\n\n // Update the moving panel's position in the map\n // 移動中のパネルの位置をマップに反映\n panelMap.set(constrainedMovingPanel.id, { ...constrainedMovingPanel });\n\n // Queue for processing panels that need to be repositioned\n // 再配置が必要なパネルの処理キュー\n const queue: PanelCoordinate[] = [{ ...constrainedMovingPanel }];\n const processed = new Set<PanelId>();\n\n // Process panels until no more collisions\n // 衝突がなくなるまでパネルを処理\n while (queue.length > 0) {\n const current = queue.shift()!;\n\n // Skip if already processed\n if (processed.has(current.id)) {\n continue;\n }\n processed.add(current.id);\n\n // Detect collisions with current panel position\n // 現在のパネル位置での衝突を検出\n const collidingIds = detectCollisions(current, panelMap);\n\n if (collidingIds.length === 0) {\n // No collisions, keep current position\n // 衝突なし、現在の位置を維持\n panelMap.set(current.id, current);\n continue;\n }\n\n // Resolve collisions by pushing colliding panels\n // 衝突したパネルを押しのけて衝突を解決\n for (const collidingId of collidingIds) {\n const colliding = panelMap.get(collidingId);\n if (!colliding) continue;\n\n // Find new position by pushing the colliding panel away\n // 衝突したパネルを押しのける方向に移動\n const newPos = findNewPosition(colliding, current, columnCount);\n\n // Update the panel's position\n // パネルの位置を更新\n const updated = { ...colliding, x: newPos.x, y: newPos.y };\n panelMap.set(collidingId, updated);\n\n // Add to queue for further collision checking\n // 再度衝突チェックのためキューに追加\n queue.push(updated);\n }\n\n // Confirm current panel's position\n // 現在のパネルの位置を確定\n panelMap.set(current.id, current);\n }\n\n // Return the rearranged panels\n // 再配置後のパネルを返す\n return Array.from(panelMap.values());\n}\n\n/**\n * Find a new position for a panel to be added\n * 追加するパネルの新しい位置を見つける\n */\nexport function findNewPositionToAddPanel(\n panelToAdd: Partial<PanelCoordinate>,\n allPanels: PanelCoordinate[],\n columnCount: number\n): { x: number; y: number } {\n const id = panelToAdd.id || Math.random().toString(36).substring(2, 15);\n const w = panelToAdd.w || 1;\n const h = panelToAdd.h || 1;\n\n // Create a map for fast panel lookup\n const panelMap = new Map<PanelId, PanelCoordinate>();\n for (const panel of allPanels) {\n panelMap.set(panel.id, panel);\n }\n\n // Calculate maximum row based on existing panels\n // Add some buffer rows to ensure we can find a position\n const maxExistingY = allPanels.length > 0 ? Math.max(...allPanels.map((p) => p.y + p.h)) : 0;\n const MAX_ROWS = Math.max(maxExistingY + 100, 1000);\n\n // Try to find a position starting from top-left\n for (let y = 0; y < MAX_ROWS; y++) {\n for (let x = 0; x <= columnCount - w; x++) {\n const candidate: PanelCoordinate = {\n id,\n x,\n y,\n w,\n h,\n };\n\n // Check if this position has any collisions\n if (!hasCollision(candidate, candidate.id, panelMap)) {\n return { x, y };\n }\n }\n }\n\n // Fallback: if no position found, place at the bottom\n return { x: 0, y: maxExistingY };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rearrangement.mjs","names":["queue: PanelCoordinate[]","candidate: PanelCoordinate"],"sources":["../../src/helpers/rearrangement.ts"],"sourcesContent":["import type { PanelCoordinate, PanelId } from \"../types\";\n\n/**\n * Check if two rectangles overlap using AABB (Axis-Aligned Bounding Box) test\n * 2つの矩形が重なっているかをAABBテストで判定\n */\nexport function rectanglesOverlap(\n a: { x: number; y: number; w: number; h: number },\n b: { x: number; y: number; w: number; h: number }\n): boolean {\n return !(\n (\n a.x + a.w <= b.x || // a is to the left of b\n b.x + b.w <= a.x || // b is to the left of a\n a.y + a.h <= b.y || // a is above b\n b.y + b.h <= a.y\n ) // b is above a\n );\n}\n\n/**\n * Detect all panels that collide with the given panel\n * 指定されたパネルと衝突する全てのパネルを検出\n */\nexport function detectCollisions(panel: PanelCoordinate, panelMap: Map<PanelId, PanelCoordinate>): PanelId[] {\n const collisions = new Set<PanelId>();\n\n for (const [id, other] of panelMap) {\n if (id === panel.id) continue;\n\n if (rectanglesOverlap(panel, other)) {\n collisions.add(id);\n }\n }\n\n return Array.from(collisions);\n}\n\n/**\n * Check if a panel at the given position would collide with any existing panels\n * 指定された位置にパネルを配置した場合に衝突があるかをチェック\n */\nexport function hasCollision(\n candidate: { x: number; y: number; w: number; h: number },\n excludeId: PanelId,\n panelMap: Map<PanelId, PanelCoordinate>\n): boolean {\n for (const [id, panel] of panelMap) {\n if (id === excludeId) continue;\n if (rectanglesOverlap(candidate, panel)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Calculate the minimum distance to push a panel to avoid collision\n * パネルを押しのけるための最小距離を計算\n */\nfunction calculatePushDistance(\n pusher: PanelCoordinate,\n pushed: PanelCoordinate,\n columnCount: number\n): { direction: \"right\" | \"down\"; distance: number } | null {\n // Calculate how far to push horizontally (to the right)\n // 横方向(右)にどれだけ押すか計算\n const pushRight = pusher.x + pusher.w - pushed.x;\n const canPushRight = pushed.x + pushed.w + pushRight <= columnCount;\n\n // Calculate how far to push vertically (down)\n // 縦方向(下)にどれだけ押すか計算\n const pushDown = pusher.y + pusher.h - pushed.y;\n\n // Priority 1: Horizontal push if possible\n // 優先順位1: 可能なら横方向に押す\n if (canPushRight && pushRight > 0) {\n return { direction: \"right\", distance: pushRight };\n }\n\n // Priority 2: Vertical push\n // 優先順位2: 縦方向に押す\n if (pushDown > 0) {\n return { direction: \"down\", distance: pushDown };\n }\n\n return null;\n}\n\n/**\n * Find a new position for a panel by pushing it away from the colliding panel\n * Priority: horizontal (right) first, then vertical (down)\n * 衝突したパネルを押しのける方向に移動させる\n * 優先順位: 横方向(右)→縦方向(下)\n */\nexport function findNewPosition(\n panel: PanelCoordinate,\n pusher: PanelCoordinate,\n columnCount: number\n): { x: number; y: number } {\n const pushInfo = calculatePushDistance(pusher, panel, columnCount);\n\n if (!pushInfo) {\n // No push needed or can't determine direction\n // Fallback: move down\n return { x: panel.x, y: panel.y + 1 };\n }\n\n if (pushInfo.direction === \"right\") {\n // Push horizontally to the right\n // 横方向(右)に押す\n const newX = panel.x + pushInfo.distance;\n if (newX + panel.w <= columnCount) {\n return { x: newX, y: panel.y };\n }\n // Can't fit horizontally, push down instead\n // 横方向に入らない場合は下に押す\n }\n\n // Push vertically down\n // 縦方向(下)に押す\n return { x: panel.x, y: panel.y + pushInfo.distance };\n}\n\n/**\n * Constrain a panel to stay within grid boundaries\n * パネルをグリッド境界内に制約\n */\nfunction constrainToGrid(panel: PanelCoordinate, columnCount: number): PanelCoordinate {\n // Ensure x + w doesn't exceed columnCount\n // x + w が columnCount を超えないようにする\n const maxX = Math.max(0, columnCount - panel.w);\n const constrainedX = Math.max(0, Math.min(panel.x, maxX));\n\n // Ensure y is non-negative\n // y が負にならないようにする\n const constrainedY = Math.max(0, panel.y);\n\n return {\n ...panel,\n x: constrainedX,\n y: constrainedY,\n };\n}\n\n/**\n * Rearrange panels to resolve collisions when a panel is moved or resized\n * Panels are moved horizontally first, then vertically if needed\n * For compound resizes (both width and height change), uses two-phase processing\n * パネルの移動・リサイズ時に衝突を解決するようにパネルを再配置\n * 横方向を優先し、必要に応じて縦方向に移動\n * 幅と高さが同時に変更される場合は、二段階処理を使用\n */\nexport function rearrangePanels(\n movingPanel: PanelCoordinate,\n allPanels: PanelCoordinate[],\n columnCount: number\n): PanelCoordinate[] {\n // Constrain the moving panel to grid boundaries\n // 移動中のパネルをグリッド境界内に制約\n const constrainedMovingPanel = constrainToGrid(movingPanel, columnCount);\n\n // Check if this is a compound resize (both width and height changed)\n // 幅と高さの両方が変更されたかチェック\n const originalPanel = allPanels.find((p) => p.id === movingPanel.id);\n if (originalPanel) {\n const widthChanged = originalPanel.w !== constrainedMovingPanel.w;\n const heightChanged = originalPanel.h !== constrainedMovingPanel.h;\n\n if (widthChanged && heightChanged) {\n // Two-phase processing: width first, then height\n // 二段階処理: 幅を先に、次に高さ\n\n // Phase 1: Apply width change only\n // フェーズ1: 幅の変更のみを適用\n const widthOnlyPanel = {\n ...constrainedMovingPanel,\n h: originalPanel.h, // Keep original height\n };\n const afterWidthChange = rearrangePanelsInternal(widthOnlyPanel, allPanels, columnCount);\n\n // Phase 2: Apply height change to the result\n // フェーズ2: 結果に高さの変更を適用\n const heightChangedPanel = {\n ...constrainedMovingPanel,\n // Use the new position if panel-1 was moved during width phase\n x: afterWidthChange.find((p) => p.id === movingPanel.id)?.x ?? constrainedMovingPanel.x,\n y: afterWidthChange.find((p) => p.id === movingPanel.id)?.y ?? constrainedMovingPanel.y,\n };\n return rearrangePanelsInternal(heightChangedPanel, afterWidthChange, columnCount);\n }\n }\n\n // Single dimension change or move - use normal processing\n // 単一次元の変更または移動 - 通常の処理を使用\n return rearrangePanelsInternal(constrainedMovingPanel, allPanels, columnCount);\n}\n\n/**\n * Internal implementation of panel rearrangement\n * パネル再配置の内部実装\n */\nfunction rearrangePanelsInternal(\n movingPanel: PanelCoordinate,\n allPanels: PanelCoordinate[],\n columnCount: number\n): PanelCoordinate[] {\n // Create a map for fast panel lookup\n // パネルIDから座標への高速マップを作成\n const panelMap = new Map<PanelId, PanelCoordinate>();\n for (const panel of allPanels) {\n panelMap.set(panel.id, { ...panel });\n }\n\n // Update the moving panel's position in the map\n // 移動中のパネルの位置をマップに反映\n panelMap.set(movingPanel.id, { ...movingPanel });\n\n // Queue for processing panels that need to be repositioned\n // 再配置が必要なパネルの処理キュー\n const queue: PanelCoordinate[] = [{ ...movingPanel }];\n\n // Track processed panels to avoid reprocessing unnecessarily\n // 不要な再処理を避けるため処理済みパネルを追跡\n const processed = new Set<PanelId>();\n\n // Track processing count to prevent infinite loops\n // 無限ループを防ぐため処理回数を追跡\n const processCount = new Map<PanelId, number>();\n const MAX_PROCESS_COUNT = 10;\n\n // Process panels until no more collisions\n // 衝突がなくなるまでパネルを処理\n while (queue.length > 0) {\n const current = queue.shift()!;\n\n // Safety check: prevent infinite loops\n // 安全チェック: 無限ループを防止\n const count = processCount.get(current.id) || 0;\n if (count >= MAX_PROCESS_COUNT) continue;\n processCount.set(current.id, count + 1);\n\n // Skip if the position in queue doesn't match current position in map (outdated entry)\n // キューの位置がマップの現在位置と一致しない場合はスキップ(古いエントリ)\n const currentInMap = panelMap.get(current.id);\n if (currentInMap && (currentInMap.x !== current.x || currentInMap.y !== current.y)) {\n continue;\n }\n\n // Skip if already processed at this position\n // この位置で既に処理済みの場合はスキップ\n if (processed.has(current.id)) {\n continue;\n }\n\n // Detect collisions with current panel position\n // 現在のパネル位置での衝突を検出\n const collidingIds = detectCollisions(current, panelMap);\n\n if (collidingIds.length === 0) {\n panelMap.set(current.id, current);\n processed.add(current.id);\n continue;\n }\n\n // Sort colliding panels by position (top-left to bottom-right) for consistent processing\n // 一貫した処理のため、衝突パネルを位置順(左上から右下)にソート\n const sortedCollidingIds = collidingIds.sort((a, b) => {\n const panelA = panelMap.get(a)!;\n const panelB = panelMap.get(b)!;\n if (panelA.y !== panelB.y) return panelA.y - panelB.y;\n return panelA.x - panelB.x;\n });\n\n // Track panels pushed in this iteration to detect secondary collisions\n // この反復で押されたパネルを追跡し、二次衝突を検出\n const pushedInIteration = new Set<PanelId>();\n\n // Resolve collisions by pushing colliding panels\n // 衝突したパネルを押しのけて衝突を解決\n for (const collidingId of sortedCollidingIds) {\n const colliding = panelMap.get(collidingId);\n if (!colliding) continue;\n\n // Calculate new position for the colliding panel\n // 衝突パネルの新しい位置を計算\n const newPos = findNewPosition(colliding, current, columnCount);\n const candidate = { ...colliding, x: newPos.x, y: newPos.y };\n\n // Check if this would collide with a panel we just pushed in this same iteration\n // 同じ反復内で押したパネルと衝突するかチェック\n let wouldCollideWithPushed = false;\n for (const pushedId of pushedInIteration) {\n const pushedPanel = panelMap.get(pushedId)!;\n if (rectanglesOverlap(candidate, pushedPanel)) {\n wouldCollideWithPushed = true;\n break;\n }\n }\n\n if (wouldCollideWithPushed) {\n // Skip pushing this panel to avoid creating secondary collisions\n // Let the queue handle it in a subsequent iteration\n continue;\n }\n\n // Update panel map and add to queue for further processing\n // パネルマップを更新し、さらなる処理のためキューに追加\n panelMap.set(collidingId, candidate);\n queue.push(candidate);\n pushedInIteration.add(collidingId);\n }\n\n panelMap.set(current.id, current);\n processed.add(current.id);\n }\n\n // Return the rearranged panels\n // 再配置後のパネルを返す\n return Array.from(panelMap.values());\n}\n\n/**\n * Find a new position for a panel to be added\n * 追加するパネルの新しい位置を見つける\n */\nexport function findNewPositionToAddPanel(\n panelToAdd: Partial<PanelCoordinate>,\n allPanels: PanelCoordinate[],\n columnCount: number\n): { x: number; y: number } {\n const id = panelToAdd.id || Math.random().toString(36).substring(2, 15);\n const w = panelToAdd.w || 1;\n const h = panelToAdd.h || 1;\n\n // Create a map for fast panel lookup\n const panelMap = new Map<PanelId, PanelCoordinate>();\n for (const panel of allPanels) {\n panelMap.set(panel.id, panel);\n }\n\n // Calculate maximum row based on existing panels\n // Add some buffer rows to ensure we can find a position\n const maxExistingY = allPanels.length > 0 ? Math.max(...allPanels.map((p) => p.y + p.h)) : 0;\n const MAX_ROWS = Math.max(maxExistingY + 100, 1000);\n\n // Try to find a position starting from top-left\n for (let y = 0; y < MAX_ROWS; y++) {\n for (let x = 0; x <= columnCount - w; x++) {\n const candidate: PanelCoordinate = {\n id,\n x,\n y,\n w,\n h,\n };\n\n // Check if this position has any collisions\n if (!hasCollision(candidate, candidate.id, panelMap)) {\n return { x, y };\n }\n }\n }\n\n // Fallback: if no position found, place at the bottom\n return { x: 0, y: maxExistingY };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panelgrid",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "A flexible and performant React grid layout library with drag-and-drop and resize capabilities",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -19,6 +19,16 @@
19
19
  },
20
20
  "default": "./dist/index.mjs"
21
21
  },
22
+ "./helpers": {
23
+ "import": {
24
+ "types": "./dist/helpers/index.d.mts",
25
+ "default": "./dist/helpers/index.mjs"
26
+ },
27
+ "require": {
28
+ "types": "./dist/helpers/index.d.cts",
29
+ "default": "./dist/helpers/index.cjs"
30
+ }
31
+ },
22
32
  "./styles.css": "./dist/styles.css",
23
33
  "./package.json": "./package.json"
24
34
  },
@@ -62,7 +72,12 @@
62
72
  "typecheck": "tsc -b --noEmit",
63
73
  "preview": "vite preview",
64
74
  "test": "vitest",
65
- "prepublishOnly": "yarn test run && yarn typecheck && yarn lint && yarn build"
75
+ "prepublishOnly": "yarn test run && yarn typecheck && yarn lint && yarn build",
76
+ "release": "commit-and-tag-version",
77
+ "release:patch": "commit-and-tag-version --release-as patch",
78
+ "release:minor": "commit-and-tag-version --release-as minor",
79
+ "release:major": "commit-and-tag-version --release-as major",
80
+ "release:dry-run": "commit-and-tag-version --dry-run"
66
81
  },
67
82
  "lint-staged": {
68
83
  "*.{js,jsx,ts,tsx}": [
@@ -86,6 +101,7 @@
86
101
  "@types/react": "^19.1.16",
87
102
  "@types/react-dom": "^19.1.9",
88
103
  "@vitejs/plugin-react-swc": "^4.1.0",
104
+ "commit-and-tag-version": "^12.6.1",
89
105
  "husky": "^9.1.7",
90
106
  "jsdom": "^27.0.1",
91
107
  "lint-staged": "^16.2.6",