@tscircuit/rectdiff 0.0.3 → 0.0.5

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 (32) hide show
  1. package/dist/index.d.ts +112 -3
  2. package/dist/index.js +869 -142
  3. package/index.html +2 -2
  4. package/lib/solvers/RectDiffSolver.ts +125 -24
  5. package/lib/solvers/rectdiff/candidates.ts +150 -104
  6. package/lib/solvers/rectdiff/engine.ts +72 -53
  7. package/lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts +28 -0
  8. package/lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts +83 -0
  9. package/lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts +100 -0
  10. package/lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts +75 -0
  11. package/lib/solvers/rectdiff/gapfill/detection.ts +3 -0
  12. package/lib/solvers/rectdiff/gapfill/engine/addPlacement.ts +27 -0
  13. package/lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts +44 -0
  14. package/lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts +43 -0
  15. package/lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts +42 -0
  16. package/lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts +57 -0
  17. package/lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts +128 -0
  18. package/lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts +78 -0
  19. package/lib/solvers/rectdiff/gapfill/engine.ts +7 -0
  20. package/lib/solvers/rectdiff/gapfill/types.ts +60 -0
  21. package/lib/solvers/rectdiff/geometry.ts +23 -11
  22. package/lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts +253 -0
  23. package/lib/solvers/rectdiff/types.ts +1 -1
  24. package/main.tsx +1 -0
  25. package/package.json +11 -8
  26. package/tests/obstacle-extra-layers.test.ts +1 -1
  27. package/tests/obstacle-zlayers.test.ts +1 -1
  28. package/tests/rect-diff-solver.test.ts +1 -4
  29. package/utils/README.md +21 -0
  30. package/utils/rectsEqual.ts +18 -0
  31. package/utils/rectsOverlap.ts +18 -0
  32. package/vite.config.ts +7 -0
