@tscircuit/rectdiff 0.0.1 → 0.0.3
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.
- package/.github/workflows/bun-formatcheck.yml +1 -1
- package/.github/workflows/bun-pver-release.yml +2 -2
- package/.github/workflows/bun-test.yml +1 -1
- package/.github/workflows/bun-typecheck.yml +1 -1
- package/README.md +139 -0
- package/dist/index.js +240 -42
- package/global.d.ts +2 -2
- package/lib/solvers/RectDiffSolver.ts +15 -3
- package/lib/solvers/rectdiff/candidates.ts +158 -43
- package/lib/solvers/rectdiff/engine.ts +117 -29
- package/lib/solvers/rectdiff/geometry.ts +81 -28
- package/lib/solvers/rectdiff/layers.ts +44 -8
- package/lib/solvers/rectdiff/rectsToMeshNodes.ts +3 -0
- package/lib/solvers/rectdiff/types.ts +4 -1
- package/package.json +6 -2
- package/pages/bugreport11.page.tsx +16 -0
- package/pages/example01.page.tsx +1 -1
- package/test-assets/bugreport11-b2de3c.json +4315 -0
- package/tests/examples/example01.test.tsx +2 -2
- package/tests/fixtures/preload.ts +1 -1
- package/tests/obstacle-extra-layers.test.ts +37 -0
- package/tests/obstacle-zlayers.test.ts +37 -0
- package/tests/rect-diff-solver.test.ts +6 -5
- package/tests/svg.test.ts +10 -11
- package/.claude/settings.local.json +0 -9
- package/bun.lock +0 -29
|
@@ -21,13 +21,13 @@ jobs:
|
|
|
21
21
|
- name: Setup bun
|
|
22
22
|
uses: oven-sh/setup-bun@v2
|
|
23
23
|
with:
|
|
24
|
-
bun-version:
|
|
24
|
+
bun-version: 1.3.1
|
|
25
25
|
- uses: actions/setup-node@v3
|
|
26
26
|
with:
|
|
27
27
|
node-version: 20
|
|
28
28
|
registry-url: https://registry.npmjs.org/
|
|
29
29
|
- run: npm install -g pver
|
|
30
|
-
- run: bun install
|
|
30
|
+
- run: bun install
|
|
31
31
|
- run: bun run build
|
|
32
32
|
- run: pver release --no-push-main
|
|
33
33
|
env:
|
package/README.md
CHANGED
|
@@ -3,3 +3,142 @@
|
|
|
3
3
|
This is a 3D rectangle diffing algorithm made to quickly break apart a circuit board
|
|
4
4
|
into capacity nodes for the purpose of global routing.
|
|
5
5
|
|
|
6
|
+
[Online Demo](https://rectdiff.tscircuit.com/?fixture=%7B%22path%22%3A%22pages%2Fexample01.page.tsx%22%7D)
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
### Basic Usage
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { RectDiffSolver } from "@tscircuit/rectdiff"
|
|
14
|
+
import type { SimpleRouteJson } from "@tscircuit/rectdiff"
|
|
15
|
+
|
|
16
|
+
// Define your circuit board layout
|
|
17
|
+
const simpleRouteJson: SimpleRouteJson = {
|
|
18
|
+
bounds: {
|
|
19
|
+
minX: 0,
|
|
20
|
+
maxX: 10,
|
|
21
|
+
minY: 0,
|
|
22
|
+
maxY: 10,
|
|
23
|
+
},
|
|
24
|
+
obstacles: [
|
|
25
|
+
{
|
|
26
|
+
type: "rect",
|
|
27
|
+
layers: ["top"],
|
|
28
|
+
center: { x: 2.5, y: 2.5 },
|
|
29
|
+
width: 2,
|
|
30
|
+
height: 2,
|
|
31
|
+
connectedTo: [],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
connections: [],
|
|
35
|
+
layerCount: 2,
|
|
36
|
+
minTraceWidth: 0.15,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create and run the solver
|
|
40
|
+
const solver = new RectDiffSolver({
|
|
41
|
+
simpleRouteJson,
|
|
42
|
+
mode: "grid",
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
solver.solve()
|
|
46
|
+
|
|
47
|
+
// Get the capacity mesh nodes
|
|
48
|
+
const output = solver.getOutput()
|
|
49
|
+
console.log(`Generated ${output.meshNodes.length} capacity mesh nodes`)
|
|
50
|
+
|
|
51
|
+
// Each mesh node contains:
|
|
52
|
+
// - center: { x, y } - center point of the node
|
|
53
|
+
// - width, height - dimensions
|
|
54
|
+
// - availableZ - array of available z-layers (e.g., [0, 1, 2])
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Advanced Configuration with Grid Options
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const solver = new RectDiffSolver({
|
|
61
|
+
simpleRouteJson,
|
|
62
|
+
mode: "grid",
|
|
63
|
+
gridOptions: {
|
|
64
|
+
minSingle: { width: 0.4, height: 0.4 },
|
|
65
|
+
minMulti: { width: 1.0, height: 1.0, minLayers: 2 },
|
|
66
|
+
preferMultiLayer: true,
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
solver.solve()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Incremental Solving with Visualization
|
|
74
|
+
|
|
75
|
+
The solver supports incremental solving for visualization and progress tracking:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const solver = new RectDiffSolver({ simpleRouteJson })
|
|
79
|
+
|
|
80
|
+
solver.setup()
|
|
81
|
+
|
|
82
|
+
// Step through the algorithm incrementally
|
|
83
|
+
while (!solver.solved) {
|
|
84
|
+
solver.step()
|
|
85
|
+
|
|
86
|
+
// Get current progress
|
|
87
|
+
const progress = solver.computeProgress()
|
|
88
|
+
console.log(`Progress: ${(progress * 100).toFixed(1)}%`)
|
|
89
|
+
|
|
90
|
+
// Visualize current state
|
|
91
|
+
const graphicsObject = solver.visualize()
|
|
92
|
+
// Use graphicsObject with graphics-debug package
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const output = solver.getOutput()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Working with Results
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const output = solver.getOutput()
|
|
102
|
+
|
|
103
|
+
// Iterate through mesh nodes
|
|
104
|
+
for (const node of output.meshNodes) {
|
|
105
|
+
console.log(`Node at (${node.center.x}, ${node.center.y})`)
|
|
106
|
+
console.log(` Size: ${node.width} x ${node.height}`)
|
|
107
|
+
console.log(` Available layers: ${node.availableZ?.join(", ") || "none"}`)
|
|
108
|
+
|
|
109
|
+
// Use node for routing decisions
|
|
110
|
+
if (node.availableZ && node.availableZ.length >= 2) {
|
|
111
|
+
console.log(" This node spans multiple layers!")
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### API Reference
|
|
117
|
+
|
|
118
|
+
#### Constructor Options
|
|
119
|
+
|
|
120
|
+
- `simpleRouteJson` (required): The circuit board layout definition
|
|
121
|
+
|
|
122
|
+
- `bounds`: Board boundaries (minX, maxX, minY, maxY)
|
|
123
|
+
- `obstacles`: Array of rectangular obstacles on the board
|
|
124
|
+
- `connections`: Connection requirements between points
|
|
125
|
+
- `layerCount`: Number of layers in the board
|
|
126
|
+
- `minTraceWidth`: Minimum trace width for routing
|
|
127
|
+
|
|
128
|
+
- `mode`: "grid" | "exact" (default: "grid")
|
|
129
|
+
|
|
130
|
+
- Currently only "grid" mode is implemented
|
|
131
|
+
|
|
132
|
+
- `gridOptions`: Fine-tune the grid-based expansion algorithm
|
|
133
|
+
- `minSingle`: Minimum dimensions for single-layer nodes
|
|
134
|
+
- `minMulti`: Minimum dimensions and layer count for multi-layer nodes
|
|
135
|
+
- `preferMultiLayer`: Whether to prefer multi-layer spanning nodes
|
|
136
|
+
|
|
137
|
+
#### Methods
|
|
138
|
+
|
|
139
|
+
- `setup()`: Initialize the solver
|
|
140
|
+
- `step()`: Execute one iteration step
|
|
141
|
+
- `solve()`: Run solver to completion
|
|
142
|
+
- `computeProgress()`: Get current progress (0.0 to 1.0)
|
|
143
|
+
- `getOutput()`: Get the capacity mesh nodes result
|
|
144
|
+
- `visualize()`: Get GraphicsObject for visualization with graphics-debug
|
package/dist/index.js
CHANGED
|
@@ -128,7 +128,12 @@ function expandRectFromSeed(startX, startY, gridSize, bounds, blockers, initialC
|
|
|
128
128
|
let best = null;
|
|
129
129
|
let bestArea = 0;
|
|
130
130
|
STRATS: for (const s of strategies) {
|
|
131
|
-
let r = {
|
|
131
|
+
let r = {
|
|
132
|
+
x: startX + s.ox,
|
|
133
|
+
y: startY + s.oy,
|
|
134
|
+
width: initialW,
|
|
135
|
+
height: initialH
|
|
136
|
+
};
|
|
132
137
|
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)) {
|
|
133
138
|
continue;
|
|
134
139
|
}
|
|
@@ -213,7 +218,14 @@ function computeCandidates3D(bounds, gridSize, layerCount, obstaclesByLayer, pla
|
|
|
213
218
|
if (Math.abs(x - bounds.x) < EPS || Math.abs(y - bounds.y) < EPS || x > bounds.x + bounds.width - gridSize - EPS || y > bounds.y + bounds.height - gridSize - EPS) {
|
|
214
219
|
continue;
|
|
215
220
|
}
|
|
216
|
-
if (isFullyOccupiedAllLayers(
|
|
221
|
+
if (isFullyOccupiedAllLayers(
|
|
222
|
+
x,
|
|
223
|
+
y,
|
|
224
|
+
layerCount,
|
|
225
|
+
obstaclesByLayer,
|
|
226
|
+
placedByLayer
|
|
227
|
+
))
|
|
228
|
+
continue;
|
|
217
229
|
let bestSpan = [];
|
|
218
230
|
let bestZ = 0;
|
|
219
231
|
for (let z = 0; z < layerCount; z++) {
|
|
@@ -263,7 +275,10 @@ function computeCandidates3D(bounds, gridSize, layerCount, obstaclesByLayer, pla
|
|
|
263
275
|
}
|
|
264
276
|
function longestFreeSpanAroundZ(x, y, z, layerCount, minSpan, maxSpan, obstaclesByLayer, placedByLayer) {
|
|
265
277
|
const isFreeAt = (layer) => {
|
|
266
|
-
const blockers = [
|
|
278
|
+
const blockers = [
|
|
279
|
+
...obstaclesByLayer[layer] ?? [],
|
|
280
|
+
...placedByLayer[layer] ?? []
|
|
281
|
+
];
|
|
267
282
|
return !blockers.some((b) => containsPoint(b, x, y));
|
|
268
283
|
};
|
|
269
284
|
let lo = z;
|
|
@@ -333,10 +348,17 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
|
|
|
333
348
|
const dedup = /* @__PURE__ */ new Set();
|
|
334
349
|
const key = (x, y, z) => `${z}|${x.toFixed(6)}|${y.toFixed(6)}`;
|
|
335
350
|
function fullyOcc(x, y) {
|
|
336
|
-
return isFullyOccupiedAllLayers(
|
|
351
|
+
return isFullyOccupiedAllLayers(
|
|
352
|
+
x,
|
|
353
|
+
y,
|
|
354
|
+
layerCount,
|
|
355
|
+
obstaclesByLayer,
|
|
356
|
+
placedByLayer
|
|
357
|
+
);
|
|
337
358
|
}
|
|
338
359
|
function pushIfFree(x, y, z) {
|
|
339
|
-
if (x < bounds.x + EPS || y < bounds.y + EPS || x > bounds.x + bounds.width - EPS || y > bounds.y + bounds.height - EPS)
|
|
360
|
+
if (x < bounds.x + EPS || y < bounds.y + EPS || x > bounds.x + bounds.width - EPS || y > bounds.y + bounds.height - EPS)
|
|
361
|
+
return;
|
|
340
362
|
if (fullyOcc(x, y)) return;
|
|
341
363
|
const hard = [
|
|
342
364
|
...obstaclesByLayer[z] ?? [],
|
|
@@ -349,11 +371,23 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
|
|
|
349
371
|
const k = key(x, y, z);
|
|
350
372
|
if (dedup.has(k)) return;
|
|
351
373
|
dedup.add(k);
|
|
352
|
-
const span = longestFreeSpanAroundZ(
|
|
374
|
+
const span = longestFreeSpanAroundZ(
|
|
375
|
+
x,
|
|
376
|
+
y,
|
|
377
|
+
z,
|
|
378
|
+
layerCount,
|
|
379
|
+
1,
|
|
380
|
+
void 0,
|
|
381
|
+
obstaclesByLayer,
|
|
382
|
+
hardPlacedByLayer
|
|
383
|
+
);
|
|
353
384
|
out.push({ x, y, z, distance: d, zSpanLen: span.length, isEdgeSeed: true });
|
|
354
385
|
}
|
|
355
386
|
for (let z = 0; z < layerCount; z++) {
|
|
356
|
-
const blockers = [
|
|
387
|
+
const blockers = [
|
|
388
|
+
...obstaclesByLayer[z] ?? [],
|
|
389
|
+
...hardPlacedByLayer[z] ?? []
|
|
390
|
+
];
|
|
357
391
|
const corners = [
|
|
358
392
|
{ x: bounds.x + \u03B4, y: bounds.y + \u03B4 },
|
|
359
393
|
// top-left
|
|
@@ -368,8 +402,16 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
|
|
|
368
402
|
pushIfFree(corner.x, corner.y, z);
|
|
369
403
|
}
|
|
370
404
|
const topY = bounds.y + \u03B4;
|
|
371
|
-
const topCovering = blockers.filter((b) => b.y <= topY && b.y + b.height >= topY).map((b) => ({
|
|
372
|
-
|
|
405
|
+
const topCovering = blockers.filter((b) => b.y <= topY && b.y + b.height >= topY).map((b) => ({
|
|
406
|
+
start: Math.max(bounds.x, b.x),
|
|
407
|
+
end: Math.min(bounds.x + bounds.width, b.x + b.width)
|
|
408
|
+
}));
|
|
409
|
+
const topUncovered = computeUncoveredSegments(
|
|
410
|
+
bounds.x + \u03B4,
|
|
411
|
+
bounds.x + bounds.width - \u03B4,
|
|
412
|
+
topCovering,
|
|
413
|
+
minSize * 0.5
|
|
414
|
+
);
|
|
373
415
|
for (const seg of topUncovered) {
|
|
374
416
|
const segLen = seg.end - seg.start;
|
|
375
417
|
if (segLen >= minSize) {
|
|
@@ -381,8 +423,16 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
|
|
|
381
423
|
}
|
|
382
424
|
}
|
|
383
425
|
const bottomY = bounds.y + bounds.height - \u03B4;
|
|
384
|
-
const bottomCovering = blockers.filter((b) => b.y <= bottomY && b.y + b.height >= bottomY).map((b) => ({
|
|
385
|
-
|
|
426
|
+
const bottomCovering = blockers.filter((b) => b.y <= bottomY && b.y + b.height >= bottomY).map((b) => ({
|
|
427
|
+
start: Math.max(bounds.x, b.x),
|
|
428
|
+
end: Math.min(bounds.x + bounds.width, b.x + b.width)
|
|
429
|
+
}));
|
|
430
|
+
const bottomUncovered = computeUncoveredSegments(
|
|
431
|
+
bounds.x + \u03B4,
|
|
432
|
+
bounds.x + bounds.width - \u03B4,
|
|
433
|
+
bottomCovering,
|
|
434
|
+
minSize * 0.5
|
|
435
|
+
);
|
|
386
436
|
for (const seg of bottomUncovered) {
|
|
387
437
|
const segLen = seg.end - seg.start;
|
|
388
438
|
if (segLen >= minSize) {
|
|
@@ -394,8 +444,16 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
|
|
|
394
444
|
}
|
|
395
445
|
}
|
|
396
446
|
const leftX = bounds.x + \u03B4;
|
|
397
|
-
const leftCovering = blockers.filter((b) => b.x <= leftX && b.x + b.width >= leftX).map((b) => ({
|
|
398
|
-
|
|
447
|
+
const leftCovering = blockers.filter((b) => b.x <= leftX && b.x + b.width >= leftX).map((b) => ({
|
|
448
|
+
start: Math.max(bounds.y, b.y),
|
|
449
|
+
end: Math.min(bounds.y + bounds.height, b.y + b.height)
|
|
450
|
+
}));
|
|
451
|
+
const leftUncovered = computeUncoveredSegments(
|
|
452
|
+
bounds.y + \u03B4,
|
|
453
|
+
bounds.y + bounds.height - \u03B4,
|
|
454
|
+
leftCovering,
|
|
455
|
+
minSize * 0.5
|
|
456
|
+
);
|
|
399
457
|
for (const seg of leftUncovered) {
|
|
400
458
|
const segLen = seg.end - seg.start;
|
|
401
459
|
if (segLen >= minSize) {
|
|
@@ -407,8 +465,16 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
|
|
|
407
465
|
}
|
|
408
466
|
}
|
|
409
467
|
const rightX = bounds.x + bounds.width - \u03B4;
|
|
410
|
-
const rightCovering = blockers.filter((b) => b.x <= rightX && b.x + b.width >= rightX).map((b) => ({
|
|
411
|
-
|
|
468
|
+
const rightCovering = blockers.filter((b) => b.x <= rightX && b.x + b.width >= rightX).map((b) => ({
|
|
469
|
+
start: Math.max(bounds.y, b.y),
|
|
470
|
+
end: Math.min(bounds.y + bounds.height, b.y + b.height)
|
|
471
|
+
}));
|
|
472
|
+
const rightUncovered = computeUncoveredSegments(
|
|
473
|
+
bounds.y + \u03B4,
|
|
474
|
+
bounds.y + bounds.height - \u03B4,
|
|
475
|
+
rightCovering,
|
|
476
|
+
minSize * 0.5
|
|
477
|
+
);
|
|
412
478
|
for (const seg of rightUncovered) {
|
|
413
479
|
const segLen = seg.end - seg.start;
|
|
414
480
|
if (segLen >= minSize) {
|
|
@@ -422,32 +488,72 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
|
|
|
422
488
|
for (const b of blockers) {
|
|
423
489
|
const obLeftX = b.x - \u03B4;
|
|
424
490
|
if (obLeftX > bounds.x + EPS && obLeftX < bounds.x + bounds.width - EPS) {
|
|
425
|
-
const obLeftCovering = blockers.filter(
|
|
426
|
-
|
|
491
|
+
const obLeftCovering = blockers.filter(
|
|
492
|
+
(bl) => bl !== b && bl.x <= obLeftX && bl.x + bl.width >= obLeftX
|
|
493
|
+
).map((bl) => ({
|
|
494
|
+
start: Math.max(b.y, bl.y),
|
|
495
|
+
end: Math.min(b.y + b.height, bl.y + bl.height)
|
|
496
|
+
}));
|
|
497
|
+
const obLeftUncovered = computeUncoveredSegments(
|
|
498
|
+
b.y,
|
|
499
|
+
b.y + b.height,
|
|
500
|
+
obLeftCovering,
|
|
501
|
+
minSize * 0.5
|
|
502
|
+
);
|
|
427
503
|
for (const seg of obLeftUncovered) {
|
|
428
504
|
pushIfFree(obLeftX, seg.center, z);
|
|
429
505
|
}
|
|
430
506
|
}
|
|
431
507
|
const obRightX = b.x + b.width + \u03B4;
|
|
432
508
|
if (obRightX > bounds.x + EPS && obRightX < bounds.x + bounds.width - EPS) {
|
|
433
|
-
const obRightCovering = blockers.filter(
|
|
434
|
-
|
|
509
|
+
const obRightCovering = blockers.filter(
|
|
510
|
+
(bl) => bl !== b && bl.x <= obRightX && bl.x + bl.width >= obRightX
|
|
511
|
+
).map((bl) => ({
|
|
512
|
+
start: Math.max(b.y, bl.y),
|
|
513
|
+
end: Math.min(b.y + b.height, bl.y + bl.height)
|
|
514
|
+
}));
|
|
515
|
+
const obRightUncovered = computeUncoveredSegments(
|
|
516
|
+
b.y,
|
|
517
|
+
b.y + b.height,
|
|
518
|
+
obRightCovering,
|
|
519
|
+
minSize * 0.5
|
|
520
|
+
);
|
|
435
521
|
for (const seg of obRightUncovered) {
|
|
436
522
|
pushIfFree(obRightX, seg.center, z);
|
|
437
523
|
}
|
|
438
524
|
}
|
|
439
525
|
const obTopY = b.y - \u03B4;
|
|
440
526
|
if (obTopY > bounds.y + EPS && obTopY < bounds.y + bounds.height - EPS) {
|
|
441
|
-
const obTopCovering = blockers.filter(
|
|
442
|
-
|
|
527
|
+
const obTopCovering = blockers.filter(
|
|
528
|
+
(bl) => bl !== b && bl.y <= obTopY && bl.y + bl.height >= obTopY
|
|
529
|
+
).map((bl) => ({
|
|
530
|
+
start: Math.max(b.x, bl.x),
|
|
531
|
+
end: Math.min(b.x + b.width, bl.x + bl.width)
|
|
532
|
+
}));
|
|
533
|
+
const obTopUncovered = computeUncoveredSegments(
|
|
534
|
+
b.x,
|
|
535
|
+
b.x + b.width,
|
|
536
|
+
obTopCovering,
|
|
537
|
+
minSize * 0.5
|
|
538
|
+
);
|
|
443
539
|
for (const seg of obTopUncovered) {
|
|
444
540
|
pushIfFree(seg.center, obTopY, z);
|
|
445
541
|
}
|
|
446
542
|
}
|
|
447
543
|
const obBottomY = b.y + b.height + \u03B4;
|
|
448
544
|
if (obBottomY > bounds.y + EPS && obBottomY < bounds.y + bounds.height - EPS) {
|
|
449
|
-
const obBottomCovering = blockers.filter(
|
|
450
|
-
|
|
545
|
+
const obBottomCovering = blockers.filter(
|
|
546
|
+
(bl) => bl !== b && bl.y <= obBottomY && bl.y + bl.height >= obBottomY
|
|
547
|
+
).map((bl) => ({
|
|
548
|
+
start: Math.max(b.x, bl.x),
|
|
549
|
+
end: Math.min(b.x + b.width, bl.x + bl.width)
|
|
550
|
+
}));
|
|
551
|
+
const obBottomUncovered = computeUncoveredSegments(
|
|
552
|
+
b.x,
|
|
553
|
+
b.x + b.width,
|
|
554
|
+
obBottomCovering,
|
|
555
|
+
minSize * 0.5
|
|
556
|
+
);
|
|
451
557
|
for (const seg of obBottomUncovered) {
|
|
452
558
|
pushIfFree(seg.center, obBottomY, z);
|
|
453
559
|
}
|
|
@@ -476,19 +582,54 @@ function canonicalizeLayerOrder(names) {
|
|
|
476
582
|
});
|
|
477
583
|
}
|
|
478
584
|
function buildZIndexMap(srj) {
|
|
479
|
-
const names = canonicalizeLayerOrder(
|
|
585
|
+
const names = canonicalizeLayerOrder(
|
|
586
|
+
(srj.obstacles ?? []).flatMap((o) => o.layers ?? [])
|
|
587
|
+
);
|
|
588
|
+
const declaredLayerCount = Math.max(1, srj.layerCount || names.length || 1);
|
|
480
589
|
const fallback = Array.from(
|
|
481
|
-
{ length:
|
|
482
|
-
(_, i) => i === 0 ? "top" : i ===
|
|
590
|
+
{ length: declaredLayerCount },
|
|
591
|
+
(_, i) => i === 0 ? "top" : i === declaredLayerCount - 1 ? "bottom" : `inner${i}`
|
|
483
592
|
);
|
|
484
|
-
const
|
|
593
|
+
const ordered = [];
|
|
594
|
+
const seen = /* @__PURE__ */ new Set();
|
|
595
|
+
const push = (n) => {
|
|
596
|
+
const key = n.toLowerCase();
|
|
597
|
+
if (seen.has(key)) return;
|
|
598
|
+
seen.add(key);
|
|
599
|
+
ordered.push(n);
|
|
600
|
+
};
|
|
601
|
+
fallback.forEach(push);
|
|
602
|
+
names.forEach(push);
|
|
603
|
+
const layerNames = ordered.slice(0, declaredLayerCount);
|
|
604
|
+
const clampIndex = (nameLower) => {
|
|
605
|
+
if (layerNames.length <= 1) return 0;
|
|
606
|
+
if (nameLower === "top") return 0;
|
|
607
|
+
if (nameLower === "bottom") return layerNames.length - 1;
|
|
608
|
+
const m = /^inner(\d+)$/i.exec(nameLower);
|
|
609
|
+
if (m) {
|
|
610
|
+
if (layerNames.length <= 2) return layerNames.length - 1;
|
|
611
|
+
const parsed = parseInt(m[1], 10);
|
|
612
|
+
const maxInner = layerNames.length - 2;
|
|
613
|
+
const clampedInner = Math.min(
|
|
614
|
+
maxInner,
|
|
615
|
+
Math.max(1, Number.isFinite(parsed) ? parsed : 1)
|
|
616
|
+
);
|
|
617
|
+
return clampedInner;
|
|
618
|
+
}
|
|
619
|
+
return 0;
|
|
620
|
+
};
|
|
485
621
|
const map = /* @__PURE__ */ new Map();
|
|
486
|
-
layerNames.forEach((n, i) => map.set(n, i));
|
|
622
|
+
layerNames.forEach((n, i) => map.set(n.toLowerCase(), i));
|
|
623
|
+
ordered.slice(layerNames.length).forEach((n) => {
|
|
624
|
+
const key = n.toLowerCase();
|
|
625
|
+
map.set(key, clampIndex(key));
|
|
626
|
+
});
|
|
487
627
|
return { layerNames, zIndexByName: map };
|
|
488
628
|
}
|
|
489
629
|
function obstacleZs(ob, zIndexByName) {
|
|
490
|
-
if (ob.zLayers?.length)
|
|
491
|
-
|
|
630
|
+
if (ob.zLayers?.length)
|
|
631
|
+
return Array.from(new Set(ob.zLayers)).sort((a, b) => a - b);
|
|
632
|
+
const fromNames = (ob.layers ?? []).map((n) => zIndexByName.get(n.toLowerCase())).filter((v) => typeof v === "number");
|
|
492
633
|
return Array.from(new Set(fromNames)).sort((a, b) => a - b);
|
|
493
634
|
}
|
|
494
635
|
function obstacleToXYRect(ob) {
|
|
@@ -508,12 +649,24 @@ function initState(srj, opts) {
|
|
|
508
649
|
width: srj.bounds.maxX - srj.bounds.minX,
|
|
509
650
|
height: srj.bounds.maxY - srj.bounds.minY
|
|
510
651
|
};
|
|
511
|
-
const obstaclesByLayer = Array.from(
|
|
652
|
+
const obstaclesByLayer = Array.from(
|
|
653
|
+
{ length: layerCount },
|
|
654
|
+
() => []
|
|
655
|
+
);
|
|
512
656
|
for (const ob of srj.obstacles ?? []) {
|
|
513
657
|
const r = obstacleToXYRect(ob);
|
|
514
658
|
if (!r) continue;
|
|
515
659
|
const zs = obstacleZs(ob, zIndexByName);
|
|
516
|
-
|
|
660
|
+
const invalidZs = zs.filter((z) => z < 0 || z >= layerCount);
|
|
661
|
+
if (invalidZs.length) {
|
|
662
|
+
throw new Error(
|
|
663
|
+
`RectDiffSolver: obstacle uses z-layer indices ${invalidZs.join(
|
|
664
|
+
","
|
|
665
|
+
)} outside 0-${layerCount - 1}`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
if ((!ob.zLayers || ob.zLayers.length === 0) && zs.length) ob.zLayers = zs;
|
|
669
|
+
for (const z of zs) obstaclesByLayer[z].push(r);
|
|
517
670
|
}
|
|
518
671
|
const trace = Math.max(0.01, srj.minTraceWidth || 0.15);
|
|
519
672
|
const defaults = {
|
|
@@ -529,7 +682,11 @@ function initState(srj, opts) {
|
|
|
529
682
|
preferMultiLayer: true,
|
|
530
683
|
maxMultiLayerSpan: void 0
|
|
531
684
|
};
|
|
532
|
-
const options = {
|
|
685
|
+
const options = {
|
|
686
|
+
...defaults,
|
|
687
|
+
...opts,
|
|
688
|
+
gridSizes: opts.gridSizes ?? defaults.gridSizes
|
|
689
|
+
};
|
|
533
690
|
const placedByLayer = Array.from({ length: layerCount }, () => []);
|
|
534
691
|
return {
|
|
535
692
|
srj,
|
|
@@ -586,8 +743,14 @@ function resizeSoftOverlaps(state, newIndex) {
|
|
|
586
743
|
if (unaffectedZ.length > 0) {
|
|
587
744
|
toAdd.push({ rect: old.rect, zLayers: unaffectedZ });
|
|
588
745
|
}
|
|
589
|
-
const minW = Math.min(
|
|
590
|
-
|
|
746
|
+
const minW = Math.min(
|
|
747
|
+
state.options.minSingle.width,
|
|
748
|
+
state.options.minMulti.width
|
|
749
|
+
);
|
|
750
|
+
const minH = Math.min(
|
|
751
|
+
state.options.minSingle.height,
|
|
752
|
+
state.options.minMulti.height
|
|
753
|
+
);
|
|
591
754
|
for (const p of parts) {
|
|
592
755
|
if (p.width + EPS >= minW && p.height + EPS >= minH) {
|
|
593
756
|
toAdd.push({ rect: p, zLayers: sharedZ.slice() });
|
|
@@ -676,14 +839,23 @@ function stepGrid(state) {
|
|
|
676
839
|
);
|
|
677
840
|
const attempts = [];
|
|
678
841
|
if (span.length >= minMulti.minLayers) {
|
|
679
|
-
attempts.push({
|
|
842
|
+
attempts.push({
|
|
843
|
+
kind: "multi",
|
|
844
|
+
layers: span,
|
|
845
|
+
minReq: { width: minMulti.width, height: minMulti.height }
|
|
846
|
+
});
|
|
680
847
|
}
|
|
681
|
-
attempts.push({
|
|
848
|
+
attempts.push({
|
|
849
|
+
kind: "single",
|
|
850
|
+
layers: [cand.z],
|
|
851
|
+
minReq: { width: minSingle.width, height: minSingle.height }
|
|
852
|
+
});
|
|
682
853
|
const ordered = preferMultiLayer ? attempts : attempts.reverse();
|
|
683
854
|
for (const attempt of ordered) {
|
|
684
855
|
const hardBlockers = [];
|
|
685
856
|
for (const z of attempt.layers) {
|
|
686
|
-
if (state.obstaclesByLayer[z])
|
|
857
|
+
if (state.obstaclesByLayer[z])
|
|
858
|
+
hardBlockers.push(...state.obstaclesByLayer[z]);
|
|
687
859
|
if (hardPlacedByLayer[z]) hardBlockers.push(...hardPlacedByLayer[z]);
|
|
688
860
|
}
|
|
689
861
|
const rect = expandRectFromSeed(
|
|
@@ -702,7 +874,9 @@ function stepGrid(state) {
|
|
|
702
874
|
const newIndex = state.placed.push(placed) - 1;
|
|
703
875
|
for (const z of attempt.layers) state.placedByLayer[z].push(rect);
|
|
704
876
|
resizeSoftOverlaps(state, newIndex);
|
|
705
|
-
state.candidates = state.candidates.filter(
|
|
877
|
+
state.candidates = state.candidates.filter(
|
|
878
|
+
(c) => !isFullyOccupiedAtPoint(state, c.x, c.y)
|
|
879
|
+
);
|
|
706
880
|
return;
|
|
707
881
|
}
|
|
708
882
|
}
|
|
@@ -745,13 +919,32 @@ function stepExpansion(state) {
|
|
|
745
919
|
state.expansionIndex += 1;
|
|
746
920
|
}
|
|
747
921
|
function finalizeRects(state) {
|
|
748
|
-
|
|
922
|
+
const out = state.placed.map((p) => ({
|
|
749
923
|
minX: p.rect.x,
|
|
750
924
|
minY: p.rect.y,
|
|
751
925
|
maxX: p.rect.x + p.rect.width,
|
|
752
926
|
maxY: p.rect.y + p.rect.height,
|
|
753
927
|
zLayers: [...p.zLayers].sort((a, b) => a - b)
|
|
754
928
|
}));
|
|
929
|
+
const layersByObstacleRect = /* @__PURE__ */ new Map();
|
|
930
|
+
state.obstaclesByLayer.forEach((layerObs, z) => {
|
|
931
|
+
for (const rect of layerObs) {
|
|
932
|
+
const layerIndices = layersByObstacleRect.get(rect) ?? [];
|
|
933
|
+
layerIndices.push(z);
|
|
934
|
+
layersByObstacleRect.set(rect, layerIndices);
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
for (const [rect, layerIndices] of layersByObstacleRect.entries()) {
|
|
938
|
+
out.push({
|
|
939
|
+
minX: rect.x,
|
|
940
|
+
minY: rect.y,
|
|
941
|
+
maxX: rect.x + rect.width,
|
|
942
|
+
maxY: rect.y + rect.height,
|
|
943
|
+
zLayers: layerIndices.sort((a, b) => a - b),
|
|
944
|
+
isObstacle: true
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
return out;
|
|
755
948
|
}
|
|
756
949
|
function computeProgress(state) {
|
|
757
950
|
const grids = state.options.gridSizes.length;
|
|
@@ -785,7 +978,9 @@ function rectsToMeshNodes(rects) {
|
|
|
785
978
|
width: w,
|
|
786
979
|
height: h,
|
|
787
980
|
layer: "top",
|
|
788
|
-
availableZ: r.zLayers.slice()
|
|
981
|
+
availableZ: r.zLayers.slice(),
|
|
982
|
+
_containsObstacle: r.isObstacle,
|
|
983
|
+
_containsTarget: r.isObstacle
|
|
789
984
|
});
|
|
790
985
|
}
|
|
791
986
|
return out;
|
|
@@ -898,7 +1093,10 @@ var RectDiffSolver = class extends BaseSolver {
|
|
|
898
1093
|
for (const p of this.state.placed) {
|
|
899
1094
|
const colors = this.getColorForZLayer(p.zLayers);
|
|
900
1095
|
rects.push({
|
|
901
|
-
center: {
|
|
1096
|
+
center: {
|
|
1097
|
+
x: p.rect.x + p.rect.width / 2,
|
|
1098
|
+
y: p.rect.y + p.rect.height / 2
|
|
1099
|
+
},
|
|
902
1100
|
width: p.rect.width,
|
|
903
1101
|
height: p.rect.height,
|
|
904
1102
|
fill: colors.fill,
|
package/global.d.ts
CHANGED