petersburg 0.0.1
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/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/index.d.mts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +363 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +336 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xfaSts9cwY6VqLNTMAtR
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Petersburg
|
|
2
|
+
|
|
3
|
+
A React layout library implementing Hermitage-style 2D bin-packing for picture arrangement.
|
|
4
|
+
|
|
5
|
+
Named after the Hermitage Museum in St. Petersburg, where curators arrange paintings tetris-style to maximize limited wall space.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **MaxRects bin-packing algorithm** — efficient 2D rectangle packing
|
|
10
|
+
- **Multiple sort strategies** — optimize for packing efficiency or preserve input order
|
|
11
|
+
- **Responsive** — auto-measures container and recalculates on resize
|
|
12
|
+
- **Accessible** — DOM order matches visual flow for proper tab navigation
|
|
13
|
+
- **Lightweight** — no dependencies beyond React
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install petersburg
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Basic Usage
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { HermitageLayout } from 'petersburg';
|
|
25
|
+
|
|
26
|
+
const items = [
|
|
27
|
+
{ id: '1', width: 200, height: 150, content: <img src="..." /> },
|
|
28
|
+
{ id: '2', width: 100, height: 100, content: <img src="..." /> },
|
|
29
|
+
{ id: '3', width: 150, height: 200, content: <img src="..." /> },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function Gallery() {
|
|
33
|
+
return (
|
|
34
|
+
<HermitageLayout
|
|
35
|
+
items={items}
|
|
36
|
+
gap={8}
|
|
37
|
+
sortStrategy="ordered"
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Props
|
|
44
|
+
|
|
45
|
+
| Prop | Type | Default | Description |
|
|
46
|
+
|------|------|---------|-------------|
|
|
47
|
+
| `items` | `LayoutItem[]` | required | Array of items to layout |
|
|
48
|
+
| `containerWidth` | `number` | auto | Fixed width in pixels. If omitted, measures parent container. |
|
|
49
|
+
| `gap` | `number` | `8` | Gap between items in pixels |
|
|
50
|
+
| `sortStrategy` | `SortStrategy` | `"none"` | How to order items during packing |
|
|
51
|
+
| `className` | `string` | — | CSS class for the container |
|
|
52
|
+
|
|
53
|
+
## Types
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
interface LayoutItem {
|
|
57
|
+
id: string;
|
|
58
|
+
width: number;
|
|
59
|
+
height: number;
|
|
60
|
+
content: ReactNode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type SortStrategy =
|
|
64
|
+
| "none" // Keep input order
|
|
65
|
+
| "height-desc" // Sort by height descending (best packing)
|
|
66
|
+
| "ordered"; // Row 1 strict order, row 2+ flexible
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Sort Strategies
|
|
70
|
+
|
|
71
|
+
### `none`
|
|
72
|
+
Items are placed in input order using the MaxRects algorithm. Good for when your input is already sorted (e.g., by date) and you want to preserve that order as much as possible.
|
|
73
|
+
|
|
74
|
+
### `height-desc`
|
|
75
|
+
Items are sorted by height (tallest first) before packing. This typically produces the most compact layout with minimal wasted space.
|
|
76
|
+
|
|
77
|
+
### `ordered`
|
|
78
|
+
A hybrid approach for galleries where order matters at the top but efficiency matters overall:
|
|
79
|
+
- **Row 1**: Items placed strictly left-to-right in input order
|
|
80
|
+
- **Row 2**: Next batch of items, can be reordered within the row to fill gaps
|
|
81
|
+
- **Row 3+**: Full algorithmic freedom for optimal packing
|
|
82
|
+
|
|
83
|
+
This is ideal for "newest items at top" layouts where the first row should show items 1, 2, 3... in order, but lower rows can be optimized.
|
|
84
|
+
|
|
85
|
+
## Responsive Layouts
|
|
86
|
+
|
|
87
|
+
Omit `containerWidth` to enable responsive mode:
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
<div style={{ width: '100%' }}>
|
|
91
|
+
<HermitageLayout items={items} gap={8} />
|
|
92
|
+
</div>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The component will measure its parent container and recalculate the layout when the container resizes.
|
|
96
|
+
|
|
97
|
+
## Fixed Width
|
|
98
|
+
|
|
99
|
+
For fixed-width layouts, provide `containerWidth`:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
<HermitageLayout
|
|
103
|
+
items={items}
|
|
104
|
+
containerWidth={800}
|
|
105
|
+
gap={8}
|
|
106
|
+
/>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Styling Items
|
|
110
|
+
|
|
111
|
+
Each item is rendered in an absolutely-positioned wrapper. Style your content to fill it:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
const items = [
|
|
115
|
+
{
|
|
116
|
+
id: '1',
|
|
117
|
+
width: 200,
|
|
118
|
+
height: 150,
|
|
119
|
+
content: (
|
|
120
|
+
<img
|
|
121
|
+
src="..."
|
|
122
|
+
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
|
123
|
+
/>
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Animations
|
|
130
|
+
|
|
131
|
+
Petersburg intentionally doesn't include animations to stay lightweight. Add your own with CSS transitions:
|
|
132
|
+
|
|
133
|
+
```css
|
|
134
|
+
.my-gallery img {
|
|
135
|
+
transition: transform 0.3s ease, opacity 0.3s ease;
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Or use your preferred animation library on the item content.
|
|
140
|
+
|
|
141
|
+
## How It Works
|
|
142
|
+
|
|
143
|
+
Petersburg uses the **MaxRects** bin-packing algorithm:
|
|
144
|
+
|
|
145
|
+
1. Start with the full container as free space
|
|
146
|
+
2. For each item, find the best position (topmost, then leftmost)
|
|
147
|
+
3. Place the item and split the remaining free space into new rectangles
|
|
148
|
+
4. Prune redundant free rectangles
|
|
149
|
+
5. Repeat until all items are placed
|
|
150
|
+
|
|
151
|
+
This produces efficient layouts where items fill gaps left by differently-sized neighbors.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface LayoutItem {
|
|
5
|
+
id: string;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
content: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
type SortStrategy = "none" | "height-desc" | "ordered";
|
|
11
|
+
interface HermitageLayoutProps {
|
|
12
|
+
items: LayoutItem[];
|
|
13
|
+
/** Fixed width. If omitted, component auto-measures its container. */
|
|
14
|
+
containerWidth?: number;
|
|
15
|
+
gap?: number;
|
|
16
|
+
sortStrategy?: SortStrategy;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
declare function HermitageLayout({ items, containerWidth: fixedWidth, gap, sortStrategy, className, }: HermitageLayoutProps): react_jsx_runtime.JSX.Element;
|
|
21
|
+
|
|
22
|
+
export { HermitageLayout, type HermitageLayoutProps, type LayoutItem, type SortStrategy };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface LayoutItem {
|
|
5
|
+
id: string;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
content: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
type SortStrategy = "none" | "height-desc" | "ordered";
|
|
11
|
+
interface HermitageLayoutProps {
|
|
12
|
+
items: LayoutItem[];
|
|
13
|
+
/** Fixed width. If omitted, component auto-measures its container. */
|
|
14
|
+
containerWidth?: number;
|
|
15
|
+
gap?: number;
|
|
16
|
+
sortStrategy?: SortStrategy;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
declare function HermitageLayout({ items, containerWidth: fixedWidth, gap, sortStrategy, className, }: HermitageLayoutProps): react_jsx_runtime.JSX.Element;
|
|
21
|
+
|
|
22
|
+
export { HermitageLayout, type HermitageLayoutProps, type LayoutItem, type SortStrategy };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
HermitageLayout: () => HermitageLayout
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/HermitageLayout.tsx
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
|
|
30
|
+
// src/maxrects.ts
|
|
31
|
+
function calcMaxHeight(items, gap) {
|
|
32
|
+
const totalHeight = items.reduce((sum, item) => sum + item.height + gap, 0);
|
|
33
|
+
return Math.max(totalHeight, 1e3);
|
|
34
|
+
}
|
|
35
|
+
function sortItems(items, strategy) {
|
|
36
|
+
if (strategy === "none" || strategy === "ordered") {
|
|
37
|
+
return items;
|
|
38
|
+
}
|
|
39
|
+
const sorted = [...items];
|
|
40
|
+
switch (strategy) {
|
|
41
|
+
case "height-desc":
|
|
42
|
+
sorted.sort((a, b) => b.height - a.height);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
return sorted;
|
|
46
|
+
}
|
|
47
|
+
function pack(items, containerWidth, gap = 0, sortStrategy = "none") {
|
|
48
|
+
if (sortStrategy === "ordered") {
|
|
49
|
+
return packOrdered(items, containerWidth, gap);
|
|
50
|
+
}
|
|
51
|
+
const sortedItems = sortItems(items, sortStrategy);
|
|
52
|
+
const maxHeight = calcMaxHeight(sortedItems, gap);
|
|
53
|
+
const freeRects = [
|
|
54
|
+
{ x: 0, y: 0, width: containerWidth, height: maxHeight }
|
|
55
|
+
];
|
|
56
|
+
const placements = [];
|
|
57
|
+
let maxY = 0;
|
|
58
|
+
for (const item of sortedItems) {
|
|
59
|
+
const paddedWidth = item.width + gap;
|
|
60
|
+
const paddedHeight = item.height + gap;
|
|
61
|
+
const position = findBestPosition(paddedWidth, paddedHeight, freeRects);
|
|
62
|
+
if (position) {
|
|
63
|
+
const placement = {
|
|
64
|
+
id: item.id,
|
|
65
|
+
x: position.x,
|
|
66
|
+
y: position.y,
|
|
67
|
+
width: item.width,
|
|
68
|
+
height: item.height
|
|
69
|
+
};
|
|
70
|
+
placements.push(placement);
|
|
71
|
+
maxY = Math.max(maxY, position.y + item.height);
|
|
72
|
+
const placedRect = {
|
|
73
|
+
x: position.x,
|
|
74
|
+
y: position.y,
|
|
75
|
+
width: paddedWidth,
|
|
76
|
+
height: paddedHeight
|
|
77
|
+
};
|
|
78
|
+
splitFreeRects(freeRects, placedRect);
|
|
79
|
+
pruneFreeRects(freeRects);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
placements,
|
|
84
|
+
totalHeight: maxY
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function packOrdered(items, containerWidth, gap) {
|
|
88
|
+
const maxHeight = calcMaxHeight(items, gap);
|
|
89
|
+
const freeRects = [
|
|
90
|
+
{ x: 0, y: 0, width: containerWidth, height: maxHeight }
|
|
91
|
+
];
|
|
92
|
+
const placements = [];
|
|
93
|
+
let maxY = 0;
|
|
94
|
+
let itemIndex = 0;
|
|
95
|
+
let row1NextX = 0;
|
|
96
|
+
const row1Count = countRow1Items(items, containerWidth, gap);
|
|
97
|
+
for (let i = 0; i < row1Count; i++) {
|
|
98
|
+
const item = items[itemIndex];
|
|
99
|
+
const paddedWidth = item.width + gap;
|
|
100
|
+
const paddedHeight = item.height + gap;
|
|
101
|
+
const placement = {
|
|
102
|
+
id: item.id,
|
|
103
|
+
x: row1NextX,
|
|
104
|
+
y: 0,
|
|
105
|
+
width: item.width,
|
|
106
|
+
height: item.height
|
|
107
|
+
};
|
|
108
|
+
placements.push(placement);
|
|
109
|
+
maxY = Math.max(maxY, item.height);
|
|
110
|
+
const placedRect = {
|
|
111
|
+
x: row1NextX,
|
|
112
|
+
y: 0,
|
|
113
|
+
width: paddedWidth,
|
|
114
|
+
height: paddedHeight
|
|
115
|
+
};
|
|
116
|
+
splitFreeRects(freeRects, placedRect);
|
|
117
|
+
pruneFreeRects(freeRects);
|
|
118
|
+
row1NextX += paddedWidth;
|
|
119
|
+
itemIndex++;
|
|
120
|
+
}
|
|
121
|
+
const remainingItems = items.slice(itemIndex);
|
|
122
|
+
if (remainingItems.length === 0) {
|
|
123
|
+
return { placements, totalHeight: maxY };
|
|
124
|
+
}
|
|
125
|
+
const row2CandidateCount = Math.min(row1Count, remainingItems.length);
|
|
126
|
+
const row2Candidates = remainingItems.slice(0, row2CandidateCount);
|
|
127
|
+
const row3Items = remainingItems.slice(row2CandidateCount);
|
|
128
|
+
const row2Sorted = [...row2Candidates].sort((a, b) => b.height - a.height);
|
|
129
|
+
for (const item of row2Sorted) {
|
|
130
|
+
const paddedWidth = item.width + gap;
|
|
131
|
+
const paddedHeight = item.height + gap;
|
|
132
|
+
const position = findBestPosition(paddedWidth, paddedHeight, freeRects);
|
|
133
|
+
if (position) {
|
|
134
|
+
const placement = {
|
|
135
|
+
id: item.id,
|
|
136
|
+
x: position.x,
|
|
137
|
+
y: position.y,
|
|
138
|
+
width: item.width,
|
|
139
|
+
height: item.height
|
|
140
|
+
};
|
|
141
|
+
placements.push(placement);
|
|
142
|
+
maxY = Math.max(maxY, position.y + item.height);
|
|
143
|
+
const placedRect = {
|
|
144
|
+
x: position.x,
|
|
145
|
+
y: position.y,
|
|
146
|
+
width: paddedWidth,
|
|
147
|
+
height: paddedHeight
|
|
148
|
+
};
|
|
149
|
+
splitFreeRects(freeRects, placedRect);
|
|
150
|
+
pruneFreeRects(freeRects);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const row3Sorted = [...row3Items].sort((a, b) => b.height - a.height);
|
|
154
|
+
for (const item of row3Sorted) {
|
|
155
|
+
const paddedWidth = item.width + gap;
|
|
156
|
+
const paddedHeight = item.height + gap;
|
|
157
|
+
const position = findBestPosition(paddedWidth, paddedHeight, freeRects);
|
|
158
|
+
if (position) {
|
|
159
|
+
const placement = {
|
|
160
|
+
id: item.id,
|
|
161
|
+
x: position.x,
|
|
162
|
+
y: position.y,
|
|
163
|
+
width: item.width,
|
|
164
|
+
height: item.height
|
|
165
|
+
};
|
|
166
|
+
placements.push(placement);
|
|
167
|
+
maxY = Math.max(maxY, position.y + item.height);
|
|
168
|
+
const placedRect = {
|
|
169
|
+
x: position.x,
|
|
170
|
+
y: position.y,
|
|
171
|
+
width: paddedWidth,
|
|
172
|
+
height: paddedHeight
|
|
173
|
+
};
|
|
174
|
+
splitFreeRects(freeRects, placedRect);
|
|
175
|
+
pruneFreeRects(freeRects);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
placements,
|
|
180
|
+
totalHeight: maxY
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function countRow1Items(items, containerWidth, gap) {
|
|
184
|
+
let x = 0;
|
|
185
|
+
let count = 0;
|
|
186
|
+
for (const item of items) {
|
|
187
|
+
const paddedWidth = item.width + gap;
|
|
188
|
+
if (x + paddedWidth > containerWidth + gap) {
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
x += paddedWidth;
|
|
192
|
+
count++;
|
|
193
|
+
}
|
|
194
|
+
return count;
|
|
195
|
+
}
|
|
196
|
+
function findBestPosition(width, height, freeRects) {
|
|
197
|
+
let bestY = Infinity;
|
|
198
|
+
let bestX = Infinity;
|
|
199
|
+
let bestPosition = null;
|
|
200
|
+
for (const rect of freeRects) {
|
|
201
|
+
if (width <= rect.width && height <= rect.height) {
|
|
202
|
+
const x = rect.x;
|
|
203
|
+
const y = rect.y;
|
|
204
|
+
if (y < bestY || y === bestY && x < bestX) {
|
|
205
|
+
bestY = y;
|
|
206
|
+
bestX = x;
|
|
207
|
+
bestPosition = { x, y };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return bestPosition;
|
|
212
|
+
}
|
|
213
|
+
function splitFreeRects(freeRects, placedRect) {
|
|
214
|
+
const newRects = [];
|
|
215
|
+
for (let i = freeRects.length - 1; i >= 0; i--) {
|
|
216
|
+
const freeRect = freeRects[i];
|
|
217
|
+
if (!rectsOverlap(freeRect, placedRect)) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
freeRects.splice(i, 1);
|
|
221
|
+
if (placedRect.x > freeRect.x) {
|
|
222
|
+
newRects.push({
|
|
223
|
+
x: freeRect.x,
|
|
224
|
+
y: freeRect.y,
|
|
225
|
+
width: placedRect.x - freeRect.x,
|
|
226
|
+
height: freeRect.height
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const placedRight = placedRect.x + placedRect.width;
|
|
230
|
+
const freeRight = freeRect.x + freeRect.width;
|
|
231
|
+
if (placedRight < freeRight) {
|
|
232
|
+
newRects.push({
|
|
233
|
+
x: placedRight,
|
|
234
|
+
y: freeRect.y,
|
|
235
|
+
width: freeRight - placedRight,
|
|
236
|
+
height: freeRect.height
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (placedRect.y > freeRect.y) {
|
|
240
|
+
newRects.push({
|
|
241
|
+
x: freeRect.x,
|
|
242
|
+
y: freeRect.y,
|
|
243
|
+
width: freeRect.width,
|
|
244
|
+
height: placedRect.y - freeRect.y
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const placedBottom = placedRect.y + placedRect.height;
|
|
248
|
+
const freeBottom = freeRect.y + freeRect.height;
|
|
249
|
+
if (placedBottom < freeBottom) {
|
|
250
|
+
newRects.push({
|
|
251
|
+
x: freeRect.x,
|
|
252
|
+
y: placedBottom,
|
|
253
|
+
width: freeRect.width,
|
|
254
|
+
height: freeBottom - placedBottom
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
freeRects.push(...newRects);
|
|
259
|
+
}
|
|
260
|
+
function pruneFreeRects(freeRects) {
|
|
261
|
+
for (let i = freeRects.length - 1; i >= 0; i--) {
|
|
262
|
+
for (let j = 0; j < freeRects.length; j++) {
|
|
263
|
+
if (i === j) continue;
|
|
264
|
+
if (rectContains(freeRects[j], freeRects[i])) {
|
|
265
|
+
freeRects.splice(i, 1);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function rectsOverlap(a, b) {
|
|
272
|
+
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
273
|
+
}
|
|
274
|
+
function rectContains(outer, inner) {
|
|
275
|
+
return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/HermitageLayout.tsx
|
|
279
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
280
|
+
function HermitageLayout({
|
|
281
|
+
items,
|
|
282
|
+
containerWidth: fixedWidth,
|
|
283
|
+
gap = 8,
|
|
284
|
+
sortStrategy = "none",
|
|
285
|
+
className
|
|
286
|
+
}) {
|
|
287
|
+
const containerRef = (0, import_react.useRef)(null);
|
|
288
|
+
const [measuredWidth, setMeasuredWidth] = (0, import_react.useState)(0);
|
|
289
|
+
const containerWidth = fixedWidth ?? measuredWidth;
|
|
290
|
+
(0, import_react.useEffect)(() => {
|
|
291
|
+
if (fixedWidth !== void 0) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const element = containerRef.current;
|
|
295
|
+
if (!element) return;
|
|
296
|
+
const observer = new ResizeObserver((entries) => {
|
|
297
|
+
for (const entry of entries) {
|
|
298
|
+
const width = entry.contentRect.width;
|
|
299
|
+
setMeasuredWidth(width);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
observer.observe(element);
|
|
303
|
+
setMeasuredWidth(element.getBoundingClientRect().width);
|
|
304
|
+
return () => observer.disconnect();
|
|
305
|
+
}, [fixedWidth]);
|
|
306
|
+
const { placements, totalHeight } = (0, import_react.useMemo)(() => {
|
|
307
|
+
if (containerWidth === 0) {
|
|
308
|
+
return { placements: [], totalHeight: 0 };
|
|
309
|
+
}
|
|
310
|
+
return pack(items, containerWidth, gap, sortStrategy);
|
|
311
|
+
}, [items, containerWidth, gap, sortStrategy]);
|
|
312
|
+
const placementMap = (0, import_react.useMemo)(() => {
|
|
313
|
+
const map = /* @__PURE__ */ new Map();
|
|
314
|
+
for (const p of placements) {
|
|
315
|
+
map.set(p.id, { x: p.x, y: p.y });
|
|
316
|
+
}
|
|
317
|
+
return map;
|
|
318
|
+
}, [placements]);
|
|
319
|
+
const sortedItems = (0, import_react.useMemo)(() => {
|
|
320
|
+
return [...items].sort((a, b) => {
|
|
321
|
+
const posA = placementMap.get(a.id);
|
|
322
|
+
const posB = placementMap.get(b.id);
|
|
323
|
+
if (!posA || !posB) return 0;
|
|
324
|
+
if (posA.y !== posB.y) return posA.y - posB.y;
|
|
325
|
+
return posA.x - posB.x;
|
|
326
|
+
});
|
|
327
|
+
}, [items, placementMap]);
|
|
328
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
329
|
+
"div",
|
|
330
|
+
{
|
|
331
|
+
ref: containerRef,
|
|
332
|
+
className,
|
|
333
|
+
style: {
|
|
334
|
+
position: "relative",
|
|
335
|
+
width: fixedWidth ?? "100%",
|
|
336
|
+
height: totalHeight || void 0
|
|
337
|
+
},
|
|
338
|
+
children: sortedItems.map((item) => {
|
|
339
|
+
const position = placementMap.get(item.id);
|
|
340
|
+
if (!position) return null;
|
|
341
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
342
|
+
"div",
|
|
343
|
+
{
|
|
344
|
+
style: {
|
|
345
|
+
position: "absolute",
|
|
346
|
+
left: position.x,
|
|
347
|
+
top: position.y,
|
|
348
|
+
width: item.width,
|
|
349
|
+
height: item.height
|
|
350
|
+
},
|
|
351
|
+
children: item.content
|
|
352
|
+
},
|
|
353
|
+
item.id
|
|
354
|
+
);
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
360
|
+
0 && (module.exports = {
|
|
361
|
+
HermitageLayout
|
|
362
|
+
});
|
|
363
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/HermitageLayout.tsx","../src/maxrects.ts"],"sourcesContent":["export { HermitageLayout } from \"./HermitageLayout\";\nexport type { HermitageLayoutProps, LayoutItem, SortStrategy } from \"./types\";\n","import { useMemo, useRef, useState, useEffect } from \"react\";\nimport { HermitageLayoutProps } from \"./types\";\nimport { pack } from \"./maxrects\";\n\nexport function HermitageLayout({\n items,\n containerWidth: fixedWidth,\n gap = 8,\n sortStrategy = \"none\",\n className,\n}: HermitageLayoutProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [measuredWidth, setMeasuredWidth] = useState<number>(0);\n\n // Use fixed width if provided, otherwise use measured width\n const containerWidth = fixedWidth ?? measuredWidth;\n\n // Measure container width using ResizeObserver\n useEffect(() => {\n if (fixedWidth !== undefined) {\n // Fixed width provided, no need to measure\n return;\n }\n\n const element = containerRef.current;\n if (!element) return;\n\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const width = entry.contentRect.width;\n setMeasuredWidth(width);\n }\n });\n\n observer.observe(element);\n\n // Initial measurement\n setMeasuredWidth(element.getBoundingClientRect().width);\n\n return () => observer.disconnect();\n }, [fixedWidth]);\n\n const { placements, totalHeight } = useMemo(() => {\n if (containerWidth === 0) {\n // Not yet measured, return empty layout\n return { placements: [], totalHeight: 0 };\n }\n return pack(items, containerWidth, gap, sortStrategy);\n }, [items, containerWidth, gap, sortStrategy]);\n\n // Create a map for quick lookup of placements by id\n const placementMap = useMemo(() => {\n const map = new Map<string, { x: number; y: number }>();\n for (const p of placements) {\n map.set(p.id, { x: p.x, y: p.y });\n }\n return map;\n }, [placements]);\n\n // Sort items by visual position (y, then x) for proper tab order\n const sortedItems = useMemo(() => {\n return [...items].sort((a, b) => {\n const posA = placementMap.get(a.id);\n const posB = placementMap.get(b.id);\n if (!posA || !posB) return 0;\n if (posA.y !== posB.y) return posA.y - posB.y;\n return posA.x - posB.x;\n });\n }, [items, placementMap]);\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={{\n position: \"relative\",\n width: fixedWidth ?? \"100%\",\n height: totalHeight || undefined,\n }}\n >\n {sortedItems.map((item) => {\n const position = placementMap.get(item.id);\n if (!position) return null;\n\n return (\n <div\n key={item.id}\n style={{\n position: \"absolute\",\n left: position.x,\n top: position.y,\n width: item.width,\n height: item.height,\n }}\n >\n {item.content}\n </div>\n );\n })}\n </div>\n );\n}\n","import { Rect, PlacedItem, PackResult, SortStrategy } from \"./types\";\n\ninterface PackInput {\n id: string;\n width: number;\n height: number;\n}\n\n/**\n * Calculate a safe maximum height for the packing area.\n * Uses sum of all item heights as worst-case (vertical stack).\n */\nfunction calcMaxHeight(items: PackInput[], gap: number): number {\n const totalHeight = items.reduce((sum, item) => sum + item.height + gap, 0);\n return Math.max(totalHeight, 1000); // At least 1000px\n}\n\nfunction sortItems(items: PackInput[], strategy: SortStrategy): PackInput[] {\n if (strategy === \"none\" || strategy === \"ordered\") {\n // \"ordered\" uses a different placement approach, not pre-sorting\n return items;\n }\n\n const sorted = [...items];\n\n switch (strategy) {\n case \"height-desc\":\n sorted.sort((a, b) => b.height - a.height);\n break;\n }\n\n return sorted;\n}\n\n/**\n * MaxRects bin-packing algorithm.\n * Places rectangles in a container, minimizing wasted space.\n */\nexport function pack(\n items: PackInput[],\n containerWidth: number,\n gap: number = 0,\n sortStrategy: SortStrategy = \"none\"\n): PackResult {\n if (sortStrategy === \"ordered\") {\n return packOrdered(items, containerWidth, gap);\n }\n\n const sortedItems = sortItems(items, sortStrategy);\n\n // Start with one large free rectangle\n const maxHeight = calcMaxHeight(sortedItems, gap);\n const freeRects: Rect[] = [\n { x: 0, y: 0, width: containerWidth, height: maxHeight },\n ];\n\n const placements: PlacedItem[] = [];\n let maxY = 0;\n\n for (const item of sortedItems) {\n // Account for gap in item dimensions during placement\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const position = findBestPosition(paddedWidth, paddedHeight, freeRects);\n\n if (position) {\n const placement: PlacedItem = {\n id: item.id,\n x: position.x,\n y: position.y,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n // Track the maximum Y extent\n maxY = Math.max(maxY, position.y + item.height);\n\n // Split free rects around the placed item (using padded dimensions)\n const placedRect: Rect = {\n x: position.x,\n y: position.y,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n }\n }\n\n return {\n placements,\n totalHeight: maxY,\n };\n}\n\n/**\n * Ordered packing strategy:\n * - Row 1 (y=0): Strict input order, left-to-right, no gap filling\n * - Row 2: Next batch of items (in input order), can be reordered for better fit\n * - Row 3+: Full algorithmic freedom (height-desc sorting)\n */\nfunction packOrdered(\n items: PackInput[],\n containerWidth: number,\n gap: number\n): PackResult {\n // Start with full container as free space\n const maxHeight = calcMaxHeight(items, gap);\n const freeRects: Rect[] = [\n { x: 0, y: 0, width: containerWidth, height: maxHeight },\n ];\n\n const placements: PlacedItem[] = [];\n let maxY = 0;\n let itemIndex = 0;\n\n // --- Row 1: Strict left-to-right order at y=0 ---\n let row1NextX = 0;\n const row1Count = countRow1Items(items, containerWidth, gap);\n\n for (let i = 0; i < row1Count; i++) {\n const item = items[itemIndex];\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const placement: PlacedItem = {\n id: item.id,\n x: row1NextX,\n y: 0,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n maxY = Math.max(maxY, item.height);\n\n // Update freeRects to account for this placement\n const placedRect: Rect = {\n x: row1NextX,\n y: 0,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n\n row1NextX += paddedWidth;\n itemIndex++;\n }\n\n // Remaining items after row 1\n const remainingItems = items.slice(itemIndex);\n\n if (remainingItems.length === 0) {\n return { placements, totalHeight: maxY };\n }\n\n // --- Row 2: Next batch of items, reorderable for better fit ---\n // Take roughly the same count as row 1 (or remaining, whichever is smaller)\n const row2CandidateCount = Math.min(row1Count, remainingItems.length);\n const row2Candidates = remainingItems.slice(0, row2CandidateCount);\n const row3Items = remainingItems.slice(row2CandidateCount);\n\n // Sort row 2 candidates by height-desc for better gap filling\n const row2Sorted = [...row2Candidates].sort((a, b) => b.height - a.height);\n\n for (const item of row2Sorted) {\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const position = findBestPosition(paddedWidth, paddedHeight, freeRects);\n\n if (position) {\n const placement: PlacedItem = {\n id: item.id,\n x: position.x,\n y: position.y,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n maxY = Math.max(maxY, position.y + item.height);\n\n const placedRect: Rect = {\n x: position.x,\n y: position.y,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n }\n }\n\n // --- Row 3+: Full freedom, height-desc sorting ---\n const row3Sorted = [...row3Items].sort((a, b) => b.height - a.height);\n\n for (const item of row3Sorted) {\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const position = findBestPosition(paddedWidth, paddedHeight, freeRects);\n\n if (position) {\n const placement: PlacedItem = {\n id: item.id,\n x: position.x,\n y: position.y,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n maxY = Math.max(maxY, position.y + item.height);\n\n const placedRect: Rect = {\n x: position.x,\n y: position.y,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n }\n }\n\n return {\n placements,\n totalHeight: maxY,\n };\n}\n\n/**\n * Count how many items fit in row 1 (strict left-to-right at y=0).\n */\nfunction countRow1Items(\n items: PackInput[],\n containerWidth: number,\n gap: number\n): number {\n let x = 0;\n let count = 0;\n\n for (const item of items) {\n const paddedWidth = item.width + gap;\n if (x + paddedWidth > containerWidth + gap) {\n break;\n }\n x += paddedWidth;\n count++;\n }\n\n return count;\n}\n\n/**\n * Find a position for an item, but only accept positions at a specific y.\n */\nfunction findPositionAtY(\n width: number,\n height: number,\n freeRects: Rect[],\n targetY: number\n): { x: number; y: number } | null {\n let bestX = Infinity;\n let bestPosition: { x: number; y: number } | null = null;\n\n for (const rect of freeRects) {\n // Only consider rects that start at targetY\n if (rect.y !== targetY) continue;\n\n if (width <= rect.width && height <= rect.height) {\n const x = rect.x;\n if (x < bestX) {\n bestX = x;\n bestPosition = { x, y: targetY };\n }\n }\n }\n\n return bestPosition;\n}\n\n/**\n * Get the maximum height among a list of items.\n */\nfunction getMaxHeight(items: PackInput[]): number {\n return items.reduce((max, item) => Math.max(max, item.height), 0);\n}\n\n/**\n * Find the best position for an item using \"Best Y then Best X\" heuristic.\n * Prefers positions higher up (smaller Y), then leftward (smaller X).\n */\nfunction findBestPosition(\n width: number,\n height: number,\n freeRects: Rect[]\n): { x: number; y: number } | null {\n let bestY = Infinity;\n let bestX = Infinity;\n let bestPosition: { x: number; y: number } | null = null;\n\n for (const rect of freeRects) {\n // Check if item fits in this free rect\n if (width <= rect.width && height <= rect.height) {\n // Position at top-left corner of free rect\n const x = rect.x;\n const y = rect.y;\n\n // Prefer smaller Y, then smaller X\n if (y < bestY || (y === bestY && x < bestX)) {\n bestY = y;\n bestX = x;\n bestPosition = { x, y };\n }\n }\n }\n\n return bestPosition;\n}\n\n/**\n * Split free rectangles that overlap with the placed rectangle.\n * Generates up to 4 new rectangles around the placed item.\n */\nfunction splitFreeRects(freeRects: Rect[], placedRect: Rect): void {\n const newRects: Rect[] = [];\n\n for (let i = freeRects.length - 1; i >= 0; i--) {\n const freeRect = freeRects[i];\n\n // Check if this free rect overlaps with placed rect\n if (!rectsOverlap(freeRect, placedRect)) {\n continue;\n }\n\n // Remove the overlapping free rect\n freeRects.splice(i, 1);\n\n // Generate new free rects from the non-overlapping portions\n\n // Left portion\n if (placedRect.x > freeRect.x) {\n newRects.push({\n x: freeRect.x,\n y: freeRect.y,\n width: placedRect.x - freeRect.x,\n height: freeRect.height,\n });\n }\n\n // Right portion\n const placedRight = placedRect.x + placedRect.width;\n const freeRight = freeRect.x + freeRect.width;\n if (placedRight < freeRight) {\n newRects.push({\n x: placedRight,\n y: freeRect.y,\n width: freeRight - placedRight,\n height: freeRect.height,\n });\n }\n\n // Top portion\n if (placedRect.y > freeRect.y) {\n newRects.push({\n x: freeRect.x,\n y: freeRect.y,\n width: freeRect.width,\n height: placedRect.y - freeRect.y,\n });\n }\n\n // Bottom portion\n const placedBottom = placedRect.y + placedRect.height;\n const freeBottom = freeRect.y + freeRect.height;\n if (placedBottom < freeBottom) {\n newRects.push({\n x: freeRect.x,\n y: placedBottom,\n width: freeRect.width,\n height: freeBottom - placedBottom,\n });\n }\n }\n\n // Add all new rects\n freeRects.push(...newRects);\n}\n\n/**\n * Remove free rectangles that are fully contained within other free rectangles.\n */\nfunction pruneFreeRects(freeRects: Rect[]): void {\n for (let i = freeRects.length - 1; i >= 0; i--) {\n for (let j = 0; j < freeRects.length; j++) {\n if (i === j) continue;\n\n if (rectContains(freeRects[j], freeRects[i])) {\n freeRects.splice(i, 1);\n break;\n }\n }\n }\n}\n\n/**\n * Check if two rectangles overlap.\n */\nfunction rectsOverlap(a: Rect, b: Rect): boolean {\n return (\n a.x < b.x + b.width &&\n a.x + a.width > b.x &&\n a.y < b.y + b.height &&\n a.y + a.height > b.y\n );\n}\n\n/**\n * Check if rectangle `outer` fully contains rectangle `inner`.\n */\nfunction rectContains(outer: Rect, inner: Rect): boolean {\n return (\n inner.x >= outer.x &&\n inner.y >= outer.y &&\n inner.x + inner.width <= outer.x + outer.width &&\n inner.y + inner.height <= outer.y + outer.height\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// src/HermitageLayout.tsx
|
|
2
|
+
import { useMemo, useRef, useState, useEffect } from "react";
|
|
3
|
+
|
|
4
|
+
// src/maxrects.ts
|
|
5
|
+
function calcMaxHeight(items, gap) {
|
|
6
|
+
const totalHeight = items.reduce((sum, item) => sum + item.height + gap, 0);
|
|
7
|
+
return Math.max(totalHeight, 1e3);
|
|
8
|
+
}
|
|
9
|
+
function sortItems(items, strategy) {
|
|
10
|
+
if (strategy === "none" || strategy === "ordered") {
|
|
11
|
+
return items;
|
|
12
|
+
}
|
|
13
|
+
const sorted = [...items];
|
|
14
|
+
switch (strategy) {
|
|
15
|
+
case "height-desc":
|
|
16
|
+
sorted.sort((a, b) => b.height - a.height);
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
return sorted;
|
|
20
|
+
}
|
|
21
|
+
function pack(items, containerWidth, gap = 0, sortStrategy = "none") {
|
|
22
|
+
if (sortStrategy === "ordered") {
|
|
23
|
+
return packOrdered(items, containerWidth, gap);
|
|
24
|
+
}
|
|
25
|
+
const sortedItems = sortItems(items, sortStrategy);
|
|
26
|
+
const maxHeight = calcMaxHeight(sortedItems, gap);
|
|
27
|
+
const freeRects = [
|
|
28
|
+
{ x: 0, y: 0, width: containerWidth, height: maxHeight }
|
|
29
|
+
];
|
|
30
|
+
const placements = [];
|
|
31
|
+
let maxY = 0;
|
|
32
|
+
for (const item of sortedItems) {
|
|
33
|
+
const paddedWidth = item.width + gap;
|
|
34
|
+
const paddedHeight = item.height + gap;
|
|
35
|
+
const position = findBestPosition(paddedWidth, paddedHeight, freeRects);
|
|
36
|
+
if (position) {
|
|
37
|
+
const placement = {
|
|
38
|
+
id: item.id,
|
|
39
|
+
x: position.x,
|
|
40
|
+
y: position.y,
|
|
41
|
+
width: item.width,
|
|
42
|
+
height: item.height
|
|
43
|
+
};
|
|
44
|
+
placements.push(placement);
|
|
45
|
+
maxY = Math.max(maxY, position.y + item.height);
|
|
46
|
+
const placedRect = {
|
|
47
|
+
x: position.x,
|
|
48
|
+
y: position.y,
|
|
49
|
+
width: paddedWidth,
|
|
50
|
+
height: paddedHeight
|
|
51
|
+
};
|
|
52
|
+
splitFreeRects(freeRects, placedRect);
|
|
53
|
+
pruneFreeRects(freeRects);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
placements,
|
|
58
|
+
totalHeight: maxY
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function packOrdered(items, containerWidth, gap) {
|
|
62
|
+
const maxHeight = calcMaxHeight(items, gap);
|
|
63
|
+
const freeRects = [
|
|
64
|
+
{ x: 0, y: 0, width: containerWidth, height: maxHeight }
|
|
65
|
+
];
|
|
66
|
+
const placements = [];
|
|
67
|
+
let maxY = 0;
|
|
68
|
+
let itemIndex = 0;
|
|
69
|
+
let row1NextX = 0;
|
|
70
|
+
const row1Count = countRow1Items(items, containerWidth, gap);
|
|
71
|
+
for (let i = 0; i < row1Count; i++) {
|
|
72
|
+
const item = items[itemIndex];
|
|
73
|
+
const paddedWidth = item.width + gap;
|
|
74
|
+
const paddedHeight = item.height + gap;
|
|
75
|
+
const placement = {
|
|
76
|
+
id: item.id,
|
|
77
|
+
x: row1NextX,
|
|
78
|
+
y: 0,
|
|
79
|
+
width: item.width,
|
|
80
|
+
height: item.height
|
|
81
|
+
};
|
|
82
|
+
placements.push(placement);
|
|
83
|
+
maxY = Math.max(maxY, item.height);
|
|
84
|
+
const placedRect = {
|
|
85
|
+
x: row1NextX,
|
|
86
|
+
y: 0,
|
|
87
|
+
width: paddedWidth,
|
|
88
|
+
height: paddedHeight
|
|
89
|
+
};
|
|
90
|
+
splitFreeRects(freeRects, placedRect);
|
|
91
|
+
pruneFreeRects(freeRects);
|
|
92
|
+
row1NextX += paddedWidth;
|
|
93
|
+
itemIndex++;
|
|
94
|
+
}
|
|
95
|
+
const remainingItems = items.slice(itemIndex);
|
|
96
|
+
if (remainingItems.length === 0) {
|
|
97
|
+
return { placements, totalHeight: maxY };
|
|
98
|
+
}
|
|
99
|
+
const row2CandidateCount = Math.min(row1Count, remainingItems.length);
|
|
100
|
+
const row2Candidates = remainingItems.slice(0, row2CandidateCount);
|
|
101
|
+
const row3Items = remainingItems.slice(row2CandidateCount);
|
|
102
|
+
const row2Sorted = [...row2Candidates].sort((a, b) => b.height - a.height);
|
|
103
|
+
for (const item of row2Sorted) {
|
|
104
|
+
const paddedWidth = item.width + gap;
|
|
105
|
+
const paddedHeight = item.height + gap;
|
|
106
|
+
const position = findBestPosition(paddedWidth, paddedHeight, freeRects);
|
|
107
|
+
if (position) {
|
|
108
|
+
const placement = {
|
|
109
|
+
id: item.id,
|
|
110
|
+
x: position.x,
|
|
111
|
+
y: position.y,
|
|
112
|
+
width: item.width,
|
|
113
|
+
height: item.height
|
|
114
|
+
};
|
|
115
|
+
placements.push(placement);
|
|
116
|
+
maxY = Math.max(maxY, position.y + item.height);
|
|
117
|
+
const placedRect = {
|
|
118
|
+
x: position.x,
|
|
119
|
+
y: position.y,
|
|
120
|
+
width: paddedWidth,
|
|
121
|
+
height: paddedHeight
|
|
122
|
+
};
|
|
123
|
+
splitFreeRects(freeRects, placedRect);
|
|
124
|
+
pruneFreeRects(freeRects);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const row3Sorted = [...row3Items].sort((a, b) => b.height - a.height);
|
|
128
|
+
for (const item of row3Sorted) {
|
|
129
|
+
const paddedWidth = item.width + gap;
|
|
130
|
+
const paddedHeight = item.height + gap;
|
|
131
|
+
const position = findBestPosition(paddedWidth, paddedHeight, freeRects);
|
|
132
|
+
if (position) {
|
|
133
|
+
const placement = {
|
|
134
|
+
id: item.id,
|
|
135
|
+
x: position.x,
|
|
136
|
+
y: position.y,
|
|
137
|
+
width: item.width,
|
|
138
|
+
height: item.height
|
|
139
|
+
};
|
|
140
|
+
placements.push(placement);
|
|
141
|
+
maxY = Math.max(maxY, position.y + item.height);
|
|
142
|
+
const placedRect = {
|
|
143
|
+
x: position.x,
|
|
144
|
+
y: position.y,
|
|
145
|
+
width: paddedWidth,
|
|
146
|
+
height: paddedHeight
|
|
147
|
+
};
|
|
148
|
+
splitFreeRects(freeRects, placedRect);
|
|
149
|
+
pruneFreeRects(freeRects);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
placements,
|
|
154
|
+
totalHeight: maxY
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function countRow1Items(items, containerWidth, gap) {
|
|
158
|
+
let x = 0;
|
|
159
|
+
let count = 0;
|
|
160
|
+
for (const item of items) {
|
|
161
|
+
const paddedWidth = item.width + gap;
|
|
162
|
+
if (x + paddedWidth > containerWidth + gap) {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
x += paddedWidth;
|
|
166
|
+
count++;
|
|
167
|
+
}
|
|
168
|
+
return count;
|
|
169
|
+
}
|
|
170
|
+
function findBestPosition(width, height, freeRects) {
|
|
171
|
+
let bestY = Infinity;
|
|
172
|
+
let bestX = Infinity;
|
|
173
|
+
let bestPosition = null;
|
|
174
|
+
for (const rect of freeRects) {
|
|
175
|
+
if (width <= rect.width && height <= rect.height) {
|
|
176
|
+
const x = rect.x;
|
|
177
|
+
const y = rect.y;
|
|
178
|
+
if (y < bestY || y === bestY && x < bestX) {
|
|
179
|
+
bestY = y;
|
|
180
|
+
bestX = x;
|
|
181
|
+
bestPosition = { x, y };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return bestPosition;
|
|
186
|
+
}
|
|
187
|
+
function splitFreeRects(freeRects, placedRect) {
|
|
188
|
+
const newRects = [];
|
|
189
|
+
for (let i = freeRects.length - 1; i >= 0; i--) {
|
|
190
|
+
const freeRect = freeRects[i];
|
|
191
|
+
if (!rectsOverlap(freeRect, placedRect)) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
freeRects.splice(i, 1);
|
|
195
|
+
if (placedRect.x > freeRect.x) {
|
|
196
|
+
newRects.push({
|
|
197
|
+
x: freeRect.x,
|
|
198
|
+
y: freeRect.y,
|
|
199
|
+
width: placedRect.x - freeRect.x,
|
|
200
|
+
height: freeRect.height
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const placedRight = placedRect.x + placedRect.width;
|
|
204
|
+
const freeRight = freeRect.x + freeRect.width;
|
|
205
|
+
if (placedRight < freeRight) {
|
|
206
|
+
newRects.push({
|
|
207
|
+
x: placedRight,
|
|
208
|
+
y: freeRect.y,
|
|
209
|
+
width: freeRight - placedRight,
|
|
210
|
+
height: freeRect.height
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (placedRect.y > freeRect.y) {
|
|
214
|
+
newRects.push({
|
|
215
|
+
x: freeRect.x,
|
|
216
|
+
y: freeRect.y,
|
|
217
|
+
width: freeRect.width,
|
|
218
|
+
height: placedRect.y - freeRect.y
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const placedBottom = placedRect.y + placedRect.height;
|
|
222
|
+
const freeBottom = freeRect.y + freeRect.height;
|
|
223
|
+
if (placedBottom < freeBottom) {
|
|
224
|
+
newRects.push({
|
|
225
|
+
x: freeRect.x,
|
|
226
|
+
y: placedBottom,
|
|
227
|
+
width: freeRect.width,
|
|
228
|
+
height: freeBottom - placedBottom
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
freeRects.push(...newRects);
|
|
233
|
+
}
|
|
234
|
+
function pruneFreeRects(freeRects) {
|
|
235
|
+
for (let i = freeRects.length - 1; i >= 0; i--) {
|
|
236
|
+
for (let j = 0; j < freeRects.length; j++) {
|
|
237
|
+
if (i === j) continue;
|
|
238
|
+
if (rectContains(freeRects[j], freeRects[i])) {
|
|
239
|
+
freeRects.splice(i, 1);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function rectsOverlap(a, b) {
|
|
246
|
+
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
247
|
+
}
|
|
248
|
+
function rectContains(outer, inner) {
|
|
249
|
+
return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/HermitageLayout.tsx
|
|
253
|
+
import { jsx } from "react/jsx-runtime";
|
|
254
|
+
function HermitageLayout({
|
|
255
|
+
items,
|
|
256
|
+
containerWidth: fixedWidth,
|
|
257
|
+
gap = 8,
|
|
258
|
+
sortStrategy = "none",
|
|
259
|
+
className
|
|
260
|
+
}) {
|
|
261
|
+
const containerRef = useRef(null);
|
|
262
|
+
const [measuredWidth, setMeasuredWidth] = useState(0);
|
|
263
|
+
const containerWidth = fixedWidth ?? measuredWidth;
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
if (fixedWidth !== void 0) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const element = containerRef.current;
|
|
269
|
+
if (!element) return;
|
|
270
|
+
const observer = new ResizeObserver((entries) => {
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
const width = entry.contentRect.width;
|
|
273
|
+
setMeasuredWidth(width);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
observer.observe(element);
|
|
277
|
+
setMeasuredWidth(element.getBoundingClientRect().width);
|
|
278
|
+
return () => observer.disconnect();
|
|
279
|
+
}, [fixedWidth]);
|
|
280
|
+
const { placements, totalHeight } = useMemo(() => {
|
|
281
|
+
if (containerWidth === 0) {
|
|
282
|
+
return { placements: [], totalHeight: 0 };
|
|
283
|
+
}
|
|
284
|
+
return pack(items, containerWidth, gap, sortStrategy);
|
|
285
|
+
}, [items, containerWidth, gap, sortStrategy]);
|
|
286
|
+
const placementMap = useMemo(() => {
|
|
287
|
+
const map = /* @__PURE__ */ new Map();
|
|
288
|
+
for (const p of placements) {
|
|
289
|
+
map.set(p.id, { x: p.x, y: p.y });
|
|
290
|
+
}
|
|
291
|
+
return map;
|
|
292
|
+
}, [placements]);
|
|
293
|
+
const sortedItems = useMemo(() => {
|
|
294
|
+
return [...items].sort((a, b) => {
|
|
295
|
+
const posA = placementMap.get(a.id);
|
|
296
|
+
const posB = placementMap.get(b.id);
|
|
297
|
+
if (!posA || !posB) return 0;
|
|
298
|
+
if (posA.y !== posB.y) return posA.y - posB.y;
|
|
299
|
+
return posA.x - posB.x;
|
|
300
|
+
});
|
|
301
|
+
}, [items, placementMap]);
|
|
302
|
+
return /* @__PURE__ */ jsx(
|
|
303
|
+
"div",
|
|
304
|
+
{
|
|
305
|
+
ref: containerRef,
|
|
306
|
+
className,
|
|
307
|
+
style: {
|
|
308
|
+
position: "relative",
|
|
309
|
+
width: fixedWidth ?? "100%",
|
|
310
|
+
height: totalHeight || void 0
|
|
311
|
+
},
|
|
312
|
+
children: sortedItems.map((item) => {
|
|
313
|
+
const position = placementMap.get(item.id);
|
|
314
|
+
if (!position) return null;
|
|
315
|
+
return /* @__PURE__ */ jsx(
|
|
316
|
+
"div",
|
|
317
|
+
{
|
|
318
|
+
style: {
|
|
319
|
+
position: "absolute",
|
|
320
|
+
left: position.x,
|
|
321
|
+
top: position.y,
|
|
322
|
+
width: item.width,
|
|
323
|
+
height: item.height
|
|
324
|
+
},
|
|
325
|
+
children: item.content
|
|
326
|
+
},
|
|
327
|
+
item.id
|
|
328
|
+
);
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
export {
|
|
334
|
+
HermitageLayout
|
|
335
|
+
};
|
|
336
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/HermitageLayout.tsx","../src/maxrects.ts"],"sourcesContent":["import { useMemo, useRef, useState, useEffect } from \"react\";\nimport { HermitageLayoutProps } from \"./types\";\nimport { pack } from \"./maxrects\";\n\nexport function HermitageLayout({\n items,\n containerWidth: fixedWidth,\n gap = 8,\n sortStrategy = \"none\",\n className,\n}: HermitageLayoutProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [measuredWidth, setMeasuredWidth] = useState<number>(0);\n\n // Use fixed width if provided, otherwise use measured width\n const containerWidth = fixedWidth ?? measuredWidth;\n\n // Measure container width using ResizeObserver\n useEffect(() => {\n if (fixedWidth !== undefined) {\n // Fixed width provided, no need to measure\n return;\n }\n\n const element = containerRef.current;\n if (!element) return;\n\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const width = entry.contentRect.width;\n setMeasuredWidth(width);\n }\n });\n\n observer.observe(element);\n\n // Initial measurement\n setMeasuredWidth(element.getBoundingClientRect().width);\n\n return () => observer.disconnect();\n }, [fixedWidth]);\n\n const { placements, totalHeight } = useMemo(() => {\n if (containerWidth === 0) {\n // Not yet measured, return empty layout\n return { placements: [], totalHeight: 0 };\n }\n return pack(items, containerWidth, gap, sortStrategy);\n }, [items, containerWidth, gap, sortStrategy]);\n\n // Create a map for quick lookup of placements by id\n const placementMap = useMemo(() => {\n const map = new Map<string, { x: number; y: number }>();\n for (const p of placements) {\n map.set(p.id, { x: p.x, y: p.y });\n }\n return map;\n }, [placements]);\n\n // Sort items by visual position (y, then x) for proper tab order\n const sortedItems = useMemo(() => {\n return [...items].sort((a, b) => {\n const posA = placementMap.get(a.id);\n const posB = placementMap.get(b.id);\n if (!posA || !posB) return 0;\n if (posA.y !== posB.y) return posA.y - posB.y;\n return posA.x - posB.x;\n });\n }, [items, placementMap]);\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={{\n position: \"relative\",\n width: fixedWidth ?? \"100%\",\n height: totalHeight || undefined,\n }}\n >\n {sortedItems.map((item) => {\n const position = placementMap.get(item.id);\n if (!position) return null;\n\n return (\n <div\n key={item.id}\n style={{\n position: \"absolute\",\n left: position.x,\n top: position.y,\n width: item.width,\n height: item.height,\n }}\n >\n {item.content}\n </div>\n );\n })}\n </div>\n );\n}\n","import { Rect, PlacedItem, PackResult, SortStrategy } from \"./types\";\n\ninterface PackInput {\n id: string;\n width: number;\n height: number;\n}\n\n/**\n * Calculate a safe maximum height for the packing area.\n * Uses sum of all item heights as worst-case (vertical stack).\n */\nfunction calcMaxHeight(items: PackInput[], gap: number): number {\n const totalHeight = items.reduce((sum, item) => sum + item.height + gap, 0);\n return Math.max(totalHeight, 1000); // At least 1000px\n}\n\nfunction sortItems(items: PackInput[], strategy: SortStrategy): PackInput[] {\n if (strategy === \"none\" || strategy === \"ordered\") {\n // \"ordered\" uses a different placement approach, not pre-sorting\n return items;\n }\n\n const sorted = [...items];\n\n switch (strategy) {\n case \"height-desc\":\n sorted.sort((a, b) => b.height - a.height);\n break;\n }\n\n return sorted;\n}\n\n/**\n * MaxRects bin-packing algorithm.\n * Places rectangles in a container, minimizing wasted space.\n */\nexport function pack(\n items: PackInput[],\n containerWidth: number,\n gap: number = 0,\n sortStrategy: SortStrategy = \"none\"\n): PackResult {\n if (sortStrategy === \"ordered\") {\n return packOrdered(items, containerWidth, gap);\n }\n\n const sortedItems = sortItems(items, sortStrategy);\n\n // Start with one large free rectangle\n const maxHeight = calcMaxHeight(sortedItems, gap);\n const freeRects: Rect[] = [\n { x: 0, y: 0, width: containerWidth, height: maxHeight },\n ];\n\n const placements: PlacedItem[] = [];\n let maxY = 0;\n\n for (const item of sortedItems) {\n // Account for gap in item dimensions during placement\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const position = findBestPosition(paddedWidth, paddedHeight, freeRects);\n\n if (position) {\n const placement: PlacedItem = {\n id: item.id,\n x: position.x,\n y: position.y,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n // Track the maximum Y extent\n maxY = Math.max(maxY, position.y + item.height);\n\n // Split free rects around the placed item (using padded dimensions)\n const placedRect: Rect = {\n x: position.x,\n y: position.y,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n }\n }\n\n return {\n placements,\n totalHeight: maxY,\n };\n}\n\n/**\n * Ordered packing strategy:\n * - Row 1 (y=0): Strict input order, left-to-right, no gap filling\n * - Row 2: Next batch of items (in input order), can be reordered for better fit\n * - Row 3+: Full algorithmic freedom (height-desc sorting)\n */\nfunction packOrdered(\n items: PackInput[],\n containerWidth: number,\n gap: number\n): PackResult {\n // Start with full container as free space\n const maxHeight = calcMaxHeight(items, gap);\n const freeRects: Rect[] = [\n { x: 0, y: 0, width: containerWidth, height: maxHeight },\n ];\n\n const placements: PlacedItem[] = [];\n let maxY = 0;\n let itemIndex = 0;\n\n // --- Row 1: Strict left-to-right order at y=0 ---\n let row1NextX = 0;\n const row1Count = countRow1Items(items, containerWidth, gap);\n\n for (let i = 0; i < row1Count; i++) {\n const item = items[itemIndex];\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const placement: PlacedItem = {\n id: item.id,\n x: row1NextX,\n y: 0,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n maxY = Math.max(maxY, item.height);\n\n // Update freeRects to account for this placement\n const placedRect: Rect = {\n x: row1NextX,\n y: 0,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n\n row1NextX += paddedWidth;\n itemIndex++;\n }\n\n // Remaining items after row 1\n const remainingItems = items.slice(itemIndex);\n\n if (remainingItems.length === 0) {\n return { placements, totalHeight: maxY };\n }\n\n // --- Row 2: Next batch of items, reorderable for better fit ---\n // Take roughly the same count as row 1 (or remaining, whichever is smaller)\n const row2CandidateCount = Math.min(row1Count, remainingItems.length);\n const row2Candidates = remainingItems.slice(0, row2CandidateCount);\n const row3Items = remainingItems.slice(row2CandidateCount);\n\n // Sort row 2 candidates by height-desc for better gap filling\n const row2Sorted = [...row2Candidates].sort((a, b) => b.height - a.height);\n\n for (const item of row2Sorted) {\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const position = findBestPosition(paddedWidth, paddedHeight, freeRects);\n\n if (position) {\n const placement: PlacedItem = {\n id: item.id,\n x: position.x,\n y: position.y,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n maxY = Math.max(maxY, position.y + item.height);\n\n const placedRect: Rect = {\n x: position.x,\n y: position.y,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n }\n }\n\n // --- Row 3+: Full freedom, height-desc sorting ---\n const row3Sorted = [...row3Items].sort((a, b) => b.height - a.height);\n\n for (const item of row3Sorted) {\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const position = findBestPosition(paddedWidth, paddedHeight, freeRects);\n\n if (position) {\n const placement: PlacedItem = {\n id: item.id,\n x: position.x,\n y: position.y,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n maxY = Math.max(maxY, position.y + item.height);\n\n const placedRect: Rect = {\n x: position.x,\n y: position.y,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n }\n }\n\n return {\n placements,\n totalHeight: maxY,\n };\n}\n\n/**\n * Count how many items fit in row 1 (strict left-to-right at y=0).\n */\nfunction countRow1Items(\n items: PackInput[],\n containerWidth: number,\n gap: number\n): number {\n let x = 0;\n let count = 0;\n\n for (const item of items) {\n const paddedWidth = item.width + gap;\n if (x + paddedWidth > containerWidth + gap) {\n break;\n }\n x += paddedWidth;\n count++;\n }\n\n return count;\n}\n\n/**\n * Find a position for an item, but only accept positions at a specific y.\n */\nfunction findPositionAtY(\n width: number,\n height: number,\n freeRects: Rect[],\n targetY: number\n): { x: number; y: number } | null {\n let bestX = Infinity;\n let bestPosition: { x: number; y: number } | null = null;\n\n for (const rect of freeRects) {\n // Only consider rects that start at targetY\n if (rect.y !== targetY) continue;\n\n if (width <= rect.width && height <= rect.height) {\n const x = rect.x;\n if (x < bestX) {\n bestX = x;\n bestPosition = { x, y: targetY };\n }\n }\n }\n\n return bestPosition;\n}\n\n/**\n * Get the maximum height among a list of items.\n */\nfunction getMaxHeight(items: PackInput[]): number {\n return items.reduce((max, item) => Math.max(max, item.height), 0);\n}\n\n/**\n * Find the best position for an item using \"Best Y then Best X\" heuristic.\n * Prefers positions higher up (smaller Y), then leftward (smaller X).\n */\nfunction findBestPosition(\n width: number,\n height: number,\n freeRects: Rect[]\n): { x: number; y: number } | null {\n let bestY = Infinity;\n let bestX = Infinity;\n let bestPosition: { x: number; y: number } | null = null;\n\n for (const rect of freeRects) {\n // Check if item fits in this free rect\n if (width <= rect.width && height <= rect.height) {\n // Position at top-left corner of free rect\n const x = rect.x;\n const y = rect.y;\n\n // Prefer smaller Y, then smaller X\n if (y < bestY || (y === bestY && x < bestX)) {\n bestY = y;\n bestX = x;\n bestPosition = { x, y };\n }\n }\n }\n\n return bestPosition;\n}\n\n/**\n * Split free rectangles that overlap with the placed rectangle.\n * Generates up to 4 new rectangles around the placed item.\n */\nfunction splitFreeRects(freeRects: Rect[], placedRect: Rect): void {\n const newRects: Rect[] = [];\n\n for (let i = freeRects.length - 1; i >= 0; i--) {\n const freeRect = freeRects[i];\n\n // Check if this free rect overlaps with placed rect\n if (!rectsOverlap(freeRect, placedRect)) {\n continue;\n }\n\n // Remove the overlapping free rect\n freeRects.splice(i, 1);\n\n // Generate new free rects from the non-overlapping portions\n\n // Left portion\n if (placedRect.x > freeRect.x) {\n newRects.push({\n x: freeRect.x,\n y: freeRect.y,\n width: placedRect.x - freeRect.x,\n height: freeRect.height,\n });\n }\n\n // Right portion\n const placedRight = placedRect.x + placedRect.width;\n const freeRight = freeRect.x + freeRect.width;\n if (placedRight < freeRight) {\n newRects.push({\n x: placedRight,\n y: freeRect.y,\n width: freeRight - placedRight,\n height: freeRect.height,\n });\n }\n\n // Top portion\n if (placedRect.y > freeRect.y) {\n newRects.push({\n x: freeRect.x,\n y: freeRect.y,\n width: freeRect.width,\n height: placedRect.y - freeRect.y,\n });\n }\n\n // Bottom portion\n const placedBottom = placedRect.y + placedRect.height;\n const freeBottom = freeRect.y + freeRect.height;\n if (placedBottom < freeBottom) {\n newRects.push({\n x: freeRect.x,\n y: placedBottom,\n width: freeRect.width,\n height: freeBottom - placedBottom,\n });\n }\n }\n\n // Add all new rects\n freeRects.push(...newRects);\n}\n\n/**\n * Remove free rectangles that are fully contained within other free rectangles.\n */\nfunction pruneFreeRects(freeRects: Rect[]): void {\n for (let i = freeRects.length - 1; i >= 0; i--) {\n for (let j = 0; j < freeRects.length; j++) {\n if (i === j) continue;\n\n if (rectContains(freeRects[j], freeRects[i])) {\n freeRects.splice(i, 1);\n break;\n }\n }\n }\n}\n\n/**\n * Check if two rectangles overlap.\n */\nfunction rectsOverlap(a: Rect, b: Rect): boolean {\n return (\n a.x < b.x + b.width &&\n a.x + a.width > b.x &&\n a.y < b.y + b.height &&\n a.y + a.height > b.y\n );\n}\n\n/**\n * Check if rectangle `outer` fully contains rectangle `inner`.\n */\nfunction rectContains(outer: Rect, inner: Rect): boolean {\n return (\n inner.x >= outer.x &&\n inner.y >= outer.y &&\n inner.x + inner.width <= outer.x + outer.width &&\n inner.y + inner.height <= outer.y + outer.height\n );\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "petersburg",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "React layout library implementing Hermitage-style 2D bin-packing for picture arrangement",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"dev": "tsup --watch",
|
|
21
|
+
"lint": "eslint src",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"react",
|
|
27
|
+
"layout",
|
|
28
|
+
"bin-packing",
|
|
29
|
+
"masonry",
|
|
30
|
+
"gallery",
|
|
31
|
+
"grid",
|
|
32
|
+
"hermitage"
|
|
33
|
+
],
|
|
34
|
+
"author": "xk7200",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/xfaSts9cwY6VqLNTMAtR/Petersburg.git"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": ">=17.0.0",
|
|
42
|
+
"react-dom": ">=17.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/react": "^18.2.0",
|
|
46
|
+
"@types/react-dom": "^18.2.0",
|
|
47
|
+
"react": "^18.2.0",
|
|
48
|
+
"react-dom": "^18.2.0",
|
|
49
|
+
"tsup": "^8.0.0",
|
|
50
|
+
"typescript": "^5.3.0"
|
|
51
|
+
}
|
|
52
|
+
}
|