@tscircuit/rectdiff 0.0.38 → 0.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.github/workflows/bun-test.yml +1 -1
  2. package/lib/RectDiffPipeline.ts +27 -0
  3. package/lib/math/layers/getUnionZ.ts +6 -0
  4. package/lib/math/layers/getZLayerName.ts +6 -0
  5. package/lib/math/layers/getZSpanMask.ts +6 -0
  6. package/lib/math/layers/hasContiguousZSpan.ts +11 -0
  7. package/lib/math/rects/intersectRects.ts +28 -0
  8. package/lib/math/rects/mergeRects.ts +12 -0
  9. package/lib/math/rects/rectArea.ts +7 -0
  10. package/lib/math/rects/rectContainsRect.ts +18 -0
  11. package/lib/math/rects/rectsTouchOrOverlap.ts +12 -0
  12. package/lib/math/rects/subtractRects.ts +23 -0
  13. package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +108 -4
  14. package/lib/solvers/SparseMultilayerPromotionSolver/SparseMultilayerPromotionSolver.ts +134 -0
  15. package/lib/solvers/SparseMultilayerPromotionSolver/cloneNode.ts +15 -0
  16. package/lib/solvers/SparseMultilayerPromotionSolver/cloneNodeWithRect.ts +34 -0
  17. package/lib/solvers/SparseMultilayerPromotionSolver/createResidualNodes.ts +42 -0
  18. package/lib/solvers/SparseMultilayerPromotionSolver/findBestCoalesceCandidate.ts +98 -0
  19. package/lib/solvers/SparseMultilayerPromotionSolver/findBestPromotionCandidate.ts +72 -0
  20. package/lib/solvers/SparseMultilayerPromotionSolver/getUsableMultilayerVolumeShare.ts +34 -0
  21. package/lib/solvers/SparseMultilayerPromotionSolver/isFreeNode.ts +8 -0
  22. package/lib/solvers/SparseMultilayerPromotionSolver/nodeToRect.ts +13 -0
  23. package/lib/solvers/SparseMultilayerPromotionSolver/solvers/CoalesceMultilayerTilesSolver.ts +104 -0
  24. package/lib/solvers/SparseMultilayerPromotionSolver/solvers/PromoteSparseMultilayerCoverageSolver.ts +148 -0
  25. package/lib/solvers/SparseMultilayerPromotionSolver/solvers/TrimContainedSingleLayerCoverageSolver.ts +137 -0
  26. package/lib/solvers/SparseMultilayerPromotionSolver/types.ts +23 -0
  27. package/package.json +1 -1
  28. package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +3 -3
  29. package/tests/solver/arduino-uno-inner2-ground-bottom-power/__snapshots__/arduino-uno-inner2-ground-bottom-power.snap.svg +3 -3
  30. package/tests/solver/arduino-uno-inner2-ground-inner1-power/__snapshots__/arduino-uno-inner2-ground-inner1-power.snap.svg +1 -1
  31. package/tests/solver/both-points-equivalent/__snapshots__/both-points-equivalent.snap.svg +1 -1
  32. package/tests/solver/bugreport02-bc4361/__snapshots__/bugreport02-bc4361.snap.svg +3 -3
  33. package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
  34. package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
  35. package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +1 -1
  36. package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +3 -3
  37. package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +3 -3
  38. package/tests/solver/bugreport13-b9a758/__snapshots__/bugreport13-b9a758.snap.svg +2 -2
  39. package/tests/solver/bugreport16-d95f38/__snapshots__/bugreport16-d95f38.snap.svg +1 -1
  40. package/tests/solver/bugreport18-1b2d06/__snapshots__/bugreport18-1b2d06.snap.svg +3 -3
  41. package/tests/solver/bugreport19/__snapshots__/bugreport19.snap.svg +1 -1
  42. package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
  43. package/tests/solver/bugreport23-LGA15x4/__snapshots__/bugreport23-LGA15x4.snap.svg +2 -2
  44. package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
  45. package/tests/solver/bugreport25-4b1d55/__snapshots__/bugreport25-4b1d55.snap.svg +3 -3
  46. package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +2 -2
  47. package/tests/solver/interaction/__snapshots__/interaction.snap.svg +3 -3
  48. package/tests/solver/multi-point/__snapshots__/multi-point.snap.svg +1 -1
  49. package/tests/solver/no-better-path/__snapshots__/no-better-path.snap.svg +1 -1
  50. package/tests/solver/offboardconnects01/__snapshots__/offboardconnects01.snap.svg +1 -1
  51. package/tests/solver/pcb_trace_id-should-return-root-connection-name/__snapshots__/pcb_trace_id-should-return-root-connection-name.snap.svg +3 -3
  52. package/tests/solver/repros/merge-single-layer-node/__snapshots__/merge-single-layer-node.snap.svg +3 -3
  53. package/tests/solver/transitivity/__snapshots__/transitivity.snap.svg +2 -2