@@ -0,0 +1,57 @@
1
+ // lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts
2
+ import type { Placed3D } from "../../types"
3
+ import type { GapFillState, GapFillOptions, LayerContext } from "../types"
4
+
5
+ const DEFAULT_OPTIONS: GapFillOptions = {
6
+ minWidth: 0.1,
7
+ minHeight: 0.1,
8
+ maxIterations: 10,
9
+ targetCoverage: 0.999,
10
+ scanResolution: 0.5,
11
+ }
12
+
13
+ /**
14
+ * Initialize the gap fill state from existing rectdiff state.
15
+ */
16
+ export function initGapFillState(
17
+ {
18
+ placed,
19
+ options,
20
+ }: {
21
+ placed: Placed3D[]
22
+ options?: Partial<GapFillOptions>
23
+ },
24
+ ctx: LayerContext,
25
+ ): GapFillState {
26
+ const opts = { ...DEFAULT_OPTIONS, ...options }
27
+
28
+ // Deep copy placed arrays to avoid mutation issues
29
+ const placedCopy = placed.map((p) => ({
30
+ rect: { ...p.rect },
31
+ zLayers: [...p.zLayers],
32
+ }))
33
+
34
+ const placedByLayerCopy = ctx.placedByLayer.map((layer) =>
35
+ layer.map((r) => ({ ...r })),
36
+ )
37
+
38
+ return {
39
+ bounds: { ...ctx.bounds },
40
+ layerCount: ctx.layerCount,
41
+ obstaclesByLayer: ctx.obstaclesByLayer,
42
+ placed: placedCopy,
43
+ placedByLayer: placedByLayerCopy,
44
+ options: opts,
45
+ iteration: 0,
46
+ gapsFound: [],
47
+ gapIndex: 0,
48
+ done: false,
49
+ initialGapCount: 0,
50
+ filledCount: 0,
51
+ // Four-stage visualization state
52
+ stage: "scan",
53
+ currentGap: null,
54
+ currentSeed: null,
55
+ expandedRect: null,
56
+ }
57
+ }
@@ -0,0 +1,128 @@
1
+ // lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts
2
+ import type { GapFillState } from "../types"
3
+ import { findAllGaps } from "../detection"
4
+ import { tryExpandGap } from "./tryExpandGap"
5
+ import { addPlacement } from "./addPlacement"
6
+
7
+ /**
8
+ * Perform one step of gap filling with four-stage visualization.
9
+ * Stages: scan → select → expand → place
10
+ * Returns true if still working, false if done.
11
+ */
12
+ export function stepGapFill(state: GapFillState): boolean {
13
+ if (state.done) return false
14
+
15
+ switch (state.stage) {
16
+ case "scan": {
17
+ // Stage 1: Gap detection/scanning
18
+
19
+ // Check if we need to find new gaps
20
+ if (
21
+ state.gapsFound.length === 0 ||
22
+ state.gapIndex >= state.gapsFound.length
23
+ ) {
24
+ // Check if we've hit max iterations
25
+ if (state.iteration >= state.options.maxIterations) {
26
+ state.done = true
27
+ return false
28
+ }
29
+
30
+ // Find new gaps
31
+ state.gapsFound = findAllGaps(
32
+ {
33
+ scanResolution: state.options.scanResolution,
34
+ minWidth: state.options.minWidth,
35
+ minHeight: state.options.minHeight,
36
+ },
37
+ {
38
+ bounds: state.bounds,
39
+ layerCount: state.layerCount,
40
+ obstaclesByLayer: state.obstaclesByLayer,
41
+ placedByLayer: state.placedByLayer,
42
+ },
43
+ )
44
+
45
+ if (state.iteration === 0) {
46
+ state.initialGapCount = state.gapsFound.length
47
+ }
48
+
49
+ state.gapIndex = 0
50
+ state.iteration++
51
+
52
+ // If no gaps found, we're done
53
+ if (state.gapsFound.length === 0) {
54
+ state.done = true
55
+ return false
56
+ }
57
+ }
58
+
59
+ // Move to select stage
60
+ state.stage = "select"
61
+ return true
62
+ }
63
+
64
+ case "select": {
65
+ // Stage 2: Show the gap being targeted
66
+ if (state.gapIndex >= state.gapsFound.length) {
67
+ // No more gaps in this iteration, go back to scan
68
+ state.stage = "scan"
69
+ return true
70
+ }
71
+
72
+ state.currentGap = state.gapsFound[state.gapIndex]!
73
+ state.currentSeed = {
74
+ x: state.currentGap.centerX,
75
+ y: state.currentGap.centerY,
76
+ }
77
+ state.expandedRect = null
78
+
79
+ // Move to expand stage
80
+ state.stage = "expand"
81
+ return true
82
+ }
83
+
84
+ case "expand": {
85
+ // Stage 3: Show expansion attempt
86
+ if (!state.currentGap) {
87
+ // Shouldn't happen, but handle gracefully
88
+ state.stage = "select"
89
+ return true
90
+ }
91
+
92
+ // Try to expand from the current seed
93
+ const expandedRect = tryExpandGap(state, {
94
+ gap: state.currentGap,
95
+ seed: state.currentSeed!,
96
+ })
97
+ state.expandedRect = expandedRect
98
+
99
+ // Move to place stage
100
+ state.stage = "place"
101
+ return true
102
+ }
103
+
104
+ case "place": {
105
+ // Stage 4: Show the placed result
106
+ if (state.expandedRect && state.currentGap) {
107
+ // Actually place the rectangle
108
+ addPlacement(state, {
109
+ rect: state.expandedRect,
110
+ zLayers: state.currentGap.zLayers,
111
+ })
112
+ state.filledCount++
113
+ }
114
+
115
+ // Move to next gap and reset to select stage
116
+ state.gapIndex++
117
+ state.currentGap = null
118
+ state.currentSeed = null
119
+ state.expandedRect = null
120
+ state.stage = "select"
121
+ return true
122
+ }
123
+
124
+ default:
125
+ state.stage = "scan"
126
+ return true
127
+ }
128
+ }
@@ -0,0 +1,78 @@
1
+ // lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts
2
+ import type { XYRect } from "../../types"
3
+ import type { GapFillState, GapRegion } from "../types"
4
+ import { expandRectFromSeed } from "../../geometry"
5
+
6
+ /**
7
+ * Try to expand a rectangle from a seed point within the gap.
8
+ * Returns the expanded rectangle or null if expansion fails.
9
+ */
10
+ export function tryExpandGap(
11
+ state: GapFillState,
12
+ {
13
+ gap,
14
+ seed,
15
+ }: {
16
+ gap: GapRegion
17
+ seed: { x: number; y: number }
18
+ },
19
+ ): XYRect | null {
20
+ // Build blockers for the gap's z-layers
21
+ const blockers: XYRect[] = []
22
+ for (const z of gap.zLayers) {
23
+ blockers.push(...(state.obstaclesByLayer[z] ?? []))
24
+ blockers.push(...(state.placedByLayer[z] ?? []))
25
+ }
26
+
27
+ // Try to expand from the seed point
28
+ const rect = expandRectFromSeed({
29
+ startX: seed.x,
30
+ startY: seed.y,
31
+ gridSize: Math.min(gap.rect.width, gap.rect.height),
32
+ bounds: state.bounds,
33
+ blockers,
34
+ initialCellRatio: 0,
35
+ maxAspectRatio: null,
36
+ minReq: { width: state.options.minWidth, height: state.options.minHeight },
37
+ })
38
+
39
+ if (!rect) {
40
+ // Try additional seed points within the gap
41
+ const seeds = [
42
+ { x: gap.rect.x + state.options.minWidth / 2, y: gap.centerY },
43
+ {
44
+ x: gap.rect.x + gap.rect.width - state.options.minWidth / 2,
45
+ y: gap.centerY,
46
+ },
47
+ { x: gap.centerX, y: gap.rect.y + state.options.minHeight / 2 },
48
+ {
49
+ x: gap.centerX,
50
+ y: gap.rect.y + gap.rect.height - state.options.minHeight / 2,
51
+ },
52
+ ]
53
+
54
+ for (const altSeed of seeds) {
55
+ const altRect = expandRectFromSeed({
56
+ startX: altSeed.x,
57
+ startY: altSeed.y,
58
+ gridSize: Math.min(gap.rect.width, gap.rect.height),
59
+ bounds: state.bounds,
60
+ blockers,
61
+ initialCellRatio: 0,
62
+ maxAspectRatio: null,
63
+ minReq: {
64
+ width: state.options.minWidth,
65
+ height: state.options.minHeight,
66
+ },
67
+ })
68
+
69
+ if (altRect) {
70
+ return altRect
71
+ }
72
+ }
73
+
74
+ return null
75
+ }
76
+
77
+ return rect
78
+ }
@@ -0,0 +1,7 @@
1
+ // lib/solvers/rectdiff/gapfill/engine.ts
2
+ export * from "./engine/calculateCoverage"
3
+ export * from "./engine/findUncoveredPoints"
4
+ export * from "./engine/getGapFillProgress"
5
+ export * from "./engine/initGapFillState"
6
+
7
+ export * from "./engine/stepGapFill"
@@ -0,0 +1,60 @@
1
+ // lib/solvers/rectdiff/gapfill/types.ts
2
+ import type { XYRect, Placed3D } from "../types"
3
+
4
+ export interface GapFillOptions {
5
+ /** Minimum width for gap-fill rectangles (can be smaller than main solver) */
6
+ minWidth: number
7
+ /** Minimum height for gap-fill rectangles */
8
+ minHeight: number
9
+ /** Maximum iterations to prevent infinite loops */
10
+ maxIterations: number
11
+ /** Target coverage percentage (0-1) to stop early */
12
+ targetCoverage: number
13
+ /** Grid resolution for gap detection */
14
+ scanResolution: number
15
+ }
16
+
17
+ export interface GapRegion {
18
+ /** Bounding box of the gap */
19
+ rect: XYRect
20
+ /** Z-layers where this gap exists */
21
+ zLayers: number[]
22
+ /** Center point for seeding */
23
+ centerX: number
24
+ centerY: number
25
+ /** Approximate area of the gap */
26
+ area: number
27
+ }
28
+
29
+ export interface GapFillState {
30
+ bounds: XYRect
31
+ layerCount: number
32
+ obstaclesByLayer: XYRect[][]
33
+ placed: Placed3D[]
34
+ placedByLayer: XYRect[][]
35
+ options: GapFillOptions
36
+
37
+ // Progress tracking
38
+ iteration: number
39
+ gapsFound: GapRegion[]
40
+ gapIndex: number
41
+ done: boolean
42
+
43
+ // Stats
44
+ initialGapCount: number
45
+ filledCount: number
46
+
47
+ // Four-stage visualization state
48
+ stage: "scan" | "select" | "expand" | "place"
49
+ currentGap: GapRegion | null
50
+ currentSeed: { x: number; y: number } | null
51
+ expandedRect: XYRect | null
52
+ }
53
+
54
+ /** Context for layer-based operations shared across gap fill functions */
55
+ export interface LayerContext {
56
+ bounds: XYRect
57
+ layerCount: number
58
+ obstaclesByLayer: XYRect[][]
59
+ placedByLayer: XYRect[][]
60
+ }
@@ -209,17 +209,28 @@ function maxExpandUp(
209
209
  return Math.max(0, e)
210
210
  }
