@tscircuit/rectdiff 0.0.28 → 0.0.30

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 (68) hide show
  1. package/.github/workflows/bun-pver-release.yml +24 -45
  2. package/lib/RectDiffPipeline.ts +0 -46
  3. package/lib/fixtures/twoNodeExpansionFixture.ts +1 -1
  4. package/lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts +4 -2
  5. package/lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts +3 -1
  6. package/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts +6 -2
  7. package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +5 -2
  8. package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +8 -5
  9. package/lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts +6 -6
  10. package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +4 -4
  11. package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +1 -1
  12. package/lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts +1 -1
  13. package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +1 -1
  14. package/lib/types/capacity-mesh-types.ts +1 -1
  15. package/lib/types/srj-types.ts +0 -1
  16. package/lib/utils/expandRectFromSeed.ts +1 -1
  17. package/lib/utils/finalizeRects.ts +1 -1
  18. package/lib/utils/isFullyOccupiedAtPoint.ts +1 -1
  19. package/lib/utils/isSelfRect.ts +1 -1
  20. package/lib/utils/rectToTree.ts +2 -2
  21. package/lib/utils/renderObstacleClearance.ts +1 -1
  22. package/lib/utils/resizeSoftOverlaps.ts +1 -1
  23. package/lib/utils/sameTreeRect.ts +1 -1
  24. package/lib/utils/searchStrip.ts +1 -1
  25. package/package.json +12 -8
  26. package/pages/repro/merge-single-layer-node.page.tsx +17 -0
  27. package/tests/__snapshots__/board-outline.snap.svg +2 -2
  28. package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +1 -1
  29. package/tests/solver/both-points-equivalent/__snapshots__/both-points-equivalent.snap.svg +1 -1
  30. package/tests/solver/bugreport01-be84eb/__snapshots__/bugreport01-be84eb.snap.svg +1 -1
  31. package/tests/solver/bugreport02-bc4361/__snapshots__/bugreport02-bc4361.snap.svg +1 -1
  32. package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
  33. package/tests/solver/bugreport07-d3f3be/__snapshots__/bugreport07-d3f3be.snap.svg +1 -1
  34. package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
  35. package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +1 -1
  36. package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +1 -1
  37. package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +1 -1
  38. package/tests/solver/bugreport12-35ce1c/__snapshots__/bugreport12-35ce1c.snap.svg +1 -1
  39. package/tests/solver/bugreport13-b9a758/__snapshots__/bugreport13-b9a758.snap.svg +1 -1
  40. package/tests/solver/bugreport16-d95f38/__snapshots__/bugreport16-d95f38.snap.svg +1 -1
  41. package/tests/solver/bugreport19/__snapshots__/bugreport19.snap.svg +1 -1
  42. package/tests/solver/bugreport20-obstacle-clipping/__snapshots__/bugreport20-obstacle-clipping.snap.svg +1 -1
  43. package/tests/solver/bugreport21-board-outline/__snapshots__/bugreport21-board-outline.snap.svg +2 -2
  44. package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
  45. package/tests/solver/bugreport23-LGA15x4/__snapshots__/bugreport23-LGA15x4.snap.svg +1 -1
  46. package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
  47. package/tests/solver/bugreport25-4b1d55/__snapshots__/bugreport25-4b1d55.snap.svg +1 -1
  48. package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +1 -1
  49. package/tests/solver/interaction/__snapshots__/interaction.snap.svg +1 -1
  50. package/tests/solver/multi-point/__snapshots__/multi-point.snap.svg +1 -1
  51. package/tests/solver/no-better-path/__snapshots__/no-better-path.snap.svg +1 -1
  52. package/tests/solver/repros/merge-single-layer-node/merge-single-layer-node.json +861 -0
  53. package/tests/solver/{bugreport50-multi-support-layer-merge/bugreport50-multi-support-layer-merge.test.ts → repros/merge-single-layer-node/merge-single-layer-node.test.ts} +7 -42
  54. package/tests/solver/transitivity/__snapshots__/transitivity.snap.svg +2 -2
  55. package/tsconfig.json +5 -1
  56. package/vite.config.ts +4 -0
  57. package/dist/index.d.ts +0 -427
  58. package/dist/index.js +0 -3319
  59. package/lib/solvers/AdjacentLayerContainmentMergeSolver/AdjacentLayerContainmentMergeSolver.ts +0 -456
  60. package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +0 -314
  61. package/pages/bugreports/bugreport50-multi-support-layer-merge.page.tsx +0 -19
  62. package/pages/pour.page.tsx +0 -18
  63. package/test-assets/bugreport49-634662.json +0 -412
  64. package/tests/outer-layer-containment-merge-solver.test.ts +0 -73
  65. package/tests/solver/bugreport49-634662/__snapshots__/bugreport49-634662.snap.svg +0 -44
  66. package/tests/solver/bugreport49-634662/bugreport49-634662.test.ts +0 -130
  67. package/tests/solver/bugreport50-multi-support-layer-merge/__snapshots__/bugreport50-multi-support-layer-merge.snap.svg +0 -44
  68. package/tests/solver/bugreport50-multi-support-layer-merge/bugreport50-multi-support-layer-merge.json +0 -972