@@ -0,0 +1,98 @@
1
+ import { getZSpanMask } from "../../math/layers/getZSpanMask"
2
+ import { mergeRects } from "../../math/rects/mergeRects"
3
+ import { rectArea } from "../../math/rects/rectArea"
4
+ import { rectContainsRect } from "../../math/rects/rectContainsRect"
5
+ import { rectsTouchOrOverlap } from "../../math/rects/rectsTouchOrOverlap"
6
+ import { subtractRects } from "../../math/rects/subtractRects"
7
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
8
+ import { isFreeNode } from "./isFreeNode"
9
+ import { nodeToRect } from "./nodeToRect"
10
+ import type { CoalesceCandidate } from "./types"
11
+
12
+ const areRectsAlignedForMerge = ({
13
+ a,
14
+ b,
15
+ }: {
16
+ a: ReturnType<typeof nodeToRect>
17
+ b: ReturnType<typeof nodeToRect>
18
+ }) => {
19
+ const sameVerticalBand =
20
+ Math.abs(a.x - b.x) <= Number.EPSILON &&
21
+ Math.abs(a.width - b.width) <= Number.EPSILON &&
22
+ a.y <= b.y + b.height + Number.EPSILON &&
23
+ b.y <= a.y + a.height + Number.EPSILON
24
+
25
+ const sameHorizontalBand =
26
+ Math.abs(a.y - b.y) <= Number.EPSILON &&
27
+ Math.abs(a.height - b.height) <= Number.EPSILON &&
28
+ a.x <= b.x + b.width + Number.EPSILON &&
29
+ b.x <= a.x + a.width + Number.EPSILON
30
+
31
+ return sameVerticalBand || sameHorizontalBand
32
+ }
33
+
34
+ /**
35
+ * Find a good merge for nearby shared tiles.
36
+ * Larger, more useful merges are preferred.
37
+ */
38
+ export const findBestCoalesceCandidate = ({
39
+ nodes,
40
+ }: {
41
+ nodes: CapacityMeshNode[]
42
+ }): CoalesceCandidate | null => {
43
+ let best: CoalesceCandidate | null = null
44
+ const nodesBySpan = new Map<
45
+ number,
46
+ Array<{ node: CapacityMeshNode; rect: ReturnType<typeof nodeToRect> }>
47
+ >()
48
+
49
+ for (const node of nodes) {
50
+ if (!isFreeNode({ node }) || node.availableZ.length <= 1) continue
51
+ const spanKey = getZSpanMask({ availableZ: node.availableZ })
52
+ const entries = nodesBySpan.get(spanKey) ?? []
53
+ entries.push({ node, rect: nodeToRect({ node }) })
54
+ nodesBySpan.set(spanKey, entries)
55
+ }
56
+
57
+ for (const entries of nodesBySpan.values()) {
58
+ for (let i = 0; i < entries.length; i++) {
59
+ const a = entries[i]!
60
+
61
+ for (let j = i + 1; j < entries.length; j++) {
62
+ const b = entries[j]!
63
+ if (
64
+ !areRectsAlignedForMerge({ a: a.rect, b: b.rect }) &&
65
+ !rectsTouchOrOverlap({ a: a.rect, b: b.rect })
66
+ ) {
67
+ continue
68
+ }
69
+
70
+ const mergedRect = mergeRects({ a: a.rect, b: b.rect })
71
+ const absorbedEntries = entries.filter((entry) =>
72
+ rectContainsRect({ inner: entry.rect, outer: mergedRect }),
73
+ )
74
+ if (absorbedEntries.length < 2) continue
75
+
76
+ if (
77
+ subtractRects({
78
+ target: mergedRect,
79
+ cutters: absorbedEntries.map((entry) => entry.rect),
80
+ }).length > 0
81
+ ) {
82
+ continue
83
+ }
84
+
85
+ const score = rectArea({ rect: mergedRect }) * absorbedEntries.length
86
+ if (!best || score > best.score) {
87
+ best = {
88
+ rect: mergedRect,
89
+ absorbedNodes: absorbedEntries.map((entry) => entry.node),
90
+ score,
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ return best
98
+ }
@@ -0,0 +1,72 @@
1
+ import { hasContiguousZSpan } from "../../math/layers/hasContiguousZSpan"
2
+ import { getUnionZ } from "../../math/layers/getUnionZ"
3
+ import { intersectRects } from "../../math/rects/intersectRects"
4
+ import { rectArea } from "../../math/rects/rectArea"
5
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
6
+ import { EPS } from "../../utils/rectdiff-geometry"
7
+ import { isFreeNode } from "./isFreeNode"
8
+ import { nodeToRect } from "./nodeToRect"
9
+ import type { PromotionCandidate } from "./types"
10
+
11
+ /**
12
+ * Find a good overlap to turn into shared space.
13
+ * Larger overlaps are preferred.
14
+ */
15
+ export const findBestPromotionCandidate = ({
16
+ minRectSize,
17
+ nodes,
18
+ }: {
19
+ minRectSize: number
20
+ nodes: CapacityMeshNode[]
21
+ }): PromotionCandidate | null => {
22
+ let best: PromotionCandidate | null = null
23
+
24
+ for (let i = 0; i < nodes.length; i++) {
25
+ const sourceNode = nodes[i]!
26
+ if (
27
+ !isFreeNode({ node: sourceNode }) ||
28
+ sourceNode.availableZ.length !== 1
29
+ ) {
30
+ continue
31
+ }
32
+
33
+ for (let j = 0; j < nodes.length; j++) {
34
+ if (i === j) continue
35
+
36
+ const targetNode = nodes[j]!
37
+ if (!isFreeNode({ node: targetNode })) continue
38
+
39
+ const unionZ = getUnionZ({
40
+ a: sourceNode.availableZ,
41
+ b: targetNode.availableZ,
42
+ })
43
+ if (unionZ.length <= targetNode.availableZ.length) continue
44
+ if (!hasContiguousZSpan({ zValues: unionZ })) continue
45
+
46
+ const overlapRect = intersectRects({
47
+ a: nodeToRect({ node: sourceNode }),
48
+ b: nodeToRect({ node: targetNode }),
49
+ })
50
+ if (!overlapRect) continue
51
+ if (
52
+ overlapRect.width + EPS < minRectSize ||
53
+ overlapRect.height + EPS < minRectSize
54
+ ) {
55
+ continue
56
+ }
57
+
58
+ const area = rectArea({ rect: overlapRect })
59
+ if (!best || area > best.area) {
60
+ best = {
61
+ rect: overlapRect,
62
+ sourceNode,
63
+ targetNode,
64
+ unionZ,
65
+ area,
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ return best
72
+ }
@@ -0,0 +1,34 @@
1
+ import { EPS } from "../../utils/rectdiff-geometry"
2
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
3
+
4
+ /**
5
+ * Measure how much usable space is shared across multiple layers.
6
+ * Obstacle-only space is not counted.
7
+ */
8
+ export const getUsableMultilayerVolumeShare = ({
9
+ nodes,
10
+ }: {
11
+ nodes: CapacityMeshNode[]
12
+ }) => {
13
+ let totalVolume = 0
14
+ let obstacleVolume = 0
15
+ let multilayerVolume = 0
16
+
17
+ for (const node of nodes) {
18
+ const volume = node.width * node.height * node.availableZ.length
19
+ totalVolume += volume
20
+
21
+ if (node._containsObstacle) {
22
+ obstacleVolume += volume
23
+ continue
24
+ }
25
+
26
+ if (node.availableZ.length > 1) {
27
+ multilayerVolume += volume
28
+ }
29
+ }
30
+
31
+ const usableVolume = totalVolume - obstacleVolume
32
+ if (usableVolume <= EPS) return 0
33
+ return multilayerVolume / usableVolume
34
+ }
@@ -0,0 +1,8 @@
1
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
2
+
3
+ /**
4
+ * Check whether a node is plain free space.
5
+ * Obstacles and targets are excluded.
6
+ */
7
+ export const isFreeNode = ({ node }: { node: CapacityMeshNode }) =>
8
+ !node._containsObstacle && !node._containsTarget
@@ -0,0 +1,13 @@
1
+ import type { XYRect } from "../../rectdiff-types"
2
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
3
+
4
+ /**
5
+ * Convert a node into a rectangle.
6
+ * This keeps geometry code simple.
7
+ */
8
+ export const nodeToRect = ({ node }: { node: CapacityMeshNode }): XYRect => ({
9
+ x: node.center.x - node.width / 2,
10
+ y: node.center.y - node.height / 2,
11
+ width: node.width,
12
+ height: node.height,
13
+ })
@@ -0,0 +1,104 @@
1
+ import { BaseSolver } from "@tscircuit/solver-utils"
2
+ import type { GraphicsObject } from "graphics-debug"
3
+ import type {
4
+ CapacityMeshNode,
5
+ CapacityMeshNodeId,
6
+ } from "../../../types/capacity-mesh-types"
7
+ import { getZLayerName } from "../../../math/layers/getZLayerName"
8
+ import { getColorForZLayer } from "../../../utils/getColorForZLayer"
9
+ import { cloneNode } from "../cloneNode"
10
+ import { cloneNodeWithRect } from "../cloneNodeWithRect"
11
+ import { findBestCoalesceCandidate } from "../findBestCoalesceCandidate"
12
+
13
+ type CoalesceMultilayerTilesSolverInput = {
14
+ meshNodes: CapacityMeshNode[]
15
+ }
16
+
17
+ /**
18
+ * Merge small shared tiles into larger regions.
19
+ * It runs until no useful merge remains.
20
+ */
21
+ export class CoalesceMultilayerTilesSolver extends BaseSolver {
22
+ private nextMergedId = 0
23
+ private outputNodes: CapacityMeshNode[] = []
24
+ private promotedNodeIds = new Set<CapacityMeshNodeId>()
25
+ private workingNodes: CapacityMeshNode[] = []
26
+
27
+ constructor(private input: CoalesceMultilayerTilesSolverInput) {
28
+ super()
29
+ }
30
+
31
+ override _setup() {
32
+ this.nextMergedId = 0
33
+ this.promotedNodeIds.clear()
34
+ this.workingNodes = this.input.meshNodes.map((node) => cloneNode({ node }))
35
+ this.outputNodes = [...this.workingNodes]
36
+ }
37
+
38
+ override _step() {
39
+ const candidate = findBestCoalesceCandidate({ nodes: this.workingNodes })
40
+ if (!candidate) {
41
+ this.outputNodes = [...this.workingNodes]
42
+ this.solved = true
43
+ return
44
+ }
45
+
46
+ const absorbedNodeIds = new Set<CapacityMeshNodeId>(
47
+ candidate.absorbedNodes.map((node) => node.capacityMeshNodeId),
48
+ )
49
+ const templateNode = this.workingNodes.find((node) =>
50
+ absorbedNodeIds.has(node.capacityMeshNodeId),
51
+ )
52
+ if (!templateNode) {
53
+ this.outputNodes = [...this.workingNodes]
54
+ this.solved = true
55
+ return
56
+ }
57
+
58
+ const mergedNode = cloneNodeWithRect({
59
+ templateNode,
60
+ rect: candidate.rect,
61
+ capacityMeshNodeId: `sparse-coalesced-${this.nextMergedId++}`,
62
+ })
63
+ this.promotedNodeIds.add(mergedNode.capacityMeshNodeId)
64
+
65
+ this.workingNodes = [
66
+ ...this.workingNodes.filter(
67
+ (node) => !absorbedNodeIds.has(node.capacityMeshNodeId),
68
+ ),
69
+ mergedNode,
70
+ ]
71
+
72
+ this.outputNodes = [...this.workingNodes]
73
+ }
74
+
75
+ override getOutput() {
76
+ return {
77
+ outputNodes: this.outputNodes,
78
+ promotedNodeIds: this.promotedNodeIds,
79
+ }
80
+ }
81
+
82
+ override visualize(): GraphicsObject {
83
+ return {
84
+ title: "CoalesceMultilayerTilesSolver",
85
+ coordinateSystem: "cartesian",
86
+ rects: this.outputNodes.map((node) => {
87
+ const colors = getColorForZLayer(node.availableZ)
88
+ const isPromoted = this.promotedNodeIds.has(node.capacityMeshNodeId)
89
+
90
+ return {
91
+ center: node.center,
92
+ width: node.width,
93
+ height: node.height,
94
+ stroke: isPromoted ? "rgba(168, 85, 247, 0.95)" : colors.stroke,
95
+ fill: isPromoted ? "rgba(192, 132, 252, 0.28)" : colors.fill,
96
+ layer: getZLayerName({ availableZ: node.availableZ }),
97
+ }
98
+ }),
99
+ points: [],
100
+ lines: [],
101
+ texts: [],
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,148 @@
1
+ import { BaseSolver } from "@tscircuit/solver-utils"
2
+ import type { GraphicsObject } from "graphics-debug"
3
+ import type {
4
+ CapacityMeshNode,
5
+ CapacityMeshNodeId,
6
+ } from "../../../types/capacity-mesh-types"
7
+ import { getZLayerName } from "../../../math/layers/getZLayerName"
8
+ import { getColorForZLayer } from "../../../utils/getColorForZLayer"
9
+ import { cloneNode } from "../cloneNode"
10
+ import { cloneNodeWithRect } from "../cloneNodeWithRect"
11
+ import { createResidualNodes } from "../createResidualNodes"
12
+ import { findBestPromotionCandidate } from "../findBestPromotionCandidate"
13
+ import { getUsableMultilayerVolumeShare } from "../getUsableMultilayerVolumeShare"
14
+
15
+ type PromoteSparseMultilayerCoverageSolverInput = {
16
+ meshNodes: CapacityMeshNode[]
17
+ minRectSize: number
18
+ promotionTargetShare: number
19
+ }
20
+
21
+ /**
22
+ * Turn overlapping single-layer space into shared space.
23
+ * It runs until the configured shared-space threshold is reached.
24
+ */
25
+ export class PromoteSparseMultilayerCoverageSolver extends BaseSolver {
26
+ private nextMergedId = 0
27
+ private nextResidualId = 0
28
+ private outputNodes: CapacityMeshNode[] = []
29
+ private promotedNodeIds = new Set<CapacityMeshNodeId>()
30
+ private residualNodeIds = new Set<CapacityMeshNodeId>()
31
+ private workingNodes: CapacityMeshNode[] = []
32
+
33
+ constructor(private input: PromoteSparseMultilayerCoverageSolverInput) {
34
+ super()
35
+ }
36
+
37
+ override _setup() {
38
+ this.nextMergedId = 0
39
+ this.nextResidualId = 0
40
+ this.promotedNodeIds.clear()
41
+ this.residualNodeIds.clear()
42
+ this.workingNodes = this.input.meshNodes.map((node) => cloneNode({ node }))
43
+ this.outputNodes = [...this.workingNodes]
44
+ }
45
+
46
+ override _step() {
47
+ if (
48
+ getUsableMultilayerVolumeShare({ nodes: this.workingNodes }) >=
49
+ this.input.promotionTargetShare
50
+ ) {
51
+ this.outputNodes = [...this.workingNodes]
52
+ this.solved = true
53
+ return
54
+ }
55
+
56
+ const candidate = findBestPromotionCandidate({
57
+ minRectSize: this.input.minRectSize,
58
+ nodes: this.workingNodes,
59
+ })
60
+ if (!candidate) {
61
+ this.outputNodes = [...this.workingNodes]
62
+ this.solved = true
63
+ return
64
+ }
65
+
66
+ const { sourceNode, targetNode } = candidate
67
+ if (!sourceNode || !targetNode) {
68
+ this.outputNodes = [...this.workingNodes]
69
+ this.solved = true
70
+ return
71
+ }
72
+
73
+ const mergedNode = cloneNodeWithRect({
74
+ templateNode: sourceNode,
75
+ rect: candidate.rect,
76
+ capacityMeshNodeId: `sparse-multilayer-merge-${this.nextMergedId++}`,
77
+ availableZ: candidate.unionZ,
78
+ })
79
+ this.promotedNodeIds.add(mergedNode.capacityMeshNodeId)
80
+
81
+ this.workingNodes = [
82
+ ...this.workingNodes.filter(
83
+ (node) =>
84
+ node.capacityMeshNodeId !== sourceNode.capacityMeshNodeId &&
85
+ node.capacityMeshNodeId !== targetNode.capacityMeshNodeId,
86
+ ),
87
+ mergedNode,
88
+ ...createResidualNodes({
89
+ cutRect: candidate.rect,
90
+ getNextResidualId: () => this.nextResidualId++,
91
+ idPrefix: `${sourceNode.capacityMeshNodeId}-sparse-residual`,
92
+ node: sourceNode,
93
+ onResidualNodeIdCreated: (nodeId) => this.residualNodeIds.add(nodeId),
94
+ }),
95
+ ...createResidualNodes({
96
+ cutRect: candidate.rect,
97
+ getNextResidualId: () => this.nextResidualId++,
98
+ idPrefix: `${targetNode.capacityMeshNodeId}-sparse-residual`,
99
+ node: targetNode,
100
+ onResidualNodeIdCreated: (nodeId) => this.residualNodeIds.add(nodeId),
101
+ }),
102
+ ]
103
+
104
+ this.outputNodes = [...this.workingNodes]
105
+ }
106
+
107
+ override getOutput() {
108
+ return {
109
+ outputNodes: this.outputNodes,
110
+ promotedNodeIds: this.promotedNodeIds,
111
+ residualNodeIds: this.residualNodeIds,
112
+ }
113
+ }
114
+
115
+ override visualize(): GraphicsObject {
116
+ return {
117
+ title: "PromoteSparseMultilayerCoverageSolver",
118
+ coordinateSystem: "cartesian",
119
+ rects: this.outputNodes.map((node) => {
120
+ const colors = getColorForZLayer(node.availableZ)
121
+ const isPromoted = this.promotedNodeIds.has(node.capacityMeshNodeId)
122
+ const isResidual = this.residualNodeIds.has(node.capacityMeshNodeId)
123
+
124
+ return {
125
+ center: node.center,
126
+ width: node.width,
127
+ height: node.height,
128
+ stroke: isPromoted
129
+ ? "rgba(168, 85, 247, 0.95)"
130
+ : isResidual
131
+ ? "rgba(14, 116, 144, 0.95)"
132
+ : colors.stroke,
133
+ fill: node._containsObstacle
134
+ ? "rgba(239, 68, 68, 0.35)"
135
+ : isPromoted
136
+ ? "rgba(192, 132, 252, 0.28)"
137
+ : isResidual
138
+ ? "rgba(34, 211, 238, 0.18)"
139
+ : colors.fill,
140
+ layer: getZLayerName({ availableZ: node.availableZ }),
141
+ }
142
+ }),
143
+ points: [],
144
+ lines: [],
145
+ texts: [],
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,137 @@
1
+ import { BaseSolver } from "@tscircuit/solver-utils"
2
+ import type { GraphicsObject } from "graphics-debug"
3
+ import type {
4
+ CapacityMeshNode,
5
+ CapacityMeshNodeId,
6
+ } from "../../../types/capacity-mesh-types"
7
+ import { getZLayerName } from "../../../math/layers/getZLayerName"
8
+ import { EPS } from "../../../utils/rectdiff-geometry"
9
+ import { cloneNode } from "../cloneNode"
10
+ import { cloneNodeWithRect } from "../cloneNodeWithRect"
11
+ import { getUsableMultilayerVolumeShare } from "../getUsableMultilayerVolumeShare"
12
+ import { isFreeNode } from "../isFreeNode"
13
+ import { nodeToRect } from "../nodeToRect"
14
+ import { getColorForZLayer } from "../../../utils/getColorForZLayer"
15
+ import { subtractRects } from "../../../math/rects/subtractRects"
16
+
17
+ type TrimContainedSingleLayerCoverageSolverInput = {
18
+ meshNodes: CapacityMeshNode[]
19
+ minRectSize: number
20
+ promotionTargetShare: number
21
+ }
22
+
23
+ /**
24
+ * Remove single-layer pieces that are already covered by shared space.
25
+ * This keeps the result smaller and simpler.
26
+ */
27
+ export class TrimContainedSingleLayerCoverageSolver extends BaseSolver {
28
+ private nextResidualId = 0
29
+ private outputNodes: CapacityMeshNode[] = []
30
+ private residualNodeIds = new Set<CapacityMeshNodeId>()
31
+
32
+ constructor(private input: TrimContainedSingleLayerCoverageSolverInput) {
33
+ super()
34
+ }
35
+
36
+ override _setup() {
37
+ this.nextResidualId = 0
38
+ this.residualNodeIds.clear()
39
+ this.outputNodes = this.input.meshNodes.map((node) => cloneNode({ node }))
40
+ }
41
+
42
+ override _step() {
43
+ if (
44
+ getUsableMultilayerVolumeShare({ nodes: this.outputNodes }) >=
45
+ this.input.promotionTargetShare
46
+ ) {
47
+ this.solved = true
48
+ return
49
+ }
50
+
51
+ const freeMultilayerNodes = this.outputNodes.filter(
52
+ (node) => isFreeNode({ node }) && node.availableZ.length > 1,
53
+ )
54
+ const nextNodes: CapacityMeshNode[] = []
55
+
56
+ for (const node of this.outputNodes) {
57
+ if (!isFreeNode({ node }) || node.availableZ.length !== 1) {
58
+ nextNodes.push(node)
59
+ continue
60
+ }
61
+
62
+ const z = node.availableZ[0]!
63
+ const coveringRects = freeMultilayerNodes
64
+ .filter((candidate) => candidate.availableZ.includes(z))
65
+ .map((candidate) => nodeToRect({ node: candidate }))
66
+
67
+ if (coveringRects.length === 0) {
68
+ nextNodes.push(node)
69
+ continue
70
+ }
71
+
72
+ const nodeRect = nodeToRect({ node })
73
+ const residuals = subtractRects({
74
+ target: nodeRect,
75
+ cutters: coveringRects,
76
+ }).filter(
77
+ (rect) =>
78
+ rect.width + EPS >= this.input.minRectSize &&
79
+ rect.height + EPS >= this.input.minRectSize,
80
+ )
81
+
82
+ if (
83
+ residuals.length === 1 &&
84
+ Math.abs(residuals[0]!.x - nodeRect.x) <= EPS &&
85
+ Math.abs(residuals[0]!.y - nodeRect.y) <= EPS &&
86
+ Math.abs(residuals[0]!.width - nodeRect.width) <= EPS &&
87
+ Math.abs(residuals[0]!.height - nodeRect.height) <= EPS
88
+ ) {
89
+ nextNodes.push(node)
90
+ continue
91
+ }
92
+
93
+ for (const rect of residuals) {
94
+ const residualNode = cloneNodeWithRect({
95
+ templateNode: node,
96
+ rect,
97
+ capacityMeshNodeId: `${node.capacityMeshNodeId}-contained-residual-${this.nextResidualId++}`,
98
+ })
99
+ this.residualNodeIds.add(residualNode.capacityMeshNodeId)
100
+ nextNodes.push(residualNode)
101
+ }
102
+ }
103
+
104
+ this.outputNodes = nextNodes
105
+ this.solved = true
106
+ }
107
+
108
+ override getOutput() {
109
+ return {
110
+ outputNodes: this.outputNodes,
111
+ residualNodeIds: this.residualNodeIds,
112
+ }
113
+ }
114
+
115
+ override visualize(): GraphicsObject {
116
+ return {
117
+ title: "TrimContainedSingleLayerCoverageSolver",
118
+ coordinateSystem: "cartesian",
119
+ rects: this.outputNodes.map((node) => {
120
+ const colors = getColorForZLayer(node.availableZ)
121
+ const isResidual = this.residualNodeIds.has(node.capacityMeshNodeId)
122
+
123
+ return {
124
+ center: node.center,
125
+ width: node.width,
126
+ height: node.height,
127
+ stroke: isResidual ? "rgba(14, 116, 144, 0.95)" : colors.stroke,
128
+ fill: isResidual ? "rgba(34, 211, 238, 0.18)" : colors.fill,
129
+ layer: getZLayerName({ availableZ: node.availableZ }),
130
+ }
131
+ }),
132
+ points: [],
133
+ lines: [],
134
+ texts: [],
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,23 @@
1
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
2
+ import type { XYRect } from "../../rectdiff-types"
3
+ import type { SimpleRouteJson } from "../../types/srj-types"
4
+
5
+ export type SparseMultilayerPromotionInput = {
6
+ meshNodes: CapacityMeshNode[]
7
+ promotionTargetShare: number
8
+ simpleRouteJson: SimpleRouteJson
9
+ }
10
+
11
+ export type PromotionCandidate = {
12
+ rect: XYRect
13
+ sourceNode: CapacityMeshNode
14
+ targetNode: CapacityMeshNode
15
+ unionZ: number[]
16
+ area: number
17
+ }
18
+
19
+ export type CoalesceCandidate = {
20
+ rect: XYRect
21
+ absorbedNodes: CapacityMeshNode[]
22
+ score: number
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/rectdiff",
3
- "version": "0.0.38",
3
+ "version": "0.0.39",
4
4
  "type": "module",
5
5
  "main": "lib/index.ts",
6
6
  "types": "lib/index.ts",