@tscircuit/rectdiff 0.0.1 → 0.0.2
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 +195 -37
- 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 +108 -29
- package/lib/solvers/rectdiff/geometry.ts +81 -28
- package/lib/solvers/rectdiff/layers.ts +11 -3
- 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/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,7 +582,9 @@ 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
|
+
);
|
|
480
588
|
const fallback = Array.from(
|
|
481
589
|
{ length: Math.max(1, srj.layerCount || 1) },
|
|
482
590
|
(_, i) => i === 0 ? "top" : i === (srj.layerCount || 1) - 1 ? "bottom" : `inner${i}`
|
|
@@ -487,7 +595,8 @@ function buildZIndexMap(srj) {
|
|
|
487
595
|
return { layerNames, zIndexByName: map };
|
|
488
596
|
}
|
|
489
597
|
function obstacleZs(ob, zIndexByName) {
|
|
490
|
-
if (ob.zLayers?.length)
|
|
598
|
+
if (ob.zLayers?.length)
|
|
599
|
+
return Array.from(new Set(ob.zLayers)).sort((a, b) => a - b);
|
|
491
600
|
const fromNames = (ob.layers ?? []).map((n) => zIndexByName.get(n)).filter((v) => typeof v === "number");
|
|
492
601
|
return Array.from(new Set(fromNames)).sort((a, b) => a - b);
|
|
493
602
|
}
|
|
@@ -508,12 +617,16 @@ function initState(srj, opts) {
|
|
|
508
617
|
width: srj.bounds.maxX - srj.bounds.minX,
|
|
509
618
|
height: srj.bounds.maxY - srj.bounds.minY
|
|
510
619
|
};
|
|
511
|
-
const obstaclesByLayer = Array.from(
|
|
620
|
+
const obstaclesByLayer = Array.from(
|
|
621
|
+
{ length: layerCount },
|
|
622
|
+
() => []
|
|
623
|
+
);
|
|
512
624
|
for (const ob of srj.obstacles ?? []) {
|
|
513
625
|
const r = obstacleToXYRect(ob);
|
|
514
626
|
if (!r) continue;
|
|
515
627
|
const zs = obstacleZs(ob, zIndexByName);
|
|
516
|
-
for (const z of zs)
|
|
628
|
+
for (const z of zs)
|
|
629
|
+
if (z >= 0 && z < layerCount) obstaclesByLayer[z].push(r);
|
|
517
630
|
}
|
|
518
631
|
const trace = Math.max(0.01, srj.minTraceWidth || 0.15);
|
|
519
632
|
const defaults = {
|
|
@@ -529,7 +642,11 @@ function initState(srj, opts) {
|
|
|
529
642
|
preferMultiLayer: true,
|
|
530
643
|
maxMultiLayerSpan: void 0
|
|
531
644
|
};
|
|
532
|
-
const options = {
|
|
645
|
+
const options = {
|
|
646
|
+
...defaults,
|
|
647
|
+
...opts,
|
|
648
|
+
gridSizes: opts.gridSizes ?? defaults.gridSizes
|
|
649
|
+
};
|
|
533
650
|
const placedByLayer = Array.from({ length: layerCount }, () => []);
|
|
534
651
|
return {
|
|
535
652
|
srj,
|
|
@@ -586,8 +703,14 @@ function resizeSoftOverlaps(state, newIndex) {
|
|
|
586
703
|
if (unaffectedZ.length > 0) {
|
|
587
704
|
toAdd.push({ rect: old.rect, zLayers: unaffectedZ });
|
|
588
705
|
}
|
|
589
|
-
const minW = Math.min(
|
|
590
|
-
|
|
706
|
+
const minW = Math.min(
|
|
707
|
+
state.options.minSingle.width,
|
|
708
|
+
state.options.minMulti.width
|
|
709
|
+
);
|
|
710
|
+
const minH = Math.min(
|
|
711
|
+
state.options.minSingle.height,
|
|
712
|
+
state.options.minMulti.height
|
|
713
|
+
);
|
|
591
714
|
for (const p of parts) {
|
|
592
715
|
if (p.width + EPS >= minW && p.height + EPS >= minH) {
|
|
593
716
|
toAdd.push({ rect: p, zLayers: sharedZ.slice() });
|
|
@@ -676,14 +799,23 @@ function stepGrid(state) {
|
|
|
676
799
|
);
|
|
677
800
|
const attempts = [];
|
|
678
801
|
if (span.length >= minMulti.minLayers) {
|
|
679
|
-
attempts.push({
|
|
802
|
+
attempts.push({
|
|
803
|
+
kind: "multi",
|
|
804
|
+
layers: span,
|
|
805
|
+
minReq: { width: minMulti.width, height: minMulti.height }
|
|
806
|
+
});
|
|
680
807
|
}
|
|
681
|
-
attempts.push({
|
|
808
|
+
attempts.push({
|
|
809
|
+
kind: "single",
|
|
810
|
+
layers: [cand.z],
|
|
811
|
+
minReq: { width: minSingle.width, height: minSingle.height }
|
|
812
|
+
});
|
|
682
813
|
const ordered = preferMultiLayer ? attempts : attempts.reverse();
|
|
683
814
|
for (const attempt of ordered) {
|
|
684
815
|
const hardBlockers = [];
|
|
685
816
|
for (const z of attempt.layers) {
|
|
686
|
-
if (state.obstaclesByLayer[z])
|
|
817
|
+
if (state.obstaclesByLayer[z])
|
|
818
|
+
hardBlockers.push(...state.obstaclesByLayer[z]);
|
|
687
819
|
if (hardPlacedByLayer[z]) hardBlockers.push(...hardPlacedByLayer[z]);
|
|
688
820
|
}
|
|
689
821
|
const rect = expandRectFromSeed(
|
|
@@ -702,7 +834,9 @@ function stepGrid(state) {
|
|
|
702
834
|
const newIndex = state.placed.push(placed) - 1;
|
|
703
835
|
for (const z of attempt.layers) state.placedByLayer[z].push(rect);
|
|
704
836
|
resizeSoftOverlaps(state, newIndex);
|
|
705
|
-
state.candidates = state.candidates.filter(
|
|
837
|
+
state.candidates = state.candidates.filter(
|
|
838
|
+
(c) => !isFullyOccupiedAtPoint(state, c.x, c.y)
|
|
839
|
+
);
|
|
706
840
|
return;
|
|
707
841
|
}
|
|
708
842
|
}
|
|
@@ -745,13 +879,32 @@ function stepExpansion(state) {
|
|
|
745
879
|
state.expansionIndex += 1;
|
|
746
880
|
}
|
|
747
881
|
function finalizeRects(state) {
|
|
748
|
-
|
|
882
|
+
const out = state.placed.map((p) => ({
|
|
749
883
|
minX: p.rect.x,
|
|
750
884
|
minY: p.rect.y,
|
|
751
885
|
maxX: p.rect.x + p.rect.width,
|
|
752
886
|
maxY: p.rect.y + p.rect.height,
|
|
753
887
|
zLayers: [...p.zLayers].sort((a, b) => a - b)
|
|
754
888
|
}));
|
|
889
|
+
const layersByObstacleRect = /* @__PURE__ */ new Map();
|
|
890
|
+
state.obstaclesByLayer.forEach((layerObs, z) => {
|
|
891
|
+
for (const rect of layerObs) {
|
|
892
|
+
const layerIndices = layersByObstacleRect.get(rect) ?? [];
|
|
893
|
+
layerIndices.push(z);
|
|
894
|
+
layersByObstacleRect.set(rect, layerIndices);
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
for (const [rect, layerIndices] of layersByObstacleRect.entries()) {
|
|
898
|
+
out.push({
|
|
899
|
+
minX: rect.x,
|
|
900
|
+
minY: rect.y,
|
|
901
|
+
maxX: rect.x + rect.width,
|
|
902
|
+
maxY: rect.y + rect.height,
|
|
903
|
+
zLayers: layerIndices.sort((a, b) => a - b),
|
|
904
|
+
isObstacle: true
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
return out;
|
|
755
908
|
}
|
|
756
909
|
function computeProgress(state) {
|
|
757
910
|
const grids = state.options.gridSizes.length;
|
|
@@ -785,7 +938,9 @@ function rectsToMeshNodes(rects) {
|
|
|
785
938
|
width: w,
|
|
786
939
|
height: h,
|
|
787
940
|
layer: "top",
|
|
788
|
-
availableZ: r.zLayers.slice()
|
|
941
|
+
availableZ: r.zLayers.slice(),
|
|
942
|
+
_containsObstacle: r.isObstacle,
|
|
943
|
+
_containsTarget: r.isObstacle
|
|
789
944
|
});
|
|
790
945
|
}
|
|
791
946
|
return out;
|
|
@@ -898,7 +1053,10 @@ var RectDiffSolver = class extends BaseSolver {
|
|
|
898
1053
|
for (const p of this.state.placed) {
|
|
899
1054
|
const colors = this.getColorForZLayer(p.zLayers);
|
|
900
1055
|
rects.push({
|
|
901
|
-
center: {
|
|
1056
|
+
center: {
|
|
1057
|
+
x: p.rect.x + p.rect.width / 2,
|
|
1058
|
+
y: p.rect.y + p.rect.height / 2
|
|
1059
|
+
},
|
|
902
1060
|
width: p.rect.width,
|
|
903
1061
|
height: p.rect.height,
|
|
904
1062
|
fill: colors.fill,
|
package/global.d.ts
CHANGED
|
@@ -5,7 +5,13 @@ import type { GraphicsObject } from "graphics-debug"
|
|
|
5
5
|
import type { CapacityMeshNode } from "../types/capacity-mesh-types"
|
|
6
6
|
|
|
7
7
|
import type { GridFill3DOptions, RectDiffState } from "./rectdiff/types"
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
initState,
|
|
10
|
+
stepGrid,
|
|
11
|
+
stepExpansion,
|
|
12
|
+
finalizeRects,
|
|
13
|
+
computeProgress,
|
|
14
|
+
} from "./rectdiff/engine"
|
|
9
15
|
import { rectsToMeshNodes } from "./rectdiff/rectsToMeshNodes"
|
|
10
16
|
|
|
11
17
|
// A streaming, one-step-per-iteration solver.
|
|
@@ -71,7 +77,10 @@ export class RectDiffSolver extends BaseSolver {
|
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
// Helper to get color based on z layer
|
|
74
|
-
private getColorForZLayer(zLayers: number[]): {
|
|
80
|
+
private getColorForZLayer(zLayers: number[]): {
|
|
81
|
+
fill: string
|
|
82
|
+
stroke: string
|
|
83
|
+
} {
|
|
75
84
|
const minZ = Math.min(...zLayers)
|
|
76
85
|
const colors = [
|
|
77
86
|
{ fill: "#dbeafe", stroke: "#3b82f6" }, // blue (z=0)
|
|
@@ -135,7 +144,10 @@ export class RectDiffSolver extends BaseSolver {
|
|
|
135
144
|
for (const p of this.state.placed) {
|
|
136
145
|
const colors = this.getColorForZLayer(p.zLayers)
|
|
137
146
|
rects.push({
|
|
138
|
-
center: {
|
|
147
|
+
center: {
|
|
148
|
+
x: p.rect.x + p.rect.width / 2,
|
|
149
|
+
y: p.rect.y + p.rect.height / 2,
|
|
150
|
+
},
|
|
139
151
|
width: p.rect.width,
|
|
140
152
|
height: p.rect.height,
|
|
141
153
|
fill: colors.fill,
|