@tscircuit/rectdiff 0.0.12 → 0.0.14
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 +169 -27
- package/dist/index.js +2012 -1672
- 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/visuallyOffsetLine.ts +5 -2
- package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +252 -0
- package/lib/solvers/{rectdiff → RectDiffExpansionSolver}/rectsToMeshNodes.ts +1 -2
- package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +106 -0
- package/lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts +70 -0
- package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +487 -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} +44 -225
- 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 +60 -0
- package/lib/types/capacity-mesh-types.ts +9 -0
- package/lib/utils/buildHardPlacedByLayer.ts +14 -0
- package/lib/{solvers/rectdiff/geometry.ts → utils/expandRectFromSeed.ts} +21 -120
- package/lib/utils/finalizeRects.ts +54 -0
- package/lib/utils/isFullyOccupiedAtPoint.ts +28 -0
- package/lib/utils/rectToTree.ts +10 -0
- package/lib/utils/rectdiff-geometry.ts +94 -0
- package/lib/utils/resizeSoftOverlaps.ts +103 -0
- package/lib/utils/sameTreeRect.ts +7 -0
- package/package.json +1 -1
- package/tests/board-outline.test.ts +2 -1
- package/tests/examples/example01.test.tsx +18 -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,9 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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"
|
|
5
|
+
import type RBush from "rbush"
|
|
6
|
+
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
188
7
|
|
|
189
8
|
/**
|
|
190
9
|
* Compute exact uncovered segments along a 1D line.
|
|
@@ -262,16 +81,16 @@ export function computeEdgeCandidates3D(params: {
|
|
|
262
81
|
bounds: XYRect
|
|
263
82
|
minSize: number
|
|
264
83
|
layerCount: number
|
|
265
|
-
|
|
266
|
-
|
|
84
|
+
obstacleIndexByLayer: Array<RBush<RTreeRect> | undefined>
|
|
85
|
+
placedIndexByLayer: Array<RBush<RTreeRect> | undefined>
|
|
267
86
|
hardPlacedByLayer: XYRect[][]
|
|
268
87
|
}): Candidate3D[] {
|
|
269
88
|
const {
|
|
270
89
|
bounds,
|
|
271
90
|
minSize,
|
|
272
91
|
layerCount,
|
|
273
|
-
|
|
274
|
-
|
|
92
|
+
obstacleIndexByLayer,
|
|
93
|
+
placedIndexByLayer,
|
|
275
94
|
hardPlacedByLayer,
|
|
276
95
|
} = params
|
|
277
96
|
|
|
@@ -279,20 +98,20 @@ export function computeEdgeCandidates3D(params: {
|
|
|
279
98
|
// Use small inset from edges for placement
|
|
280
99
|
const δ = Math.max(minSize * 0.15, EPS * 3)
|
|
281
100
|
const dedup = new Set<string>()
|
|
282
|
-
const key = (x: number
|
|
283
|
-
`${z}|${x.toFixed(6)}|${y.toFixed(6)}`
|
|
101
|
+
const key = (p: { x: number; y: number; z: number }) =>
|
|
102
|
+
`${p.z}|${p.x.toFixed(6)}|${p.y.toFixed(6)}`
|
|
284
103
|
|
|
285
|
-
function fullyOcc(x: number
|
|
286
|
-
return
|
|
287
|
-
x,
|
|
288
|
-
y,
|
|
104
|
+
function fullyOcc(p: { x: number; y: number }) {
|
|
105
|
+
return isFullyOccupiedAtPoint({
|
|
289
106
|
layerCount,
|
|
290
|
-
|
|
291
|
-
|
|
107
|
+
obstacleIndexByLayer,
|
|
108
|
+
placedIndexByLayer,
|
|
109
|
+
point: p,
|
|
292
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,21 +119,21 @@ 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 = [
|
|
307
|
-
...(
|
|
126
|
+
...(obstacleIndexByLayer[z]?.all() ?? []),
|
|
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
|
|
|
@@ -326,15 +145,15 @@ export function computeEdgeCandidates3D(params: {
|
|
|
326
145
|
layerCount,
|
|
327
146
|
minSpan: 1,
|
|
328
147
|
maxSpan: undefined,
|
|
329
|
-
|
|
330
|
-
|
|
148
|
+
obstacleIndexByLayer,
|
|
149
|
+
additionalBlockersByLayer: hardPlacedByLayer,
|
|
331
150
|
})
|
|
332
151
|
out.push({ x, y, z, distance: d, zSpanLen: span.length, isEdgeSeed: true })
|
|
333
152
|
}
|
|
334
153
|
|
|
335
154
|
for (let z = 0; z < layerCount; z++) {
|
|
336
155
|
const blockers = [
|
|
337
|
-
...(
|
|
156
|
+
...(obstacleIndexByLayer[z]?.all() ?? []),
|
|
338
157
|
...(hardPlacedByLayer[z] ?? []),
|
|
339
158
|
]
|
|
340
159
|
|
|
@@ -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,60 @@
|
|
|
1
|
+
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
2
|
+
import type { XYRect } from "../../rectdiff-types"
|
|
3
|
+
import { clamp, containsPoint } from "../../utils/rectdiff-geometry"
|
|
4
|
+
import type RBush from "rbush"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Find the longest contiguous free span around z (optionally capped).
|
|
8
|
+
*/
|
|
9
|
+
export function longestFreeSpanAroundZ(params: {
|
|
10
|
+
x: number
|
|
11
|
+
y: number
|
|
12
|
+
z: number
|
|
13
|
+
layerCount: number
|
|
14
|
+
minSpan: number
|
|
15
|
+
maxSpan: number | undefined
|
|
16
|
+
obstacleIndexByLayer: Array<RBush<RTreeRect> | undefined>
|
|
17
|
+
additionalBlockersByLayer?: XYRect[][]
|
|
18
|
+
}): number[] {
|
|
19
|
+
const {
|
|
20
|
+
x,
|
|
21
|
+
y,
|
|
22
|
+
z,
|
|
23
|
+
layerCount,
|
|
24
|
+
minSpan,
|
|
25
|
+
maxSpan,
|
|
26
|
+
obstacleIndexByLayer,
|
|
27
|
+
additionalBlockersByLayer,
|
|
28
|
+
} = params
|
|
29
|
+
|
|
30
|
+
const isFreeAt = (layer: number) => {
|
|
31
|
+
const query = {
|
|
32
|
+
minX: x,
|
|
33
|
+
minY: y,
|
|
34
|
+
maxX: x,
|
|
35
|
+
maxY: y,
|
|
36
|
+
}
|
|
37
|
+
const obstacleIdx = obstacleIndexByLayer[layer]
|
|
38
|
+
if (obstacleIdx && obstacleIdx.search(query).length > 0) return false
|
|
39
|
+
|
|
40
|
+
const extras = additionalBlockersByLayer?.[layer] ?? []
|
|
41
|
+
return !extras.some((b) => containsPoint(b, { x, y }))
|
|
42
|
+
}
|
|
43
|
+
let lo = z
|
|
44
|
+
let hi = z
|
|
45
|
+
while (lo - 1 >= 0 && isFreeAt(lo - 1)) lo--
|
|
46
|
+
while (hi + 1 < layerCount && isFreeAt(hi + 1)) hi++
|
|
47
|
+
|
|
48
|
+
if (typeof maxSpan === "number") {
|
|
49
|
+
const target = clamp(maxSpan, 1, layerCount)
|
|
50
|
+
// trim symmetrically (keeping z inside)
|
|
51
|
+
while (hi - lo + 1 > target) {
|
|
52
|
+
if (z - lo > hi - z) lo++
|
|
53
|
+
else hi--
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const res: number[] = []
|
|
58
|
+
for (let i = lo; i <= hi; i++) res.push(i)
|
|
59
|
+
return res.length >= minSpan ? res : []
|
|
60
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { XYRect } from "lib/rectdiff-types"
|
|
2
|
+
|
|
1
3
|
export type CapacityMeshNodeId = string
|
|
2
4
|
|
|
3
5
|
export interface CapacityMesh {
|
|
@@ -31,3 +33,10 @@ export interface CapacityMeshEdge {
|
|
|
31
33
|
capacityMeshEdgeId: string
|
|
32
34
|
nodeIds: [CapacityMeshNodeId, CapacityMeshNodeId]
|
|
33
35
|
}
|
|
36
|
+
|
|
37
|
+
export type RTreeRect = XYRect & {
|
|
38
|
+
minX: number
|
|
39
|
+
minY: number
|
|
40
|
+
maxX: number
|
|
41
|
+
maxY: number
|
|
42
|
+
}
|
|
@@ -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
|
+
}
|