@tscircuit/rectdiff 0.0.9 → 0.0.11
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/dist/index.d.ts +97 -12
- package/dist/index.js +714 -81
- package/lib/RectDiffPipeline.ts +79 -13
- package/lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts +284 -0
- package/lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts +213 -0
- package/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts +129 -0
- package/lib/solvers/GapFillSolver/edge-constants.ts +48 -0
- package/lib/solvers/GapFillSolver/getBoundsFromCorners.ts +10 -0
- package/lib/solvers/GapFillSolver/projectToUncoveredSegments.ts +92 -0
- package/lib/solvers/GapFillSolver/visuallyOffsetLine.ts +32 -0
- package/lib/solvers/RectDiffSolver.ts +1 -33
- package/package.json +9 -6
- package/tests/board-outline.test.ts +1 -1
- package/tsconfig.json +4 -0
- package/vite.config.ts +6 -0
- package/lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts +0 -28
- package/lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts +0 -83
- package/lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts +0 -100
- package/lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts +0 -75
- package/lib/solvers/rectdiff/gapfill/detection.ts +0 -3
- package/lib/solvers/rectdiff/gapfill/engine/addPlacement.ts +0 -27
- package/lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts +0 -44
- package/lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts +0 -43
- package/lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts +0 -42
- package/lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts +0 -57
- package/lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts +0 -128
- package/lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts +0 -78
- package/lib/solvers/rectdiff/gapfill/engine.ts +0 -7
- package/lib/solvers/rectdiff/gapfill/types.ts +0 -60
- package/lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts +0 -253
package/lib/RectDiffPipeline.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BasePipelineSolver,
|
|
3
|
+
definePipelineStep,
|
|
4
|
+
type PipelineStep,
|
|
5
|
+
} from "@tscircuit/solver-utils"
|
|
2
6
|
import type { SimpleRouteJson } from "./types/srj-types"
|
|
3
7
|
import type { GridFill3DOptions } from "./solvers/rectdiff/types"
|
|
4
8
|
import { RectDiffSolver } from "./solvers/RectDiffSolver"
|
|
5
9
|
import type { CapacityMeshNode } from "./types/capacity-mesh-types"
|
|
6
10
|
import type { GraphicsObject } from "graphics-debug"
|
|
7
11
|
import { createBaseVisualization } from "./solvers/rectdiff/visualization"
|
|
12
|
+
import { GapFillSolverPipeline } from "./solvers/GapFillSolver/GapFillSolverPipeline"
|
|
8
13
|
|
|
9
14
|
export interface RectDiffPipelineInput {
|
|
10
15
|
simpleRouteJson: SimpleRouteJson
|
|
@@ -13,15 +18,16 @@ export interface RectDiffPipelineInput {
|
|
|
13
18
|
|
|
14
19
|
export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput> {
|
|
15
20
|
rectDiffSolver?: RectDiffSolver
|
|
21
|
+
gapFillSolver?: GapFillSolverPipeline
|
|
16
22
|
|
|
17
|
-
override pipelineDef = [
|
|
23
|
+
override pipelineDef: PipelineStep<any>[] = [
|
|
18
24
|
definePipelineStep(
|
|
19
25
|
"rectDiffSolver",
|
|
20
26
|
RectDiffSolver,
|
|
21
|
-
(
|
|
27
|
+
(rectDiffPipeline) => [
|
|
22
28
|
{
|
|
23
|
-
simpleRouteJson:
|
|
24
|
-
gridOptions:
|
|
29
|
+
simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson,
|
|
30
|
+
gridOptions: rectDiffPipeline.inputProblem.gridOptions,
|
|
25
31
|
},
|
|
26
32
|
],
|
|
27
33
|
{
|
|
@@ -30,6 +36,16 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
30
36
|
},
|
|
31
37
|
},
|
|
32
38
|
),
|
|
39
|
+
definePipelineStep(
|
|
40
|
+
"gapFillSolver",
|
|
41
|
+
GapFillSolverPipeline,
|
|
42
|
+
(rectDiffPipeline: RectDiffPipeline) => [
|
|
43
|
+
{
|
|
44
|
+
meshNodes:
|
|
45
|
+
rectDiffPipeline.rectDiffSolver?.getOutput().meshNodes ?? [],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
),
|
|
33
49
|
]
|
|
34
50
|
|
|
35
51
|
override getConstructorParams() {
|
|
@@ -37,19 +53,69 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
override getOutput(): { meshNodes: CapacityMeshNode[] } {
|
|
40
|
-
|
|
56
|
+
const gapFillOutput = this.gapFillSolver?.getOutput()
|
|
57
|
+
if (gapFillOutput) {
|
|
58
|
+
return { meshNodes: gapFillOutput.outputNodes }
|
|
59
|
+
}
|
|
60
|
+
return this.rectDiffSolver!.getOutput()
|
|
41
61
|
}
|
|
42
62
|
|
|
43
|
-
override
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
override initialVisualize(): GraphicsObject {
|
|
64
|
+
console.log("RectDiffPipeline - initialVisualize")
|
|
65
|
+
const graphics = createBaseVisualization(
|
|
66
|
+
this.inputProblem.simpleRouteJson,
|
|
67
|
+
"RectDiffPipeline - Initial",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// Show initial mesh nodes from rectDiffSolver if available
|
|
71
|
+
const initialNodes = this.rectDiffSolver?.getOutput().meshNodes ?? []
|
|
72
|
+
for (const node of initialNodes) {
|
|
73
|
+
graphics.rects!.push({
|
|
74
|
+
center: node.center,
|
|
75
|
+
width: node.width,
|
|
76
|
+
height: node.height,
|
|
77
|
+
stroke: "rgba(0, 0, 0, 0.3)",
|
|
78
|
+
fill: "rgba(100, 100, 100, 0.1)",
|
|
79
|
+
layer: `z${node.availableZ.join(",")}`,
|
|
80
|
+
label: [
|
|
81
|
+
`node ${node.capacityMeshNodeId}`,
|
|
82
|
+
`z:${node.availableZ.join(",")}`,
|
|
83
|
+
].join("\n"),
|
|
84
|
+
})
|
|
47
85
|
}
|
|
48
86
|
|
|
49
|
-
|
|
50
|
-
|
|
87
|
+
return graphics
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
override finalVisualize(): GraphicsObject {
|
|
91
|
+
const graphics = createBaseVisualization(
|
|
51
92
|
this.inputProblem.simpleRouteJson,
|
|
52
|
-
"
|
|
93
|
+
"RectDiffPipeline - Final",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const { meshNodes: outputNodes } = this.getOutput()
|
|
97
|
+
const initialNodeIds = new Set(
|
|
98
|
+
(this.rectDiffSolver?.getOutput().meshNodes ?? []).map(
|
|
99
|
+
(n) => n.capacityMeshNodeId,
|
|
100
|
+
),
|
|
53
101
|
)
|
|
102
|
+
|
|
103
|
+
for (const node of outputNodes) {
|
|
104
|
+
const isExpanded = !initialNodeIds.has(node.capacityMeshNodeId)
|
|
105
|
+
graphics.rects!.push({
|
|
106
|
+
center: node.center,
|
|
107
|
+
width: node.width,
|
|
108
|
+
height: node.height,
|
|
109
|
+
stroke: isExpanded ? "rgba(0, 128, 0, 0.8)" : "rgba(0, 0, 0, 0.3)",
|
|
110
|
+
fill: isExpanded ? "rgba(0, 200, 0, 0.3)" : "rgba(100, 100, 100, 0.1)",
|
|
111
|
+
layer: `z${node.availableZ.join(",")}`,
|
|
112
|
+
label: [
|
|
113
|
+
`${isExpanded ? "[expanded] " : ""}node ${node.capacityMeshNodeId}`,
|
|
114
|
+
`z:${node.availableZ.join(",")}`,
|
|
115
|
+
].join("\n"),
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return graphics
|
|
54
120
|
}
|
|
55
121
|
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { BaseSolver } from "@tscircuit/solver-utils"
|
|
2
|
+
import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
|
|
3
|
+
import type { SegmentWithAdjacentEmptySpace } from "./FindSegmentsWithAdjacentEmptySpaceSolver"
|
|
4
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
5
|
+
import RBush from "rbush"
|
|
6
|
+
import { EDGE_MAP, EDGES } from "./edge-constants"
|
|
7
|
+
import { getBoundsFromCorners } from "./getBoundsFromCorners"
|
|
8
|
+
import type { Bounds } from "@tscircuit/math-utils"
|
|
9
|
+
import { midpoint, segmentToBoxMinDistance } from "@tscircuit/math-utils"
|
|
10
|
+
|
|
11
|
+
const EPS = 1e-4
|
|
12
|
+
|
|
13
|
+
export interface ExpandedSegment {
|
|
14
|
+
segment: SegmentWithAdjacentEmptySpace
|
|
15
|
+
newNode: CapacityMeshNode
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ExpandEdgesToEmptySpaceSolver extends BaseSolver {
|
|
19
|
+
unprocessedSegments: Array<SegmentWithAdjacentEmptySpace> = []
|
|
20
|
+
expandedSegments: Array<ExpandedSegment> = []
|
|
21
|
+
|
|
22
|
+
lastSegment: SegmentWithAdjacentEmptySpace | null = null
|
|
23
|
+
lastSearchBounds: Bounds | null = null
|
|
24
|
+
lastCollidingNodes: CapacityMeshNode[] | null = null
|
|
25
|
+
lastSearchCorner1: { x: number; y: number } | null = null
|
|
26
|
+
lastSearchCorner2: { x: number; y: number } | null = null
|
|
27
|
+
lastExpandedSegment: ExpandedSegment | null = null
|
|
28
|
+
|
|
29
|
+
rectSpatialIndex: RBush<CapacityMeshNode>
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
private input: {
|
|
33
|
+
inputMeshNodes: CapacityMeshNode[]
|
|
34
|
+
segmentsWithAdjacentEmptySpace: Array<SegmentWithAdjacentEmptySpace>
|
|
35
|
+
},
|
|
36
|
+
) {
|
|
37
|
+
super()
|
|
38
|
+
this.unprocessedSegments = [...this.input.segmentsWithAdjacentEmptySpace]
|
|
39
|
+
this.rectSpatialIndex = new RBush<CapacityMeshNode>()
|
|
40
|
+
this.rectSpatialIndex.load(
|
|
41
|
+
this.input.inputMeshNodes.map((n) => ({
|
|
42
|
+
...n,
|
|
43
|
+
minX: n.center.x - n.width / 2,
|
|
44
|
+
minY: n.center.y - n.height / 2,
|
|
45
|
+
maxX: n.center.x + n.width / 2,
|
|
46
|
+
maxY: n.center.y + n.height / 2,
|
|
47
|
+
})),
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override _step() {
|
|
52
|
+
if (this.unprocessedSegments.length === 0) {
|
|
53
|
+
this.solved = true
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const segment = this.unprocessedSegments.shift()!
|
|
58
|
+
this.lastSegment = segment
|
|
59
|
+
|
|
60
|
+
const { dx, dy } = EDGE_MAP[segment.facingDirection]
|
|
61
|
+
|
|
62
|
+
// Determine the largest empty space that can be created by creating a rect
|
|
63
|
+
// that grows in segment.facingDirection by progressively expanding the
|
|
64
|
+
// bounds that we search for empty space. As soon as any rect appears in our
|
|
65
|
+
// bounds we know the maximum size of the empty space that can be created.
|
|
66
|
+
|
|
67
|
+
const deltaStartEnd = {
|
|
68
|
+
x: segment.end.x - segment.start.x,
|
|
69
|
+
y: segment.end.y - segment.start.y,
|
|
70
|
+
}
|
|
71
|
+
const segLength = Math.sqrt(deltaStartEnd.x ** 2 + deltaStartEnd.y ** 2)
|
|
72
|
+
const normDeltaStartEnd = {
|
|
73
|
+
x: deltaStartEnd.x / segLength,
|
|
74
|
+
y: deltaStartEnd.y / segLength,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let collidingNodes: CapacityMeshNode[] | null = null
|
|
78
|
+
let searchDistance = 1
|
|
79
|
+
const searchCorner1 = {
|
|
80
|
+
x: segment.start.x + dx * EPS + normDeltaStartEnd.x * EPS * 10,
|
|
81
|
+
y: segment.start.y + dy * EPS + normDeltaStartEnd.y * EPS * 10,
|
|
82
|
+
}
|
|
83
|
+
const searchCorner2 = {
|
|
84
|
+
x: segment.end.x + dx * EPS - normDeltaStartEnd.x * EPS * 10,
|
|
85
|
+
y: segment.end.y + dy * EPS - normDeltaStartEnd.y * EPS * 10,
|
|
86
|
+
}
|
|
87
|
+
this.lastSearchCorner1 = searchCorner1
|
|
88
|
+
this.lastSearchCorner2 = searchCorner2
|
|
89
|
+
while (
|
|
90
|
+
(!collidingNodes || collidingNodes.length === 0) &&
|
|
91
|
+
searchDistance < 1000
|
|
92
|
+
) {
|
|
93
|
+
const searchBounds = getBoundsFromCorners([
|
|
94
|
+
searchCorner1,
|
|
95
|
+
searchCorner2,
|
|
96
|
+
{
|
|
97
|
+
x: searchCorner1.x + dx * searchDistance,
|
|
98
|
+
y: searchCorner1.y + dy * searchDistance,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
x: searchCorner2.x + dx * searchDistance,
|
|
102
|
+
y: searchCorner2.y + dy * searchDistance,
|
|
103
|
+
},
|
|
104
|
+
])
|
|
105
|
+
this.lastSearchBounds = searchBounds
|
|
106
|
+
collidingNodes = this.rectSpatialIndex
|
|
107
|
+
.search(searchBounds)
|
|
108
|
+
.filter((n) => n.availableZ.includes(segment.z))
|
|
109
|
+
.filter(
|
|
110
|
+
(n) => n.capacityMeshNodeId !== segment.parent.capacityMeshNodeId,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
searchDistance *= 4
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!collidingNodes || collidingNodes.length === 0) {
|
|
117
|
+
// TODO, this means we need to expand the node to the boundary
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
this.lastCollidingNodes = collidingNodes
|
|
121
|
+
|
|
122
|
+
// Determine the expand distance from the colliding nodes
|
|
123
|
+
let smallestDistance = Infinity
|
|
124
|
+
for (const node of collidingNodes) {
|
|
125
|
+
const distance = segmentToBoxMinDistance(segment.start, segment.end, node)
|
|
126
|
+
if (distance < smallestDistance) {
|
|
127
|
+
smallestDistance = distance
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const expandDistance = smallestDistance
|
|
131
|
+
|
|
132
|
+
const nodeBounds = getBoundsFromCorners([
|
|
133
|
+
segment.start,
|
|
134
|
+
segment.end,
|
|
135
|
+
{
|
|
136
|
+
x: segment.start.x + dx * expandDistance,
|
|
137
|
+
y: segment.start.y + dy * expandDistance,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
x: segment.end.x + dx * expandDistance,
|
|
141
|
+
y: segment.end.y + dy * expandDistance,
|
|
142
|
+
},
|
|
143
|
+
])
|
|
144
|
+
const nodeCenter = {
|
|
145
|
+
x: (nodeBounds.minX + nodeBounds.maxX) / 2,
|
|
146
|
+
y: (nodeBounds.minY + nodeBounds.maxY) / 2,
|
|
147
|
+
}
|
|
148
|
+
const nodeWidth = nodeBounds.maxX - nodeBounds.minX
|
|
149
|
+
const nodeHeight = nodeBounds.maxY - nodeBounds.minY
|
|
150
|
+
|
|
151
|
+
const expandedSegment = {
|
|
152
|
+
segment,
|
|
153
|
+
newNode: {
|
|
154
|
+
capacityMeshNodeId: `new-${segment.parent.capacityMeshNodeId}-${this.expandedSegments.length}`,
|
|
155
|
+
center: nodeCenter,
|
|
156
|
+
width: nodeWidth,
|
|
157
|
+
height: nodeHeight,
|
|
158
|
+
availableZ: [segment.z],
|
|
159
|
+
layer: segment.parent.layer,
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
this.lastExpandedSegment = expandedSegment
|
|
163
|
+
|
|
164
|
+
if (nodeWidth < EPS || nodeHeight < EPS) {
|
|
165
|
+
// Node is too small, skipping
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.expandedSegments.push(expandedSegment)
|
|
170
|
+
this.rectSpatialIndex.insert({
|
|
171
|
+
...expandedSegment.newNode,
|
|
172
|
+
...nodeBounds,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
override getOutput() {
|
|
177
|
+
return {
|
|
178
|
+
expandedSegments: this.expandedSegments,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
override visualize() {
|
|
183
|
+
const graphics: Required<GraphicsObject> = {
|
|
184
|
+
title: "ExpandEdgesToEmptySpace",
|
|
185
|
+
coordinateSystem: "cartesian" as const,
|
|
186
|
+
rects: [],
|
|
187
|
+
points: [],
|
|
188
|
+
lines: [],
|
|
189
|
+
circles: [],
|
|
190
|
+
arrows: [],
|
|
191
|
+
texts: [],
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Draw capacity mesh nodes with gray, faded rects
|
|
195
|
+
for (const node of this.input.inputMeshNodes) {
|
|
196
|
+
graphics.rects.push({
|
|
197
|
+
center: node.center,
|
|
198
|
+
width: node.width,
|
|
199
|
+
height: node.height,
|
|
200
|
+
stroke: "rgba(0, 0, 0, 0.1)",
|
|
201
|
+
layer: `z${node.availableZ.join(",")}`,
|
|
202
|
+
label: [
|
|
203
|
+
`node ${node.capacityMeshNodeId}`,
|
|
204
|
+
`z:${node.availableZ.join(",")}`,
|
|
205
|
+
].join("\n"),
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// for (const segment of this.unprocessedSegments) {
|
|
210
|
+
// graphics.lines.push({
|
|
211
|
+
// points: [segment.start, segment.end],
|
|
212
|
+
// strokeColor: "rgba(0, 0, 255, 0.5)",
|
|
213
|
+
// })
|
|
214
|
+
// }
|
|
215
|
+
|
|
216
|
+
for (const { newNode } of this.expandedSegments) {
|
|
217
|
+
graphics.rects.push({
|
|
218
|
+
center: newNode.center,
|
|
219
|
+
width: newNode.width,
|
|
220
|
+
height: newNode.height,
|
|
221
|
+
fill: "green",
|
|
222
|
+
label: `expandedSegment (z=${newNode.availableZ.join(",")})`,
|
|
223
|
+
layer: `z${newNode.availableZ.join(",")}`,
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (this.lastSegment) {
|
|
228
|
+
graphics.lines.push({
|
|
229
|
+
points: [this.lastSegment.start, this.lastSegment.end],
|
|
230
|
+
strokeColor: "rgba(0, 0, 255, 0.5)",
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (this.lastSearchBounds) {
|
|
235
|
+
graphics.rects.push({
|
|
236
|
+
center: {
|
|
237
|
+
x: (this.lastSearchBounds.minX + this.lastSearchBounds.maxX) / 2,
|
|
238
|
+
y: (this.lastSearchBounds.minY + this.lastSearchBounds.maxY) / 2,
|
|
239
|
+
},
|
|
240
|
+
width: this.lastSearchBounds.maxX - this.lastSearchBounds.minX,
|
|
241
|
+
height: this.lastSearchBounds.maxY - this.lastSearchBounds.minY,
|
|
242
|
+
fill: "rgba(0, 0, 255, 0.25)",
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (this.lastSearchCorner1 && this.lastSearchCorner2) {
|
|
247
|
+
graphics.points.push({
|
|
248
|
+
x: this.lastSearchCorner1.x,
|
|
249
|
+
y: this.lastSearchCorner1.y,
|
|
250
|
+
color: "rgba(0, 0, 255, 0.5)",
|
|
251
|
+
label: ["searchCorner1", `z=${this.lastSegment?.z}`].join("\n"),
|
|
252
|
+
})
|
|
253
|
+
graphics.points.push({
|
|
254
|
+
x: this.lastSearchCorner2.x,
|
|
255
|
+
y: this.lastSearchCorner2.y,
|
|
256
|
+
color: "rgba(0, 0, 255, 0.5)",
|
|
257
|
+
label: ["searchCorner2", `z=${this.lastSegment?.z}`].join("\n"),
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (this.lastExpandedSegment) {
|
|
262
|
+
graphics.rects.push({
|
|
263
|
+
center: this.lastExpandedSegment.newNode.center,
|
|
264
|
+
width: this.lastExpandedSegment.newNode.width,
|
|
265
|
+
height: this.lastExpandedSegment.newNode.height,
|
|
266
|
+
fill: "purple",
|
|
267
|
+
label: `expandedSegment (z=${this.lastExpandedSegment.segment.z})`,
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (this.lastCollidingNodes) {
|
|
272
|
+
for (const node of this.lastCollidingNodes) {
|
|
273
|
+
graphics.rects.push({
|
|
274
|
+
center: node.center,
|
|
275
|
+
width: node.width,
|
|
276
|
+
height: node.height,
|
|
277
|
+
fill: "rgba(255, 0, 0, 0.5)",
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return graphics
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { BaseSolver } from "@tscircuit/solver-utils"
|
|
2
|
+
import Flatbush from "flatbush"
|
|
3
|
+
import type { GraphicsObject, NinePointAnchor } from "graphics-debug"
|
|
4
|
+
import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
|
|
5
|
+
import { projectToUncoveredSegments } from "./projectToUncoveredSegments"
|
|
6
|
+
import { EDGES } from "./edge-constants"
|
|
7
|
+
import { visuallyOffsetLine } from "./visuallyOffsetLine"
|
|
8
|
+
import { midpoint } from "@tscircuit/math-utils"
|
|
9
|
+
|
|
10
|
+
export interface SegmentWithAdjacentEmptySpace {
|
|
11
|
+
parent: CapacityMeshNode
|
|
12
|
+
start: { x: number; y: number }
|
|
13
|
+
end: { x: number; y: number }
|
|
14
|
+
z: number
|
|
15
|
+
facingDirection: "x+" | "x-" | "y+" | "y-"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const EPS = 1e-4
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Find edges with adjacent empty space in the mesh
|
|
22
|
+
*
|
|
23
|
+
* Do this by iterating over each edge of the rect (each step is one edge)
|
|
24
|
+
* and checking if the is completely covered by any other edge
|
|
25
|
+
*
|
|
26
|
+
* If it is completely covered, then it doesn't have an adjacent empty space,
|
|
27
|
+
* continue
|
|
28
|
+
*
|
|
29
|
+
* If it is partially uncovered, then divide it into uncovered segments and add
|
|
30
|
+
* each uncovered segment as a new edge with an adjacent empty space
|
|
31
|
+
*/
|
|
32
|
+
export class FindSegmentsWithAdjacentEmptySpaceSolver extends BaseSolver {
|
|
33
|
+
allEdges: Array<SegmentWithAdjacentEmptySpace>
|
|
34
|
+
unprocessedEdges: Array<SegmentWithAdjacentEmptySpace> = []
|
|
35
|
+
|
|
36
|
+
segmentsWithAdjacentEmptySpace: Array<SegmentWithAdjacentEmptySpace> = []
|
|
37
|
+
|
|
38
|
+
edgeSpatialIndex: Flatbush
|
|
39
|
+
|
|
40
|
+
lastCandidateEdge: SegmentWithAdjacentEmptySpace | null = null
|
|
41
|
+
lastOverlappingEdges: Array<SegmentWithAdjacentEmptySpace> | null = null
|
|
42
|
+
lastUncoveredSegments: Array<SegmentWithAdjacentEmptySpace> | null = null
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
private input: {
|
|
46
|
+
meshNodes: CapacityMeshNode[]
|
|
47
|
+
},
|
|
48
|
+
) {
|
|
49
|
+
super()
|
|
50
|
+
for (const node of this.input.meshNodes) {
|
|
51
|
+
for (const edge of EDGES) {
|
|
52
|
+
let start = {
|
|
53
|
+
x: node.center.x + node.width * edge.startX,
|
|
54
|
+
y: node.center.y + node.height * edge.startY,
|
|
55
|
+
}
|
|
56
|
+
let end = {
|
|
57
|
+
x: node.center.x + node.width * edge.endX,
|
|
58
|
+
y: node.center.y + node.height * edge.endY,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Ensure start.x < end.x and if x is the same, ensure start.y < end.y
|
|
62
|
+
if (start.x > end.x) {
|
|
63
|
+
;[start, end] = [end, start]
|
|
64
|
+
}
|
|
65
|
+
if (Math.abs(start.x - end.x) < EPS && start.y > end.y) {
|
|
66
|
+
;[start, end] = [end, start]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const z of node.availableZ) {
|
|
70
|
+
this.unprocessedEdges.push({
|
|
71
|
+
parent: node,
|
|
72
|
+
start,
|
|
73
|
+
end,
|
|
74
|
+
facingDirection: edge.facingDirection,
|
|
75
|
+
z,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
this.allEdges = [...this.unprocessedEdges]
|
|
81
|
+
|
|
82
|
+
this.edgeSpatialIndex = new Flatbush(this.allEdges.length)
|
|
83
|
+
for (const edge of this.allEdges) {
|
|
84
|
+
this.edgeSpatialIndex.add(
|
|
85
|
+
edge.start.x,
|
|
86
|
+
edge.start.y,
|
|
87
|
+
edge.end.x,
|
|
88
|
+
edge.end.y,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
this.edgeSpatialIndex.finish()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override _step() {
|
|
95
|
+
if (this.unprocessedEdges.length === 0) {
|
|
96
|
+
this.solved = true
|
|
97
|
+
this.lastCandidateEdge = null
|
|
98
|
+
this.lastOverlappingEdges = null
|
|
99
|
+
this.lastUncoveredSegments = null
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const candidateEdge = this.unprocessedEdges.shift()!
|
|
104
|
+
this.lastCandidateEdge = candidateEdge
|
|
105
|
+
|
|
106
|
+
// Find all edges that are nearby
|
|
107
|
+
const nearbyEdges = this.edgeSpatialIndex.search(
|
|
108
|
+
candidateEdge.start.x - EPS,
|
|
109
|
+
candidateEdge.start.y - EPS,
|
|
110
|
+
candidateEdge.end.x + EPS,
|
|
111
|
+
candidateEdge.end.y + EPS,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const overlappingEdges = nearbyEdges
|
|
115
|
+
.map((i) => this.allEdges[i]!)
|
|
116
|
+
.filter((e) => e.z === candidateEdge.z)
|
|
117
|
+
this.lastOverlappingEdges = overlappingEdges
|
|
118
|
+
|
|
119
|
+
const uncoveredSegments = projectToUncoveredSegments(
|
|
120
|
+
candidateEdge,
|
|
121
|
+
overlappingEdges,
|
|
122
|
+
)
|
|
123
|
+
this.lastUncoveredSegments = uncoveredSegments
|
|
124
|
+
this.segmentsWithAdjacentEmptySpace.push(...uncoveredSegments)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
override getOutput(): {
|
|
128
|
+
segmentsWithAdjacentEmptySpace: Array<SegmentWithAdjacentEmptySpace>
|
|
129
|
+
} {
|
|
130
|
+
return {
|
|
131
|
+
segmentsWithAdjacentEmptySpace: this.segmentsWithAdjacentEmptySpace,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
override visualize() {
|
|
136
|
+
const graphics: Required<GraphicsObject> = {
|
|
137
|
+
title: "FindSegmentsWithAdjacentEmptySpace",
|
|
138
|
+
coordinateSystem: "cartesian" as const,
|
|
139
|
+
rects: [],
|
|
140
|
+
points: [],
|
|
141
|
+
lines: [],
|
|
142
|
+
circles: [],
|
|
143
|
+
arrows: [],
|
|
144
|
+
texts: [],
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Draw the capacity mesh nodes with gray, faded rects
|
|
148
|
+
for (const node of this.input.meshNodes) {
|
|
149
|
+
graphics.rects.push({
|
|
150
|
+
center: node.center,
|
|
151
|
+
width: node.width,
|
|
152
|
+
height: node.height,
|
|
153
|
+
stroke: "rgba(0, 0, 0, 0.1)",
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const unprocessedEdge of this.unprocessedEdges) {
|
|
158
|
+
graphics.lines.push({
|
|
159
|
+
points: visuallyOffsetLine(
|
|
160
|
+
[unprocessedEdge.start, unprocessedEdge.end],
|
|
161
|
+
unprocessedEdge.facingDirection,
|
|
162
|
+
-0.1,
|
|
163
|
+
),
|
|
164
|
+
strokeColor: "rgba(0, 0, 255, 0.5)",
|
|
165
|
+
strokeDash: "5 5",
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const edge of this.segmentsWithAdjacentEmptySpace) {
|
|
170
|
+
graphics.lines.push({
|
|
171
|
+
points: [edge.start, edge.end],
|
|
172
|
+
strokeColor: "rgba(0,255,0, 0.5)",
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (this.lastCandidateEdge) {
|
|
177
|
+
graphics.lines.push({
|
|
178
|
+
points: [this.lastCandidateEdge.start, this.lastCandidateEdge.end],
|
|
179
|
+
strokeColor: "blue",
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (this.lastOverlappingEdges) {
|
|
184
|
+
for (const edge of this.lastOverlappingEdges) {
|
|
185
|
+
graphics.lines.push({
|
|
186
|
+
points: visuallyOffsetLine(
|
|
187
|
+
[edge.start, edge.end],
|
|
188
|
+
edge.facingDirection,
|
|
189
|
+
0.05,
|
|
190
|
+
),
|
|
191
|
+
strokeColor: "red",
|
|
192
|
+
strokeDash: "2 2",
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (this.lastUncoveredSegments) {
|
|
198
|
+
for (const edge of this.lastUncoveredSegments) {
|
|
199
|
+
graphics.lines.push({
|
|
200
|
+
points: visuallyOffsetLine(
|
|
201
|
+
[edge.start, edge.end],
|
|
202
|
+
edge.facingDirection,
|
|
203
|
+
-0.05,
|
|
204
|
+
),
|
|
205
|
+
strokeColor: "green",
|
|
206
|
+
strokeDash: "2 2",
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return graphics
|
|
212
|
+
}
|
|
213
|
+
}
|