@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.
@@ -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,19 +582,54 @@ 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
+ );
588
+ const declaredLayerCount = Math.max(1, srj.layerCount || names.length || 1);
480
589
  const fallback = Array.from(
481
- { length: Math.max(1, srj.layerCount || 1) },
482
- (_, i) => i === 0 ? "top" : i === (srj.layerCount || 1) - 1 ? "bottom" : `inner${i}`
590
+ { length: declaredLayerCount },
591
+ (_, i) => i === 0 ? "top" : i === declaredLayerCount - 1 ? "bottom" : `inner${i}`
483
592
  );
484
- const layerNames = names.length ? names : fallback;
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) return Array.from(new Set(ob.zLayers)).sort((a, b) => a - b);
491
- const fromNames = (ob.layers ?? []).map((n) => zIndexByName.get(n)).filter((v) => typeof v === "number");
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({ length: layerCount }, () => []);
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
- for (const z of zs) if (z >= 0 && z < layerCount) obstaclesByLayer[z].push(r);
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 = { ...defaults, ...opts, gridSizes: opts.gridSizes ?? defaults.gridSizes };
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(state.options.minSingle.width, state.options.minMulti.width);
590
- const minH = Math.min(state.options.minSingle.height, state.options.minMulti.height);
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({ kind: "multi", layers: span, minReq: { width: minMulti.width, height: minMulti.height } });
842
+ attempts.push({
843
+ kind: "multi",
844
+ layers: span,
845
+ minReq: { width: minMulti.width, height: minMulti.height }
846
+ });
680
847
  }
681
- attempts.push({ kind: "single", layers: [cand.z], minReq: { width: minSingle.width, height: minSingle.height } });
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]) hardBlockers.push(...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((c) => !isFullyOccupiedAtPoint(state, c.x, c.y));
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
- return state.placed.map((p) => ({
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: { x: p.rect.x + p.rect.width / 2, y: p.rect.y + p.rect.height / 2 },
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
@@ -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
  }