@tscircuit/rectdiff 0.0.18 → 0.0.19
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 +3 -19
- package/dist/index.js +175 -97
- package/lib/fixtures/twoNodeExpansionFixture.ts +2 -9
- package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +50 -106
- package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +22 -7
- package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +3 -9
- package/lib/utils/expandRectFromSeed.ts +86 -2
- package/lib/utils/isSelfRect.ts +15 -0
- 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 +3 -3
- 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,25 @@ 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) placedIndex.insert(rectToTree(placement.rect))
|
|
120
60
|
}
|
|
121
61
|
}
|
|
122
62
|
}
|
|
@@ -126,51 +66,41 @@ export class RectDiffExpansionSolver extends BaseSolver {
|
|
|
126
66
|
|
|
127
67
|
this._stepExpansion()
|
|
128
68
|
|
|
129
|
-
this.stats.gridIndex = this.gridIndex
|
|
130
|
-
this.stats.placed = this.placed.length
|
|
69
|
+
this.stats.gridIndex = this.input.gridIndex
|
|
70
|
+
this.stats.placed = this.input.placed.length
|
|
131
71
|
|
|
132
|
-
if (this.expansionIndex >= this.placed.length) {
|
|
72
|
+
if (this.input.expansionIndex >= this.input.placed.length) {
|
|
133
73
|
this.finalizeIfNeeded()
|
|
134
74
|
}
|
|
135
75
|
}
|
|
136
76
|
|
|
137
77
|
private _stepExpansion(): void {
|
|
138
|
-
if (this.expansionIndex >= this.placed.length) {
|
|
78
|
+
if (this.input.expansionIndex >= this.input.placed.length) {
|
|
139
79
|
return
|
|
140
80
|
}
|
|
141
81
|
|
|
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
|
-
}
|
|
82
|
+
const idx = this.input.expansionIndex
|
|
83
|
+
const p = this.input.placed[idx]!
|
|
84
|
+
const lastGrid =
|
|
85
|
+
this.input.options.gridSizes[this.input.options.gridSizes.length - 1]!
|
|
158
86
|
|
|
159
87
|
const oldRect = p.rect
|
|
160
88
|
const expanded = expandRectFromSeed({
|
|
161
89
|
startX: p.rect.x + p.rect.width / 2,
|
|
162
90
|
startY: p.rect.y + p.rect.height / 2,
|
|
163
91
|
gridSize: lastGrid,
|
|
164
|
-
bounds: this.bounds,
|
|
165
|
-
|
|
92
|
+
bounds: this.input.bounds,
|
|
93
|
+
obsticalIndexByLayer: this.input.obstacleIndexByLayer,
|
|
94
|
+
placedIndexByLayer: this.placedIndexByLayer,
|
|
166
95
|
initialCellRatio: 0,
|
|
167
96
|
maxAspectRatio: null,
|
|
168
97
|
minReq: { width: p.rect.width, height: p.rect.height },
|
|
98
|
+
zLayers: p.zLayers,
|
|
169
99
|
})
|
|
170
100
|
|
|
171
101
|
if (expanded) {
|
|
172
102
|
// Update placement + per-layer index (replace old rect object)
|
|
173
|
-
this.placed[idx] = { rect: expanded, zLayers: p.zLayers }
|
|
103
|
+
this.input.placed[idx] = { rect: expanded, zLayers: p.zLayers }
|
|
174
104
|
for (const z of p.zLayers) {
|
|
175
105
|
const tree = this.placedIndexByLayer[z]
|
|
176
106
|
if (tree) {
|
|
@@ -182,25 +112,25 @@ export class RectDiffExpansionSolver extends BaseSolver {
|
|
|
182
112
|
// Carve overlapped soft neighbors (respect full-stack nodes)
|
|
183
113
|
resizeSoftOverlaps(
|
|
184
114
|
{
|
|
185
|
-
layerCount: this.layerCount,
|
|
186
|
-
placed: this.placed,
|
|
187
|
-
options: this.options,
|
|
115
|
+
layerCount: this.input.layerCount,
|
|
116
|
+
placed: this.input.placed,
|
|
117
|
+
options: this.input.options,
|
|
188
118
|
placedIndexByLayer: this.placedIndexByLayer,
|
|
189
119
|
},
|
|
190
120
|
idx,
|
|
191
121
|
)
|
|
192
122
|
}
|
|
193
123
|
|
|
194
|
-
this.expansionIndex += 1
|
|
124
|
+
this.input.expansionIndex += 1
|
|
195
125
|
}
|
|
196
126
|
|
|
197
127
|
private finalizeIfNeeded() {
|
|
198
128
|
if (this.solved) return
|
|
199
129
|
|
|
200
130
|
const rects = finalizeRects({
|
|
201
|
-
placed: this.placed,
|
|
202
|
-
srj: this.srj,
|
|
203
|
-
boardVoidRects: this.boardVoidRects,
|
|
131
|
+
placed: this.input.placed,
|
|
132
|
+
srj: this.input.srj,
|
|
133
|
+
boardVoidRects: this.input.boardVoidRects,
|
|
204
134
|
})
|
|
205
135
|
this._meshNodes = rectsToMeshNodes(rects)
|
|
206
136
|
this.solved = true
|
|
@@ -208,25 +138,39 @@ export class RectDiffExpansionSolver extends BaseSolver {
|
|
|
208
138
|
|
|
209
139
|
computeProgress(): number {
|
|
210
140
|
if (this.solved) return 1
|
|
211
|
-
const grids = this.options.gridSizes.length
|
|
141
|
+
const grids = this.input.options.gridSizes.length
|
|
212
142
|
const base = grids / (grids + 1)
|
|
213
|
-
const denom = Math.max(1, this.placed.length)
|
|
214
|
-
const frac = denom ? this.expansionIndex / denom : 1
|
|
143
|
+
const denom = Math.max(1, this.input.placed.length)
|
|
144
|
+
const frac = denom ? this.input.expansionIndex / denom : 1
|
|
215
145
|
return Math.min(0.999, base + frac * (1 / (grids + 1)))
|
|
216
146
|
}
|
|
217
147
|
|
|
218
148
|
override getOutput(): { meshNodes: CapacityMeshNode[] } {
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
149
|
+
if (this.solved) return { meshNodes: this._meshNodes }
|
|
150
|
+
|
|
151
|
+
// Provide a live preview of the placements before finalization so debuggers
|
|
152
|
+
// can inspect intermediary states without forcing the solver to finish.
|
|
153
|
+
const previewNodes: CapacityMeshNode[] = this.input.placed.map(
|
|
154
|
+
(placement, idx) => ({
|
|
155
|
+
capacityMeshNodeId: `expand-preview-${idx}`,
|
|
156
|
+
center: {
|
|
157
|
+
x: placement.rect.x + placement.rect.width / 2,
|
|
158
|
+
y: placement.rect.y + placement.rect.height / 2,
|
|
159
|
+
},
|
|
160
|
+
width: placement.rect.width,
|
|
161
|
+
height: placement.rect.height,
|
|
162
|
+
availableZ: placement.zLayers.slice(),
|
|
163
|
+
layer: `z${placement.zLayers.join(",")}`,
|
|
164
|
+
}),
|
|
165
|
+
)
|
|
166
|
+
return { meshNodes: previewNodes }
|
|
223
167
|
}
|
|
224
168
|
|
|
225
169
|
/** Simple visualization of expanded placements. */
|
|
226
170
|
override visualize(): GraphicsObject {
|
|
227
171
|
const rects: NonNullable<GraphicsObject["rects"]> = []
|
|
228
172
|
|
|
229
|
-
for (const placement of this.placed ?? []) {
|
|
173
|
+
for (const placement of this.input.placed ?? []) {
|
|
230
174
|
rects.push({
|
|
231
175
|
center: {
|
|
232
176
|
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
|
|
|
@@ -238,23 +238,17 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
238
238
|
const ordered = preferMultiLayer ? attempts : attempts.reverse()
|
|
239
239
|
|
|
240
240
|
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
241
|
const rect = expandRectFromSeed({
|
|
250
242
|
startX: cand.x,
|
|
251
243
|
startY: cand.y,
|
|
252
244
|
gridSize: grid,
|
|
253
245
|
bounds: this.bounds,
|
|
254
|
-
|
|
246
|
+
obsticalIndexByLayer: this.input.obstacleIndexByLayer,
|
|
247
|
+
placedIndexByLayer: this.placedIndexByLayer,
|
|
255
248
|
initialCellRatio,
|
|
256
249
|
maxAspectRatio,
|
|
257
250
|
minReq: attempt.minReq,
|
|
251
|
+
zLayers: attempt.layers,
|
|
258
252
|
})
|
|
259
253
|
if (!rect) continue
|
|
260
254
|
|
|
@@ -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,40 @@ 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
|
+
|
|
220
|
+
// Ignore the existing placement we are expanding so it doesn't self-block.
|
|
221
|
+
|
|
222
|
+
const collectBlockers = (searchRect: XYRect) => {
|
|
223
|
+
const query = toQueryRect({ bounds, rect: searchRect })
|
|
224
|
+
if (!query) return
|
|
225
|
+
for (const z of params.zLayers) {
|
|
226
|
+
const blockersIndex = obsticalIndexByLayer[z]
|
|
227
|
+
if (blockersIndex) {
|
|
228
|
+
for (const entry of blockersIndex.search(query))
|
|
229
|
+
addBlocker({ rect: toRect(entry), seen, blockers })
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const placedLayer = placedIndexByLayer[z]
|
|
233
|
+
if (placedLayer) {
|
|
234
|
+
for (const entry of placedLayer.search(query)) {
|
|
235
|
+
const rect = toRect(entry)
|
|
236
|
+
if (
|
|
237
|
+
isSelfRect({
|
|
238
|
+
rect,
|
|
239
|
+
startX,
|
|
240
|
+
startY,
|
|
241
|
+
initialW,
|
|
242
|
+
initialH,
|
|
243
|
+
})
|
|
244
|
+
)
|
|
245
|
+
continue
|
|
246
|
+
addBlocker({ rect, seen, blockers })
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
176
251
|
|
|
177
252
|
const strategies = [
|
|
178
253
|
{ ox: 0, oy: 0 },
|
|
@@ -192,6 +267,7 @@ export function expandRectFromSeed(params: {
|
|
|
192
267
|
width: initialW,
|
|
193
268
|
height: initialH,
|
|
194
269
|
}
|
|
270
|
+
collectBlockers(r)
|
|
195
271
|
|
|
196
272
|
// keep initial inside board
|
|
197
273
|
if (
|
|
@@ -212,27 +288,35 @@ export function expandRectFromSeed(params: {
|
|
|
212
288
|
improved = false
|
|
213
289
|
const commonParams = { bounds, blockers, maxAspect: maxAspectRatio }
|
|
214
290
|
|
|
291
|
+
collectBlockers(searchStripRight({ rect: r, bounds }))
|
|
215
292
|
const eR = maxExpandRight({ ...commonParams, r })
|
|
216
293
|
if (eR > 0) {
|
|
217
294
|
r = { ...r, width: r.width + eR }
|
|
295
|
+
collectBlockers(r)
|
|
218
296
|
improved = true
|
|
219
297
|
}
|
|
220
298
|
|
|
299
|
+
collectBlockers(searchStripDown({ rect: r, bounds }))
|
|
221
300
|
const eD = maxExpandDown({ ...commonParams, r })
|
|
222
301
|
if (eD > 0) {
|
|
223
302
|
r = { ...r, height: r.height + eD }
|
|
303
|
+
collectBlockers(r)
|
|
224
304
|
improved = true
|
|
225
305
|
}
|
|
226
306
|
|
|
307
|
+
collectBlockers(searchStripLeft({ rect: r, bounds }))
|
|
227
308
|
const eL = maxExpandLeft({ ...commonParams, r })
|
|
228
309
|
if (eL > 0) {
|
|
229
310
|
r = { x: r.x - eL, y: r.y, width: r.width + eL, height: r.height }
|
|
311
|
+
collectBlockers(r)
|
|
230
312
|
improved = true
|
|
231
313
|
}
|
|
232
314
|
|
|
315
|
+
collectBlockers(searchStripUp({ rect: r, bounds }))
|
|
233
316
|
const eU = maxExpandUp({ ...commonParams, r })
|
|
234
317
|
if (eU > 0) {
|
|
235
318
|
r = { x: r.x, y: r.y - eU, width: r.width, height: r.height + eU }
|
|
319
|
+
collectBlockers(r)
|
|
236
320
|
improved = true
|
|
237
321
|
}
|
|
238
322
|
}
|
|
@@ -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
|
|
@@ -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
|
+
})
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svg width="640" height="480" viewBox="0 0 640 480" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="white"/><g><rect data-type="rect" data-label="node" data-x="
|
|
1
|
+
<svg width="640" height="480" viewBox="0 0 640 480" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="white"/><g><polyline data-points="0,0 12,0 12,4 0,4 0,0" data-type="line" data-label="bounds" points="40,333.3333333333333 600,333.3333333333333 600,146.66666666666666 40,146.66666666666666 40,333.3333333333333" fill="none" stroke="#111827" stroke-width="4.666666666666667"/></g><g><rect data-type="rect" data-label="node" data-x="4.75" data-y="2" x="40" y="146.66666666666666" width="443.3333333333333" height="186.66666666666666" fill="#dbeafe" stroke="black" stroke-width="0.02142857142857143"/></g><g><rect data-type="rect" data-label="node" data-x="10.75" data-y="2" x="483.3333333333333" y="146.66666666666666" width="116.66666666666669" height="186.66666666666666" fill="#dbeafe" stroke="black" stroke-width="0.02142857142857143"/></g><g id="crosshair" style="display: none"><line id="crosshair-h" y1="0" y2="480" stroke="#666" stroke-width="0.5"/><line id="crosshair-v" x1="0" x2="640" stroke="#666" stroke-width="0.5"/><text id="coordinates" font-family="monospace" font-size="12" fill="#666"></text></g><script><![CDATA[
|
|
2
2
|
document.currentScript.parentElement.addEventListener('mousemove', (e) => {
|
|
3
3
|
const svg = e.currentTarget;
|
|
4
4
|
const rect = svg.getBoundingClientRect();
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
v.setAttribute('y2', '480');
|
|
21
21
|
|
|
22
22
|
// Calculate real coordinates using inverse transformation
|
|
23
|
-
const matrix = {"a":
|
|
23
|
+
const matrix = {"a":46.666666666666664,"c":0,"e":40,"b":0,"d":-46.666666666666664,"f":333.3333333333333};
|
|
24
24
|
// Manually invert and apply the affine transform
|
|
25
25
|
// Since we only use translate and scale, we can directly compute:
|
|
26
26
|
// x' = (x - tx) / sx
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { GraphicsObject, Line } from "graphics-debug"
|
|
2
|
+
import type { SimpleRouteJson } from "lib/types/srj-types"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a GraphicsObject that draws the SRJ outline (or bounds fallback).
|
|
6
|
+
*/
|
|
7
|
+
export const makeSimpleRouteOutlineGraphics = (
|
|
8
|
+
srj: SimpleRouteJson,
|
|
9
|
+
): GraphicsObject => {
|
|
10
|
+
const lines: NonNullable<Line[]> = []
|
|
11
|
+
|
|
12
|
+
if (srj.outline && srj.outline.length > 1) {
|
|
13
|
+
lines.push({
|
|
14
|
+
points: [...srj.outline, srj.outline[0]!],
|
|
15
|
+
strokeColor: "#111827",
|
|
16
|
+
strokeWidth: 0.1,
|
|
17
|
+
label: "outline",
|
|
18
|
+
})
|
|
19
|
+
} else {
|
|
20
|
+
const { minX, maxX, minY, maxY } = srj.bounds
|
|
21
|
+
lines.push({
|
|
22
|
+
points: [
|
|
23
|
+
{ x: minX, y: minY },
|
|
24
|
+
{ x: maxX, y: minY },
|
|
25
|
+
{ x: maxX, y: maxY },
|
|
26
|
+
{ x: minX, y: maxY },
|
|
27
|
+
{ x: minX, y: minY },
|
|
28
|
+
],
|
|
29
|
+
strokeColor: "#111827",
|
|
30
|
+
strokeWidth: 0.1,
|
|
31
|
+
label: "bounds",
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
title: "SimpleRoute Outline",
|
|
37
|
+
coordinateSystem: "cartesian",
|
|
38
|
+
lines,
|
|
39
|
+
points: [],
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -28,7 +28,7 @@ test("RectDiffSolver supports incremental stepping", () => {
|
|
|
28
28
|
|
|
29
29
|
// Step advances one candidate at a time
|
|
30
30
|
let stepCount = 0
|
|
31
|
-
const maxSteps =
|
|
31
|
+
const maxSteps = 5000 // safety limit
|
|
32
32
|
|
|
33
33
|
while (!pipeline.solved && stepCount < maxSteps) {
|
|
34
34
|
pipeline.step()
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { expect, test } from "bun:test"
|
|
2
|
-
import { getSvgFromGraphicsObject } from "graphics-debug"
|
|
2
|
+
import { getSvgFromGraphicsObject, mergeGraphics } from "graphics-debug"
|
|
3
3
|
import { RectDiffExpansionSolver } from "lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver"
|
|
4
4
|
import { createTwoNodeExpansionInput } from "lib/fixtures/twoNodeExpansionFixture"
|
|
5
5
|
import { makeCapacityMeshNodeWithLayerInfo } from "./fixtures/makeCapacityMeshNodeWithLayerInfo"
|
|
6
|
+
import { makeSimpleRouteOutlineGraphics } from "./fixtures/makeSimpleRouteOutlineGraphics"
|
|
6
7
|
|
|
7
8
|
test("RectDiff expansion reproduces the two-node gap fixture", async () => {
|
|
8
|
-
const
|
|
9
|
+
const input = createTwoNodeExpansionInput()
|
|
10
|
+
const solver = new RectDiffExpansionSolver(input)
|
|
9
11
|
|
|
10
12
|
solver.solve()
|
|
11
13
|
|
|
@@ -13,8 +15,9 @@ test("RectDiff expansion reproduces the two-node gap fixture", async () => {
|
|
|
13
15
|
expect(meshNodes.length).toBeGreaterThanOrEqual(2)
|
|
14
16
|
|
|
15
17
|
const finalGraphics = makeCapacityMeshNodeWithLayerInfo(meshNodes)
|
|
18
|
+
const outline = makeSimpleRouteOutlineGraphics(input.srj)
|
|
16
19
|
const svg = getSvgFromGraphicsObject(
|
|
17
|
-
{ rects: finalGraphics.values().toArray().flat() },
|
|
20
|
+
mergeGraphics({ rects: finalGraphics.values().toArray().flat() }, outline),
|
|
18
21
|
{
|
|
19
22
|
svgWidth: 640,
|
|
20
23
|
svgHeight: 480,
|