@tscircuit/rectdiff 0.0.18 → 0.0.20
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 +4 -19
- package/dist/index.js +202 -131
- package/lib/fixtures/twoNodeExpansionFixture.ts +2 -9
- package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +55 -108
- package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +22 -7
- package/lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts +2 -1
- package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +5 -16
- package/lib/types/capacity-mesh-types.ts +1 -0
- package/lib/utils/expandRectFromSeed.ts +89 -2
- package/lib/utils/isSelfRect.ts +15 -0
- package/lib/utils/rectToTree.ts +5 -1
- package/lib/utils/resizeSoftOverlaps.ts +8 -16
- package/lib/utils/searchStrip.ts +50 -0
- package/package.json +1 -1
- package/tests/__snapshots__/should-expand-node.snap.svg +2 -2
- package/tests/fixtures/makeSimpleRouteOutlineGraphics.ts +41 -0
- package/tests/incremental-solver.test.ts +1 -1
- package/tests/should-expand-node.test.ts +6 -3
- package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +1 -1
- package/tests/solver/rectDiffGridSolverPipeline.test.ts +5 -1
|
@@ -3,28 +3,21 @@ import type { GraphicsObject } from "graphics-debug"
|
|
|
3
3
|
import type { CapacityMeshNode, RTreeRect } from "lib/types/capacity-mesh-types"
|
|
4
4
|
import { expandRectFromSeed } from "../../utils/expandRectFromSeed"
|
|
5
5
|
import { finalizeRects } from "../../utils/finalizeRects"
|
|
6
|
-
import { allLayerNode } from "../../utils/buildHardPlacedByLayer"
|
|
7
6
|
import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps"
|
|
8
7
|
import { rectsToMeshNodes } from "./rectsToMeshNodes"
|
|
9
8
|
import type { XYRect, Candidate3D, Placed3D } from "../../rectdiff-types"
|
|
10
9
|
import type { SimpleRouteJson } from "lib/types/srj-types"
|
|
11
|
-
import {
|
|
12
|
-
buildZIndexMap,
|
|
13
|
-
obstacleToXYRect,
|
|
14
|
-
obstacleZs,
|
|
15
|
-
} from "../RectDiffSeedingSolver/layers"
|
|
16
10
|
import RBush from "rbush"
|
|
17
11
|
import { rectToTree } from "../../utils/rectToTree"
|
|
18
12
|
import { sameTreeRect } from "../../utils/sameTreeRect"
|
|
19
13
|
|
|
20
|
-
export type
|
|
14
|
+
export type RectDiffExpansionSolverInput = {
|
|
21
15
|
srj: SimpleRouteJson
|
|
22
16
|
layerNames: string[]
|
|
23
17
|
layerCount: number
|
|
24
18
|
bounds: XYRect
|
|
25
19
|
options: {
|
|
26
20
|
gridSizes: number[]
|
|
27
|
-
// the engine only uses gridSizes here, other options are ignored
|
|
28
21
|
[key: string]: any
|
|
29
22
|
}
|
|
30
23
|
boardVoidRects: XYRect[]
|
|
@@ -35,10 +28,6 @@ export type RectDiffExpansionSolverSnapshot = {
|
|
|
35
28
|
edgeAnalysisDone: boolean
|
|
36
29
|
totalSeedsThisGrid: number
|
|
37
30
|
consumedSeedsThisGrid: number
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export type RectDiffExpansionSolverInput = {
|
|
41
|
-
initialSnapshot: RectDiffExpansionSolverSnapshot
|
|
42
31
|
obstacleIndexByLayer: Array<RBush<RTreeRect>>
|
|
43
32
|
}
|
|
44
33
|
|
|
@@ -49,74 +38,28 @@ export type RectDiffExpansionSolverInput = {
|
|
|
49
38
|
* and runs the EXPANSION phase, then finalizes to capacity mesh nodes.
|
|
50
39
|
*/
|
|
51
40
|
export class RectDiffExpansionSolver extends BaseSolver {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
private layerNames!: string[]
|
|
55
|
-
private layerCount!: number
|
|
56
|
-
private bounds!: XYRect
|
|
57
|
-
private options!: {
|
|
58
|
-
gridSizes: number[]
|
|
59
|
-
// the engine only uses gridSizes here, other options are ignored
|
|
60
|
-
[key: string]: any
|
|
61
|
-
}
|
|
62
|
-
private boardVoidRects!: XYRect[]
|
|
63
|
-
private gridIndex!: number
|
|
64
|
-
private candidates!: Candidate3D[]
|
|
65
|
-
private placed!: Placed3D[]
|
|
66
|
-
private placedIndexByLayer!: Array<RBush<RTreeRect>>
|
|
67
|
-
private expansionIndex!: number
|
|
68
|
-
private edgeAnalysisDone!: boolean
|
|
69
|
-
private totalSeedsThisGrid!: number
|
|
70
|
-
private consumedSeedsThisGrid!: number
|
|
71
|
-
|
|
72
|
-
private _meshNodes: CapacityMeshNode[] = []
|
|
73
|
-
|
|
41
|
+
placedIndexByLayer: Array<RBush<RTreeRect>> = []
|
|
42
|
+
_meshNodes: CapacityMeshNode[] = []
|
|
74
43
|
constructor(private input: RectDiffExpansionSolverInput) {
|
|
75
44
|
super()
|
|
76
|
-
// Copy engine snapshot fields directly onto this solver instance
|
|
77
|
-
Object.assign(this, this.input.initialSnapshot)
|
|
78
45
|
}
|
|
79
46
|
|
|
80
47
|
override _setup() {
|
|
81
48
|
this.stats = {
|
|
82
|
-
gridIndex: this.gridIndex,
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (this.input.obstacleIndexByLayer) {
|
|
86
|
-
} else {
|
|
87
|
-
const { zIndexByName } = buildZIndexMap(this.srj)
|
|
88
|
-
this.input.obstacleIndexByLayer = Array.from(
|
|
89
|
-
{ length: this.layerCount },
|
|
90
|
-
() => new RBush<RTreeRect>(),
|
|
91
|
-
)
|
|
92
|
-
const insertObstacle = (rect: XYRect, z: number) => {
|
|
93
|
-
const tree = this.input.obstacleIndexByLayer[z]
|
|
94
|
-
if (tree) tree.insert(rectToTree(rect))
|
|
95
|
-
}
|
|
96
|
-
for (const voidRect of this.boardVoidRects ?? []) {
|
|
97
|
-
for (let z = 0; z < this.layerCount; z++) insertObstacle(voidRect, z)
|
|
98
|
-
}
|
|
99
|
-
for (const obstacle of this.srj.obstacles ?? []) {
|
|
100
|
-
const rect = obstacleToXYRect(obstacle as any)
|
|
101
|
-
if (!rect) continue
|
|
102
|
-
const zLayers =
|
|
103
|
-
obstacle.zLayers?.length && obstacle.zLayers.length > 0
|
|
104
|
-
? obstacle.zLayers
|
|
105
|
-
: obstacleZs(obstacle as any, zIndexByName)
|
|
106
|
-
zLayers.forEach((z) => {
|
|
107
|
-
if (z >= 0 && z < this.layerCount) insertObstacle(rect, z)
|
|
108
|
-
})
|
|
109
|
-
}
|
|
49
|
+
gridIndex: this.input.gridIndex,
|
|
110
50
|
}
|
|
111
51
|
|
|
112
52
|
this.placedIndexByLayer = Array.from(
|
|
113
|
-
{ length: this.layerCount },
|
|
53
|
+
{ length: this.input.layerCount },
|
|
114
54
|
() => new RBush<RTreeRect>(),
|
|
115
55
|
)
|
|
116
|
-
for (const placement of this.placed
|
|
56
|
+
for (const placement of this.input.placed) {
|
|
117
57
|
for (const z of placement.zLayers) {
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
58
|
+
const placedIndex = this.placedIndexByLayer[z]
|
|
59
|
+
if (placedIndex)
|
|
60
|
+
placedIndex.insert(
|
|
61
|
+
rectToTree(placement.rect, { zLayers: placement.zLayers }),
|
|
62
|
+
)
|
|
120
63
|
}
|
|
121
64
|
}
|
|
122
65
|
}
|
|
@@ -126,81 +69,71 @@ export class RectDiffExpansionSolver extends BaseSolver {
|
|
|
126
69
|
|
|
127
70
|
this._stepExpansion()
|
|
128
71
|
|
|
129
|
-
this.stats.gridIndex = this.gridIndex
|
|
130
|
-
this.stats.placed = this.placed.length
|
|
72
|
+
this.stats.gridIndex = this.input.gridIndex
|
|
73
|
+
this.stats.placed = this.input.placed.length
|
|
131
74
|
|
|
132
|
-
if (this.expansionIndex >= this.placed.length) {
|
|
75
|
+
if (this.input.expansionIndex >= this.input.placed.length) {
|
|
133
76
|
this.finalizeIfNeeded()
|
|
134
77
|
}
|
|
135
78
|
}
|
|
136
79
|
|
|
137
80
|
private _stepExpansion(): void {
|
|
138
|
-
if (this.expansionIndex >= this.placed.length) {
|
|
81
|
+
if (this.input.expansionIndex >= this.input.placed.length) {
|
|
139
82
|
return
|
|
140
83
|
}
|
|
141
84
|
|
|
142
|
-
const idx = this.expansionIndex
|
|
143
|
-
const p = this.placed[idx]!
|
|
144
|
-
const lastGrid =
|
|
145
|
-
|
|
146
|
-
const hardPlacedByLayer = allLayerNode({
|
|
147
|
-
layerCount: this.layerCount,
|
|
148
|
-
placed: this.placed,
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
// HARD blockers only: obstacles on p.zLayers + full-stack nodes
|
|
152
|
-
const hardBlockers: XYRect[] = []
|
|
153
|
-
for (const z of p.zLayers) {
|
|
154
|
-
const obstacleTree = this.input.obstacleIndexByLayer[z]
|
|
155
|
-
if (obstacleTree) hardBlockers.push(...obstacleTree.all())
|
|
156
|
-
hardBlockers.push(...(hardPlacedByLayer[z] ?? []))
|
|
157
|
-
}
|
|
85
|
+
const idx = this.input.expansionIndex
|
|
86
|
+
const p = this.input.placed[idx]!
|
|
87
|
+
const lastGrid =
|
|
88
|
+
this.input.options.gridSizes[this.input.options.gridSizes.length - 1]!
|
|
158
89
|
|
|
159
90
|
const oldRect = p.rect
|
|
160
91
|
const expanded = expandRectFromSeed({
|
|
161
92
|
startX: p.rect.x + p.rect.width / 2,
|
|
162
93
|
startY: p.rect.y + p.rect.height / 2,
|
|
163
94
|
gridSize: lastGrid,
|
|
164
|
-
bounds: this.bounds,
|
|
165
|
-
|
|
95
|
+
bounds: this.input.bounds,
|
|
96
|
+
obsticalIndexByLayer: this.input.obstacleIndexByLayer,
|
|
97
|
+
placedIndexByLayer: this.placedIndexByLayer,
|
|
166
98
|
initialCellRatio: 0,
|
|
167
99
|
maxAspectRatio: null,
|
|
168
100
|
minReq: { width: p.rect.width, height: p.rect.height },
|
|
101
|
+
zLayers: p.zLayers,
|
|
169
102
|
})
|
|
170
103
|
|
|
171
104
|
if (expanded) {
|
|
172
105
|
// Update placement + per-layer index (replace old rect object)
|
|
173
|
-
this.placed[idx] = { rect: expanded, zLayers: p.zLayers }
|
|
106
|
+
this.input.placed[idx] = { rect: expanded, zLayers: p.zLayers }
|
|
174
107
|
for (const z of p.zLayers) {
|
|
175
108
|
const tree = this.placedIndexByLayer[z]
|
|
176
109
|
if (tree) {
|
|
177
|
-
tree.remove(rectToTree(oldRect), sameTreeRect)
|
|
178
|
-
tree.insert(rectToTree(expanded))
|
|
110
|
+
tree.remove(rectToTree(oldRect, { zLayers: p.zLayers }), sameTreeRect)
|
|
111
|
+
tree.insert(rectToTree(expanded, { zLayers: p.zLayers }))
|
|
179
112
|
}
|
|
180
113
|
}
|
|
181
114
|
|
|
182
115
|
// Carve overlapped soft neighbors (respect full-stack nodes)
|
|
183
116
|
resizeSoftOverlaps(
|
|
184
117
|
{
|
|
185
|
-
layerCount: this.layerCount,
|
|
186
|
-
placed: this.placed,
|
|
187
|
-
options: this.options,
|
|
118
|
+
layerCount: this.input.layerCount,
|
|
119
|
+
placed: this.input.placed,
|
|
120
|
+
options: this.input.options,
|
|
188
121
|
placedIndexByLayer: this.placedIndexByLayer,
|
|
189
122
|
},
|
|
190
123
|
idx,
|
|
191
124
|
)
|
|
192
125
|
}
|
|
193
126
|
|
|
194
|
-
this.expansionIndex += 1
|
|
127
|
+
this.input.expansionIndex += 1
|
|
195
128
|
}
|
|
196
129
|
|
|
197
130
|
private finalizeIfNeeded() {
|
|
198
131
|
if (this.solved) return
|
|
199
132
|
|
|
200
133
|
const rects = finalizeRects({
|
|
201
|
-
placed: this.placed,
|
|
202
|
-
srj: this.srj,
|
|
203
|
-
boardVoidRects: this.boardVoidRects,
|
|
134
|
+
placed: this.input.placed,
|
|
135
|
+
srj: this.input.srj,
|
|
136
|
+
boardVoidRects: this.input.boardVoidRects,
|
|
204
137
|
})
|
|
205
138
|
this._meshNodes = rectsToMeshNodes(rects)
|
|
206
139
|
this.solved = true
|
|
@@ -208,25 +141,39 @@ export class RectDiffExpansionSolver extends BaseSolver {
|
|
|
208
141
|
|
|
209
142
|
computeProgress(): number {
|
|
210
143
|
if (this.solved) return 1
|
|
211
|
-
const grids = this.options.gridSizes.length
|
|
144
|
+
const grids = this.input.options.gridSizes.length
|
|
212
145
|
const base = grids / (grids + 1)
|
|
213
|
-
const denom = Math.max(1, this.placed.length)
|
|
214
|
-
const frac = denom ? this.expansionIndex / denom : 1
|
|
146
|
+
const denom = Math.max(1, this.input.placed.length)
|
|
147
|
+
const frac = denom ? this.input.expansionIndex / denom : 1
|
|
215
148
|
return Math.min(0.999, base + frac * (1 / (grids + 1)))
|
|
216
149
|
}
|
|
217
150
|
|
|
218
151
|
override getOutput(): { meshNodes: CapacityMeshNode[] } {
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
152
|
+
if (this.solved) return { meshNodes: this._meshNodes }
|
|
153
|
+
|
|
154
|
+
// Provide a live preview of the placements before finalization so debuggers
|
|
155
|
+
// can inspect intermediary states without forcing the solver to finish.
|
|
156
|
+
const previewNodes: CapacityMeshNode[] = this.input.placed.map(
|
|
157
|
+
(placement, idx) => ({
|
|
158
|
+
capacityMeshNodeId: `expand-preview-${idx}`,
|
|
159
|
+
center: {
|
|
160
|
+
x: placement.rect.x + placement.rect.width / 2,
|
|
161
|
+
y: placement.rect.y + placement.rect.height / 2,
|
|
162
|
+
},
|
|
163
|
+
width: placement.rect.width,
|
|
164
|
+
height: placement.rect.height,
|
|
165
|
+
availableZ: placement.zLayers.slice(),
|
|
166
|
+
layer: `z${placement.zLayers.join(",")}`,
|
|
167
|
+
}),
|
|
168
|
+
)
|
|
169
|
+
return { meshNodes: previewNodes }
|
|
223
170
|
}
|
|
224
171
|
|
|
225
172
|
/** Simple visualization of expanded placements. */
|
|
226
173
|
override visualize(): GraphicsObject {
|
|
227
174
|
const rects: NonNullable<GraphicsObject["rects"]> = []
|
|
228
175
|
|
|
229
|
-
for (const placement of this.placed ?? []) {
|
|
176
|
+
for (const placement of this.input.placed ?? []) {
|
|
230
177
|
rects.push({
|
|
231
178
|
center: {
|
|
232
179
|
x: placement.rect.x + placement.rect.width / 2,
|
|
@@ -48,15 +48,30 @@ export class RectDiffGridSolverPipeline extends BasePipelineSolver<RectDiffGridS
|
|
|
48
48
|
definePipelineStep(
|
|
49
49
|
"rectDiffExpansionSolver",
|
|
50
50
|
RectDiffExpansionSolver,
|
|
51
|
-
(pipeline: RectDiffGridSolverPipeline) =>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
(pipeline: RectDiffGridSolverPipeline) => {
|
|
52
|
+
const output = pipeline.rectDiffSeedingSolver?.getOutput()
|
|
53
|
+
if (!output) {
|
|
54
|
+
throw new Error("RectDiffSeedingSolver did not produce output")
|
|
55
|
+
}
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
srj: pipeline.inputProblem.simpleRouteJson,
|
|
59
|
+
layerNames: output.layerNames ?? [],
|
|
55
60
|
boardVoidRects: pipeline.inputProblem.boardVoidRects ?? [],
|
|
61
|
+
layerCount: pipeline.inputProblem.simpleRouteJson.layerCount,
|
|
62
|
+
bounds: output.bounds!,
|
|
63
|
+
candidates: output.candidates,
|
|
64
|
+
consumedSeedsThisGrid: output.placed.length,
|
|
65
|
+
totalSeedsThisGrid: output.candidates.length,
|
|
66
|
+
placed: output.placed,
|
|
67
|
+
edgeAnalysisDone: output.edgeAnalysisDone,
|
|
68
|
+
gridIndex: output.gridIndex,
|
|
69
|
+
expansionIndex: output.expansionIndex,
|
|
70
|
+
obstacleIndexByLayer: pipeline.obstacleIndexByLayer,
|
|
71
|
+
options: output.options,
|
|
56
72
|
},
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
],
|
|
73
|
+
]
|
|
74
|
+
},
|
|
60
75
|
),
|
|
61
76
|
]
|
|
62
77
|
|
|
@@ -30,12 +30,13 @@ export const buildObstacleIndexesByLayer = (params: {
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
const insertObstacle = (rect: XYRect, z: number) => {
|
|
33
|
-
const treeRect = {
|
|
33
|
+
const treeRect: RTreeRect = {
|
|
34
34
|
...rect,
|
|
35
35
|
minX: rect.x,
|
|
36
36
|
minY: rect.y,
|
|
37
37
|
maxX: rect.x + rect.width,
|
|
38
38
|
maxY: rect.y + rect.height,
|
|
39
|
+
zLayers: [z],
|
|
39
40
|
}
|
|
40
41
|
obstacleIndexByLayer[z]?.insert(treeRect)
|
|
41
42
|
}
|
|
@@ -21,6 +21,7 @@ import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps"
|
|
|
21
21
|
import { getColorForZLayer } from "lib/utils/getColorForZLayer"
|
|
22
22
|
import RBush from "rbush"
|
|
23
23
|
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
24
|
+
import { rectToTree } from "lib/utils/rectToTree"
|
|
24
25
|
|
|
25
26
|
export type RectDiffSeedingSolverInput = {
|
|
26
27
|
simpleRouteJson: SimpleRouteJson
|
|
@@ -238,23 +239,17 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
238
239
|
const ordered = preferMultiLayer ? attempts : attempts.reverse()
|
|
239
240
|
|
|
240
241
|
for (const attempt of ordered) {
|
|
241
|
-
// HARD blockers only: obstacles on those layers + full-stack nodes
|
|
242
|
-
const hardBlockers: XYRect[] = []
|
|
243
|
-
for (const z of attempt.layers) {
|
|
244
|
-
const obstacleLayer = this.input.obstacleIndexByLayer[z]
|
|
245
|
-
if (obstacleLayer) hardBlockers.push(...obstacleLayer.all())
|
|
246
|
-
if (hardPlacedByLayer[z]) hardBlockers.push(...hardPlacedByLayer[z]!)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
242
|
const rect = expandRectFromSeed({
|
|
250
243
|
startX: cand.x,
|
|
251
244
|
startY: cand.y,
|
|
252
245
|
gridSize: grid,
|
|
253
246
|
bounds: this.bounds,
|
|
254
|
-
|
|
247
|
+
obsticalIndexByLayer: this.input.obstacleIndexByLayer,
|
|
248
|
+
placedIndexByLayer: this.placedIndexByLayer,
|
|
255
249
|
initialCellRatio,
|
|
256
250
|
maxAspectRatio,
|
|
257
251
|
minReq: attempt.minReq,
|
|
252
|
+
zLayers: attempt.layers,
|
|
258
253
|
})
|
|
259
254
|
if (!rect) continue
|
|
260
255
|
|
|
@@ -264,13 +259,7 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
264
259
|
for (const z of attempt.layers) {
|
|
265
260
|
const idx = this.placedIndexByLayer[z]
|
|
266
261
|
if (idx) {
|
|
267
|
-
idx.insert({
|
|
268
|
-
...rect,
|
|
269
|
-
minX: rect.x,
|
|
270
|
-
minY: rect.y,
|
|
271
|
-
maxX: rect.x + rect.width,
|
|
272
|
-
maxY: rect.y + rect.height,
|
|
273
|
-
})
|
|
262
|
+
idx.insert(rectToTree(rect, { zLayers: placed.zLayers }))
|
|
274
263
|
}
|
|
275
264
|
}
|
|
276
265
|
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
import type RBush from "rbush"
|
|
1
2
|
import type { XYRect } from "../rectdiff-types"
|
|
2
3
|
import { EPS, gt, gte, lt, lte, overlaps } from "./rectdiff-geometry"
|
|
4
|
+
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
5
|
+
import { isSelfRect } from "./isSelfRect"
|
|
6
|
+
import {
|
|
7
|
+
searchStripDown,
|
|
8
|
+
searchStripLeft,
|
|
9
|
+
searchStripRight,
|
|
10
|
+
searchStripUp,
|
|
11
|
+
} from "./searchStrip"
|
|
3
12
|
|
|
4
13
|
type ExpandDirectionParams = {
|
|
5
14
|
r: XYRect
|
|
@@ -148,23 +157,55 @@ function maxExpandUp(params: ExpandDirectionParams) {
|
|
|
148
157
|
return Math.max(0, e)
|
|
149
158
|
}
|
|
150
159
|
|
|
160
|
+
const toRect = (tree: RTreeRect): XYRect => ({
|
|
161
|
+
x: tree.minX,
|
|
162
|
+
y: tree.minY,
|
|
163
|
+
width: tree.maxX - tree.minX,
|
|
164
|
+
height: tree.maxY - tree.minY,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const addBlocker = (params: {
|
|
168
|
+
rect: XYRect
|
|
169
|
+
seen: Set<string>
|
|
170
|
+
blockers: XYRect[]
|
|
171
|
+
}) => {
|
|
172
|
+
const { rect, seen, blockers } = params
|
|
173
|
+
const key = `${rect.x}|${rect.y}|${rect.width}|${rect.height}`
|
|
174
|
+
if (seen.has(key)) return
|
|
175
|
+
seen.add(key)
|
|
176
|
+
blockers.push(rect)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const toQueryRect = (params: { bounds: XYRect; rect: XYRect }) => {
|
|
180
|
+
const { bounds, rect } = params
|
|
181
|
+
const minX = Math.max(bounds.x, rect.x)
|
|
182
|
+
const minY = Math.max(bounds.y, rect.y)
|
|
183
|
+
const maxX = Math.min(bounds.x + bounds.width, rect.x + rect.width)
|
|
184
|
+
const maxY = Math.min(bounds.y + bounds.height, rect.y + rect.height)
|
|
185
|
+
if (maxX <= minX + EPS || maxY <= minY + EPS) return null
|
|
186
|
+
return { minX, minY, maxX, maxY }
|
|
187
|
+
}
|
|
188
|
+
|
|
151
189
|
/** Grow a rect around a seed point, honoring bounds/blockers/aspect/min sizes. */
|
|
152
190
|
export function expandRectFromSeed(params: {
|
|
153
191
|
startX: number
|
|
154
192
|
startY: number
|
|
155
193
|
gridSize: number
|
|
156
194
|
bounds: XYRect
|
|
157
|
-
|
|
195
|
+
obsticalIndexByLayer: Array<RBush<RTreeRect>>
|
|
196
|
+
placedIndexByLayer: Array<RBush<RTreeRect>>
|
|
158
197
|
initialCellRatio: number
|
|
159
198
|
maxAspectRatio: number | null | undefined
|
|
160
199
|
minReq: { width: number; height: number }
|
|
200
|
+
zLayers: number[]
|
|
161
201
|
}): XYRect | null {
|
|
162
202
|
const {
|
|
163
203
|
startX,
|
|
164
204
|
startY,
|
|
165
205
|
gridSize,
|
|
166
206
|
bounds,
|
|
167
|
-
|
|
207
|
+
obsticalIndexByLayer,
|
|
208
|
+
placedIndexByLayer,
|
|
168
209
|
initialCellRatio,
|
|
169
210
|
maxAspectRatio,
|
|
170
211
|
minReq,
|
|
@@ -173,6 +214,43 @@ export function expandRectFromSeed(params: {
|
|
|
173
214
|
const minSide = Math.max(1e-9, gridSize * initialCellRatio)
|
|
174
215
|
const initialW = Math.max(minSide, minReq.width)
|
|
175
216
|
const initialH = Math.max(minSide, minReq.height)
|
|
217
|
+
const blockers: XYRect[] = []
|
|
218
|
+
const seen = new Set<string>()
|
|
219
|
+
const totalLayers = placedIndexByLayer.length
|
|
220
|
+
|
|
221
|
+
// Ignore the existing placement we are expanding so it doesn't self-block.
|
|
222
|
+
|
|
223
|
+
const collectBlockers = (searchRect: XYRect) => {
|
|
224
|
+
const query = toQueryRect({ bounds, rect: searchRect })
|
|
225
|
+
if (!query) return
|
|
226
|
+
for (const z of params.zLayers) {
|
|
227
|
+
const blockersIndex = obsticalIndexByLayer[z]
|
|
228
|
+
if (blockersIndex) {
|
|
229
|
+
for (const entry of blockersIndex.search(query))
|
|
230
|
+
addBlocker({ rect: toRect(entry), seen, blockers })
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const placedLayer = placedIndexByLayer[z]
|
|
234
|
+
if (placedLayer) {
|
|
235
|
+
for (const entry of placedLayer.search(query)) {
|
|
236
|
+
const isFullStack = entry.zLayers.length >= totalLayers
|
|
237
|
+
if (!isFullStack) continue
|
|
238
|
+
const rect = toRect(entry)
|
|
239
|
+
if (
|
|
240
|
+
isSelfRect({
|
|
241
|
+
rect,
|
|
242
|
+
startX,
|
|
243
|
+
startY,
|
|
244
|
+
initialW,
|
|
245
|
+
initialH,
|
|
246
|
+
})
|
|
247
|
+
)
|
|
248
|
+
continue
|
|
249
|
+
addBlocker({ rect, seen, blockers })
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
176
254
|
|
|
177
255
|
const strategies = [
|
|
178
256
|
{ ox: 0, oy: 0 },
|
|
@@ -192,6 +270,7 @@ export function expandRectFromSeed(params: {
|
|
|
192
270
|
width: initialW,
|
|
193
271
|
height: initialH,
|
|
194
272
|
}
|
|
273
|
+
collectBlockers(r)
|
|
195
274
|
|
|
196
275
|
// keep initial inside board
|
|
197
276
|
if (
|
|
@@ -212,27 +291,35 @@ export function expandRectFromSeed(params: {
|
|
|
212
291
|
improved = false
|
|
213
292
|
const commonParams = { bounds, blockers, maxAspect: maxAspectRatio }
|
|
214
293
|
|
|
294
|
+
collectBlockers(searchStripRight({ rect: r, bounds }))
|
|
215
295
|
const eR = maxExpandRight({ ...commonParams, r })
|
|
216
296
|
if (eR > 0) {
|
|
217
297
|
r = { ...r, width: r.width + eR }
|
|
298
|
+
collectBlockers(r)
|
|
218
299
|
improved = true
|
|
219
300
|
}
|
|
220
301
|
|
|
302
|
+
collectBlockers(searchStripDown({ rect: r, bounds }))
|
|
221
303
|
const eD = maxExpandDown({ ...commonParams, r })
|
|
222
304
|
if (eD > 0) {
|
|
223
305
|
r = { ...r, height: r.height + eD }
|
|
306
|
+
collectBlockers(r)
|
|
224
307
|
improved = true
|
|
225
308
|
}
|
|
226
309
|
|
|
310
|
+
collectBlockers(searchStripLeft({ rect: r, bounds }))
|
|
227
311
|
const eL = maxExpandLeft({ ...commonParams, r })
|
|
228
312
|
if (eL > 0) {
|
|
229
313
|
r = { x: r.x - eL, y: r.y, width: r.width + eL, height: r.height }
|
|
314
|
+
collectBlockers(r)
|
|
230
315
|
improved = true
|
|
231
316
|
}
|
|
232
317
|
|
|
318
|
+
collectBlockers(searchStripUp({ rect: r, bounds }))
|
|
233
319
|
const eU = maxExpandUp({ ...commonParams, r })
|
|
234
320
|
if (eU > 0) {
|
|
235
321
|
r = { x: r.x, y: r.y - eU, width: r.width, height: r.height + eU }
|
|
322
|
+
collectBlockers(r)
|
|
236
323
|
improved = true
|
|
237
324
|
}
|
|
238
325
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { XYRect } from "lib/rectdiff-types"
|
|
2
|
+
|
|
3
|
+
const EPS = 1e-9
|
|
4
|
+
|
|
5
|
+
export const isSelfRect = (params: {
|
|
6
|
+
rect: XYRect
|
|
7
|
+
startX: number
|
|
8
|
+
startY: number
|
|
9
|
+
initialW: number
|
|
10
|
+
initialH: number
|
|
11
|
+
}) =>
|
|
12
|
+
Math.abs(params.rect.x + params.rect.width / 2 - params.startX) < EPS &&
|
|
13
|
+
Math.abs(params.rect.y + params.rect.height / 2 - params.startY) < EPS &&
|
|
14
|
+
Math.abs(params.rect.width - params.initialW) < EPS &&
|
|
15
|
+
Math.abs(params.rect.height - params.initialH) < EPS
|
package/lib/utils/rectToTree.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { XYRect } from "lib/rectdiff-types"
|
|
2
2
|
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
3
3
|
|
|
4
|
-
export const rectToTree = (
|
|
4
|
+
export const rectToTree = (
|
|
5
|
+
rect: XYRect,
|
|
6
|
+
opts: { zLayers: number[] },
|
|
7
|
+
): RTreeRect => ({
|
|
5
8
|
...rect,
|
|
6
9
|
minX: rect.x,
|
|
7
10
|
minY: rect.y,
|
|
8
11
|
maxX: rect.x + rect.width,
|
|
9
12
|
maxY: rect.y + rect.height,
|
|
13
|
+
zLayers: opts.zLayers,
|
|
10
14
|
})
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
2
|
-
import type { Placed3D
|
|
2
|
+
import type { Placed3D } from "../rectdiff-types"
|
|
3
3
|
import { overlaps, subtractRect2D, EPS } from "./rectdiff-geometry"
|
|
4
4
|
import type RBush from "rbush"
|
|
5
|
+
import { rectToTree } from "./rectToTree"
|
|
5
6
|
|
|
6
7
|
export function resizeSoftOverlaps(
|
|
7
8
|
params: {
|
|
@@ -57,13 +58,6 @@ export function resizeSoftOverlaps(
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
// Remove fully overlapped nodes and keep indexes in sync
|
|
60
|
-
const rectToTree = (rect: XYRect): RTreeRect => ({
|
|
61
|
-
...rect,
|
|
62
|
-
minX: rect.x,
|
|
63
|
-
minY: rect.y,
|
|
64
|
-
maxX: rect.x + rect.width,
|
|
65
|
-
maxY: rect.y + rect.height,
|
|
66
|
-
})
|
|
67
61
|
const sameRect = (a: RTreeRect, b: RTreeRect) =>
|
|
68
62
|
a.minX === b.minX &&
|
|
69
63
|
a.minY === b.minY &&
|
|
@@ -77,7 +71,11 @@ export function resizeSoftOverlaps(
|
|
|
77
71
|
if (params.placedIndexByLayer) {
|
|
78
72
|
for (const z of rem.zLayers) {
|
|
79
73
|
const tree = params.placedIndexByLayer[z]
|
|
80
|
-
if (tree)
|
|
74
|
+
if (tree)
|
|
75
|
+
tree.remove(
|
|
76
|
+
rectToTree(rem.rect, { zLayers: rem.zLayers }),
|
|
77
|
+
sameRect,
|
|
78
|
+
)
|
|
81
79
|
}
|
|
82
80
|
}
|
|
83
81
|
})
|
|
@@ -89,13 +87,7 @@ export function resizeSoftOverlaps(
|
|
|
89
87
|
if (params.placedIndexByLayer) {
|
|
90
88
|
const idx = params.placedIndexByLayer[z]
|
|
91
89
|
if (idx) {
|
|
92
|
-
idx.insert({
|
|
93
|
-
...p.rect,
|
|
94
|
-
minX: p.rect.x,
|
|
95
|
-
minY: p.rect.y,
|
|
96
|
-
maxX: p.rect.x + p.rect.width,
|
|
97
|
-
maxY: p.rect.y + p.rect.height,
|
|
98
|
-
})
|
|
90
|
+
idx.insert(rectToTree(p.rect, { zLayers: p.zLayers.slice() }))
|
|
99
91
|
}
|
|
100
92
|
}
|
|
101
93
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { XYRect } from "lib/rectdiff-types"
|
|
2
|
+
|
|
3
|
+
export const searchStripRight = ({
|
|
4
|
+
rect,
|
|
5
|
+
bounds,
|
|
6
|
+
}: {
|
|
7
|
+
rect: XYRect
|
|
8
|
+
bounds: XYRect
|
|
9
|
+
}): XYRect => ({
|
|
10
|
+
x: rect.x,
|
|
11
|
+
y: rect.y,
|
|
12
|
+
width: bounds.x + bounds.width - rect.x,
|
|
13
|
+
height: rect.height,
|
|
14
|
+
})
|
|
15
|
+
export const searchStripDown = ({
|
|
16
|
+
rect,
|
|
17
|
+
bounds,
|
|
18
|
+
}: {
|
|
19
|
+
rect: XYRect
|
|
20
|
+
bounds: XYRect
|
|
21
|
+
}): XYRect => ({
|
|
22
|
+
x: rect.x,
|
|
23
|
+
y: rect.y,
|
|
24
|
+
width: rect.width,
|
|
25
|
+
height: bounds.y + bounds.height - rect.y,
|
|
26
|
+
})
|
|
27
|
+
export const searchStripLeft = ({
|
|
28
|
+
rect,
|
|
29
|
+
bounds,
|
|
30
|
+
}: {
|
|
31
|
+
rect: XYRect
|
|
32
|
+
bounds: XYRect
|
|
33
|
+
}): XYRect => ({
|
|
34
|
+
x: bounds.x,
|
|
35
|
+
y: rect.y,
|
|
36
|
+
width: rect.x - bounds.x,
|
|
37
|
+
height: rect.height,
|
|
38
|
+
})
|
|
39
|
+
export const searchStripUp = ({
|
|
40
|
+
rect,
|
|
41
|
+
bounds,
|
|
42
|
+
}: {
|
|
43
|
+
rect: XYRect
|
|
44
|
+
bounds: XYRect
|
|
45
|
+
}): XYRect => ({
|
|
46
|
+
x: rect.x,
|
|
47
|
+
y: bounds.y,
|
|
48
|
+
width: rect.width,
|
|
49
|
+
height: rect.y - bounds.y,
|
|
50
|
+
})
|