@tscircuit/rectdiff 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/.claude/settings.local.json +9 -0
- package/.github/workflows/bun-formatcheck.yml +26 -0
- package/.github/workflows/bun-pver-release.yml +71 -0
- package/.github/workflows/bun-test.yml +31 -0
- package/.github/workflows/bun-typecheck.yml +26 -0
- package/CLAUDE.md +23 -0
- package/README.md +5 -0
- package/biome.json +93 -0
- package/bun.lock +29 -0
- package/bunfig.toml +5 -0
- package/components/SolverDebugger3d.tsx +833 -0
- package/cosmos.config.json +6 -0
- package/cosmos.decorator.tsx +21 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +921 -0
- package/experiments/rect-fill-2d.tsx +983 -0
- package/experiments/rect3d_visualizer.html +640 -0
- package/global.d.ts +4 -0
- package/index.html +12 -0
- package/lib/index.ts +1 -0
- package/lib/solvers/RectDiffSolver.ts +158 -0
- package/lib/solvers/rectdiff/candidates.ts +397 -0
- package/lib/solvers/rectdiff/engine.ts +355 -0
- package/lib/solvers/rectdiff/geometry.ts +284 -0
- package/lib/solvers/rectdiff/layers.ts +48 -0
- package/lib/solvers/rectdiff/rectsToMeshNodes.ts +22 -0
- package/lib/solvers/rectdiff/types.ts +63 -0
- package/lib/types/capacity-mesh-types.ts +33 -0
- package/lib/types/srj-types.ts +37 -0
- package/package.json +33 -0
- package/pages/example01.page.tsx +11 -0
- package/test-assets/example01.json +933 -0
- package/tests/__snapshots__/svg.snap.svg +3 -0
- package/tests/examples/__snapshots__/example01.snap.svg +121 -0
- package/tests/examples/example01.test.tsx +65 -0
- package/tests/fixtures/preload.ts +1 -0
- package/tests/incremental-solver.test.ts +100 -0
- package/tests/rect-diff-solver.test.ts +154 -0
- package/tests/svg.test.ts +12 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,983 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import React, { useState, useEffect, useRef } from "react"
|
|
3
|
+
import { Play, Square, SkipForward } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
const RectFillVisualizer = () => {
|
|
6
|
+
const canvasRef = useRef(null)
|
|
7
|
+
|
|
8
|
+
const GRID_PROGRESSION = [100, 50, 20]
|
|
9
|
+
const MIN_RECT_SIZE_IN_CELL_RATIO = 0.2
|
|
10
|
+
|
|
11
|
+
const [maxRatio, setMaxRatio] = useState(2)
|
|
12
|
+
const [isRunning, setIsRunning] = useState(false)
|
|
13
|
+
const [currentStep, setCurrentStep] = useState(0)
|
|
14
|
+
const [fillRects, setFillRects] = useState([])
|
|
15
|
+
const [candidatePoints, setCandidatePoints] = useState([])
|
|
16
|
+
const [expansionPhase, setExpansionPhase] = useState(false)
|
|
17
|
+
const [expansionIndex, setExpansionIndex] = useState(0)
|
|
18
|
+
const [gapFillingPhase, setGapFillingPhase] = useState(false)
|
|
19
|
+
const [gapCandidates, setGapCandidates] = useState([])
|
|
20
|
+
const [currentGridIndex, setCurrentGridIndex] = useState(0)
|
|
21
|
+
const [currentGridSize, setCurrentGridSize] = useState(GRID_PROGRESSION[0])
|
|
22
|
+
|
|
23
|
+
// Example problem setup
|
|
24
|
+
const outerBorder = { x: 50, y: 50, width: 700, height: 500 }
|
|
25
|
+
const obstacles = [
|
|
26
|
+
{ x: 150, y: 100, width: 100, height: 80 },
|
|
27
|
+
{ x: 400, y: 150, width: 120, height: 100 },
|
|
28
|
+
{ x: 250, y: 300, width: 80, height: 120 },
|
|
29
|
+
{ x: 600, y: 250, width: 100, height: 150 },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const pointToLineSegmentDistance = (px, py, x1, y1, x2, y2) => {
|
|
33
|
+
const A = px - x1
|
|
34
|
+
const B = py - y1
|
|
35
|
+
const C = x2 - x1
|
|
36
|
+
const D = y2 - y1
|
|
37
|
+
|
|
38
|
+
const dot = A * C + B * D
|
|
39
|
+
const lenSq = C * C + D * D
|
|
40
|
+
let param = -1
|
|
41
|
+
|
|
42
|
+
if (lenSq !== 0) param = dot / lenSq
|
|
43
|
+
|
|
44
|
+
let xx, yy
|
|
45
|
+
|
|
46
|
+
if (param < 0) {
|
|
47
|
+
xx = x1
|
|
48
|
+
yy = y1
|
|
49
|
+
} else if (param > 1) {
|
|
50
|
+
xx = x2
|
|
51
|
+
yy = y2
|
|
52
|
+
} else {
|
|
53
|
+
xx = x1 + param * C
|
|
54
|
+
yy = y1 + param * D
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const dx = px - xx
|
|
58
|
+
const dy = py - yy
|
|
59
|
+
return Math.sqrt(dx * dx + dy * dy)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const distanceToRect = (px, py, rect) => {
|
|
63
|
+
const edges = [
|
|
64
|
+
[rect.x, rect.y, rect.x + rect.width, rect.y],
|
|
65
|
+
[rect.x + rect.width, rect.y, rect.x + rect.width, rect.y + rect.height],
|
|
66
|
+
[rect.x + rect.width, rect.y + rect.height, rect.x, rect.y + rect.height],
|
|
67
|
+
[rect.x, rect.y + rect.height, rect.x, rect.y],
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
return Math.min(
|
|
71
|
+
...edges.map(([x1, y1, x2, y2]) =>
|
|
72
|
+
pointToLineSegmentDistance(px, py, x1, y1, x2, y2),
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const isPointInRect = (px, py, rect) => {
|
|
78
|
+
return (
|
|
79
|
+
px >= rect.x &&
|
|
80
|
+
px <= rect.x + rect.width &&
|
|
81
|
+
py >= rect.y &&
|
|
82
|
+
py <= rect.y + rect.height
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const rectOverlaps = (r1, r2) => {
|
|
87
|
+
return !(
|
|
88
|
+
r1.x + r1.width <= r2.x ||
|
|
89
|
+
r2.x + r2.width <= r1.x ||
|
|
90
|
+
r1.y + r1.height <= r2.y ||
|
|
91
|
+
r2.y + r2.height <= r1.y
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const computeCandidatePoints = (gridSize, existingRects = []) => {
|
|
96
|
+
const points = []
|
|
97
|
+
const allBlockers = [...obstacles, ...existingRects]
|
|
98
|
+
|
|
99
|
+
for (
|
|
100
|
+
let x = outerBorder.x;
|
|
101
|
+
x < outerBorder.x + outerBorder.width;
|
|
102
|
+
x += gridSize
|
|
103
|
+
) {
|
|
104
|
+
for (
|
|
105
|
+
let y = outerBorder.y;
|
|
106
|
+
y < outerBorder.y + outerBorder.height;
|
|
107
|
+
y += gridSize
|
|
108
|
+
) {
|
|
109
|
+
// Skip points along the border edges
|
|
110
|
+
if (
|
|
111
|
+
x === outerBorder.x ||
|
|
112
|
+
y === outerBorder.y ||
|
|
113
|
+
x >= outerBorder.x + outerBorder.width - gridSize ||
|
|
114
|
+
y >= outerBorder.y + outerBorder.height - gridSize
|
|
115
|
+
) {
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check if point is inside any blocker
|
|
120
|
+
let insideBlocker = false
|
|
121
|
+
|
|
122
|
+
for (const blocker of allBlockers) {
|
|
123
|
+
if (isPointInRect(x, y, blocker)) {
|
|
124
|
+
insideBlocker = true
|
|
125
|
+
const remainingHeight = blocker.y + blocker.height - y
|
|
126
|
+
const skipAmount = Math.max(
|
|
127
|
+
0,
|
|
128
|
+
Math.floor(remainingHeight / gridSize),
|
|
129
|
+
)
|
|
130
|
+
if (skipAmount > 0) {
|
|
131
|
+
y += (skipAmount - 1) * gridSize
|
|
132
|
+
}
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (insideBlocker) {
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let minDist = distanceToRect(x, y, outerBorder)
|
|
142
|
+
|
|
143
|
+
for (const obs of obstacles) {
|
|
144
|
+
const dist = distanceToRect(x, y, obs)
|
|
145
|
+
minDist = Math.min(minDist, dist)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
points.push({ x, y, distance: minDist })
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return points.sort((a, b) => b.distance - a.distance)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const findGapCandidates = (rects) => {
|
|
156
|
+
const allBlockers = [...obstacles, ...rects]
|
|
157
|
+
const gapPoints = []
|
|
158
|
+
const sampleStep = 10
|
|
159
|
+
|
|
160
|
+
rects.forEach((rect, rectIdx) => {
|
|
161
|
+
const edges = [
|
|
162
|
+
{
|
|
163
|
+
name: "right",
|
|
164
|
+
points: () => {
|
|
165
|
+
const pts = []
|
|
166
|
+
for (let y = rect.y; y <= rect.y + rect.height; y += sampleStep) {
|
|
167
|
+
pts.push({
|
|
168
|
+
x: rect.x + rect.width + 1,
|
|
169
|
+
y: Math.min(y, rect.y + rect.height),
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
return pts
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: "bottom",
|
|
177
|
+
points: () => {
|
|
178
|
+
const pts = []
|
|
179
|
+
for (let x = rect.x; x <= rect.x + rect.width; x += sampleStep) {
|
|
180
|
+
pts.push({
|
|
181
|
+
x: Math.min(x, rect.x + rect.width),
|
|
182
|
+
y: rect.y + rect.height + 1,
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
return pts
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "left",
|
|
190
|
+
points: () => {
|
|
191
|
+
const pts = []
|
|
192
|
+
for (let y = rect.y; y <= rect.y + rect.height; y += sampleStep) {
|
|
193
|
+
pts.push({ x: rect.x - 1, y: Math.min(y, rect.y + rect.height) })
|
|
194
|
+
}
|
|
195
|
+
return pts
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "top",
|
|
200
|
+
points: () => {
|
|
201
|
+
const pts = []
|
|
202
|
+
for (let x = rect.x; x <= rect.x + rect.width; x += sampleStep) {
|
|
203
|
+
pts.push({ x: Math.min(x, rect.x + rect.width), y: rect.y - 1 })
|
|
204
|
+
}
|
|
205
|
+
return pts
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
edges.forEach((edge) => {
|
|
211
|
+
const edgePoints = edge.points()
|
|
212
|
+
|
|
213
|
+
edgePoints.forEach((point) => {
|
|
214
|
+
if (
|
|
215
|
+
point.x < outerBorder.x ||
|
|
216
|
+
point.x > outerBorder.x + outerBorder.width ||
|
|
217
|
+
point.y < outerBorder.y ||
|
|
218
|
+
point.y > outerBorder.y + outerBorder.height
|
|
219
|
+
) {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let isOccupied = false
|
|
224
|
+
for (const blocker of allBlockers) {
|
|
225
|
+
if (blocker === rect) continue
|
|
226
|
+
if (isPointInRect(point.x, point.y, blocker)) {
|
|
227
|
+
isOccupied = true
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!isOccupied) {
|
|
233
|
+
gapPoints.push({ x: point.x, y: point.y, sourceRect: rectIdx })
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
return gapPoints
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const expandExistingRect = (existingRect, existingRects = []) => {
|
|
243
|
+
let rect = { ...existingRect }
|
|
244
|
+
const allBlockers = [...obstacles, ...existingRects]
|
|
245
|
+
|
|
246
|
+
let improved = true
|
|
247
|
+
|
|
248
|
+
while (improved) {
|
|
249
|
+
improved = false
|
|
250
|
+
|
|
251
|
+
const maxRight = outerBorder.x + outerBorder.width - (rect.x + rect.width)
|
|
252
|
+
if (maxRight > 0) {
|
|
253
|
+
let bestExpansion = 0
|
|
254
|
+
|
|
255
|
+
for (let expand = 1; expand <= maxRight; expand++) {
|
|
256
|
+
let testRect = { ...rect, width: rect.width + expand }
|
|
257
|
+
|
|
258
|
+
let hasCollision = false
|
|
259
|
+
for (const blocker of allBlockers) {
|
|
260
|
+
if (rectOverlaps(testRect, blocker)) {
|
|
261
|
+
const maxWidth = blocker.x - rect.x
|
|
262
|
+
if (maxWidth > rect.width) {
|
|
263
|
+
bestExpansion = maxWidth - rect.width
|
|
264
|
+
}
|
|
265
|
+
hasCollision = true
|
|
266
|
+
break
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (hasCollision) break
|
|
271
|
+
bestExpansion = expand
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (bestExpansion > 0) {
|
|
275
|
+
rect.width += bestExpansion
|
|
276
|
+
improved = true
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const maxDown =
|
|
281
|
+
outerBorder.y + outerBorder.height - (rect.y + rect.height)
|
|
282
|
+
if (maxDown > 0) {
|
|
283
|
+
let bestExpansion = 0
|
|
284
|
+
|
|
285
|
+
for (let expand = 1; expand <= maxDown; expand++) {
|
|
286
|
+
let testRect = { ...rect, height: rect.height + expand }
|
|
287
|
+
|
|
288
|
+
let hasCollision = false
|
|
289
|
+
for (const blocker of allBlockers) {
|
|
290
|
+
if (rectOverlaps(testRect, blocker)) {
|
|
291
|
+
const maxHeight = blocker.y - rect.y
|
|
292
|
+
if (maxHeight > rect.height) {
|
|
293
|
+
bestExpansion = maxHeight - rect.height
|
|
294
|
+
}
|
|
295
|
+
hasCollision = true
|
|
296
|
+
break
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (hasCollision) break
|
|
301
|
+
bestExpansion = expand
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (bestExpansion > 0) {
|
|
305
|
+
rect.height += bestExpansion
|
|
306
|
+
improved = true
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const maxLeft = rect.x - outerBorder.x
|
|
311
|
+
if (maxLeft > 0) {
|
|
312
|
+
let bestExpansion = 0
|
|
313
|
+
|
|
314
|
+
for (let expand = 1; expand <= maxLeft; expand++) {
|
|
315
|
+
let testRect = {
|
|
316
|
+
x: rect.x - expand,
|
|
317
|
+
y: rect.y,
|
|
318
|
+
width: rect.width + expand,
|
|
319
|
+
height: rect.height,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let hasCollision = false
|
|
323
|
+
for (const blocker of allBlockers) {
|
|
324
|
+
if (rectOverlaps(testRect, blocker)) {
|
|
325
|
+
const newLeft = blocker.x + blocker.width
|
|
326
|
+
if (newLeft < rect.x) {
|
|
327
|
+
bestExpansion = rect.x - newLeft
|
|
328
|
+
}
|
|
329
|
+
hasCollision = true
|
|
330
|
+
break
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (hasCollision) break
|
|
335
|
+
bestExpansion = expand
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (bestExpansion > 0) {
|
|
339
|
+
rect.x -= bestExpansion
|
|
340
|
+
rect.width += bestExpansion
|
|
341
|
+
improved = true
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const maxUp = rect.y - outerBorder.y
|
|
346
|
+
if (maxUp > 0) {
|
|
347
|
+
let bestExpansion = 0
|
|
348
|
+
|
|
349
|
+
for (let expand = 1; expand <= maxUp; expand++) {
|
|
350
|
+
let testRect = {
|
|
351
|
+
x: rect.x,
|
|
352
|
+
y: rect.y - expand,
|
|
353
|
+
width: rect.width,
|
|
354
|
+
height: rect.height + expand,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let hasCollision = false
|
|
358
|
+
for (const blocker of allBlockers) {
|
|
359
|
+
if (rectOverlaps(testRect, blocker)) {
|
|
360
|
+
const newTop = blocker.y + blocker.height
|
|
361
|
+
if (newTop < rect.y) {
|
|
362
|
+
bestExpansion = rect.y - newTop
|
|
363
|
+
}
|
|
364
|
+
hasCollision = true
|
|
365
|
+
break
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (hasCollision) break
|
|
370
|
+
bestExpansion = expand
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (bestExpansion > 0) {
|
|
374
|
+
rect.y -= bestExpansion
|
|
375
|
+
rect.height += bestExpansion
|
|
376
|
+
improved = true
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return rect
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const expandRect = (
|
|
385
|
+
startX,
|
|
386
|
+
startY,
|
|
387
|
+
gridSize,
|
|
388
|
+
maxRatio,
|
|
389
|
+
existingRects = [],
|
|
390
|
+
) => {
|
|
391
|
+
const minSize = Math.max(1, gridSize * MIN_RECT_SIZE_IN_CELL_RATIO)
|
|
392
|
+
|
|
393
|
+
const strategies = [
|
|
394
|
+
{ startOffsetX: 0, startOffsetY: 0 },
|
|
395
|
+
{ startOffsetX: -minSize, startOffsetY: 0 },
|
|
396
|
+
{ startOffsetX: 0, startOffsetY: -minSize },
|
|
397
|
+
{ startOffsetX: -minSize, startOffsetY: -minSize },
|
|
398
|
+
{ startOffsetX: -minSize / 2, startOffsetY: -minSize / 2 },
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
let bestRect = null
|
|
402
|
+
let bestArea = 0
|
|
403
|
+
|
|
404
|
+
for (const strategy of strategies) {
|
|
405
|
+
let rect = {
|
|
406
|
+
x: startX + strategy.startOffsetX,
|
|
407
|
+
y: startY + strategy.startOffsetY,
|
|
408
|
+
width: minSize,
|
|
409
|
+
height: minSize,
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (
|
|
413
|
+
rect.x < outerBorder.x ||
|
|
414
|
+
rect.y < outerBorder.y ||
|
|
415
|
+
rect.x + rect.width > outerBorder.x + outerBorder.width ||
|
|
416
|
+
rect.y + rect.height > outerBorder.y + outerBorder.height
|
|
417
|
+
) {
|
|
418
|
+
continue
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
let hasOverlap =
|
|
422
|
+
obstacles.some((obs) => rectOverlaps(rect, obs)) ||
|
|
423
|
+
existingRects.some((fr) => rectOverlaps(rect, fr))
|
|
424
|
+
|
|
425
|
+
if (hasOverlap) continue
|
|
426
|
+
|
|
427
|
+
const allBlockers = [...obstacles, ...existingRects]
|
|
428
|
+
|
|
429
|
+
let improved = true
|
|
430
|
+
|
|
431
|
+
while (improved) {
|
|
432
|
+
improved = false
|
|
433
|
+
|
|
434
|
+
const maxRight =
|
|
435
|
+
outerBorder.x + outerBorder.width - (rect.x + rect.width)
|
|
436
|
+
if (maxRight > 0) {
|
|
437
|
+
let bestExpansion = 0
|
|
438
|
+
|
|
439
|
+
for (let expand = 1; expand <= maxRight; expand++) {
|
|
440
|
+
let testRect = { ...rect, width: rect.width + expand }
|
|
441
|
+
|
|
442
|
+
if (maxRatio !== null && maxRatio !== undefined) {
|
|
443
|
+
const ratio = Math.max(
|
|
444
|
+
testRect.width / testRect.height,
|
|
445
|
+
testRect.height / testRect.width,
|
|
446
|
+
)
|
|
447
|
+
if (ratio > maxRatio) break
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let hasCollision = false
|
|
451
|
+
for (const blocker of allBlockers) {
|
|
452
|
+
if (rectOverlaps(testRect, blocker)) {
|
|
453
|
+
const maxWidth = blocker.x - rect.x
|
|
454
|
+
if (maxWidth > rect.width) {
|
|
455
|
+
bestExpansion = maxWidth - rect.width
|
|
456
|
+
}
|
|
457
|
+
hasCollision = true
|
|
458
|
+
break
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (hasCollision) break
|
|
463
|
+
bestExpansion = expand
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (bestExpansion > 0) {
|
|
467
|
+
rect.width += bestExpansion
|
|
468
|
+
improved = true
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const maxDown =
|
|
473
|
+
outerBorder.y + outerBorder.height - (rect.y + rect.height)
|
|
474
|
+
if (maxDown > 0) {
|
|
475
|
+
let bestExpansion = 0
|
|
476
|
+
|
|
477
|
+
for (let expand = 1; expand <= maxDown; expand++) {
|
|
478
|
+
let testRect = { ...rect, height: rect.height + expand }
|
|
479
|
+
|
|
480
|
+
if (maxRatio !== null && maxRatio !== undefined) {
|
|
481
|
+
const ratio = Math.max(
|
|
482
|
+
testRect.width / testRect.height,
|
|
483
|
+
testRect.height / testRect.width,
|
|
484
|
+
)
|
|
485
|
+
if (ratio > maxRatio) break
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
let hasCollision = false
|
|
489
|
+
for (const blocker of allBlockers) {
|
|
490
|
+
if (rectOverlaps(testRect, blocker)) {
|
|
491
|
+
const maxHeight = blocker.y - rect.y
|
|
492
|
+
if (maxHeight > rect.height) {
|
|
493
|
+
bestExpansion = maxHeight - rect.height
|
|
494
|
+
}
|
|
495
|
+
hasCollision = true
|
|
496
|
+
break
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (hasCollision) break
|
|
501
|
+
bestExpansion = expand
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (bestExpansion > 0) {
|
|
505
|
+
rect.height += bestExpansion
|
|
506
|
+
improved = true
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const maxLeft = rect.x - outerBorder.x
|
|
511
|
+
if (maxLeft > 0) {
|
|
512
|
+
let bestExpansion = 0
|
|
513
|
+
|
|
514
|
+
for (let expand = 1; expand <= maxLeft; expand++) {
|
|
515
|
+
let testRect = {
|
|
516
|
+
x: rect.x - expand,
|
|
517
|
+
y: rect.y,
|
|
518
|
+
width: rect.width + expand,
|
|
519
|
+
height: rect.height,
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (maxRatio !== null && maxRatio !== undefined) {
|
|
523
|
+
const ratio = Math.max(
|
|
524
|
+
testRect.width / testRect.height,
|
|
525
|
+
testRect.height / testRect.width,
|
|
526
|
+
)
|
|
527
|
+
if (ratio > maxRatio) break
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let hasCollision = false
|
|
531
|
+
for (const blocker of allBlockers) {
|
|
532
|
+
if (rectOverlaps(testRect, blocker)) {
|
|
533
|
+
const newLeft = blocker.x + blocker.width
|
|
534
|
+
if (newLeft < rect.x) {
|
|
535
|
+
bestExpansion = rect.x - newLeft
|
|
536
|
+
}
|
|
537
|
+
hasCollision = true
|
|
538
|
+
break
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (hasCollision) break
|
|
543
|
+
bestExpansion = expand
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (bestExpansion > 0) {
|
|
547
|
+
rect.x -= bestExpansion
|
|
548
|
+
rect.width += bestExpansion
|
|
549
|
+
improved = true
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const maxUp = rect.y - outerBorder.y
|
|
554
|
+
if (maxUp > 0) {
|
|
555
|
+
let bestExpansion = 0
|
|
556
|
+
|
|
557
|
+
for (let expand = 1; expand <= maxUp; expand++) {
|
|
558
|
+
let testRect = {
|
|
559
|
+
x: rect.x,
|
|
560
|
+
y: rect.y - expand,
|
|
561
|
+
width: rect.width,
|
|
562
|
+
height: rect.height + expand,
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (maxRatio !== null && maxRatio !== undefined) {
|
|
566
|
+
const ratio = Math.max(
|
|
567
|
+
testRect.width / testRect.height,
|
|
568
|
+
testRect.height / testRect.width,
|
|
569
|
+
)
|
|
570
|
+
if (ratio > maxRatio) break
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
let hasCollision = false
|
|
574
|
+
for (const blocker of allBlockers) {
|
|
575
|
+
if (rectOverlaps(testRect, blocker)) {
|
|
576
|
+
const newTop = blocker.y + blocker.height
|
|
577
|
+
if (newTop < rect.y) {
|
|
578
|
+
bestExpansion = rect.y - newTop
|
|
579
|
+
}
|
|
580
|
+
hasCollision = true
|
|
581
|
+
break
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (hasCollision) break
|
|
586
|
+
bestExpansion = expand
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (bestExpansion > 0) {
|
|
590
|
+
rect.y -= bestExpansion
|
|
591
|
+
rect.height += bestExpansion
|
|
592
|
+
improved = true
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const area = rect.width * rect.height
|
|
598
|
+
if (area > bestArea) {
|
|
599
|
+
bestArea = area
|
|
600
|
+
bestRect = rect
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return bestRect
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const step = () => {
|
|
608
|
+
if (!expansionPhase && !gapFillingPhase && candidatePoints.length === 0) {
|
|
609
|
+
if (currentGridIndex < GRID_PROGRESSION.length - 1) {
|
|
610
|
+
const nextGridIndex = currentGridIndex + 1
|
|
611
|
+
const nextGridSize = GRID_PROGRESSION[nextGridIndex]
|
|
612
|
+
setCurrentGridIndex(nextGridIndex)
|
|
613
|
+
setCurrentGridSize(nextGridSize)
|
|
614
|
+
const newCandidates = computeCandidatePoints(nextGridSize, fillRects)
|
|
615
|
+
setCandidatePoints(newCandidates)
|
|
616
|
+
return
|
|
617
|
+
} else {
|
|
618
|
+
setExpansionPhase(true)
|
|
619
|
+
setExpansionIndex(0)
|
|
620
|
+
return
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (gapFillingPhase) {
|
|
625
|
+
if (gapCandidates.length === 0) return
|
|
626
|
+
|
|
627
|
+
const point = gapCandidates[0]
|
|
628
|
+
const newRect = expandRect(point.x, point.y, 5, null, fillRects)
|
|
629
|
+
|
|
630
|
+
if (newRect) {
|
|
631
|
+
const newFillRects = [...fillRects, newRect]
|
|
632
|
+
setFillRects(newFillRects)
|
|
633
|
+
|
|
634
|
+
const remainingGaps = gapCandidates.filter(
|
|
635
|
+
(p) => !isPointInRect(p.x, p.y, newRect),
|
|
636
|
+
)
|
|
637
|
+
setGapCandidates(remainingGaps)
|
|
638
|
+
} else {
|
|
639
|
+
setGapCandidates(gapCandidates.slice(1))
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
setCurrentStep(currentStep + 1)
|
|
643
|
+
} else if (expansionPhase) {
|
|
644
|
+
if (expansionIndex >= fillRects.length) {
|
|
645
|
+
const gaps = findGapCandidates(fillRects)
|
|
646
|
+
setGapCandidates(gaps)
|
|
647
|
+
setGapFillingPhase(true)
|
|
648
|
+
setExpansionPhase(false)
|
|
649
|
+
return
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const rectToExpand = fillRects[expansionIndex]
|
|
653
|
+
const otherRects = fillRects.filter((_, i) => i !== expansionIndex)
|
|
654
|
+
|
|
655
|
+
const expandedRect = expandExistingRect(rectToExpand, otherRects)
|
|
656
|
+
|
|
657
|
+
if (expandedRect) {
|
|
658
|
+
const newFillRects = [...fillRects]
|
|
659
|
+
newFillRects[expansionIndex] = expandedRect
|
|
660
|
+
setFillRects(newFillRects)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
setExpansionIndex(expansionIndex + 1)
|
|
664
|
+
} else {
|
|
665
|
+
const point = candidatePoints[0]
|
|
666
|
+
const newRect = expandRect(
|
|
667
|
+
point.x,
|
|
668
|
+
point.y,
|
|
669
|
+
currentGridSize,
|
|
670
|
+
maxRatio,
|
|
671
|
+
fillRects,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
if (newRect) {
|
|
675
|
+
const newFillRects = [...fillRects, newRect]
|
|
676
|
+
setFillRects(newFillRects)
|
|
677
|
+
|
|
678
|
+
const remainingCandidates = candidatePoints.filter(
|
|
679
|
+
(p) => !isPointInRect(p.x, p.y, newRect),
|
|
680
|
+
)
|
|
681
|
+
setCandidatePoints(remainingCandidates)
|
|
682
|
+
} else {
|
|
683
|
+
setCandidatePoints(candidatePoints.slice(1))
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
setCurrentStep(currentStep + 1)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const runAll = () => {
|
|
691
|
+
setIsRunning(true)
|
|
692
|
+
let rects = [...fillRects]
|
|
693
|
+
let steps = currentStep
|
|
694
|
+
|
|
695
|
+
for (
|
|
696
|
+
let gridIdx = currentGridIndex;
|
|
697
|
+
gridIdx < GRID_PROGRESSION.length;
|
|
698
|
+
gridIdx++
|
|
699
|
+
) {
|
|
700
|
+
const gridSize = GRID_PROGRESSION[gridIdx]
|
|
701
|
+
let remainingCandidates = computeCandidatePoints(gridSize, rects)
|
|
702
|
+
|
|
703
|
+
while (remainingCandidates.length > 0) {
|
|
704
|
+
const point = remainingCandidates[0]
|
|
705
|
+
const newRect = expandRect(point.x, point.y, gridSize, maxRatio, rects)
|
|
706
|
+
|
|
707
|
+
if (newRect) {
|
|
708
|
+
rects.push(newRect)
|
|
709
|
+
|
|
710
|
+
remainingCandidates = remainingCandidates.filter(
|
|
711
|
+
(p) => !isPointInRect(p.x, p.y, newRect),
|
|
712
|
+
)
|
|
713
|
+
} else {
|
|
714
|
+
remainingCandidates = remainingCandidates.slice(1)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
steps++
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
for (let i = 0; i < rects.length; i++) {
|
|
722
|
+
const rectToExpand = rects[i]
|
|
723
|
+
const otherRects = rects.filter((_, idx) => idx !== i)
|
|
724
|
+
const expandedRect = expandExistingRect(rectToExpand, otherRects)
|
|
725
|
+
if (expandedRect) {
|
|
726
|
+
rects[i] = expandedRect
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
let gaps = findGapCandidates(rects)
|
|
731
|
+
while (gaps.length > 0) {
|
|
732
|
+
const point = gaps[0]
|
|
733
|
+
const newRect = expandRect(point.x, point.y, 5, null, rects)
|
|
734
|
+
|
|
735
|
+
if (newRect) {
|
|
736
|
+
rects.push(newRect)
|
|
737
|
+
gaps = gaps.filter((p) => !isPointInRect(p.x, p.y, newRect))
|
|
738
|
+
} else {
|
|
739
|
+
gaps = gaps.slice(1)
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
steps++
|
|
743
|
+
|
|
744
|
+
if (steps > currentStep + 1000) break
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
setFillRects(rects)
|
|
748
|
+
setCandidatePoints([])
|
|
749
|
+
setGapCandidates([])
|
|
750
|
+
setCurrentStep(steps)
|
|
751
|
+
setCurrentGridIndex(GRID_PROGRESSION.length - 1)
|
|
752
|
+
setCurrentGridSize(GRID_PROGRESSION[GRID_PROGRESSION.length - 1])
|
|
753
|
+
setExpansionPhase(false)
|
|
754
|
+
setGapFillingPhase(true)
|
|
755
|
+
setExpansionIndex(rects.length)
|
|
756
|
+
setIsRunning(false)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const reset = () => {
|
|
760
|
+
setIsRunning(false)
|
|
761
|
+
setCurrentStep(0)
|
|
762
|
+
setFillRects([])
|
|
763
|
+
setExpansionPhase(false)
|
|
764
|
+
setGapFillingPhase(false)
|
|
765
|
+
setExpansionIndex(0)
|
|
766
|
+
setGapCandidates([])
|
|
767
|
+
setCurrentGridIndex(0)
|
|
768
|
+
const firstGridSize = GRID_PROGRESSION[0]
|
|
769
|
+
setCurrentGridSize(firstGridSize)
|
|
770
|
+
const points = computeCandidatePoints(firstGridSize, [])
|
|
771
|
+
setCandidatePoints(points)
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
useEffect(() => {
|
|
775
|
+
reset()
|
|
776
|
+
}, [maxRatio])
|
|
777
|
+
|
|
778
|
+
useEffect(() => {
|
|
779
|
+
const canvas = canvasRef.current
|
|
780
|
+
if (!canvas) return
|
|
781
|
+
|
|
782
|
+
const ctx = canvas.getContext("2d")
|
|
783
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
784
|
+
|
|
785
|
+
ctx.strokeStyle = "#22c55e"
|
|
786
|
+
ctx.lineWidth = 3
|
|
787
|
+
ctx.strokeRect(
|
|
788
|
+
outerBorder.x,
|
|
789
|
+
outerBorder.y,
|
|
790
|
+
outerBorder.width,
|
|
791
|
+
outerBorder.height,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
ctx.fillStyle = "#ef4444"
|
|
795
|
+
obstacles.forEach((obs) => {
|
|
796
|
+
ctx.fillRect(obs.x, obs.y, obs.width, obs.height)
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
if (!expansionPhase && !gapFillingPhase && candidatePoints.length > 0) {
|
|
800
|
+
ctx.fillStyle = "rgba(156, 163, 175, 0.3)"
|
|
801
|
+
candidatePoints.forEach((point) => {
|
|
802
|
+
ctx.fillRect(point.x - 1, point.y - 1, 2, 2)
|
|
803
|
+
})
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
fillRects.forEach((rect, idx) => {
|
|
807
|
+
if (expansionPhase && idx === expansionIndex - 1) {
|
|
808
|
+
ctx.fillStyle = "rgba(251, 191, 36, 0.5)"
|
|
809
|
+
ctx.strokeStyle = "#fbbf24"
|
|
810
|
+
ctx.lineWidth = 2
|
|
811
|
+
} else {
|
|
812
|
+
ctx.fillStyle = "rgba(59, 130, 246, 0.5)"
|
|
813
|
+
ctx.strokeStyle = "#3b82f6"
|
|
814
|
+
ctx.lineWidth = 1
|
|
815
|
+
}
|
|
816
|
+
ctx.fillRect(rect.x, rect.y, rect.width, rect.height)
|
|
817
|
+
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height)
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
if (gapFillingPhase && gapCandidates.length > 0) {
|
|
821
|
+
ctx.fillStyle = "#ec4899"
|
|
822
|
+
gapCandidates.forEach((point) => {
|
|
823
|
+
ctx.beginPath()
|
|
824
|
+
ctx.arc(point.x, point.y, 3, 0, 2 * Math.PI)
|
|
825
|
+
ctx.fill()
|
|
826
|
+
})
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (!expansionPhase && !gapFillingPhase && candidatePoints.length > 0) {
|
|
830
|
+
const point = candidatePoints[0]
|
|
831
|
+
ctx.fillStyle = "#fbbf24"
|
|
832
|
+
ctx.beginPath()
|
|
833
|
+
ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI)
|
|
834
|
+
ctx.fill()
|
|
835
|
+
}
|
|
836
|
+
}, [
|
|
837
|
+
fillRects,
|
|
838
|
+
currentStep,
|
|
839
|
+
candidatePoints,
|
|
840
|
+
expansionPhase,
|
|
841
|
+
expansionIndex,
|
|
842
|
+
currentGridSize,
|
|
843
|
+
gapFillingPhase,
|
|
844
|
+
gapCandidates,
|
|
845
|
+
])
|
|
846
|
+
|
|
847
|
+
return (
|
|
848
|
+
<div className="w-full h-full bg-gray-900 text-white p-6">
|
|
849
|
+
<div className="max-w-6xl mx-auto">
|
|
850
|
+
<h1 className="text-3xl font-bold mb-2">
|
|
851
|
+
Space-Filling Rectangle Algorithm
|
|
852
|
+
</h1>
|
|
853
|
+
<p className="text-gray-400 mb-6">
|
|
854
|
+
Fills space with rectangles while avoiding obstacles using progressive
|
|
855
|
+
grid refinement (100px → 50px → 20px). Points furthest from obstacles
|
|
856
|
+
are prioritized. After all grids are processed, rectangles expand
|
|
857
|
+
without ratio constraints. Finally, edge gaps are identified and
|
|
858
|
+
filled with additional rectangles.
|
|
859
|
+
</p>
|
|
860
|
+
|
|
861
|
+
<div className="bg-gray-800 rounded-lg p-6 mb-4">
|
|
862
|
+
<canvas
|
|
863
|
+
ref={canvasRef}
|
|
864
|
+
width={800}
|
|
865
|
+
height={600}
|
|
866
|
+
className="border border-gray-700 rounded"
|
|
867
|
+
/>
|
|
868
|
+
</div>
|
|
869
|
+
|
|
870
|
+
<div className="grid grid-cols-1 gap-4 mb-4">
|
|
871
|
+
<div className="bg-gray-800 rounded-lg p-4">
|
|
872
|
+
<label className="block text-sm font-medium mb-2">
|
|
873
|
+
Max Dimension Ratio: {maxRatio.toFixed(1)}
|
|
874
|
+
</label>
|
|
875
|
+
<input
|
|
876
|
+
type="range"
|
|
877
|
+
min="1"
|
|
878
|
+
max="5"
|
|
879
|
+
step="0.1"
|
|
880
|
+
value={maxRatio}
|
|
881
|
+
onChange={(e) => setMaxRatio(parseFloat(e.target.value))}
|
|
882
|
+
className="w-full"
|
|
883
|
+
/>
|
|
884
|
+
</div>
|
|
885
|
+
</div>
|
|
886
|
+
|
|
887
|
+
<div className="flex gap-3">
|
|
888
|
+
<button
|
|
889
|
+
onClick={step}
|
|
890
|
+
disabled={gapFillingPhase && gapCandidates.length === 0}
|
|
891
|
+
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 px-4 py-2 rounded-lg transition"
|
|
892
|
+
>
|
|
893
|
+
<SkipForward size={20} />
|
|
894
|
+
Step
|
|
895
|
+
</button>
|
|
896
|
+
|
|
897
|
+
<button
|
|
898
|
+
onClick={runAll}
|
|
899
|
+
disabled={isRunning}
|
|
900
|
+
className="flex items-center gap-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 px-4 py-2 rounded-lg transition"
|
|
901
|
+
>
|
|
902
|
+
<Play size={20} />
|
|
903
|
+
Run All
|
|
904
|
+
</button>
|
|
905
|
+
|
|
906
|
+
<button
|
|
907
|
+
onClick={reset}
|
|
908
|
+
className="flex items-center gap-2 bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg transition"
|
|
909
|
+
>
|
|
910
|
+
<Square size={20} />
|
|
911
|
+
Reset
|
|
912
|
+
</button>
|
|
913
|
+
|
|
914
|
+
<div className="ml-auto bg-gray-800 px-4 py-2 rounded-lg">
|
|
915
|
+
<span className="text-gray-400">Grid:</span> {currentGridSize}px
|
|
916
|
+
<span className="ml-4 text-gray-400">Phase:</span>{" "}
|
|
917
|
+
{gapFillingPhase
|
|
918
|
+
? "Gap Filling"
|
|
919
|
+
: expansionPhase
|
|
920
|
+
? "Expansion"
|
|
921
|
+
: `Candidates (${currentGridIndex + 1}/${GRID_PROGRESSION.length})`}
|
|
922
|
+
{!expansionPhase && !gapFillingPhase && (
|
|
923
|
+
<>
|
|
924
|
+
<span className="ml-4 text-gray-400">Remaining:</span>{" "}
|
|
925
|
+
{candidatePoints.length}
|
|
926
|
+
</>
|
|
927
|
+
)}
|
|
928
|
+
{expansionPhase && (
|
|
929
|
+
<>
|
|
930
|
+
<span className="ml-4 text-gray-400">Expanding:</span>{" "}
|
|
931
|
+
{expansionIndex}/{fillRects.length}
|
|
932
|
+
</>
|
|
933
|
+
)}
|
|
934
|
+
{gapFillingPhase && (
|
|
935
|
+
<>
|
|
936
|
+
<span className="ml-4 text-gray-400">Gaps:</span>{" "}
|
|
937
|
+
{gapCandidates.length}
|
|
938
|
+
</>
|
|
939
|
+
)}
|
|
940
|
+
<span className="ml-4 text-gray-400">Rectangles:</span>{" "}
|
|
941
|
+
{fillRects.length}
|
|
942
|
+
</div>
|
|
943
|
+
</div>
|
|
944
|
+
|
|
945
|
+
<div className="mt-4 bg-gray-800 rounded-lg p-4">
|
|
946
|
+
<h3 className="font-semibold mb-2">Legend:</h3>
|
|
947
|
+
<div className="flex gap-6 flex-wrap">
|
|
948
|
+
<div className="flex items-center gap-2">
|
|
949
|
+
<div className="w-4 h-4 border-2 border-green-500"></div>
|
|
950
|
+
<span>Outer Border</span>
|
|
951
|
+
</div>
|
|
952
|
+
<div className="flex items-center gap-2">
|
|
953
|
+
<div className="w-4 h-4 bg-red-500"></div>
|
|
954
|
+
<span>Obstacles</span>
|
|
955
|
+
</div>
|
|
956
|
+
<div className="flex items-center gap-2">
|
|
957
|
+
<div className="w-2 h-2 bg-gray-400 opacity-30"></div>
|
|
958
|
+
<span>Grid Candidates</span>
|
|
959
|
+
</div>
|
|
960
|
+
<div className="flex items-center gap-2">
|
|
961
|
+
<div className="w-4 h-4 bg-blue-500 opacity-50"></div>
|
|
962
|
+
<span>Fill Rectangles</span>
|
|
963
|
+
</div>
|
|
964
|
+
<div className="flex items-center gap-2">
|
|
965
|
+
<div className="w-3 h-3 bg-yellow-400 rounded-full"></div>
|
|
966
|
+
<span>Next Candidate</span>
|
|
967
|
+
</div>
|
|
968
|
+
<div className="flex items-center gap-2">
|
|
969
|
+
<div className="w-4 h-4 bg-yellow-400 opacity-50 border border-yellow-400"></div>
|
|
970
|
+
<span>Expanding Rectangle</span>
|
|
971
|
+
</div>
|
|
972
|
+
<div className="flex items-center gap-2">
|
|
973
|
+
<div className="w-3 h-3 bg-pink-500 rounded-full"></div>
|
|
974
|
+
<span>Gap Candidates</span>
|
|
975
|
+
</div>
|
|
976
|
+
</div>
|
|
977
|
+
</div>
|
|
978
|
+
</div>
|
|
979
|
+
</div>
|
|
980
|
+
)
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
export default RectFillVisualizer
|