@tscircuit/rectdiff 0.0.32 → 0.0.34

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 (28) hide show
  1. package/components/SolverDebugger3d.tsx +64 -10
  2. package/lib/fixtures/twoNodeExpansionFixture.ts +1 -1
  3. package/lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts +2 -2
  4. package/lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts +1 -1
  5. package/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts +2 -2
  6. package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +5 -2
  7. package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +8 -5
  8. package/lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts +6 -6
  9. package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +4 -4
  10. package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +1 -1
  11. package/lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts +1 -1
  12. package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +1 -1
  13. package/lib/types/capacity-mesh-types.ts +1 -1
  14. package/lib/utils/expandRectFromSeed.ts +1 -1
  15. package/lib/utils/finalizeRects.ts +1 -1
  16. package/lib/utils/isFullyOccupiedAtPoint.ts +1 -1
  17. package/lib/utils/isSelfRect.ts +1 -1
  18. package/lib/utils/rectToTree.ts +2 -2
  19. package/lib/utils/renderObstacleClearance.ts +1 -1
  20. package/lib/utils/resizeSoftOverlaps.ts +1 -1
  21. package/lib/utils/sameTreeRect.ts +1 -1
  22. package/lib/utils/searchStrip.ts +1 -1
  23. package/lib/utils/z-filter.ts +43 -0
  24. package/package.json +6 -2
  25. package/tests/srj-layer-summary.test.ts +94 -0
  26. package/tests/z-filter.test.ts +33 -0
  27. package/dist/index.d.ts +0 -381
  28. package/dist/index.js +0 -2764
