@tscircuit/rectdiff 0.0.33 → 0.0.35

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 (36) hide show
  1. package/components/SolverDebugger3d.tsx +64 -10
  2. package/lib/RectDiffPipeline.ts +23 -0
  3. package/lib/fixtures/twoNodeExpansionFixture.ts +1 -1
  4. package/lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts +2 -2
  5. package/lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts +1 -1
  6. package/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts +2 -2
  7. package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +311 -0
  8. package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +5 -2
  9. package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +8 -5
  10. package/lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts +6 -6
  11. package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +4 -4
  12. package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +1 -1
  13. package/lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts +1 -1
  14. package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +1 -1
  15. package/lib/types/capacity-mesh-types.ts +1 -1
  16. package/lib/types/srj-types.ts +1 -0
  17. package/lib/utils/expandRectFromSeed.ts +1 -1
  18. package/lib/utils/finalizeRects.ts +1 -1
  19. package/lib/utils/isFullyOccupiedAtPoint.ts +1 -1
  20. package/lib/utils/isSelfRect.ts +1 -1
  21. package/lib/utils/rectToTree.ts +2 -2
  22. package/lib/utils/renderObstacleClearance.ts +1 -1
  23. package/lib/utils/resizeSoftOverlaps.ts +1 -1
  24. package/lib/utils/sameTreeRect.ts +1 -1
  25. package/lib/utils/searchStrip.ts +1 -1
  26. package/lib/utils/z-filter.ts +43 -0
  27. package/package.json +6 -2
  28. package/tests/solver/arduino-uno-inner2-ground-bottom-power/__snapshots__/arduino-uno-inner2-ground-bottom-power.snap.svg +1 -1
  29. package/tests/solver/arduino-uno-inner2-ground-inner1-power/__snapshots__/arduino-uno-inner2-ground-inner1-power.snap.svg +3 -3
  30. package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
  31. package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
  32. package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
  33. package/tests/solver/repros/merge-single-layer-node/__snapshots__/merge-single-layer-node.snap.svg +44 -0
  34. package/tests/z-filter.test.ts +33 -0
  35. package/dist/index.d.ts +0 -381
  36. package/dist/index.js +0 -2764
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
2
2
  import { GenericSolverDebugger } from "@tscircuit/solver-utils/react"
3
3
  import type { SimpleRouteJson } from "../lib/types/srj-types"
4
4
  import type { CapacityMeshNode } from "../lib/types/capacity-mesh-types"
5
+ import { matchesExactZFilter, parseZFilterInput } from "../lib/utils/z-filter"
5
6
  import * as THREE from "three"
6
7
  import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"
7
8
  import type { BaseSolver } from "@tscircuit/solver-utils"