211
211
 
212
- /** Grow a rect around (startX,startY), honoring bounds/blockers/aspect/min sizes */
213
- export function expandRectFromSeed(
214
- startX: number,
215
- startY: number,
216
- gridSize: number,
217
- bounds: XYRect,
218
- blockers: XYRect[],
219
- initialCellRatio: number,
220
- maxAspectRatio: number | null | undefined,
221
- minReq: { width: number; height: number },
222
- ): XYRect | null {
212
+ /** Grow a rect around a seed point, honoring bounds/blockers/aspect/min sizes. */
213
+ export function expandRectFromSeed(params: {
214
+ startX: number
215
+ startY: number
216
+ gridSize: number
217
+ bounds: XYRect
218
+ blockers: XYRect[]
219
+ initialCellRatio: number
220
+ maxAspectRatio: number | null | undefined
221
+ minReq: { width: number; height: number }
222
+ }): XYRect | null {
223
+ const {
224
+ startX,
225
+ startY,
226
+ gridSize,
227
+ bounds,
228
+ blockers,
229
+ initialCellRatio,
230
+ maxAspectRatio,
231
+ minReq,
232
+ } = params
233
+
223
234
  const minSide = Math.max(1e-9, gridSize * initialCellRatio)
224
235
  const initialW = Math.max(minSide, minReq.width)
225
236
  const initialH = Math.max(minSide, minReq.height)
@@ -297,6 +308,7 @@ export function expandRectFromSeed(
297
308
  return best
298
309
  }
299
310
 
311
+ /** Find the intersection of two 1D intervals, or null if they don't overlap. */
300
312
  export function intersect1D(a0: number, a1: number, b0: number, b1: number) {
301
313
  const lo = Math.max(a0, b0)
302
314
  const hi = Math.min(a1, b1)
@@ -0,0 +1,253 @@
1
+ // lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts
2
+ import { BaseSolver } from "@tscircuit/solver-utils"
3
+ import type { GraphicsObject } from "graphics-debug"
4
+ import type { XYRect, Placed3D } from "../types"
5
+ import type {
6
+ GapFillState,
7
+ GapFillOptions,
8
+ LayerContext,
9
+ } from "../gapfill/types"
10
+ import {
11
+ initGapFillState,
12
+ stepGapFill,
13
+ getGapFillProgress,
14
+ } from "../gapfill/engine"
15
+
16
+ /**
17
+ * A sub-solver that fills empty spaces (gaps) left by the main grid-based
18
+ * placement algorithm.
19
+ *
20
+ * The preceding grid-based placement is fast but can leave irregular un-placed
21
+ * areas. This solver maximizes board coverage by finding and filling these
22
+ * gaps, which is critical for producing a high-quality capacity mesh.
23
+ *
24
+ * The core of the algorithm is its gap-detection phase. It works by first
25
+ * collecting all unique x and y-coordinates from the edges of existing
26
+ * obstacles and placed rectangles. This set of coordinates is supplemented by a
27
+ * uniform grid based on the `scanResolution` parameter. Together, these form a
28
+ * non-uniform grid of cells. The solver then tests the center of each cell for
29
+ * coverage. Contiguous uncovered cells are merged into larger, maximal
30
+ * rectangles, which become the candidate gaps to be filled.
31
+ *
32
+ * Once a prioritized list of gaps is generated (favoring larger, multi-layer
33
+ * gaps), the solver iteratively attempts to fill each one by expanding a new
34
+ * rectangle from a seed point until it collides with an existing boundary.
35
+ *
36
+ * The time complexity is dominated by the gap detection, which is approximately
37
+ * O((N+1/R)^2 * B), where N is the number of objects, R is the scan
38
+ * resolution, and B is the number of blockers. The algorithm's performance is
39
+ * therefore highly dependent on the `scanResolution`. It is a heuristic
40
+ * designed to be "fast enough" by avoiding a brute-force search, instead
41
+ * relying on this grid-based cell checking to find significant gaps.
42
+ */
43
+ export class GapFillSubSolver extends BaseSolver {
44
+ private state: GapFillState
45
+ private layerCtx: LayerContext
46
+
47
+ constructor(params: {
48
+ placed: Placed3D[]
49
+ options?: Partial<GapFillOptions>
50
+ layerCtx: LayerContext
51
+ }) {
52
+ super()
53
+ this.layerCtx = params.layerCtx
54
+ this.state = initGapFillState(
55
+ {
56
+ placed: params.placed,
57
+ options: params.options,
58
+ },
59
+ params.layerCtx,
60
+ )
61
+ }
62
+
63
+ /**
64
+ * Execute one step of the gap fill algorithm.
65
+ * Each gap goes through four stages: scan for gaps, select a target gap,
66
+ * expand a rectangle from seed point, then place the final result.
67
+ */
68
+ override _step() {
69
+ const stillWorking = stepGapFill(this.state)
70
+ if (!stillWorking) {
71
+ this.solved = true
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Calculate progress as a value between 0 and 1.
77
+ * Accounts for iterations, gaps processed, and current stage within each gap.
78
+ */
79
+ computeProgress(): number {
80
+ return getGapFillProgress(this.state)
81
+ }
82
+
83
+ /**
84
+ * Get all placed rectangles including original ones plus newly created gap-fill rectangles.
85
+ */
86
+ getPlaced(): Placed3D[] {
87
+ return this.state.placed
88
+ }
89
+
90
+ /**
91
+ * Get placed rectangles organized by Z-layer for efficient layer-based operations.
92
+ */
93
+ getPlacedByLayer(): XYRect[][] {
94
+ return this.state.placedByLayer
95
+ }
96
+
97
+ override getOutput() {
98
+ return {
99
+ placed: this.state.placed,
100
+ placedByLayer: this.state.placedByLayer,
101
+ filledCount: this.state.filledCount,
102
+ }
103
+ }
104
+
105
+ /** Zen visualization: show four-stage gap filling process. */
106
+ override visualize(): GraphicsObject {
107
+ const rects: NonNullable<GraphicsObject["rects"]> = []
108
+ const points: NonNullable<GraphicsObject["points"]> = []
109
+
110
+ // Board bounds (subtle)
111
+ rects.push({
112
+ center: {
113
+ x: this.layerCtx.bounds.x + this.layerCtx.bounds.width / 2,
114
+ y: this.layerCtx.bounds.y + this.layerCtx.bounds.height / 2,
115
+ },
116
+ width: this.layerCtx.bounds.width,
117
+ height: this.layerCtx.bounds.height,
118
+ fill: "none",
119
+ stroke: "#e5e7eb",
120
+ label: "",
121
+ })
122
+
123
+ switch (this.state.stage) {
124
+ case "scan": {
125
+ // Stage 1: Show scanning/detection phase with light blue overlay
126
+ rects.push({
127
+ center: {
128
+ x: this.layerCtx.bounds.x + this.layerCtx.bounds.width / 2,
129
+ y: this.layerCtx.bounds.y + this.layerCtx.bounds.height / 2,
130
+ },
131
+ width: this.layerCtx.bounds.width,
132
+ height: this.layerCtx.bounds.height,
133
+ fill: "#dbeafe",
134
+ stroke: "#3b82f6",
135
+ label: "scanning",
136
+ })
137
+ break
138
+ }
139
+
140
+ case "select": {
141
+ // Stage 2: Show the gap being targeted (red outline)
142
+ if (this.state.currentGap) {
143
+ rects.push({
144
+ center: {
145
+ x:
146
+ this.state.currentGap.rect.x +
147
+ this.state.currentGap.rect.width / 2,
148
+ y:
149
+ this.state.currentGap.rect.y +
150
+ this.state.currentGap.rect.height / 2,
151
+ },
152
+ width: this.state.currentGap.rect.width,
153
+ height: this.state.currentGap.rect.height,
154
+ fill: "#fecaca",
155
+ stroke: "#ef4444",
156
+ label: "target gap",
157
+ })
158
+
159
+ // Show the seed point
160
+ if (this.state.currentSeed) {
161
+ points.push({
162
+ x: this.state.currentSeed.x,
163
+ y: this.state.currentSeed.y,
164
+ color: "#dc2626",
165
+ label: "seed",
166
+ })
167
+ }
168
+ }
169
+ break
170
+ }
171
+
172
+ case "expand": {
173
+ // Stage 3: Show expansion attempt (yellow growing rectangle + seed)
174
+ if (this.state.currentGap) {
175
+ // Show gap outline (faded)
176
+ rects.push({
177
+ center: {
178
+ x:
179
+ this.state.currentGap.rect.x +
180
+ this.state.currentGap.rect.width / 2,
181
+ y:
182
+ this.state.currentGap.rect.y +
183
+ this.state.currentGap.rect.height / 2,
184
+ },
185
+ width: this.state.currentGap.rect.width,
186
+ height: this.state.currentGap.rect.height,
187
+ fill: "none",
188
+ stroke: "#f87171",
189
+ label: "",
190
+ })
191
+ }
192
+
193
+ if (this.state.currentSeed) {
194
+ // Show seed point
195
+ points.push({
196
+ x: this.state.currentSeed.x,
197
+ y: this.state.currentSeed.y,
198
+ color: "#f59e0b",
199
+ label: "expanding",
200
+ })
201
+ }
202
+
203
+ if (this.state.expandedRect) {
204
+ // Show expanded rectangle
205
+ rects.push({
206
+ center: {
207
+ x: this.state.expandedRect.x + this.state.expandedRect.width / 2,
208
+ y: this.state.expandedRect.y + this.state.expandedRect.height / 2,
209
+ },
210
+ width: this.state.expandedRect.width,
211
+ height: this.state.expandedRect.height,
212
+ fill: "#fef3c7",
213
+ stroke: "#f59e0b",
214
+ label: "expanding",
215
+ })
216
+ }
217
+ break
218
+ }
219
+
220
+ case "place": {
221
+ // Stage 4: Show final placed rectangle (green)
222
+ if (this.state.expandedRect) {
223
+ rects.push({
224
+ center: {
225
+ x: this.state.expandedRect.x + this.state.expandedRect.width / 2,
226
+ y: this.state.expandedRect.y + this.state.expandedRect.height / 2,
227
+ },
228
+ width: this.state.expandedRect.width,
229
+ height: this.state.expandedRect.height,
230
+ fill: "#bbf7d0",
231
+ stroke: "#22c55e",
232
+ label: "placed",
233
+ })
234
+ }
235
+ break
236
+ }
237
+ }
238
+
239
+ const stageNames = {
240
+ scan: "scanning",
241
+ select: "selecting",
242
+ expand: "expanding",
243
+ place: "placing",
244
+ }
245
+
246
+ return {
247
+ title: `GapFill (${stageNames[this.state.stage]}): ${this.state.filledCount} filled`,
248
+ coordinateSystem: "cartesian",
249
+ rects,
250
+ points,
251
+ }
252
+ }
253
+ }
@@ -34,7 +34,7 @@ export type Candidate3D = {
34
34
  }
35
35
  export type Placed3D = { rect: XYRect; zLayers: number[] }
36
36
 
37
- export type Phase = "GRID" | "EXPANSION" | "DONE"
37
+ export type Phase = "GRID" | "EXPANSION" | "GAP_FILL" | "DONE"
38
38
 
39
39
  export type RectDiffState = {
40
40
  // static
package/main.tsx ADDED
@@ -0,0 +1 @@
1
+ import "react-cosmos/client"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/rectdiff",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -15,23 +15,26 @@
15
15
  "@react-hook/resize-observer": "^2.0.2",
16
16
  "@tscircuit/solver-utils": "^0.0.3",
17
17
  "@types/bun": "latest",
18
+ "@types/react": "^18",
19
+ "@types/react-dom": "^18",
18
20
  "@types/three": "^0.181.0",
19
21
  "biome": "^0.3.3",
20
22
  "bun-match-svg": "^0.0.14",
21
23
  "graphics-debug": "^0.0.70",
22
24
  "rbush": "^4.0.1",
23
- "react": "^19.2.0",
24
- "react-cosmos": "^7.0.0",
25
- "react-cosmos-plugin-vite": "^7.0.0",
26
- "react-dom": "^19.2.0",
25
+ "react": "18",
26
+ "react-cosmos": "^6.2.3",
27
+ "react-cosmos-plugin-vite": "^6.2.0",
28
+ "react-dom": "18",
27
29
  "three": "^0.181.1",
28
- "tsup": "^8.5.1"
30
+ "tsup": "^8.5.1",
31
+ "vite": "^6.0.11",
32
+ "@vitejs/plugin-react": "^4"
29
33
  },
30
34
  "peerDependencies": {
31
35
  "typescript": "^5"
32
36
  },
33
37
  "dependencies": {
34
- "D": "^1.0.0",
35
- "vite": "^7.2.2"
38
+ "D": "^1.0.0"
36
39
  }
37
40
  }
@@ -29,7 +29,7 @@ test("RectDiffSolver clamps extra layer names to available z indices", () => {
29
29
  ],
30
30
  }
31
31
 
32
- const solver = new RectDiffSolver({ simpleRouteJson: srj, mode: "grid" })
32
+ const solver = new RectDiffSolver({ simpleRouteJson: srj })
33
33
  solver.setup()
34
34
 
35
35
  expect(srj.obstacles[0]?.zLayers).toEqual([1])