package/dist/index.js DELETED
@@ -1,2764 +0,0 @@
1
- // lib/RectDiffPipeline.ts
2
- import {
3
- BasePipelineSolver as BasePipelineSolver3,
4
- definePipelineStep as definePipelineStep3
5
- } from "@tscircuit/solver-utils";
6
-
7
- // lib/solvers/GapFillSolver/GapFillSolverPipeline.ts
8
- import {
9
- BasePipelineSolver,
10
- definePipelineStep
11
- } from "@tscircuit/solver-utils";
12
-
13
- // lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts
14
- import { BaseSolver } from "@tscircuit/solver-utils";
15
- import Flatbush from "flatbush";
16
-
17
- // lib/solvers/GapFillSolver/projectToUncoveredSegments.ts
18
- var EPS = 1e-4;
19
- function projectToUncoveredSegments(primaryEdge, overlappingEdges) {
20
- const isHorizontal = Math.abs(primaryEdge.start.y - primaryEdge.end.y) < EPS;
21
- const isVertical = Math.abs(primaryEdge.start.x - primaryEdge.end.x) < EPS;
22
- if (!isHorizontal && !isVertical) return [];
23
- const axis = isHorizontal ? "x" : "y";
24
- const perp = isHorizontal ? "y" : "x";
25
- const lineCoord = primaryEdge.start[perp];
26
- const p0 = primaryEdge.start[axis];
27
- const p1 = primaryEdge.end[axis];
28
- const pMin = Math.min(p0, p1);
29
- const pMax = Math.max(p0, p1);
30
- const clamp2 = (v) => Math.max(pMin, Math.min(pMax, v));
31
- const intervals = [];
32
- for (const e of overlappingEdges) {
33
- if (e === primaryEdge) continue;
34
- const eIsHorizontal = Math.abs(e.start.y - e.end.y) < EPS;
35
- const eIsVertical = Math.abs(e.start.x - e.end.x) < EPS;
36
- if (axis === "x" && !eIsHorizontal) continue;
37
- if (axis === "y" && !eIsVertical) continue;
38
- if (Math.abs(e.start[perp] - lineCoord) > EPS) continue;
39
- const eMin = Math.min(e.start[axis], e.end[axis]);
40
- const eMax = Math.max(e.start[axis], e.end[axis]);
41
- const s = clamp2(eMin);
42
- const t = clamp2(eMax);
43
- if (t - s > EPS) intervals.push({ s, e: t });
44
- }
45
- if (intervals.length === 0) {
46
- return [
47
- {
48
- ...primaryEdge,
49
- start: { ...primaryEdge.start },
50
- end: { ...primaryEdge.end }
51
- }
52
- ];
53
- }
54
- intervals.sort((a, b) => a.s - b.s);
55
- const merged = [];
56
- for (const it of intervals) {
57
- const last = merged[merged.length - 1];
58
- if (!last || it.s > last.e + EPS) merged.push({ ...it });
59
- else last.e = Math.max(last.e, it.e);
60
- }
61
- const uncovered = [];
62
- let cursor = pMin;
63
- for (const m of merged) {
64
- if (m.s > cursor + EPS) uncovered.push({ s: cursor, e: m.s });
65
- cursor = Math.max(cursor, m.e);
66
- if (cursor >= pMax - EPS) break;
67
- }
68
- if (pMax > cursor + EPS) uncovered.push({ s: cursor, e: pMax });
69
- if (uncovered.length === 0) return [];
70
- return uncovered.filter((u) => u.e - u.s > EPS).map((u) => {
71
- const start = axis === "x" ? { x: u.s, y: lineCoord } : { x: lineCoord, y: u.s };
72
- const end = axis === "x" ? { x: u.e, y: lineCoord } : { x: lineCoord, y: u.e };
73
- return {
74
- parent: primaryEdge.parent,
75
- facingDirection: primaryEdge.facingDirection,
76
- start,
77
- end,
78
- z: primaryEdge.z
79
- };
80
- });
81
- }
82
-
83
- // lib/solvers/GapFillSolver/edge-constants.ts
84
- var EDGES = [
85
- {
86
- facingDirection: "x-",
87
- dx: -1,
88
- dy: 0,
89
- startX: -0.5,
90
- startY: 0.5,
91
- endX: -0.5,
92
- endY: -0.5
93
- },
94
- {
95
- facingDirection: "x+",
96
- dx: 1,
97
- dy: 0,
98
- startX: 0.5,
99
- startY: 0.5,
100
- endX: 0.5,
101
- endY: -0.5
102
- },
103
- {
104
- facingDirection: "y-",
105
- dx: 0,
106
- dy: -1,
107
- startX: -0.5,
108
- startY: -0.5,
109
- endX: 0.5,
110
- endY: -0.5
111
- },
112
- {
113
- facingDirection: "y+",
114
- dx: 0,
115
- dy: 1,
116
- startX: 0.5,
117
- startY: 0.5,
118
- endX: -0.5,
119
- endY: 0.5
120
- }
121
- ];
122
- var EDGE_MAP = {
123
- "x-": EDGES.find((e) => e.facingDirection === "x-"),
124
- "x+": EDGES.find((e) => e.facingDirection === "x+"),
125
- "y-": EDGES.find((e) => e.facingDirection === "y-"),
126
- "y+": EDGES.find((e) => e.facingDirection === "y+")
127
- };
128
-
129
- // lib/solvers/GapFillSolver/visuallyOffsetLine.ts
130
- var OFFSET_DIR_MAP = {
131
- "x-": {
132
- x: -1,
133
- y: 0
134
- },
135
- "x+": {
136
- x: 1,
137
- y: 0
138
- },
139
- "y-": {
140
- x: 0,
141
- y: -1
142
- },
143
- "y+": {
144
- x: 0,
145
- y: 1
146
- }
147
- };
148
- var visuallyOffsetLine = (line, options) => {
149
- const { dir, amt } = options;
150
- const offset = OFFSET_DIR_MAP[dir];
151
- return line.map((p) => ({
152
- x: p.x + offset.x * amt,
153
- y: p.y + offset.y * amt
154
- }));
155
- };
156
-
157
- // lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts
158
- import "@tscircuit/math-utils";
159
- var EPS2 = 1e-4;
160
- var FindSegmentsWithAdjacentEmptySpaceSolver = class extends BaseSolver {
161
- constructor(input) {
162
- super();
163
- this.input = input;
164
- for (const node of this.input.meshNodes) {
165
- for (const edge of EDGES) {
166
- let start = {
167
- x: node.center.x + node.width * edge.startX,
168
- y: node.center.y + node.height * edge.startY
169
- };
170
- let end = {
171
- x: node.center.x + node.width * edge.endX,
172
- y: node.center.y + node.height * edge.endY
173
- };
174
- if (start.x > end.x) {
175
- ;
176
- [start, end] = [end, start];
177
- }
178
- if (Math.abs(start.x - end.x) < EPS2 && start.y > end.y) {
179
- ;
180
- [start, end] = [end, start];
181
- }
182
- for (const z of node.availableZ) {
183
- this.unprocessedEdges.push({
184
- parent: node,
185
- start,
186
- end,
187
- facingDirection: edge.facingDirection,
188
- z
189
- });
190
- }
191
- }
192
- }
193
- this.allEdges = [...this.unprocessedEdges];
194
- this.edgeSpatialIndex = new Flatbush(this.allEdges.length);
195
- for (const edge of this.allEdges) {
196
- this.edgeSpatialIndex.add(
197
- edge.start.x,
198
- edge.start.y,
199
- edge.end.x,
200
- edge.end.y
201
- );
202
- }
203
- this.edgeSpatialIndex.finish();
204
- }
205
- input;
206
- allEdges;
207
- unprocessedEdges = [];
208
- segmentsWithAdjacentEmptySpace = [];
209
- edgeSpatialIndex;
210
- lastCandidateEdge = null;
211
- lastOverlappingEdges = null;
212
- lastUncoveredSegments = null;
213
- _step() {
214
- if (this.unprocessedEdges.length === 0) {
215
- this.solved = true;
216
- this.lastCandidateEdge = null;
217
- this.lastOverlappingEdges = null;
218
- this.lastUncoveredSegments = null;
219
- return;
220
- }
221
- const candidateEdge = this.unprocessedEdges.shift();
222
- this.lastCandidateEdge = candidateEdge;
223
- const nearbyEdges = this.edgeSpatialIndex.search(
224
- candidateEdge.start.x - EPS2,
225
- candidateEdge.start.y - EPS2,
226
- candidateEdge.end.x + EPS2,
227
- candidateEdge.end.y + EPS2
228
- );
229
- const overlappingEdges = nearbyEdges.map((i) => this.allEdges[i]).filter((e) => e.z === candidateEdge.z);
230
- this.lastOverlappingEdges = overlappingEdges;
231
- const uncoveredSegments = projectToUncoveredSegments(
232
- candidateEdge,
233
- overlappingEdges
234
- );
235
- this.lastUncoveredSegments = uncoveredSegments;
236
- this.segmentsWithAdjacentEmptySpace.push(...uncoveredSegments);
237
- }
238
- getOutput() {
239
- return {
240
- segmentsWithAdjacentEmptySpace: this.segmentsWithAdjacentEmptySpace
241
- };
242
- }
243
- visualize() {
244
- const graphics = {
245
- title: "FindSegmentsWithAdjacentEmptySpace",
246
- coordinateSystem: "cartesian",
247
- rects: [],
248
- points: [],
249
- lines: [],
250
- circles: [],
251
- arrows: [],
252
- texts: []
253
- };
254
- for (const node of this.input.meshNodes) {
255
- graphics.rects.push({
256
- center: node.center,
257
- width: node.width,
258
- height: node.height,
259
- stroke: "rgba(0, 0, 0, 0.1)"
260
- });
261
- }
262
- for (const unprocessedEdge of this.unprocessedEdges) {
263
- graphics.lines.push({
264
- points: visuallyOffsetLine(
265
- [unprocessedEdge.start, unprocessedEdge.end],
266
- { dir: unprocessedEdge.facingDirection, amt: -0.1 }
267
- ),
268
- strokeColor: "rgba(0, 0, 255, 0.5)",
269
- strokeDash: "5 5"
270
- });
271
- }
272
- for (const edge of this.segmentsWithAdjacentEmptySpace) {
273
- graphics.lines.push({
274
- points: [edge.start, edge.end],
275
- strokeColor: "rgba(0,255,0, 0.5)"
276
- });
277
- }
278
- if (this.lastCandidateEdge) {
279
- graphics.lines.push({
280
- points: [this.lastCandidateEdge.start, this.lastCandidateEdge.end],
281
- strokeColor: "blue"
282
- });
283
- }
284
- if (this.lastOverlappingEdges) {
285
- for (const edge of this.lastOverlappingEdges) {
286
- graphics.lines.push({
287
- points: visuallyOffsetLine([edge.start, edge.end], {
288
- dir: edge.facingDirection,
289
- amt: 0.05
290
- }),
291
- strokeColor: "red",
292
- strokeDash: "2 2"
293
- });
294
- }
295
- }
296
- if (this.lastUncoveredSegments) {
297
- for (const edge of this.lastUncoveredSegments) {
298
- graphics.lines.push({
299
- points: visuallyOffsetLine([edge.start, edge.end], {
300
- dir: edge.facingDirection,
301
- amt: -0.05
302
- }),
303
- strokeColor: "green",
304
- strokeDash: "2 2"
305
- });
306
- }
307
- }
308
- return graphics;
309
- }
310
- };
311
-
312
- // lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts
313
- import { BaseSolver as BaseSolver2 } from "@tscircuit/solver-utils";
314
- import RBush from "rbush";
315
-
316
- // lib/solvers/GapFillSolver/getBoundsFromCorners.ts
317
- var getBoundsFromCorners = (corners) => {
318
- return {
319
- minX: Math.min(...corners.map((c) => c.x)),
320
- minY: Math.min(...corners.map((c) => c.y)),
321
- maxX: Math.max(...corners.map((c) => c.x)),
322
- maxY: Math.max(...corners.map((c) => c.y))
323
- };
324
- };
325
-
326
- // lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts
327
- import { segmentToBoxMinDistance } from "@tscircuit/math-utils";
328
- var EPS3 = 1e-4;
329
- var ExpandEdgesToEmptySpaceSolver = class extends BaseSolver2 {
330
- constructor(input) {
331
- super();
332
- this.input = input;
333
- this.unprocessedSegments = [...this.input.segmentsWithAdjacentEmptySpace];
334
- this.rectSpatialIndex = new RBush();
335
- this.rectSpatialIndex.load(
336
- this.input.boardVoid?.boardVoidRects.map((rect, index) => ({
337
- capacityMeshNodeId: `void-rect-${index}`,
338
- center: {
339
- x: rect.x + rect.width / 2,
340
- y: rect.y + rect.height / 2
341
- },
342
- width: rect.width,
343
- height: rect.height,
344
- availableZ: Array.from(
345
- { length: this.input.boardVoid?.layerCount || 0 },
346
- (_, i) => i
347
- ),
348
- layer: "void",
349
- minX: rect.x,
350
- minY: rect.y,
351
- maxX: rect.x + rect.width,
352
- maxY: rect.y + rect.height
353
- })) || []
354
- );
355
- this.rectSpatialIndex.load(
356
- this.input.inputMeshNodes.map((n) => ({
357
- ...n,
358
- minX: n.center.x - n.width / 2,
359
- minY: n.center.y - n.height / 2,
360
- maxX: n.center.x + n.width / 2,
361
- maxY: n.center.y + n.height / 2
362
- }))
363
- );
364
- }
365
- input;
366
- unprocessedSegments = [];
367
- expandedSegments = [];
368
- lastSegment = null;
369
- lastSearchBounds = null;
370
- lastCollidingNodes = null;
371
- lastSearchCorner1 = null;
372
- lastSearchCorner2 = null;
373
- lastExpandedSegment = null;
374
- rectSpatialIndex;
375
- _step() {
376
- if (this.unprocessedSegments.length === 0) {
377
- this.solved = true;
378
- return;
379
- }
380
- const segment = this.unprocessedSegments.shift();
381
- this.lastSegment = segment;
382
- const { dx, dy } = EDGE_MAP[segment.facingDirection];
383
- const deltaStartEnd = {
384
- x: segment.end.x - segment.start.x,
385
- y: segment.end.y - segment.start.y
386
- };
387
- const segLength = Math.sqrt(deltaStartEnd.x ** 2 + deltaStartEnd.y ** 2);
388
- const normDeltaStartEnd = {
389
- x: deltaStartEnd.x / segLength,
390
- y: deltaStartEnd.y / segLength
391
- };
392
- let collidingNodes = null;
393
- let searchDistance = 1;
394
- const searchCorner1 = {
395
- x: segment.start.x + dx * EPS3 + normDeltaStartEnd.x * EPS3 * 10,
396
- y: segment.start.y + dy * EPS3 + normDeltaStartEnd.y * EPS3 * 10
397
- };
398
- const searchCorner2 = {
399
- x: segment.end.x + dx * EPS3 - normDeltaStartEnd.x * EPS3 * 10,
400
- y: segment.end.y + dy * EPS3 - normDeltaStartEnd.y * EPS3 * 10
401
- };
402
- this.lastSearchCorner1 = searchCorner1;
403
- this.lastSearchCorner2 = searchCorner2;
404
- while ((!collidingNodes || collidingNodes.length === 0) && searchDistance < 1e3) {
405
- const searchBounds = getBoundsFromCorners([
406
- searchCorner1,
407
- searchCorner2,
408
- {
409
- x: searchCorner1.x + dx * searchDistance,
410
- y: searchCorner1.y + dy * searchDistance
411
- },
412
- {
413
- x: searchCorner2.x + dx * searchDistance,
414
- y: searchCorner2.y + dy * searchDistance
415
- }
416
- ]);
417
- this.lastSearchBounds = searchBounds;
418
- collidingNodes = this.rectSpatialIndex.search(searchBounds).filter((n) => n.availableZ.includes(segment.z)).filter(
419
- (n) => n.capacityMeshNodeId !== segment.parent.capacityMeshNodeId
420
- );
421
- searchDistance *= 4;
422
- }
423
- if (!collidingNodes || collidingNodes.length === 0) {
424
- return;
425
- }
426
- this.lastCollidingNodes = collidingNodes;
427
- let smallestDistance = Infinity;
428
- for (const node of collidingNodes) {
429
- const distance = segmentToBoxMinDistance(segment.start, segment.end, node);
430
- if (distance < smallestDistance) {
431
- smallestDistance = distance;
432
- }
433
- }
434
- const expandDistance = smallestDistance;
435
- const nodeBounds = getBoundsFromCorners([
436
- segment.start,
437
- segment.end,
438
- {
439
- x: segment.start.x + dx * expandDistance,
440
- y: segment.start.y + dy * expandDistance
441
- },
442
- {
443
- x: segment.end.x + dx * expandDistance,
444
- y: segment.end.y + dy * expandDistance
445
- }
446
- ]);
447
- const nodeCenter = {
448
- x: (nodeBounds.minX + nodeBounds.maxX) / 2,
449
- y: (nodeBounds.minY + nodeBounds.maxY) / 2
450
- };
451
- const nodeWidth = nodeBounds.maxX - nodeBounds.minX;
452
- const nodeHeight = nodeBounds.maxY - nodeBounds.minY;
453
- const expandedSegment = {
454
- segment,
455
- newNode: {
456
- capacityMeshNodeId: `new-${segment.parent.capacityMeshNodeId}-${this.expandedSegments.length}`,
457
- center: nodeCenter,
458
- width: nodeWidth,
459
- height: nodeHeight,
460
- availableZ: [segment.z],
461
- layer: segment.parent.layer
462
- }
463
- };
464
- this.lastExpandedSegment = expandedSegment;
465
- if (nodeWidth < EPS3 || nodeHeight < EPS3) {
466
- return;
467
- }
468
- this.expandedSegments.push(expandedSegment);
469
- this.rectSpatialIndex.insert({
470
- ...expandedSegment.newNode,
471
- ...nodeBounds
472
- });
473
- }
474
- getOutput() {
475
- return {
476
- expandedSegments: this.expandedSegments
477
- };
478
- }
479
- visualize() {
480
- const graphics = {
481
- title: "ExpandEdgesToEmptySpace",
482
- coordinateSystem: "cartesian",
483
- rects: [],
484
- points: [],
485
- lines: [],
486
- circles: [],
487
- arrows: [],
488
- texts: []
489
- };
490
- for (const node of this.input.inputMeshNodes) {
491
- graphics.rects.push({
492
- center: node.center,
493
- width: node.width,
494
- height: node.height,
495
- stroke: "rgba(0, 0, 0, 0.1)",
496
- layer: `z${node.availableZ.join(",")}`,
497
- label: [
498
- `node ${node.capacityMeshNodeId}`,
499
- `z:${node.availableZ.join(",")}`
500
- ].join("\n")
501
- });
502
- }
503
- for (const { newNode } of this.expandedSegments) {
504
- graphics.rects.push({
505
- center: newNode.center,
506
- width: newNode.width,
507
- height: newNode.height,
508
- fill: "green",
509
- label: `expandedSegment (z=${newNode.availableZ.join(",")})`,
510
- layer: `z${newNode.availableZ.join(",")}`
511
- });
512
- }
513
- if (this.lastSegment) {
514
- graphics.lines.push({
515
- points: [this.lastSegment.start, this.lastSegment.end],
516
- strokeColor: "rgba(0, 0, 255, 0.5)"
517
- });
518
- }
519
- if (this.lastSearchBounds) {
520
- graphics.rects.push({
521
- center: {
522
- x: (this.lastSearchBounds.minX + this.lastSearchBounds.maxX) / 2,
523
- y: (this.lastSearchBounds.minY + this.lastSearchBounds.maxY) / 2
524
- },
525
- width: this.lastSearchBounds.maxX - this.lastSearchBounds.minX,
526
- height: this.lastSearchBounds.maxY - this.lastSearchBounds.minY,
527
- fill: "rgba(0, 0, 255, 0.25)"
528
- });
529
- }
530
- if (this.lastSearchCorner1 && this.lastSearchCorner2) {
531
- graphics.points.push({
532
- x: this.lastSearchCorner1.x,
533
- y: this.lastSearchCorner1.y,
534
- color: "rgba(0, 0, 255, 0.5)",
535
- label: ["searchCorner1", `z=${this.lastSegment?.z}`].join("\n")
536
- });
537
- graphics.points.push({
538
- x: this.lastSearchCorner2.x,
539
- y: this.lastSearchCorner2.y,
540
- color: "rgba(0, 0, 255, 0.5)",
541
- label: ["searchCorner2", `z=${this.lastSegment?.z}`].join("\n")
542
- });
543
- }
544
- if (this.lastExpandedSegment) {
545
- graphics.rects.push({
546
- center: this.lastExpandedSegment.newNode.center,
547
- width: this.lastExpandedSegment.newNode.width,
548
- height: this.lastExpandedSegment.newNode.height,
549
- fill: "purple",
550
- label: `expandedSegment (z=${this.lastExpandedSegment.segment.z})`
551
- });
552
- }
553
- if (this.lastCollidingNodes) {
554
- for (const node of this.lastCollidingNodes) {
555
- graphics.rects.push({
556
- center: node.center,
557
- width: node.width,
558
- height: node.height,
559
- fill: "rgba(255, 0, 0, 0.5)"
560
- });
561
- }
562
- }
563
- return graphics;
564
- }
565
- };
566
-
567
- // lib/solvers/GapFillSolver/GapFillSolverPipeline.ts
568
- var GapFillSolverPipeline = class extends BasePipelineSolver {
569
- findSegmentsWithAdjacentEmptySpaceSolver;
570
- expandEdgesToEmptySpaceSolver;
571
- pipelineDef = [
572
- definePipelineStep(
573
- "findSegmentsWithAdjacentEmptySpaceSolver",
574
- FindSegmentsWithAdjacentEmptySpaceSolver,
575
- (gapFillPipeline) => [
576
- {
577
- meshNodes: gapFillPipeline.inputProblem.meshNodes
578
- }
579
- ],
580
- {
581
- onSolved: () => {
582
- }
583
- }
584
- ),
585
- definePipelineStep(
586
- "expandEdgesToEmptySpaceSolver",
587
- ExpandEdgesToEmptySpaceSolver,
588
- (gapFillPipeline) => [
589
- {
590
- inputMeshNodes: gapFillPipeline.inputProblem.meshNodes,
591
- segmentsWithAdjacentEmptySpace: gapFillPipeline.findSegmentsWithAdjacentEmptySpaceSolver.getOutput().segmentsWithAdjacentEmptySpace,
592
- boardVoid: gapFillPipeline.inputProblem.boardVoid
593
- }
594
- ],
595
- {
596
- onSolved: () => {
597
- }
598
- }
599
- )
600
- ];
601
- getOutput() {
602
- const expandedSegments = this.expandEdgesToEmptySpaceSolver?.getOutput().expandedSegments ?? [];
603
- const expandedNodes = expandedSegments.map((es) => es.newNode);
604
- return {
605
- outputNodes: [...this.inputProblem.meshNodes, ...expandedNodes]
606
- };
607
- }
608
- initialVisualize() {
609
- const graphics = {
610
- title: "GapFillSolverPipeline - Initial",
611
- coordinateSystem: "cartesian",
612
- rects: [],
613
- points: [],
614
- lines: [],
615
- circles: [],
616
- arrows: [],
617
- texts: []
618
- };
619
- for (const node of this.inputProblem.meshNodes) {
620
- graphics.rects.push({
621
- center: node.center,
622
- width: node.width,
623
- height: node.height,
624
- stroke: "rgba(0, 0, 0, 0.3)",
625
- fill: "rgba(100, 100, 100, 0.1)",
626
- layer: `z${node.availableZ.join(",")}`,
627
- label: [
628
- `node ${node.capacityMeshNodeId}`,
629
- `z:${node.availableZ.join(",")}`
630
- ].join("\n")
631
- });
632
- }
633
- return graphics;
634
- }
635
- finalVisualize() {
636
- const graphics = {
637
- title: "GapFillSolverPipeline - Final",
638
- coordinateSystem: "cartesian",
639
- rects: [],
640
- points: [],
641
- lines: [],
642
- circles: [],
643
- arrows: [],
644
- texts: []
645
- };
646
- const { outputNodes } = this.getOutput();
647
- const expandedSegments = this.expandEdgesToEmptySpaceSolver?.getOutput().expandedSegments ?? [];
648
- const expandedNodeIds = new Set(
649
- expandedSegments.map((es) => es.newNode.capacityMeshNodeId)
650
- );
651
- for (const node of outputNodes) {
652
- const isExpanded = expandedNodeIds.has(node.capacityMeshNodeId);
653
- graphics.rects.push({
654
- center: node.center,
655
- width: node.width,
656
- height: node.height,
657
- stroke: isExpanded ? "rgba(0, 128, 0, 0.8)" : "rgba(0, 0, 0, 0.3)",
658
- fill: isExpanded ? "rgba(0, 200, 0, 0.3)" : "rgba(100, 100, 100, 0.1)",
659
- layer: `z${node.availableZ.join(",")}`,
660
- label: [
661
- `${isExpanded ? "[expanded] " : ""}node ${node.capacityMeshNodeId}`,
662
- `z:${node.availableZ.join(",")}`
663
- ].join("\n")
664
- });
665
- }
666
- return graphics;
667
- }
668
- };
669
-
670
- // lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts
671
- import {
672
- BasePipelineSolver as BasePipelineSolver2,
673
- definePipelineStep as definePipelineStep2
674
- } from "@tscircuit/solver-utils";
675
-
676
- // lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts
677
- import { BaseSolver as BaseSolver3 } from "@tscircuit/solver-utils";
678
-
679
- // lib/utils/rectdiff-geometry.ts
680
- var EPS4 = 1e-9;
681
- var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
682
- var gt = (a, b) => a > b + EPS4;
683
- var gte = (a, b) => a > b - EPS4;
684
- var lt = (a, b) => a < b - EPS4;
685
- var lte = (a, b) => a < b + EPS4;
686
- function overlaps(a, b) {
687
- return !(a.x + a.width <= b.x + EPS4 || b.x + b.width <= a.x + EPS4 || a.y + a.height <= b.y + EPS4 || b.y + b.height <= a.y + EPS4);
688
- }
689
- function containsPoint(r, p) {
690
- return p.x >= r.x - EPS4 && p.x <= r.x + r.width + EPS4 && p.y >= r.y - EPS4 && p.y <= r.y + r.height + EPS4;
691
- }
692
- function distancePointToRectEdges(p, r) {
693
- const minX = r.x;
694
- const maxX = r.x + r.width;
695
- const minY = r.y;
696
- const maxY = r.y + r.height;
697
- if (p.x >= minX && p.x <= maxX && p.y >= minY && p.y <= maxY) {
698
- return Math.min(p.x - minX, maxX - p.x, p.y - minY, maxY - p.y);
699
- }
700
- const dx = p.x < minX ? minX - p.x : p.x > maxX ? p.x - maxX : 0;
701
- const dy = p.y < minY ? minY - p.y : p.y > maxY ? p.y - maxY : 0;
702
- if (dx === 0) return dy;
703
- if (dy === 0) return dx;
704
- return Math.hypot(dx, dy);
705
- }
706
- function intersect1D(r1, r2) {
707
- const lo = Math.max(r1[0], r2[0]);
708
- const hi = Math.min(r1[1], r2[1]);
709
- return hi > lo + EPS4 ? [lo, hi] : null;
710
- }
711
- function subtractRect2D(A, B) {
712
- if (!overlaps(A, B)) return [A];
713
- const Xi = intersect1D([A.x, A.x + A.width], [B.x, B.x + B.width]);
714
- const Yi = intersect1D([A.y, A.y + A.height], [B.y, B.y + B.height]);
715
- if (!Xi || !Yi) return [A];
716
- const [X0, X1] = Xi;
717
- const [Y0, Y1] = Yi;
718
- const out = [];
719
- if (X0 > A.x + EPS4) {
720
- out.push({ x: A.x, y: A.y, width: X0 - A.x, height: A.height });
721
- }
722
- if (A.x + A.width > X1 + EPS4) {
723
- out.push({ x: X1, y: A.y, width: A.x + A.width - X1, height: A.height });
724
- }
725
- const midW = Math.max(0, X1 - X0);
726
- if (midW > EPS4 && Y0 > A.y + EPS4) {
727
- out.push({ x: X0, y: A.y, width: midW, height: Y0 - A.y });
728
- }
729
- if (midW > EPS4 && A.y + A.height > Y1 + EPS4) {
730
- out.push({ x: X0, y: Y1, width: midW, height: A.y + A.height - Y1 });
731
- }
732
- return out.filter((r) => r.width > EPS4 && r.height > EPS4);
733
- }
734
-
735
- // lib/solvers/RectDiffSeedingSolver/isPointInPolygon.ts
736
- function isPointInPolygon(p, polygon) {
737
- let inside = false;
738
- for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
739
- const xi = polygon[i].x, yi = polygon[i].y;
740
- const xj = polygon[j].x, yj = polygon[j].y;
741
- const intersect = yi > p.y !== yj > p.y && p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi;
742
- if (intersect) inside = !inside;
743
- }
744
- return inside;
745
- }
746
-
747
- // lib/solvers/RectDiffSeedingSolver/computeInverseRects.ts
748
- function simplifyPolygon(polygon, precision) {
749
- const round = (v) => Math.round(v / precision) * precision;
750
- const seen = /* @__PURE__ */ new Set();
751
- const result = [];
752
- for (const p of polygon) {
753
- const rx = round(p.x);
754
- const ry = round(p.y);
755
- const key = `${rx},${ry}`;
756
- if (!seen.has(key)) {
757
- seen.add(key);
758
- result.push({ x: rx, y: ry });
759
- }
760
- }
761
- return result;
762
- }
763
- function computeInverseRects(bounds, polygon) {
764
- if (!polygon || polygon.length < 3) return [];
765
- const MAX_POLYGON_POINTS = 100;
766
- const workingPolygon = polygon.length > MAX_POLYGON_POINTS ? simplifyPolygon(
767
- polygon,
768
- Math.max(bounds.width, bounds.height) / MAX_POLYGON_POINTS
769
- ) : polygon;
770
- const xs = /* @__PURE__ */ new Set([bounds.x, bounds.x + bounds.width]);
771
- const ys = /* @__PURE__ */ new Set([bounds.y, bounds.y + bounds.height]);
772
- for (const p of workingPolygon) {
773
- xs.add(p.x);
774
- ys.add(p.y);
775
- }
776
- const xSorted = Array.from(xs).sort((a, b) => a - b);
777
- const ySorted = Array.from(ys).sort((a, b) => a - b);
778
- const rawRects = [];
779
- for (let i = 0; i < xSorted.length - 1; i++) {
780
- for (let j = 0; j < ySorted.length - 1; j++) {
781
- const x0 = xSorted[i];
782
- const x1 = xSorted[i + 1];
783
- const y0 = ySorted[j];
784
- const y1 = ySorted[j + 1];
785
- const cx = (x0 + x1) / 2;
786
- const cy = (y0 + y1) / 2;
787
- if (cx >= bounds.x && cx <= bounds.x + bounds.width && cy >= bounds.y && cy <= bounds.y + bounds.height) {
788
- if (!isPointInPolygon({ x: cx, y: cy }, polygon)) {
789
- rawRects.push({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
790
- }
791
- }
792
- }
793
- }
794
- const finalRects = [];
795
- rawRects.sort((a, b) => {
796
- if (Math.abs(a.y - b.y) > EPS4) return a.y - b.y;
797
- return a.x - b.x;
798
- });
799
- let current = null;
800
- for (const r of rawRects) {
801
- if (!current) {
802
- current = r;
803
- continue;
804
- }
805
- const sameY = Math.abs(current.y - r.y) < EPS4;
806
- const sameHeight = Math.abs(current.height - r.height) < EPS4;
807
- const touchesX = Math.abs(current.x + current.width - r.x) < EPS4;
808
- if (sameY && sameHeight && touchesX) {
809
- current.width += r.width;
810
- } else {
811
- finalRects.push(current);
812
- current = r;
813
- }
814
- }
815
- if (current) finalRects.push(current);
816
- finalRects.sort((a, b) => {
817
- if (Math.abs(a.x - b.x) > EPS4) return a.x - b.x;
818
- return a.y - b.y;
819
- });
820
- const mergedVertical = [];
821
- current = null;
822
- for (const r of finalRects) {
823
- if (!current) {
824
- current = r;
825
- continue;
826
- }
827
- const sameX = Math.abs(current.x - r.x) < EPS4;
828
- const sameWidth = Math.abs(current.width - r.width) < EPS4;
829
- const touchesY = Math.abs(current.y + current.height - r.y) < EPS4;
830
- if (sameX && sameWidth && touchesY) {
831
- current.height += r.height;
832
- } else {
833
- mergedVertical.push(current);
834
- current = r;
835
- }
836
- }
837
- if (current) mergedVertical.push(current);
838
- return mergedVertical;
839
- }
840
-
841
- // lib/solvers/RectDiffSeedingSolver/layers.ts
842
- function layerSortKey(n) {
843
- const L = n.toLowerCase();
844
- if (L === "top") return -1e6;
845
- if (L === "bottom") return 1e6;
846
- const m = /^inner(\d+)$/i.exec(L);
847
- if (m) return parseInt(m[1], 10) || 0;
848
- return 100 + L.charCodeAt(0);
849
- }
850
- function canonicalizeLayerOrder(names) {
851
- return Array.from(new Set(names)).sort((a, b) => {
852
- const ka = layerSortKey(a);
853
- const kb = layerSortKey(b);
854
- if (ka !== kb) return ka - kb;
855
- return a.localeCompare(b);
856
- });
857
- }
858
- function buildZIndexMap(params) {
859
- const names = canonicalizeLayerOrder(
860
- (params.obstacles ?? []).flatMap((o) => o.layers ?? [])
861
- );
862
- const declaredLayerCount = Math.max(1, params.layerCount || names.length || 1);
863
- const fallback = Array.from(
864
- { length: declaredLayerCount },
865
- (_, i) => i === 0 ? "top" : i === declaredLayerCount - 1 ? "bottom" : `inner${i}`
866
- );
867
- const ordered = [];
868
- const seen = /* @__PURE__ */ new Set();
869
- const push = (n) => {
870
- const key = n.toLowerCase();
871
- if (seen.has(key)) return;
872
- seen.add(key);
873
- ordered.push(n);
874
- };
875
- fallback.forEach(push);
876
- names.forEach(push);
877
- const layerNames = ordered.slice(0, declaredLayerCount);
878
- const clampIndex = (nameLower) => {
879
- if (layerNames.length <= 1) return 0;
880
- if (nameLower === "top") return 0;
881
- if (nameLower === "bottom") return layerNames.length - 1;
882
- const m = /^inner(\d+)$/i.exec(nameLower);
883
- if (m) {
884
- if (layerNames.length <= 2) return layerNames.length - 1;
885
- const parsed = parseInt(m[1], 10);
886
- const maxInner = layerNames.length - 2;
887
- const clampedInner = Math.min(
888
- maxInner,
889
- Math.max(1, Number.isFinite(parsed) ? parsed : 1)
890
- );
891
- return clampedInner;
892
- }
893
- return 0;
894
- };
895
- const map = /* @__PURE__ */ new Map();
896
- layerNames.forEach((n, i) => map.set(n.toLowerCase(), i));
897
- ordered.slice(layerNames.length).forEach((n) => {
898
- const key = n.toLowerCase();
899
- map.set(key, clampIndex(key));
900
- });
901
- return { layerNames, zIndexByName: map };
902
- }
903
- function obstacleZs(ob, zIndexByName) {
904
- if (ob.zLayers?.length)
905
- return Array.from(new Set(ob.zLayers)).sort((a, b) => a - b);
906
- const fromNames = (ob.layers ?? []).map((n) => zIndexByName.get(n.toLowerCase())).filter((v) => typeof v === "number");
907
- return Array.from(new Set(fromNames)).sort((a, b) => a - b);
908
- }
909
- function obstacleToXYRect(ob) {
910
- const w = ob.width;
911
- const h = ob.height;
912
- if (typeof w !== "number" || typeof h !== "number") return null;
913
- return { x: ob.center.x - w / 2, y: ob.center.y - h / 2, width: w, height: h };
914
- }
915
-
916
- // lib/utils/isSelfRect.ts
917
- var EPS5 = 1e-9;
918
- var isSelfRect = (params) => Math.abs(params.rect.x + params.rect.width / 2 - params.startX) < EPS5 && Math.abs(params.rect.y + params.rect.height / 2 - params.startY) < EPS5 && Math.abs(params.rect.width - params.initialW) < EPS5 && Math.abs(params.rect.height - params.initialH) < EPS5;
919
-
920
- // lib/utils/searchStrip.ts
921
- var searchStripRight = ({
922
- rect,
923
- bounds
924
- }) => ({
925
- x: rect.x,
926
- y: rect.y,
927
- width: bounds.x + bounds.width - rect.x,
928
- height: rect.height
929
- });
930
- var searchStripDown = ({
931
- rect,
932
- bounds
933
- }) => ({
934
- x: rect.x,
935
- y: rect.y,
936
- width: rect.width,
937
- height: bounds.y + bounds.height - rect.y
938
- });
939
- var searchStripLeft = ({
940
- rect,
941
- bounds
942
- }) => ({
943
- x: bounds.x,
944
- y: rect.y,
945
- width: rect.x - bounds.x,
946
- height: rect.height
947
- });
948
- var searchStripUp = ({
949
- rect,
950
- bounds
951
- }) => ({
952
- x: rect.x,
953
- y: bounds.y,
954
- width: rect.width,
955
- height: rect.y - bounds.y
956
- });
957
-
958
- // lib/utils/expandRectFromSeed.ts
959
- var quantize = (value, precision = 1e-6) => Math.round(value / precision) * precision;
960
- var quantizeRect = (rect) => ({
961
- x: quantize(rect.x),
962
- y: quantize(rect.y),
963
- width: quantize(rect.width),
964
- height: quantize(rect.height)
965
- });
966
- function maxExpandRight(params) {
967
- const { r, bounds, blockers, maxAspect } = params;
968
- let maxWidth = bounds.x + bounds.width - r.x;
969
- for (const b of blockers) {
970
- const verticallyOverlaps = r.y + r.height > b.y + EPS4 && b.y + b.height > r.y + EPS4;
971
- if (verticallyOverlaps) {
972
- if (gte(b.x, r.x + r.width)) {
973
- maxWidth = Math.min(maxWidth, b.x - r.x);
974
- } else if (b.x + b.width > r.x + r.width - EPS4 && b.x < r.x + r.width + EPS4) {
975
- return 0;
976
- }
977
- }
978
- }
979
- let e = Math.max(0, maxWidth - r.width);
980
- if (e <= 0) return 0;
981
- if (maxAspect != null) {
982
- const w = r.width, h = r.height;
983
- if (w >= h) e = Math.min(e, maxAspect * h - w);
984
- }
985
- return Math.max(0, e);
986
- }
987
- function maxExpandDown(params) {
988
- const { r, bounds, blockers, maxAspect } = params;
989
- let maxHeight = bounds.y + bounds.height - r.y;
990
- for (const b of blockers) {
991
- const horizOverlaps = r.x + r.width > b.x + EPS4 && b.x + b.width > r.x + EPS4;
992
- if (horizOverlaps) {
993
- if (gte(b.y, r.y + r.height)) {
994
- maxHeight = Math.min(maxHeight, b.y - r.y);
995
- } else if (b.y + b.height > r.y + r.height - EPS4 && b.y < r.y + r.height + EPS4) {
996
- return 0;
997
- }
998
- }
999
- }
1000
- let e = Math.max(0, maxHeight - r.height);
1001
- if (e <= 0) return 0;
1002
- if (maxAspect != null) {
1003
- const w = r.width, h = r.height;
1004
- if (h >= w) e = Math.min(e, maxAspect * w - h);
1005
- }
1006
- return Math.max(0, e);
1007
- }
1008
- function maxExpandLeft(params) {
1009
- const { r, bounds, blockers, maxAspect } = params;
1010
- let minX = bounds.x;
1011
- for (const b of blockers) {
1012
- const verticallyOverlaps = r.y + r.height > b.y + EPS4 && b.y + b.height > r.y + EPS4;
1013
- if (verticallyOverlaps) {
1014
- if (lte(b.x + b.width, r.x)) {
1015
- minX = Math.max(minX, b.x + b.width);
1016
- } else if (b.x < r.x + EPS4 && b.x + b.width > r.x - EPS4) {
1017
- return 0;
1018
- }
1019
- }
1020
- }
1021
- let e = Math.max(0, r.x - minX);
1022
- if (e <= 0) return 0;
1023
- if (maxAspect != null) {
1024
- const w = r.width, h = r.height;
1025
- if (w >= h) e = Math.min(e, maxAspect * h - w);
1026
- }
1027
- return Math.max(0, e);
1028
- }
1029
- function maxExpandUp(params) {
1030
- const { r, bounds, blockers, maxAspect } = params;
1031
- let minY = bounds.y;
1032
- for (const b of blockers) {
1033
- const horizOverlaps = r.x + r.width > b.x + EPS4 && b.x + b.width > r.x + EPS4;
1034
- if (horizOverlaps) {
1035
- if (lte(b.y + b.height, r.y)) {
1036
- minY = Math.max(minY, b.y + b.height);
1037
- } else if (b.y < r.y + EPS4 && b.y + b.height > r.y - EPS4) {
1038
- return 0;
1039
- }
1040
- }
1041
- }
1042
- let e = Math.max(0, r.y - minY);
1043
- if (e <= 0) return 0;
1044
- if (maxAspect != null) {
1045
- const w = r.width, h = r.height;
1046
- if (h >= w) e = Math.min(e, maxAspect * w - h);
1047
- }
1048
- return Math.max(0, e);
1049
- }
1050
- var addBlocker = (params) => {
1051
- const { rect, seen, blockers } = params;
1052
- if (seen.has(rect)) return;
1053
- seen.add(rect);
1054
- blockers.push(rect);
1055
- };
1056
- var toQueryRect = (params) => {
1057
- const { bounds, rect } = params;
1058
- const minX = Math.max(bounds.x, rect.x);
1059
- const minY = Math.max(bounds.y, rect.y);
1060
- const maxX = Math.min(bounds.x + bounds.width, rect.x + rect.width);
1061
- const maxY = Math.min(bounds.y + bounds.height, rect.y + rect.height);
1062
- if (maxX <= minX + EPS4 || maxY <= minY + EPS4) return null;
1063
- return { minX, minY, maxX, maxY };
1064
- };
1065
- function expandRectFromSeed(params) {
1066
- const {
1067
- startX,
1068
- startY,
1069
- gridSize,
1070
- bounds,
1071
- obsticalIndexByLayer,
1072
- placedIndexByLayer,
1073
- initialCellRatio,
1074
- maxAspectRatio,
1075
- minReq
1076
- } = params;
1077
- const minSide = Math.max(1e-9, gridSize * initialCellRatio);
1078
- const initialW = Math.max(minSide, minReq.width);
1079
- const initialH = Math.max(minSide, minReq.height);
1080
- const blockers = [];
1081
- const seen = /* @__PURE__ */ new Set();
1082
- const totalLayers = placedIndexByLayer.length;
1083
- const collectBlockers = (searchRect) => {
1084
- const query = toQueryRect({ bounds, rect: searchRect });
1085
- if (!query) return;
1086
- for (const z of params.zLayers) {
1087
- const blockersIndex = obsticalIndexByLayer[z];
1088
- if (blockersIndex) {
1089
- for (const entry of blockersIndex.search(query))
1090
- addBlocker({ rect: entry, seen, blockers });
1091
- }
1092
- const placedLayer = placedIndexByLayer[z];
1093
- if (placedLayer) {
1094
- for (const entry of placedLayer.search(query)) {
1095
- const isFullStack = entry.zLayers.length >= totalLayers;
1096
- if (!isFullStack) continue;
1097
- if (isSelfRect({
1098
- rect: entry,
1099
- startX,
1100
- startY,
1101
- initialW,
1102
- initialH
1103
- }))
1104
- continue;
1105
- addBlocker({ rect: entry, seen, blockers });
1106
- }
1107
- }
1108
- }
1109
- };
1110
- const strategies = [
1111
- { ox: 0, oy: 0 },
1112
- { ox: -initialW, oy: 0 },
1113
- { ox: 0, oy: -initialH },
1114
- { ox: -initialW, oy: -initialH },
1115
- { ox: -initialW / 2, oy: -initialH / 2 }
1116
- ];
1117
- let best = null;
1118
- let bestArea = 0;
1119
- STRATS: for (const s of strategies) {
1120
- let r = {
1121
- x: startX + s.ox,
1122
- y: startY + s.oy,
1123
- width: initialW,
1124
- height: initialH
1125
- };
1126
- collectBlockers(r);
1127
- if (lt(r.x, bounds.x) || lt(r.y, bounds.y) || gt(r.x + r.width, bounds.x + bounds.width) || gt(r.y + r.height, bounds.y + bounds.height)) {
1128
- continue;
1129
- }
1130
- for (const b of blockers) if (overlaps(r, b)) continue STRATS;
1131
- const MIN_EXPANSION = 1e-6;
1132
- const MAX_ITERATIONS = 1e3;
1133
- let improved = true;
1134
- let iterations = 0;
1135
- while (improved && iterations < MAX_ITERATIONS) {
1136
- iterations++;
1137
- improved = false;
1138
- const commonParams = { bounds, blockers, maxAspect: maxAspectRatio };
1139
- collectBlockers(searchStripRight({ rect: r, bounds }));
1140
- const eR = maxExpandRight({ ...commonParams, r });
1141
- if (eR > MIN_EXPANSION) {
1142
- r = { ...r, width: r.width + eR };
1143
- collectBlockers(r);
1144
- improved = true;
1145
- }
1146
- collectBlockers(searchStripDown({ rect: r, bounds }));
1147
- const eD = maxExpandDown({ ...commonParams, r });
1148
- if (eD > MIN_EXPANSION) {
1149
- r = { ...r, height: r.height + eD };
1150
- collectBlockers(r);
1151
- improved = true;
1152
- }
1153
- collectBlockers(searchStripLeft({ rect: r, bounds }));
1154
- const eL = maxExpandLeft({ ...commonParams, r });
1155
- if (eL > MIN_EXPANSION) {
1156
- r = { x: r.x - eL, y: r.y, width: r.width + eL, height: r.height };
1157
- collectBlockers(r);
1158
- improved = true;
1159
- }
1160
- collectBlockers(searchStripUp({ rect: r, bounds }));
1161
- const eU = maxExpandUp({ ...commonParams, r });
1162
- if (eU > MIN_EXPANSION) {
1163
- r = { x: r.x, y: r.y - eU, width: r.width, height: r.height + eU };
1164
- collectBlockers(r);
1165
- improved = true;
1166
- }
1167
- }
1168
- if (r.width + EPS4 >= minReq.width && r.height + EPS4 >= minReq.height) {
1169
- const area = r.width * r.height;
1170
- if (area > bestArea) {
1171
- best = quantizeRect(r);
1172
- bestArea = area;
1173
- }
1174
- }
1175
- }
1176
- return best;
1177
- }
1178
-
1179
- // lib/solvers/RectDiffSeedingSolver/computeDefaultGridSizes.ts
1180
- function computeDefaultGridSizes(bounds) {
1181
- const ref = Math.max(bounds.width, bounds.height);
1182
- return [ref / 8, ref / 16, ref / 32];
1183
- }
1184
-
1185
- // lib/utils/isFullyOccupiedAtPoint.ts
1186
- import "rbush";
1187
- function isFullyOccupiedAtPoint(params) {
1188
- const query = {
1189
- minX: params.point.x,
1190
- minY: params.point.y,
1191
- maxX: params.point.x,
1192
- maxY: params.point.y
1193
- };
1194
- for (let z = 0; z < params.layerCount; z++) {
1195
- const obstacleIdx = params.obstacleIndexByLayer[z];
1196
- const hasObstacle = !!obstacleIdx && obstacleIdx.collides(query);
1197
- const placedIdx = params.placedIndexByLayer[z];
1198
- const hasPlaced = !!placedIdx && placedIdx.collides(query);
1199
- if (!hasObstacle && !hasPlaced) return false;
1200
- }
1201
- return true;
1202
- }
1203
-
1204
- // lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts
1205
- function longestFreeSpanAroundZ(params) {
1206
- const {
1207
- x,
1208
- y,
1209
- z,
1210
- layerCount,
1211
- minSpan,
1212
- maxSpan,
1213
- obstacleIndexByLayer,
1214
- additionalBlockersByLayer
1215
- } = params;
1216
- const isFreeAt = (layer) => {
1217
- const query = {
1218
- minX: x,
1219
- minY: y,
1220
- maxX: x,
1221
- maxY: y
1222
- };
1223
- const obstacleIdx = obstacleIndexByLayer[layer];
1224
- if (obstacleIdx && obstacleIdx.collides(query)) return false;
1225
- const extras = additionalBlockersByLayer?.[layer] ?? [];
1226
- return !extras.some((b) => containsPoint(b, { x, y }));
1227
- };
1228
- let lo = z;
1229
- let hi = z;
1230
- while (lo - 1 >= 0 && isFreeAt(lo - 1)) lo--;
1231
- while (hi + 1 < layerCount && isFreeAt(hi + 1)) hi++;
1232
- if (typeof maxSpan === "number") {
1233
- const target = clamp(maxSpan, 1, layerCount);
1234
- while (hi - lo + 1 > target) {
1235
- if (z - lo > hi - z) lo++;
1236
- else hi--;
1237
- }
1238
- }
1239
- const res = [];
1240
- for (let i = lo; i <= hi; i++) res.push(i);
1241
- return res.length >= minSpan ? res : [];
1242
- }
1243
-
1244
- // lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts
1245
- var quantize2 = (value, precision = 1e-6) => Math.round(value / precision) * precision;
1246
- function computeCandidates3D(params) {
1247
- const {
1248
- bounds,
1249
- gridSize,
1250
- layerCount,
1251
- obstacleIndexByLayer,
1252
- placedIndexByLayer,
1253
- hardPlacedByLayer
1254
- } = params;
1255
- const out = /* @__PURE__ */ new Map();
1256
- const hardRectsByLayer = Array.from({ length: layerCount }, (_, z) => [
1257
- ...obstacleIndexByLayer[z]?.all() ?? [],
1258
- ...hardPlacedByLayer[z] ?? []
1259
- ]);
1260
- for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
1261
- for (let y = bounds.y; y < bounds.y + bounds.height; y += gridSize) {
1262
- if (Math.abs(x - bounds.x) < EPS4 || Math.abs(y - bounds.y) < EPS4 || x > bounds.x + bounds.width - gridSize - EPS4 || y > bounds.y + bounds.height - gridSize - EPS4) {
1263
- continue;
1264
- }
1265
- if (isFullyOccupiedAtPoint({
1266
- layerCount,
1267
- obstacleIndexByLayer,
1268
- placedIndexByLayer,
1269
- point: { x, y }
1270
- }))
1271
- continue;
1272
- let bestSpan = [];
1273
- let bestZ = 0;
1274
- for (let z = 0; z < layerCount; z++) {
1275
- const s = longestFreeSpanAroundZ({
1276
- x,
1277
- y,
1278
- z,
1279
- layerCount,
1280
- minSpan: 1,
1281
- maxSpan: void 0,
1282
- obstacleIndexByLayer,
1283
- additionalBlockersByLayer: hardPlacedByLayer
1284
- });
1285
- if (s.length > bestSpan.length) {
1286
- bestSpan = s;
1287
- bestZ = z;
1288
- }
1289
- }
1290
- const anchorZ = bestSpan.length ? bestSpan[Math.floor(bestSpan.length / 2)] : bestZ;
1291
- const hardAtZ = hardRectsByLayer[anchorZ] ?? [];
1292
- let d = distancePointToRectEdges({ x, y }, bounds);
1293
- for (const blocker of hardAtZ) {
1294
- d = Math.min(d, distancePointToRectEdges({ x, y }, blocker));
1295
- }
1296
- const distance = quantize2(d);
1297
- const k = `${x.toFixed(6)}|${y.toFixed(6)}`;
1298
- const cand = {
1299
- x,
1300
- y,
1301
- z: anchorZ,
1302
- distance,
1303
- zSpanLen: bestSpan.length
1304
- };
1305
- const prev = out.get(k);
1306
- if (!prev || cand.zSpanLen > (prev.zSpanLen ?? 0) || cand.zSpanLen === prev.zSpanLen && cand.distance > prev.distance) {
1307
- out.set(k, cand);
1308
- }
1309
- }
1310
- }
1311
- const arr = Array.from(out.values());
1312
- arr.sort(
1313
- (a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance || a.z - b.z || a.x - b.x || a.y - b.y
1314
- );
1315
- return arr;
1316
- }
1317
-
1318
- // lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts
1319
- var quantize3 = (value, precision = 1e-6) => Math.round(value / precision) * precision;
1320
- var toRect = (rect) => "minX" in rect ? {
1321
- x: rect.minX,
1322
- y: rect.minY,
1323
- width: rect.maxX - rect.minX,
1324
- height: rect.maxY - rect.minY
1325
- } : rect;
1326
- function computeUncoveredSegments(params) {
1327
- const { lineStart, lineEnd, coveringIntervals, minSegmentLength } = params;
1328
- const lineStartQ = quantize3(lineStart);
1329
- const lineEndQ = quantize3(lineEnd);
1330
- const normalizedIntervals = coveringIntervals.map((i) => {
1331
- const s = quantize3(i.start);
1332
- const e = quantize3(i.end);
1333
- return { start: Math.min(s, e), end: Math.max(s, e) };
1334
- }).filter((i) => i.end > i.start + EPS4);
1335
- if (normalizedIntervals.length === 0) {
1336
- const center = (lineStartQ + lineEndQ) / 2;
1337
- return [{ start: lineStartQ, end: lineEndQ, center }];
1338
- }
1339
- const sorted = [...normalizedIntervals].sort((a, b) => a.start - b.start);
1340
- const merged = [];
1341
- let current = { ...sorted[0] };
1342
- for (let i = 1; i < sorted.length; i++) {
1343
- const interval = sorted[i];
1344
- if (interval.start <= current.end + EPS4) {
1345
- current.end = Math.max(current.end, interval.end);
1346
- } else {
1347
- merged.push(current);
1348
- current = { ...interval };
1349
- }
1350
- }
1351
- merged.push(current);
1352
- const uncovered = [];
1353
- if (merged[0].start > lineStartQ + EPS4) {
1354
- const start = lineStartQ;
1355
- const end = merged[0].start;
1356
- if (end - start >= minSegmentLength) {
1357
- uncovered.push({ start, end, center: (start + end) / 2 });
1358
- }
1359
- }
1360
- for (let i = 0; i < merged.length - 1; i++) {
1361
- const start = merged[i].end;
1362
- const end = merged[i + 1].start;
1363
- if (end - start >= minSegmentLength) {
1364
- uncovered.push({ start, end, center: (start + end) / 2 });
1365
- }
1366
- }
1367
- if (merged[merged.length - 1].end < lineEndQ - EPS4) {
1368
- const start = merged[merged.length - 1].end;
1369
- const end = lineEndQ;
1370
- if (end - start >= minSegmentLength) {
1371
- uncovered.push({ start, end, center: (start + end) / 2 });
1372
- }
1373
- }
1374
- return uncovered;
1375
- }
1376
- function computeEdgeCandidates3D(params) {
1377
- const {
1378
- bounds,
1379
- minSize,
1380
- layerCount,
1381
- obstacleIndexByLayer,
1382
- placedIndexByLayer,
1383
- hardPlacedByLayer
1384
- } = params;
1385
- const out = [];
1386
- const \u03B4 = Math.max(minSize * 0.15, EPS4 * 3);
1387
- const dedup = /* @__PURE__ */ new Set();
1388
- const hardRectsByLayer = Array.from(
1389
- { length: layerCount },
1390
- (_, z) => [
1391
- ...obstacleIndexByLayer[z]?.all() ?? [],
1392
- ...hardPlacedByLayer[z] ?? []
1393
- ].map(toRect)
1394
- );
1395
- const key = (p) => `${p.z}|${p.x.toFixed(6)}|${p.y.toFixed(6)}`;
1396
- function fullyOcc(p) {
1397
- return isFullyOccupiedAtPoint({
1398
- layerCount,
1399
- obstacleIndexByLayer,
1400
- placedIndexByLayer,
1401
- point: p
1402
- });
1403
- }
1404
- function pushIfFree(p) {
1405
- const qx = quantize3(p.x);
1406
- const qy = quantize3(p.y);
1407
- const { z } = p;
1408
- const x = qx;
1409
- const y = qy;
1410
- if (x < bounds.x + EPS4 || y < bounds.y + EPS4 || x > bounds.x + bounds.width - EPS4 || y > bounds.y + bounds.height - EPS4)
1411
- return;
1412
- if (fullyOcc({ x, y })) return;
1413
- const hard = hardRectsByLayer[z] ?? [];
1414
- let d = distancePointToRectEdges({ x, y }, bounds);
1415
- for (const blocker of hard) {
1416
- d = Math.min(d, distancePointToRectEdges({ x, y }, blocker));
1417
- }
1418
- const distance = quantize3(d);
1419
- const k = key({ x, y, z });
1420
- if (dedup.has(k)) return;
1421
- dedup.add(k);
1422
- const span = longestFreeSpanAroundZ({
1423
- x,
1424
- y,
1425
- z,
1426
- layerCount,
1427
- minSpan: 1,
1428
- maxSpan: void 0,
1429
- obstacleIndexByLayer,
1430
- additionalBlockersByLayer: hardPlacedByLayer
1431
- });
1432
- out.push({
1433
- x,
1434
- y,
1435
- z,
1436
- distance,
1437
- zSpanLen: span.length,
1438
- isEdgeSeed: true
1439
- });
1440
- }
1441
- for (let z = 0; z < layerCount; z++) {
1442
- const blockers = (hardRectsByLayer[z] ?? []).map((b) => ({
1443
- x: quantize3(b.x),
1444
- y: quantize3(b.y),
1445
- width: quantize3(b.width),
1446
- height: quantize3(b.height)
1447
- }));
1448
- const corners = [
1449
- { x: bounds.x + \u03B4, y: bounds.y + \u03B4 },
1450
- // top-left
1451
- { x: bounds.x + bounds.width - \u03B4, y: bounds.y + \u03B4 },
1452
- // top-right
1453
- { x: bounds.x + \u03B4, y: bounds.y + bounds.height - \u03B4 },
1454
- // bottom-left
1455
- { x: bounds.x + bounds.width - \u03B4, y: bounds.y + bounds.height - \u03B4 }
1456
- // bottom-right
1457
- ];
1458
- for (const corner of corners) {
1459
- pushIfFree({ x: corner.x, y: corner.y, z });
1460
- }
1461
- const topY = bounds.y + \u03B4;
1462
- const topCovering = blockers.filter((b) => b.y <= topY && b.y + b.height >= topY).map((b) => ({
1463
- start: Math.max(bounds.x, b.x),
1464
- end: Math.min(bounds.x + bounds.width, b.x + b.width)
1465
- }));
1466
- const topUncovered = computeUncoveredSegments({
1467
- lineStart: bounds.x + \u03B4,
1468
- lineEnd: bounds.x + bounds.width - \u03B4,
1469
- coveringIntervals: topCovering,
1470
- minSegmentLength: minSize * 0.5
1471
- });
1472
- for (const seg of topUncovered) {
1473
- const segLen = seg.end - seg.start;
1474
- if (segLen >= minSize) {
1475
- pushIfFree({ x: seg.center, y: topY, z });
1476
- if (segLen > minSize * 1.5) {
1477
- pushIfFree({ x: seg.start + minSize * 0.4, y: topY, z });
1478
- pushIfFree({ x: seg.end - minSize * 0.4, y: topY, z });
1479
- }
1480
- }
1481
- }
1482
- const bottomY = bounds.y + bounds.height - \u03B4;
1483
- const bottomCovering = blockers.filter((b) => b.y <= bottomY && b.y + b.height >= bottomY).map((b) => ({
1484
- start: Math.max(bounds.x, b.x),
1485
- end: Math.min(bounds.x + bounds.width, b.x + b.width)
1486
- }));
1487
- const bottomUncovered = computeUncoveredSegments({
1488
- lineStart: bounds.x + \u03B4,
1489
- lineEnd: bounds.x + bounds.width - \u03B4,
1490
- coveringIntervals: bottomCovering,
1491
- minSegmentLength: minSize * 0.5
1492
- });
1493
- for (const seg of bottomUncovered) {
1494
- const segLen = seg.end - seg.start;
1495
- if (segLen >= minSize) {
1496
- pushIfFree({ x: seg.center, y: bottomY, z });
1497
- if (segLen > minSize * 1.5) {
1498
- pushIfFree({ x: seg.start + minSize * 0.4, y: bottomY, z });
1499
- pushIfFree({ x: seg.end - minSize * 0.4, y: bottomY, z });
1500
- }
1501
- }
1502
- }
1503
- const leftX = bounds.x + \u03B4;
1504
- const leftCovering = blockers.filter((b) => b.x <= leftX && b.x + b.width >= leftX).map((b) => ({
1505
- start: Math.max(bounds.y, b.y),
1506
- end: Math.min(bounds.y + bounds.height, b.y + b.height)
1507
- }));
1508
- const leftUncovered = computeUncoveredSegments({
1509
- lineStart: bounds.y + \u03B4,
1510
- lineEnd: bounds.y + bounds.height - \u03B4,
1511
- coveringIntervals: leftCovering,
1512
- minSegmentLength: minSize * 0.5
1513
- });
1514
- for (const seg of leftUncovered) {
1515
- const segLen = seg.end - seg.start;
1516
- if (segLen >= minSize) {
1517
- pushIfFree({ x: leftX, y: seg.center, z });
1518
- if (segLen > minSize * 1.5) {
1519
- pushIfFree({ x: leftX, y: seg.start + minSize * 0.4, z });
1520
- pushIfFree({ x: leftX, y: seg.end - minSize * 0.4, z });
1521
- }
1522
- }
1523
- }
1524
- const rightX = bounds.x + bounds.width - \u03B4;
1525
- const rightCovering = blockers.filter((b) => b.x <= rightX && b.x + b.width >= rightX).map((b) => ({
1526
- start: Math.max(bounds.y, b.y),
1527
- end: Math.min(bounds.y + bounds.height, b.y + b.height)
1528
- }));
1529
- const rightUncovered = computeUncoveredSegments({
1530
- lineStart: bounds.y + \u03B4,
1531
- lineEnd: bounds.y + bounds.height - \u03B4,
1532
- coveringIntervals: rightCovering,
1533
- minSegmentLength: minSize * 0.5
1534
- });
1535
- for (const seg of rightUncovered) {
1536
- const segLen = seg.end - seg.start;
1537
- if (segLen >= minSize) {
1538
- pushIfFree({ x: rightX, y: seg.center, z });
1539
- if (segLen > minSize * 1.5) {
1540
- pushIfFree({ x: rightX, y: seg.start + minSize * 0.4, z });
1541
- pushIfFree({ x: rightX, y: seg.end - minSize * 0.4, z });
1542
- }
1543
- }
1544
- }
1545
- for (const b of blockers) {
1546
- const obLeftX = b.x - \u03B4;
1547
- if (obLeftX > bounds.x + EPS4 && obLeftX < bounds.x + bounds.width - EPS4) {
1548
- const obLeftCovering = blockers.filter(
1549
- (bl) => bl !== b && bl.x <= obLeftX && bl.x + bl.width >= obLeftX
1550
- ).map((bl) => ({
1551
- start: Math.max(b.y, bl.y),
1552
- end: Math.min(b.y + b.height, bl.y + bl.height)
1553
- }));
1554
- const obLeftUncovered = computeUncoveredSegments({
1555
- lineStart: b.y,
1556
- lineEnd: b.y + b.height,
1557
- coveringIntervals: obLeftCovering,
1558
- minSegmentLength: minSize * 0.5
1559
- });
1560
- for (const seg of obLeftUncovered) {
1561
- pushIfFree({ x: obLeftX, y: seg.center, z });
1562
- }
1563
- }
1564
- const obRightX = b.x + b.width + \u03B4;
1565
- if (obRightX > bounds.x + EPS4 && obRightX < bounds.x + bounds.width - EPS4) {
1566
- const obRightCovering = blockers.filter(
1567
- (bl) => bl !== b && bl.x <= obRightX && bl.x + bl.width >= obRightX
1568
- ).map((bl) => ({
1569
- start: Math.max(b.y, bl.y),
1570
- end: Math.min(b.y + b.height, bl.y + bl.height)
1571
- }));
1572
- const obRightUncovered = computeUncoveredSegments({
1573
- lineStart: b.y,
1574
- lineEnd: b.y + b.height,
1575
- coveringIntervals: obRightCovering,
1576
- minSegmentLength: minSize * 0.5
1577
- });
1578
- for (const seg of obRightUncovered) {
1579
- pushIfFree({ x: obRightX, y: seg.center, z });
1580
- }
1581
- }
1582
- const obTopY = b.y - \u03B4;
1583
- if (obTopY > bounds.y + EPS4 && obTopY < bounds.y + bounds.height - EPS4) {
1584
- const obTopCovering = blockers.filter(
1585
- (bl) => bl !== b && bl.y <= obTopY && bl.y + bl.height >= obTopY
1586
- ).map((bl) => ({
1587
- start: Math.max(b.x, bl.x),
1588
- end: Math.min(b.x + b.width, bl.x + bl.width)
1589
- }));
1590
- const obTopUncovered = computeUncoveredSegments({
1591
- lineStart: b.x,
1592
- lineEnd: b.x + b.width,
1593
- coveringIntervals: obTopCovering,
1594
- minSegmentLength: minSize * 0.5
1595
- });
1596
- for (const seg of obTopUncovered) {
1597
- pushIfFree({ x: seg.center, y: obTopY, z });
1598
- }
1599
- }
1600
- const obBottomY = b.y + b.height + \u03B4;
1601
- if (obBottomY > bounds.y + EPS4 && obBottomY < bounds.y + bounds.height - EPS4) {
1602
- const obBottomCovering = blockers.filter(
1603
- (bl) => bl !== b && bl.y <= obBottomY && bl.y + bl.height >= obBottomY
1604
- ).map((bl) => ({
1605
- start: Math.max(b.x, bl.x),
1606
- end: Math.min(b.x + b.width, bl.x + bl.width)
1607
- }));
1608
- const obBottomUncovered = computeUncoveredSegments({
1609
- lineStart: b.x,
1610
- lineEnd: b.x + b.width,
1611
- coveringIntervals: obBottomCovering,
1612
- minSegmentLength: minSize * 0.5
1613
- });
1614
- for (const seg of obBottomUncovered) {
1615
- pushIfFree({ x: seg.center, y: obBottomY, z });
1616
- }
1617
- }
1618
- }
1619
- }
1620
- out.sort(
1621
- (a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance || a.z - b.z || a.x - b.x || a.y - b.y
1622
- );
1623
- return out;
1624
- }
1625
-
1626
- // lib/utils/buildHardPlacedByLayer.ts
1627
- function allLayerNode(params) {
1628
- const out = Array.from({ length: params.layerCount }, () => []);
1629
- for (const p of params.placed) {
1630
- if (p.zLayers.length >= params.layerCount) {
1631
- for (const z of p.zLayers) out[z].push(p.rect);
1632
- }
1633
- }
1634
- return out;
1635
- }
1636
-
1637
- // lib/utils/rectToTree.ts
1638
- var rectToTree = (rect, opts) => ({
1639
- ...rect,
1640
- minX: rect.x,
1641
- minY: rect.y,
1642
- maxX: rect.x + rect.width,
1643
- maxY: rect.y + rect.height,
1644
- zLayers: opts.zLayers
1645
- });
1646
-
1647
- // lib/utils/resizeSoftOverlaps.ts
1648
- function resizeSoftOverlaps(params, newIndex) {
1649
- const newcomer = params.placed[newIndex];
1650
- const { rect: newR, zLayers: newZs } = newcomer;
1651
- const layerCount = params.layerCount;
1652
- const removeIdx = [];
1653
- const toAdd = [];
1654
- for (let i = 0; i < params.placed.length; i++) {
1655
- if (i === newIndex) continue;
1656
- const old = params.placed[i];
1657
- if (old.zLayers.length >= layerCount) continue;
1658
- const sharedZ = old.zLayers.filter((z) => newZs.includes(z));
1659
- if (sharedZ.length === 0) continue;
1660
- if (!overlaps(old.rect, newR)) continue;
1661
- const parts = subtractRect2D(old.rect, newR);
1662
- removeIdx.push(i);
1663
- const unaffectedZ = old.zLayers.filter((z) => !newZs.includes(z));
1664
- if (unaffectedZ.length > 0) {
1665
- toAdd.push({ rect: old.rect, zLayers: unaffectedZ });
1666
- }
1667
- const minW = Math.min(
1668
- params.options.minSingle.width,
1669
- params.options.minMulti.width
1670
- );
1671
- const minH = Math.min(
1672
- params.options.minSingle.height,
1673
- params.options.minMulti.height
1674
- );
1675
- for (const p of parts) {
1676
- if (p.width + EPS4 >= minW && p.height + EPS4 >= minH) {
1677
- toAdd.push({ rect: p, zLayers: sharedZ.slice() });
1678
- }
1679
- }
1680
- }
1681
- const sameRect = (a, b) => a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
1682
- removeIdx.sort((a, b) => b - a).forEach((idx) => {
1683
- const rem = params.placed.splice(idx, 1)[0];
1684
- if (params.placedIndexByLayer) {
1685
- for (const z of rem.zLayers) {
1686
- const tree = params.placedIndexByLayer[z];
1687
- if (tree)
1688
- tree.remove(
1689
- rectToTree(rem.rect, { zLayers: rem.zLayers }),
1690
- sameRect
1691
- );
1692
- }
1693
- }
1694
- });
1695
- for (const p of toAdd) {
1696
- params.placed.push(p);
1697
- for (const z of p.zLayers) {
1698
- if (params.placedIndexByLayer) {
1699
- const idx = params.placedIndexByLayer[z];
1700
- if (idx) {
1701
- idx.insert(rectToTree(p.rect, { zLayers: p.zLayers.slice() }));
1702
- }
1703
- }
1704
- }
1705
- }
1706
- }
1707
-
1708
- // lib/utils/getColorForZLayer.ts
1709
- var getColorForZLayer = (zLayers) => {
1710
- const minZ = Math.min(...zLayers);
1711
- const colors = [
1712
- { fill: "#dbeafe", stroke: "#3b82f6" },
1713
- { fill: "#fef3c7", stroke: "#f59e0b" },
1714
- { fill: "#d1fae5", stroke: "#10b981" },
1715
- { fill: "#e9d5ff", stroke: "#a855f7" },
1716
- { fill: "#fed7aa", stroke: "#f97316" },
1717
- { fill: "#fecaca", stroke: "#ef4444" }
1718
- ];
1719
- return colors[minZ % colors.length];
1720
- };
1721
-
1722
- // lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts
1723
- import RBush3 from "rbush";
1724
- var RectDiffSeedingSolver = class extends BaseSolver3 {
1725
- constructor(input) {
1726
- super();
1727
- this.input = input;
1728
- }
1729
- input;
1730
- // Engine fields (mirrors initState / engine.ts)
1731
- srj;
1732
- layerNames;
1733
- layerCount;
1734
- bounds;
1735
- options;
1736
- boardVoidRects;
1737
- gridIndex;
1738
- candidates;
1739
- placed;
1740
- placedIndexByLayer;
1741
- hardPlacedByLayer;
1742
- expansionIndex;
1743
- edgeAnalysisDone;
1744
- totalSeedsThisGrid;
1745
- consumedSeedsThisGrid;
1746
- _setup() {
1747
- const srj = this.input.simpleRouteJson;
1748
- const opts = this.input.gridOptions ?? {};
1749
- const precomputed = this.input.layerNames && this.input.zIndexByName;
1750
- const { layerNames, zIndexByName } = precomputed ? {
1751
- layerNames: this.input.layerNames,
1752
- zIndexByName: this.input.zIndexByName
1753
- } : buildZIndexMap({
1754
- obstacles: srj.obstacles,
1755
- layerCount: srj.layerCount
1756
- });
1757
- const layerCount = Math.max(1, layerNames.length, srj.layerCount || 1);
1758
- const bounds = {
1759
- x: srj.bounds.minX,
1760
- y: srj.bounds.minY,
1761
- width: srj.bounds.maxX - srj.bounds.minX,
1762
- height: srj.bounds.maxY - srj.bounds.minY
1763
- };
1764
- const trace = Math.max(0.01, srj.minTraceWidth || 0.15);
1765
- const defaults = {
1766
- gridSizes: [],
1767
- initialCellRatio: 0.2,
1768
- maxAspectRatio: 3,
1769
- minSingle: { width: 2 * trace, height: 2 * trace },
1770
- minMulti: {
1771
- width: 4 * trace,
1772
- height: 4 * trace,
1773
- minLayers: Math.min(2, Math.max(1, srj.layerCount || 1))
1774
- },
1775
- preferMultiLayer: true,
1776
- maxMultiLayerSpan: void 0
1777
- };
1778
- const options = {
1779
- ...defaults,
1780
- ...opts,
1781
- gridSizes: opts.gridSizes ?? // re-use the helper that was previously in engine
1782
- computeDefaultGridSizes(bounds)
1783
- };
1784
- this.srj = srj;
1785
- this.layerNames = layerNames;
1786
- this.layerCount = layerCount;
1787
- this.bounds = bounds;
1788
- this.options = options;
1789
- this.boardVoidRects = this.input.boardVoidRects;
1790
- this.gridIndex = 0;
1791
- this.candidates = [];
1792
- this.placed = [];
1793
- this.placedIndexByLayer = Array.from(
1794
- { length: layerCount },
1795
- () => new RBush3()
1796
- );
1797
- this.hardPlacedByLayer = Array.from({ length: layerCount }, () => []);
1798
- this.expansionIndex = 0;
1799
- this.edgeAnalysisDone = false;
1800
- this.totalSeedsThisGrid = 0;
1801
- this.consumedSeedsThisGrid = 0;
1802
- this.stats = {
1803
- gridIndex: this.gridIndex
1804
- };
1805
- }
1806
- /** Exactly ONE grid candidate step per call. */
1807
- _step() {
1808
- this._stepGrid();
1809
- this.stats.gridIndex = this.gridIndex;
1810
- this.stats.placed = this.placed.length;
1811
- }
1812
- /**
1813
- * One micro-step during the GRID phase: handle exactly one candidate.
1814
- */
1815
- _stepGrid() {
1816
- const {
1817
- gridSizes,
1818
- initialCellRatio,
1819
- maxAspectRatio,
1820
- minSingle,
1821
- minMulti,
1822
- preferMultiLayer,
1823
- maxMultiLayerSpan
1824
- } = this.options;
1825
- const grid = gridSizes[this.gridIndex];
1826
- if (this.candidates.length === 0 && this.consumedSeedsThisGrid === 0) {
1827
- this.candidates = computeCandidates3D({
1828
- bounds: this.bounds,
1829
- gridSize: grid,
1830
- layerCount: this.layerCount,
1831
- hardPlacedByLayer: this.hardPlacedByLayer,
1832
- obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1833
- placedIndexByLayer: this.placedIndexByLayer
1834
- });
1835
- this.totalSeedsThisGrid = this.candidates.length;
1836
- this.consumedSeedsThisGrid = 0;
1837
- }
1838
- if (this.consumedSeedsThisGrid >= this.candidates.length) {
1839
- if (this.gridIndex + 1 < gridSizes.length) {
1840
- this.gridIndex += 1;
1841
- this.candidates = [];
1842
- this.totalSeedsThisGrid = 0;
1843
- this.consumedSeedsThisGrid = 0;
1844
- return;
1845
- } else {
1846
- if (!this.edgeAnalysisDone) {
1847
- const minSize = Math.min(minSingle.width, minSingle.height);
1848
- this.candidates = computeEdgeCandidates3D({
1849
- bounds: this.bounds,
1850
- minSize,
1851
- layerCount: this.layerCount,
1852
- obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1853
- placedIndexByLayer: this.placedIndexByLayer,
1854
- hardPlacedByLayer: this.hardPlacedByLayer
1855
- });
1856
- this.edgeAnalysisDone = true;
1857
- this.totalSeedsThisGrid = this.candidates.length;
1858
- this.consumedSeedsThisGrid = 0;
1859
- return;
1860
- }
1861
- this.candidates = [];
1862
- this.solved = true;
1863
- this.expansionIndex = 0;
1864
- return;
1865
- }
1866
- }
1867
- const cand = this.candidates[this.consumedSeedsThisGrid++];
1868
- if (isFullyOccupiedAtPoint({
1869
- layerCount: this.layerCount,
1870
- obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1871
- placedIndexByLayer: this.placedIndexByLayer,
1872
- point: { x: cand.x, y: cand.y }
1873
- })) {
1874
- return;
1875
- }
1876
- const span = longestFreeSpanAroundZ({
1877
- x: cand.x,
1878
- y: cand.y,
1879
- z: cand.z,
1880
- layerCount: this.layerCount,
1881
- minSpan: minMulti.minLayers,
1882
- maxSpan: maxMultiLayerSpan,
1883
- obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1884
- additionalBlockersByLayer: this.hardPlacedByLayer
1885
- });
1886
- const attempts = [];
1887
- if (span.length >= minMulti.minLayers) {
1888
- attempts.push({
1889
- kind: "multi",
1890
- layers: span,
1891
- minReq: { width: minMulti.width, height: minMulti.height }
1892
- });
1893
- }
1894
- attempts.push({
1895
- kind: "single",
1896
- layers: [cand.z],
1897
- minReq: { width: minSingle.width, height: minSingle.height }
1898
- });
1899
- const ordered = preferMultiLayer ? attempts : attempts.reverse();
1900
- for (const attempt of ordered) {
1901
- const rect = expandRectFromSeed({
1902
- startX: cand.x,
1903
- startY: cand.y,
1904
- gridSize: grid,
1905
- bounds: this.bounds,
1906
- obsticalIndexByLayer: this.input.obstacleIndexByLayer,
1907
- placedIndexByLayer: this.placedIndexByLayer,
1908
- initialCellRatio,
1909
- maxAspectRatio,
1910
- minReq: attempt.minReq,
1911
- zLayers: attempt.layers
1912
- });
1913
- if (!rect) continue;
1914
- const placed = { rect, zLayers: [...attempt.layers] };
1915
- const newIndex = this.placed.push(placed) - 1;
1916
- for (const z of attempt.layers) {
1917
- const idx = this.placedIndexByLayer[z];
1918
- if (idx) {
1919
- idx.insert(rectToTree(rect, { zLayers: placed.zLayers }));
1920
- }
1921
- }
1922
- resizeSoftOverlaps(
1923
- {
1924
- layerCount: this.layerCount,
1925
- placed: this.placed,
1926
- options: this.options,
1927
- placedIndexByLayer: this.placedIndexByLayer
1928
- },
1929
- newIndex
1930
- );
1931
- this.hardPlacedByLayer = allLayerNode({
1932
- layerCount: this.layerCount,
1933
- placed: this.placed
1934
- });
1935
- return;
1936
- }
1937
- }
1938
- /** Compute solver progress (0 to 1) during GRID phase. */
1939
- computeProgress() {
1940
- if (this.solved) {
1941
- return 1;
1942
- }
1943
- const grids = this.options.gridSizes.length;
1944
- const g = this.gridIndex;
1945
- const base = g / (grids + 1);
1946
- const denom = Math.max(1, this.totalSeedsThisGrid);
1947
- const frac = denom ? this.consumedSeedsThisGrid / denom : 1;
1948
- return Math.min(0.999, base + frac * (1 / (grids + 1)));
1949
- }
1950
- /**
1951
- * Output the intermediate RectDiff engine data to feed into the
1952
- * expansion phase solver.
1953
- */
1954
- getOutput() {
1955
- return {
1956
- layerNames: this.layerNames,
1957
- layerCount: this.layerCount,
1958
- bounds: this.bounds,
1959
- options: this.options,
1960
- boardVoidRects: this.boardVoidRects,
1961
- gridIndex: this.gridIndex,
1962
- candidates: this.candidates,
1963
- placed: this.placed,
1964
- expansionIndex: this.expansionIndex,
1965
- edgeAnalysisDone: this.edgeAnalysisDone,
1966
- totalSeedsThisGrid: this.totalSeedsThisGrid,
1967
- consumedSeedsThisGrid: this.consumedSeedsThisGrid,
1968
- obstacles: this.srj.obstacles,
1969
- obstacleClearance: this.input.obstacleClearance
1970
- };
1971
- }
1972
- /** Visualization focused on the grid seeding phase. */
1973
- visualize() {
1974
- const rects = [];
1975
- const points = [];
1976
- const lines = [];
1977
- const srj = this.srj ?? this.input.simpleRouteJson;
1978
- const boardBounds = {
1979
- minX: srj.bounds.minX,
1980
- maxX: srj.bounds.maxX,
1981
- minY: srj.bounds.minY,
1982
- maxY: srj.bounds.maxY
1983
- };
1984
- if (srj.outline && srj.outline.length > 1) {
1985
- lines.push({
1986
- points: [...srj.outline, srj.outline[0]],
1987
- strokeColor: "#111827",
1988
- strokeWidth: 0.01,
1989
- label: "outline"
1990
- });
1991
- } else {
1992
- rects.push({
1993
- center: {
1994
- x: (boardBounds.minX + boardBounds.maxX) / 2,
1995
- y: (boardBounds.minY + boardBounds.maxY) / 2
1996
- },
1997
- width: boardBounds.maxX - boardBounds.minX,
1998
- height: boardBounds.maxY - boardBounds.minY,
1999
- fill: "none",
2000
- stroke: "#111827",
2001
- label: "board"
2002
- });
2003
- }
2004
- for (const obstacle of srj.obstacles ?? []) {
2005
- if (obstacle.type === "rect" || obstacle.type === "oval") {
2006
- rects.push({
2007
- center: { x: obstacle.center.x, y: obstacle.center.y },
2008
- width: obstacle.width,
2009
- height: obstacle.height,
2010
- fill: "#fee2e2",
2011
- stroke: "#ef4444",
2012
- layer: "obstacle",
2013
- label: "obstacle"
2014
- });
2015
- }
2016
- }
2017
- if (this.boardVoidRects) {
2018
- let outlineBBox = null;
2019
- if (srj.outline && srj.outline.length > 0) {
2020
- const xs = srj.outline.map((p) => p.x);
2021
- const ys = srj.outline.map((p) => p.y);
2022
- const minX = Math.min(...xs);
2023
- const minY = Math.min(...ys);
2024
- outlineBBox = {
2025
- x: minX,
2026
- y: minY,
2027
- width: Math.max(...xs) - minX,
2028
- height: Math.max(...ys) - minY
2029
- };
2030
- }
2031
- for (const r of this.boardVoidRects) {
2032
- if (outlineBBox && !overlaps(r, outlineBBox)) {
2033
- continue;
2034
- }
2035
- rects.push({
2036
- center: { x: r.x + r.width / 2, y: r.y + r.height / 2 },
2037
- width: r.width,
2038
- height: r.height,
2039
- fill: "rgba(0, 0, 0, 0.5)",
2040
- stroke: "none",
2041
- label: "void"
2042
- });
2043
- }
2044
- }
2045
- if (this.candidates?.length) {
2046
- for (const cand of this.candidates) {
2047
- points.push({
2048
- x: cand.x,
2049
- y: cand.y,
2050
- label: `z:${cand.z}`
2051
- });
2052
- }
2053
- }
2054
- if (this.placed?.length) {
2055
- for (const placement of this.placed) {
2056
- const colors = getColorForZLayer(placement.zLayers);
2057
- rects.push({
2058
- center: {
2059
- x: placement.rect.x + placement.rect.width / 2,
2060
- y: placement.rect.y + placement.rect.height / 2
2061
- },
2062
- width: placement.rect.width,
2063
- height: placement.rect.height,
2064
- fill: colors.fill,
2065
- stroke: colors.stroke,
2066
- layer: `z${placement.zLayers.join(",")}`,
2067
- label: `free
2068
- z:${placement.zLayers.join(",")}`
2069
- });
2070
- }
2071
- }
2072
- return {
2073
- title: "RectDiff Grid",
2074
- coordinateSystem: "cartesian",
2075
- rects,
2076
- points,
2077
- lines
2078
- };
2079
- }
2080
- };
2081
-
2082
- // lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts
2083
- import { BaseSolver as BaseSolver4 } from "@tscircuit/solver-utils";
2084
-
2085
- // lib/utils/finalizeRects.ts
2086
- function finalizeRects(params) {
2087
- const out = params.placed.map((p) => ({
2088
- minX: p.rect.x,
2089
- minY: p.rect.y,
2090
- maxX: p.rect.x + p.rect.width,
2091
- maxY: p.rect.y + p.rect.height,
2092
- zLayers: [...p.zLayers].sort((a, b) => a - b)
2093
- }));
2094
- const layersByKey = /* @__PURE__ */ new Map();
2095
- for (const obstacle of params.obstacles ?? []) {
2096
- const baseRect = obstacleToXYRect(obstacle);
2097
- if (!baseRect) continue;
2098
- const rect = params.obstacleClearance ? {
2099
- x: baseRect.x - params.obstacleClearance,
2100
- y: baseRect.y - params.obstacleClearance,
2101
- width: baseRect.width + 2 * params.obstacleClearance,
2102
- height: baseRect.height + 2 * params.obstacleClearance
2103
- } : baseRect;
2104
- const zLayers = obstacle.zLayers?.length && obstacle.zLayers.length > 0 ? obstacle.zLayers : obstacleZs(obstacle, params.zIndexByName);
2105
- const key = `${rect.x}:${rect.y}:${rect.width}:${rect.height}`;
2106
- let entry = layersByKey.get(key);
2107
- if (!entry) {
2108
- entry = { rect, layers: /* @__PURE__ */ new Set() };
2109
- layersByKey.set(key, entry);
2110
- }
2111
- zLayers.forEach((layer) => entry.layers.add(layer));
2112
- }
2113
- for (const { rect, layers } of layersByKey.values()) {
2114
- out.push({
2115
- minX: rect.x,
2116
- minY: rect.y,
2117
- maxX: rect.x + rect.width,
2118
- maxY: rect.y + rect.height,
2119
- zLayers: Array.from(layers).sort((a, b) => a - b),
2120
- isObstacle: true
2121
- });
2122
- }
2123
- return out;
2124
- }
2125
-
2126
- // lib/solvers/RectDiffExpansionSolver/rectsToMeshNodes.ts
2127
- function rectsToMeshNodes(rects) {
2128
- let id = 0;
2129
- const out = [];
2130
- for (const r of rects) {
2131
- const w = Math.max(0, r.maxX - r.minX);
2132
- const h = Math.max(0, r.maxY - r.minY);
2133
- if (w <= 0 || h <= 0 || r.zLayers.length === 0) continue;
2134
- out.push({
2135
- capacityMeshNodeId: `cmn_${id++}`,
2136
- center: { x: (r.minX + r.maxX) / 2, y: (r.minY + r.maxY) / 2 },
2137
- width: w,
2138
- height: h,
2139
- layer: "top",
2140
- availableZ: r.zLayers.slice(),
2141
- _containsObstacle: r.isObstacle,
2142
- _containsTarget: r.isObstacle
2143
- });
2144
- }
2145
- return out;
2146
- }
2147
-
2148
- // lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts
2149
- import RBush4 from "rbush";
2150
-
2151
- // lib/utils/sameTreeRect.ts
2152
- var sameTreeRect = (a, b) => a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
2153
-
2154
- // lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts
2155
- var RectDiffExpansionSolver = class extends BaseSolver4 {
2156
- constructor(input) {
2157
- super();
2158
- this.input = input;
2159
- }
2160
- input;
2161
- placedIndexByLayer = [];
2162
- _meshNodes = [];
2163
- _setup() {
2164
- this.stats = {
2165
- gridIndex: this.input.gridIndex
2166
- };
2167
- this.placedIndexByLayer = Array.from(
2168
- { length: this.input.layerCount },
2169
- () => new RBush4()
2170
- );
2171
- for (const placement of this.input.placed) {
2172
- for (const z of placement.zLayers) {
2173
- const placedIndex = this.placedIndexByLayer[z];
2174
- if (placedIndex)
2175
- placedIndex.insert(
2176
- rectToTree(placement.rect, { zLayers: placement.zLayers })
2177
- );
2178
- }
2179
- }
2180
- }
2181
- _step() {
2182
- if (this.solved) return;
2183
- this._stepExpansion();
2184
- this.stats.gridIndex = this.input.gridIndex;
2185
- this.stats.placed = this.input.placed.length;
2186
- if (this.input.expansionIndex >= this.input.placed.length) {
2187
- this.finalizeIfNeeded();
2188
- }
2189
- }
2190
- _stepExpansion() {
2191
- if (this.input.expansionIndex >= this.input.placed.length) {
2192
- return;
2193
- }
2194
- const idx = this.input.expansionIndex;
2195
- const p = this.input.placed[idx];
2196
- const lastGrid = this.input.options.gridSizes[this.input.options.gridSizes.length - 1];
2197
- const oldRect = p.rect;
2198
- const expanded = expandRectFromSeed({
2199
- startX: p.rect.x + p.rect.width / 2,
2200
- startY: p.rect.y + p.rect.height / 2,
2201
- gridSize: lastGrid,
2202
- bounds: this.input.bounds,
2203
- obsticalIndexByLayer: this.input.obstacleIndexByLayer,
2204
- placedIndexByLayer: this.placedIndexByLayer,
2205
- initialCellRatio: 0,
2206
- maxAspectRatio: null,
2207
- minReq: { width: p.rect.width, height: p.rect.height },
2208
- zLayers: p.zLayers
2209
- });
2210
- if (expanded) {
2211
- this.input.placed[idx] = { rect: expanded, zLayers: p.zLayers };
2212
- for (const z of p.zLayers) {
2213
- const tree = this.placedIndexByLayer[z];
2214
- if (tree) {
2215
- tree.remove(rectToTree(oldRect, { zLayers: p.zLayers }), sameTreeRect);
2216
- tree.insert(rectToTree(expanded, { zLayers: p.zLayers }));
2217
- }
2218
- }
2219
- resizeSoftOverlaps(
2220
- {
2221
- layerCount: this.input.layerCount,
2222
- placed: this.input.placed,
2223
- options: this.input.options,
2224
- placedIndexByLayer: this.placedIndexByLayer
2225
- },
2226
- idx
2227
- );
2228
- }
2229
- this.input.expansionIndex += 1;
2230
- }
2231
- finalizeIfNeeded() {
2232
- if (this.solved) return;
2233
- const rects = finalizeRects({
2234
- placed: this.input.placed,
2235
- obstacles: this.input.obstacles,
2236
- zIndexByName: this.input.zIndexByName,
2237
- boardVoidRects: this.input.boardVoidRects,
2238
- obstacleClearance: this.input.obstacleClearance
2239
- });
2240
- this._meshNodes = rectsToMeshNodes(rects);
2241
- this.solved = true;
2242
- }
2243
- computeProgress() {
2244
- if (this.solved) return 1;
2245
- const grids = this.input.options.gridSizes.length;
2246
- const base = grids / (grids + 1);
2247
- const denom = Math.max(1, this.input.placed.length);
2248
- const frac = denom ? this.input.expansionIndex / denom : 1;
2249
- return Math.min(0.999, base + frac * (1 / (grids + 1)));
2250
- }
2251
- getOutput() {
2252
- if (this.solved) return { meshNodes: this._meshNodes };
2253
- const previewNodes = this.input.placed.map(
2254
- (placement, idx) => ({
2255
- capacityMeshNodeId: `expand-preview-${idx}`,
2256
- center: {
2257
- x: placement.rect.x + placement.rect.width / 2,
2258
- y: placement.rect.y + placement.rect.height / 2
2259
- },
2260
- width: placement.rect.width,
2261
- height: placement.rect.height,
2262
- availableZ: placement.zLayers.slice(),
2263
- layer: `z${placement.zLayers.join(",")}`
2264
- })
2265
- );
2266
- return { meshNodes: previewNodes };
2267
- }
2268
- /** Simple visualization of expanded placements. */
2269
- visualize() {
2270
- const rects = [];
2271
- for (const placement of this.input.placed ?? []) {
2272
- rects.push({
2273
- center: {
2274
- x: placement.rect.x + placement.rect.width / 2,
2275
- y: placement.rect.y + placement.rect.height / 2
2276
- },
2277
- width: placement.rect.width,
2278
- height: placement.rect.height,
2279
- stroke: "rgba(37, 99, 235, 0.9)",
2280
- fill: "rgba(191, 219, 254, 0.5)",
2281
- layer: `z${placement.zLayers.join(",")}`,
2282
- label: `expanded
2283
- z:${placement.zLayers.join(",")}`
2284
- });
2285
- }
2286
- return {
2287
- title: "RectDiff Expansion",
2288
- coordinateSystem: "cartesian",
2289
- rects,
2290
- points: [],
2291
- lines: []
2292
- };
2293
- }
2294
- };
2295
-
2296
- // lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts
2297
- import "rbush";
2298
-
2299
- // lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts
2300
- import RBush5 from "rbush";
2301
-
2302
- // lib/utils/padRect.ts
2303
- var padRect = (rect, clearance) => {
2304
- if (!clearance || clearance <= 0) return rect;
2305
- return {
2306
- x: rect.x - clearance,
2307
- y: rect.y - clearance,
2308
- width: rect.width + 2 * clearance,
2309
- height: rect.height + 2 * clearance
2310
- };
2311
- };
2312
-
2313
- // lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts
2314
- var buildObstacleIndexesByLayer = (params) => {
2315
- const { srj, boardVoidRects, obstacleClearance } = params;
2316
- const { layerNames, zIndexByName } = buildZIndexMap({
2317
- obstacles: srj.obstacles,
2318
- layerCount: srj.layerCount
2319
- });
2320
- const layerCount = Math.max(1, layerNames.length, srj.layerCount || 1);
2321
- const bounds = {
2322
- x: srj.bounds.minX,
2323
- y: srj.bounds.minY,
2324
- width: srj.bounds.maxX - srj.bounds.minX,
2325
- height: srj.bounds.maxY - srj.bounds.minY
2326
- };
2327
- const obstacleIndexByLayer = Array.from(
2328
- { length: layerCount },
2329
- () => new RBush5()
2330
- );
2331
- const insertObstacle = (rect, z) => {
2332
- const treeRect = {
2333
- ...rect,
2334
- minX: rect.x,
2335
- minY: rect.y,
2336
- maxX: rect.x + rect.width,
2337
- maxY: rect.y + rect.height,
2338
- zLayers: [z]
2339
- };
2340
- obstacleIndexByLayer[z]?.insert(treeRect);
2341
- };
2342
- if (srj.outline && srj.outline.length > 2) {
2343
- for (const voidRect of boardVoidRects ?? []) {
2344
- for (let z = 0; z < layerCount; z++) insertObstacle(voidRect, z);
2345
- }
2346
- }
2347
- for (const obstacle of srj.obstacles ?? []) {
2348
- const rectBase = obstacleToXYRect(obstacle);
2349
- if (!rectBase) continue;
2350
- const rect = padRect(rectBase, obstacleClearance ?? 0);
2351
- const zLayers = obstacleZs(obstacle, zIndexByName);
2352
- const invalidZs = zLayers.filter((z) => z < 0 || z >= layerCount);
2353
- if (invalidZs.length) {
2354
- throw new Error(
2355
- `RectDiff: obstacle uses z-layer indices ${invalidZs.join(",")} outside 0-${layerCount - 1}`
2356
- );
2357
- }
2358
- if ((!obstacle.zLayers || obstacle.zLayers.length === 0) && zLayers.length) {
2359
- obstacle.zLayers = zLayers;
2360
- }
2361
- for (const z of zLayers) insertObstacle(rect, z);
2362
- }
2363
- return { obstacleIndexByLayer, layerNames, zIndexByName };
2364
- };
2365
-
2366
- // lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts
2367
- var RectDiffGridSolverPipeline = class extends BasePipelineSolver2 {
2368
- rectDiffSeedingSolver;
2369
- rectDiffExpansionSolver;
2370
- obstacleIndexByLayer;
2371
- layerNames;
2372
- zIndexByName;
2373
- constructor(inputProblem) {
2374
- super(inputProblem);
2375
- const { obstacleIndexByLayer, layerNames, zIndexByName } = buildObstacleIndexesByLayer({
2376
- srj: {
2377
- bounds: inputProblem.bounds,
2378
- obstacles: inputProblem.obstacles,
2379
- connections: inputProblem.connections,
2380
- outline: inputProblem.outline?.outline,
2381
- layerCount: inputProblem.layerCount,
2382
- minTraceWidth: inputProblem.minTraceWidth
2383
- },
2384
- boardVoidRects: inputProblem.boardVoidRects,
2385
- obstacleClearance: inputProblem.obstacleClearance
2386
- });
2387
- this.obstacleIndexByLayer = obstacleIndexByLayer;
2388
- this.layerNames = inputProblem.layerNames ?? layerNames;
2389
- this.zIndexByName = inputProblem.zIndexByName ?? zIndexByName;
2390
- }
2391
- pipelineDef = [
2392
- definePipelineStep2(
2393
- "rectDiffSeedingSolver",
2394
- RectDiffSeedingSolver,
2395
- (pipeline) => [
2396
- {
2397
- simpleRouteJson: {
2398
- bounds: pipeline.inputProblem.bounds,
2399
- obstacles: pipeline.inputProblem.obstacles,
2400
- connections: pipeline.inputProblem.connections,
2401
- outline: pipeline.inputProblem.outline?.outline,
2402
- layerCount: pipeline.inputProblem.layerCount,
2403
- minTraceWidth: pipeline.inputProblem.minTraceWidth
2404
- },
2405
- gridOptions: pipeline.inputProblem.gridOptions,
2406
- obstacleIndexByLayer: pipeline.obstacleIndexByLayer,
2407
- boardVoidRects: pipeline.inputProblem.boardVoidRects,
2408
- layerNames: pipeline.layerNames,
2409
- zIndexByName: pipeline.zIndexByName,
2410
- obstacleClearance: pipeline.inputProblem.obstacleClearance
2411
- }
2412
- ]
2413
- ),
2414
- definePipelineStep2(
2415
- "rectDiffExpansionSolver",
2416
- RectDiffExpansionSolver,
2417
- (pipeline) => {
2418
- const output = pipeline.rectDiffSeedingSolver?.getOutput();
2419
- if (!output) {
2420
- throw new Error("RectDiffSeedingSolver did not produce output");
2421
- }
2422
- return [
2423
- {
2424
- layerNames: output.layerNames ?? [],
2425
- boardVoidRects: pipeline.inputProblem.boardVoidRects ?? [],
2426
- layerCount: pipeline.inputProblem.layerCount,
2427
- bounds: output.bounds,
2428
- candidates: output.candidates,
2429
- consumedSeedsThisGrid: output.placed.length,
2430
- totalSeedsThisGrid: output.candidates.length,
2431
- placed: output.placed,
2432
- edgeAnalysisDone: output.edgeAnalysisDone,
2433
- gridIndex: output.gridIndex,
2434
- expansionIndex: output.expansionIndex,
2435
- obstacleIndexByLayer: pipeline.obstacleIndexByLayer,
2436
- options: output.options,
2437
- zIndexByName: pipeline.zIndexByName,
2438
- layerNamesCanonical: pipeline.layerNames,
2439
- obstacles: pipeline.inputProblem.obstacles,
2440
- obstacleClearance: pipeline.inputProblem.obstacleClearance
2441
- }
2442
- ];
2443
- }
2444
- )
2445
- ];
2446
- getConstructorParams() {
2447
- return [this.inputProblem];
2448
- }
2449
- getOutput() {
2450
- if (this.rectDiffExpansionSolver) {
2451
- return this.rectDiffExpansionSolver.getOutput();
2452
- }
2453
- if (this.rectDiffSeedingSolver) {
2454
- const snapshot = this.rectDiffSeedingSolver.getOutput();
2455
- const meshNodes = snapshot.placed.map(
2456
- (placement, idx) => ({
2457
- capacityMeshNodeId: `grid-${idx}`,
2458
- center: {
2459
- x: placement.rect.x + placement.rect.width / 2,
2460
- y: placement.rect.y + placement.rect.height / 2
2461
- },
2462
- width: placement.rect.width,
2463
- height: placement.rect.height,
2464
- availableZ: placement.zLayers,
2465
- layer: `z${placement.zLayers.join(",")}`
2466
- })
2467
- );
2468
- return { meshNodes };
2469
- }
2470
- return { meshNodes: [] };
2471
- }
2472
- visualize() {
2473
- if (this.rectDiffExpansionSolver) {
2474
- return this.rectDiffExpansionSolver.visualize();
2475
- }
2476
- if (this.rectDiffSeedingSolver) {
2477
- return this.rectDiffSeedingSolver.visualize();
2478
- }
2479
- return {
2480
- title: "RectDiff Grid Pipeline",
2481
- coordinateSystem: "cartesian",
2482
- rects: [],
2483
- points: [],
2484
- lines: []
2485
- };
2486
- }
2487
- };
2488
-
2489
- // lib/rectdiff-visualization.ts
2490
- function createBaseVisualization(srj, title = "RectDiff") {
2491
- const rects = [];
2492
- const lines = [];
2493
- const boardBounds = {
2494
- minX: srj.bounds.minX,
2495
- maxX: srj.bounds.maxX,
2496
- minY: srj.bounds.minY,
2497
- maxY: srj.bounds.maxY
2498
- };
2499
- if (srj.outline && srj.outline.length > 1) {
2500
- lines.push({
2501
- points: [...srj.outline, srj.outline[0]],
2502
- strokeColor: "#111827",
2503
- strokeWidth: 0.01,
2504
- label: "outline"
2505
- });
2506
- } else {
2507
- rects.push({
2508
- center: {
2509
- x: (boardBounds.minX + boardBounds.maxX) / 2,
2510
- y: (boardBounds.minY + boardBounds.maxY) / 2
2511
- },
2512
- width: boardBounds.maxX - boardBounds.minX,
2513
- height: boardBounds.maxY - boardBounds.minY,
2514
- fill: "none",
2515
- stroke: "#111827",
2516
- label: "board"
2517
- });
2518
- }
2519
- for (const obstacle of srj.obstacles ?? []) {
2520
- if (obstacle.type === "rect" || obstacle.type === "oval") {
2521
- const layerLabel = (obstacle.zLayers ?? []).join(",") || "all";
2522
- rects.push({
2523
- center: { x: obstacle.center.x, y: obstacle.center.y },
2524
- width: obstacle.width,
2525
- height: obstacle.height,
2526
- fill: "#fee2e2",
2527
- stroke: "#ef4444",
2528
- layer: "obstacle",
2529
- label: `obstacle
2530
- z:${layerLabel}`
2531
- });
2532
- }
2533
- }
2534
- return {
2535
- title,
2536
- coordinateSystem: "cartesian",
2537
- rects,
2538
- points: [],
2539
- lines
2540
- };
2541
- }
2542
-
2543
- // lib/buildFinalRectDiffVisualization.ts
2544
- import { mergeGraphics } from "graphics-debug";
2545
-
2546
- // lib/utils/buildOutlineGraphics.ts
2547
- var buildOutlineGraphics = ({
2548
- srj
2549
- }) => {
2550
- const hasOutline = srj.outline && srj.outline.length > 1;
2551
- const lines = hasOutline ? [
2552
- {
2553
- points: [...srj.outline, srj.outline[0]],
2554
- strokeColor: "#111827",
2555
- strokeWidth: 0.1,
2556
- label: "outline"
2557
- }
2558
- ] : [
2559
- {
2560
- points: [
2561
- { x: srj.bounds.minX, y: srj.bounds.minY },
2562
- { x: srj.bounds.maxX, y: srj.bounds.minY },
2563
- { x: srj.bounds.maxX, y: srj.bounds.maxY },
2564
- { x: srj.bounds.minX, y: srj.bounds.maxY },
2565
- { x: srj.bounds.minX, y: srj.bounds.minY }
2566
- ],
2567
- strokeColor: "#111827",
2568
- strokeWidth: 0.1,
2569
- label: "bounds"
2570
- }
2571
- ];
2572
- return {
2573
- title: "SimpleRoute Outline",
2574
- coordinateSystem: "cartesian",
2575
- lines
2576
- };
2577
- };
2578
-
2579
- // lib/utils/renderObstacleClearance.ts
2580
- var buildObstacleClearanceGraphics = (params) => {
2581
- const { srj, clearance } = params;
2582
- const c = clearance ?? 0;
2583
- if (c <= 0) {
2584
- return {
2585
- title: "Obstacle Clearance",
2586
- coordinateSystem: "cartesian",
2587
- rects: []
2588
- };
2589
- }
2590
- const rects = [];
2591
- for (const obstacle of srj.obstacles ?? []) {
2592
- if (obstacle.type !== "rect" && obstacle.type !== "oval") continue;
2593
- const expanded = {
2594
- x: obstacle.center.x - obstacle.width / 2 - c,
2595
- y: obstacle.center.y - obstacle.height / 2 - c,
2596
- width: obstacle.width + 2 * c,
2597
- height: obstacle.height + 2 * c
2598
- };
2599
- rects.push({
2600
- center: {
2601
- x: expanded.x + expanded.width / 2,
2602
- y: expanded.y + expanded.height / 2
2603
- },
2604
- width: expanded.width,
2605
- height: expanded.height,
2606
- stroke: "rgba(202, 138, 4, 0.9)",
2607
- fill: "rgba(234, 179, 8, 0.15)",
2608
- layer: "obstacle-clearance",
2609
- label: `clearance
2610
- z:${(obstacle.zLayers ?? []).join(",") || "all"}`
2611
- });
2612
- }
2613
- return {
2614
- title: "Obstacle Clearance",
2615
- coordinateSystem: "cartesian",
2616
- rects
2617
- };
2618
- };
2619
-
2620
- // lib/buildFinalRectDiffVisualization.ts
2621
- var buildFinalRectDiffVisualization = ({
2622
- srj,
2623
- meshNodes,
2624
- obstacleClearance
2625
- }) => {
2626
- const outline = buildOutlineGraphics({ srj });
2627
- const clearance = buildObstacleClearanceGraphics({
2628
- srj,
2629
- clearance: obstacleClearance
2630
- });
2631
- const rects = meshNodes.map((node) => ({
2632
- center: node.center,
2633
- width: node.width,
2634
- height: node.height,
2635
- stroke: getColorForZLayer(node.availableZ).stroke,
2636
- fill: node._containsObstacle ? "#fca5a5" : getColorForZLayer(node.availableZ).fill,
2637
- layer: `z${node.availableZ.join(",")}`,
2638
- label: `node ${node.capacityMeshNodeId}
2639
- z:${node.availableZ.join(",")}`
2640
- }));
2641
- const nodesGraphic = {
2642
- title: "RectDiffPipeline - Final",
2643
- coordinateSystem: "cartesian",
2644
- rects,
2645
- lines: [],
2646
- points: [],
2647
- texts: []
2648
- };
2649
- return mergeGraphics(mergeGraphics(nodesGraphic, outline), clearance);
2650
- };
2651
-
2652
- // lib/RectDiffPipeline.ts
2653
- import { mergeGraphics as mergeGraphics2 } from "graphics-debug";
2654
- var RectDiffPipeline = class extends BasePipelineSolver3 {
2655
- rectDiffGridSolverPipeline;
2656
- gapFillSolver;
2657
- boardVoidRects;
2658
- zIndexByName;
2659
- layerNames;
2660
- pipelineDef = [
2661
- definePipelineStep3(
2662
- "rectDiffGridSolverPipeline",
2663
- RectDiffGridSolverPipeline,
2664
- (rectDiffPipeline) => [
2665
- {
2666
- bounds: rectDiffPipeline.inputProblem.simpleRouteJson.bounds,
2667
- obstacles: rectDiffPipeline.inputProblem.simpleRouteJson.obstacles,
2668
- connections: rectDiffPipeline.inputProblem.simpleRouteJson.connections,
2669
- outline: rectDiffPipeline.inputProblem.simpleRouteJson.outline ? { outline: rectDiffPipeline.inputProblem.simpleRouteJson.outline } : void 0,
2670
- layerCount: rectDiffPipeline.inputProblem.simpleRouteJson.layerCount,
2671
- gridOptions: rectDiffPipeline.inputProblem.gridOptions,
2672
- boardVoidRects: rectDiffPipeline.boardVoidRects,
2673
- layerNames: rectDiffPipeline.layerNames,
2674
- zIndexByName: rectDiffPipeline.zIndexByName,
2675
- minTraceWidth: rectDiffPipeline.inputProblem.simpleRouteJson.minTraceWidth,
2676
- obstacleClearance: rectDiffPipeline.inputProblem.obstacleClearance
2677
- }
2678
- ]
2679
- ),
2680
- definePipelineStep3(
2681
- "gapFillSolver",
2682
- GapFillSolverPipeline,
2683
- (rectDiffPipeline) => [
2684
- {
2685
- meshNodes: rectDiffPipeline.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? [],
2686
- boardVoid: {
2687
- boardVoidRects: rectDiffPipeline.boardVoidRects || [],
2688
- layerCount: rectDiffPipeline.inputProblem.simpleRouteJson.layerCount || 0
2689
- }
2690
- }
2691
- ]
2692
- )
2693
- ];
2694
- _setup() {
2695
- const { zIndexByName, layerNames } = buildZIndexMap({
2696
- obstacles: this.inputProblem.simpleRouteJson.obstacles,
2697
- layerCount: this.inputProblem.simpleRouteJson.layerCount
2698
- });
2699
- this.zIndexByName = zIndexByName;
2700
- this.layerNames = layerNames;
2701
- if (this.inputProblem.simpleRouteJson.outline) {
2702
- this.boardVoidRects = computeInverseRects(
2703
- {
2704
- x: this.inputProblem.simpleRouteJson.bounds.minX,
2705
- y: this.inputProblem.simpleRouteJson.bounds.minY,
2706
- width: this.inputProblem.simpleRouteJson.bounds.maxX - this.inputProblem.simpleRouteJson.bounds.minX,
2707
- height: this.inputProblem.simpleRouteJson.bounds.maxY - this.inputProblem.simpleRouteJson.bounds.minY
2708
- },
2709
- this.inputProblem.simpleRouteJson.outline ?? []
2710
- );
2711
- }
2712
- }
2713
- getConstructorParams() {
2714
- return [this.inputProblem];
2715
- }
2716
- getOutput() {
2717
- const gapFillOutput = this.gapFillSolver?.getOutput();
2718
- if (gapFillOutput) {
2719
- return { meshNodes: gapFillOutput.outputNodes };
2720
- }
2721
- if (this.rectDiffGridSolverPipeline) {
2722
- return this.rectDiffGridSolverPipeline.getOutput();
2723
- }
2724
- return { meshNodes: [] };
2725
- }
2726
- initialVisualize() {
2727
- const base = createBaseVisualization(
2728
- this.inputProblem.simpleRouteJson,
2729
- "RectDiffPipeline - Initial"
2730
- );
2731
- const clearance = buildObstacleClearanceGraphics({
2732
- srj: this.inputProblem.simpleRouteJson,
2733
- clearance: this.inputProblem.obstacleClearance
2734
- });
2735
- const initialNodes = this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? [];
2736
- const nodeRects = {
2737
- title: "Initial Nodes",
2738
- coordinateSystem: "cartesian",
2739
- rects: initialNodes.map((node) => ({
2740
- center: node.center,
2741
- width: node.width,
2742
- height: node.height,
2743
- stroke: "rgba(0, 0, 0, 0.3)",
2744
- fill: "rgba(100, 100, 100, 0.1)",
2745
- layer: `z${node.availableZ.join(",")}`,
2746
- label: [
2747
- `node ${node.capacityMeshNodeId}`,
2748
- `z:${node.availableZ.join(",")}`
2749
- ].join("\n")
2750
- }))
2751
- };
2752
- return mergeGraphics2(mergeGraphics2(base, clearance), nodeRects);
2753
- }
2754
- finalVisualize() {
2755
- return buildFinalRectDiffVisualization({
2756
- srj: this.inputProblem.simpleRouteJson,
2757
- meshNodes: this.getOutput().meshNodes,
2758
- obstacleClearance: this.inputProblem.obstacleClearance
2759
- });
2760
- }
2761
- };
2762
- export {
2763
- RectDiffPipeline
2764
- };