@tscircuit/rectdiff 0.0.12 → 0.0.13

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 +163 -27
  2. package/dist/index.js +1884 -1675
  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 +205 -0
  9. package/lib/solvers/{rectdiff → RectDiffExpansionSolver}/rectsToMeshNodes.ts +1 -2
  10. package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +87 -0
  11. package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +516 -0
  12. package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +109 -0
  13. package/lib/solvers/RectDiffSeedingSolver/computeDefaultGridSizes.ts +9 -0
  14. package/lib/solvers/{rectdiff/candidates.ts → RectDiffSeedingSolver/computeEdgeCandidates3D.ts} +39 -220
  15. package/lib/solvers/{rectdiff/geometry → RectDiffSeedingSolver}/computeInverseRects.ts +6 -2
  16. package/lib/solvers/{rectdiff → RectDiffSeedingSolver}/layers.ts +4 -3
  17. package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +52 -0
  18. package/lib/utils/buildHardPlacedByLayer.ts +14 -0
  19. package/lib/{solvers/rectdiff/geometry.ts → utils/expandRectFromSeed.ts} +21 -120
  20. package/lib/utils/finalizeRects.ts +49 -0
  21. package/lib/utils/isFullyOccupiedAtPoint.ts +21 -0
  22. package/lib/utils/rectdiff-geometry.ts +94 -0
  23. package/lib/utils/resizeSoftOverlaps.ts +74 -0
  24. package/package.json +1 -1
  25. package/tests/board-outline.test.ts +2 -1
  26. package/tests/obstacle-extra-layers.test.ts +1 -1
  27. package/tests/obstacle-zlayers.test.ts +1 -1
  28. package/utils/rectsEqual.ts +2 -2
  29. package/utils/rectsOverlap.ts +2 -2
  30. package/lib/solvers/RectDiffSolver.ts +0 -231
  31. package/lib/solvers/rectdiff/engine.ts +0 -481
  32. /package/lib/solvers/{rectdiff/geometry → RectDiffSeedingSolver}/isPointInPolygon.ts +0 -0
