@tscircuit/rectdiff 0.0.10 → 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.
@@ -1,10 +1,15 @@
1
- import { BasePipelineSolver, definePipelineStep } from "@tscircuit/solver-utils"
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
- (instance) => [
27
+ (rectDiffPipeline) => [
22
28
  {
23
- simpleRouteJson: instance.inputProblem.simpleRouteJson,
24
- gridOptions: instance.inputProblem.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
- return this.getSolver<RectDiffSolver>("rectDiffSolver")!.getOutput()
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 visualize(): GraphicsObject {
44
- const solver = this.getSolver<RectDiffSolver>("rectDiffSolver")
45
- if (solver) {
46
- return solver.visualize()
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
- // Show board and obstacles even before solver is initialized
50
- return createBaseVisualization(
87
+ return graphics
88
+ }
89
+
90
+ override finalVisualize(): GraphicsObject {
91
+ const graphics = createBaseVisualization(
51
92
  this.inputProblem.simpleRouteJson,
52
- "RectDiff Pipeline (not started)",
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
+ }