@tscircuit/rectdiff 0.0.11 → 0.0.13
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/dist/index.d.ts +163 -27
- package/dist/index.js +1885 -1676
- package/lib/RectDiffPipeline.ts +18 -17
- package/lib/{solvers/rectdiff/types.ts → rectdiff-types.ts} +0 -34
- package/lib/{solvers/rectdiff/visualization.ts → rectdiff-visualization.ts} +2 -1
- package/lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts +9 -12
- package/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts +1 -1
- package/lib/solvers/GapFillSolver/visuallyOffsetLine.ts +5 -2
- package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +205 -0
- package/lib/solvers/{rectdiff → RectDiffExpansionSolver}/rectsToMeshNodes.ts +1 -2
- package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +87 -0
- package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +516 -0
- package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +109 -0
- package/lib/solvers/RectDiffSeedingSolver/computeDefaultGridSizes.ts +9 -0
- package/lib/solvers/{rectdiff/candidates.ts → RectDiffSeedingSolver/computeEdgeCandidates3D.ts} +39 -220
- package/lib/solvers/{rectdiff/geometry → RectDiffSeedingSolver}/computeInverseRects.ts +6 -2
- package/lib/solvers/{rectdiff → RectDiffSeedingSolver}/layers.ts +4 -3
- package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +52 -0
- package/lib/utils/buildHardPlacedByLayer.ts +14 -0
- package/lib/{solvers/rectdiff/geometry.ts → utils/expandRectFromSeed.ts} +21 -120
- package/lib/utils/finalizeRects.ts +49 -0
- package/lib/utils/isFullyOccupiedAtPoint.ts +21 -0
- package/lib/utils/rectdiff-geometry.ts +94 -0
- package/lib/utils/resizeSoftOverlaps.ts +74 -0
- package/package.json +1 -1
- package/tests/board-outline.test.ts +2 -1
- package/tests/obstacle-extra-layers.test.ts +1 -1
- package/tests/obstacle-zlayers.test.ts +1 -1
- package/utils/rectsEqual.ts +2 -2
- package/utils/rectsOverlap.ts +2 -2
- package/lib/solvers/RectDiffSolver.ts +0 -231
- package/lib/solvers/rectdiff/engine.ts +0 -481
- /package/lib/solvers/{rectdiff/geometry → RectDiffSeedingSolver}/isPointInPolygon.ts +0 -0
package/lib/solvers/{rectdiff/candidates.ts → RectDiffSeedingSolver/computeEdgeCandidates3D.ts}
RENAMED
|
@@ -1,190 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Check if a point is occupied on all layers.
|
|
7
|
-
*/
|
|
8
|
-
function isFullyOccupiedAllLayers(params: {
|
|
9
|
-
x: number
|
|
10
|
-
y: number
|
|
11
|
-
layerCount: number
|
|
12
|
-
obstaclesByLayer: XYRect[][]
|
|
13
|
-
placedByLayer: XYRect[][]
|
|
14
|
-
}): boolean {
|
|
15
|
-
const { x, y, layerCount, obstaclesByLayer, placedByLayer } = params
|
|
16
|
-
for (let z = 0; z < layerCount; z++) {
|
|
17
|
-
const obs = obstaclesByLayer[z] ?? []
|
|
18
|
-
const placed = placedByLayer[z] ?? []
|
|
19
|
-
const occ =
|
|
20
|
-
obs.some((b) => containsPoint(b, x, y)) ||
|
|
21
|
-
placed.some((b) => containsPoint(b, x, y))
|
|
22
|
-
if (!occ) return false
|
|
23
|
-
}
|
|
24
|
-
return true
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Compute candidate seed points for a given grid size.
|
|
29
|
-
*/
|
|
30
|
-
export function computeCandidates3D(params: {
|
|
31
|
-
bounds: XYRect
|
|
32
|
-
gridSize: number
|
|
33
|
-
layerCount: number
|
|
34
|
-
obstaclesByLayer: XYRect[][]
|
|
35
|
-
placedByLayer: XYRect[][]
|
|
36
|
-
hardPlacedByLayer: XYRect[][]
|
|
37
|
-
}): Candidate3D[] {
|
|
38
|
-
const {
|
|
39
|
-
bounds,
|
|
40
|
-
gridSize,
|
|
41
|
-
layerCount,
|
|
42
|
-
obstaclesByLayer,
|
|
43
|
-
placedByLayer,
|
|
44
|
-
hardPlacedByLayer,
|
|
45
|
-
} = params
|
|
46
|
-
const out = new Map<string, Candidate3D>() // key by (x,y)
|
|
47
|
-
|
|
48
|
-
for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
|
|
49
|
-
for (let y = bounds.y; y < bounds.y + bounds.height; y += gridSize) {
|
|
50
|
-
// Skip outermost row/col (stable with prior behavior)
|
|
51
|
-
if (
|
|
52
|
-
Math.abs(x - bounds.x) < EPS ||
|
|
53
|
-
Math.abs(y - bounds.y) < EPS ||
|
|
54
|
-
x > bounds.x + bounds.width - gridSize - EPS ||
|
|
55
|
-
y > bounds.y + bounds.height - gridSize - EPS
|
|
56
|
-
) {
|
|
57
|
-
continue
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// New rule: Only drop if EVERY layer is occupied (by obstacle or node)
|
|
61
|
-
if (
|
|
62
|
-
isFullyOccupiedAllLayers({
|
|
63
|
-
x,
|
|
64
|
-
y,
|
|
65
|
-
layerCount,
|
|
66
|
-
obstaclesByLayer,
|
|
67
|
-
placedByLayer,
|
|
68
|
-
})
|
|
69
|
-
)
|
|
70
|
-
continue
|
|
71
|
-
|
|
72
|
-
// Find the best (longest) free contiguous Z span at (x,y) ignoring soft nodes.
|
|
73
|
-
let bestSpan: number[] = []
|
|
74
|
-
let bestZ = 0
|
|
75
|
-
for (let z = 0; z < layerCount; z++) {
|
|
76
|
-
const s = longestFreeSpanAroundZ({
|
|
77
|
-
x,
|
|
78
|
-
y,
|
|
79
|
-
z,
|
|
80
|
-
layerCount,
|
|
81
|
-
minSpan: 1,
|
|
82
|
-
maxSpan: undefined,
|
|
83
|
-
obstaclesByLayer,
|
|
84
|
-
placedByLayer: hardPlacedByLayer,
|
|
85
|
-
})
|
|
86
|
-
if (s.length > bestSpan.length) {
|
|
87
|
-
bestSpan = s
|
|
88
|
-
bestZ = z
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
const anchorZ = bestSpan.length
|
|
92
|
-
? bestSpan[Math.floor(bestSpan.length / 2)]!
|
|
93
|
-
: bestZ
|
|
94
|
-
|
|
95
|
-
// Distance heuristic against hard blockers only (obstacles + full-stack)
|
|
96
|
-
const hardAtZ = [
|
|
97
|
-
...(obstaclesByLayer[anchorZ] ?? []),
|
|
98
|
-
...(hardPlacedByLayer[anchorZ] ?? []),
|
|
99
|
-
]
|
|
100
|
-
const d = Math.min(
|
|
101
|
-
distancePointToRectEdges(x, y, bounds),
|
|
102
|
-
...(hardAtZ.length
|
|
103
|
-
? hardAtZ.map((b) => distancePointToRectEdges(x, y, b))
|
|
104
|
-
: [Infinity]),
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
const k = `${x.toFixed(6)}|${y.toFixed(6)}`
|
|
108
|
-
const cand: Candidate3D = {
|
|
109
|
-
x,
|
|
110
|
-
y,
|
|
111
|
-
z: anchorZ,
|
|
112
|
-
distance: d,
|
|
113
|
-
zSpanLen: bestSpan.length,
|
|
114
|
-
}
|
|
115
|
-
const prev = out.get(k)
|
|
116
|
-
if (
|
|
117
|
-
!prev ||
|
|
118
|
-
cand.zSpanLen! > (prev.zSpanLen ?? 0) ||
|
|
119
|
-
(cand.zSpanLen === prev.zSpanLen && cand.distance > prev.distance)
|
|
120
|
-
) {
|
|
121
|
-
out.set(k, cand)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const arr = Array.from(out.values())
|
|
127
|
-
arr.sort((a, b) => b.zSpanLen! - a.zSpanLen! || b.distance - a.distance)
|
|
128
|
-
return arr
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Find the longest contiguous free span around z (optionally capped).
|
|
133
|
-
*/
|
|
134
|
-
export function longestFreeSpanAroundZ(params: {
|
|
135
|
-
x: number
|
|
136
|
-
y: number
|
|
137
|
-
z: number
|
|
138
|
-
layerCount: number
|
|
139
|
-
minSpan: number
|
|
140
|
-
maxSpan: number | undefined
|
|
141
|
-
obstaclesByLayer: XYRect[][]
|
|
142
|
-
placedByLayer: XYRect[][]
|
|
143
|
-
}): number[] {
|
|
144
|
-
const {
|
|
145
|
-
x,
|
|
146
|
-
y,
|
|
147
|
-
z,
|
|
148
|
-
layerCount,
|
|
149
|
-
minSpan,
|
|
150
|
-
maxSpan,
|
|
151
|
-
obstaclesByLayer,
|
|
152
|
-
placedByLayer,
|
|
153
|
-
} = params
|
|
154
|
-
|
|
155
|
-
const isFreeAt = (layer: number) => {
|
|
156
|
-
const blockers = [
|
|
157
|
-
...(obstaclesByLayer[layer] ?? []),
|
|
158
|
-
...(placedByLayer[layer] ?? []),
|
|
159
|
-
]
|
|
160
|
-
return !blockers.some((b) => containsPoint(b, x, y))
|
|
161
|
-
}
|
|
162
|
-
let lo = z
|
|
163
|
-
let hi = z
|
|
164
|
-
while (lo - 1 >= 0 && isFreeAt(lo - 1)) lo--
|
|
165
|
-
while (hi + 1 < layerCount && isFreeAt(hi + 1)) hi++
|
|
166
|
-
|
|
167
|
-
if (typeof maxSpan === "number") {
|
|
168
|
-
const target = clamp(maxSpan, 1, layerCount)
|
|
169
|
-
// trim symmetrically (keeping z inside)
|
|
170
|
-
while (hi - lo + 1 > target) {
|
|
171
|
-
if (z - lo > hi - z) lo++
|
|
172
|
-
else hi--
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const res: number[] = []
|
|
177
|
-
for (let i = lo; i <= hi; i++) res.push(i)
|
|
178
|
-
return res.length >= minSpan ? res : []
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Compute default grid sizes based on bounds.
|
|
183
|
-
*/
|
|
184
|
-
export function computeDefaultGridSizes(bounds: XYRect): number[] {
|
|
185
|
-
const ref = Math.max(bounds.width, bounds.height)
|
|
186
|
-
return [ref / 8, ref / 16, ref / 32]
|
|
187
|
-
}
|
|
1
|
+
import type { Candidate3D, XYRect } from "../../rectdiff-types"
|
|
2
|
+
import { EPS, distancePointToRectEdges } from "../../utils/rectdiff-geometry"
|
|
3
|
+
import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
|
|
4
|
+
import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
|
|
188
5
|
|
|
189
6
|
/**
|
|
190
7
|
* Compute exact uncovered segments along a 1D line.
|
|
@@ -279,20 +96,22 @@ export function computeEdgeCandidates3D(params: {
|
|
|
279
96
|
// Use small inset from edges for placement
|
|
280
97
|
const δ = Math.max(minSize * 0.15, EPS * 3)
|
|
281
98
|
const dedup = new Set<string>()
|
|
282
|
-
const key = (x: number
|
|
283
|
-
`${z}|${x.toFixed(6)}|${y.toFixed(6)}`
|
|
284
|
-
|
|
285
|
-
function fullyOcc(x: number
|
|
286
|
-
return
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
99
|
+
const key = (p: { x: number; y: number; z: number }) =>
|
|
100
|
+
`${p.z}|${p.x.toFixed(6)}|${p.y.toFixed(6)}`
|
|
101
|
+
|
|
102
|
+
function fullyOcc(p: { x: number; y: number }) {
|
|
103
|
+
return isFullyOccupiedAtPoint(
|
|
104
|
+
{
|
|
105
|
+
layerCount,
|
|
106
|
+
obstaclesByLayer,
|
|
107
|
+
placedByLayer,
|
|
108
|
+
},
|
|
109
|
+
p,
|
|
110
|
+
)
|
|
293
111
|
}
|
|
294
112
|
|
|
295
|
-
function pushIfFree(x: number
|
|
113
|
+
function pushIfFree(p: { x: number; y: number; z: number }) {
|
|
114
|
+
const { x, y, z } = p
|
|
296
115
|
if (
|
|
297
116
|
x < bounds.x + EPS ||
|
|
298
117
|
y < bounds.y + EPS ||
|
|
@@ -300,7 +119,7 @@ export function computeEdgeCandidates3D(params: {
|
|
|
300
119
|
y > bounds.y + bounds.height - EPS
|
|
301
120
|
)
|
|
302
121
|
return
|
|
303
|
-
if (fullyOcc(x, y)) return // new rule: only drop if truly impossible
|
|
122
|
+
if (fullyOcc({ x, y })) return // new rule: only drop if truly impossible
|
|
304
123
|
|
|
305
124
|
// Distance uses obstacles + hard nodes (soft nodes ignored for ranking)
|
|
306
125
|
const hard = [
|
|
@@ -308,13 +127,13 @@ export function computeEdgeCandidates3D(params: {
|
|
|
308
127
|
...(hardPlacedByLayer[z] ?? []),
|
|
309
128
|
]
|
|
310
129
|
const d = Math.min(
|
|
311
|
-
distancePointToRectEdges(x, y, bounds),
|
|
130
|
+
distancePointToRectEdges({ x, y }, bounds),
|
|
312
131
|
...(hard.length
|
|
313
|
-
? hard.map((b) => distancePointToRectEdges(x, y, b))
|
|
132
|
+
? hard.map((b) => distancePointToRectEdges({ x, y }, b))
|
|
314
133
|
: [Infinity]),
|
|
315
134
|
)
|
|
316
135
|
|
|
317
|
-
const k = key(x, y, z)
|
|
136
|
+
const k = key({ x, y, z })
|
|
318
137
|
if (dedup.has(k)) return
|
|
319
138
|
dedup.add(k)
|
|
320
139
|
|
|
@@ -348,7 +167,7 @@ export function computeEdgeCandidates3D(params: {
|
|
|
348
167
|
{ x: bounds.x + bounds.width - δ, y: bounds.y + bounds.height - δ }, // bottom-right
|
|
349
168
|
]
|
|
350
169
|
for (const corner of corners) {
|
|
351
|
-
pushIfFree(corner.x, corner.y, z)
|
|
170
|
+
pushIfFree({ x: corner.x, y: corner.y, z })
|
|
352
171
|
}
|
|
353
172
|
|
|
354
173
|
// Top edge (y = bounds.y + δ)
|
|
@@ -370,10 +189,10 @@ export function computeEdgeCandidates3D(params: {
|
|
|
370
189
|
const segLen = seg.end - seg.start
|
|
371
190
|
if (segLen >= minSize) {
|
|
372
191
|
// Seed center and a few strategic points
|
|
373
|
-
pushIfFree(seg.center, topY, z)
|
|
192
|
+
pushIfFree({ x: seg.center, y: topY, z })
|
|
374
193
|
if (segLen > minSize * 1.5) {
|
|
375
|
-
pushIfFree(seg.start + minSize * 0.4, topY, z)
|
|
376
|
-
pushIfFree(seg.end - minSize * 0.4, topY, z)
|
|
194
|
+
pushIfFree({ x: seg.start + minSize * 0.4, y: topY, z })
|
|
195
|
+
pushIfFree({ x: seg.end - minSize * 0.4, y: topY, z })
|
|
377
196
|
}
|
|
378
197
|
}
|
|
379
198
|
}
|
|
@@ -395,10 +214,10 @@ export function computeEdgeCandidates3D(params: {
|
|
|
395
214
|
for (const seg of bottomUncovered) {
|
|
396
215
|
const segLen = seg.end - seg.start
|
|
397
216
|
if (segLen >= minSize) {
|
|
398
|
-
pushIfFree(seg.center, bottomY, z)
|
|
217
|
+
pushIfFree({ x: seg.center, y: bottomY, z })
|
|
399
218
|
if (segLen > minSize * 1.5) {
|
|
400
|
-
pushIfFree(seg.start + minSize * 0.4, bottomY, z)
|
|
401
|
-
pushIfFree(seg.end - minSize * 0.4, bottomY, z)
|
|
219
|
+
pushIfFree({ x: seg.start + minSize * 0.4, y: bottomY, z })
|
|
220
|
+
pushIfFree({ x: seg.end - minSize * 0.4, y: bottomY, z })
|
|
402
221
|
}
|
|
403
222
|
}
|
|
404
223
|
}
|
|
@@ -420,10 +239,10 @@ export function computeEdgeCandidates3D(params: {
|
|
|
420
239
|
for (const seg of leftUncovered) {
|
|
421
240
|
const segLen = seg.end - seg.start
|
|
422
241
|
if (segLen >= minSize) {
|
|
423
|
-
pushIfFree(leftX, seg.center, z)
|
|
242
|
+
pushIfFree({ x: leftX, y: seg.center, z })
|
|
424
243
|
if (segLen > minSize * 1.5) {
|
|
425
|
-
pushIfFree(leftX, seg.start + minSize * 0.4, z)
|
|
426
|
-
pushIfFree(leftX, seg.end - minSize * 0.4, z)
|
|
244
|
+
pushIfFree({ x: leftX, y: seg.start + minSize * 0.4, z })
|
|
245
|
+
pushIfFree({ x: leftX, y: seg.end - minSize * 0.4, z })
|
|
427
246
|
}
|
|
428
247
|
}
|
|
429
248
|
}
|
|
@@ -445,10 +264,10 @@ export function computeEdgeCandidates3D(params: {
|
|
|
445
264
|
for (const seg of rightUncovered) {
|
|
446
265
|
const segLen = seg.end - seg.start
|
|
447
266
|
if (segLen >= minSize) {
|
|
448
|
-
pushIfFree(rightX, seg.center, z)
|
|
267
|
+
pushIfFree({ x: rightX, y: seg.center, z })
|
|
449
268
|
if (segLen > minSize * 1.5) {
|
|
450
|
-
pushIfFree(rightX, seg.start + minSize * 0.4, z)
|
|
451
|
-
pushIfFree(rightX, seg.end - minSize * 0.4, z)
|
|
269
|
+
pushIfFree({ x: rightX, y: seg.start + minSize * 0.4, z })
|
|
270
|
+
pushIfFree({ x: rightX, y: seg.end - minSize * 0.4, z })
|
|
452
271
|
}
|
|
453
272
|
}
|
|
454
273
|
}
|
|
@@ -473,7 +292,7 @@ export function computeEdgeCandidates3D(params: {
|
|
|
473
292
|
minSegmentLength: minSize * 0.5,
|
|
474
293
|
})
|
|
475
294
|
for (const seg of obLeftUncovered) {
|
|
476
|
-
pushIfFree(obLeftX, seg.center, z)
|
|
295
|
+
pushIfFree({ x: obLeftX, y: seg.center, z })
|
|
477
296
|
}
|
|
478
297
|
}
|
|
479
298
|
|
|
@@ -498,7 +317,7 @@ export function computeEdgeCandidates3D(params: {
|
|
|
498
317
|
minSegmentLength: minSize * 0.5,
|
|
499
318
|
})
|
|
500
319
|
for (const seg of obRightUncovered) {
|
|
501
|
-
pushIfFree(obRightX, seg.center, z)
|
|
320
|
+
pushIfFree({ x: obRightX, y: seg.center, z })
|
|
502
321
|
}
|
|
503
322
|
}
|
|
504
323
|
|
|
@@ -520,7 +339,7 @@ export function computeEdgeCandidates3D(params: {
|
|
|
520
339
|
minSegmentLength: minSize * 0.5,
|
|
521
340
|
})
|
|
522
341
|
for (const seg of obTopUncovered) {
|
|
523
|
-
pushIfFree(seg.center, obTopY, z)
|
|
342
|
+
pushIfFree({ x: seg.center, y: obTopY, z })
|
|
524
343
|
}
|
|
525
344
|
}
|
|
526
345
|
|
|
@@ -546,7 +365,7 @@ export function computeEdgeCandidates3D(params: {
|
|
|
546
365
|
minSegmentLength: minSize * 0.5,
|
|
547
366
|
})
|
|
548
367
|
for (const seg of obBottomUncovered) {
|
|
549
|
-
pushIfFree(seg.center, obBottomY, z)
|
|
368
|
+
pushIfFree({ x: seg.center, y: obBottomY, z })
|
|
550
369
|
}
|
|
551
370
|
}
|
|
552
371
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type { XYRect } from "
|
|
1
|
+
import type { XYRect } from "../../rectdiff-types"
|
|
2
|
+
import {
|
|
3
|
+
containsPoint,
|
|
4
|
+
subtractRect2D,
|
|
5
|
+
EPS,
|
|
6
|
+
} from "../../utils/rectdiff-geometry"
|
|
2
7
|
import { isPointInPolygon } from "./isPointInPolygon"
|
|
3
|
-
import { EPS } from "../geometry" // Import EPS from common geometry file
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Decompose the empty space inside 'bounds' but outside 'polygon' into rectangles.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
import type {
|
|
3
|
-
|
|
1
|
+
import type { SimpleRouteJson } from "../../types/srj-types"
|
|
2
|
+
import type { XYRect } from "../../rectdiff-types"
|
|
3
|
+
|
|
4
|
+
type Obstacle = NonNullable<SimpleRouteJson["obstacles"]>[number]
|
|
4
5
|
|
|
5
6
|
function layerSortKey(n: string) {
|
|
6
7
|
const L = n.toLowerCase()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { XYRect } from "../../rectdiff-types"
|
|
2
|
+
import { clamp, containsPoint } from "../../utils/rectdiff-geometry"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Find the longest contiguous free span around z (optionally capped).
|
|
6
|
+
*/
|
|
7
|
+
export function longestFreeSpanAroundZ(params: {
|
|
8
|
+
x: number
|
|
9
|
+
y: number
|
|
10
|
+
z: number
|
|
11
|
+
layerCount: number
|
|
12
|
+
minSpan: number
|
|
13
|
+
maxSpan: number | undefined
|
|
14
|
+
obstaclesByLayer: XYRect[][]
|
|
15
|
+
placedByLayer: XYRect[][]
|
|
16
|
+
}): number[] {
|
|
17
|
+
const {
|
|
18
|
+
x,
|
|
19
|
+
y,
|
|
20
|
+
z,
|
|
21
|
+
layerCount,
|
|
22
|
+
minSpan,
|
|
23
|
+
maxSpan,
|
|
24
|
+
obstaclesByLayer,
|
|
25
|
+
placedByLayer,
|
|
26
|
+
} = params
|
|
27
|
+
|
|
28
|
+
const isFreeAt = (layer: number) => {
|
|
29
|
+
const blockers = [
|
|
30
|
+
...(obstaclesByLayer[layer] ?? []),
|
|
31
|
+
...(placedByLayer[layer] ?? []),
|
|
32
|
+
]
|
|
33
|
+
return !blockers.some((b) => containsPoint(b, { x, y }))
|
|
34
|
+
}
|
|
35
|
+
let lo = z
|
|
36
|
+
let hi = z
|
|
37
|
+
while (lo - 1 >= 0 && isFreeAt(lo - 1)) lo--
|
|
38
|
+
while (hi + 1 < layerCount && isFreeAt(hi + 1)) hi++
|
|
39
|
+
|
|
40
|
+
if (typeof maxSpan === "number") {
|
|
41
|
+
const target = clamp(maxSpan, 1, layerCount)
|
|
42
|
+
// trim symmetrically (keeping z inside)
|
|
43
|
+
while (hi - lo + 1 > target) {
|
|
44
|
+
if (z - lo > hi - z) lo++
|
|
45
|
+
else hi--
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const res: number[] = []
|
|
50
|
+
for (let i = lo; i <= hi; i++) res.push(i)
|
|
51
|
+
return res.length >= minSpan ? res : []
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Placed3D, XYRect } from "../rectdiff-types"
|
|
2
|
+
|
|
3
|
+
export function allLayerNode(params: {
|
|
4
|
+
layerCount: number
|
|
5
|
+
placed: Placed3D[]
|
|
6
|
+
}): XYRect[][] {
|
|
7
|
+
const out: XYRect[][] = Array.from({ length: params.layerCount }, () => [])
|
|
8
|
+
for (const p of params.placed) {
|
|
9
|
+
if (p.zLayers.length >= params.layerCount) {
|
|
10
|
+
for (const z of p.zLayers) out[z]!.push(p.rect)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return out
|
|
14
|
+
}
|
|
@@ -1,64 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import type { XYRect } from "../rectdiff-types"
|
|
2
|
+
import { EPS, gt, gte, lt, lte, overlaps } from "./rectdiff-geometry"
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export const lt = (a: number, b: number) => a < b - EPS
|
|
10
|
-
export const lte = (a: number, b: number) => a < b + EPS
|
|
11
|
-
|
|
12
|
-
export function overlaps(a: XYRect, b: XYRect) {
|
|
13
|
-
return !(
|
|
14
|
-
a.x + a.width <= b.x + EPS ||
|
|
15
|
-
b.x + b.width <= a.x + EPS ||
|
|
16
|
-
a.y + a.height <= b.y + EPS ||
|
|
17
|
-
b.y + b.height <= a.y + EPS
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function containsPoint(r: XYRect, x: number, y: number) {
|
|
22
|
-
return (
|
|
23
|
-
x >= r.x - EPS &&
|
|
24
|
-
x <= r.x + r.width + EPS &&
|
|
25
|
-
y >= r.y - EPS &&
|
|
26
|
-
y <= r.y + r.height + EPS
|
|
27
|
-
)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function distancePointToRectEdges(px: number, py: number, r: XYRect) {
|
|
31
|
-
const edges: [number, number, number, number][] = [
|
|
32
|
-
[r.x, r.y, r.x + r.width, r.y],
|
|
33
|
-
[r.x + r.width, r.y, r.x + r.width, r.y + r.height],
|
|
34
|
-
[r.x + r.width, r.y + r.height, r.x, r.y + r.height],
|
|
35
|
-
[r.x, r.y + r.height, r.x, r.y],
|
|
36
|
-
]
|
|
37
|
-
let best = Infinity
|
|
38
|
-
for (const [x1, y1, x2, y2] of edges) {
|
|
39
|
-
const A = px - x1,
|
|
40
|
-
B = py - y1,
|
|
41
|
-
C = x2 - x1,
|
|
42
|
-
D = y2 - y1
|
|
43
|
-
const dot = A * C + B * D
|
|
44
|
-
const lenSq = C * C + D * D
|
|
45
|
-
let t = lenSq !== 0 ? dot / lenSq : 0
|
|
46
|
-
t = clamp(t, 0, 1)
|
|
47
|
-
const xx = x1 + t * C
|
|
48
|
-
const yy = y1 + t * D
|
|
49
|
-
best = Math.min(best, Math.hypot(px - xx, py - yy))
|
|
50
|
-
}
|
|
51
|
-
return best
|
|
4
|
+
type ExpandDirectionParams = {
|
|
5
|
+
r: XYRect
|
|
6
|
+
bounds: XYRect
|
|
7
|
+
blockers: XYRect[]
|
|
8
|
+
maxAspect: number | null | undefined
|
|
52
9
|
}
|
|
53
10
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
function maxExpandRight(
|
|
57
|
-
r: XYRect,
|
|
58
|
-
bounds: XYRect,
|
|
59
|
-
blockers: XYRect[],
|
|
60
|
-
maxAspect: number | null | undefined,
|
|
61
|
-
) {
|
|
11
|
+
function maxExpandRight(params: ExpandDirectionParams) {
|
|
12
|
+
const { r, bounds, blockers, maxAspect } = params
|
|
62
13
|
// Start with board boundary
|
|
63
14
|
let maxWidth = bounds.x + bounds.width - r.x
|
|
64
15
|
|
|
@@ -94,12 +45,8 @@ function maxExpandRight(
|
|
|
94
45
|
return Math.max(0, e)
|
|
95
46
|
}
|
|
96
47
|
|
|
97
|
-
function maxExpandDown(
|
|
98
|
-
r
|
|
99
|
-
bounds: XYRect,
|
|
100
|
-
blockers: XYRect[],
|
|
101
|
-
maxAspect: number | null | undefined,
|
|
102
|
-
) {
|
|
48
|
+
function maxExpandDown(params: ExpandDirectionParams) {
|
|
49
|
+
const { r, bounds, blockers, maxAspect } = params
|
|
103
50
|
// Start with board boundary
|
|
104
51
|
let maxHeight = bounds.y + bounds.height - r.y
|
|
105
52
|
|
|
@@ -134,12 +81,8 @@ function maxExpandDown(
|
|
|
134
81
|
return Math.max(0, e)
|
|
135
82
|
}
|
|
136
83
|
|
|
137
|
-
function maxExpandLeft(
|
|
138
|
-
r
|
|
139
|
-
bounds: XYRect,
|
|
140
|
-
blockers: XYRect[],
|
|
141
|
-
maxAspect: number | null | undefined,
|
|
142
|
-
) {
|
|
84
|
+
function maxExpandLeft(params: ExpandDirectionParams) {
|
|
85
|
+
const { r, bounds, blockers, maxAspect } = params
|
|
143
86
|
// Start with board boundary
|
|
144
87
|
let minX = bounds.x
|
|
145
88
|
|
|
@@ -172,12 +115,8 @@ function maxExpandLeft(
|
|
|
172
115
|
return Math.max(0, e)
|
|
173
116
|
}
|
|
174
117
|
|
|
175
|
-
function maxExpandUp(
|
|
176
|
-
r
|
|
177
|
-
bounds: XYRect,
|
|
178
|
-
blockers: XYRect[],
|
|
179
|
-
maxAspect: number | null | undefined,
|
|
180
|
-
) {
|
|
118
|
+
function maxExpandUp(params: ExpandDirectionParams) {
|
|
119
|
+
const { r, bounds, blockers, maxAspect } = params
|
|
181
120
|
// Start with board boundary
|
|
182
121
|
let minY = bounds.y
|
|
183
122
|
|
|
@@ -271,25 +210,27 @@ export function expandRectFromSeed(params: {
|
|
|
271
210
|
let improved = true
|
|
272
211
|
while (improved) {
|
|
273
212
|
improved = false
|
|
274
|
-
const
|
|
213
|
+
const commonParams = { bounds, blockers, maxAspect: maxAspectRatio }
|
|
214
|
+
|
|
215
|
+
const eR = maxExpandRight({ ...commonParams, r })
|
|
275
216
|
if (eR > 0) {
|
|
276
217
|
r = { ...r, width: r.width + eR }
|
|
277
218
|
improved = true
|
|
278
219
|
}
|
|
279
220
|
|
|
280
|
-
const eD = maxExpandDown(
|
|
221
|
+
const eD = maxExpandDown({ ...commonParams, r })
|
|
281
222
|
if (eD > 0) {
|
|
282
223
|
r = { ...r, height: r.height + eD }
|
|
283
224
|
improved = true
|
|
284
225
|
}
|
|
285
226
|
|
|
286
|
-
const eL = maxExpandLeft(
|
|
227
|
+
const eL = maxExpandLeft({ ...commonParams, r })
|
|
287
228
|
if (eL > 0) {
|
|
288
229
|
r = { x: r.x - eL, y: r.y, width: r.width + eL, height: r.height }
|
|
289
230
|
improved = true
|
|
290
231
|
}
|
|
291
232
|
|
|
292
|
-
const eU = maxExpandUp(
|
|
233
|
+
const eU = maxExpandUp({ ...commonParams, r })
|
|
293
234
|
if (eU > 0) {
|
|
294
235
|
r = { x: r.x, y: r.y - eU, width: r.width, height: r.height + eU }
|
|
295
236
|
improved = true
|
|
@@ -307,43 +248,3 @@ export function expandRectFromSeed(params: {
|
|
|
307
248
|
|
|
308
249
|
return best
|
|
309
250
|
}
|
|
310
|
-
|
|
311
|
-
/** Find the intersection of two 1D intervals, or null if they don't overlap. */
|
|
312
|
-
export function intersect1D(a0: number, a1: number, b0: number, b1: number) {
|
|
313
|
-
const lo = Math.max(a0, b0)
|
|
314
|
-
const hi = Math.min(a1, b1)
|
|
315
|
-
return hi > lo + EPS ? ([lo, hi] as const) : null
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/** Return A \ B as up to 4 non-overlapping rectangles (or [A] if no overlap). */
|
|
319
|
-
export function subtractRect2D(A: XYRect, B: XYRect): XYRect[] {
|
|
320
|
-
if (!overlaps(A, B)) return [A]
|
|
321
|
-
|
|
322
|
-
const Xi = intersect1D(A.x, A.x + A.width, B.x, B.x + B.width)
|
|
323
|
-
const Yi = intersect1D(A.y, A.y + A.height, B.y, B.y + B.height)
|
|
324
|
-
if (!Xi || !Yi) return [A]
|
|
325
|
-
|
|
326
|
-
const [X0, X1] = Xi
|
|
327
|
-
const [Y0, Y1] = Yi
|
|
328
|
-
const out: XYRect[] = []
|
|
329
|
-
|
|
330
|
-
// Left strip
|
|
331
|
-
if (X0 > A.x + EPS) {
|
|
332
|
-
out.push({ x: A.x, y: A.y, width: X0 - A.x, height: A.height })
|
|
333
|
-
}
|
|
334
|
-
// Right strip
|
|
335
|
-
if (A.x + A.width > X1 + EPS) {
|
|
336
|
-
out.push({ x: X1, y: A.y, width: A.x + A.width - X1, height: A.height })
|
|
337
|
-
}
|
|
338
|
-
// Top wedge in the middle band
|
|
339
|
-
const midW = Math.max(0, X1 - X0)
|
|
340
|
-
if (midW > EPS && Y0 > A.y + EPS) {
|
|
341
|
-
out.push({ x: X0, y: A.y, width: midW, height: Y0 - A.y })
|
|
342
|
-
}
|
|
343
|
-
// Bottom wedge in the middle band
|
|
344
|
-
if (midW > EPS && A.y + A.height > Y1 + EPS) {
|
|
345
|
-
out.push({ x: X0, y: Y1, width: midW, height: A.y + A.height - Y1 })
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return out.filter((r) => r.width > EPS && r.height > EPS)
|
|
349
|
-
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Placed3D, Rect3d, XYRect } from "../rectdiff-types"
|
|
2
|
+
|
|
3
|
+
export function finalizeRects(params: {
|
|
4
|
+
placed: Placed3D[]
|
|
5
|
+
obstaclesByLayer: XYRect[][]
|
|
6
|
+
boardVoidRects: XYRect[]
|
|
7
|
+
}): Rect3d[] {
|
|
8
|
+
// Convert all placed (free space) nodes to output format
|
|
9
|
+
const out: Rect3d[] = params.placed.map((p) => ({
|
|
10
|
+
minX: p.rect.x,
|
|
11
|
+
minY: p.rect.y,
|
|
12
|
+
maxX: p.rect.x + p.rect.width,
|
|
13
|
+
maxY: p.rect.y + p.rect.height,
|
|
14
|
+
zLayers: [...p.zLayers].sort((a, b) => a - b),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Recover obstacles as mesh nodes.
|
|
19
|
+
* Obstacles are stored per-layer in `obstaclesByLayer`, but we want to emit
|
|
20
|
+
* single 3D nodes for multi-layer obstacles if they share the same rect.
|
|
21
|
+
* We use the `XYRect` object reference identity to group layers.
|
|
22
|
+
*/
|
|
23
|
+
const layersByObstacleRect = new Map<XYRect, number[]>()
|
|
24
|
+
|
|
25
|
+
params.obstaclesByLayer.forEach((layerObs, z) => {
|
|
26
|
+
for (const rect of layerObs) {
|
|
27
|
+
const layerIndices = layersByObstacleRect.get(rect) ?? []
|
|
28
|
+
layerIndices.push(z)
|
|
29
|
+
layersByObstacleRect.set(rect, layerIndices)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Append obstacle nodes to the output
|
|
34
|
+
const voidSet = new Set(params.boardVoidRects || [])
|
|
35
|
+
for (const [rect, layerIndices] of layersByObstacleRect.entries()) {
|
|
36
|
+
if (voidSet.has(rect)) continue // Skip void rects
|
|
37
|
+
|
|
38
|
+
out.push({
|
|
39
|
+
minX: rect.x,
|
|
40
|
+
minY: rect.y,
|
|
41
|
+
maxX: rect.x + rect.width,
|
|
42
|
+
maxY: rect.y + rect.height,
|
|
43
|
+
zLayers: layerIndices.sort((a, b) => a - b),
|
|
44
|
+
isObstacle: true,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return out
|
|
49
|
+
}
|