@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.
@@ -17,7 +17,7 @@ jobs:
17
17
  - name: Setup bun
18
18
  uses: oven-sh/setup-bun@v2
19
19
  with:
20
- bun-version: latest
20
+ bun-version: 1.3.1
21
21
 
22
22
  - name: Install dependencies
23
23
  run: bun install
@@ -21,13 +21,13 @@ jobs:
21
21
  - name: Setup bun
22
22
  uses: oven-sh/setup-bun@v2
23
23
  with:
24
- bun-version: latest
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 --frozen-lockfile
30
+ - run: bun install
31
31
  - run: bun run build
32
32
  - run: pver release --no-push-main
33
33
  env:
@@ -22,7 +22,7 @@ jobs:
22
22
  - name: Setup bun
23
23
  uses: oven-sh/setup-bun@v2
24
24
  with:
25
- bun-version: latest
25
+ bun-version: 1.3.1
26
26
 
27
27
  - name: Install dependencies
28
28
  run: bun install
@@ -17,7 +17,7 @@ jobs:
17
17
  - name: Setup bun
18
18
  uses: oven-sh/setup-bun@v2
19
19
  with:
20
- bun-version: latest
20
+ bun-version: 1.3.1
21
21
 
22
22
  - name: Install dependencies
23
23
  run: bun i
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 = { x: startX + s.ox, y: startY + s.oy, width: initialW, height: initialH };
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(x, y, layerCount, obstaclesByLayer, placedByLayer)) continue;
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 = [...obstaclesByLayer[layer] ?? [], ...placedByLayer[layer] ?? []];
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(x, y, layerCount, obstaclesByLayer, placedByLayer);
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) return;
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(x, y, z, layerCount, 1, void 0, obstaclesByLayer, hardPlacedByLayer);
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 = [...obstaclesByLayer[z] ?? [], ...hardPlacedByLayer[z] ?? []];
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) => ({ start: Math.max(bounds.x, b.x), end: Math.min(bounds.x + bounds.width, b.x + b.width) }));
372
- const topUncovered = computeUncoveredSegments(bounds.x + \u03B4, bounds.x + bounds.width - \u03B4, topCovering, minSize * 0.5);
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) => ({ start: Math.max(bounds.x, b.x), end: Math.min(bounds.x + bounds.width, b.x + b.width) }));
385
- const bottomUncovered = computeUncoveredSegments(bounds.x + \u03B4, bounds.x + bounds.width - \u03B4, bottomCovering, minSize * 0.5);
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) => ({ start: Math.max(bounds.y, b.y), end: Math.min(bounds.y + bounds.height, b.y + b.height) }));
398
- const leftUncovered = computeUncoveredSegments(bounds.y + \u03B4, bounds.y + bounds.height - \u03B4, leftCovering, minSize * 0.5);
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) => ({ start: Math.max(bounds.y, b.y), end: Math.min(bounds.y + bounds.height, b.y + b.height) }));
411
- const rightUncovered = computeUncoveredSegments(bounds.y + \u03B4, bounds.y + bounds.height - \u03B4, rightCovering, minSize * 0.5);
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((bl) => bl !== b && bl.x <= obLeftX && bl.x + bl.width >= obLeftX).map((bl) => ({ start: Math.max(b.y, bl.y), end: Math.min(b.y + b.height, bl.y + bl.height) }));
426
- const obLeftUncovered = computeUncoveredSegments(b.y, b.y + b.height, obLeftCovering, minSize * 0.5);
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((bl) => bl !== b && bl.x <= obRightX && bl.x + bl.width >= obRightX).map((bl) => ({ start: Math.max(b.y, bl.y), end: Math.min(b.y + b.height, bl.y + bl.height) }));
434
- const obRightUncovered = computeUncoveredSegments(b.y, b.y + b.height, obRightCovering, minSize * 0.5);
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((bl) => bl !== b && bl.y <= obTopY && bl.y + bl.height >= obTopY).map((bl) => ({ start: Math.max(b.x, bl.x), end: Math.min(b.x + b.width, bl.x + bl.width) }));
442
- const obTopUncovered = computeUncoveredSegments(b.x, b.x + b.width, obTopCovering, minSize * 0.5);
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((bl) => bl !== b && bl.y <= obBottomY && bl.y + bl.height >= obBottomY).map((bl) => ({ start: Math.max(b.x, bl.x), end: Math.min(b.x + b.width, bl.x + bl.width) }));
450
- const obBottomUncovered = computeUncoveredSegments(b.x, b.x + b.width, obBottomCovering, minSize * 0.5);
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((srj.obstacles ?? []).flatMap((o) => o.layers ?? []));
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) return Array.from(new Set(ob.zLayers)).sort((a, b) => a - b);
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({ length: layerCount }, () => []);
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) if (z >= 0 && z < layerCount) obstaclesByLayer[z].push(r);
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 = { ...defaults, ...opts, gridSizes: opts.gridSizes ?? defaults.gridSizes };
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(state.options.minSingle.width, state.options.minMulti.width);
590
- const minH = Math.min(state.options.minSingle.height, state.options.minMulti.height);
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({ kind: "multi", layers: span, minReq: { width: minMulti.width, height: minMulti.height } });
802
+ attempts.push({
803
+ kind: "multi",
804
+ layers: span,
805
+ minReq: { width: minMulti.width, height: minMulti.height }
806
+ });
680
807
  }
681
- attempts.push({ kind: "single", layers: [cand.z], minReq: { width: minSingle.width, height: minSingle.height } });
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]) hardBlockers.push(...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((c) => !isFullyOccupiedAtPoint(state, c.x, c.y));
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
- return state.placed.map((p) => ({
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: { x: p.rect.x + p.rect.width / 2, y: p.rect.y + p.rect.height / 2 },
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
@@ -1,4 +1,4 @@
1
1
  declare module "*.json" {
2
- const value: any;
3
- export default value;
2
+ const value: any
3
+ export default value
4
4
  }
@@ -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 { initState, stepGrid, stepExpansion, finalizeRects, computeProgress } from "./rectdiff/engine"
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[]): { fill: string; stroke: string } {
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: { x: p.rect.x + p.rect.width / 2, y: p.rect.y + p.rect.height / 2 },
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,