@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.
Files changed (37) hide show
  1. package/dist/index.d.ts +169 -27
  2. package/dist/index.js +2012 -1672
  3. package/lib/RectDiffPipeline.ts +18 -17
  4. package/lib/{solvers/rectdiff/types.ts → rectdiff-types.ts} +0 -34
  5. package/lib/{solvers/rectdiff/visualization.ts → rectdiff-visualization.ts} +2 -1
  6. package/lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts +9 -12
  7. package/lib/solvers/GapFillSolver/visuallyOffsetLine.ts +5 -2
  8. package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +252 -0
  9. package/lib/solvers/{rectdiff → RectDiffExpansionSolver}/rectsToMeshNodes.ts +1 -2
  10. package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +106 -0
  11. package/lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts +70 -0
  12. package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +487 -0
  13. package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +109 -0
  14. package/lib/solvers/RectDiffSeedingSolver/computeDefaultGridSizes.ts +9 -0
  15. package/lib/solvers/{rectdiff/candidates.ts → RectDiffSeedingSolver/computeEdgeCandidates3D.ts} +44 -225
  16. package/lib/solvers/{rectdiff/geometry → RectDiffSeedingSolver}/computeInverseRects.ts +6 -2
  17. package/lib/solvers/{rectdiff → RectDiffSeedingSolver}/layers.ts +4 -3
  18. package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +60 -0
  19. package/lib/types/capacity-mesh-types.ts +9 -0
  20. package/lib/utils/buildHardPlacedByLayer.ts +14 -0
  21. package/lib/{solvers/rectdiff/geometry.ts → utils/expandRectFromSeed.ts} +21 -120
  22. package/lib/utils/finalizeRects.ts +54 -0
  23. package/lib/utils/isFullyOccupiedAtPoint.ts +28 -0
  24. package/lib/utils/rectToTree.ts +10 -0
  25. package/lib/utils/rectdiff-geometry.ts +94 -0
  26. package/lib/utils/resizeSoftOverlaps.ts +103 -0
  27. package/lib/utils/sameTreeRect.ts +7 -0
  28. package/package.json +1 -1
  29. package/tests/board-outline.test.ts +2 -1
  30. package/tests/examples/example01.test.tsx +18 -1
  31. package/tests/obstacle-extra-layers.test.ts +1 -1
  32. package/tests/obstacle-zlayers.test.ts +1 -1
  33. package/utils/rectsEqual.ts +2 -2
  34. package/utils/rectsOverlap.ts +2 -2
  35. package/lib/solvers/RectDiffSolver.ts +0 -231
  36. package/lib/solvers/rectdiff/engine.ts +0 -481
  37. /package/lib/solvers/{rectdiff/geometry → RectDiffSeedingSolver}/isPointInPolygon.ts +0 -0
@@ -1,190 +1,9 @@
1
- // lib/solvers/rectdiff/candidates.ts
2
- import type { Candidate3D, XYRect } from "./types"
3
- import { EPS, clamp, containsPoint, distancePointToRectEdges } from "./geometry"
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"
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
- obstaclesByLayer: XYRect[][]
266
- placedByLayer: XYRect[][]
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
- obstaclesByLayer,
274
- placedByLayer,
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, y: number, z: 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, y: number) {
286
- return isFullyOccupiedAllLayers({
287
- x,
288
- y,
104
+ function fullyOcc(p: { x: number; y: number }) {
105
+ return isFullyOccupiedAtPoint({
289
106
  layerCount,
290
- obstaclesByLayer,
291
- placedByLayer,
107
+ obstacleIndexByLayer,
108
+ placedIndexByLayer,
109
+ point: p,
292
110
  })
293
111
  }
294
112
 
295
- function pushIfFree(x: number, y: number, z: 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
- ...(obstaclesByLayer[z] ?? []),
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
- obstaclesByLayer,
330
- placedByLayer: hardPlacedByLayer,
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
- ...(obstaclesByLayer[z] ?? []),
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 "../types"
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
- // lib/solvers/rectdiff/layers.ts
2
- import type { SimpleRouteJson, Obstacle } from "../../types/srj-types"
3
- import type { XYRect } from "./types"
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
+ }