@@ -168,6 +169,7 @@ const ThreeBoardView: React.FC<{
168
169
  shrinkBoxes: boolean
169
170
  boxShrinkAmount: number
170
171
  showBorders: boolean
172
+ selectedZValues: number[] | null
171
173
  }> = ({
172
174
  nodes,
173
175
  srj,
@@ -181,6 +183,7 @@ const ThreeBoardView: React.FC<{
181
183
  shrinkBoxes,
182
184
  boxShrinkAmount,
183
185
  showBorders,
186
+ selectedZValues,
184
187
  }) => {
185
188
  const containerRef = useRef<HTMLDivElement>(null)
186
189
  const destroyRef = useRef<() => void>(() => {})
@@ -203,9 +206,17 @@ const ThreeBoardView: React.FC<{
203
206
 
204
207
  const layerCount = layerNames.length || srj?.layerCount || 1
205
208
 
206
- const prisms = useMemo(
207
- () => buildPrismsFromNodes(nodes, layerCount),
208
- [nodes, layerCount],
209
+ const filteredNodes = useMemo(
210
+ () =>
211
+ nodes.filter((node) =>
212
+ matchesExactZFilter(node.availableZ ?? [], selectedZValues),
213
+ ),
214
+ [nodes, selectedZValues],
215
+ )
216
+
217
+ const filteredPrisms = useMemo(
218
+ () => buildPrismsFromNodes(filteredNodes, layerCount),
219
+ [filteredNodes, layerCount],
209
220
  )
210
221
 
211
222
  useEffect(() => {
@@ -372,6 +383,8 @@ const ThreeBoardView: React.FC<{
372
383
  .map((name) => zIndexByLayerName.get(name))
373
384
  .filter((z): z is number => typeof z === "number")
374
385
 
386
+ if (!matchesExactZFilter(zs, selectedZValues)) continue
387
+
375
388
  for (const z of zs) {
376
389
  if (z < 0 || z >= layerCount) continue
377
390
  obstaclesGroup.add(
@@ -389,7 +402,7 @@ const ThreeBoardView: React.FC<{
389
402
 
390
403
  // Output prisms from nodes (wireframe toggle like the experiment)
391
404
  if (showOutput) {
392
- for (const p of prisms) {
405
+ for (const p of filteredPrisms) {
393
406
  let box = p
394
407
  if (shrinkBoxes && boxShrinkAmount > 0) {
395
408
  const s = boxShrinkAmount
@@ -442,7 +455,7 @@ const ThreeBoardView: React.FC<{
442
455
  z1: layerCount,
443
456
  }
444
457
  : (() => {
445
- if (prisms.length === 0) {
458
+ if (filteredPrisms.length === 0) {
446
459
  return {
447
460
  minX: -10,
448
461
  maxX: 10,
@@ -456,7 +469,7 @@ const ThreeBoardView: React.FC<{
456
469
  minY = Infinity,
457
470
  maxX = -Infinity,
458
471
  maxY = -Infinity
459
- for (const p of prisms) {
472
+ for (const p of filteredPrisms) {
460
473
  minX = Math.min(minX, p.minX)
461
474
  maxX = Math.max(maxX, p.maxX)
462
475
  minY = Math.min(minY, p.minY)
@@ -517,7 +530,7 @@ const ThreeBoardView: React.FC<{
517
530
  }
518
531
  }, [
519
532
  srj,
520
- prisms,
533
+ filteredPrisms,
521
534
  layerCount,
522
535
  layerThickness,
523
536
  height,
@@ -530,6 +543,7 @@ const ThreeBoardView: React.FC<{
530
543
  shrinkBoxes,
531
544
  boxShrinkAmount,
532
545
  showBorders,
546
+ selectedZValues,
533
547
  ])
534
548
 
535
549
  return (
@@ -572,10 +586,18 @@ export const SolverDebugger3d: React.FC<SolverDebugger3dProps> = ({
572
586
  const [shrinkBoxes, setShrinkBoxes] = useState(true)
573
587
  const [boxShrinkAmount, setBoxShrinkAmount] = useState(0.1)
574
588
  const [showBorders, setShowBorders] = useState(true)
589
+ const [zFilterInput, setZFilterInput] = useState("")
575
590
 
576
591
  // Mesh nodes state - updated when solver completes or during stepping
577
592
  const [meshNodes, setMeshNodes] = useState<CapacityMeshNode[]>([])
578
593
 
594
+ const selectedZValues = useMemo(
595
+ () => parseZFilterInput(zFilterInput),
596
+ [zFilterInput],
597
+ )
598
+ const hasInvalidZFilter =
599
+ zFilterInput.trim().length > 0 && selectedZValues === null
600
+
579
601
  // Update mesh nodes from solver output
580
602
  const updateMeshNodes = useCallback(() => {
581
603
  try {
@@ -601,11 +623,16 @@ export const SolverDebugger3d: React.FC<SolverDebugger3dProps> = ({
601
623
  // Poll for updates during stepping (GenericSolverDebugger doesn't have onStep)
602
624
  useEffect(() => {
603
625
  const interval = setInterval(() => {
604
- // Only update if solver has output available
605
- if (solver.solved || solver.stats?.placed > 0) {
626
+ if (solver.solved) {
606
627
  updateMeshNodes()
628
+ clearInterval(interval)
629
+ return
607
630
  }
608
- }, 100) // Poll every 100ms during active solving
631
+
632
+ if (solver.stats?.placed > 0) {
633
+ updateMeshNodes()
634
+ }
635
+ }, 100)
609
636
 
610
637
  return () => clearInterval(interval)
611
638
  }, [updateMeshNodes, solver])
@@ -724,6 +751,32 @@ export const SolverDebugger3d: React.FC<SolverDebugger3dProps> = ({
724
751
  />
725
752
  Wireframe
726
753
  </label>
754
+
755
+ <label
756
+ style={{
757
+ display: "inline-flex",
758
+ gap: 8,
759
+ alignItems: "center",
760
+ fontSize: 13,
761
+ }}
762
+ >
763
+ <span>Z Filter</span>
764
+ <input
765
+ type="text"
766
+ value={zFilterInput}
767
+ onChange={(e) => setZFilterInput(e.target.value)}
768
+ placeholder="1 or 1,2,3"
769
+ style={{
770
+ width: 120,
771
+ padding: "6px 10px",
772
+ borderRadius: 6,
773
+ border: `1px solid ${hasInvalidZFilter ? "#dc2626" : "#cbd5e1"}`,
774
+ background: "white",
775
+ fontSize: 13,
776
+ }}
777
+ title="Show only exact z matches. Example: 1 matches [1], 1,2,3 matches [1,2,3]."
778
+ />
779
+ </label>
727
780
  </>
728
781
  )}
729
782
  </div>
@@ -752,6 +805,7 @@ export const SolverDebugger3d: React.FC<SolverDebugger3dProps> = ({
752
805
  shrinkBoxes={shrinkBoxes}
753
806
  boxShrinkAmount={boxShrinkAmount}
754
807
  showBorders={showBorders}
808
+ selectedZValues={selectedZValues}
755
809
  />
756
810
  )}
757
811
  </div>
@@ -8,6 +8,7 @@ import type { GridFill3DOptions, XYRect } from "./rectdiff-types"
8
8
  import type { CapacityMeshNode } from "./types/capacity-mesh-types"
9
9
  import type { GraphicsObject } from "graphics-debug"
10
10
  import { GapFillSolverPipeline } from "./solvers/GapFillSolver/GapFillSolverPipeline"
11
+ import { OuterLayerContainmentMergeSolver } from "./solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver"
11
12
  import { RectDiffGridSolverPipeline } from "./solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline"
12
13
  import { createBaseVisualization } from "./rectdiff-visualization"
13
14
  import { buildFinalRectDiffVisualization } from "./buildFinalRectDiffVisualization"
@@ -25,6 +26,7 @@ export interface RectDiffPipelineInput {
25
26
  export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput> {
26
27
  rectDiffGridSolverPipeline?: RectDiffGridSolverPipeline
27
28
  gapFillSolver?: GapFillSolverPipeline
29
+ outerLayerContainmentMergeSolver?: OuterLayerContainmentMergeSolver
28
30
  boardVoidRects: XYRect[] | undefined
29
31
  zIndexByName?: Map<string, number>
30
32
  layerNames?: string[]
@@ -69,6 +71,22 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
69
71
  },
70
72
  ],
71
73
  ),
74
+ definePipelineStep(
75
+ "outerLayerContainmentMergeSolver",
76
+ OuterLayerContainmentMergeSolver,
77
+ (rectDiffPipeline: RectDiffPipeline) => [
78
+ {
79
+ meshNodes:
80
+ rectDiffPipeline.gapFillSolver?.getOutput().outputNodes ??
81
+ rectDiffPipeline.rectDiffGridSolverPipeline?.getOutput()
82
+ .meshNodes ??
83
+ [],
84
+ simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson,
85
+ zIndexByName: rectDiffPipeline.zIndexByName ?? new Map(),
86
+ obstacleClearance: rectDiffPipeline.inputProblem.obstacleClearance,
87
+ },
88
+ ],
89
+ ),
72
90
  ]
73
91
 
74
92
  override _setup(): void {
@@ -100,6 +118,11 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
100
118
  }
101
119
 
102
120
  override getOutput(): { meshNodes: CapacityMeshNode[] } {
121
+ const outerLayerMergeOutput =
122
+ this.outerLayerContainmentMergeSolver?.getOutput()
123
+ if (outerLayerMergeOutput) {
124
+ return { meshNodes: outerLayerMergeOutput.outputNodes }
125
+ }
103
126
  const gapFillOutput = this.gapFillSolver?.getOutput()
104
127
  if (gapFillOutput) {
105
128
  return { meshNodes: gapFillOutput.outputNodes }
@@ -2,7 +2,7 @@ import RBush from "rbush"
2
2
  import type { RectDiffExpansionSolverInput } from "../solvers/RectDiffExpansionSolver/RectDiffExpansionSolver"
3
3
  import type { SimpleRouteJson } from "../types/srj-types"
4
4
  import type { XYRect } from "../rectdiff-types"
5
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
5
+ import type { RTreeRect } from "../types/capacity-mesh-types"
6
6
  import { buildZIndexMap } from "../solvers/RectDiffSeedingSolver/layers"
7
7
 
8
8
  /**
@@ -1,5 +1,5 @@
1
1
  import { BaseSolver } from "@tscircuit/solver-utils"
2
- import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
2
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
3
3
  import type { SegmentWithAdjacentEmptySpace } from "./FindSegmentsWithAdjacentEmptySpaceSolver"
4
4
  import type { GraphicsObject } from "graphics-debug"
5
5
  import RBush from "rbush"
@@ -7,7 +7,7 @@ import { EDGE_MAP, EDGES } from "./edge-constants"
7
7
  import { getBoundsFromCorners } from "./getBoundsFromCorners"
8
8
  import type { Bounds } from "@tscircuit/math-utils"
9
9
  import { midpoint, segmentToBoxMinDistance } from "@tscircuit/math-utils"
10
- import type { XYRect } from "lib/rectdiff-types"
10
+ import type { XYRect } from "../../rectdiff-types"
11
11
 
12
12
  const EPS = 1e-4
13
13
 
@@ -1,7 +1,7 @@
1
1
  import { BaseSolver } from "@tscircuit/solver-utils"
2
2
  import Flatbush from "flatbush"
3
3
  import type { GraphicsObject, NinePointAnchor } from "graphics-debug"
4
- import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
4
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
5
5
  import { projectToUncoveredSegments } from "./projectToUncoveredSegments"
6
6
  import { EDGES } from "./edge-constants"
7
7
  import { visuallyOffsetLine } from "./visuallyOffsetLine"
@@ -3,11 +3,11 @@ import {
3
3
  definePipelineStep,
4
4
  type PipelineStep,
5
5
  } from "@tscircuit/solver-utils"
6
- import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
6
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
7
7
  import type { GraphicsObject } from "graphics-debug"
8
8
  import { FindSegmentsWithAdjacentEmptySpaceSolver } from "./FindSegmentsWithAdjacentEmptySpaceSolver"
9
9
  import { ExpandEdgesToEmptySpaceSolver } from "./ExpandEdgesToEmptySpaceSolver"
10
- import type { XYRect } from "lib/rectdiff-types"
10
+ import type { XYRect } from "../../rectdiff-types"
11
11
 
12
12
  type GapFillSolverInput = {
13
13
  meshNodes: CapacityMeshNode[]
@@ -0,0 +1,311 @@
1
+ import { BaseSolver } from "@tscircuit/solver-utils"
2
+ import type { GraphicsObject } from "graphics-debug"
3
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
4
+ import type { XYRect } from "../../rectdiff-types"
5
+ import type { Obstacle, SimpleRouteJson } from "../../types/srj-types"
6
+ import { obstacleToXYRect, obstacleZs } from "../RectDiffSeedingSolver/layers"
7
+ import { getColorForZLayer } from "../../utils/getColorForZLayer"
8
+ import { subtractRect2D, overlaps, EPS } from "../../utils/rectdiff-geometry"
9
+ import { padRect } from "../../utils/padRect"
10
+
11
+ type OuterLayerContainmentMergeSolverInput = {
12
+ meshNodes: CapacityMeshNode[]
13
+ simpleRouteJson: SimpleRouteJson
14
+ zIndexByName: Map<string, number>
15
+ obstacleClearance?: number
16
+ }
17
+
18
+ type ObstacleWithRect = {
19
+ obstacle: Obstacle
20
+ rect: XYRect
21
+ }
22
+
23
+ const nodeToRect = (node: CapacityMeshNode): XYRect => ({
24
+ x: node.center.x - node.width / 2,
25
+ y: node.center.y - node.height / 2,
26
+ width: node.width,
27
+ height: node.height,
28
+ })
29
+
30
+ const rectArea = (rect: XYRect) => rect.width * rect.height
31
+
32
+ const cloneNode = (node: CapacityMeshNode): CapacityMeshNode => ({
33
+ ...node,
34
+ center: { ...node.center },
35
+ availableZ: [...node.availableZ],
36
+ })
37
+
38
+ const cloneNodeWithRect = (
39
+ node: CapacityMeshNode,
40
+ rect: XYRect,
41
+ capacityMeshNodeId: string,
42
+ ): CapacityMeshNode => ({
43
+ ...node,
44
+ capacityMeshNodeId,
45
+ center: {
46
+ x: rect.x + rect.width / 2,
47
+ y: rect.y + rect.height / 2,
48
+ },
49
+ width: rect.width,
50
+ height: rect.height,
51
+ availableZ: [...node.availableZ],
52
+ layer: `z${node.availableZ.join(",")}`,
53
+ })
54
+
55
+ const isFreeNode = (node: CapacityMeshNode) =>
56
+ !node._containsObstacle && !node._containsTarget
57
+
58
+ const isSingletonOuterNode = (node: CapacityMeshNode, outerZ: number) =>
59
+ node.availableZ.length === 1 && node.availableZ[0] === outerZ
60
+
61
+ const sameRect = (a: XYRect, b: XYRect) =>
62
+ Math.abs(a.x - b.x) <= EPS &&
63
+ Math.abs(a.y - b.y) <= EPS &&
64
+ Math.abs(a.width - b.width) <= EPS &&
65
+ Math.abs(a.height - b.height) <= EPS
66
+
67
+ const subtractRects = (target: XYRect, cutters: XYRect[]) => {
68
+ let remaining: XYRect[] = [target]
69
+
70
+ for (const cutter of cutters) {
71
+ if (remaining.length === 0) return remaining
72
+
73
+ const nextRemaining: XYRect[] = []
74
+ for (const piece of remaining) {
75
+ nextRemaining.push(...subtractRect2D(piece, cutter))
76
+ }
77
+ remaining = nextRemaining
78
+ }
79
+
80
+ return remaining
81
+ }
82
+
83
+ const isFullyCoveredByRects = (target: XYRect, coveringRects: XYRect[]) => {
84
+ return subtractRects(target, coveringRects).length === 0
85
+ }
86
+
87
+ export class OuterLayerContainmentMergeSolver extends BaseSolver {
88
+ private outputNodes: CapacityMeshNode[] = []
89
+ private promotedNodeIds = new Set<string>()
90
+ private residualNodeIds = new Set<string>()
91
+
92
+ constructor(private input: OuterLayerContainmentMergeSolverInput) {
93
+ super()
94
+ }
95
+
96
+ override _setup() {
97
+ this.outputNodes = this.input.meshNodes.map(cloneNode)
98
+ this.promotedNodeIds.clear()
99
+ this.residualNodeIds.clear()
100
+ }
101
+
102
+ override _step() {
103
+ this.outputNodes = this.processOuterLayerContainmentMerges()
104
+ this.solved = true
105
+ }
106
+
107
+ private processOuterLayerContainmentMerges(): CapacityMeshNode[] {
108
+ const srj = this.input.simpleRouteJson
109
+ const layerCount = Math.max(1, srj.layerCount || 1)
110
+ if (layerCount < 3) {
111
+ return this.input.meshNodes.map(cloneNode)
112
+ }
113
+
114
+ const topZ = 0
115
+ const bottomZ = layerCount - 1
116
+ const viaMinSize = Math.max(srj.minViaDiameter ?? 0, srj.minTraceWidth || 0)
117
+ const originalNodes = this.input.meshNodes.map(cloneNode)
118
+ const obstaclesByLayer = this.buildObstaclesByLayer(layerCount)
119
+ const mutableOuterNodes = originalNodes.filter(
120
+ (node) =>
121
+ isFreeNode(node) &&
122
+ (isSingletonOuterNode(node, topZ) ||
123
+ isSingletonOuterNode(node, bottomZ)),
124
+ )
125
+ const immutableNodes = originalNodes.filter(
126
+ (node) => !mutableOuterNodes.includes(node),
127
+ )
128
+ const freeSupportRectsByOuterLayer = new Map<number, XYRect[]>()
129
+ freeSupportRectsByOuterLayer.set(
130
+ topZ,
131
+ originalNodes
132
+ .filter((node) => isFreeNode(node) && node.availableZ.includes(topZ))
133
+ .map(nodeToRect),
134
+ )
135
+ freeSupportRectsByOuterLayer.set(
136
+ bottomZ,
137
+ originalNodes
138
+ .filter((node) => isFreeNode(node) && node.availableZ.includes(bottomZ))
139
+ .map(nodeToRect),
140
+ )
141
+
142
+ const promotedNodes: CapacityMeshNode[] = []
143
+ const promotedRects: XYRect[] = []
144
+ const candidateNodes = mutableOuterNodes
145
+ .filter(
146
+ (node) =>
147
+ node.width + EPS >= viaMinSize && node.height + EPS >= viaMinSize,
148
+ )
149
+ .sort((a, b) => rectArea(nodeToRect(b)) - rectArea(nodeToRect(a)))
150
+
151
+ for (const candidate of candidateNodes) {
152
+ const candidateZ = candidate.availableZ[0]!
153
+ const oppositeZ = candidateZ === topZ ? bottomZ : topZ
154
+ const candidateRect = nodeToRect(candidate)
155
+ const oppositeSupportRects =
156
+ freeSupportRectsByOuterLayer.get(oppositeZ) ?? []
157
+
158
+ if (
159
+ !this.isTransitCompatibleAcrossIntermediateLayers({
160
+ rect: candidateRect,
161
+ fromZ: candidateZ,
162
+ toZ: oppositeZ,
163
+ obstaclesByLayer,
164
+ })
165
+ ) {
166
+ continue
167
+ }
168
+ if (!isFullyCoveredByRects(candidateRect, oppositeSupportRects)) {
169
+ continue
170
+ }
171
+
172
+ promotedNodes.push({
173
+ ...candidate,
174
+ availableZ: [topZ, bottomZ],
175
+ layer: `z${topZ},${bottomZ}`,
176
+ })
177
+ promotedRects.push(candidateRect)
178
+ this.promotedNodeIds.add(candidate.capacityMeshNodeId)
179
+ }
180
+
181
+ let nextResidualId = 0
182
+ const residualNodes: CapacityMeshNode[] = []
183
+
184
+ for (const node of mutableOuterNodes) {
185
+ if (this.promotedNodeIds.has(node.capacityMeshNodeId)) {
186
+ continue
187
+ }
188
+
189
+ const nodeRect = nodeToRect(node)
190
+ const remainingPieces = subtractRects(nodeRect, promotedRects)
191
+
192
+ if (
193
+ remainingPieces.length === 1 &&
194
+ sameRect(remainingPieces[0]!, nodeRect)
195
+ ) {
196
+ residualNodes.push(node)
197
+ continue
198
+ }
199
+
200
+ for (const piece of remainingPieces) {
201
+ const residualNode = cloneNodeWithRect(
202
+ node,
203
+ piece,
204
+ `${node.capacityMeshNodeId}-outer-merge-${nextResidualId++}`,
205
+ )
206
+ residualNodes.push(residualNode)
207
+ this.residualNodeIds.add(residualNode.capacityMeshNodeId)
208
+ }
209
+ }
210
+
211
+ return [...immutableNodes, ...promotedNodes, ...residualNodes]
212
+ }
213
+
214
+ private buildObstaclesByLayer(layerCount: number): ObstacleWithRect[][] {
215
+ const out = Array.from(
216
+ { length: layerCount },
217
+ () => [] as ObstacleWithRect[],
218
+ )
219
+
220
+ for (const obstacle of this.input.simpleRouteJson.obstacles ?? []) {
221
+ const baseRect = obstacleToXYRect(obstacle)
222
+ if (!baseRect) continue
223
+ const rect = padRect(baseRect, this.input.obstacleClearance ?? 0)
224
+ const zLayers = obstacleZs(obstacle, this.input.zIndexByName)
225
+
226
+ for (const z of zLayers) {
227
+ if (z < 0 || z >= layerCount) continue
228
+ out[z]!.push({ obstacle, rect })
229
+ }
230
+ }
231
+
232
+ return out
233
+ }
234
+
235
+ private isTransitCompatibleAcrossIntermediateLayers(params: {
236
+ rect: XYRect
237
+ fromZ: number
238
+ toZ: number
239
+ obstaclesByLayer: ObstacleWithRect[][]
240
+ }) {
241
+ const { rect, fromZ, toZ, obstaclesByLayer } = params
242
+ const lo = Math.min(fromZ, toZ)
243
+ const hi = Math.max(fromZ, toZ)
244
+
245
+ if (hi - lo < 2) return false
246
+
247
+ for (let z = lo + 1; z < hi; z++) {
248
+ const overlapping = (obstaclesByLayer[z] ?? []).filter((entry) =>
249
+ overlaps(entry.rect, rect),
250
+ )
251
+ if (overlapping.length === 0) return false
252
+
253
+ const nonCopperOverlap = overlapping.some(
254
+ (entry) => !entry.obstacle.isCopperPour,
255
+ )
256
+ if (nonCopperOverlap) return false
257
+
258
+ const copperRects = overlapping
259
+ .filter((entry) => entry.obstacle.isCopperPour)
260
+ .map((entry) => entry.rect)
261
+
262
+ if (!isFullyCoveredByRects(rect, copperRects)) {
263
+ return false
264
+ }
265
+ }
266
+
267
+ return true
268
+ }
269
+
270
+ override getOutput(): { outputNodes: CapacityMeshNode[] } {
271
+ return { outputNodes: this.outputNodes }
272
+ }
273
+
274
+ override visualize(): GraphicsObject {
275
+ return {
276
+ title: "OuterLayerContainmentMergeSolver",
277
+ coordinateSystem: "cartesian",
278
+ rects: this.outputNodes.map((node) => {
279
+ const colors = getColorForZLayer(node.availableZ)
280
+ const isPromoted = this.promotedNodeIds.has(node.capacityMeshNodeId)
281
+ const isResidual = this.residualNodeIds.has(node.capacityMeshNodeId)
282
+
283
+ return {
284
+ center: node.center,
285
+ width: node.width,
286
+ height: node.height,
287
+ stroke: isPromoted
288
+ ? "rgba(22, 163, 74, 0.95)"
289
+ : isResidual
290
+ ? "rgba(37, 99, 235, 0.95)"
291
+ : colors.stroke,
292
+ fill: node._containsObstacle
293
+ ? "rgba(239, 68, 68, 0.35)"
294
+ : isPromoted
295
+ ? "rgba(34, 197, 94, 0.28)"
296
+ : isResidual
297
+ ? "rgba(59, 130, 246, 0.18)"
298
+ : colors.fill,
299
+ layer: `z${node.availableZ.join(",")}`,
300
+ label: [
301
+ `node ${node.capacityMeshNodeId}`,
302
+ `z:${node.availableZ.join(",")}`,
303
+ ].join("\n"),
304
+ }
305
+ }),
306
+ points: [],
307
+ lines: [],
308
+ texts: [],
309
+ }
310
+ }
311
+ }
@@ -1,12 +1,15 @@
1
1
  import { BaseSolver } from "@tscircuit/solver-utils"
2
2
  import type { GraphicsObject } from "graphics-debug"
3
- import type { CapacityMeshNode, RTreeRect } from "lib/types/capacity-mesh-types"
3
+ import type {
4
+ CapacityMeshNode,
5
+ RTreeRect,
6
+ } from "../../types/capacity-mesh-types"
4
7
  import { expandRectFromSeed } from "../../utils/expandRectFromSeed"
5
8
  import { finalizeRects } from "../../utils/finalizeRects"
6
9
  import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps"
7
10
  import { rectsToMeshNodes } from "./rectsToMeshNodes"
8
11
  import type { XYRect, Candidate3D, Placed3D } from "../../rectdiff-types"
9
- import type { Obstacle } from "lib/types/srj-types"
12
+ import type { Obstacle } from "../../types/srj-types"
10
13
  import RBush from "rbush"
11
14
  import { rectToTree } from "../../utils/rectToTree"
12
15
  import { sameTreeRect } from "../../utils/sameTreeRect"
@@ -7,11 +7,14 @@ import type {
7
7
  Obstacle,
8
8
  SimpleRouteConnection,
9
9
  SimpleRouteJson,
10
- } from "lib/types/srj-types"
11
- import type { GridFill3DOptions, XYRect } from "lib/rectdiff-types"
12
- import type { CapacityMeshNode, RTreeRect } from "lib/types/capacity-mesh-types"
13
- import { RectDiffSeedingSolver } from "lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver"
14
- import { RectDiffExpansionSolver } from "lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver"
10
+ } from "../../types/srj-types"
11
+ import type { GridFill3DOptions, XYRect } from "../../rectdiff-types"
12
+ import type {
13
+ CapacityMeshNode,
14
+ RTreeRect,
15
+ } from "../../types/capacity-mesh-types"
16
+ import { RectDiffSeedingSolver } from "../RectDiffSeedingSolver/RectDiffSeedingSolver"
17
+ import { RectDiffExpansionSolver } from "../RectDiffExpansionSolver/RectDiffExpansionSolver"
15
18
  import type { GraphicsObject } from "graphics-debug"
16
19
  import RBush from "rbush"
17
20
  import { buildObstacleIndexesByLayer } from "./buildObstacleIndexes"
@@ -1,14 +1,14 @@
1
- import type { SimpleRouteJson } from "lib/types/srj-types"
1
+ import type { SimpleRouteJson } from "../../types/srj-types"
2
2
  import RBush from "rbush"
3
- import { computeInverseRects } from "lib/solvers/RectDiffSeedingSolver/computeInverseRects"
3
+ import { computeInverseRects } from "../RectDiffSeedingSolver/computeInverseRects"
4
4
  import {
5
5
  buildZIndexMap,
6
6
  obstacleToXYRect,
7
7
  obstacleZs,
8
- } from "lib/solvers/RectDiffSeedingSolver/layers"
9
- import type { XYRect } from "lib/rectdiff-types"
10
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
11
- import { padRect } from "lib/utils/padRect"
8
+ } from "../RectDiffSeedingSolver/layers"
9
+ import type { XYRect } from "../../rectdiff-types"
10
+ import type { RTreeRect } from "../../types/capacity-mesh-types"
11
+ import { padRect } from "../../utils/padRect"
12
12
 
13
13
  export const buildObstacleIndexesByLayer = (params: {
14
14
  srj: SimpleRouteJson
@@ -16,12 +16,12 @@ import { computeCandidates3D } from "./computeCandidates3D"
16
16
  import { computeEdgeCandidates3D } from "./computeEdgeCandidates3D"
17
17
  import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
18
18
  import { allLayerNode } from "../../utils/buildHardPlacedByLayer"
19
- import { isFullyOccupiedAtPoint } from "lib/utils/isFullyOccupiedAtPoint"
19
+ import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
20
20
  import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps"
21
- import { getColorForZLayer } from "lib/utils/getColorForZLayer"
21
+ import { getColorForZLayer } from "../../utils/getColorForZLayer"
22
22
  import RBush from "rbush"
23
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
24
- import { rectToTree } from "lib/utils/rectToTree"
23
+ import type { RTreeRect } from "../../types/capacity-mesh-types"
24
+ import { rectToTree } from "../../utils/rectToTree"
25
25
 
26
26
  export type RectDiffSeedingSolverInput = {
27
27
  simpleRouteJson: SimpleRouteJson
@@ -3,7 +3,7 @@ import { EPS, distancePointToRectEdges } from "../../utils/rectdiff-geometry"
3
3
  import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
4
4
  import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
5
5
  import type RBush from "rbush"
6
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
6
+ import type { RTreeRect } from "../../types/capacity-mesh-types"
7
7
  const quantize = (value: number, precision = 1e-6) =>
8
8
  Math.round(value / precision) * precision
9
9