@@ -1,456 +0,0 @@
1
- import { BaseSolver } from "@tscircuit/solver-utils"
2
- import type { GraphicsObject } from "graphics-debug"
3
- import type { XYRect } from "lib/rectdiff-types"
4
- import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
5
- import type { SimpleRouteJson } from "lib/types/srj-types"
6
- import { getColorForZLayer } from "lib/utils/getColorForZLayer"
7
- import { EPS, overlaps, subtractRect2D } from "lib/utils/rectdiff-geometry"
8
-
9
- type AdjacentLayerContainmentMergeSolverInput = {
10
- meshNodes: CapacityMeshNode[]
11
- simpleRouteJson: SimpleRouteJson
12
- minFragmentArea?: number
13
- }
14
-
15
- const DEFAULT_MIN_FRAGMENT_AREA = 0.2 ** 2
16
-
17
- const nodeToRect = (node: CapacityMeshNode): XYRect => ({
18
- x: node.center.x - node.width / 2,
19
- y: node.center.y - node.height / 2,
20
- width: node.width,
21
- height: node.height,
22
- })
23
-
24
- const rectArea = (rect: XYRect) => rect.width * rect.height
25
-
26
- const cloneNode = (node: CapacityMeshNode): CapacityMeshNode => ({
27
- ...node,
28
- center: { ...node.center },
29
- availableZ: [...node.availableZ],
30
- })
31
-
32
- const cloneNodeWithRect = (
33
- node: CapacityMeshNode,
34
- rect: XYRect,
35
- capacityMeshNodeId: string,
36
- ): CapacityMeshNode => ({
37
- ...node,
38
- capacityMeshNodeId,
39
- center: {
40
- x: rect.x + rect.width / 2,
41
- y: rect.y + rect.height / 2,
42
- },
43
- width: rect.width,
44
- height: rect.height,
45
- availableZ: [...node.availableZ],
46
- layer: `z${node.availableZ.join(",")}`,
47
- })
48
-
49
- const clonePromotedNodeWithRect = (
50
- node: CapacityMeshNode,
51
- rect: XYRect,
52
- capacityMeshNodeId: string,
53
- availableZ: number[],
54
- ): CapacityMeshNode => ({
55
- ...node,
56
- capacityMeshNodeId,
57
- center: {
58
- x: rect.x + rect.width / 2,
59
- y: rect.y + rect.height / 2,
60
- },
61
- width: rect.width,
62
- height: rect.height,
63
- availableZ: [...availableZ],
64
- layer: `z${availableZ.join(",")}`,
65
- })
66
-
67
- const isFreeNode = (node: CapacityMeshNode) =>
68
- !node._containsObstacle && !node._containsTarget
69
-
70
- const isSingletonNodeOnLayer = (node: CapacityMeshNode, z: number) =>
71
- node.availableZ.length === 1 && node.availableZ[0] === z
72
-
73
- const sameRect = (a: XYRect, b: XYRect) =>
74
- Math.abs(a.x - b.x) <= EPS &&
75
- Math.abs(a.y - b.y) <= EPS &&
76
- Math.abs(a.width - b.width) <= EPS &&
77
- Math.abs(a.height - b.height) <= EPS
78
-
79
- const subtractRects = (target: XYRect, cutters: XYRect[]) => {
80
- let remaining: XYRect[] = [target]
81
-
82
- for (const cutter of cutters) {
83
- if (remaining.length === 0) return remaining
84
-
85
- const nextRemaining: XYRect[] = []
86
- for (const piece of remaining) {
87
- nextRemaining.push(...subtractRect2D(piece, cutter))
88
- }
89
- remaining = nextRemaining
90
- }
91
-
92
- return remaining
93
- }
94
-
95
- const isFullyCoveredByRects = (target: XYRect, coveringRects: XYRect[]) => {
96
- return subtractRects(target, coveringRects).length === 0
97
- }
98
-
99
- const sortAndDedupeCuts = (values: number[]) => {
100
- const sorted = [...values].sort((a, b) => a - b)
101
- const out: number[] = []
102
-
103
- for (const value of sorted) {
104
- const last = out[out.length - 1]
105
- if (last == null || Math.abs(last - value) > EPS) {
106
- out.push(value)
107
- }
108
- }
109
-
110
- return out
111
- }
112
-
113
- const partitionRectByRects = (target: XYRect, supportRects: XYRect[]) => {
114
- const xCuts = [target.x, target.x + target.width]
115
- const yCuts = [target.y, target.y + target.height]
116
- const targetMaxX = target.x + target.width
117
- const targetMaxY = target.y + target.height
118
-
119
- for (const rect of supportRects) {
120
- const x0 = Math.max(target.x, rect.x)
121
- const x1 = Math.min(targetMaxX, rect.x + rect.width)
122
- const y0 = Math.max(target.y, rect.y)
123
- const y1 = Math.min(targetMaxY, rect.y + rect.height)
124
-
125
- if (x1 <= x0 + EPS || y1 <= y0 + EPS) continue
126
-
127
- xCuts.push(x0, x1)
128
- yCuts.push(y0, y1)
129
- }
130
-
131
- const xs = sortAndDedupeCuts(xCuts)
132
- const ys = sortAndDedupeCuts(yCuts)
133
- const cells: XYRect[] = []
134
-
135
- for (let xi = 0; xi < xs.length - 1; xi++) {
136
- const x0 = xs[xi]!
137
- const x1 = xs[xi + 1]!
138
-
139
- if (x1 <= x0 + EPS) continue
140
-
141
- for (let yi = 0; yi < ys.length - 1; yi++) {
142
- const y0 = ys[yi]!
143
- const y1 = ys[yi + 1]!
144
-
145
- if (y1 <= y0 + EPS) continue
146
-
147
- cells.push({
148
- x: x0,
149
- y: y0,
150
- width: x1 - x0,
151
- height: y1 - y0,
152
- })
153
- }
154
- }
155
-
156
- return cells
157
- }
158
-
159
- const canMergeHorizontally = (a: XYRect, b: XYRect) =>
160
- Math.abs(a.y - b.y) <= EPS &&
161
- Math.abs(a.height - b.height) <= EPS &&
162
- Math.abs(a.x + a.width - b.x) <= EPS
163
-
164
- const canMergeVertically = (a: XYRect, b: XYRect) =>
165
- Math.abs(a.x - b.x) <= EPS &&
166
- Math.abs(a.width - b.width) <= EPS &&
167
- Math.abs(a.y + a.height - b.y) <= EPS
168
-
169
- const mergeTouchingRects = (rects: XYRect[]) => {
170
- const out = rects.map((rect) => ({ ...rect }))
171
- let changed = true
172
-
173
- while (changed) {
174
- changed = false
175
-
176
- outer: for (let i = 0; i < out.length; i++) {
177
- for (let j = i + 1; j < out.length; j++) {
178
- const a = out[i]!
179
- const b = out[j]!
180
-
181
- if (canMergeHorizontally(a, b) || canMergeHorizontally(b, a)) {
182
- const merged: XYRect = {
183
- x: Math.min(a.x, b.x),
184
- y: a.y,
185
- width: a.width + b.width,
186
- height: a.height,
187
- }
188
- out.splice(j, 1)
189
- out.splice(i, 1, merged)
190
- changed = true
191
- break outer
192
- }
193
-
194
- if (canMergeVertically(a, b) || canMergeVertically(b, a)) {
195
- const merged: XYRect = {
196
- x: a.x,
197
- y: Math.min(a.y, b.y),
198
- width: a.width,
199
- height: a.height + b.height,
200
- }
201
- out.splice(j, 1)
202
- out.splice(i, 1, merged)
203
- changed = true
204
- break outer
205
- }
206
- }
207
- }
208
- }
209
-
210
- return out
211
- }
212
-
213
- const isPromotableRect = (params: {
214
- rect: XYRect
215
- viaMinSize: number
216
- minFragmentArea: number
217
- }) => {
218
- const { rect, viaMinSize, minFragmentArea } = params
219
-
220
- return (
221
- rect.width > EPS &&
222
- rect.height > EPS &&
223
- rectArea(rect) + EPS >= minFragmentArea &&
224
- rect.width + EPS >= viaMinSize &&
225
- rect.height + EPS >= viaMinSize
226
- )
227
- }
228
-
229
- const isResidualRect = (rect: XYRect, minFragmentArea: number) =>
230
- rect.width > EPS &&
231
- rect.height > EPS &&
232
- rectArea(rect) + EPS >= minFragmentArea
233
-
234
- const computePromotablePieces = (params: {
235
- target: XYRect
236
- supportRects: XYRect[]
237
- viaMinSize: number
238
- minFragmentArea: number
239
- }) => {
240
- const { target, supportRects, viaMinSize, minFragmentArea } = params
241
- const overlappingSupports = supportRects.filter((rect) =>
242
- overlaps(rect, target),
243
- )
244
-
245
- if (overlappingSupports.length === 0) return []
246
-
247
- if (
248
- isFullyCoveredByRects(target, overlappingSupports) &&
249
- isPromotableRect({ rect: target, viaMinSize, minFragmentArea })
250
- ) {
251
- return [target]
252
- }
253
-
254
- const partitioned = partitionRectByRects(target, overlappingSupports)
255
- const coveredPieces = partitioned.filter((piece) =>
256
- isFullyCoveredByRects(piece, overlappingSupports),
257
- )
258
-
259
- if (coveredPieces.length === 0) return []
260
-
261
- const mergedCoveredPieces = mergeTouchingRects(coveredPieces)
262
-
263
- return mergedCoveredPieces.filter(
264
- (piece) =>
265
- isFullyCoveredByRects(piece, overlappingSupports) &&
266
- isPromotableRect({ rect: piece, viaMinSize, minFragmentArea }),
267
- )
268
- }
269
-
270
- export class AdjacentLayerContainmentMergeSolver extends BaseSolver {
271
- private outputNodes: CapacityMeshNode[] = []
272
- private promotedNodeIds = new Set<string>()
273
- private residualNodeIds = new Set<string>()
274
-
275
- constructor(private input: AdjacentLayerContainmentMergeSolverInput) {
276
- super()
277
- }
278
-
279
- override _setup() {
280
- this.outputNodes = this.input.meshNodes.map(cloneNode)
281
- this.promotedNodeIds.clear()
282
- this.residualNodeIds.clear()
283
- }
284
-
285
- override _step() {
286
- this.outputNodes = this.processAdjacentLayerContainmentMerges()
287
- this.solved = true
288
- }
289
-
290
- private processAdjacentLayerContainmentMerges(): CapacityMeshNode[] {
291
- const srj = this.input.simpleRouteJson
292
- const layerCount = Math.max(1, srj.layerCount || 1)
293
-
294
- if (layerCount < 2) {
295
- return this.input.meshNodes.map(cloneNode)
296
- }
297
-
298
- const viaMinSize = Math.max(srj.minViaDiameter ?? 0, srj.minTraceWidth || 0)
299
- const minFragmentArea = Math.max(
300
- EPS,
301
- this.input.minFragmentArea ?? DEFAULT_MIN_FRAGMENT_AREA,
302
- )
303
-
304
- let workingNodes = this.input.meshNodes.map(cloneNode)
305
- let nextResidualId = 0
306
- let nextPromotedId = 0
307
-
308
- for (let lowerZ = 0; lowerZ < layerCount - 1; lowerZ++) {
309
- const upperZ = lowerZ + 1
310
- const mutableNodes = workingNodes.filter(
311
- (node) =>
312
- isFreeNode(node) &&
313
- (isSingletonNodeOnLayer(node, lowerZ) ||
314
- isSingletonNodeOnLayer(node, upperZ)),
315
- )
316
-
317
- if (mutableNodes.length === 0) continue
318
-
319
- const immutableNodes = workingNodes.filter(
320
- (node) => !mutableNodes.includes(node),
321
- )
322
- const supportRectsByLayer = new Map<number, XYRect[]>()
323
-
324
- supportRectsByLayer.set(
325
- lowerZ,
326
- mutableNodes
327
- .filter((node) => isSingletonNodeOnLayer(node, lowerZ))
328
- .map(nodeToRect),
329
- )
330
- supportRectsByLayer.set(
331
- upperZ,
332
- mutableNodes
333
- .filter((node) => isSingletonNodeOnLayer(node, upperZ))
334
- .map(nodeToRect),
335
- )
336
-
337
- const promotedRects: XYRect[] = []
338
- const promotedNodes: CapacityMeshNode[] = []
339
- const candidateNodes = mutableNodes
340
- .filter((node) =>
341
- isPromotableRect({
342
- rect: nodeToRect(node),
343
- viaMinSize,
344
- minFragmentArea,
345
- }),
346
- )
347
- .sort((a, b) => rectArea(nodeToRect(b)) - rectArea(nodeToRect(a)))
348
-
349
- for (const candidate of candidateNodes) {
350
- const candidateRect = nodeToRect(candidate)
351
- const candidatePieces = subtractRects(
352
- candidateRect,
353
- promotedRects,
354
- ).filter((piece) => isResidualRect(piece, minFragmentArea))
355
- const candidateZ = candidate.availableZ[0]!
356
- const oppositeZ = candidateZ === lowerZ ? upperZ : lowerZ
357
- const supportRects = supportRectsByLayer.get(oppositeZ) ?? []
358
-
359
- for (const piece of candidatePieces) {
360
- const promotablePieces = computePromotablePieces({
361
- target: piece,
362
- supportRects,
363
- viaMinSize,
364
- minFragmentArea,
365
- })
366
-
367
- for (const promotablePiece of promotablePieces) {
368
- promotedRects.push(promotablePiece)
369
-
370
- const promotedNode = clonePromotedNodeWithRect(
371
- candidate,
372
- promotablePiece,
373
- `${candidate.capacityMeshNodeId}-adjacent-merge-${nextPromotedId++}`,
374
- [lowerZ, upperZ],
375
- )
376
- promotedNodes.push(promotedNode)
377
- this.promotedNodeIds.add(promotedNode.capacityMeshNodeId)
378
- }
379
- }
380
- }
381
-
382
- const residualNodes: CapacityMeshNode[] = []
383
-
384
- for (const node of mutableNodes) {
385
- const nodeRect = nodeToRect(node)
386
- const remainingPieces = subtractRects(nodeRect, promotedRects).filter(
387
- (piece) => isResidualRect(piece, minFragmentArea),
388
- )
389
-
390
- if (
391
- remainingPieces.length === 1 &&
392
- sameRect(remainingPieces[0]!, nodeRect)
393
- ) {
394
- residualNodes.push(node)
395
- continue
396
- }
397
-
398
- for (const piece of remainingPieces) {
399
- const residualNode = cloneNodeWithRect(
400
- node,
401
- piece,
402
- `${node.capacityMeshNodeId}-adjacent-residual-${nextResidualId++}`,
403
- )
404
- residualNodes.push(residualNode)
405
- this.residualNodeIds.add(residualNode.capacityMeshNodeId)
406
- }
407
- }
408
-
409
- workingNodes = [...immutableNodes, ...promotedNodes, ...residualNodes]
410
- }
411
-
412
- return workingNodes
413
- }
414
-
415
- override getOutput(): { outputNodes: CapacityMeshNode[] } {
416
- return { outputNodes: this.outputNodes }
417
- }
418
-
419
- override visualize(): GraphicsObject {
420
- return {
421
- title: "AdjacentLayerContainmentMergeSolver",
422
- coordinateSystem: "cartesian",
423
- rects: this.outputNodes.map((node) => {
424
- const colors = getColorForZLayer(node.availableZ)
425
- const isPromoted = this.promotedNodeIds.has(node.capacityMeshNodeId)
426
- const isResidual = this.residualNodeIds.has(node.capacityMeshNodeId)
427
-
428
- return {
429
- center: node.center,
430
- width: node.width,
431
- height: node.height,
432
- stroke: isPromoted
433
- ? "rgba(245, 158, 11, 0.95)"
434
- : isResidual
435
- ? "rgba(37, 99, 235, 0.95)"
436
- : colors.stroke,
437
- fill: node._containsObstacle
438
- ? "rgba(239, 68, 68, 0.35)"
439
- : isPromoted
440
- ? "rgba(251, 191, 36, 0.28)"
441
- : isResidual
442
- ? "rgba(59, 130, 246, 0.18)"
443
- : colors.fill,
444
- layer: `z${node.availableZ.join(",")}`,
445
- label: [
446
- `node ${node.capacityMeshNodeId}`,
447
- `z:${node.availableZ.join(",")}`,
448
- ].join("\n"),
449
- }
450
- }),
451
- points: [],
452
- lines: [],
453
- texts: [],
454
- }
455
- }
456
- }