@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.
- package/.github/workflows/bun-test.yml +1 -1
- package/lib/RectDiffPipeline.ts +27 -0
- package/lib/math/layers/getUnionZ.ts +6 -0
- package/lib/math/layers/getZLayerName.ts +6 -0
- package/lib/math/layers/getZSpanMask.ts +6 -0
- package/lib/math/layers/hasContiguousZSpan.ts +11 -0
- package/lib/math/rects/intersectRects.ts +28 -0
- package/lib/math/rects/mergeRects.ts +12 -0
- package/lib/math/rects/rectArea.ts +7 -0
- package/lib/math/rects/rectContainsRect.ts +18 -0
- package/lib/math/rects/rectsTouchOrOverlap.ts +12 -0
- package/lib/math/rects/subtractRects.ts +23 -0
- package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +108 -4
- package/lib/solvers/SparseMultilayerPromotionSolver/SparseMultilayerPromotionSolver.ts +134 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/cloneNode.ts +15 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/cloneNodeWithRect.ts +34 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/createResidualNodes.ts +42 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/findBestCoalesceCandidate.ts +98 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/findBestPromotionCandidate.ts +72 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/getUsableMultilayerVolumeShare.ts +34 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/isFreeNode.ts +8 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/nodeToRect.ts +13 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/solvers/CoalesceMultilayerTilesSolver.ts +104 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/solvers/PromoteSparseMultilayerCoverageSolver.ts +148 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/solvers/TrimContainedSingleLayerCoverageSolver.ts +137 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/types.ts +23 -0
- package/package.json +1 -1
- package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +3 -3
- package/tests/solver/arduino-uno-inner2-ground-bottom-power/__snapshots__/arduino-uno-inner2-ground-bottom-power.snap.svg +3 -3
- package/tests/solver/arduino-uno-inner2-ground-inner1-power/__snapshots__/arduino-uno-inner2-ground-inner1-power.snap.svg +1 -1
- package/tests/solver/both-points-equivalent/__snapshots__/both-points-equivalent.snap.svg +1 -1
- package/tests/solver/bugreport02-bc4361/__snapshots__/bugreport02-bc4361.snap.svg +3 -3
- package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
- package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
- package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +1 -1
- package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +3 -3
- package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +3 -3
- package/tests/solver/bugreport13-b9a758/__snapshots__/bugreport13-b9a758.snap.svg +2 -2
- package/tests/solver/bugreport16-d95f38/__snapshots__/bugreport16-d95f38.snap.svg +1 -1
- package/tests/solver/bugreport18-1b2d06/__snapshots__/bugreport18-1b2d06.snap.svg +3 -3
- package/tests/solver/bugreport19/__snapshots__/bugreport19.snap.svg +1 -1
- package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
- package/tests/solver/bugreport23-LGA15x4/__snapshots__/bugreport23-LGA15x4.snap.svg +2 -2
- package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
- package/tests/solver/bugreport25-4b1d55/__snapshots__/bugreport25-4b1d55.snap.svg +3 -3
- package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +2 -2
- package/tests/solver/interaction/__snapshots__/interaction.snap.svg +3 -3
- package/tests/solver/multi-point/__snapshots__/multi-point.snap.svg +1 -1
- package/tests/solver/no-better-path/__snapshots__/no-better-path.snap.svg +1 -1
- package/tests/solver/offboardconnects01/__snapshots__/offboardconnects01.snap.svg +1 -1
- package/tests/solver/pcb_trace_id-should-return-root-connection-name/__snapshots__/pcb_trace_id-should-return-root-connection-name.snap.svg +3 -3
- package/tests/solver/repros/merge-single-layer-node/__snapshots__/merge-single-layer-node.snap.svg +3 -3
- 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
|
+
}
|
package/lib/solvers/SparseMultilayerPromotionSolver/solvers/PromoteSparseMultilayerCoverageSolver.ts
ADDED
|
@@ -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
|
+
}
|