@@ -0,0 +1,21 @@
1
+ import type { XYRect } from "../rectdiff-types"
2
+ import { containsPoint } from "./rectdiff-geometry"
3
+
4
+ export function isFullyOccupiedAtPoint(
5
+ params: {
6
+ layerCount: number
7
+ obstaclesByLayer: XYRect[][]
8
+ placedByLayer: XYRect[][]
9
+ },
10
+ point: { x: number; y: number },
11
+ ): boolean {
12
+ for (let z = 0; z < params.layerCount; z++) {
13
+ const obs = params.obstaclesByLayer[z] ?? []
14
+ const placed = params.placedByLayer[z] ?? []
15
+ const occ =
16
+ obs.some((b) => containsPoint(b, point)) ||
17
+ placed.some((b) => containsPoint(b, point))
18
+ if (!occ) return false
19
+ }
20
+ return true
21
+ }
@@ -0,0 +1,94 @@
1
+ import type { XYRect } from "../rectdiff-types"
2
+
3
+ export const EPS = 1e-9
4
+ export const clamp = (v: number, lo: number, hi: number) =>
5
+ Math.max(lo, Math.min(hi, v))
6
+ export const gt = (a: number, b: number) => a > b + EPS
7
+ export const gte = (a: number, b: number) => a > b - EPS
8
+ export const lt = (a: number, b: number) => a < b - EPS
9
+ export const lte = (a: number, b: number) => a < b + EPS
10
+
11
+ export function overlaps(a: XYRect, b: XYRect) {
12
+ return !(
13
+ a.x + a.width <= b.x + EPS ||
14
+ b.x + b.width <= a.x + EPS ||
15
+ a.y + a.height <= b.y + EPS ||
16
+ b.y + b.height <= a.y + EPS
17
+ )
18
+ }
19
+
20
+ export function containsPoint(r: XYRect, p: { x: number; y: number }) {
21
+ return (
22
+ p.x >= r.x - EPS &&
23
+ p.x <= r.x + r.width + EPS &&
24
+ p.y >= r.y - EPS &&
25
+ p.y <= r.y + r.height + EPS
26
+ )
27
+ }
28
+
29
+ export function distancePointToRectEdges(
30
+ p: { x: number; y: number },
31
+ r: XYRect,
32
+ ) {
33
+ const edges: [number, number, number, number][] = [
34
+ [r.x, r.y, r.x + r.width, r.y],
35
+ [r.x + r.width, r.y, r.x + r.width, r.y + r.height],
36
+ [r.x + r.width, r.y + r.height, r.x, r.y + r.height],
37
+ [r.x, r.y + r.height, r.x, r.y],
38
+ ]
39
+ let best = Infinity
40
+ for (const [x1, y1, x2, y2] of edges) {
41
+ const A = p.x - x1,
42
+ B = p.y - y1,
43
+ C = x2 - x1,
44
+ D = y2 - y1
45
+ const dot = A * C + B * D
46
+ const lenSq = C * C + D * D
47
+ let t = lenSq !== 0 ? dot / lenSq : 0
48
+ t = clamp(t, 0, 1)
49
+ const xx = x1 + t * C
50
+ const yy = y1 + t * D
51
+ best = Math.min(best, Math.hypot(p.x - xx, p.y - yy))
52
+ }
53
+ return best
54
+ }
55
+
56
+ /** Find the intersection of two 1D intervals, or null if they don't overlap. */
57
+ export function intersect1D(r1: [number, number], r2: [number, number]) {
58
+ const lo = Math.max(r1[0], r2[0])
59
+ const hi = Math.min(r1[1], r2[1])
60
+ return hi > lo + EPS ? ([lo, hi] as const) : null
61
+ }
62
+
63
+ /** Return A \ B as up to 4 non-overlapping rectangles (or [A] if no overlap). */
64
+ export function subtractRect2D(A: XYRect, B: XYRect): XYRect[] {
65
+ if (!overlaps(A, B)) return [A]
66
+
67
+ const Xi = intersect1D([A.x, A.x + A.width], [B.x, B.x + B.width])
68
+ const Yi = intersect1D([A.y, A.y + A.height], [B.y, B.y + B.height])
69
+ if (!Xi || !Yi) return [A]
70
+
71
+ const [X0, X1] = Xi
72
+ const [Y0, Y1] = Yi
73
+ const out: XYRect[] = []
74
+
75
+ // Left strip
76
+ if (X0 > A.x + EPS) {
77
+ out.push({ x: A.x, y: A.y, width: X0 - A.x, height: A.height })
78
+ }
79
+ // Right strip
80
+ if (A.x + A.width > X1 + EPS) {
81
+ out.push({ x: X1, y: A.y, width: A.x + A.width - X1, height: A.height })
82
+ }
83
+ // Top wedge in the middle band
84
+ const midW = Math.max(0, X1 - X0)
85
+ if (midW > EPS && Y0 > A.y + EPS) {
86
+ out.push({ x: X0, y: A.y, width: midW, height: Y0 - A.y })
87
+ }
88
+ // Bottom wedge in the middle band
89
+ if (midW > EPS && A.y + A.height > Y1 + EPS) {
90
+ out.push({ x: X0, y: Y1, width: midW, height: A.y + A.height - Y1 })
91
+ }
92
+
93
+ return out.filter((r) => r.width > EPS && r.height > EPS)
94
+ }
@@ -0,0 +1,74 @@
1
+ import type { Placed3D, XYRect } from "../rectdiff-types"
2
+ import { overlaps, subtractRect2D, EPS } from "./rectdiff-geometry"
3
+
4
+ export function resizeSoftOverlaps(
5
+ params: {
6
+ layerCount: number
7
+ placed: Placed3D[]
8
+ placedByLayer: XYRect[][]
9
+ options: any
10
+ },
11
+ newIndex: number,
12
+ ) {
13
+ const newcomer = params.placed[newIndex]!
14
+ const { rect: newR, zLayers: newZs } = newcomer
15
+ const layerCount = params.layerCount
16
+
17
+ const removeIdx: number[] = []
18
+ const toAdd: typeof params.placed = []
19
+
20
+ for (let i = 0; i < params.placed.length; i++) {
21
+ if (i === newIndex) continue
22
+ const old = params.placed[i]!
23
+ // Protect full-stack nodes
24
+ if (old.zLayers.length >= layerCount) continue
25
+
26
+ const sharedZ = old.zLayers.filter((z) => newZs.includes(z))
27
+ if (sharedZ.length === 0) continue
28
+ if (!overlaps(old.rect, newR)) continue
29
+
30
+ // Carve the overlap on the shared layers
31
+ const parts = subtractRect2D(old.rect, newR)
32
+
33
+ // We will replace `old` entirely; re-add unaffected layers (same rect object).
34
+ removeIdx.push(i)
35
+
36
+ const unaffectedZ = old.zLayers.filter((z) => !newZs.includes(z))
37
+ if (unaffectedZ.length > 0) {
38
+ toAdd.push({ rect: old.rect, zLayers: unaffectedZ })
39
+ }
40
+
41
+ // Re-add carved pieces for affected layers, dropping tiny slivers
42
+ const minW = Math.min(
43
+ params.options.minSingle.width,
44
+ params.options.minMulti.width,
45
+ )
46
+ const minH = Math.min(
47
+ params.options.minSingle.height,
48
+ params.options.minMulti.height,
49
+ )
50
+ for (const p of parts) {
51
+ if (p.width + EPS >= minW && p.height + EPS >= minH) {
52
+ toAdd.push({ rect: p, zLayers: sharedZ.slice() })
53
+ }
54
+ }
55
+ }
56
+
57
+ // Remove (and clear placedByLayer)
58
+ removeIdx
59
+ .sort((a, b) => b - a)
60
+ .forEach((idx) => {
61
+ const rem = params.placed.splice(idx, 1)[0]!
62
+ for (const z of rem.zLayers) {
63
+ const arr = params.placedByLayer[z]!
64
+ const j = arr.findIndex((r) => r === rem.rect)
65
+ if (j >= 0) arr.splice(j, 1)
66
+ }
67
+ })
68
+
69
+ // Add replacements
70
+ for (const p of toAdd) {
71
+ params.placed.push(p)
72
+ for (const z of p.zLayers) params.placedByLayer[z]!.push(p.rect)
73
+ }
74
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/rectdiff",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -11,7 +11,8 @@ test("board outline snapshot", async () => {
11
11
  // Run to completion
12
12
  solver.solve()
13
13
 
14
- const viz = solver.rectDiffSolver!.visualize()
14
+ const viz =
15
+ solver.rectDiffGridSolverPipeline!.rectDiffSeedingSolver!.visualize()
15
16
  const svg = getSvgFromGraphicsObject(viz)
16
17
 
17
18
  await expect(svg).toMatchSvgSnapshot(import.meta.path)
@@ -40,5 +40,5 @@ test("RectDiffSolver clamps extra layer names to available z indices", () => {
40
40
  expect(output.meshNodes.length).toBeGreaterThan(0)
41
41
 
42
42
  // Verify solver was instantiated and processed obstacles
43
- expect(pipeline.rectDiffSolver).toBeDefined()
43
+ expect(pipeline.rectDiffGridSolverPipeline).toBeDefined()
44
44
  })
@@ -41,5 +41,5 @@ test("RectDiffSolver maps obstacle layers to numeric zLayers", () => {
41
41
 
42
42
  // Verify obstacles were processed correctly
43
43
  // The internal solver should have mapped layer names to z indices
44
- expect(pipeline.rectDiffSolver).toBeDefined()
44
+ expect(pipeline.rectDiffGridSolverPipeline).toBeDefined()
45
45
  })
@@ -1,6 +1,6 @@
1
1
  // utils/rectsEqual.ts
2
- import type { XYRect } from "../lib/solvers/rectdiff/types"
3
- import { EPS } from "../lib/solvers/rectdiff/geometry"
2
+ import type { XYRect } from "../lib/rectdiff-types"
3
+ import { EPS } from "../lib/utils/rectdiff-geometry"
4
4
 
5
5
  /**
6
6
  * Checks if two rectangles are equal within a small tolerance (EPS).
@@ -1,6 +1,6 @@
1
1
  // utils/rectsOverlap.ts
2
- import type { XYRect } from "../lib/solvers/rectdiff/types"
3
- import { EPS } from "../lib/solvers/rectdiff/geometry"
2
+ import type { XYRect } from "../lib/rectdiff-types"
3
+ import { EPS } from "../lib/utils/rectdiff-geometry"
4
4
 
5
5
  /**
6
6
  * Checks if two rectangles overlap.
@@ -1,231 +0,0 @@
1
- // lib/solvers/RectDiffSolver.ts
2
- import { BaseSolver, BasePipelineSolver } from "@tscircuit/solver-utils"
3
- import type { SimpleRouteJson } from "../types/srj-types"
4
- import type { GraphicsObject } from "graphics-debug"
5
- import type { CapacityMeshNode } from "../types/capacity-mesh-types"
6
-
7
- import type { GridFill3DOptions, RectDiffState } from "./rectdiff/types"
8
- import {
9
- initState,
10
- stepGrid,
11
- stepExpansion,
12
- finalizeRects,
13
- computeProgress,
14
- } from "./rectdiff/engine"
15
- import { rectsToMeshNodes } from "./rectdiff/rectsToMeshNodes"
16
- import { overlaps } from "./rectdiff/geometry"
17
-
18
- /**
19
- * A streaming, one-step-per-iteration solver for capacity mesh generation.
20
- */
21
- export class RectDiffSolver extends BaseSolver {
22
- private srj: SimpleRouteJson
23
- private gridOptions: Partial<GridFill3DOptions>
24
- private state!: RectDiffState
25
- private _meshNodes: CapacityMeshNode[] = []
26
-
27
- constructor(opts: {
28
- simpleRouteJson: SimpleRouteJson
29
- gridOptions?: Partial<GridFill3DOptions>
30
- }) {
31
- super()
32
- this.srj = opts.simpleRouteJson
33
- this.gridOptions = opts.gridOptions ?? {}
34
- }
35
-
36
- override _setup() {
37
- this.state = initState(this.srj, this.gridOptions)
38
- this.stats = {
39
- phase: this.state.phase,
40
- gridIndex: this.state.gridIndex,
41
- }
42
- }
43
-
44
- /** Exactly ONE small step per call. */
45
- override _step() {
46
- if (this.state.phase === "GRID") {
47
- stepGrid(this.state)
48
- } else if (this.state.phase === "EXPANSION") {
49
- stepExpansion(this.state)
50
- } else if (this.state.phase === "GAP_FILL") {
51
- this.state.phase = "DONE"
52
- } else if (this.state.phase === "DONE") {
53
- // Finalize once
54
- if (!this.solved) {
55
- const rects = finalizeRects(this.state)
56
- this._meshNodes = rectsToMeshNodes(rects)
57
- this.solved = true
58
- }
59
- return
60
- }
61
-
62
- // Lightweight stats for debugger
63
- this.stats.phase = this.state.phase
64
- this.stats.gridIndex = this.state.gridIndex
65
- this.stats.placed = this.state.placed.length
66
- }
67
-
68
- /** Compute solver progress (0 to 1). */
69
- computeProgress(): number {
70
- if (this.solved || this.state.phase === "DONE") {
71
- return 1
72
- }
73
- return computeProgress(this.state)
74
- }
75
-
76
- override getOutput(): { meshNodes: CapacityMeshNode[] } {
77
- return { meshNodes: this._meshNodes }
78
- }
79
-
80
- /** Get color based on z layer for visualization. */
81
- private getColorForZLayer(zLayers: number[]): {
82
- fill: string
83
- stroke: string
84
- } {
85
- const minZ = Math.min(...zLayers)
86
- const colors = [
87
- { fill: "#dbeafe", stroke: "#3b82f6" },
88
- { fill: "#fef3c7", stroke: "#f59e0b" },
89
- { fill: "#d1fae5", stroke: "#10b981" },
90
- { fill: "#e9d5ff", stroke: "#a855f7" },
91
- { fill: "#fed7aa", stroke: "#f97316" },
92
- { fill: "#fecaca", stroke: "#ef4444" },
93
- ]
94
- return colors[minZ % colors.length]!
95
- }
96
-
97
- /** Streaming visualization: board + obstacles + current placements. */
98
- override visualize(): GraphicsObject {
99
- const rects: NonNullable<GraphicsObject["rects"]> = []
100
- const points: NonNullable<GraphicsObject["points"]> = []
101
- const lines: NonNullable<GraphicsObject["lines"]> = [] // Initialize lines array
102
-
103
- // Board bounds - use srj bounds which is always available
104
- const boardBounds = {
105
- minX: this.srj.bounds.minX,
106
- maxX: this.srj.bounds.maxX,
107
- minY: this.srj.bounds.minY,
108
- maxY: this.srj.bounds.maxY,
109
- }
110
-
111
- // board or outline
112
- if (this.srj.outline && this.srj.outline.length > 1) {
113
- lines.push({
114
- points: [...this.srj.outline, this.srj.outline[0]!], // Close the loop by adding the first point again
115
- strokeColor: "#111827",
116
- strokeWidth: 0.01,
117
- label: "outline",
118
- })
119
- } else {
120
- rects.push({
121
- center: {
122
- x: (boardBounds.minX + boardBounds.maxX) / 2,
123
- y: (boardBounds.minY + boardBounds.maxY) / 2,
124
- },
125
- width: boardBounds.maxX - boardBounds.minX,
126
- height: boardBounds.maxY - boardBounds.minY,
127
- fill: "none",
128
- stroke: "#111827",
129
- label: "board",
130
- })
131
- }
132
-
133
- // obstacles (rect & oval as bounding boxes)
134
- for (const obstacle of this.srj.obstacles ?? []) {
135
- if (obstacle.type === "rect" || obstacle.type === "oval") {
136
- rects.push({
137
- center: { x: obstacle.center.x, y: obstacle.center.y },
138
- width: obstacle.width,
139
- height: obstacle.height,
140
- fill: "#fee2e2",
141
- stroke: "#ef4444",
142
- layer: "obstacle",
143
- label: "obstacle",
144
- })
145
- }
146
- }
147
-
148
- // board void rects
149
- if (this.state?.boardVoidRects) {
150
- // If outline exists, compute its bbox to hide outer padding voids
151
- let outlineBBox: {
152
- x: number
153
- y: number
154
- width: number
155
- height: number
156
- } | null = null
157
-
158
- if (this.srj.outline && this.srj.outline.length > 0) {
159
- const xs = this.srj.outline.map((p) => p.x)
160
- const ys = this.srj.outline.map((p) => p.y)
161
- const minX = Math.min(...xs)
162
- const minY = Math.min(...ys)
163
- outlineBBox = {
164
- x: minX,
165
- y: minY,
166
- width: Math.max(...xs) - minX,
167
- height: Math.max(...ys) - minY,
168
- }
169
- }
170
-
171
- for (const r of this.state.boardVoidRects) {
172
- // If we have an outline, only show voids that overlap its bbox (hides outer padding)
173
- if (outlineBBox && !overlaps(r, outlineBBox)) {
174
- continue
175
- }
176
-
177
- rects.push({
178
- center: { x: r.x + r.width / 2, y: r.y + r.height / 2 },
179
- width: r.width,
180
- height: r.height,
181
- fill: "rgba(0, 0, 0, 0.5)",
182
- stroke: "none",
183
- label: "void",
184
- })
185
- }
186
- }
187
-
188
- // candidate positions (where expansion started from)
189
- if (this.state?.candidates?.length) {
190
- for (const cand of this.state.candidates) {
191
- points.push({
192
- x: cand.x,
193
- y: cand.y,
194
- fill: "#9333ea",
195
- stroke: "#6b21a8",
196
- label: `z:${cand.z}`,
197
- } as any)
198
- }
199
- }
200
-
201
- // current placements (streaming) if not yet solved
202
- if (this.state?.placed?.length) {
203
- for (const p of this.state.placed) {
204
- const colors = this.getColorForZLayer(p.zLayers)
205
- rects.push({
206
- center: {
207
- x: p.rect.x + p.rect.width / 2,
208
- y: p.rect.y + p.rect.height / 2,
209
- },
210
- width: p.rect.width,
211
- height: p.rect.height,
212
- fill: colors.fill,
213
- stroke: colors.stroke,
214
- layer: `z${p.zLayers.join(",")}`,
215
- label: `free\nz:${p.zLayers.join(",")}`,
216
- })
217
- }
218
- }
219
-
220
- return {
221
- title: `RectDiff (${this.state?.phase ?? "init"})`,
222
- coordinateSystem: "cartesian",
223
- rects,
224
- points,
225
- lines, // Include lines in the returned GraphicsObject
226
- }
227
- }
228
- }
229
-
230
- // Re-export types for convenience
231
- export type { GridFill3DOptions } from "./rectdiff/types"